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
242 lines
8.7 KiB
Ruby
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
|