vagrant/lib/vagrant-zeus/commands/zeus-file-monitor.rb
module VagrantPlugins::Zeus
module Commands
class FileMonitor < Vagrant.plugin(2, :command)
# these are pointing to ext/ and buid/ in the gem root
if /linux/ =~ RUBY_PLATFORM
FILE_MONITOR_EXECUTABLE = File.join(
File.dirname(__FILE__),
'../../../ext/inotify-wrapper/inotify-wrapper'
)
else
FILE_MONITOR_EXECUTABLE = File.join(
File.dirname(__FILE__),
'../../../build/fsevents-wrapper'
)
end
class FileWatcher
def initialize(machine, env)
@machine = machine
@env = env
end
def run
@zeus_connection = spawn_zeus_connection
@file_monitor = spawn_file_monitor
ui.info("Connected to zeus, watching for changes...")
modified_files_buf = ''
watched_files_buf = ''
ended = false
while !ended
ready = IO.select([@file_monitor, @zeus_connection])
ready[0].each do |fh|
if fh == @file_monitor
begin
modified_files_buf += @file_monitor.readpartial(4096)
rescue EOFError
ui.error("Lost connection to the file monitor process, exiting!")
ended = true
end
modified_files_buf = process_modified_files(modified_files_buf)
elsif fh == @zeus_connection
begin
watched_files_buf += @zeus_connection.readpartial(4096)
rescue EOFError
ui.error("Lost connection to the zeus socket, exiting!")
ended = true
end
watched_files_buf = process_watched_files(watched_files_buf)
end
end
end
ensure
if @file_monitor
begin
Process.kill("KILL", @file_monitor.pid)
Process.waitpid(@file_monitor.pid)
rescue => e
$stderr.puts(e)
end
end
end
private
def spawn_file_monitor
IO.popen(FILE_MONITOR_EXECUTABLE, 'r+')
end
def spawn_zeus_connection
TCPSocket.new('localhost', @machine.config.zeus.file_monitor_port)
end
def process_modified_files(buf)
lines = buf.sub(/.*\z/, '').split(/\n/)
remaining = buf.sub(/\A.*\n/m, '')
lines.each do |line|
file = host_to_guest_path(line)
if file
@zeus_connection.write("#{file}\n")
end
end
remaining
end
def process_watched_files(buf)
lines = buf.sub(/.*\z/, '').split(/\n/)
remaining = buf.sub(/\A.*\n/m, '')
lines.each do |line|
file = guest_to_host_path(line)
if file
@file_monitor.write("#{file}\n")
end
end
remaining
end
def guest_to_host_path(path)
guest_to_host_map.each do |guest, host|
if path.start_with?(guest)
return path.sub(/\A#{guest}/, host)
end
end
# NOTE: we are explicitly not passing through things that don't live
# on a synced folder, because they won't exist in a useful form on
# the other side (and the inotify watcher has a slow poll for files
# that don't exist)
nil
end
def host_to_guest_path(path)
host_to_guest_map.each do |host, guest|
if path.start_with?(host)
return path.sub(/\A#{host}/, guest)
end
end
# NOTE: see above - this is less important because it doesn't trigger
# the slow poll path, but still unnecessary
nil
end
def guest_to_host_map
paths = []
@machine.config.vm.synced_folders.map do |_, options|
if !options[:disabled]
paths.push([
options[:guestpath],
File.absolute_path(options[:hostpath])
])
end
end
paths.sort_by { |guest, host| guest.length }.reverse
end
def host_to_guest_map
paths = []
@machine.config.vm.synced_folders.map do |_, options|
if !options[:disabled]
paths.push([
File.absolute_path(options[:hostpath]),
options[:guestpath]
])
end
end
paths.sort_by { |host, guest| host.length }.reverse
end
def ui
@env.ui
end
end
def execute
with_target_vms(nil, :single_target => true) do |machine|
watcher = FileWatcher.new(machine, @env)
while true
begin
watcher.run
rescue => e
@env.ui.error(e.message)
end
sleep 1
end
end
0
end
end
end
end