mixlib-shellout 2.2.3 → 2.2.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
- module Mixlib
2
- class ShellOut
3
- VERSION = "2.2.3"
4
- end
5
- end
1
+ module Mixlib
2
+ class ShellOut
3
+ VERSION = "2.2.5"
4
+ end
5
+ end
@@ -1,362 +1,362 @@
1
- #--
2
- # Author:: Daniel DeLeo (<dan@opscode.com>)
3
- # Author:: John Keiser (<jkeiser@opscode.com>)
4
- # Author:: Ho-Sheng Hsiao (<hosh@opscode.com>)
5
- # Copyright:: Copyright (c) 2011, 2012 Opscode, Inc.
6
- # License:: Apache License, Version 2.0
7
- #
8
- # Licensed under the Apache License, Version 2.0 (the "License");
9
- # you may not use this file except in compliance with the License.
10
- # You may obtain a copy of the License at
11
- #
12
- # http://www.apache.org/licenses/LICENSE-2.0
13
- #
14
- # Unless required by applicable law or agreed to in writing, software
15
- # distributed under the License is distributed on an "AS IS" BASIS,
16
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
- # See the License for the specific language governing permissions and
18
- # limitations under the License.
19
- #
20
-
21
- require 'win32/process'
22
- require 'mixlib/shellout/windows/core_ext'
23
-
24
- module Mixlib
25
- class ShellOut
26
- module Windows
27
-
28
- include Process::Functions
29
- include Process::Constants
30
-
31
- TIME_SLICE = 0.05
32
-
33
- # Option validation that is windows specific
34
- def validate_options(opts)
35
- if opts[:user]
36
- unless opts[:password]
37
- raise InvalidCommandOption, "You must supply both a username and password when supplying a user in windows"
38
- end
39
- end
40
- end
41
-
42
- #--
43
- # Missing lots of features from the UNIX version, such as
44
- # uid, etc.
45
- def run_command
46
- #
47
- # Create pipes to capture stdout and stderr,
48
- #
49
- stdout_read, stdout_write = IO.pipe
50
- stderr_read, stderr_write = IO.pipe
51
- stdin_read, stdin_write = IO.pipe
52
- open_streams = [ stdout_read, stderr_read ]
53
-
54
- begin
55
-
56
- #
57
- # Set cwd, environment, appname, etc.
58
- #
59
- app_name, command_line = command_to_run(self.command)
60
- create_process_args = {
61
- :app_name => app_name,
62
- :command_line => command_line,
63
- :startup_info => {
64
- :stdout => stdout_write,
65
- :stderr => stderr_write,
66
- :stdin => stdin_read
67
- },
68
- :environment => inherit_environment.map { |k,v| "#{k}=#{v}" },
69
- :close_handles => false
70
- }
71
- create_process_args[:cwd] = cwd if cwd
72
- # default to local account database if domain is not specified
73
- create_process_args[:domain] = domain.nil? ? "." : domain
74
- create_process_args[:with_logon] = with_logon if with_logon
75
- create_process_args[:password] = password if password
76
-
77
- #
78
- # Start the process
79
- #
80
- process = Process.create(create_process_args)
81
- logger.debug(Utils.format_process(process, app_name, command_line, timeout)) if logger
82
- begin
83
- # Start pushing data into input
84
- stdin_write << input if input
85
-
86
- # Close pipe to kick things off
87
- stdin_write.close
88
-
89
- #
90
- # Wait for the process to finish, consuming output as we go
91
- #
92
- start_wait = Time.now
93
- while true
94
- wait_status = WaitForSingleObject(process.process_handle, 0)
95
- case wait_status
96
- when WAIT_OBJECT_0
97
- # Get process exit code
98
- exit_code = [0].pack('l')
99
- unless GetExitCodeProcess(process.process_handle, exit_code)
100
- raise get_last_error
101
- end
102
- @status = ThingThatLooksSortOfLikeAProcessStatus.new
103
- @status.exitstatus = exit_code.unpack('l').first
104
-
105
- return self
106
- when WAIT_TIMEOUT
107
- # Kill the process
108
- if (Time.now - start_wait) > timeout
109
- begin
110
- require 'wmi-lite/wmi'
111
- wmi = WmiLite::Wmi.new
112
- Utils.kill_process_tree(process.process_id, wmi, logger)
113
- Process.kill(:KILL, process.process_id)
114
- rescue Errno::EIO, SystemCallError
115
- logger.warn("Failed to kill timed out process #{process.process_id}") if logger
116
- end
117
-
118
- raise Mixlib::ShellOut::CommandTimeout, [
119
- "command timed out:",
120
- format_for_exception,
121
- Utils.format_process(process, app_name, command_line, timeout)
122
- ].join("\n")
123
- end
124
-
125
- consume_output(open_streams, stdout_read, stderr_read)
126
- else
127
- raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout*1000}): #{wait_status}"
128
- end
129
-
130
- end
131
-
132
- ensure
133
- CloseHandle(process.thread_handle) if process.thread_handle
134
- CloseHandle(process.process_handle) if process.process_handle
135
- end
136
-
137
- ensure
138
- #
139
- # Consume all remaining data from the pipes until they are closed
140
- #
141
- stdout_write.close
142
- stderr_write.close
143
-
144
- while consume_output(open_streams, stdout_read, stderr_read)
145
- end
146
- end
147
- end
148
-
149
- private
150
-
151
- class ThingThatLooksSortOfLikeAProcessStatus
152
- attr_accessor :exitstatus
153
- def success?
154
- exitstatus == 0
155
- end
156
- end
157
-
158
- def consume_output(open_streams, stdout_read, stderr_read)
159
- return false if open_streams.length == 0
160
- ready = IO.select(open_streams, nil, nil, READ_WAIT_TIME)
161
- return true if ! ready
162
-
163
- if ready.first.include?(stdout_read)
164
- begin
165
- next_chunk = stdout_read.readpartial(READ_SIZE)
166
- @stdout << next_chunk
167
- @live_stdout << next_chunk if @live_stdout
168
- rescue EOFError
169
- stdout_read.close
170
- open_streams.delete(stdout_read)
171
- end
172
- end
173
-
174
- if ready.first.include?(stderr_read)
175
- begin
176
- next_chunk = stderr_read.readpartial(READ_SIZE)
177
- @stderr << next_chunk
178
- @live_stderr << next_chunk if @live_stderr
179
- rescue EOFError
180
- stderr_read.close
181
- open_streams.delete(stderr_read)
182
- end
183
- end
184
-
185
- return true
186
- end
187
-
188
- IS_BATCH_FILE = /\.bat"?$|\.cmd"?$/i
189
-
190
- def command_to_run(command)
191
- return _run_under_cmd(command) if Utils.should_run_under_cmd?(command)
192
-
193
- candidate = candidate_executable_for_command(command)
194
-
195
- # Don't do searching for empty commands. Let it fail when it runs.
196
- return [ nil, command ] if candidate.length == 0
197
-
198
- # Check if the exe exists directly. Otherwise, search PATH.
199
- exe = Utils.find_executable(candidate)
200
- exe = Utils.which(unquoted_executable_path(command)) if exe.nil? && exe !~ /[\\\/]/
201
-
202
- # Batch files MUST use cmd; and if we couldn't find the command we're looking for,
203
- # we assume it must be a cmd builtin.
204
- if exe.nil? || exe =~ IS_BATCH_FILE
205
- _run_under_cmd(command)
206
- else
207
- _run_directly(command, exe)
208
- end
209
- end
210
-
211
- # cmd does not parse multiple quotes well unless the whole thing is wrapped up in quotes.
212
- # https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4837859
213
- # http://ss64.com/nt/syntax-esc.html
214
- def _run_under_cmd(command)
215
- [ ENV['COMSPEC'], "cmd /c \"#{command}\"" ]
216
- end
217
-
218
- def _run_directly(command, exe)
219
- [ exe, command ]
220
- end
221
-
222
- def unquoted_executable_path(command)
223
- command[0,command.index(/\s/) || command.length]
224
- end
225
-
226
- def candidate_executable_for_command(command)
227
- if command =~ /^\s*"(.*?)"/
228
- # If we have quotes, do an exact match
229
- $1
230
- else
231
- # Otherwise check everything up to the first space
232
- unquoted_executable_path(command).strip
233
- end
234
- end
235
-
236
- def inherit_environment
237
- result = {}
238
- ENV.each_pair do |k,v|
239
- result[k] = v
240
- end
241
-
242
- environment.each_pair do |k,v|
243
- if v == nil
244
- result.delete(k)
245
- else
246
- result[k] = v
247
- end
248
- end
249
- result
250
- end
251
-
252
- module Utils
253
- # api: semi-private
254
- # If there are special characters parsable by cmd.exe (such as file redirection), then
255
- # this method should return true.
256
- #
257
- # This parser is based on
258
- # https://github.com/ruby/ruby/blob/9073db5cb1d3173aff62be5b48d00f0fb2890991/win32/win32.c#L1437
259
- def self.should_run_under_cmd?(command)
260
- return true if command =~ /^@/
261
-
262
- quote = nil
263
- env = false
264
- env_first_char = false
265
-
266
- command.dup.each_char do |c|
267
- case c
268
- when "'", '"'
269
- if (!quote)
270
- quote = c
271
- elsif quote == c
272
- quote = nil
273
- end
274
- next
275
- when '>', '<', '|', '&', "\n"
276
- return true unless quote
277
- when '%'
278
- return true if env
279
- env = env_first_char = true
280
- next
281
- else
282
- next unless env
283
- if env_first_char
284
- env_first_char = false
285
- env = false and next if c !~ /[A-Za-z_]/
286
- end
287
- env = false if c !~ /[A-Za-z1-9_]/
288
- end
289
- end
290
- return false
291
- end
292
-
293
- def self.pathext
294
- @pathext ||= ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : ['']
295
- end
296
-
297
- # which() mimicks the Unix which command
298
- # FIXME: it is not working
299
- def self.which(cmd)
300
- ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
301
- exe = find_executable("#{path}/#{cmd}")
302
- return exe if exe
303
- end
304
- return nil
305
- end
306
-
307
- # Windows has a different notion of what "executable" means
308
- # The OS will search through valid the extensions and look
309
- # for a binary there.
310
- def self.find_executable(path)
311
- return path if executable? path
312
-
313
- pathext.each do |ext|
314
- exe = "#{path}#{ext}"
315
- return exe if executable? exe
316
- end
317
- return nil
318
- end
319
-
320
- def self.executable?(path)
321
- File.executable?(path) && !File.directory?(path)
322
- end
323
-
324
- # recursively kills all child processes of given pid
325
- # calls itself querying for children child procs until
326
- # none remain. Important that a single WmiLite instance
327
- # is passed in since each creates its own WMI rpc process
328
- def self.kill_process_tree(pid, wmi, logger)
329
- wmi.query("select * from Win32_Process where ParentProcessID=#{pid}").each do |instance|
330
- child_pid = instance.wmi_ole_object.processid
331
- kill_process_tree(child_pid, wmi, logger)
332
- begin
333
- logger.debug([
334
- "killing child process #{child_pid}::",
335
- "#{instance.wmi_ole_object.Name} of parent #{pid}"
336
- ].join) if logger
337
- kill_process(instance)
338
- rescue Errno::EIO, SystemCallError
339
- logger.debug([
340
- "Failed to kill child process #{child_pid}::",
341
- "#{instance.wmi_ole_object.Name} of parent #{pid}"
342
- ].join) if logger
343
- end
344
- end
345
- end
346
-
347
- def self.kill_process(instance)
348
- Process.kill(:KILL, instance.wmi_ole_object.processid)
349
- end
350
-
351
- def self.format_process(process, app_name, command_line, timeout)
352
- msg = []
353
- msg << "ProcessId: #{process.process_id}"
354
- msg << "app_name: #{app_name}"
355
- msg << "command_line: #{command_line}"
356
- msg << "timeout: #{timeout}"
357
- msg.join("\n")
358
- end
359
- end
360
- end # class
361
- end
362
- end
1
+ #--
2
+ # Author:: Daniel DeLeo (<dan@opscode.com>)
3
+ # Author:: John Keiser (<jkeiser@opscode.com>)
4
+ # Author:: Ho-Sheng Hsiao (<hosh@opscode.com>)
5
+ # Copyright:: Copyright (c) 2011, 2012 Opscode, Inc.
6
+ # License:: Apache License, Version 2.0
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ #
20
+
21
+ require 'win32/process'
22
+ require 'mixlib/shellout/windows/core_ext'
23
+
24
+ module Mixlib
25
+ class ShellOut
26
+ module Windows
27
+
28
+ include Process::Functions
29
+ include Process::Constants
30
+
31
+ TIME_SLICE = 0.05
32
+
33
+ # Option validation that is windows specific
34
+ def validate_options(opts)
35
+ if opts[:user]
36
+ unless opts[:password]
37
+ raise InvalidCommandOption, "You must supply both a username and password when supplying a user in windows"
38
+ end
39
+ end
40
+ end
41
+
42
+ #--
43
+ # Missing lots of features from the UNIX version, such as
44
+ # uid, etc.
45
+ def run_command
46
+ #
47
+ # Create pipes to capture stdout and stderr,
48
+ #
49
+ stdout_read, stdout_write = IO.pipe
50
+ stderr_read, stderr_write = IO.pipe
51
+ stdin_read, stdin_write = IO.pipe
52
+ open_streams = [ stdout_read, stderr_read ]
53
+
54
+ begin
55
+
56
+ #
57
+ # Set cwd, environment, appname, etc.
58
+ #
59
+ app_name, command_line = command_to_run(self.command)
60
+ create_process_args = {
61
+ :app_name => app_name,
62
+ :command_line => command_line,
63
+ :startup_info => {
64
+ :stdout => stdout_write,
65
+ :stderr => stderr_write,
66
+ :stdin => stdin_read
67
+ },
68
+ :environment => inherit_environment.map { |k,v| "#{k}=#{v}" },
69
+ :close_handles => false
70
+ }
71
+ create_process_args[:cwd] = cwd if cwd
72
+ # default to local account database if domain is not specified
73
+ create_process_args[:domain] = domain.nil? ? "." : domain
74
+ create_process_args[:with_logon] = with_logon if with_logon
75
+ create_process_args[:password] = password if password
76
+
77
+ #
78
+ # Start the process
79
+ #
80
+ process = Process.create(create_process_args)
81
+ logger.debug(Utils.format_process(process, app_name, command_line, timeout)) if logger
82
+ begin
83
+ # Start pushing data into input
84
+ stdin_write << input if input
85
+
86
+ # Close pipe to kick things off
87
+ stdin_write.close
88
+
89
+ #
90
+ # Wait for the process to finish, consuming output as we go
91
+ #
92
+ start_wait = Time.now
93
+ while true
94
+ wait_status = WaitForSingleObject(process.process_handle, 0)
95
+ case wait_status
96
+ when WAIT_OBJECT_0
97
+ # Get process exit code
98
+ exit_code = [0].pack('l')
99
+ unless GetExitCodeProcess(process.process_handle, exit_code)
100
+ raise get_last_error
101
+ end
102
+ @status = ThingThatLooksSortOfLikeAProcessStatus.new
103
+ @status.exitstatus = exit_code.unpack('l').first
104
+
105
+ return self
106
+ when WAIT_TIMEOUT
107
+ # Kill the process
108
+ if (Time.now - start_wait) > timeout
109
+ begin
110
+ require 'wmi-lite/wmi'
111
+ wmi = WmiLite::Wmi.new
112
+ Utils.kill_process_tree(process.process_id, wmi, logger)
113
+ Process.kill(:KILL, process.process_id)
114
+ rescue Errno::EIO, SystemCallError
115
+ logger.warn("Failed to kill timed out process #{process.process_id}") if logger
116
+ end
117
+
118
+ raise Mixlib::ShellOut::CommandTimeout, [
119
+ "command timed out:",
120
+ format_for_exception,
121
+ Utils.format_process(process, app_name, command_line, timeout)
122
+ ].join("\n")
123
+ end
124
+
125
+ consume_output(open_streams, stdout_read, stderr_read)
126
+ else
127
+ raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout*1000}): #{wait_status}"
128
+ end
129
+
130
+ end
131
+
132
+ ensure
133
+ CloseHandle(process.thread_handle) if process.thread_handle
134
+ CloseHandle(process.process_handle) if process.process_handle
135
+ end
136
+
137
+ ensure
138
+ #
139
+ # Consume all remaining data from the pipes until they are closed
140
+ #
141
+ stdout_write.close
142
+ stderr_write.close
143
+
144
+ while consume_output(open_streams, stdout_read, stderr_read)
145
+ end
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ class ThingThatLooksSortOfLikeAProcessStatus
152
+ attr_accessor :exitstatus
153
+ def success?
154
+ exitstatus == 0
155
+ end
156
+ end
157
+
158
+ def consume_output(open_streams, stdout_read, stderr_read)
159
+ return false if open_streams.length == 0
160
+ ready = IO.select(open_streams, nil, nil, READ_WAIT_TIME)
161
+ return true if ! ready
162
+
163
+ if ready.first.include?(stdout_read)
164
+ begin
165
+ next_chunk = stdout_read.readpartial(READ_SIZE)
166
+ @stdout << next_chunk
167
+ @live_stdout << next_chunk if @live_stdout
168
+ rescue EOFError
169
+ stdout_read.close
170
+ open_streams.delete(stdout_read)
171
+ end
172
+ end
173
+
174
+ if ready.first.include?(stderr_read)
175
+ begin
176
+ next_chunk = stderr_read.readpartial(READ_SIZE)
177
+ @stderr << next_chunk
178
+ @live_stderr << next_chunk if @live_stderr
179
+ rescue EOFError
180
+ stderr_read.close
181
+ open_streams.delete(stderr_read)
182
+ end
183
+ end
184
+
185
+ return true
186
+ end
187
+
188
+ IS_BATCH_FILE = /\.bat"?$|\.cmd"?$/i
189
+
190
+ def command_to_run(command)
191
+ return _run_under_cmd(command) if Utils.should_run_under_cmd?(command)
192
+
193
+ candidate = candidate_executable_for_command(command)
194
+
195
+ # Don't do searching for empty commands. Let it fail when it runs.
196
+ return [ nil, command ] if candidate.length == 0
197
+
198
+ # Check if the exe exists directly. Otherwise, search PATH.
199
+ exe = Utils.find_executable(candidate)
200
+ exe = Utils.which(unquoted_executable_path(command)) if exe.nil? && exe !~ /[\\\/]/
201
+
202
+ # Batch files MUST use cmd; and if we couldn't find the command we're looking for,
203
+ # we assume it must be a cmd builtin.
204
+ if exe.nil? || exe =~ IS_BATCH_FILE
205
+ _run_under_cmd(command)
206
+ else
207
+ _run_directly(command, exe)
208
+ end
209
+ end
210
+
211
+ # cmd does not parse multiple quotes well unless the whole thing is wrapped up in quotes.
212
+ # https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4837859
213
+ # http://ss64.com/nt/syntax-esc.html
214
+ def _run_under_cmd(command)
215
+ [ ENV['COMSPEC'], "cmd /c \"#{command}\"" ]
216
+ end
217
+
218
+ def _run_directly(command, exe)
219
+ [ exe, command ]
220
+ end
221
+
222
+ def unquoted_executable_path(command)
223
+ command[0,command.index(/\s/) || command.length]
224
+ end
225
+
226
+ def candidate_executable_for_command(command)
227
+ if command =~ /^\s*"(.*?)"/
228
+ # If we have quotes, do an exact match
229
+ $1
230
+ else
231
+ # Otherwise check everything up to the first space
232
+ unquoted_executable_path(command).strip
233
+ end
234
+ end
235
+
236
+ def inherit_environment
237
+ result = {}
238
+ ENV.each_pair do |k,v|
239
+ result[k] = v
240
+ end
241
+
242
+ environment.each_pair do |k,v|
243
+ if v == nil
244
+ result.delete(k)
245
+ else
246
+ result[k] = v
247
+ end
248
+ end
249
+ result
250
+ end
251
+
252
+ module Utils
253
+ # api: semi-private
254
+ # If there are special characters parsable by cmd.exe (such as file redirection), then
255
+ # this method should return true.
256
+ #
257
+ # This parser is based on
258
+ # https://github.com/ruby/ruby/blob/9073db5cb1d3173aff62be5b48d00f0fb2890991/win32/win32.c#L1437
259
+ def self.should_run_under_cmd?(command)
260
+ return true if command =~ /^@/
261
+
262
+ quote = nil
263
+ env = false
264
+ env_first_char = false
265
+
266
+ command.dup.each_char do |c|
267
+ case c
268
+ when "'", '"'
269
+ if (!quote)
270
+ quote = c
271
+ elsif quote == c
272
+ quote = nil
273
+ end
274
+ next
275
+ when '>', '<', '|', '&', "\n"
276
+ return true unless quote
277
+ when '%'
278
+ return true if env
279
+ env = env_first_char = true
280
+ next
281
+ else
282
+ next unless env
283
+ if env_first_char
284
+ env_first_char = false
285
+ env = false and next if c !~ /[A-Za-z_]/
286
+ end
287
+ env = false if c !~ /[A-Za-z1-9_]/
288
+ end
289
+ end
290
+ return false
291
+ end
292
+
293
+ def self.pathext
294
+ @pathext ||= ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : ['']
295
+ end
296
+
297
+ # which() mimicks the Unix which command
298
+ # FIXME: it is not working
299
+ def self.which(cmd)
300
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
301
+ exe = find_executable("#{path}/#{cmd}")
302
+ return exe if exe
303
+ end
304
+ return nil
305
+ end
306
+
307
+ # Windows has a different notion of what "executable" means
308
+ # The OS will search through valid the extensions and look
309
+ # for a binary there.
310
+ def self.find_executable(path)
311
+ return path if executable? path
312
+
313
+ pathext.each do |ext|
314
+ exe = "#{path}#{ext}"
315
+ return exe if executable? exe
316
+ end
317
+ return nil
318
+ end
319
+
320
+ def self.executable?(path)
321
+ File.executable?(path) && !File.directory?(path)
322
+ end
323
+
324
+ # recursively kills all child processes of given pid
325
+ # calls itself querying for children child procs until
326
+ # none remain. Important that a single WmiLite instance
327
+ # is passed in since each creates its own WMI rpc process
328
+ def self.kill_process_tree(pid, wmi, logger)
329
+ wmi.query("select * from Win32_Process where ParentProcessID=#{pid}").each do |instance|
330
+ child_pid = instance.wmi_ole_object.processid
331
+ kill_process_tree(child_pid, wmi, logger)
332
+ begin
333
+ logger.debug([
334
+ "killing child process #{child_pid}::",
335
+ "#{instance.wmi_ole_object.Name} of parent #{pid}"
336
+ ].join) if logger
337
+ kill_process(instance)
338
+ rescue Errno::EIO, SystemCallError
339
+ logger.debug([
340
+ "Failed to kill child process #{child_pid}::",
341
+ "#{instance.wmi_ole_object.Name} of parent #{pid}"
342
+ ].join) if logger
343
+ end
344
+ end
345
+ end
346
+
347
+ def self.kill_process(instance)
348
+ Process.kill(:KILL, instance.wmi_ole_object.processid)
349
+ end
350
+
351
+ def self.format_process(process, app_name, command_line, timeout)
352
+ msg = []
353
+ msg << "ProcessId: #{process.process_id}"
354
+ msg << "app_name: #{app_name}"
355
+ msg << "command_line: #{command_line}"
356
+ msg << "timeout: #{timeout}"
357
+ msg.join("\n")
358
+ end
359
+ end
360
+ end # class
361
+ end
362
+ end