vagrant-share 0.0.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/vagrant-share.rb +18 -0
- data/lib/vagrant-share/cap/tinycore.rb +89 -0
- data/lib/vagrant-share/command.rb +9 -0
- data/lib/vagrant-share/command/connect.rb +82 -0
- data/lib/vagrant-share/command/ngrok.rb +14 -0
- data/lib/vagrant-share/command/ngrok/connect.rb +236 -0
- data/lib/vagrant-share/command/ngrok/share.rb +500 -0
- data/lib/vagrant-share/command/share.rb +153 -0
- data/lib/vagrant-share/errors.rb +96 -0
- data/lib/vagrant-share/helper.rb +488 -0
- data/lib/vagrant-share/helper/api.rb +46 -0
- data/lib/vagrant-share/helper/word_list.rb +550 -0
- data/lib/vagrant-share/plugin.rb +49 -0
- data/lib/vagrant-share/version.rb +5 -0
- data/locales/en.yml +297 -0
- data/version.txt +1 -0
- metadata +58 -13
- data/vagrant-share.gemspec +0 -9
@@ -0,0 +1,500 @@
|
|
1
|
+
module VagrantPlugins
|
2
|
+
module Share
|
3
|
+
module Command
|
4
|
+
# Ngrok specific implementation
|
5
|
+
module Ngrok
|
6
|
+
# Ngrok share implementation
|
7
|
+
module Share
|
8
|
+
# Guest port within proxy
|
9
|
+
GUEST_PROXY_PORT = 31338
|
10
|
+
|
11
|
+
# Start the ngrok based share
|
12
|
+
#
|
13
|
+
# @param [Array<String>] argv CLI arguments
|
14
|
+
# @param [Hash] options CLI options
|
15
|
+
def start_share(argv, options)
|
16
|
+
validate_ngrok_installation!
|
17
|
+
|
18
|
+
# Define variables to ensure availability within ensure block
|
19
|
+
# Set this here so they're available in our ensure block
|
20
|
+
vagrant_port = nil
|
21
|
+
share_machine = nil
|
22
|
+
share_api = nil
|
23
|
+
share_api_runner = nil
|
24
|
+
proxy_runner = nil
|
25
|
+
output_runner = nil
|
26
|
+
port_file = nil
|
27
|
+
ui = nil
|
28
|
+
share_info_output = Queue.new
|
29
|
+
configuration = {
|
30
|
+
"tunnels" => {}
|
31
|
+
}
|
32
|
+
|
33
|
+
begin
|
34
|
+
with_target_vms(argv, single_target: true) do |machine|
|
35
|
+
ui = machine.ui
|
36
|
+
machine.ui.output(I18n.t("vagrant_share.detecting"))
|
37
|
+
|
38
|
+
target = validate_target_machine(machine, options) || "127.0.0.1"
|
39
|
+
|
40
|
+
restrict = false
|
41
|
+
ports = []
|
42
|
+
|
43
|
+
machine.ui.detail("Local machine address: #{target}")
|
44
|
+
if target == "127.0.0.1"
|
45
|
+
machine.ui.detail(" \n" + I18n.t(
|
46
|
+
"vagrant_share.local_address_only",
|
47
|
+
provider: machine.provider_name.to_s,
|
48
|
+
) + "\n ")
|
49
|
+
|
50
|
+
# Restrict the ports that can be accessed since we're
|
51
|
+
# on localhost.
|
52
|
+
restrict = true
|
53
|
+
ports = Helper.forwarded_ports(machine).keys
|
54
|
+
end
|
55
|
+
|
56
|
+
if !options[:http_port] && !options[:disable_http]
|
57
|
+
begin
|
58
|
+
@logger.debug("No HTTP port set. Auto-detection will be attempted.")
|
59
|
+
# Always target localhost when using ngrok
|
60
|
+
detect_ports!(options, "127.0.0.1", machine)
|
61
|
+
options[:https_port] = nil if options[:disable_https]
|
62
|
+
rescue Errors::DetectHTTPForwardedPortFailed,
|
63
|
+
Errors::DetectHTTPCommonPortFailed
|
64
|
+
# If SSH isn't enabled, raise the errors. If SSH is enabled,
|
65
|
+
# then we can ignore that HTTP is unavailable.
|
66
|
+
raise if !options[:ssh]
|
67
|
+
|
68
|
+
machine.ui.detail(I18n.t("vagrant_share.ssh_no_http") + "\n ")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if options[:ssh]
|
73
|
+
options[:ssh_username], options[:ssh_port] = configure_ssh_share(machine, "127.0.0.1", options)
|
74
|
+
options[:ssh_password], options[:ssh_privkey] = configure_ssh_connect(machine, configuration, options)
|
75
|
+
end
|
76
|
+
|
77
|
+
machine.ui.detail(
|
78
|
+
"Local HTTP port: #{options[:http_port] || "disabled"}")
|
79
|
+
machine.ui.detail(
|
80
|
+
"Local HTTPS port: #{options[:https_port] || "disabled"}")
|
81
|
+
if options[:ssh]
|
82
|
+
machine.ui.detail("SSH Port: #{options[:ssh_port]}")
|
83
|
+
end
|
84
|
+
if restrict
|
85
|
+
ports.each do |port|
|
86
|
+
machine.ui.detail("Port: #{port}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
machine.ui.output(I18n.t("vagrant_share.creating"))
|
91
|
+
|
92
|
+
if options[:http_port]
|
93
|
+
configuration["tunnels"]["http"] = {
|
94
|
+
"proto" => "http",
|
95
|
+
"bind_tls" => false,
|
96
|
+
"addr" => options[:http_port]
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
if options[:https_port]
|
101
|
+
configuration["tunnels"]["https"] = {
|
102
|
+
"proto" => "tls",
|
103
|
+
"addr" => options[:https_port]
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
if options[:full_share] || (options[:ssh] && !options[:ssh_no_password])
|
108
|
+
configuration["tunnels"].delete("ssh")
|
109
|
+
|
110
|
+
if options[:full_share]
|
111
|
+
options[:shared_ports] = ports
|
112
|
+
else
|
113
|
+
options[:shared_ports] = [options[:ssh_port]]
|
114
|
+
end
|
115
|
+
|
116
|
+
@logger.debug("Starting local Vagrant API")
|
117
|
+
|
118
|
+
share_api = setup_share_api(machine)
|
119
|
+
|
120
|
+
options[:vagrant_api_port] = share_api.listeners.first.addr[1]
|
121
|
+
proxy_port, port_file = Helper.acquire_port(@env)
|
122
|
+
|
123
|
+
@logger.debug("Local Vagrant API is listening on port `#{vagrant_port}`")
|
124
|
+
@logger.debug("Local port for proxy forwarding: `#{proxy_port}`")
|
125
|
+
configuration["tunnels"]["proxy"] = {
|
126
|
+
"proto" => "tcp",
|
127
|
+
"addr" => proxy_port
|
128
|
+
}
|
129
|
+
share_machine = Helper.share_machine(@env, port: {guest: GUEST_PROXY_PORT, host: proxy_port}, name: "share")
|
130
|
+
|
131
|
+
ui = share_machine.ui
|
132
|
+
proxy_ui = share_machine.ui.dup
|
133
|
+
proxy_ui.opts[:bold] = false
|
134
|
+
proxy_ui.opts[:prefix_spaces] = true
|
135
|
+
port_forwards = target ? options[:shared_ports] : []
|
136
|
+
port_forwards << options[:vagrant_api_port]
|
137
|
+
|
138
|
+
@logger.debug("Starting share proxy VM")
|
139
|
+
share_machine.with_ui(proxy_ui) do
|
140
|
+
share_machine.action(:up)
|
141
|
+
share_machine.guest.capability(:share_proxy,
|
142
|
+
proxy: GUEST_PROXY_PORT,
|
143
|
+
forwards: port_forwards,
|
144
|
+
target: target
|
145
|
+
)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
if share_api
|
151
|
+
@logger.debug("Starting internal Vagrant API")
|
152
|
+
share_api_runner = Thread.new{ share_api.start }
|
153
|
+
end
|
154
|
+
output_runner = start_connect_info_watcher(share_info_output, ui, options)
|
155
|
+
ngrok_process = start_ngrok_proxy(ui, configuration, share_info_output, options)
|
156
|
+
|
157
|
+
# Allow user to halt the share process and
|
158
|
+
# proxy VM via ctrl-c
|
159
|
+
Helper.signal_retrap("INT") do
|
160
|
+
ui.warn("Halting Vagrant share!")
|
161
|
+
ngrok_process.stop
|
162
|
+
share_api.stop if share_api
|
163
|
+
share_info_output.push(nil)
|
164
|
+
end
|
165
|
+
ensure
|
166
|
+
if port_file
|
167
|
+
port_file.close
|
168
|
+
File.delete(port_file) rescue nil
|
169
|
+
end
|
170
|
+
output_runner.join if output_runner
|
171
|
+
share_api_runner.join if share_api_runner
|
172
|
+
if share_machine
|
173
|
+
share_machine.action(:destroy, force_confirm_destroy: true)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Start the ngrok proxy process
|
179
|
+
#
|
180
|
+
# @param [Vagrant::UI] ui UI instance for output
|
181
|
+
# @param [Hash] configurations ngrok process configuration
|
182
|
+
# @param [Queue] share_info_output location to push share information
|
183
|
+
# @param [Hash] options CLI options
|
184
|
+
def start_ngrok_proxy(ui, configuration, share_info_output, options)
|
185
|
+
ngrok_process = nil
|
186
|
+
base_config = File.expand_path("~/.ngrok2/ngrok.yml")
|
187
|
+
share_config = Tempfile.new("vagrant-share")
|
188
|
+
share_config.write(configuration.to_yaml)
|
189
|
+
share_config.close
|
190
|
+
if !File.exists?(base_config)
|
191
|
+
base_config = share_config.path
|
192
|
+
end
|
193
|
+
@logger.debug("Generated configuration for ngrok:\n#{configuration.to_yaml}")
|
194
|
+
@logger.debug("Starting ngrok proxy process.")
|
195
|
+
|
196
|
+
ngrok_process = Vagrant::Util::Subprocess.new(
|
197
|
+
*["ngrok", "start", "--config", base_config, "--config", share_config.path,
|
198
|
+
"--all", "--log", "stdout", "--log-format", "json", "--log-level", "debug"],
|
199
|
+
notify: [:stdout]
|
200
|
+
)
|
201
|
+
|
202
|
+
Thread.new do
|
203
|
+
begin
|
204
|
+
share_info = {}
|
205
|
+
share_info_keys = []
|
206
|
+
share_info_keys.push(:http) if options[:http_port]
|
207
|
+
share_info_keys.push(:https) if options[:https_port]
|
208
|
+
share_info_keys.push(:name) if options[:ssh] || options[:full_share]
|
209
|
+
|
210
|
+
ngrok_process.execute do |type, data|
|
211
|
+
if type == :stdout
|
212
|
+
data.split("\n").each do |line|
|
213
|
+
begin
|
214
|
+
info = JSON.parse(line)
|
215
|
+
if info["msg"].to_s == "decoded response"
|
216
|
+
begin
|
217
|
+
r_info = info["resp"]
|
218
|
+
if !r_info["Error"].to_s.empty?
|
219
|
+
@logger.error("Error encountered with ngrok connection: #{r_info["Error"]}")
|
220
|
+
share_info_output.push(r_info["Error"])
|
221
|
+
Process.kill("INT", Process.pid)
|
222
|
+
end
|
223
|
+
|
224
|
+
if r_info["URL"] && r_info["Proto"]
|
225
|
+
share_info[:uri] = URI.parse(r_info["URL"])
|
226
|
+
case share_info[:uri].scheme
|
227
|
+
when "http"
|
228
|
+
share_info[:http] = share_info[:uri].to_s
|
229
|
+
when "https"
|
230
|
+
share_info[:https] = share_info[:uri].to_s
|
231
|
+
when "tcp"
|
232
|
+
connect_name = [share_info[:uri].port, options[:vagrant_api_port]].map do |item|
|
233
|
+
Helper.wordify(item).join('_')
|
234
|
+
end
|
235
|
+
share_info[:tcp] = share_info[:uri].to_s
|
236
|
+
share_info[:name] = connect_name.join(":")
|
237
|
+
if share_info[:uri].host != DEFAULT_NGROK_TCP_ENDPOINT
|
238
|
+
host_num = share_info[:uri].host.split(".").first
|
239
|
+
host_num_word = Helper.wordify(host_num.to_i).join("_")
|
240
|
+
share_info[:name] += "@#{host_num_word}"
|
241
|
+
end
|
242
|
+
else
|
243
|
+
@logger.warn("Unhandled URI scheme detected: #{share_info[:uri].scheme} - `#{share_info[:uri]}`")
|
244
|
+
share_info.delete(:uri)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
rescue => err
|
248
|
+
@logger.warn("Failed to parse line: #{err}")
|
249
|
+
end
|
250
|
+
end
|
251
|
+
if info["err"] && info["msg"] == "start tunnel listen" && info["err"] != "<nil>"
|
252
|
+
@logger.error("Error encountered with ngrok connection: #{info["err"]}")
|
253
|
+
share_info_output.push(info["err"])
|
254
|
+
# Force shutdown
|
255
|
+
Process.kill("INT", Process.pid)
|
256
|
+
end
|
257
|
+
if share_info_keys.all?{|key| share_info.keys.include?(key)}
|
258
|
+
share_info_output.push(share_info.dup)
|
259
|
+
share_info = {}
|
260
|
+
end
|
261
|
+
rescue => e
|
262
|
+
@logger.warn("Failure handling ngrok process output line: #{e.class} - #{e} (`#{line}`)")
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
ensure
|
268
|
+
share_config.unlink
|
269
|
+
end
|
270
|
+
end
|
271
|
+
ngrok_process
|
272
|
+
end
|
273
|
+
|
274
|
+
# Start the share information watcher to print connect instructions
|
275
|
+
#
|
276
|
+
# @param [Queue] share_info_output Queue to receive share information
|
277
|
+
# @param [Vagrant::UI] ui UI for output
|
278
|
+
# @param [Hash] options CLI options
|
279
|
+
def start_connect_info_watcher(share_info_output, ui, options)
|
280
|
+
Thread.new do
|
281
|
+
until((info = share_info_output.pop).nil?)
|
282
|
+
begin
|
283
|
+
case info
|
284
|
+
when String
|
285
|
+
ui.error(info)
|
286
|
+
when Hash
|
287
|
+
if info[:name]
|
288
|
+
i_uri = URI.parse(info[:tcp])
|
289
|
+
if i_uri.host != DEFAULT_NGROK_TCP_ENDPOINT
|
290
|
+
driver_name = i_uri.host
|
291
|
+
else
|
292
|
+
driver_name = "ngrok"
|
293
|
+
end
|
294
|
+
ui.success("")
|
295
|
+
ui.success(I18n.t("vagrant_share.started", name: info[:name]))
|
296
|
+
if options[:full_share]
|
297
|
+
ui.success("")
|
298
|
+
ui.success(I18n.t("vagrant_share.ngrok.started_full", name: info[:name], driver: driver_name))
|
299
|
+
end
|
300
|
+
if options[:ssh]
|
301
|
+
ui.success("")
|
302
|
+
ui.success(I18n.t("vagrant_share.ngrok.started_ssh", name: info[:name], driver: driver_name))
|
303
|
+
end
|
304
|
+
ui.success("")
|
305
|
+
end
|
306
|
+
if info[:http]
|
307
|
+
ui.success("HTTP URL: #{info[:http]}")
|
308
|
+
ui.success("")
|
309
|
+
end
|
310
|
+
if info[:https]
|
311
|
+
ui.success("HTTPS URL: #{info[:https]}")
|
312
|
+
ui.success("")
|
313
|
+
end
|
314
|
+
else
|
315
|
+
@logger.warn("Unknown data type receied for output: #{e.class} - #{e}")
|
316
|
+
end
|
317
|
+
rescue => e
|
318
|
+
@logger.error("Unexpected error processing connect information: #{e.class} - #{e}")
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# Validate the target machine for the share
|
325
|
+
#
|
326
|
+
# @param [Vagrant::Machine] machine
|
327
|
+
# @param [Hash] options
|
328
|
+
# @return [String, NilClass] public address
|
329
|
+
def validate_target_machine(machine, options)
|
330
|
+
if !machine.ssh_info
|
331
|
+
# We use this as a flag of whether or not the machine is
|
332
|
+
# running. We can't share a machine that is not running.
|
333
|
+
raise Errors::MachineNotReady
|
334
|
+
end
|
335
|
+
|
336
|
+
if options[:ssh]
|
337
|
+
# Do some quick checks to make sure we can setup this
|
338
|
+
# machine for SSH access from other users.
|
339
|
+
begin
|
340
|
+
if !machine.guest.capability?(:insert_public_key)
|
341
|
+
raise Errors::SSHCantInsertKey,
|
342
|
+
guest: machine.guest.name.to_s
|
343
|
+
end
|
344
|
+
rescue Vagrant::Errors::MachineGuestNotReady
|
345
|
+
raise Errors::SSHNotReady
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
target = nil
|
350
|
+
if !machine.provider.capability?(:public_address)
|
351
|
+
machine.ui.warn(I18n.t(
|
352
|
+
"vagrant_share.provider_unsupported",
|
353
|
+
provider: machine.provider_name.to_s,
|
354
|
+
))
|
355
|
+
else
|
356
|
+
target = machine.provider.capability(:public_address)
|
357
|
+
end
|
358
|
+
target
|
359
|
+
end
|
360
|
+
|
361
|
+
# Configure settings for SSH sharing
|
362
|
+
#
|
363
|
+
# @param [Vagrant::Machine] machine
|
364
|
+
# @param [Hash] options
|
365
|
+
# @return [Array<String>] ssh username, ssh port
|
366
|
+
def configure_ssh_share(machine, target, options)
|
367
|
+
ssh_username = nil
|
368
|
+
ssh_port = options[:ssh_port] if options[:ssh] && options[:ssh_port]
|
369
|
+
if options[:ssh]
|
370
|
+
ssh_info = machine.ssh_info
|
371
|
+
raise Errors::SSHNotReady if !ssh_info
|
372
|
+
|
373
|
+
ssh_username = ssh_info[:username]
|
374
|
+
if !ssh_port
|
375
|
+
ssh_port = ssh_info[:port]
|
376
|
+
if ssh_info[:host] == "127.0.0.1" && target != "127.0.0.1"
|
377
|
+
# Since we're targetting ourselves, the port probably
|
378
|
+
# points to a forwarded port. Look it up.
|
379
|
+
ssh_port = Helper.guest_forwarded_port(machine, ssh_port)
|
380
|
+
|
381
|
+
if !ssh_port
|
382
|
+
raise Errors::SSHPortNotDetected
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
if target == "127.0.0.1" && ssh_info[:host] != "127.0.0.1"
|
387
|
+
# The opposite case now. We're proxying to localhost, but
|
388
|
+
# the SSH port is NOT on localhost. We need to look for
|
389
|
+
# a host forwarded port.
|
390
|
+
guest_port = ssh_port
|
391
|
+
ssh_port = Helper.host_forwarded_port(machine, guest_port)
|
392
|
+
|
393
|
+
if !ssh_port
|
394
|
+
raise Errors::SSHHostPortNotDetected,
|
395
|
+
guest_port: guest_port.to_s
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
[ssh_username, ssh_port]
|
401
|
+
end
|
402
|
+
|
403
|
+
# Configure settings for SSH connect
|
404
|
+
#
|
405
|
+
# @param [Vagrant::Machine] machine Machine to share
|
406
|
+
# @param [Hash] configuration ngrok configuration hash
|
407
|
+
# @param [Hash] options CLI options
|
408
|
+
# @param [Array<String>] ssh password, ssh privkey
|
409
|
+
def configure_ssh_connect(machine, configuration, options)
|
410
|
+
ssh_password = nil
|
411
|
+
ssh_privkey = nil
|
412
|
+
machine.ui.output(I18n.t("vagrant_share.generating_ssh_key"))
|
413
|
+
|
414
|
+
if !options[:ssh_no_password]
|
415
|
+
while !ssh_password
|
416
|
+
ssh_password = machine.ui.ask(
|
417
|
+
"#{I18n.t("vagrant_share.ssh_password_prompt")} ",
|
418
|
+
echo: false)
|
419
|
+
end
|
420
|
+
|
421
|
+
while ssh_password.length < 4
|
422
|
+
machine.ui.warn(
|
423
|
+
"#{I18n.t("vagrant_share.password_not_long_enough")}")
|
424
|
+
ssh_password = machine.ui.ask(
|
425
|
+
"#{I18n.t("vagrant_share.ssh_password_prompt")} ",
|
426
|
+
echo: false)
|
427
|
+
end
|
428
|
+
|
429
|
+
confirm_password = nil
|
430
|
+
while confirm_password != ssh_password
|
431
|
+
confirm_password = machine.ui.ask(
|
432
|
+
"#{I18n.t("vagrant_share.ssh_password_confirm_prompt")} ",
|
433
|
+
echo: false)
|
434
|
+
end
|
435
|
+
else
|
436
|
+
configuration["tunnels"]["ssh"] = {
|
437
|
+
"proto" => "tcp",
|
438
|
+
"addr" => ssh_port
|
439
|
+
}
|
440
|
+
end
|
441
|
+
|
442
|
+
_, ssh_privkey, openssh_key = Helper.generate_keypair(ssh_password)
|
443
|
+
|
444
|
+
machine.ui.detail(I18n.t("vagrant_share.inserting_ssh_key"))
|
445
|
+
machine.guest.capability(:insert_public_key, openssh_key)
|
446
|
+
[ssh_password, ssh_privkey]
|
447
|
+
end
|
448
|
+
|
449
|
+
# Setup the local share API
|
450
|
+
#
|
451
|
+
# @param [Vagrant::Machine] machine
|
452
|
+
# @return [WEBrick::HTTPServer]
|
453
|
+
def setup_share_api(machine)
|
454
|
+
share_api = Helper::Api.start_api(machine) do |api|
|
455
|
+
api.mount_proc("/ping") do |req, res|
|
456
|
+
res.status = 200
|
457
|
+
res.body = {message: "pong"}.to_json
|
458
|
+
end
|
459
|
+
api.mount_proc("/share-info") do |req, res|
|
460
|
+
res.status = 200
|
461
|
+
res.body = {
|
462
|
+
ports: options[:shared_ports],
|
463
|
+
has_private_key: !!options[:ssh_privkey],
|
464
|
+
private_key_password: !options[:ssh_no_password],
|
465
|
+
ssh_username: options[:ssh_username],
|
466
|
+
ssh_port: options[:ssh_port]
|
467
|
+
}.to_json
|
468
|
+
end
|
469
|
+
api.mount_proc("/shared-ports") do |req, res|
|
470
|
+
res.body = {ports: options[:shared_ports]}.to_json
|
471
|
+
res.status = 200
|
472
|
+
end
|
473
|
+
api.mount_proc("/connect-ssh") do |req, res|
|
474
|
+
res.body = {
|
475
|
+
ssh_username: options[:ssh_username],
|
476
|
+
ssh_port: options[:ssh_port],
|
477
|
+
ssh_key: options[:ssh_privkey],
|
478
|
+
has_private_key: !!options[:ssh_privkey],
|
479
|
+
private_key_password: !options[:ssh_no_password]
|
480
|
+
}.to_json
|
481
|
+
res.status = 200
|
482
|
+
end
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
# Check that ngrok is available on user's PATH
|
487
|
+
def validate_ngrok_installation!
|
488
|
+
begin
|
489
|
+
Vagrant::Util::Subprocess.new("ngrok")
|
490
|
+
rescue Vagrant::Errors::CommandUnavailable
|
491
|
+
raise Errors::NgrokUnavailable
|
492
|
+
end
|
493
|
+
end
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
Thread.abort_on_exception = true
|