diff --git a/lib/vagrant/batch_action.rb b/lib/vagrant/batch_action.rb index d27e87d7e..e7bc39318 100644 --- a/lib/vagrant/batch_action.rb +++ b/lib/vagrant/batch_action.rb @@ -71,6 +71,10 @@ module Vagrant thread = Thread.new do Thread.current[:error] = nil + # Note that this thread is being used for running + # a batch action + Thread.current[:batch_parallel_action] = par + # Record our pid when we started in order to figure out if # we've forked... start_pid = Process.pid @@ -160,6 +164,16 @@ module Vagrant if !errors.empty? raise Errors::BatchMultiError, message: errors.join("\n\n") end + + # Check if any threads set an exit code and exit if found. If + # multiple threads have exit code values set, the first encountered + # will be the value used. + threads.each do |thread| + if thread[:exit_code] + @logger.debug("Found exit code set within batch action thread. Exiting") + Process.exit!(thread[:exit_code]) + end + end end end end diff --git a/lib/vagrant/plugin/v2/trigger.rb b/lib/vagrant/plugin/v2/trigger.rb index ca6ea5a34..7ab4d73d8 100644 --- a/lib/vagrant/plugin/v2/trigger.rb +++ b/lib/vagrant/plugin/v2/trigger.rb @@ -300,8 +300,17 @@ module Vagrant # # @param [Integer] code Code to exit Vagrant on def trigger_abort(exit_code) - @ui.warn(I18n.t("vagrant.trigger.abort")) - Process.exit!(exit_code) + if Thread.current[:batch_parallel_action] + @ui.warn(I18n.t("vagrant.trigger.abort_threaded")) + @logger.debug("Trigger abort within parallel batch action. " \ + "Setting exit code and terminating.") + Thread.current[:exit_code] = exit_code + Thread.current.terminate + else + @ui.warn(I18n.t("vagrant.trigger.abort")) + @logger.debug("Trigger abort within non-parallel action, exiting directly") + Process.exit!(exit_code) + end end # Calls the given ruby block for execution diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 5927d6b21..e5543ad57 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -297,6 +297,9 @@ en: Trigger configured to continue on error... abort: |- Vagrant has been configured to abort. Terminating now... + abort_threaded: |- + Vagrant has been configured to abort. Vagrant will terminate + after remaining running actions have completed... start: |- Running %{type} triggers %{stage} %{action} ... fire_with_name: |- diff --git a/test/unit/vagrant/batch_action_test.rb b/test/unit/vagrant/batch_action_test.rb index f62913aab..e1e600303 100644 --- a/test/unit/vagrant/batch_action_test.rb +++ b/test/unit/vagrant/batch_action_test.rb @@ -63,5 +63,24 @@ describe Vagrant::BatchAction do subject.action(machine, "up") subject.run end + + context "with provider supporting parallel actions" do + let(:provider_options) { {parallel: true} } + + it "should flag threads as being parallel actions" do + parallel = nil + subject.custom(machine) { |m| parallel = Thread.current[:batch_parallel_action] } + subject.custom(machine) { |*_| } + subject.run + expect(parallel).to eq(true) + end + + it "should exit the process if exit_code has been set" do + subject.custom(machine) { |m| Thread.current[:exit_code] = 1} + subject.custom(machine) { |*_| } + expect(Process).to receive(:exit!).with(1) + subject.run + end + end end end diff --git a/test/unit/vagrant/plugin/v2/trigger_test.rb b/test/unit/vagrant/plugin/v2/trigger_test.rb index b39f7e30e..370b4b5db 100644 --- a/test/unit/vagrant/plugin/v2/trigger_test.rb +++ b/test/unit/vagrant/plugin/v2/trigger_test.rb @@ -423,6 +423,37 @@ describe Vagrant::Plugin::V2::Trigger do expect(Process).to receive(:exit!).with(3) subject.send(:trigger_abort, 3) end + + context "when running in parallel" do + let(:thread) { + @t ||= Thread.new do + Thread.current[:batch_parallel_action] = true + Thread.stop + subject.send(:trigger_abort, exit_code) + end + } + let(:exit_code) { 22 } + + before do + expect(Process).not_to receive(:exit!) + sleep(0.1) until thread.stop? + end + + after { @t = nil } + + it "should terminate the thread" do + expect(thread).to receive(:terminate).and_call_original + thread.wakeup + thread.join(1) while thread.alive? + end + + it "should set the exit code into the thread data" do + expect(thread).to receive(:terminate).and_call_original + thread.wakeup + thread.join(1) while thread.alive? + expect(thread[:exit_code]).to eq(exit_code) + end + end end context "#ruby" do