net-ssh-open3 0.1.2

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.
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: