vagrant-reflect 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/vagrant-reflect/command/reflect.rb +285 -0
- data/lib/vagrant-reflect/helper.rb +167 -0
- data/lib/vagrant-reflect/patched/subprocess.rb +316 -0
- data/lib/vagrant-reflect/plugin.rb +12 -0
- data/lib/vagrant-reflect/version.rb +3 -0
- data/lib/vagrant-reflect.rb +7 -0
- data/templates/locales/en.yml +37 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 60e8d10dfb9b80d4280d579c126cd27ca7fdc7c0
|
4
|
+
data.tar.gz: 24af14e99f05a3fc076016b46289d6b428bee72c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d6715d8afd918ac9f9d785b7fa4efe8317afabd485f018852685a51a7e08095c59bb4da540bd2fa53fcfceea68c657d63ec647cf6016636a7246c4f37ca088b5
|
7
|
+
data.tar.gz: 89a8795fa57b45c46fd80034a38be6493e40a3eac3446869f9ed457d4ec923e0b8c865f2cbc6fffdf8bf35f6da0b05f8e4338fca3f2883b1f9a0fd4c56e314a3
|
@@ -0,0 +1,285 @@
|
|
1
|
+
require 'log4r'
|
2
|
+
require 'optparse'
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
require 'vagrant/action/builtin/mixin_synced_folders'
|
6
|
+
require 'vagrant/util/busy'
|
7
|
+
require 'vagrant/util/platform'
|
8
|
+
|
9
|
+
require_relative '../helper'
|
10
|
+
|
11
|
+
require 'driskell-listen'
|
12
|
+
|
13
|
+
module VagrantReflect
|
14
|
+
module Command
|
15
|
+
class Reflect < Vagrant.plugin('2', :command)
|
16
|
+
include Vagrant::Action::Builtin::MixinSyncedFolders
|
17
|
+
|
18
|
+
def self.synopsis
|
19
|
+
'a better rsync-auto'
|
20
|
+
end
|
21
|
+
|
22
|
+
def execute
|
23
|
+
@logger = Log4r::Logger.new('vagrant::commands::reflect')
|
24
|
+
|
25
|
+
options = {
|
26
|
+
poll: false,
|
27
|
+
incremental: true
|
28
|
+
}
|
29
|
+
opts = OptionParser.new do |o|
|
30
|
+
o.banner = 'Usage: vagrant reflect [vm-name]'
|
31
|
+
o.separator ''
|
32
|
+
o.separator 'Options:'
|
33
|
+
o.separator ''
|
34
|
+
|
35
|
+
o.on('--[no-]poll', 'Force polling filesystem (slow)') do |poll|
|
36
|
+
options[:poll] = poll
|
37
|
+
end
|
38
|
+
o.on(
|
39
|
+
'--[no-]incremental',
|
40
|
+
'Perform incremental copies of changes where possible (fast)'
|
41
|
+
) do |incremental|
|
42
|
+
options[:incremental] = incremental
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Parse the options and return if we don't have any target.
|
47
|
+
argv = parse_options(opts)
|
48
|
+
return unless argv
|
49
|
+
|
50
|
+
# Build up the paths that we need to listen to.
|
51
|
+
paths = {}
|
52
|
+
with_target_vms(argv) do |machine|
|
53
|
+
if machine.provider.capability?(:proxy_machine)
|
54
|
+
proxy = machine.provider.capability(:proxy_machine)
|
55
|
+
if proxy
|
56
|
+
machine.ui.warn(
|
57
|
+
I18n.t(
|
58
|
+
'vagrant.plugins.vagrant-reflect.rsync_proxy_machine',
|
59
|
+
name: machine.name.to_s,
|
60
|
+
provider: machine.provider_name.to_s))
|
61
|
+
|
62
|
+
machine = proxy
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
cached = synced_folders(machine, cached: true)
|
67
|
+
fresh = synced_folders(machine)
|
68
|
+
diff = synced_folders_diff(cached, fresh)
|
69
|
+
unless diff[:added].empty?
|
70
|
+
machine.ui.warn(
|
71
|
+
I18n.t('vagrant.plugins.vagrant-reflect.rsync_auto_new_folders'))
|
72
|
+
end
|
73
|
+
|
74
|
+
folders = cached[:rsync]
|
75
|
+
next if !folders || folders.empty?
|
76
|
+
|
77
|
+
# Get the SSH info for this machine so we can do an initial
|
78
|
+
# sync to the VM.
|
79
|
+
ssh_info = machine.ssh_info
|
80
|
+
if ssh_info
|
81
|
+
machine.ui.info(
|
82
|
+
I18n.t('vagrant.plugins.vagrant-reflect.rsync_auto_initial'))
|
83
|
+
folders.each do |_, folder_opts|
|
84
|
+
SyncHelper.sync_single(machine, ssh_info, folder_opts)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
folders.each do |id, folder_opts|
|
89
|
+
# If we marked this folder to not auto sync, then
|
90
|
+
# don't do it.
|
91
|
+
next if folder_opts.key?(:auto) && !folder_opts[:auto]
|
92
|
+
|
93
|
+
hostpath = folder_opts[:hostpath]
|
94
|
+
hostpath = File.expand_path(hostpath, machine.env.root_path)
|
95
|
+
paths[hostpath] ||= []
|
96
|
+
paths[hostpath] << {
|
97
|
+
id: id,
|
98
|
+
machine: machine,
|
99
|
+
opts: folder_opts
|
100
|
+
}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Exit immediately if there is nothing to watch
|
105
|
+
if paths.empty?
|
106
|
+
@env.ui.info(
|
107
|
+
I18n.t('vagrant.plugins.vagrant-reflect.rsync_auto_no_paths'))
|
108
|
+
return 1
|
109
|
+
end
|
110
|
+
|
111
|
+
@logger.info(
|
112
|
+
"Listening via: #{Driskell::Listen::Adapter.select.inspect}")
|
113
|
+
|
114
|
+
# Create a listener for each path so the callback can easily
|
115
|
+
# distinguish which path changed
|
116
|
+
listeners = paths.keys.sort.collect do |path|
|
117
|
+
opts = paths[path]
|
118
|
+
callback = method(:callback).to_proc.curry[path][opts][options]
|
119
|
+
|
120
|
+
ignores = []
|
121
|
+
opts.each do |path_opts|
|
122
|
+
path_opts[:machine].ui.info(
|
123
|
+
I18n.t(
|
124
|
+
'vagrant.plugins.vagrant-reflect.rsync_auto_path',
|
125
|
+
path: path.to_s))
|
126
|
+
|
127
|
+
next unless path_opts[:exclude]
|
128
|
+
|
129
|
+
Array(path_opts[:exclude]).each do |pattern|
|
130
|
+
ignores << SyncHelper.exclude_to_regexp(hostpath, pattern.to_s)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
@logger.info("Listening to path: #{path}")
|
135
|
+
@logger.info("Ignoring #{ignores.length} paths:")
|
136
|
+
ignores.each do |ignore|
|
137
|
+
@logger.info("-- #{ignore}")
|
138
|
+
end
|
139
|
+
|
140
|
+
listopts = { ignore: ignores, force_polling: options[:poll] }
|
141
|
+
Driskell::Listen.to(path, listopts, &callback)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Create the callback that lets us know when we've been interrupted
|
145
|
+
queue = Queue.new
|
146
|
+
callback = lambda do
|
147
|
+
# This needs to execute in another thread because Thread
|
148
|
+
# synchronization can't happen in a trap context.
|
149
|
+
Thread.new { queue << true }
|
150
|
+
end
|
151
|
+
|
152
|
+
# Run the listeners in a busy block so that we can cleanly
|
153
|
+
# exit once we receive an interrupt.
|
154
|
+
Vagrant::Util::Busy.busy(callback) do
|
155
|
+
listeners.each(&:start)
|
156
|
+
queue.pop
|
157
|
+
listeners.each do |listener|
|
158
|
+
listener.stop if listener.state != :stopped
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
0
|
163
|
+
end
|
164
|
+
|
165
|
+
# This is the callback that is called when any changes happen
|
166
|
+
def callback(path, opts, options, modified, added, removed)
|
167
|
+
@logger.info("File change callback called for #{path}!")
|
168
|
+
@logger.info(" - Modified: #{modified.inspect}")
|
169
|
+
@logger.info(" - Added: #{added.inspect}")
|
170
|
+
@logger.info(" - Removed: #{removed.inspect}")
|
171
|
+
|
172
|
+
# Perform the sync for each machine
|
173
|
+
opts.each do |path_opts|
|
174
|
+
# Reload so we get the latest ID
|
175
|
+
path_opts[:machine].reload
|
176
|
+
if !path_opts[:machine].id || path_opts[:machine].id == ''
|
177
|
+
# Skip since we can't get SSH info without an ID
|
178
|
+
next
|
179
|
+
end
|
180
|
+
|
181
|
+
begin
|
182
|
+
# If we have any removals or have disabled incremental, perform a
|
183
|
+
# single full sync
|
184
|
+
# It's seemingly impossible to ask rsync to only do a deletion
|
185
|
+
if !options[:incremental] || !removed.empty?
|
186
|
+
removed.each do |remove|
|
187
|
+
path_opts[:machine].ui.info(
|
188
|
+
I18n.t('vagrant.plugins.vagrant-reflect.rsync_auto_remove',
|
189
|
+
path: remove))
|
190
|
+
end
|
191
|
+
|
192
|
+
[modified, added].each do |changes|
|
193
|
+
changes.each do |change|
|
194
|
+
path_opts[:machine].ui.info(
|
195
|
+
I18n.t('vagrant.plugins.vagrant-reflect.rsync_auto_change',
|
196
|
+
path: change))
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
SyncHelper.sync_single(
|
201
|
+
path_opts[:machine],
|
202
|
+
path_opts[:machine].ssh_info,
|
203
|
+
path_opts[:opts]
|
204
|
+
)
|
205
|
+
elsif !modified.empty? || !added.empty?
|
206
|
+
synchronize_changes(path, path_opts, options, [modified, added])
|
207
|
+
end
|
208
|
+
|
209
|
+
path_opts[:machine].ui.info(
|
210
|
+
I18n.t('vagrant.plugins.vagrant-reflect.rsync_auto_synced'))
|
211
|
+
rescue Vagrant::Errors::MachineGuestNotReady
|
212
|
+
# Error communicating to the machine, probably a reload or
|
213
|
+
# halt is happening. Just notify the user but don't fail out.
|
214
|
+
path_opts[:machine].ui.error(
|
215
|
+
I18n.t('vagrant.plugins.vagrant-reflect.'\
|
216
|
+
'rsync_communicator_not_ready_callback'))
|
217
|
+
rescue Vagrant::Errors::VagrantError => e
|
218
|
+
path_opts[:machine].ui.error(e)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Helper to pull the next change from a set of changes of the form
|
224
|
+
# [ set1, set2, set3 ]
|
225
|
+
# Sets are removed as they are emptied
|
226
|
+
def next_change(sets, path)
|
227
|
+
line = sets[0].pop
|
228
|
+
sets.shift while !sets.empty? && sets[0].empty?
|
229
|
+
line[path.length..line.length] + "\n"
|
230
|
+
end
|
231
|
+
|
232
|
+
def synchronize_changes(path, path_opts, options, sets)
|
233
|
+
# Grab the first change
|
234
|
+
sets.shift while sets[0].empty?
|
235
|
+
line = false
|
236
|
+
|
237
|
+
# Pass the list of changes to rsync so we quickly synchronise only
|
238
|
+
# the changed files instead of the whole folder
|
239
|
+
SyncHelper.sync_single(
|
240
|
+
path_opts[:machine],
|
241
|
+
path_opts[:machine].ssh_info,
|
242
|
+
path_opts[:opts].merge(from_stdin: true)
|
243
|
+
) do |what, io|
|
244
|
+
next if what != :stdin
|
245
|
+
|
246
|
+
if line.nil?
|
247
|
+
io.close_write
|
248
|
+
next
|
249
|
+
end
|
250
|
+
|
251
|
+
begin
|
252
|
+
loop do
|
253
|
+
# If we don't have a line, grab one and print it
|
254
|
+
unless line
|
255
|
+
line = next_change(sets, path)
|
256
|
+
path_opts[:machine].ui.info(
|
257
|
+
I18n.t('vagrant.plugins.vagrant-reflect.rsync_auto_increment',
|
258
|
+
path: line))
|
259
|
+
end
|
260
|
+
|
261
|
+
# Handle partial writes
|
262
|
+
n = io.write_nonblock(line)
|
263
|
+
if n < line.length
|
264
|
+
line = line[n..line.length]
|
265
|
+
break
|
266
|
+
end
|
267
|
+
|
268
|
+
# When we've finished giving rsync the file list, set line to nil
|
269
|
+
# and return - on the next notify we will EOT stdin
|
270
|
+
if sets.empty?
|
271
|
+
line = nil
|
272
|
+
break
|
273
|
+
end
|
274
|
+
|
275
|
+
# Request a new line for next write
|
276
|
+
line = false
|
277
|
+
end
|
278
|
+
rescue IO::WaitWritable, Errno::EINTR
|
279
|
+
# Ignore
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'vagrant/util/platform'
|
2
|
+
|
3
|
+
require_relative 'patched/subprocess'
|
4
|
+
|
5
|
+
module VagrantReflect
|
6
|
+
# This is a helper that abstracts out the functionality of rsyncing
|
7
|
+
# folders so that it can be called from anywhere.
|
8
|
+
class SyncHelper
|
9
|
+
# This converts an rsync exclude pattern to a regular expression
|
10
|
+
# we can send to Listen.
|
11
|
+
def self.exclude_to_regexp(path, exclude)
|
12
|
+
start_anchor = false
|
13
|
+
|
14
|
+
if exclude.start_with?('/')
|
15
|
+
start_anchor = true
|
16
|
+
exclude = exclude[1..-1]
|
17
|
+
end
|
18
|
+
|
19
|
+
path = "#{path}/" unless path.end_with?('/')
|
20
|
+
regexp = "^#{Regexp.escape(path)}"
|
21
|
+
regexp += '.*' unless start_anchor
|
22
|
+
|
23
|
+
# This is REALLY ghetto, but its a start. We can improve and
|
24
|
+
# keep unit tests passing in the future.
|
25
|
+
exclude = exclude.gsub('**', '|||GLOBAL|||')
|
26
|
+
exclude = exclude.gsub('*', '|||PATH|||')
|
27
|
+
exclude = exclude.gsub('|||PATH|||', '[^/]*')
|
28
|
+
exclude = exclude.gsub('|||GLOBAL|||', '.*')
|
29
|
+
regexp += exclude
|
30
|
+
|
31
|
+
Regexp.new(regexp)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.sync_single(machine, ssh_info, opts, &block)
|
35
|
+
# Folder info
|
36
|
+
guestpath = opts[:guestpath]
|
37
|
+
hostpath = opts[:hostpath]
|
38
|
+
hostpath = File.expand_path(hostpath, machine.env.root_path)
|
39
|
+
hostpath = Vagrant::Util::Platform.fs_real_path(hostpath).to_s
|
40
|
+
|
41
|
+
if Vagrant::Util::Platform.windows?
|
42
|
+
# rsync for Windows expects cygwin style paths, always.
|
43
|
+
hostpath = Vagrant::Util::Platform.cygwin_path(hostpath)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Make sure the host path ends with a "/" to avoid creating
|
47
|
+
# a nested directory...
|
48
|
+
hostpath += '/' unless hostpath.end_with?('/')
|
49
|
+
|
50
|
+
# Folder options
|
51
|
+
opts[:owner] ||= ssh_info[:username]
|
52
|
+
opts[:group] ||= ssh_info[:username]
|
53
|
+
|
54
|
+
# Connection information
|
55
|
+
username = ssh_info[:username]
|
56
|
+
host = ssh_info[:host]
|
57
|
+
proxy_command = ''
|
58
|
+
if ssh_info[:proxy_command]
|
59
|
+
proxy_command = "-o ProxyCommand='#{ssh_info[:proxy_command]}' "
|
60
|
+
end
|
61
|
+
|
62
|
+
rsh = [
|
63
|
+
"ssh -p #{ssh_info[:port]} " +
|
64
|
+
proxy_command +
|
65
|
+
'-o StrictHostKeyChecking=no '\
|
66
|
+
'-o IdentitiesOnly=true '\
|
67
|
+
'-o UserKnownHostsFile=/dev/null',
|
68
|
+
ssh_info[:private_key_path].map { |p| "-i '#{p}'" }
|
69
|
+
].flatten.join(' ')
|
70
|
+
|
71
|
+
# Exclude some files by default, and any that might be configured
|
72
|
+
# by the user.
|
73
|
+
excludes = ['.vagrant/']
|
74
|
+
excludes += Array(opts[:exclude]).map(&:to_s) if opts[:exclude]
|
75
|
+
excludes.uniq!
|
76
|
+
|
77
|
+
# Get the command-line arguments
|
78
|
+
args = nil
|
79
|
+
args = Array(opts[:args]).dup if opts[:args]
|
80
|
+
args ||= ['--verbose', '--archive', '--delete', '-z', '--copy-links']
|
81
|
+
|
82
|
+
# On Windows, we have to set a default chmod flag to avoid permission
|
83
|
+
# issues
|
84
|
+
if Vagrant::Util::Platform.windows?
|
85
|
+
unless args.any? { |arg| arg.start_with?('--chmod=') }
|
86
|
+
# Ensures that all non-masked bits get enabled
|
87
|
+
args << '--chmod=ugo=rwX'
|
88
|
+
|
89
|
+
# Remove the -p option if --archive is enabled (--archive equals
|
90
|
+
# -rlptgoD) otherwise new files will not have the destination-default
|
91
|
+
# permissions
|
92
|
+
args << '--no-perms' if
|
93
|
+
args.include?('--archive') || args.include?('-a')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Disable rsync's owner/group preservation (implied by --archive) unless
|
98
|
+
# specifically requested, since we adjust owner/group to match shared
|
99
|
+
# folder setting ourselves.
|
100
|
+
args << '--no-owner' unless
|
101
|
+
args.include?('--owner') || args.include?('-o')
|
102
|
+
args << '--no-group' unless
|
103
|
+
args.include?('--group') || args.include?('-g')
|
104
|
+
|
105
|
+
# Tell local rsync how to invoke remote rsync with sudo
|
106
|
+
if machine.guest.capability?(:rsync_command)
|
107
|
+
args << '--rsync-path' << machine.guest.capability(:rsync_command)
|
108
|
+
end
|
109
|
+
|
110
|
+
args << '--files-from=-' if opts[:from_stdin] && block_given?
|
111
|
+
|
112
|
+
# Build up the actual command to execute
|
113
|
+
command = [
|
114
|
+
'rsync',
|
115
|
+
args,
|
116
|
+
'-e', rsh,
|
117
|
+
excludes.map { |e| ['--exclude', e] },
|
118
|
+
hostpath,
|
119
|
+
"#{username}@#{host}:#{guestpath}"
|
120
|
+
].flatten
|
121
|
+
|
122
|
+
# The working directory should be the root path
|
123
|
+
command_opts = {}
|
124
|
+
command_opts[:workdir] = machine.env.root_path.to_s
|
125
|
+
command_opts[:notify] = [:stdin] if opts[:from_stdin] && block_given?
|
126
|
+
|
127
|
+
if opts[:from_stdin] && block_given?
|
128
|
+
machine.ui.info(
|
129
|
+
I18n.t(
|
130
|
+
'vagrant.plugins.vagrant-reflect.rsync_folder_changes',
|
131
|
+
guestpath: guestpath,
|
132
|
+
hostpath: hostpath))
|
133
|
+
else
|
134
|
+
machine.ui.info(
|
135
|
+
I18n.t(
|
136
|
+
'vagrant.plugins.vagrant-reflect.rsync_folder',
|
137
|
+
guestpath: guestpath,
|
138
|
+
hostpath: hostpath))
|
139
|
+
end
|
140
|
+
if excludes.length > 1
|
141
|
+
machine.ui.info(
|
142
|
+
I18n.t(
|
143
|
+
'vagrant.plugins.vagrant-reflect.rsync_folder_excludes',
|
144
|
+
excludes: excludes.inspect))
|
145
|
+
end
|
146
|
+
|
147
|
+
# If we have tasks to do before rsyncing, do those.
|
148
|
+
if machine.guest.capability?(:rsync_pre)
|
149
|
+
machine.guest.capability(:rsync_pre, opts)
|
150
|
+
end
|
151
|
+
|
152
|
+
r = Vagrant::Util::SubprocessPatched.execute(*(command + [command_opts]), &block)
|
153
|
+
if r.exit_code != 0
|
154
|
+
raise Vagrant::Errors::RSyncError,
|
155
|
+
command: command.join(' '),
|
156
|
+
guestpath: guestpath,
|
157
|
+
hostpath: hostpath,
|
158
|
+
stderr: r.stderr
|
159
|
+
end
|
160
|
+
|
161
|
+
# If we have tasks to do after rsyncing, do those.
|
162
|
+
if machine.guest.capability?(:rsync_post)
|
163
|
+
machine.guest.capability(:rsync_post, opts)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
require 'childprocess'
|
4
|
+
require 'log4r'
|
5
|
+
|
6
|
+
require 'vagrant/util/io'
|
7
|
+
require 'vagrant/util/platform'
|
8
|
+
require 'vagrant/util/safe_chdir'
|
9
|
+
require 'vagrant/util/which'
|
10
|
+
|
11
|
+
module Vagrant
|
12
|
+
module Util
|
13
|
+
# Execute a command in a subprocess, gathering the results and
|
14
|
+
# exit status.
|
15
|
+
#
|
16
|
+
# This class also allows you to read the data as it is outputted
|
17
|
+
# from the subprocess in real time, by simply passing a block to
|
18
|
+
# the execute method.
|
19
|
+
class SubprocessPatched
|
20
|
+
# Convenience method for executing a method.
|
21
|
+
def self.execute(*command, &block)
|
22
|
+
new(*command).execute(&block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(*command)
|
26
|
+
@options = command.last.is_a?(Hash) ? command.pop : {}
|
27
|
+
@command = command.dup
|
28
|
+
@command = @command.map { |s| s.encode(Encoding.default_external) }
|
29
|
+
@command[0] = Which.which(@command[0]) if !File.file?(@command[0])
|
30
|
+
if !@command[0]
|
31
|
+
raise Errors::CommandUnavailableWindows, file: command[0] if Platform.windows?
|
32
|
+
raise Errors::CommandUnavailable, file: command[0]
|
33
|
+
end
|
34
|
+
|
35
|
+
@logger = Log4r::Logger.new("vagrant::util::subprocess")
|
36
|
+
end
|
37
|
+
|
38
|
+
def execute
|
39
|
+
# Get the timeout, if we have one
|
40
|
+
timeout = @options[:timeout]
|
41
|
+
|
42
|
+
# Get the working directory
|
43
|
+
workdir = @options[:workdir] || Dir.pwd
|
44
|
+
|
45
|
+
# Get what we're interested in being notified about
|
46
|
+
notify = @options[:notify] || []
|
47
|
+
notify = [notify] if !notify.is_a?(Array)
|
48
|
+
if notify.empty? && block_given?
|
49
|
+
# If a block is given, subscribers must be given, otherwise the
|
50
|
+
# block is never called. This is usually NOT what you want, so this
|
51
|
+
# is an error.
|
52
|
+
message = "A list of notify subscriptions must be given if a block is given"
|
53
|
+
raise ArgumentError, message
|
54
|
+
end
|
55
|
+
|
56
|
+
# Let's get some more useful booleans that we access a lot so
|
57
|
+
# we're not constantly calling an `include` check
|
58
|
+
notify_table = {}
|
59
|
+
notify_table[:stderr] = notify.include?(:stderr)
|
60
|
+
notify_table[:stdout] = notify.include?(:stdout)
|
61
|
+
notify_stdin = notify.include?(:stdin)
|
62
|
+
|
63
|
+
# Build the ChildProcess
|
64
|
+
@logger.info("Starting process: #{@command.inspect}")
|
65
|
+
process = ChildProcess.build(*@command)
|
66
|
+
|
67
|
+
# Create the pipes so we can read the output in real time as
|
68
|
+
# we execute the command.
|
69
|
+
stdout, stdout_writer = ::IO.pipe
|
70
|
+
stderr, stderr_writer = ::IO.pipe
|
71
|
+
process.io.stdout = stdout_writer
|
72
|
+
process.io.stderr = stderr_writer
|
73
|
+
process.duplex = true
|
74
|
+
|
75
|
+
# Special installer-related things
|
76
|
+
if Vagrant.in_installer?
|
77
|
+
installer_dir = ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"].to_s.downcase
|
78
|
+
|
79
|
+
# If we're in an installer on Mac and we're executing a command
|
80
|
+
# in the installer context, then force DYLD_LIBRARY_PATH to look
|
81
|
+
# at our libs first.
|
82
|
+
if Platform.darwin?
|
83
|
+
if @command[0].downcase.include?(installer_dir)
|
84
|
+
@logger.info("Command in the installer. Specifying DYLD_LIBRARY_PATH...")
|
85
|
+
process.environment["DYLD_LIBRARY_PATH"] =
|
86
|
+
"#{installer_dir}/lib:#{ENV["DYLD_LIBRARY_PATH"]}"
|
87
|
+
else
|
88
|
+
@logger.debug("Command not in installer, not touching env vars.")
|
89
|
+
end
|
90
|
+
|
91
|
+
if File.setuid?(@command[0]) || File.setgid?(@command[0])
|
92
|
+
@logger.info("Command is setuid/setgid, clearing DYLD_LIBRARY_PATH")
|
93
|
+
process.environment["DYLD_LIBRARY_PATH"] = ""
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# If the command that is being run is not inside the installer, reset
|
98
|
+
# the original environment - this is required for shelling out to
|
99
|
+
# other subprocesses that depend on environment variables (like Ruby
|
100
|
+
# and $GEM_PATH for example)
|
101
|
+
internal = [installer_dir, Vagrant.user_data_path.to_s.downcase].
|
102
|
+
any? { |path| @command[0].downcase.include?(path) }
|
103
|
+
if !internal
|
104
|
+
@logger.info("Command not in installer, restoring original environment...")
|
105
|
+
jailbreak(process.environment)
|
106
|
+
end
|
107
|
+
else
|
108
|
+
@logger.info("Vagrant not running in installer, restoring original environment...")
|
109
|
+
jailbreak(process.environment)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Set the environment on the process if we must
|
113
|
+
if @options[:env]
|
114
|
+
@options[:env].each do |k, v|
|
115
|
+
process.environment[k] = v
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Start the process
|
120
|
+
begin
|
121
|
+
SafeChdir.safe_chdir(workdir) do
|
122
|
+
process.start
|
123
|
+
end
|
124
|
+
rescue ChildProcess::LaunchError => ex
|
125
|
+
# Raise our own version of the error so that users of the class
|
126
|
+
# don't need to be aware of ChildProcess
|
127
|
+
raise LaunchError.new(ex.message)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Make sure the stdin does not buffer
|
131
|
+
process.io.stdin.sync = true
|
132
|
+
|
133
|
+
if RUBY_PLATFORM != "java"
|
134
|
+
# On Java, we have to close after. See down the method...
|
135
|
+
# Otherwise, we close the writers right here, since we're
|
136
|
+
# not on the writing side.
|
137
|
+
stdout_writer.close
|
138
|
+
stderr_writer.close
|
139
|
+
end
|
140
|
+
|
141
|
+
# Create a dictionary to store all the output we see.
|
142
|
+
io_data = { stdout: "", stderr: "" }
|
143
|
+
|
144
|
+
# Record the start time for timeout purposes
|
145
|
+
start_time = Time.now.to_i
|
146
|
+
|
147
|
+
@logger.debug("Selecting on IO")
|
148
|
+
while true
|
149
|
+
# PATCH BEGINS HERE TO FIX STDIN CLOSURE
|
150
|
+
writers = notify_stdin && !process.io.stdin.closed? ? [process.io.stdin] : []
|
151
|
+
# PATCH END
|
152
|
+
results = ::IO.select([stdout, stderr], writers, nil, 0.1)
|
153
|
+
results ||= []
|
154
|
+
readers = results[0]
|
155
|
+
writers = results[1]
|
156
|
+
|
157
|
+
# Check if we have exceeded our timeout
|
158
|
+
raise TimeoutExceeded, process.pid if timeout && (Time.now.to_i - start_time) > timeout
|
159
|
+
|
160
|
+
# Check the readers to see if they're ready
|
161
|
+
if readers && !readers.empty?
|
162
|
+
readers.each do |r|
|
163
|
+
# Read from the IO object
|
164
|
+
data = IO.read_until_block(r)
|
165
|
+
|
166
|
+
# We don't need to do anything if the data is empty
|
167
|
+
next if data.empty?
|
168
|
+
|
169
|
+
io_name = r == stdout ? :stdout : :stderr
|
170
|
+
@logger.debug("#{io_name}: #{data.chomp}")
|
171
|
+
|
172
|
+
io_data[io_name] += data
|
173
|
+
yield io_name, data if block_given? && notify_table[io_name]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Break out if the process exited. We have to do this before
|
178
|
+
# attempting to write to stdin otherwise we'll get a broken pipe
|
179
|
+
# error.
|
180
|
+
break if process.exited?
|
181
|
+
|
182
|
+
# Check the writers to see if they're ready, and notify any listeners
|
183
|
+
if writers && !writers.empty? && block_given?
|
184
|
+
yield :stdin, process.io.stdin
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Wait for the process to end.
|
189
|
+
begin
|
190
|
+
remaining = (timeout || 32000) - (Time.now.to_i - start_time)
|
191
|
+
remaining = 0 if remaining < 0
|
192
|
+
@logger.debug("Waiting for process to exit. Remaining to timeout: #{remaining}")
|
193
|
+
|
194
|
+
process.poll_for_exit(remaining)
|
195
|
+
rescue ChildProcess::TimeoutError
|
196
|
+
raise TimeoutExceeded, process.pid
|
197
|
+
end
|
198
|
+
|
199
|
+
@logger.debug("Exit status: #{process.exit_code}")
|
200
|
+
|
201
|
+
# Read the final output data, since it is possible we missed a small
|
202
|
+
# amount of text between the time we last read data and when the
|
203
|
+
# process exited.
|
204
|
+
[stdout, stderr].each do |io|
|
205
|
+
# Read the extra data, ignoring if there isn't any
|
206
|
+
extra_data = IO.read_until_block(io)
|
207
|
+
next if extra_data == ""
|
208
|
+
|
209
|
+
# Log it out and accumulate
|
210
|
+
io_name = io == stdout ? :stdout : :stderr
|
211
|
+
io_data[io_name] += extra_data
|
212
|
+
@logger.debug("#{io_name}: #{extra_data.chomp}")
|
213
|
+
|
214
|
+
# Yield to any listeners any remaining data
|
215
|
+
yield io_name, extra_data if block_given? && notify_table[io_name]
|
216
|
+
end
|
217
|
+
|
218
|
+
if RUBY_PLATFORM == "java"
|
219
|
+
# On JRuby, we need to close the writers after the process,
|
220
|
+
# for some reason. See GH-711.
|
221
|
+
stdout_writer.close
|
222
|
+
stderr_writer.close
|
223
|
+
end
|
224
|
+
|
225
|
+
# Return an exit status container
|
226
|
+
return Result.new(process.exit_code, io_data[:stdout], io_data[:stderr])
|
227
|
+
ensure
|
228
|
+
if process && process.alive?
|
229
|
+
# Make sure no matter what happens, the process exits
|
230
|
+
process.stop(2)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
protected
|
235
|
+
|
236
|
+
# An error which raises when a process fails to start
|
237
|
+
class LaunchError < StandardError; end
|
238
|
+
|
239
|
+
# An error which occurs when the process doesn't end within
|
240
|
+
# the given timeout.
|
241
|
+
class TimeoutExceeded < StandardError
|
242
|
+
attr_reader :pid
|
243
|
+
|
244
|
+
def initialize(pid)
|
245
|
+
super()
|
246
|
+
@pid = pid
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Container class to store the results of executing a subprocess.
|
251
|
+
class Result
|
252
|
+
attr_reader :exit_code
|
253
|
+
attr_reader :stdout
|
254
|
+
attr_reader :stderr
|
255
|
+
|
256
|
+
def initialize(exit_code, stdout, stderr)
|
257
|
+
@exit_code = exit_code
|
258
|
+
@stdout = stdout
|
259
|
+
@stderr = stderr
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
private
|
264
|
+
|
265
|
+
# This is, quite possibly, the saddest function in all of Vagrant.
|
266
|
+
#
|
267
|
+
# If a user is running Vagrant via Bundler (but not via the official
|
268
|
+
# installer), we want to reset to the "original" environment so that when
|
269
|
+
# shelling out to other Ruby processes (specifically), the original
|
270
|
+
# environment is restored. This is super important for things like
|
271
|
+
# rbenv and chruby, who rely on environment variables to locate gems, but
|
272
|
+
# Bundler stomps on those environment variables like an angry T-Rex after
|
273
|
+
# watching Jurassic Park 2 and realizing they replaced you with CGI.
|
274
|
+
#
|
275
|
+
# If a user is running in Vagrant via the official installer, BUT trying
|
276
|
+
# to execute a subprocess *outside* of the installer, we want to reset to
|
277
|
+
# the "original" environment. In this case, the Vagrant installer actually
|
278
|
+
# knows what the original environment was and replaces it completely.
|
279
|
+
#
|
280
|
+
# Finally, we reset any Bundler-specific environment variables, since the
|
281
|
+
# subprocess being called could, itself, be Bundler. And Bundler does not
|
282
|
+
# behave very nicely in these circumstances.
|
283
|
+
#
|
284
|
+
# This function was added in Vagrant 1.7.3, but there is a failsafe
|
285
|
+
# because the author doesn't trust himself that this functionality won't
|
286
|
+
# break existing assumptions, so users can specify
|
287
|
+
# `VAGRANT_SKIP_SUBPROCESS_JAILBREAK` and none of the above will happen.
|
288
|
+
#
|
289
|
+
# This function modifies the given hash in place!
|
290
|
+
#
|
291
|
+
# @return [nil]
|
292
|
+
def jailbreak(env = {})
|
293
|
+
return if ENV.key?("VAGRANT_SKIP_SUBPROCESS_JAILBREAK")
|
294
|
+
|
295
|
+
env.replace(::Bundler::ORIGINAL_ENV) if defined?(::Bundler::ORIGINAL_ENV)
|
296
|
+
env.merge!(Vagrant.original_env)
|
297
|
+
|
298
|
+
# Bundler does this, so I guess we should as well, since I think it
|
299
|
+
# other subprocesses that use Bundler will reload it
|
300
|
+
env["MANPATH"] = ENV["BUNDLE_ORIG_MANPATH"]
|
301
|
+
|
302
|
+
# Replace all current environment BUNDLE_ variables to nil
|
303
|
+
ENV.each do |k,_|
|
304
|
+
env[k] = nil if k[0,7] == "BUNDLE_"
|
305
|
+
end
|
306
|
+
|
307
|
+
# If RUBYOPT was set, unset it with Bundler
|
308
|
+
if ENV.key?("RUBYOPT")
|
309
|
+
env["RUBYOPT"] = ENV["RUBYOPT"].sub("-rbundler/setup", "")
|
310
|
+
end
|
311
|
+
|
312
|
+
nil
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
en:
|
2
|
+
vagrant:
|
3
|
+
plugins:
|
4
|
+
vagrant-reflect:
|
5
|
+
rsync_auto_initial: |-
|
6
|
+
Doing an initial rsync...
|
7
|
+
rsync_auto_new_folders: |-
|
8
|
+
New synced folders were added to the Vagrantfile since running
|
9
|
+
`vagrant reload`. If these new synced folders are backed by rsync,
|
10
|
+
they won't be automatically synced until a `vagrant reload` is run.
|
11
|
+
rsync_auto_no_paths: |-
|
12
|
+
There are no paths to watch! This is either because you have no
|
13
|
+
synced folders using rsync, or any rsync synced folders you have
|
14
|
+
have specified `rsync_auto` to be false.
|
15
|
+
rsync_auto_path: |-
|
16
|
+
Watching: %{path}
|
17
|
+
rsync_auto_increment: |-
|
18
|
+
Sending change: %{path}
|
19
|
+
rsync_auto_change: |-
|
20
|
+
Processing change: %{path}
|
21
|
+
rsync_auto_remove: |-
|
22
|
+
Processing removal: %{path}
|
23
|
+
rsync_auto_synced: |-
|
24
|
+
Synchronization completed
|
25
|
+
rsync_communicator_not_ready_callback: |-
|
26
|
+
Failed to connect to remote machine. This is usually caused by the
|
27
|
+
machine rebooting or being halted. Please make sure the machine is
|
28
|
+
running, and modify a file to try again.
|
29
|
+
rsync_folder: |-
|
30
|
+
Rsyncing folder: %{hostpath} => %{guestpath}
|
31
|
+
rsync_folder_changes: |-
|
32
|
+
Rsyncing changes only: %{hostpath} => %{guestpath}
|
33
|
+
rsync_folder_excludes: " - Exclude: %{excludes}"
|
34
|
+
rsync_proxy_machine: |-
|
35
|
+
The provider ('%{provider}') for the machine '%{name}' is
|
36
|
+
using a proxy machine. RSync will sync to this proxy
|
37
|
+
instead of directly to the environment itself.
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vagrant-reflect
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jason Woods
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: driskell-listen
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.0.6.10
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.0.6.10
|
27
|
+
description: Vagrant Reflect
|
28
|
+
email:
|
29
|
+
- devel@jasonwoods.me.uk
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- lib/vagrant-reflect.rb
|
35
|
+
- lib/vagrant-reflect/command/reflect.rb
|
36
|
+
- lib/vagrant-reflect/helper.rb
|
37
|
+
- lib/vagrant-reflect/patched/subprocess.rb
|
38
|
+
- lib/vagrant-reflect/plugin.rb
|
39
|
+
- lib/vagrant-reflect/version.rb
|
40
|
+
- templates/locales/en.yml
|
41
|
+
homepage: https://github.com/driskell/vagrant-reflect
|
42
|
+
licenses:
|
43
|
+
- Apache
|
44
|
+
metadata: {}
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubyforge_project: nowarning
|
61
|
+
rubygems_version: 2.4.2
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: A better vagrant rsync-auto
|
65
|
+
test_files: []
|
66
|
+
has_rdoc:
|