Paul Hinze 2707d09181
Fix prepend/append action hooks firing multiple times
This addresses the surprising behavior that the StoreBoxMetadata hook
was running many times during a machine up, including during failed
operations where a destroy_on_error deleted the machine. This was
resulting in an error that looked like:

> No such file or directory @ rb_sysopen [...] /[...]/box_meta

Plugin action hooks using prepend/append were attaching every time a
Builder was run, including sub-Builders that show up for things like
Call actions.

To fix this, we tell Builders if they are "primary" and only run
prepend/append on those. See inline comments for more explanation.
2022-04-25 12:26:55 -05:00

110 lines
3.6 KiB
Ruby

require 'log4r'
require 'vagrant/action/hook'
require 'vagrant/util/busy'
require 'vagrant/util/experimental'
module Vagrant
module Action
class Runner
@@reported_interrupt = false
# @param globals [Hash] variables for the env to be passed to the action
# @yieldreturn [Hash] lazy-loaded vars merged into the env before action run
def initialize(globals=nil, &block)
@globals = globals || {}
@lazy_globals = block
@logger = Log4r::Logger.new("vagrant::action::runner")
end
# @see PrimaryRunner
# @see Vagrant::Action::Builder#primary
def primary?
false
end
def run(callable_id, options=nil)
callable = callable_id
if !callable.kind_of?(Builder)
if callable_id.kind_of?(Class) || callable_id.respond_to?(:call)
callable = Builder.build(callable_id)
end
end
if !callable || !callable.respond_to?(:call)
raise ArgumentError,
"Argument to run must be a callable object or registered action."
end
if callable.is_a?(Builder)
callable.primary = self.primary?
end
# Create the initial environment with the options given
environment = {}
environment.merge!(@globals)
environment.merge!(@lazy_globals.call) if @lazy_globals
environment.merge!(options || {})
# NOTE: Triggers are initialized later in the Action::Runer because of
# how `@lazy_globals` are evaluated. Rather than trying to guess where
# the `env` is coming from, we can wait until they're merged into a single
# hash above.
env = environment[:env]
machine = environment[:machine]
environment[:triggers] = machine.triggers if machine
if env
ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant")
environment[:triggers] ||= Vagrant::Plugin::V2::Trigger.
new(env, env.vagrantfile.config.trigger, machine, ui)
end
# Run the action chain in a busy block, marking the environment as
# interrupted if a SIGINT occurs, and exiting cleanly once the
# chain has been run.
ui = environment[:ui] if environment.key?(:ui)
int_callback = lambda do
if environment[:interrupted]
if ui
begin
ui.error I18n.t("vagrant.actions.runner.exit_immediately")
rescue ThreadError
# We're being called in a trap-context. Wrap in a thread.
Thread.new {
ui.error I18n.t("vagrant.actions.runner.exit_immediately")
}.join(THREAD_MAX_JOIN_TIMEOUT)
end
end
abort
end
if ui && !@@reported_interrupt
begin
ui.warn I18n.t("vagrant.actions.runner.waiting_cleanup")
rescue ThreadError
# We're being called in a trap-context. Wrap in a thread.
Thread.new {
ui.warn I18n.t("vagrant.actions.runner.waiting_cleanup")
}.join(THREAD_MAX_JOIN_TIMEOUT)
end
end
environment[:interrupted] = true
@@reported_interrupt = true
end
action_name = environment[:action_name]
# We place a process lock around every action that is called
@logger.info("Running action: #{action_name} #{callable_id}")
Util::Busy.busy(int_callback) { callable.call(environment) }
# Return the environment in case there are things in there that
# the caller wants to use.
environment
end
end
end
end