train-core 2.1.7 → 2.1.13
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 +4 -4
- data/lib/train.rb +20 -20
- data/lib/train/errors.rb +1 -1
- data/lib/train/extras.rb +2 -2
- data/lib/train/extras/command_wrapper.rb +24 -24
- data/lib/train/extras/stat.rb +27 -27
- data/lib/train/file.rb +30 -30
- data/lib/train/file/local.rb +8 -8
- data/lib/train/file/local/unix.rb +5 -5
- data/lib/train/file/local/windows.rb +1 -1
- data/lib/train/file/remote.rb +8 -8
- data/lib/train/file/remote/aix.rb +1 -1
- data/lib/train/file/remote/linux.rb +2 -2
- data/lib/train/file/remote/qnx.rb +8 -8
- data/lib/train/file/remote/unix.rb +10 -14
- data/lib/train/file/remote/windows.rb +5 -5
- data/lib/train/globals.rb +1 -1
- data/lib/train/options.rb +8 -8
- data/lib/train/platforms.rb +8 -8
- data/lib/train/platforms/common.rb +1 -1
- data/lib/train/platforms/detect/helpers/os_common.rb +36 -32
- data/lib/train/platforms/detect/helpers/os_linux.rb +12 -12
- data/lib/train/platforms/detect/helpers/os_windows.rb +27 -29
- data/lib/train/platforms/detect/scanner.rb +4 -4
- data/lib/train/platforms/detect/specifications/api.rb +8 -8
- data/lib/train/platforms/detect/specifications/os.rb +252 -252
- data/lib/train/platforms/detect/uuid.rb +5 -7
- data/lib/train/platforms/platform.rb +9 -5
- data/lib/train/plugin_test_helper.rb +12 -12
- data/lib/train/plugins.rb +5 -5
- data/lib/train/plugins/base_connection.rb +13 -13
- data/lib/train/plugins/transport.rb +7 -7
- data/lib/train/transports/cisco_ios_connection.rb +20 -20
- data/lib/train/transports/local.rb +22 -22
- data/lib/train/transports/mock.rb +33 -35
- data/lib/train/transports/ssh.rb +47 -47
- data/lib/train/transports/ssh_connection.rb +28 -28
- data/lib/train/transports/winrm.rb +37 -37
- data/lib/train/transports/winrm_connection.rb +12 -12
- data/lib/train/version.rb +1 -1
- metadata +2 -2
@@ -1,11 +1,9 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require 'train/plugins'
|
4
|
-
require 'digest'
|
1
|
+
require "train/plugins"
|
2
|
+
require "digest"
|
5
3
|
|
6
4
|
module Train::Transports
|
7
5
|
class Mock < Train.plugin(1)
|
8
|
-
name
|
6
|
+
name "mock"
|
9
7
|
|
10
8
|
def initialize(conf = nil)
|
11
9
|
@conf = conf || {}
|
@@ -17,39 +15,39 @@ module Train::Transports
|
|
17
15
|
end
|
18
16
|
|
19
17
|
def to_s
|
20
|
-
|
18
|
+
"Mock Transport"
|
21
19
|
end
|
22
20
|
|
23
21
|
private
|
24
22
|
|
25
23
|
def trace_calls
|
26
24
|
interface_methods = {
|
27
|
-
|
25
|
+
"Train::Transports::Mock" =>
|
28
26
|
Train::Transports::Mock.instance_methods(false),
|
29
|
-
|
27
|
+
"Train::Transports::Mock::Connection" =>
|
30
28
|
Connection.instance_methods(false),
|
31
|
-
|
29
|
+
"Train::Transports::Mock::Connection::File" =>
|
32
30
|
Connection::FileCommon.instance_methods(false),
|
33
|
-
|
31
|
+
"Train::Transports::Mock::Connection::OS" =>
|
34
32
|
Train::Platform.instance_methods(false),
|
35
33
|
}
|
36
34
|
|
37
35
|
# rubocop:disable Metrics/ParameterLists
|
38
36
|
# rubocop:disable Lint/Eval
|
39
|
-
set_trace_func
|
40
|
-
unless classname.to_s.start_with?(
|
41
|
-
|
42
|
-
|
37
|
+
set_trace_func(proc { |event, _file, _line, id, binding, classname|
|
38
|
+
unless classname.to_s.start_with?("Train::Transports::Mock") &&
|
39
|
+
(event == "call") &&
|
40
|
+
interface_methods[classname.to_s].include?(id)
|
43
41
|
next
|
44
42
|
end
|
45
43
|
# kindly borrowed from the wonderful simple-tracer by matugm
|
46
44
|
arg_names = eval(
|
47
|
-
|
45
|
+
"method(__method__).parameters.map { |arg| arg[1].to_s }",
|
48
46
|
binding)
|
49
|
-
args = eval("#{arg_names}.map { |arg| eval(arg) }", binding).join(
|
50
|
-
prefix =
|
47
|
+
args = eval("#{arg_names}.map { |arg| eval(arg) }", binding).join(", ")
|
48
|
+
prefix = "-" * (classname.to_s.count(":") - 2) + "> "
|
51
49
|
puts("#{prefix}#{id} #{args}")
|
52
|
-
}
|
50
|
+
})
|
53
51
|
# rubocop:enable all
|
54
52
|
end
|
55
53
|
end
|
@@ -67,14 +65,14 @@ class Train::Transports::Mock
|
|
67
65
|
end
|
68
66
|
|
69
67
|
def uri
|
70
|
-
|
68
|
+
"mock://"
|
71
69
|
end
|
72
70
|
|
73
71
|
def mock_os(value = {})
|
74
72
|
# if a user passes a nil value, set to an empty hash so the merge still succeeds
|
75
73
|
value ||= {}
|
76
|
-
value.each { |k, v| value[k] =
|
77
|
-
value = { name:
|
74
|
+
value.each { |k, v| value[k] = "unknown" if v.nil? }
|
75
|
+
value = { name: "mock", family: "mock", release: "unknown", arch: "unknown" }.merge(value)
|
78
76
|
|
79
77
|
platform = Train::Platforms.name(value[:name])
|
80
78
|
platform.find_family_hierarchy
|
@@ -100,26 +98,26 @@ class Train::Transports::Mock
|
|
100
98
|
end
|
101
99
|
|
102
100
|
def mock_command(cmd, stdout = nil, stderr = nil, exit_status = 0)
|
103
|
-
@cache[:command][cmd] = Command.new(stdout ||
|
101
|
+
@cache[:command][cmd] = Command.new(stdout || "", stderr || "", exit_status)
|
104
102
|
end
|
105
103
|
|
106
104
|
def command_not_found(cmd)
|
107
105
|
if @options[:verbose]
|
108
|
-
|
109
|
-
|
110
|
-
|
106
|
+
$stderr.puts("Command not mocked:")
|
107
|
+
$stderr.puts(" " + cmd.to_s.split("\n").join("\n "))
|
108
|
+
$stderr.puts(" SHA: " + Digest::SHA256.hexdigest(cmd.to_s))
|
111
109
|
end
|
112
110
|
# return a non-zero exit code
|
113
111
|
mock_command(cmd, nil, nil, 1)
|
114
112
|
end
|
115
113
|
|
116
114
|
def file_not_found(path)
|
117
|
-
|
115
|
+
$stderr.puts("File not mocked: " + path.to_s) if @options[:verbose]
|
118
116
|
File.new(self, path)
|
119
117
|
end
|
120
118
|
|
121
119
|
def to_s
|
122
|
-
|
120
|
+
"Mock Connection"
|
123
121
|
end
|
124
122
|
|
125
123
|
private
|
@@ -142,23 +140,23 @@ end
|
|
142
140
|
class Train::Transports::Mock::Connection
|
143
141
|
class File < Train::File
|
144
142
|
def self.from_json(json)
|
145
|
-
res = new(json[
|
146
|
-
json[
|
147
|
-
json[
|
148
|
-
res.type = json[
|
143
|
+
res = new(json["backend"],
|
144
|
+
json["path"],
|
145
|
+
json["follow_symlink"])
|
146
|
+
res.type = json["type"]
|
149
147
|
Train::File::DATA_FIELDS.each do |f|
|
150
|
-
m = (f.tr(
|
148
|
+
m = (f.tr("?", "") + "=").to_sym
|
151
149
|
res.method(m).call(json[f])
|
152
150
|
end
|
153
151
|
res
|
154
152
|
end
|
155
153
|
|
156
154
|
Train::File::DATA_FIELDS.each do |m|
|
157
|
-
attr_accessor m.tr(
|
158
|
-
next unless m.include?(
|
155
|
+
attr_accessor m.tr("?", "").to_sym
|
156
|
+
next unless m.include?("?")
|
159
157
|
|
160
158
|
define_method m.to_sym do
|
161
|
-
method(m.tr(
|
159
|
+
method(m.tr("?", "").to_sym).call
|
162
160
|
end
|
163
161
|
end
|
164
162
|
attr_accessor :type
|
data/lib/train/transports/ssh.rb
CHANGED
@@ -18,9 +18,9 @@
|
|
18
18
|
# See the License for the specific language governing permissions and
|
19
19
|
# limitations under the License.
|
20
20
|
|
21
|
-
require
|
22
|
-
require
|
23
|
-
require
|
21
|
+
require "net/ssh"
|
22
|
+
require "net/scp"
|
23
|
+
require "train/errors"
|
24
24
|
|
25
25
|
module Train::Transports
|
26
26
|
# Wrapped exception for any internally raised SSH-related errors.
|
@@ -34,10 +34,10 @@ module Train::Transports
|
|
34
34
|
#
|
35
35
|
# @author Fletcher Nichol <fnichol@nichol.ca>
|
36
36
|
class SSH < Train.plugin(1) # rubocop:disable Metrics/ClassLength
|
37
|
-
name
|
37
|
+
name "ssh"
|
38
38
|
|
39
|
-
require
|
40
|
-
require
|
39
|
+
require "train/transports/ssh_connection"
|
40
|
+
require "train/transports/cisco_ios_connection"
|
41
41
|
|
42
42
|
# add options for submodules
|
43
43
|
include_options Train::Extras::CommandWrapper
|
@@ -45,7 +45,7 @@ module Train::Transports
|
|
45
45
|
# common target configuration
|
46
46
|
option :host, required: true
|
47
47
|
option :port, default: 22, required: true
|
48
|
-
option :user, default:
|
48
|
+
option :user, default: "root", required: true
|
49
49
|
option :key_files, default: nil
|
50
50
|
option :password, default: nil
|
51
51
|
|
@@ -60,7 +60,7 @@ module Train::Transports
|
|
60
60
|
option :pty, default: false
|
61
61
|
option :proxy_command, default: nil
|
62
62
|
option :bastion_host, default: nil
|
63
|
-
option :bastion_user, default:
|
63
|
+
option :bastion_user, default: "root"
|
64
64
|
option :bastion_port, default: 22
|
65
65
|
option :non_interactive, default: false
|
66
66
|
option :verify_host_key, default: false
|
@@ -89,34 +89,34 @@ module Train::Transports
|
|
89
89
|
super(options)
|
90
90
|
|
91
91
|
key_files = Array(options[:key_files])
|
92
|
-
options[:auth_methods] ||= [
|
92
|
+
options[:auth_methods] ||= ["none"]
|
93
93
|
|
94
94
|
unless key_files.empty?
|
95
|
-
options[:auth_methods].push(
|
95
|
+
options[:auth_methods].push("publickey")
|
96
96
|
options[:keys_only] = true if options[:password].nil?
|
97
97
|
options[:key_files] = key_files
|
98
98
|
end
|
99
99
|
|
100
100
|
unless options[:password].nil?
|
101
|
-
options[:auth_methods].push(
|
101
|
+
options[:auth_methods].push("password", "keyboard-interactive")
|
102
102
|
end
|
103
103
|
|
104
|
-
if options[:auth_methods] == [
|
104
|
+
if options[:auth_methods] == ["none"]
|
105
105
|
if ssh_known_identities.empty?
|
106
|
-
|
107
|
-
|
106
|
+
raise Train::ClientError,
|
107
|
+
"Your SSH Agent has no keys added, and you have not specified a password or a key file"
|
108
108
|
else
|
109
|
-
logger.debug(
|
110
|
-
options[:auth_methods].push(
|
109
|
+
logger.debug("[SSH] Using Agent keys as no password or key file have been specified")
|
110
|
+
options[:auth_methods].push("publickey")
|
111
111
|
end
|
112
112
|
end
|
113
113
|
|
114
114
|
if options[:pty]
|
115
|
-
logger.warn(
|
115
|
+
logger.warn("[SSH] PTY requested: stderr will be merged into stdout")
|
116
116
|
end
|
117
117
|
|
118
118
|
if [options[:proxy_command], options[:bastion_host]].all? { |type| !type.nil? }
|
119
|
-
|
119
|
+
raise Train::ClientError, "Only one of proxy_command or bastion_host needs to be specified"
|
120
120
|
end
|
121
121
|
|
122
122
|
super
|
@@ -142,30 +142,30 @@ module Train::Transports
|
|
142
142
|
# @api private
|
143
143
|
def connection_options(opts)
|
144
144
|
connection_options = {
|
145
|
-
logger:
|
146
|
-
user_known_hosts_file:
|
147
|
-
hostname:
|
148
|
-
port:
|
149
|
-
username:
|
150
|
-
compression:
|
151
|
-
compression_level:
|
152
|
-
keepalive:
|
153
|
-
keepalive_interval:
|
154
|
-
timeout:
|
155
|
-
connection_retries:
|
145
|
+
logger: logger,
|
146
|
+
user_known_hosts_file: "/dev/null",
|
147
|
+
hostname: opts[:host],
|
148
|
+
port: opts[:port],
|
149
|
+
username: opts[:user],
|
150
|
+
compression: opts[:compression],
|
151
|
+
compression_level: opts[:compression_level],
|
152
|
+
keepalive: opts[:keepalive],
|
153
|
+
keepalive_interval: opts[:keepalive_interval],
|
154
|
+
timeout: opts[:connection_timeout],
|
155
|
+
connection_retries: opts[:connection_retries],
|
156
156
|
connection_retry_sleep: opts[:connection_retry_sleep],
|
157
|
-
max_wait_until_ready:
|
158
|
-
auth_methods:
|
159
|
-
keys_only:
|
160
|
-
keys:
|
161
|
-
password:
|
162
|
-
forward_agent:
|
163
|
-
proxy_command:
|
164
|
-
bastion_host:
|
165
|
-
bastion_user:
|
166
|
-
bastion_port:
|
167
|
-
non_interactive:
|
168
|
-
transport_options:
|
157
|
+
max_wait_until_ready: opts[:max_wait_until_ready],
|
158
|
+
auth_methods: opts[:auth_methods],
|
159
|
+
keys_only: opts[:keys_only],
|
160
|
+
keys: opts[:key_files],
|
161
|
+
password: opts[:password],
|
162
|
+
forward_agent: opts[:forward_agent],
|
163
|
+
proxy_command: opts[:proxy_command],
|
164
|
+
bastion_host: opts[:bastion_host],
|
165
|
+
bastion_user: opts[:bastion_user],
|
166
|
+
bastion_port: opts[:bastion_port],
|
167
|
+
non_interactive: opts[:non_interactive],
|
168
|
+
transport_options: opts,
|
169
169
|
}
|
170
170
|
# disable host key verification. The hash key and value to use
|
171
171
|
# depends on the version of net-ssh in use.
|
@@ -203,20 +203,20 @@ module Train::Transports
|
|
203
203
|
# 5.0+ style
|
204
204
|
{
|
205
205
|
# It's not a boolean anymore.
|
206
|
-
|
207
|
-
|
206
|
+
"true" => :always,
|
207
|
+
"false" => :never,
|
208
208
|
true => :always,
|
209
209
|
false => :never,
|
210
210
|
# May be correct value, but strings from JSON config
|
211
|
-
|
212
|
-
|
211
|
+
"always" => :always,
|
212
|
+
"never" => :never,
|
213
213
|
nil => :never,
|
214
214
|
}.fetch(given, given)
|
215
215
|
else
|
216
216
|
# up to 4.2 style
|
217
217
|
{
|
218
|
-
|
219
|
-
|
218
|
+
"true" => true,
|
219
|
+
"false" => false,
|
220
220
|
nil => false,
|
221
221
|
}.fetch(given, given)
|
222
222
|
end
|
@@ -18,9 +18,9 @@
|
|
18
18
|
# See the License for the specific language governing permissions and
|
19
19
|
# limitations under the License.
|
20
20
|
|
21
|
-
require
|
22
|
-
require
|
23
|
-
require
|
21
|
+
require "net/ssh"
|
22
|
+
require "net/scp"
|
23
|
+
require "timeout"
|
24
24
|
|
25
25
|
class Train::Transports::SSH
|
26
26
|
# A Connection instance can be generated and re-generated, given new
|
@@ -63,17 +63,17 @@ class Train::Transports::SSH
|
|
63
63
|
end
|
64
64
|
|
65
65
|
def ssh_opts
|
66
|
-
level = logger.debug? ?
|
67
|
-
fwd_agent = options[:forward_agent] ?
|
66
|
+
level = logger.debug? ? "VERBOSE" : "ERROR"
|
67
|
+
fwd_agent = options[:forward_agent] ? "yes" : "no"
|
68
68
|
|
69
69
|
args = %w{ -o UserKnownHostsFile=/dev/null }
|
70
70
|
args += %w{ -o StrictHostKeyChecking=no }
|
71
71
|
args += %w{ -o IdentitiesOnly=yes } if options[:keys]
|
72
72
|
args += %w{ -o BatchMode=yes } if options[:non_interactive]
|
73
|
-
args += %W
|
74
|
-
args += %W
|
73
|
+
args += %W{ -o LogLevel=#{level} }
|
74
|
+
args += %W{ -o ForwardAgent=#{fwd_agent} } if options.key?(:forward_agent)
|
75
75
|
Array(options[:keys]).each do |ssh_key|
|
76
|
-
args += %W
|
76
|
+
args += %W{ -i #{ssh_key} }
|
77
77
|
end
|
78
78
|
args
|
79
79
|
end
|
@@ -86,19 +86,19 @@ class Train::Transports::SSH
|
|
86
86
|
return @proxy_command unless @proxy_command.nil?
|
87
87
|
args = %w{ ssh }
|
88
88
|
args += ssh_opts
|
89
|
-
args += %W
|
90
|
-
args += %W
|
89
|
+
args += %W{ #{@bastion_user}@#{@bastion_host} }
|
90
|
+
args += %W{ -p #{@bastion_port} }
|
91
91
|
args += %w{ -W %h:%p }
|
92
|
-
args.join(
|
92
|
+
args.join(" ")
|
93
93
|
end
|
94
94
|
|
95
95
|
# (see Base::Connection#login_command)
|
96
96
|
def login_command
|
97
97
|
args = ssh_opts
|
98
|
-
args += %W
|
99
|
-
args += %W
|
100
|
-
args += %W
|
101
|
-
LoginCommand.new(
|
98
|
+
args += %W{ -o ProxyCommand='#{generate_proxy_command}' } if check_proxy
|
99
|
+
args += %W{ -p #{@port} }
|
100
|
+
args += %W{ #{@username}@#{@hostname} }
|
101
|
+
LoginCommand.new("ssh", args)
|
102
102
|
end
|
103
103
|
|
104
104
|
# (see Base::Connection#upload)
|
@@ -138,7 +138,7 @@ class Train::Transports::SSH
|
|
138
138
|
retries: @max_wait_until_ready / delay,
|
139
139
|
delay: delay,
|
140
140
|
message: "Waiting for SSH service on #{@hostname}:#{@port}, " \
|
141
|
-
"retrying in #{delay} seconds"
|
141
|
+
"retrying in #{delay} seconds"
|
142
142
|
)
|
143
143
|
run_command(PING_COMMAND.dup)
|
144
144
|
end
|
@@ -172,14 +172,14 @@ class Train::Transports::SSH
|
|
172
172
|
def establish_connection(opts)
|
173
173
|
logger.debug("[SSH] opening connection to #{self}")
|
174
174
|
if check_proxy
|
175
|
-
require
|
175
|
+
require "net/ssh/proxy/command"
|
176
176
|
@options[:proxy] = Net::SSH::Proxy::Command.new(generate_proxy_command)
|
177
177
|
end
|
178
178
|
Net::SSH.start(@hostname, @username, @options.clone.delete_if { |_key, value| value.nil? })
|
179
179
|
rescue *RESCUE_EXCEPTIONS_ON_ESTABLISH => e
|
180
180
|
if (opts[:retries] -= 1) <= 0
|
181
181
|
logger.warn("[SSH] connection failed, terminating (#{e.inspect})")
|
182
|
-
raise Train::Transports::SSHFailed,
|
182
|
+
raise Train::Transports::SSHFailed, "SSH session could not be established"
|
183
183
|
end
|
184
184
|
|
185
185
|
if opts[:message]
|
@@ -200,7 +200,7 @@ class Train::Transports::SSH
|
|
200
200
|
Train::File::Remote::Aix.new(self, path)
|
201
201
|
elsif os.solaris?
|
202
202
|
Train::File::Remote::Unix.new(self, path)
|
203
|
-
elsif os[:name] ==
|
203
|
+
elsif os[:name] == "qnx"
|
204
204
|
Train::File::Remote::Qnx.new(self, path)
|
205
205
|
elsif os.windows?
|
206
206
|
Train::File::Remote::Windows.new(self, path)
|
@@ -210,7 +210,7 @@ class Train::Transports::SSH
|
|
210
210
|
end
|
211
211
|
|
212
212
|
def run_command_via_connection(cmd, &data_handler)
|
213
|
-
cmd.dup.force_encoding(
|
213
|
+
cmd.dup.force_encoding("binary") if cmd.respond_to?(:force_encoding)
|
214
214
|
logger.debug("[SSH] #{self} (#{cmd})")
|
215
215
|
|
216
216
|
reset_session if session.closed?
|
@@ -228,7 +228,7 @@ class Train::Transports::SSH
|
|
228
228
|
# transport. This retries the command if this is the case.
|
229
229
|
# See:
|
230
230
|
# https://github.com/inspec/train/pull/271
|
231
|
-
logger.debug(
|
231
|
+
logger.debug("[SSH] Possible Cisco IOS race condition, retrying command")
|
232
232
|
|
233
233
|
# Only attempt retry up to 5 times to avoid infinite loop
|
234
234
|
@ios_cmd_retries += 1
|
@@ -246,7 +246,7 @@ class Train::Transports::SSH
|
|
246
246
|
def session(retry_options = {})
|
247
247
|
@session ||= establish_connection({
|
248
248
|
retries: @connection_retries.to_i,
|
249
|
-
delay:
|
249
|
+
delay: @connection_retry_sleep.to_i,
|
250
250
|
}.merge(retry_options))
|
251
251
|
end
|
252
252
|
|
@@ -260,7 +260,7 @@ class Train::Transports::SSH
|
|
260
260
|
# @api private
|
261
261
|
def to_s
|
262
262
|
options_to_print = @options.clone
|
263
|
-
options_to_print[:password] =
|
263
|
+
options_to_print[:password] = "<hidden>" if options_to_print.key?(:password)
|
264
264
|
"#{@username}@#{@hostname}<#{options_to_print.inspect}>"
|
265
265
|
end
|
266
266
|
|
@@ -274,7 +274,7 @@ class Train::Transports::SSH
|
|
274
274
|
#
|
275
275
|
# @api private
|
276
276
|
def execute_on_channel(cmd, &data_handler)
|
277
|
-
stdout = stderr =
|
277
|
+
stdout = stderr = ""
|
278
278
|
exit_status = nil
|
279
279
|
session.open_channel do |channel|
|
280
280
|
# wrap commands if that is configured
|
@@ -282,11 +282,11 @@ class Train::Transports::SSH
|
|
282
282
|
|
283
283
|
if @transport_options[:pty]
|
284
284
|
channel.request_pty do |_ch, success|
|
285
|
-
|
285
|
+
raise Train::Transports::SSHPTYFailed, "Requesting PTY failed" unless success
|
286
286
|
end
|
287
287
|
end
|
288
288
|
channel.exec(cmd) do |_, success|
|
289
|
-
abort
|
289
|
+
abort "Couldn't execute command on SSH." unless success
|
290
290
|
channel.on_data do |_, data|
|
291
291
|
yield(data) unless data_handler.nil?
|
292
292
|
stdout += data
|
@@ -297,11 +297,11 @@ class Train::Transports::SSH
|
|
297
297
|
stderr += data
|
298
298
|
end
|
299
299
|
|
300
|
-
channel.on_request(
|
300
|
+
channel.on_request("exit-status") do |_, data|
|
301
301
|
exit_status = data.read_long
|
302
302
|
end
|
303
303
|
|
304
|
-
channel.on_request(
|
304
|
+
channel.on_request("exit-signal") do |_, data|
|
305
305
|
exit_status = data.read_long
|
306
306
|
end
|
307
307
|
end
|