Brian Cain 1b0148bc78
Fixes #10895: Use relative paths to machines folder path for Listener
Prior to this commit, the rsync helper expanded all exclude paths that
should be ignored to be full qualified and regexp escaped. However the
Listen gem expects these ignore paths to be relative to the path passed
into the listener, not a full path. This commit fixes that by using the
path given by the user for the `rsync__exclude` option
2019-06-11 14:58:42 -07:00

242 lines
8.7 KiB
Ruby

require "log4r"
require 'optparse'
require "thread"
require "vagrant/action/builtin/mixin_synced_folders"
require "vagrant/util/busy"
require "vagrant/util/platform"
require_relative "../helper"
require "listen"
module VagrantPlugins
module SyncedFolderRSync
module Command
class RsyncAuto < Vagrant.plugin("2", :command)
include Vagrant::Action::Builtin::MixinSyncedFolders
def self.synopsis
"syncs rsync synced folders automatically when files change"
end
def execute
@logger = Log4r::Logger.new("vagrant::commands::rsync-auto")
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant rsync-auto [vm-name]"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("--[no-]poll", "Force polling filesystem (slow)") do |poll|
options[:poll] = poll
end
o.on("--[no-]rsync-chown", "Use rsync to modify ownership") do |chown|
options[:rsync_chown] = chown
end
end
# Parse the options and return if we don't have any target.
argv = parse_options(opts)
return if !argv
# Build up the paths that we need to listen to.
paths = {}
ignores = []
with_target_vms(argv) do |machine|
next if machine.state.id == :not_created
cwd = machine.env.cwd.to_s
if machine.provider.capability?(:proxy_machine)
proxy = machine.provider.capability(:proxy_machine)
if proxy
machine.ui.warn(I18n.t(
"vagrant.rsync_proxy_machine",
name: machine.name.to_s,
provider: machine.provider_name.to_s))
machine = proxy
end
end
cached = synced_folders(machine, cached: true)
fresh = synced_folders(machine)
diff = synced_folders_diff(cached, fresh)
if !diff[:added].empty?
machine.ui.warn(I18n.t("vagrant.rsync_auto_new_folders"))
end
folders = cached[:rsync]
next if !folders || folders.empty?
# NOTE: This check is required with boot2docker since all containers
# share the same virtual machine. This prevents rsync-auto from
# syncing all known containers with rsync to the boot2docker vm
# and only syncs the current working dirs folders.
sync_folders = {}
# Still sync existing synced folders from vagrantfile
config_synced_folders = machine.config.vm.synced_folders.values.map { |x| x[:hostpath] }
config_synced_folders.map! { |x| File.expand_path(x, machine.env.root_path) }
folders.each do |id, folder_opts|
if cwd != folder_opts[:hostpath] &&
!config_synced_folders.include?(folder_opts[:hostpath])
machine.ui.info(I18n.t("vagrant.rsync_auto_remove_folder",
folder: folder_opts[:hostpath]))
else
if options.has_key?(:rsync_chown)
folder_opts = folder_opts.merge(rsync_ownership: options[:rsync_chown])
end
sync_folders[id] = folder_opts
end
end
folders = sync_folders
# Get the SSH info for this machine so we can do an initial
# sync to the VM.
ssh_info = machine.ssh_info
if ssh_info
machine.ui.info(I18n.t("vagrant.rsync_auto_initial"))
folders.each do |id, folder_opts|
RsyncHelper.rsync_single(machine, ssh_info, folder_opts)
end
end
folders.each do |id, folder_opts|
# If we marked this folder to not auto sync, then
# don't do it.
next if folder_opts.key?(:auto) && !folder_opts[:auto]
hostpath = folder_opts[:hostpath]
hostpath = File.expand_path(hostpath, machine.env.root_path)
paths[hostpath] ||= []
paths[hostpath] << {
id: id,
machine: machine,
opts: folder_opts,
}
if folder_opts[:exclude]
Array(folder_opts[:exclude]).each do |pattern|
ignores << RsyncHelper.exclude_to_regexp(pattern.to_s)
end
end
# Always ignore Vagrant
ignores << /.vagrant\//
ignores.uniq!
end
end
# Exit immediately if there is nothing to watch
if paths.empty?
@env.ui.info(I18n.t("vagrant.rsync_auto_no_paths"))
return 1
end
# Output to the user what paths we'll be watching
paths.keys.sort.each do |path|
paths[path].each do |path_opts|
path_opts[:machine].ui.info(I18n.t(
"vagrant.rsync_auto_path",
path: path.to_s,
))
end
end
@logger.info("Listening to paths: #{paths.keys.sort.inspect}")
@logger.info("Ignoring #{ignores.length} paths:")
ignores.each do |ignore|
@logger.info(" -- #{ignore.to_s}")
end
@logger.info("Listening via: #{Listen::Adapter.select.inspect}")
callback = method(:callback).to_proc.curry[paths]
listopts = { ignore: ignores, force_polling: !!options[:poll] }
listener = Listen.to(*paths.keys, listopts, &callback)
# Create the callback that lets us know when we've been interrupted
queue = Queue.new
callback = lambda do
# This needs to execute in another thread because Thread
# synchronization can't happen in a trap context.
Thread.new { queue << true }
end
# Run the listener in a busy block so that we can cleanly
# exit once we receive an interrupt.
Vagrant::Util::Busy.busy(callback) do
listener.start
queue.pop
listener.stop if listener.state != :stopped
end
0
end
# This is the callback that is called when any changes happen
def callback(paths, modified, added, removed)
@logger.info("File change callback called!")
@logger.info(" - Modified: #{modified.inspect}")
@logger.info(" - Added: #{added.inspect}")
@logger.info(" - Removed: #{removed.inspect}")
tosync = []
paths.each do |hostpath, folders|
# Find out if this path should be synced
found = catch(:done) do
[modified, added, removed].each do |changed|
changed.each do |listenpath|
throw :done, true if listenpath.start_with?(hostpath)
end
end
# Make sure to return false if all else fails so that we
# don't sync to this machine.
false
end
# If it should be synced, store it for later
tosync << folders if found
end
# Sync all the folders that need to be synced
tosync.each do |folders|
folders.each do |opts|
# Reload so we get the latest ID
opts[:machine].reload
if !opts[:machine].id || opts[:machine].id == ""
# Skip since we can't get SSH info without an ID
next
end
ssh_info = opts[:machine].ssh_info
begin
start = Time.now
RsyncHelper.rsync_single(opts[:machine], ssh_info, opts[:opts])
finish = Time.now
@logger.info("Time spent in rsync: #{finish-start} (in seconds)")
rescue Vagrant::Errors::MachineGuestNotReady
# Error communicating to the machine, probably a reload or
# halt is happening. Just notify the user but don't fail out.
opts[:machine].ui.error(I18n.t(
"vagrant.rsync_communicator_not_ready_callback"))
rescue Vagrant::Errors::RSyncPostCommandError => e
# Error executing rsync chown command
opts[:machine].ui.error(I18n.t(
"vagrant.rsync_auto_post_command_error", message: e.to_s))
rescue Vagrant::Errors::RSyncError => e
# Error executing rsync, so show an error
opts[:machine].ui.error(I18n.t(
"vagrant.rsync_auto_rsync_error", message: e.to_s))
end
end
end
end
end
end
end
end