Skip to content

Commit

Permalink
Only one copy of Vagrant can run at any given time. [closes hashicorp…
Browse files Browse the repository at this point in the history
…GH-364]

This is to protect against issues with VirtualBox overwriting
each other.
  • Loading branch information
mitchellh committed Jul 9, 2011
1 parent 79a2ffb commit 874a976
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -25,6 +25,7 @@
- Multiple Chef provisioners no longer overwrite cookbook folders. [GH-407]
- `package` won't delete previously existing file. [GH-408]
- Vagrantfile can be lowercase now. [GH-399]
- Only one copy of Vagrant may be running at any given time. [GH-364]

## 0.7.6 (July 2, 2011)

Expand Down
5 changes: 4 additions & 1 deletion lib/vagrant/action.rb
Expand Up @@ -128,7 +128,10 @@ def run(callable_id, options=nil)
@@reported_interrupt = true
end

Busy.busy(int_callback) { callable.call(action_environment) }
# We place a process lock around every action that is called
env.lock do
Busy.busy(int_callback) { callable.call(action_environment) }
end
end
end
end
37 changes: 36 additions & 1 deletion lib/vagrant/environment.rb
Expand Up @@ -60,7 +60,8 @@ def initialize(opts=nil)
:parent => nil,
:vm => nil,
:cwd => nil,
:vagrantfile_name => nil
:vagrantfile_name => nil,
:lock_path => nil
}.merge(opts || {})

# Set the default working directory to look for the vagrantfile
Expand All @@ -77,6 +78,7 @@ def initialize(opts=nil)
end

@loaded = false
@lock_acquired = false
end

#---------------------------------------------------------------
Expand Down Expand Up @@ -281,6 +283,39 @@ def root_path
@root_path = root_finder.call(cwd)
end

# This returns the path which Vagrant uses to determine the location
# of the file lock. This is specific to each operating system.
def lock_path
@lock_path || tmp_path.join("vagrant.lock")
end

# This locks Vagrant for the duration of the block passed to this
# method. During this time, any other environment which attempts
# to lock which points to the same lock file will fail.
def lock
# This allows multiple locks in the same process to be nested
return yield if @lock_acquired

File.open(lock_path, "w+") do |f|
# The file locking fails only if it returns "false." If it
# succeeds it returns a 0, so we must explicitly check for
# the proper error case.
raise Errors::EnvironmentLockedError if f.flock(File::LOCK_EX | File::LOCK_NB) === false

begin
# Mark that we have a lock
@lock_acquired = true

yield
ensure
# We need to make sure that no matter what this is always
# reset to false so we don't think we have a lock when we
# actually don't.
@lock_acquired = false
end
end
end

#---------------------------------------------------------------
# Config Methods
#---------------------------------------------------------------
Expand Down
5 changes: 5 additions & 0 deletions lib/vagrant/errors.rb
Expand Up @@ -153,6 +153,11 @@ class DownloaderHTTPStatusError < VagrantError
error_key(:status_error, "vagrant.downloaders.http")
end

class EnvironmentLockedError < VagrantError
status_code(52)
error_key(:environment_locked)
end

class ForwardPortAutolistEmpty < VagrantError
status_code(27)
error_key(:auto_empty, "vagrant.actions.vm.forward_ports")
Expand Down
5 changes: 5 additions & 0 deletions templates/locales/en.yml
Expand Up @@ -31,6 +31,11 @@ en:
this command in another directory. If you aren't in a home directory,
then please rename ".vagrant" to something else, or configure Vagrant
to use another filename by modifying `config.vagrant.dotfile_name`.
environment_locked: |-
An instance of Vagrant is already running. Only one instance of Vagrant
may run at any given time to avoid problems with VirtualBox inconsistencies
occurring. Please wait for the other instance of Vagrant to end and then
try again.
interrupted: "Vagrant exited after cleanup due to external interrupt."
multi_vm_required: "A multi-vm environment is required for name specification to this command."
multi_vm_target_required: "`vagrant %{command}` requires a specific VM name to target in a multi-VM environment."
Expand Down
61 changes: 61 additions & 0 deletions test/vagrant/environment_test.rb
@@ -1,5 +1,6 @@
require "test_helper"
require "pathname"
require "tempfile"

class EnvironmentTest < Test::Unit::TestCase
setup do
Expand Down Expand Up @@ -291,6 +292,66 @@ class EnvironmentTest < Test::Unit::TestCase
end
end

context "locking" do
setup do
@instance = @klass.new(:lock_path => Tempfile.new('vagrant-test').path)
end

should "allow nesting locks" do
assert_nothing_raised do
@instance.lock do
@instance.lock do
# Nothing
end
end
end
end

should "raise an exception if an environment already has a lock" do
@another = @klass.new(:lock_path => @instance.lock_path)

# Create first locked thread which should succeed
first = Thread.new do
begin
@instance.lock do
Thread.current[:locked] = true
loop { sleep 1000 }
end
rescue Vagrant::Errors::EnvironmentLockedError
Thread.current[:locked] = false
end
end

# Wait until the first thread has acquired the lock
loop do
break if first[:locked] || !first.alive?
Thread.pass
end

# Verify that the fist got the lock
assert first[:locked]

# Create second locked thread which should fail
second = Thread.new do
begin
@another.lock do
Thread.current[:error] = false
end
rescue Vagrant::Errors::EnvironmentLockedError
Thread.current[:error] = true
end
end

# Wait for the second to end and verify it failed
second.join
assert second[:error]

# Make sure both threads are killed
first.kill
second.kill
end
end

context "accessing the configuration" do
should "load the environment if its not already loaded" do
env = @klass.new(:cwd => vagrantfile)
Expand Down

0 comments on commit 874a976

Please sign in to comment.