scout-essentials 1.8.7 → 1.9.0
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/.vimproject +26 -12
- data/README.md +83 -112
- data/VERSION +1 -1
- data/doc/Improvements.md +226 -0
- data/doc/StartHere.md +122 -0
- data/doc/developer/AnnotationSystem.md +184 -0
- data/doc/developer/Architecture.md +147 -0
- data/doc/developer/Configuration.md +238 -0
- data/doc/developer/CoreUtilities.md +265 -0
- data/doc/developer/DesignPrinciples.md +129 -0
- data/doc/developer/ErrorHandling.md +203 -0
- data/doc/developer/LockingAndConcurrency.md +157 -0
- data/doc/developer/PathResolution.md +200 -0
- data/doc/developer/PersistenceAndResources.md +119 -0
- data/doc/developer/StreamingModel.md +236 -0
- data/doc/user/AnnotatingData.md +202 -0
- data/doc/user/CachingResults.md +183 -0
- data/doc/user/CommandLineOptions.md +189 -0
- data/doc/user/Cookbook.md +211 -0
- data/doc/user/HandlingStreams.md +236 -0
- data/doc/user/LoggingAndProgress.md +158 -0
- data/doc/user/ProducingResources.md +177 -0
- data/doc/user/RemoteData.md +157 -0
- data/doc/user/RunningCommands.md +218 -0
- data/doc/user/WorkingWithFiles.md +217 -0
- data/lib/scout/cmd.rb +343 -40
- data/lib/scout/concurrent_stream.rb +14 -1
- data/lib/scout/indiferent_hash.rb +1 -1
- data/lib/scout/log/fingerprint.rb +13 -8
- data/lib/scout/log/progress/report.rb +1 -1
- data/lib/scout/log.rb +4 -1
- data/lib/scout/misc/digest.rb +6 -5
- data/lib/scout/misc/format.rb +24 -0
- data/lib/scout/named_array.rb +1 -1
- data/lib/scout/open/stream.rb +2 -2
- data/lib/scout/open/util.rb +8 -4
- data/lib/scout/open.rb +3 -3
- data/lib/scout/path/find.rb +3 -2
- data/lib/scout/persist.rb +14 -10
- data/lib/scout/resource/produce.rb +9 -1
- data/research/annotations-data-analysis.md +206 -0
- data/research/behavior-probes.md +1925 -0
- data/research/commands-streaming-analysis.md +272 -0
- data/research/design-philosophy-analysis.md +383 -0
- data/research/doc-audit-findings.md +294 -0
- data/research/ecosystem-attribution.md +118 -0
- data/research/implementation-inventory-core.md +1029 -0
- data/research/implementation-inventory-open.md +417 -0
- data/research/implementation-inventory-path-persist-resource.md +774 -0
- data/research/io-paths-analysis.md +228 -0
- data/research/persistence-resources-analysis.md +244 -0
- data/research/synthesis-report.md +80 -0
- data/scout-essentials.gemspec +37 -15
- data/test/scout/open/test_remote.rb +1 -2
- data/test/scout/test_cmd.rb +411 -0
- metadata +36 -14
- data/doc/Annotation.md +0 -352
- data/doc/CMD.md +0 -363
- data/doc/ConcurrentStream.md +0 -163
- data/doc/IndiferentHash.md +0 -240
- data/doc/Log.md +0 -235
- data/doc/NamedArray.md +0 -174
- data/doc/Open.md +0 -331
- data/doc/Path.md +0 -217
- data/doc/Persist.md +0 -214
- data/doc/Resource.md +0 -229
- data/doc/SimpleOPT.md +0 -236
- data/doc/TmpFile.md +0 -154
data/lib/scout/cmd.rb
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
require_relative 'indiferent_hash'
|
|
2
2
|
require_relative 'concurrent_stream'
|
|
3
3
|
require_relative 'log'
|
|
4
|
+
require_relative 'exceptions'
|
|
4
5
|
require_relative 'open/stream'
|
|
5
6
|
require 'stringio'
|
|
6
7
|
require 'open3'
|
|
8
|
+
require 'fileutils'
|
|
7
9
|
|
|
8
10
|
module CMD
|
|
9
11
|
|
|
12
|
+
# Grace period, in seconds, between sending :INT and escalating to :KILL
|
|
13
|
+
# when a command exceeds its :timeout option
|
|
14
|
+
TIMEOUT_KILL_GRACE = 1.0
|
|
15
|
+
|
|
16
|
+
# ScoutCoder: exception raised by CMD.cmd when a command exceeds its
|
|
17
|
+
# :timeout option. It subclasses ProcessFailed, so generic rescue clauses
|
|
18
|
+
# for ProcessFailed also catch it. In pipe mode the stream is aborted with
|
|
19
|
+
# this exception (ConcurrentStream#abort), so consumers observe it through
|
|
20
|
+
# ConcurrentStream#stream_raise_exception when reading or joining the
|
|
21
|
+
# returned stream.
|
|
22
|
+
class Timeout < ProcessFailed
|
|
23
|
+
attr_reader :command, :timeout
|
|
24
|
+
def initialize(pid = Process.pid, command = nil, timeout = nil)
|
|
25
|
+
@command = command
|
|
26
|
+
@timeout = timeout
|
|
27
|
+
super(pid, "command '#{command}' exceeded timeout of #{timeout} seconds")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
10
31
|
TOOLS = IndiferentHash.setup({})
|
|
11
32
|
def self.tool(tool, claim = nil, test = nil, cmd = nil, &block)
|
|
12
33
|
TOOLS[tool] = [claim, test, block, cmd]
|
|
@@ -124,6 +145,36 @@ module CMD
|
|
|
124
145
|
string.strip
|
|
125
146
|
end
|
|
126
147
|
|
|
148
|
+
# ScoutCoder: process_cmd_options_array mirrors process_cmd_options but
|
|
149
|
+
# returns an array of separate argument strings instead of a single shell
|
|
150
|
+
# string, suitable for the no-shell array form of Open3.popen3
|
|
151
|
+
def self.process_cmd_options_array(options = {})
|
|
152
|
+
add_dashes = IndiferentHash.process_options options, :add_option_dashes
|
|
153
|
+
|
|
154
|
+
result = []
|
|
155
|
+
options.each do |option, value|
|
|
156
|
+
raise "Invalid option key: #{option.inspect}" if option.to_s !~ /^[a-z_0-9\-=.]+$/i
|
|
157
|
+
|
|
158
|
+
option = "--" << option.to_s if add_dashes and option.to_s[0] != '-'
|
|
159
|
+
|
|
160
|
+
case
|
|
161
|
+
when value.nil? || FalseClass === value
|
|
162
|
+
next
|
|
163
|
+
when TrueClass === value
|
|
164
|
+
result << option.to_s
|
|
165
|
+
else
|
|
166
|
+
if option.to_s.chars.to_a.last == "="
|
|
167
|
+
result << "#{option}#{value}"
|
|
168
|
+
else
|
|
169
|
+
result << option.to_s
|
|
170
|
+
result << value.to_s
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
result
|
|
176
|
+
end
|
|
177
|
+
|
|
127
178
|
def self.cmd(tool, cmd = nil, options = {}, &block)
|
|
128
179
|
options, cmd = cmd, nil if Hash === cmd
|
|
129
180
|
|
|
@@ -140,65 +191,226 @@ module CMD
|
|
|
140
191
|
xvfb = options.delete(:xvfb)
|
|
141
192
|
bar = options.delete(:progress_bar)
|
|
142
193
|
save_stderr = options.delete(:save_stderr)
|
|
194
|
+
|
|
195
|
+
# :save_stderr also accepts a file path (String, Scout Path or Pathname)
|
|
196
|
+
# or an IO-like object (anything that responds to :write). A path is
|
|
197
|
+
# opened here for writing (truncating, creating missing parent
|
|
198
|
+
# directories) and CMD owns it: it is closed when the command ends. An
|
|
199
|
+
# IO-like object is used as-is and never closed by CMD. In addition to
|
|
200
|
+
# writing the destination, std_err keeps accumulating (String and Path are
|
|
201
|
+
# truthy, so the existing `if save_stderr` behaviour covers this)
|
|
202
|
+
save_stderr_dst = nil
|
|
203
|
+
save_stderr_close = false
|
|
204
|
+
if save_stderr && ! (TrueClass === save_stderr || FalseClass === save_stderr)
|
|
205
|
+
if String === save_stderr || Pathname === save_stderr
|
|
206
|
+
path = save_stderr
|
|
207
|
+
path = path.find if Path === path
|
|
208
|
+
path = path.to_s unless String === path
|
|
209
|
+
dir = File.dirname(path)
|
|
210
|
+
FileUtils.mkdir_p dir unless File.exist?(dir)
|
|
211
|
+
save_stderr_dst = File.open(path, 'w')
|
|
212
|
+
save_stderr_close = true
|
|
213
|
+
elsif save_stderr.respond_to?(:write) || save_stderr.respond_to?(:<<)
|
|
214
|
+
save_stderr_dst = save_stderr
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
143
218
|
autojoin = options.delete(:autojoin)
|
|
144
219
|
autojoin = no_wait if autojoin.nil?
|
|
220
|
+
timeout = options.delete(:timeout)
|
|
145
221
|
|
|
146
222
|
dont_close_in = options.delete(:dont_close_in)
|
|
147
223
|
|
|
148
224
|
log = true if log.nil?
|
|
149
225
|
|
|
150
|
-
|
|
151
|
-
|
|
226
|
+
array_mode = Array === tool
|
|
227
|
+
|
|
228
|
+
if array_mode
|
|
229
|
+
cmd_array = tool.dup
|
|
230
|
+
cmd_array << cmd if cmd.is_a?(String) && !cmd.empty?
|
|
231
|
+
|
|
232
|
+
case xvfb
|
|
233
|
+
when TrueClass
|
|
234
|
+
cmd_array = ["xvfb-run", "--server-args=-screen 0 1024x768x24", "--auto-servernum"] + cmd_array
|
|
235
|
+
when String
|
|
236
|
+
cmd_array = ["xvfb-run", "--server-args=#{xvfb}", "--auto-servernum", "--server-num=1"] + cmd_array
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
if stderr == true
|
|
240
|
+
stderr = Log::HIGH
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
cmd_array += process_cmd_options_array options
|
|
244
|
+
|
|
245
|
+
cmd_array = ["sudo"] + cmd_array if sudo
|
|
246
|
+
|
|
247
|
+
# Build cmd string for logging/error messages
|
|
248
|
+
cmd = cmd_array.map { |e| e.to_s.include?(' ') ? "'#{e}'" : e.to_s }.join(' ')
|
|
152
249
|
else
|
|
153
|
-
|
|
154
|
-
if cmd.nil?
|
|
250
|
+
if cmd.nil? && ! Symbol === tool
|
|
155
251
|
cmd = tool
|
|
156
252
|
else
|
|
157
|
-
|
|
158
|
-
|
|
253
|
+
tool = get_tool(tool)
|
|
254
|
+
if cmd.nil?
|
|
255
|
+
cmd = tool
|
|
256
|
+
else
|
|
257
|
+
cmd = tool + ' ' + cmd
|
|
258
|
+
end
|
|
159
259
|
|
|
160
|
-
|
|
260
|
+
end
|
|
161
261
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
262
|
+
case xvfb
|
|
263
|
+
when TrueClass
|
|
264
|
+
cmd = "xvfb-run --server-args='-screen 0 1024x768x24' --auto-servernum #{cmd}"
|
|
265
|
+
when String
|
|
266
|
+
cmd = "xvfb-run --server-args='#{xvfb}' --auto-servernum --server-num=1 #{cmd}"
|
|
267
|
+
when String
|
|
268
|
+
end
|
|
169
269
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
270
|
+
if stderr == true
|
|
271
|
+
stderr = Log::HIGH
|
|
272
|
+
end
|
|
173
273
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
274
|
+
cmd_options = process_cmd_options options
|
|
275
|
+
if cmd =~ /'\{opt\}'/
|
|
276
|
+
cmd = cmd.sub('\'{opt}\'', cmd_options)
|
|
277
|
+
else
|
|
278
|
+
cmd += " " + cmd_options
|
|
279
|
+
end
|
|
180
280
|
|
|
181
|
-
|
|
182
|
-
|
|
281
|
+
if sudo
|
|
282
|
+
cmd = "sudo " + cmd
|
|
283
|
+
end
|
|
183
284
|
end
|
|
184
285
|
|
|
185
286
|
in_content = StringIO.new in_content if String === in_content
|
|
186
287
|
|
|
187
288
|
sin, sout, serr, wait_thr = begin
|
|
188
|
-
|
|
289
|
+
if array_mode
|
|
290
|
+
Open3.popen3(ENV, *cmd_array)
|
|
291
|
+
else
|
|
292
|
+
Open3.popen3(ENV, cmd)
|
|
293
|
+
end
|
|
189
294
|
rescue
|
|
190
295
|
Log.warn $!.message
|
|
296
|
+
# The command never started; do not leak the
|
|
297
|
+
# file opened for it above
|
|
298
|
+
save_stderr_dst.close if save_stderr_dst && save_stderr_close
|
|
191
299
|
raise ProcessFailed, nil, cmd unless no_fail
|
|
192
300
|
return
|
|
193
301
|
end
|
|
194
302
|
pid = wait_thr.pid
|
|
195
303
|
|
|
196
|
-
Log.
|
|
304
|
+
if Log.severity >= 2
|
|
305
|
+
Log.medium("CMD [#{pid}] #{Log.fingerprint(cmd)}")
|
|
306
|
+
else
|
|
307
|
+
Log.debug("CMD [#{pid}] #{cmd.strip}")
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# ScoutCoder: :timeout support. A watchdog thread starts a counter
|
|
311
|
+
# (deadline) for the command; if the process is still running when the
|
|
312
|
+
# timeout is reached it kills it and raises CMD::Timeout.
|
|
313
|
+
#
|
|
314
|
+
# In pipe mode the exception is routed through ConcurrentStream#abort:
|
|
315
|
+
# it is stored in stream_exception, the stream is aborted (which
|
|
316
|
+
# interrupts any consumer blocked reading from it, kills the process and
|
|
317
|
+
# clears the pid list) and it is re-raised when the consumer reads or
|
|
318
|
+
# joins the stream. In non-pipe mode the watchdog raises directly in the
|
|
319
|
+
# thread running CMD.cmd, unblocking its read, and the internal stream is
|
|
320
|
+
# aborted in the rescue below.
|
|
321
|
+
#
|
|
322
|
+
# The watchdog is deliberately NOT registered in sout.threads, so it never
|
|
323
|
+
# delays a normal join, and it always ends up reaping the pid (see
|
|
324
|
+
# reap_timeout_pid below) to avoid leaving a zombie behind when the
|
|
325
|
+
# Process::Waiter thread is interrupted by the abort.
|
|
326
|
+
timeout_thread = nil
|
|
327
|
+
timeout_exception = nil
|
|
328
|
+
timed_out = false
|
|
329
|
+
if Numeric === timeout && timeout > 0
|
|
330
|
+
timeout_exception = Timeout.new(pid, cmd, timeout)
|
|
331
|
+
caller_thread = Thread.current
|
|
332
|
+
|
|
333
|
+
# The helper threads are interrupted with the timeout exception when
|
|
334
|
+
# the command is killed; do not report that as a thread crash
|
|
335
|
+
wait_thr.report_on_exception = false
|
|
336
|
+
|
|
337
|
+
# Kill the process: :INT first, escalate to :KILL after a grace period,
|
|
338
|
+
# and reap it so that no zombie is left behind. Works whether or not
|
|
339
|
+
# the Process::Waiter thread is still alive: whoever waits first reaps
|
|
340
|
+
# the pid and the other one gets Errno::ECHILD.
|
|
341
|
+
reap_timeout_pid = Proc.new do
|
|
342
|
+
begin
|
|
343
|
+
Process.kill(:INT, pid)
|
|
344
|
+
rescue Errno::ESRCH
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
grace_end = Time.now + TIMEOUT_KILL_GRACE
|
|
348
|
+
reaped = false
|
|
349
|
+
while Time.now < grace_end
|
|
350
|
+
begin
|
|
351
|
+
reaped = ! Process.waitpid(pid, Process::WNOHANG).nil?
|
|
352
|
+
break if reaped
|
|
353
|
+
rescue Errno::ECHILD
|
|
354
|
+
reaped = true
|
|
355
|
+
break
|
|
356
|
+
end
|
|
357
|
+
sleep 0.05
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
unless reaped
|
|
361
|
+
begin
|
|
362
|
+
Process.kill(:KILL, pid)
|
|
363
|
+
rescue Errno::ESRCH
|
|
364
|
+
end
|
|
365
|
+
begin
|
|
366
|
+
Process.waitpid(pid)
|
|
367
|
+
rescue Errno::ECHILD
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
timeout_thread = Thread.new do
|
|
373
|
+
Thread.current["name"] = "CMD timeout: [#{pid}] #{cmd}"
|
|
374
|
+
Thread.current.report_on_exception = false
|
|
375
|
+
|
|
376
|
+
begin
|
|
377
|
+
# Counter: deadline of the command, measured from spawn time. The
|
|
378
|
+
# extra join(0) discards the boundary race in which the process
|
|
379
|
+
# finished exactly at the deadline
|
|
380
|
+
if wait_thr.join(timeout).nil? && wait_thr.join(0).nil?
|
|
381
|
+
timed_out = true
|
|
382
|
+
Log.low "CMD: [#{pid}] #{cmd} timed out after #{timeout} seconds. Killing"
|
|
383
|
+
|
|
384
|
+
if pipe
|
|
385
|
+
# Store the exception and abort the stream BEFORE the pid is
|
|
386
|
+
# gone, so that consumers blocked on a read are interrupted and
|
|
387
|
+
# get the exception instead of a plain EOF. abort (rather than
|
|
388
|
+
# stream_raise_exception) is used so the helper threads are
|
|
389
|
+
# interrupted with the timeout exception itself: if they died
|
|
390
|
+
# with any other exception, joining them would overwrite
|
|
391
|
+
# stream_exception and hide the timeout
|
|
392
|
+
sout.abort timeout_exception if sout.respond_to?(:abort) && ! sout.joined?
|
|
393
|
+
reap_timeout_pid.call
|
|
394
|
+
else
|
|
395
|
+
# Unblock the thread running CMD.cmd immediately; the kill and
|
|
396
|
+
# the clean-up of the internal stream happen in its rescue path
|
|
397
|
+
# and in reap_timeout_pid below
|
|
398
|
+
caller_thread.raise timeout_exception
|
|
399
|
+
reap_timeout_pid.call
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
rescue Exception
|
|
403
|
+
# Never let the watchdog crash the process
|
|
404
|
+
Log.exception $!
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
Thread.pass until timeout_thread["name"]
|
|
408
|
+
end
|
|
197
409
|
|
|
198
410
|
if in_content.respond_to?(:read)
|
|
199
411
|
in_thread = Thread.new(Thread.current) do |parent|
|
|
200
412
|
begin
|
|
201
|
-
Thread.current.report_on_exception = false if no_fail
|
|
413
|
+
Thread.current.report_on_exception = false if no_fail || timeout_exception
|
|
202
414
|
Thread.current["name"] = "CMD in"
|
|
203
415
|
while c = in_content.read(Open::BLOCK_SIZE)
|
|
204
416
|
sin << c
|
|
@@ -213,7 +425,9 @@ module CMD
|
|
|
213
425
|
in_content.join if in_content.respond_to? :join
|
|
214
426
|
end
|
|
215
427
|
rescue Exception
|
|
216
|
-
|
|
428
|
+
unless no_fail || Aborted === $! || Timeout === $!
|
|
429
|
+
Log.error "Error in CMD [#{pid}] #{cmd}: #{$!.message}"
|
|
430
|
+
end
|
|
217
431
|
sin.close unless sin.closed?
|
|
218
432
|
sin.join if sin.respond_to? :join
|
|
219
433
|
raise $!
|
|
@@ -233,20 +447,45 @@ module CMD
|
|
|
233
447
|
|
|
234
448
|
sout.callback = post if post
|
|
235
449
|
|
|
236
|
-
if (Integer === stderr and log) || bar
|
|
450
|
+
if (Integer === stderr and log) || bar || save_stderr_dst
|
|
237
451
|
err_thread = Thread.new do
|
|
238
452
|
Thread.current["name"] = "Error log: [#{pid}] #{ cmd }"
|
|
453
|
+
Thread.current.report_on_exception = false
|
|
239
454
|
begin
|
|
240
455
|
while line = serr.gets
|
|
241
456
|
bar.process(line) if bar
|
|
242
457
|
sout.log = line
|
|
243
458
|
sout.std_err << line if save_stderr
|
|
459
|
+
# Write each line to the destination as soon as it arrives and
|
|
460
|
+
# flush, so that a `tail -f` on the file follows the progress
|
|
461
|
+
# of the command while the pipe is still being consumed
|
|
462
|
+
if save_stderr_dst
|
|
463
|
+
save_stderr_dst << line
|
|
464
|
+
save_stderr_dst.flush if save_stderr_dst.respond_to?(:flush)
|
|
465
|
+
end
|
|
244
466
|
Log.log "STDERR [#{pid}]: " + line, stderr if log
|
|
245
467
|
end
|
|
246
468
|
serr.close
|
|
469
|
+
rescue Aborted
|
|
470
|
+
# The stream was aborted (e.g. by a :timeout): stop silently, the
|
|
471
|
+
# consumer of sout gets the real exception from the stream. The
|
|
472
|
+
# destination is flushed and closed, whatever was written so far
|
|
473
|
+
# is kept in place (the log file is not removed)
|
|
474
|
+
serr.close unless serr.closed?
|
|
247
475
|
rescue
|
|
248
|
-
|
|
476
|
+
# When the stream is being aborted (e.g. by a :timeout) the
|
|
477
|
+
# consumer gets the real exception from the stream itself
|
|
478
|
+
Log.exception $! unless sout.aborted?
|
|
249
479
|
raise $!
|
|
480
|
+
ensure
|
|
481
|
+
# No fd leak: CMD closes what it opened (a caller provided IO is
|
|
482
|
+
# only flushed, closing is up to the caller)
|
|
483
|
+
begin
|
|
484
|
+
save_stderr_dst.flush if save_stderr_dst && save_stderr_dst.respond_to?(:flush) && ! save_stderr_dst.closed?
|
|
485
|
+
rescue Exception
|
|
486
|
+
Log.exception $!
|
|
487
|
+
end
|
|
488
|
+
save_stderr_dst.close if save_stderr_dst && save_stderr_close && ! save_stderr_dst.closed?
|
|
250
489
|
end
|
|
251
490
|
end
|
|
252
491
|
else
|
|
@@ -260,19 +499,45 @@ module CMD
|
|
|
260
499
|
err = ""
|
|
261
500
|
if bar
|
|
262
501
|
err_thread = Thread.new do
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
502
|
+
Thread.current.report_on_exception = false
|
|
503
|
+
begin
|
|
504
|
+
while not serr.eof?
|
|
505
|
+
line = serr.gets
|
|
506
|
+
bar.process(line)
|
|
507
|
+
err << line if save_stderr || (Integer === stderr and log)
|
|
508
|
+
end
|
|
509
|
+
serr.close
|
|
510
|
+
rescue Exception
|
|
511
|
+
# Interrupted while the stream is aborted (e.g. by a :timeout);
|
|
512
|
+
# the CMD.cmd thread surfaces the real exception
|
|
513
|
+
serr.close unless serr.closed?
|
|
267
514
|
end
|
|
268
|
-
serr.close
|
|
269
515
|
end
|
|
270
516
|
elsif log and Integer === stderr
|
|
271
517
|
err_thread = Thread.new do
|
|
272
|
-
|
|
273
|
-
|
|
518
|
+
Thread.current.report_on_exception = false
|
|
519
|
+
begin
|
|
520
|
+
while not serr.eof?
|
|
521
|
+
err += serr.gets
|
|
522
|
+
end
|
|
523
|
+
serr.close
|
|
524
|
+
rescue Exception
|
|
525
|
+
serr.close unless serr.closed?
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
elsif save_stderr_dst
|
|
529
|
+
# Without log level and bar stderr would be discarded; accumulate it
|
|
530
|
+
# so the destination never ends up silently empty
|
|
531
|
+
err_thread = Thread.new do
|
|
532
|
+
Thread.current.report_on_exception = false
|
|
533
|
+
begin
|
|
534
|
+
while not serr.eof?
|
|
535
|
+
err += serr.gets
|
|
536
|
+
end
|
|
537
|
+
serr.close
|
|
538
|
+
rescue Exception
|
|
539
|
+
serr.close unless serr.closed?
|
|
274
540
|
end
|
|
275
|
-
serr.close
|
|
276
541
|
end
|
|
277
542
|
else
|
|
278
543
|
Open.consume_stream(serr, true)
|
|
@@ -285,11 +550,35 @@ module CMD
|
|
|
285
550
|
out = StringIO.new sout.read
|
|
286
551
|
status = wait_thr.value
|
|
287
552
|
|
|
553
|
+
# Settle the watchdog decision: if the process finished right at the
|
|
554
|
+
# deadline the watchdog may still be deciding, so wait for it before
|
|
555
|
+
# reporting success. A real timeout raises directly in this thread
|
|
556
|
+
# and is handled by the rescue below.
|
|
557
|
+
timeout_thread.join if timeout_thread
|
|
558
|
+
|
|
559
|
+
# A timeout was detected while waiting; the exception wins over any
|
|
560
|
+
# exit-status error of the killed process
|
|
561
|
+
raise timeout_exception if timed_out
|
|
562
|
+
|
|
288
563
|
sout.join
|
|
289
564
|
sout.close unless sout.closed?
|
|
290
565
|
sout.annotate(out)
|
|
291
566
|
|
|
292
567
|
out.exit_status = status.exitstatus
|
|
568
|
+
|
|
569
|
+
# Save stderr before reporting a failure: the ProcessFailed message
|
|
570
|
+
# embeds it and the destination must keep it, even when the command
|
|
571
|
+
# fails (sout.join above already filled err completely)
|
|
572
|
+
out.std_err = err if save_stderr
|
|
573
|
+
if save_stderr_dst
|
|
574
|
+
begin
|
|
575
|
+
save_stderr_dst << err unless err.empty?
|
|
576
|
+
save_stderr_dst.flush if save_stderr_dst.respond_to?(:flush) && ! save_stderr_dst.closed?
|
|
577
|
+
rescue Exception
|
|
578
|
+
Log.exception $!
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
|
|
293
582
|
if status && ! status.success? && ! no_fail
|
|
294
583
|
if !err.empty?
|
|
295
584
|
raise ProcessFailed.new pid, "#{cmd} failed with error status #{status.exitstatus}.\n#{err}"
|
|
@@ -299,9 +588,23 @@ module CMD
|
|
|
299
588
|
else
|
|
300
589
|
Log.log err, stderr if Integer === stderr and log
|
|
301
590
|
end
|
|
302
|
-
out.std_err = err if save_stderr
|
|
303
591
|
out
|
|
592
|
+
rescue Timeout
|
|
593
|
+
# Abort the internal stream so pipes are closed, the input and error
|
|
594
|
+
# threads are interrupted and the pid list is cleared
|
|
595
|
+
sout.abort($!) unless sout.aborted?
|
|
596
|
+
raise $!
|
|
304
597
|
ensure
|
|
598
|
+
# No fd leak in the non-pipe path either: flush what CMD opened and
|
|
599
|
+
# close it (a caller provided IO stays open, closing is up to it)
|
|
600
|
+
if save_stderr_dst
|
|
601
|
+
begin
|
|
602
|
+
save_stderr_dst.flush if save_stderr_dst.respond_to?(:flush) && ! save_stderr_dst.closed?
|
|
603
|
+
rescue Exception
|
|
604
|
+
Log.exception $!
|
|
605
|
+
end
|
|
606
|
+
save_stderr_dst.close if save_stderr_close && ! save_stderr_dst.closed?
|
|
607
|
+
end
|
|
305
608
|
post.call if post
|
|
306
609
|
end
|
|
307
610
|
end
|
|
@@ -93,7 +93,14 @@ module ConcurrentStream
|
|
|
93
93
|
if ! (t.value.success? || no_fail)
|
|
94
94
|
|
|
95
95
|
if log
|
|
96
|
-
|
|
96
|
+
if std_err && log.length < 10 && !(log.downcase.include?("error") || log.downcase.include?('exception'))
|
|
97
|
+
exception_line = std_err.split("\n").reverse.find{|line| line.downcase.include?("error") || line.downcase.include?('exception') || line.include?('::') }
|
|
98
|
+
end
|
|
99
|
+
if exception_line
|
|
100
|
+
msg = "Error joining #{self.filename || self.inspect}. Exception line: #{exception_line}"
|
|
101
|
+
else
|
|
102
|
+
msg = "Error joining #{self.filename || self.inspect}. Last log line: #{log}"
|
|
103
|
+
end
|
|
97
104
|
else
|
|
98
105
|
msg = "Error joining #{self.filename || self.inspect}"
|
|
99
106
|
end
|
|
@@ -164,6 +171,7 @@ module ConcurrentStream
|
|
|
164
171
|
|
|
165
172
|
threads = @threads.dup
|
|
166
173
|
@threads.clear
|
|
174
|
+
aborted_threads = []
|
|
167
175
|
threads.each do |t|
|
|
168
176
|
next if t == Thread.current
|
|
169
177
|
next if t["aborted"]
|
|
@@ -171,6 +179,10 @@ module ConcurrentStream
|
|
|
171
179
|
exception = exception.nil? ? Aborted.new : exception
|
|
172
180
|
Log.debug "Aborting thread #{Log.fingerprint(t)} with exception: #{exception}"
|
|
173
181
|
t.raise(exception)
|
|
182
|
+
aborted_threads << t
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
aborted_threads.each do |t|
|
|
174
186
|
t.join
|
|
175
187
|
end
|
|
176
188
|
end
|
|
@@ -242,6 +254,7 @@ module ConcurrentStream
|
|
|
242
254
|
super(*args)
|
|
243
255
|
rescue Exception
|
|
244
256
|
@stream_exception ||= $!
|
|
257
|
+
self.abort
|
|
245
258
|
raise @stream_exception
|
|
246
259
|
ensure
|
|
247
260
|
if ! @stream_exception && autojoin && ! closed?
|
|
@@ -3,6 +3,18 @@ module Log
|
|
|
3
3
|
FP_MAX_STRING = 150
|
|
4
4
|
FP_MAX_ARRAY = 20
|
|
5
5
|
FP_MAX_HASH = 10
|
|
6
|
+
|
|
7
|
+
def self.truncate_string(string, max=FP_MAX_STRING)
|
|
8
|
+
if string.length > max
|
|
9
|
+
digest = Digest::MD5.hexdigest(string)
|
|
10
|
+
middle = "<...#{string.length} - #{digest[0..4]}...>"
|
|
11
|
+
s = (max - middle.length) / 2
|
|
12
|
+
string.slice(0,s-1) + middle + string.slice(-s, string.length )
|
|
13
|
+
else
|
|
14
|
+
string
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
6
18
|
def self.fingerprint(obj)
|
|
7
19
|
return obj.fingerprint if obj.respond_to?(:fingerprint)
|
|
8
20
|
|
|
@@ -17,14 +29,7 @@ module Log
|
|
|
17
29
|
":" + obj.to_s
|
|
18
30
|
when String
|
|
19
31
|
obj = obj.gsub("\n", '\n')
|
|
20
|
-
|
|
21
|
-
digest = Digest::MD5.hexdigest(obj)
|
|
22
|
-
middle = "<...#{obj.length} - #{digest[0..4]}...>"
|
|
23
|
-
s = (FP_MAX_STRING - middle.length) / 2
|
|
24
|
-
"'" + obj.slice(0,s-1) + middle + obj.slice(-s, obj.length ) + "'"
|
|
25
|
-
else
|
|
26
|
-
"'" + obj + "'"
|
|
27
|
-
end
|
|
32
|
+
"'" + Log.truncate_string(obj) + "'"
|
|
28
33
|
when ConcurrentStream
|
|
29
34
|
name = obj.inspect + " " + obj.object_id.to_s
|
|
30
35
|
name += " #{obj.filename}" if obj.filename
|
data/lib/scout/log.rb
CHANGED
|
@@ -359,7 +359,10 @@ def ppp(message)
|
|
|
359
359
|
stack = caller
|
|
360
360
|
puts "#{Log.color :cyan, "PRINT:"} " << stack.first
|
|
361
361
|
puts ""
|
|
362
|
-
|
|
362
|
+
begin
|
|
363
|
+
message = message.pretty_print if message.respond_to?(:pretty_print)
|
|
364
|
+
rescue
|
|
365
|
+
end
|
|
363
366
|
if message.length > 200 or message.include? "\n"
|
|
364
367
|
puts Log.color(:cyan, "=>|") << "\n" << message.to_s
|
|
365
368
|
else
|
data/lib/scout/misc/digest.rb
CHANGED
|
@@ -7,11 +7,12 @@ module Misc
|
|
|
7
7
|
else
|
|
8
8
|
case obj
|
|
9
9
|
when String
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
if Path.is_filename?(obj) && Open.exists?(obj)
|
|
11
|
+
@@digest_str_cache ||= {}
|
|
12
|
+
@@digest_str_cache[obj] ||= Path.setup(obj).digest_str
|
|
13
|
+
else
|
|
14
|
+
obj.dup
|
|
15
|
+
end
|
|
15
16
|
when Integer, Symbol
|
|
16
17
|
obj.to_s
|
|
17
18
|
when Array
|
data/lib/scout/misc/format.rb
CHANGED
|
@@ -212,6 +212,30 @@ module Misc
|
|
|
212
212
|
end
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
+
def self.human_number(n)
|
|
216
|
+
units = ["", "K", "M", "B", "T"]
|
|
217
|
+
|
|
218
|
+
sign = n < 0 ? "-" : ""
|
|
219
|
+
value = n.abs.to_f
|
|
220
|
+
unit = 0
|
|
221
|
+
|
|
222
|
+
while value >= 999.5 && unit < units.size - 1
|
|
223
|
+
value /= 1000.0
|
|
224
|
+
unit += 1
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
str =
|
|
228
|
+
if value >= 100
|
|
229
|
+
value.round.to_s
|
|
230
|
+
elsif value >= 10
|
|
231
|
+
value.round.to_s
|
|
232
|
+
else
|
|
233
|
+
("%.1f" % value).sub(/\.0$/, "")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
"#{sign}#{str}#{units[unit]}"
|
|
237
|
+
end
|
|
238
|
+
|
|
215
239
|
def self.parse_sql_values(txt)
|
|
216
240
|
io = StringIO.new txt.strip
|
|
217
241
|
|
data/lib/scout/named_array.rb
CHANGED
data/lib/scout/open/stream.rb
CHANGED
|
@@ -121,8 +121,8 @@ module Open
|
|
|
121
121
|
when (IO === content or StringIO === content or File === content)
|
|
122
122
|
Open.write(tmp_path) do |f|
|
|
123
123
|
while block = content.read(BLOCK_SIZE)
|
|
124
|
-
f.write block
|
|
125
|
-
break if content.closed?
|
|
124
|
+
f.write block if block
|
|
125
|
+
break if content.closed? || content.eof?
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
128
|
else
|