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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +26 -12
  3. data/README.md +83 -112
  4. data/VERSION +1 -1
  5. data/doc/Improvements.md +226 -0
  6. data/doc/StartHere.md +122 -0
  7. data/doc/developer/AnnotationSystem.md +184 -0
  8. data/doc/developer/Architecture.md +147 -0
  9. data/doc/developer/Configuration.md +238 -0
  10. data/doc/developer/CoreUtilities.md +265 -0
  11. data/doc/developer/DesignPrinciples.md +129 -0
  12. data/doc/developer/ErrorHandling.md +203 -0
  13. data/doc/developer/LockingAndConcurrency.md +157 -0
  14. data/doc/developer/PathResolution.md +200 -0
  15. data/doc/developer/PersistenceAndResources.md +119 -0
  16. data/doc/developer/StreamingModel.md +236 -0
  17. data/doc/user/AnnotatingData.md +202 -0
  18. data/doc/user/CachingResults.md +183 -0
  19. data/doc/user/CommandLineOptions.md +189 -0
  20. data/doc/user/Cookbook.md +211 -0
  21. data/doc/user/HandlingStreams.md +236 -0
  22. data/doc/user/LoggingAndProgress.md +158 -0
  23. data/doc/user/ProducingResources.md +177 -0
  24. data/doc/user/RemoteData.md +157 -0
  25. data/doc/user/RunningCommands.md +218 -0
  26. data/doc/user/WorkingWithFiles.md +217 -0
  27. data/lib/scout/cmd.rb +343 -40
  28. data/lib/scout/concurrent_stream.rb +14 -1
  29. data/lib/scout/indiferent_hash.rb +1 -1
  30. data/lib/scout/log/fingerprint.rb +13 -8
  31. data/lib/scout/log/progress/report.rb +1 -1
  32. data/lib/scout/log.rb +4 -1
  33. data/lib/scout/misc/digest.rb +6 -5
  34. data/lib/scout/misc/format.rb +24 -0
  35. data/lib/scout/named_array.rb +1 -1
  36. data/lib/scout/open/stream.rb +2 -2
  37. data/lib/scout/open/util.rb +8 -4
  38. data/lib/scout/open.rb +3 -3
  39. data/lib/scout/path/find.rb +3 -2
  40. data/lib/scout/persist.rb +14 -10
  41. data/lib/scout/resource/produce.rb +9 -1
  42. data/research/annotations-data-analysis.md +206 -0
  43. data/research/behavior-probes.md +1925 -0
  44. data/research/commands-streaming-analysis.md +272 -0
  45. data/research/design-philosophy-analysis.md +383 -0
  46. data/research/doc-audit-findings.md +294 -0
  47. data/research/ecosystem-attribution.md +118 -0
  48. data/research/implementation-inventory-core.md +1029 -0
  49. data/research/implementation-inventory-open.md +417 -0
  50. data/research/implementation-inventory-path-persist-resource.md +774 -0
  51. data/research/io-paths-analysis.md +228 -0
  52. data/research/persistence-resources-analysis.md +244 -0
  53. data/research/synthesis-report.md +80 -0
  54. data/scout-essentials.gemspec +37 -15
  55. data/test/scout/open/test_remote.rb +1 -2
  56. data/test/scout/test_cmd.rb +411 -0
  57. metadata +36 -14
  58. data/doc/Annotation.md +0 -352
  59. data/doc/CMD.md +0 -363
  60. data/doc/ConcurrentStream.md +0 -163
  61. data/doc/IndiferentHash.md +0 -240
  62. data/doc/Log.md +0 -235
  63. data/doc/NamedArray.md +0 -174
  64. data/doc/Open.md +0 -331
  65. data/doc/Path.md +0 -217
  66. data/doc/Persist.md +0 -214
  67. data/doc/Resource.md +0 -229
  68. data/doc/SimpleOPT.md +0 -236
  69. 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
- if cmd.nil? && ! Symbol === tool
151
- cmd = tool
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
- tool = get_tool(tool)
154
- if cmd.nil?
250
+ if cmd.nil? && ! Symbol === tool
155
251
  cmd = tool
156
252
  else
157
- cmd = tool + ' ' + cmd
158
- end
253
+ tool = get_tool(tool)
254
+ if cmd.nil?
255
+ cmd = tool
256
+ else
257
+ cmd = tool + ' ' + cmd
258
+ end
159
259
 
160
- end
260
+ end
161
261
 
162
- case xvfb
163
- when TrueClass
164
- cmd = "xvfb-run --server-args='-screen 0 1024x768x24' --auto-servernum #{cmd}"
165
- when String
166
- cmd = "xvfb-run --server-args='#{xvfb}' --auto-servernum --server-num=1 #{cmd}"
167
- when String
168
- end
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
- if stderr == true
171
- stderr = Log::HIGH
172
- end
270
+ if stderr == true
271
+ stderr = Log::HIGH
272
+ end
173
273
 
174
- cmd_options = process_cmd_options options
175
- if cmd =~ /'\{opt\}'/
176
- cmd = cmd.sub('\'{opt}\'', cmd_options)
177
- else
178
- cmd += " " + cmd_options
179
- end
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
- if sudo
182
- cmd = "sudo " + cmd
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
- Open3.popen3(ENV, cmd)
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.debug{"CMD: [#{pid}] #{cmd}".strip if 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
- Log.error "Error in CMD [#{pid}] #{cmd}: #{$!.message}" unless no_fail
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
- Log.exception $!
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
- while not serr.eof?
264
- line = serr.gets
265
- bar.process(line)
266
- err << line if Integer === stderr and log
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
- while not serr.eof?
273
- err += serr.gets
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
- msg = "Error joining #{self.filename || self.inspect}. Last log line: #{log}"
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?
@@ -137,7 +137,7 @@ module IndiferentHash
137
137
  new
138
138
  end
139
139
 
140
- def prety_print
140
+ def pretty_print
141
141
  Misc.format_definition_list(self, sep: "\n")
142
142
  end
143
143
 
@@ -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
- if obj.length > FP_MAX_STRING
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
@@ -56,7 +56,7 @@ module Log
56
56
  thr = short_mean
57
57
  else
58
58
  thr = begin
59
- d = Time.now - @start
59
+ d = Time.now - (@start || Time.now)
60
60
  if d == 0
61
61
  1
62
62
  else
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
- message = message.prety_print if message.respond_to?(:prety_print)
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
@@ -7,11 +7,12 @@ module Misc
7
7
  else
8
8
  case obj
9
9
  when String
10
- if Path.is_filename?(obj) && Open.exists?(obj)
11
- Path.setup(obj).digest_str
12
- else
13
- obj.dup
14
- end
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
@@ -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
 
@@ -159,7 +159,7 @@ module NamedArray
159
159
  end
160
160
  end
161
161
 
162
- def prety_print
162
+ def pretty_print
163
163
  Misc.format_definition_list(self.to_hash, sep: "\n")
164
164
  end
165
165
  end
@@ -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