net-ssh-open3 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +34 -0
  3. data/lib/net-ssh-open3.rb +542 -0
  4. metadata +60 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 03ff6821052cebdec70bfe367fa1c3da6211b1ae
4
+ data.tar.gz: 3aff420588c6faa56eabfec790a29acd6ec45566
5
+ SHA512:
6
+ metadata.gz: 0c51a2d3c56cb849ffc46540c70579cc40529223f210b3a268369919412e986a47897c75fdd070ccec2222b6ff9918c6dc098dfc077e8578d0224ecc610135cc
7
+ data.tar.gz: 5b8caa2194cf369a502f1f6f7064c98e3e8f4f6d26e91b2f2c49fc29d6334291d4df4c0aa03c73aa9deba8630f1219cd51b9ec4100b2f7dd9bda6447bbc02388
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ net-ssh-open3
2
+ =============
3
+
4
+ [![Gem Version](https://badge.fury.io/rb/net-ssh-open3.png)](http://badge.fury.io/rb/net-ssh-open3)
5
+
6
+ Thread-safe Open3 for Net::SSH.
7
+
8
+ Adds some Open3-style functions to Net::SSH::Connection::Session.
9
+
10
+ See [ruby 1.9.3 doc](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/open3/rdoc/Open3.html)
11
+ or [ruby 2.0 doc](http://www.ruby-doc.org/stdlib-2.0/libdoc/open3/rdoc/Open3.html).
12
+
13
+ Usage example:
14
+
15
+ irb(main):001:0> require 'net-ssh-open3'
16
+ => true
17
+ irb(main):002:0> session = Net::SSH.start('localhost', 'root'); nil
18
+ => nil
19
+ irb(main):003:0> puts session.capture2e('ls', '/boot') # also: 'ls /boot'
20
+ grub
21
+ initramfs-linux-fallback.img
22
+ initramfs-linux.img
23
+ lost+found
24
+ memtest86+
25
+ vmlinuz-linux
26
+ pid 1594 exit 0
27
+ => nil
28
+ irb(main):004:0> session.popen2e('sh') { |i, oe, w| i.puts('kill $$'); w[:status] }
29
+ => #<Net::SSH::Process::Status: pid 16864 TERM (signal 15) core false>
30
+
31
+ Note: a single SSH session may have several channels, i.e. you may run several Open3 methods on the same session in parallel (in different threads).
32
+
33
+ For more information please see documentation inside.
34
+ Recommended starting point is "Methods included from Net::SSH::Open3" in Net::SSH::Connection::Session.
@@ -0,0 +1,542 @@
1
+ require 'shellwords' # String#shellescape
2
+ require 'thread' # ConditionVariable
3
+ require 'net/ssh' # Monkeypatching
4
+ require 'stringio' # StringIO for capture*
5
+
6
+ class Class
7
+ unless method_defined?(:alias_method_once)
8
+ private
9
+ # Create an alias +new_method+ to +old_method+ unless +new_method+ is already defined.
10
+ def alias_method_once(new_method, old_method) #:nodoc:
11
+ alias_method new_method, old_method unless method_defined?(new_method)
12
+ end
13
+ end
14
+ end
15
+
16
+ module Net::SSH
17
+ module Process
18
+ # Encapsulates the information on the status of remote process, similar to ::Process::Status.
19
+ #
20
+ # Note that it's impossible to retrieve PID (process ID) via an SSH channel (thus impossible to properly signal it).
21
+ #
22
+ # Although RFC4254 allows sending signals to the process (see http://tools.ietf.org/html/rfc4254#section-6.9),
23
+ # current OpenSSH server implementation does not support this feature, though there are some patches:
24
+ # https://bugzilla.mindrot.org/show_bug.cgi?id=1424 and http://marc.info/?l=openssh-unix-dev&m=104300802407848&w=2
25
+ #
26
+ # As a workaround one can request a PTY and send SIGINT or SIGQUIT via ^C, ^\ or other sequences,
27
+ # see 'pty' option in Net::SSH::Open3 for more information.
28
+ #
29
+ # Open3 prepends your command with 'echo $$; ' which will echo PID of your process, then intercepts this line from STDOUT.
30
+ class Status
31
+ # Integer exit code in range 0..255, 0 usually meaning success.
32
+ # Assigned only if the process has exited normally (i.e. not by a signal).
33
+ # More information about standard exit codes: http://tldp.org/LDP/abs/html/exitcodes.html
34
+ attr_reader :exitstatus
35
+
36
+ # Process ID of a remote command interpreter or a remote process.
37
+ # See note on Net::SSH::Process::Status class for more information on how this is fetched.
38
+ attr_reader :pid
39
+
40
+ # true when process has been killed by a signal and a core dump has been generated for it.
41
+ def coredump?
42
+ @coredump
43
+ end
44
+
45
+ # Integer representation of a signal that killed a process, if available.
46
+ #
47
+ # Translated to local system (so you can use Signal.list to map it to String).
48
+ # Explanation: when local system is Linux (USR1=10) and remote is FreeBSD (USR1=30),
49
+ # 10 will be returned in case remote process receives USR1 (30).
50
+ #
51
+ # Not all signal names are delivered by ssh: for example, SIGTRAP is delivered as "SIG@openssh.com"
52
+ # and therefore may not be translated. Returns String in this case.
53
+ def termsig
54
+ Signal.list[@termsig] || @termsig
55
+ end
56
+
57
+ # true if the process has exited normally and returned an exit code.
58
+ def exited?
59
+ !!@exitstatus
60
+ end
61
+
62
+ # true if the process has been killed by a signal.
63
+ def signaled?
64
+ !!@termsig
65
+ end
66
+
67
+ # true if the process is still running (actually if we haven't received it's exit status or signal).
68
+ def active?
69
+ not (exited? or signaled?)
70
+ end
71
+
72
+ # Returns true if the process has exited with code 0, false for other codes and nil if killed by a signal.
73
+ def success?
74
+ exited? ? exitstatus == 0 : nil
75
+ end
76
+
77
+ # String representation of exit status.
78
+ def to_s
79
+ if @pid
80
+ "pid #@pid " <<
81
+ if exited?
82
+ "exit #@exitstatus"
83
+ elsif signaled?
84
+ "#@termsig (signal #{termsig}) core #@coredump"
85
+ else
86
+ 'active'
87
+ end
88
+ else
89
+ 'uninitialized'
90
+ end
91
+ end
92
+
93
+ # Inspect this instance.
94
+ def inspect
95
+ "#<#{self.class}: #{to_s}>"
96
+ end
97
+ end
98
+ end
99
+
100
+ # Net::SSH Open3 extensions.
101
+ # All methods have the same argument list.
102
+ #
103
+ # *optional* +env+: custom environment variables +Hash+. Note that SSH server typically restricts changeable variables to a very small set,
104
+ # e.g. for OpenSSH see +AcceptEnv+ in +/etc/ssh/sshd_config+ (+AcceptEnv+ +LANG+ +LC_*+)
105
+ #
106
+ # +command+: a single shell command (like in +sh+ +-c+), or an executable program.
107
+ #
108
+ # *optional* +arg1+, +arg2+, +...+: arguments to an executable mentioned above.
109
+ #
110
+ # *optional* +options+: options hash, keys:
111
+ # * +redirects+: Hash of redirections which will be appended to a command line (you can't transfer a pipe to a remote system).
112
+ # Key: one of +:in+, +:out+, +:err+ or a +String+, value: +Integer+ to redirect to fd, +String+ to redirect to a file.
113
+ # Example:
114
+ # { '>>' => '/tmp/log', err: 1 }
115
+ # translates to
116
+ # '>>/tmp/log 2>&1'
117
+ # * +channel_retries+: +Integer+ number of retries in case of channel open failure (ssh server usually limits a session to 10 channels),
118
+ # or an array of [+retries+, +delay+]
119
+ # * +stdin_data+: for +capture*+ only, specifies data to be immediately sent to +stdin+ of a remote process.
120
+ # stdin is immediately closed then.
121
+ # * +logger+: an object which responds to +debug/info/warn/error+ and optionally +init/stdin/stdout/stderr+ to log debug information
122
+ # and data exchange stream
123
+ # * +pty+: true or a +Hash+ of PTY settings to request a pseudo-TTY, see Net::SSH documentation for more information.
124
+ # A note about sending TERM/QUIT: use modes, e.g.:
125
+ # Net::SSH.start('localhost', ENV['USER']).capture2e('cat', pty: {
126
+ # modes: {
127
+ # Net::SSH::Connection::Term::VINTR => 0x01020304, # INT on this 4-byte-sequence
128
+ # Net::SSH::Connection::Term::VQUIT => 0xdeadbeef, # QUIT on this 4-byte sequence
129
+ # Net::SSH::Connection::Term::VEOF => 0xfacefeed, # EOF sequence
130
+ # Net::SSH::Connection::Term::ECHO => 0, # disable echoing
131
+ # Net::SSH::Connection::Term::ISIG => 1 # enable sending signals
132
+ # }
133
+ # },
134
+ # stdin_data: [0xDEADBEEF].pack('L'),
135
+ # logger: Class.new { alias method_missing puts; def respond_to?(_); true end }.new)
136
+ # # log skipped ...
137
+ # # => ["", #<Net::SSH::Process::Status: pid 1744 QUIT (signal 3) core true>]
138
+ # Note that just closing stdin is not enough for PTY. You should explicitly send VEOF as a first char of a line, see termios(3).
139
+ module Open3
140
+ SSH_EXTENDED_DATA_STDERR = 1 #:nodoc:
141
+ REMOTE_PACKET_THRESHOLD = 512 # headers etc #:nodoc:
142
+
143
+ private_constant :SSH_EXTENDED_DATA_STDERR, :REMOTE_PACKET_THRESHOLD
144
+
145
+ # Captures stdout only. Returns [String, Net::SSH::Process::Status]
146
+ def capture2(*args)
147
+ stdout = StringIO.new
148
+ stdin_data = args.last[:stdin_data] if Hash === args.last
149
+
150
+ run_popen(*args,
151
+ stdin: stdin_data,
152
+ stdout: stdout,
153
+ block_pipes: [stdout]) do |stdout, waiter_thread|
154
+ [stdout.string, waiter_thread.value]
155
+ end
156
+ end
157
+
158
+ # Captures stdout and stderr into one string. Returns [String, Net::SSH::Process::Status]
159
+ def capture2e(*args)
160
+ stdout = StringIO.new
161
+ stdin_data = args.last[:stdin_data] if Hash === args.last
162
+
163
+ run_popen(*args,
164
+ stdin: stdin_data,
165
+ stdout: stdout,
166
+ stderr: stdout,
167
+ block_pipes: [stdout]) do |stdout, waiter_thread|
168
+ [stdout.string, waiter_thread.value]
169
+ end
170
+ end
171
+
172
+ # Captures stdout and stderr into separate strings. Returns [String, String, Net::SSH::Process::Status]
173
+ def capture3(*args)
174
+ stdout, stderr = StringIO.new, StringIO.new
175
+ stdin_data = args.last[:stdin_data] if Hash === args.last
176
+
177
+ run_popen(*args,
178
+ stdin: stdin_data,
179
+ stdout: stdout,
180
+ stderr: stderr,
181
+ block_pipes: [stdout, stderr]) do |stdout, stderr, waiter_thread|
182
+ [stdout.string, stderr.string, waiter_thread.value]
183
+ end
184
+ end
185
+
186
+ # Opens pipes to a remote process.
187
+ # Yields +stdin+, +stdout+, +stderr+, +waiter_thread+ into a block. Will wait for a process to finish.
188
+ # Joining (or getting a value of) +waither_thread+ inside a block will wait for a process right there.
189
+ # 'status' Thread-Attribute of +waiter_thread+ holds an instance of Net::SSH::Process::Status for a remote process.
190
+ # Careful: don't forget to read +stderr+, otherwise if your process generates too much stderr output
191
+ # the pipe may overload and ssh loop will get stuck writing to it.
192
+ def popen3(*args, &block)
193
+ stdin_inner, stdin_outer = IO.pipe
194
+ stdout_outer, stdout_inner = IO.pipe
195
+ stderr_outer, stderr_inner = IO.pipe
196
+
197
+ run_popen(*args,
198
+ stdin: stdin_inner,
199
+ stdout: stdout_inner,
200
+ stderr: stderr_inner,
201
+ block_pipes: [stdin_outer, stdout_outer, stderr_outer],
202
+ &block)
203
+ end
204
+
205
+ # Yields +stdin+, +stdout-stderr+, +waiter_thread+ into a block.
206
+ def popen2e(*args, &block)
207
+ stdin_inner, stdin_outer = IO.pipe
208
+ stdout_outer, stdout_inner = IO.pipe
209
+
210
+ run_popen(*args,
211
+ stdin: stdin_inner,
212
+ stdout: stdout_inner,
213
+ stderr: stdout_inner,
214
+ block_pipes: [stdin_outer, stdout_outer],
215
+ &block)
216
+ end
217
+
218
+ # Yields +stdin+, +stdout+, +waiter_thread+ into a block.
219
+ def popen2(*args, &block)
220
+ stdin_inner, stdin_outer = IO.pipe
221
+ stdout_outer, stdout_inner = IO.pipe
222
+
223
+ run_popen(*args,
224
+ stdin: stdin_inner,
225
+ stdout: stdout_inner,
226
+ block_pipes: [stdin_outer, stdout_outer],
227
+ &block)
228
+ end
229
+
230
+ private
231
+ def install_channel_callbacks(channel, options)
232
+ logger, stdin, stdout, stderr =
233
+ options[:logger], options[:stdin], options[:stdout], options[:stderr]
234
+ pid_initialized = false
235
+
236
+ channel.on_open_failed do |_channel, code, desc|
237
+ message = "cannot open channel (error code #{code}): #{desc}"
238
+ logger.error(message) if logger
239
+ raise message
240
+ end
241
+
242
+ channel.on_data do |_channel, data|
243
+ unless pid_initialized
244
+ # First arrived line contains PID (see run_popen).
245
+ pid_initialized = true
246
+ pid, data = data.split(nil, 2)
247
+ channel.open3_waiter_thread[:status].instance_variable_set(:@pid, pid.to_i)
248
+ channel.open3_signal_open
249
+ next if data.empty?
250
+ end
251
+ logger.stdout(data) if logger.respond_to?(:stdout)
252
+ if stdout
253
+ stdout.write(data)
254
+ stdout.flush
255
+ end
256
+ end
257
+
258
+ channel.on_extended_data do |_channel, type, data|
259
+ if type == SSH_EXTENDED_DATA_STDERR
260
+ logger.stderr(data) if logger.respond_to?(:stderr)
261
+ if stderr
262
+ stderr.write(data)
263
+ stderr.flush
264
+ end
265
+ else
266
+ logger.warn("unknown extended data type #{type}") if logger
267
+ end
268
+ end
269
+
270
+ channel.on_request('exit-status') do |_channel, data|
271
+ channel.open3_waiter_thread[:status].tap do |status|
272
+ status.instance_variable_set(:@exitstatus, data.read_long)
273
+ logger.debug("exit status arrived: #{status.exitstatus}") if logger
274
+ end
275
+ end
276
+
277
+ channel.on_request('exit-signal') do |_channel, data|
278
+ channel.open3_waiter_thread[:status].tap do |status|
279
+ status.instance_variable_set(:@termsig, data.read_string)
280
+ status.instance_variable_set(:@coredump, data.read_bool)
281
+ logger.debug("exit signal arrived: #{status.termsig.inspect}, core #{status.coredump?}") if logger
282
+ end
283
+ end
284
+
285
+ channel.on_eof do
286
+ logger.debug('server reports EOF') if logger
287
+ [stdout, stderr].each { |io| io.close unless io.nil? || io.closed? }
288
+ end
289
+
290
+ channel.on_close do
291
+ logger.debug('channel close command received, will enforce EOF afterwards') if logger
292
+ if stdin.is_a?(IO)
293
+ self.stop_listening_to(stdin)
294
+ stdin.close unless stdin.closed?
295
+ end
296
+ channel.do_eof # Should already be done, but just in case.
297
+ end
298
+
299
+ if stdin.is_a?(IO)
300
+ send_packet_size = [1024, channel.remote_maximum_packet_size - REMOTE_PACKET_THRESHOLD].max
301
+ logger.debug("will split stdin into packets with size = #{send_packet_size}") if logger
302
+ self.listen_to(stdin) do
303
+ begin
304
+ data = stdin.readpartial(send_packet_size)
305
+ logger.stdin(data) if logger.respond_to?(:stdin)
306
+ channel.send_data(data)
307
+ rescue EOFError
308
+ logger.debug('sending EOF command') if logger
309
+ self.stop_listening_to(stdin)
310
+ channel.eof!
311
+ end
312
+ end
313
+ elsif stdin.is_a?(String)
314
+ logger.stdin(stdin) if logger.respond_to?(:stdin)
315
+ channel.send_data(stdin)
316
+ channel.eof!
317
+ end
318
+ end
319
+
320
+ REDIRECT_MAPPING = { #:nodoc:
321
+ in: '<',
322
+ out: '>',
323
+ err: '2>'
324
+ }
325
+
326
+ def run_popen(*args, internal_options)
327
+ options = (args.pop if Hash === args.last) || {}
328
+ env = (args.shift if Hash === args.first) || {}
329
+ cmdline = args.size == 1 ? args.first : Shellwords.join(args.map(&:to_s))
330
+
331
+ redirects = options[:redirects] and redirects.each_pair do |fd_and_dir, destination|
332
+ cmdline += " #{REDIRECT_MAPPING[fd_and_dir] || fd_and_dir}#{popen_io_name(destination)}"
333
+ end
334
+ logger = options[:logger]
335
+ pty_options = options[:pty]
336
+ retries, delay = options[:channel_retries]
337
+ retries ||= 5
338
+ delay ||= 1
339
+
340
+ logger.init(host: self.transport.host_as_string, cmdline: cmdline,
341
+ env: env, pty: pty_options) if logger.respond_to?(:init)
342
+
343
+ begin
344
+ channel = open3_open_channel do |channel|
345
+ channel.request_pty(Hash === pty_options ? pty_options : {}) if pty_options
346
+ env.each_pair { |var_name, var_value| channel.env(var_name, var_value) }
347
+
348
+ channel.exec("echo $$; #{cmdline}")
349
+
350
+ install_channel_callbacks channel,
351
+ stdin: internal_options[:stdin],
352
+ stdout: internal_options[:stdout],
353
+ stderr: internal_options[:stderr],
354
+ logger: logger
355
+ end.open3_wait_open
356
+ rescue ChannelOpenFailed
357
+ logger.warn("channel open failed: #$!, #{retries} retries left") if logger
358
+ if (retries -= 1) >= 0
359
+ sleep delay
360
+ retry
361
+ else raise
362
+ end
363
+ end
364
+ logger.debug('channel is open and ready, calling user-defined block') if logger
365
+ begin
366
+ yield(*internal_options[:block_pipes], channel.open3_waiter_thread)
367
+ ensure
368
+ channel.wait
369
+ end
370
+ ensure
371
+ [
372
+ *internal_options[:block_pipes],
373
+ internal_options[:stdin],
374
+ internal_options[:stdout],
375
+ internal_options[:stderr]
376
+ ].each { |io| io.close if io.is_a?(IO) && !io.closed? }
377
+ end
378
+
379
+ def popen_io_name(name)
380
+ Fixnum === name ? "&#{name}" : Shellwords.shellescape(name)
381
+ end
382
+ end
383
+
384
+ module Connection
385
+ class Session
386
+ include Open3
387
+
388
+ alias_method_once :initialize_without_open3, :initialize
389
+ # Overridden version of +initialize+ which starts an Open3 SSH loop.
390
+ # @private
391
+ def initialize(*args, &block)
392
+ initialize_without_open3(*args, &block)
393
+
394
+ @open3_channels_mutex = Mutex.new
395
+
396
+ # open3_ping method will pull waiter thread out of select(2) call
397
+ # to update watched Channels and IOs and process incomes.
398
+ pinger_reader, @open3_pinger_writer = IO.pipe
399
+ listen_to(pinger_reader) { pinger_reader.readpartial(1) }
400
+
401
+ @session_loop = Thread.new { open3_loop }
402
+ end
403
+
404
+ private
405
+ def open3_open_channel(type = 'session', *extra, &on_confirm)
406
+ @open3_channels_mutex.synchronize do
407
+ local_id = get_next_channel_id
408
+ channel = Connection::Channel.new(self, type, local_id, @max_pkt_size, @max_win_size, &on_confirm)
409
+ channel.open3_waiter_thread = Thread.new do
410
+ status = Thread.current[:status] = Process::Status.new
411
+ @open3_channels_mutex.synchronize do
412
+ msg = Buffer.from(:byte, CHANNEL_OPEN, :string, type, :long, local_id,
413
+ :long, channel.local_maximum_window_size,
414
+ :long, channel.local_maximum_packet_size, *extra)
415
+ send_message(msg)
416
+ channels[local_id] = channel
417
+ open3_ping
418
+
419
+ channel.open3_close_semaphore.wait(@open3_channels_mutex) if channels.key?(channel.local_id)
420
+ end
421
+ raise *channel.open3_exception if channel.open3_exception
422
+ status
423
+ end
424
+
425
+ channel
426
+ end
427
+ end
428
+
429
+ def open3_ping
430
+ @open3_pinger_writer.write(?P)
431
+ end
432
+
433
+ def open3_loop
434
+ r, w = nil
435
+ while not closed?
436
+ @open3_channels_mutex.synchronize do
437
+ break unless preprocess { not closed? } # This may remove some channels.
438
+ r = listeners.keys
439
+ w = r.select { |w2| w2.respond_to?(:pending_write?) && w2.pending_write? }
440
+ end
441
+
442
+ readers, writers, = Compat.io_select(r, w, nil, nil)
443
+ postprocess(readers, writers)
444
+ end
445
+
446
+ channels.each do |_id, channel|
447
+ @open3_channels_mutex.synchronize do
448
+ channel.open3_signal_open
449
+ channel.open3_signal_close
450
+ channel.do_close
451
+ end
452
+ end
453
+ rescue
454
+ warn "Caught exception in an Open3 loop: #$!; thread terminating, connections will hang."
455
+ end
456
+ end
457
+
458
+ # All methods in this class were created for private use of Net::SSH::Open3.
459
+ # You probably won't need to call them directly.
460
+ # @private
461
+ class Channel
462
+ # A semaphore to flag this channel as closed.
463
+ attr_reader :open3_close_semaphore
464
+
465
+ # An exception tracked during channel opening, if any.
466
+ attr_reader :open3_exception
467
+
468
+ # Waiter thread that watches this channel.
469
+ attr_reader :open3_waiter_thread
470
+
471
+ alias_method_once :initialize_without_open3, :initialize
472
+ # Overridden version of +initialize+ which creates synchronization objects.
473
+ def initialize(*args, &block)
474
+ initialize_without_open3(*args, &block)
475
+ @open3_close_semaphore = ConditionVariable.new
476
+
477
+ @open3_open_mutex = Mutex.new
478
+ @open3_open_semaphore = ConditionVariable.new
479
+ end
480
+
481
+ alias_method_once :do_close_without_open3, :do_close
482
+ # Overridden version of +do_close+ which tracks exceptions and sync.
483
+ def do_close(*args)
484
+ do_close_without_open3(*args)
485
+ rescue
486
+ @open3_exception = $!
487
+ ensure
488
+ open3_signal_close
489
+ end
490
+
491
+ alias_method_once :do_open_confirmation_without_open3, :do_open_confirmation
492
+ # Overridden version of +do_open_confirmation+ which tracks exceptions.
493
+ def do_open_confirmation(*args)
494
+ do_open_confirmation_without_open3(*args)
495
+ # Do not signal right now: we will signal as soon as PID arrives.
496
+ rescue
497
+ @open3_exception = $!
498
+ end
499
+
500
+ alias_method_once :do_open_failed_without_open3, :do_open_failed
501
+ # Overridden version of +do_open_failed+ which tracks exceptions and sync.
502
+ def do_open_failed(*args)
503
+ do_open_failed_without_open3(*args)
504
+ rescue
505
+ @open3_exception = $!
506
+ ensure
507
+ open3_signal_open
508
+ open3_signal_close
509
+ end
510
+
511
+ # +waiter_thread+ setter which may only be called once with non-false argument.
512
+ def open3_waiter_thread=(value)
513
+ @open3_waiter_thread = value unless @open3_waiter_thread
514
+ end
515
+
516
+ # Suspend current thread execution until this channel is opened.
517
+ # Raises an exception if tracked during opening.
518
+ def open3_wait_open
519
+ @open3_open_mutex.synchronize { @open3_open_semaphore.wait(@open3_open_mutex) }
520
+ raise *open3_exception if open3_exception
521
+ self
522
+ end
523
+
524
+ # Wait for this channel to be closed.
525
+ def wait
526
+ @open3_waiter_thread.join
527
+ self
528
+ end
529
+
530
+ # Flag this channel as opened and deliver signals.
531
+ def open3_signal_open
532
+ @open3_open_mutex.synchronize { @open3_open_semaphore.signal }
533
+ end
534
+
535
+ # Flag this channel as closed and deliver signals.
536
+ # Should be called from within session's mutex.
537
+ def open3_signal_close
538
+ @open3_close_semaphore.signal
539
+ end
540
+ end
541
+ end
542
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: net-ssh-open3
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Artem Sheremet
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-10-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-ssh
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '2.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '2.6'
27
+ description:
28
+ email: dot.doom@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - lib/net-ssh-open3.rb
34
+ - README.md
35
+ homepage: http://github.com/dotdoom/net-ssh-open3
36
+ licenses:
37
+ - MIT
38
+ metadata: {}
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubyforge_project:
55
+ rubygems_version: 2.0.3
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: Thread-safe Open3 for Net::SSH
59
+ test_files: []
60
+ has_rdoc: