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.
- checksums.yaml +7 -0
- data/README.md +34 -0
- data/lib/net-ssh-open3.rb +542 -0
- 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
|
+
[](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:
|