vagrant-reflect 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|