net-ssh-open3 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![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:
|