train-core 2.1.7 → 2.1.13
Sign up to get free protection for your applications and to get access to all the features.
- 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
|