Module: EnvUtil

Defined in:
lib/envutil.rb,
lib/find_executable.rb

Defined Under Namespace

Classes: Debugger

Constant Summary collapse

LANG_ENVS =
%w"LANG LC_ALL LC_CTYPE"
DEFAULT_SIGNALS =
Signal.list
RUBYLIB =
ENV["RUBYLIB"]
DIAGNOSTIC_REPORTS_PATH =
File.expand_path("~/Library/Logs/DiagnosticReports")
DIAGNOSTIC_REPORTS_TIMEFORMAT =
'%Y-%m-%d-%H%M%S'

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.original_external_encodingObject (readonly)

Returns the value of attribute original_external_encoding.



48
49
50
# File 'lib/envutil.rb', line 48

def original_external_encoding
  @original_external_encoding
end

.original_internal_encodingObject (readonly)

Returns the value of attribute original_internal_encoding.



48
49
50
# File 'lib/envutil.rb', line 48

def original_internal_encoding
  @original_internal_encoding
end

.original_verboseObject (readonly)

Returns the value of attribute original_verbose.



48
49
50
# File 'lib/envutil.rb', line 48

def original_verbose
  @original_verbose
end

.original_warningObject (readonly)

Returns the value of attribute original_warning.



48
49
50
# File 'lib/envutil.rb', line 48

def original_warning
  @original_warning
end

.timeout_scaleObject

Returns the value of attribute timeout_scale.



47
48
49
# File 'lib/envutil.rb', line 47

def timeout_scale
  @timeout_scale
end

Class Method Details

.apply_timeout_scale(t) ⇒ Object



74
75
76
77
78
79
80
# File 'lib/envutil.rb', line 74

def apply_timeout_scale(t)
  if scale = EnvUtil.timeout_scale
    t * scale
  else
    t
  end
end

.capture_global_valuesObject



51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/envutil.rb', line 51

def capture_global_values
  @original_internal_encoding = Encoding.default_internal
  @original_external_encoding = Encoding.default_external
  @original_verbose = $VERBOSE
  @original_warning =
    if defined?(Warning.categories)
      Warning.categories.to_h {|i| [i, Warning[i]]}
    elsif defined?(Warning.[]) # 2.7+
      %i[deprecated experimental performance].to_h do |i|
        [i, begin Warning[i]; rescue ArgumentError; end]
      end.compact
    end
end

.current_parserObject



292
293
294
295
# File 'lib/envutil.rb', line 292

def current_parser
  features = RUBY_DESCRIPTION[%r{\)\K [-+*/%._0-9a-zA-Z\[\] ]*(?=\[[-+*/%._0-9a-zA-Z]+\]\z)}]
  features&.split&.include?("+PRISM") ? "prism" : "parse.y"
end

.default_warningObject



329
330
331
332
333
334
# File 'lib/envutil.rb', line 329

def default_warning
  $VERBOSE = false
  yield
ensure
  $VERBOSE = EnvUtil.original_verbose
end

.deprecation_warningObject



315
316
317
318
319
320
321
# File 'lib/envutil.rb', line 315

def deprecation_warning
  previous_deprecated = Warning[:deprecated]
  Warning[:deprecated] = true
  yield
ensure
  Warning[:deprecated] = previous_deprecated
end

.diagnostic_reports(signame, pid, now) ⇒ Object



422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/envutil.rb', line 422

def self.diagnostic_reports(signame, pid, now)
  return unless %w[ABRT QUIT SEGV ILL TRAP].include?(signame)
  cmd = File.basename(rubybin)
  cmd = @ruby_install_name if "ruby-runner#{RbConfig::CONFIG["EXEEXT"]}" == cmd
  path = DIAGNOSTIC_REPORTS_PATH
  timeformat = DIAGNOSTIC_REPORTS_TIMEFORMAT
  pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.{crash,ips}"
  first = true
  30.times do
    first ? (first = false) : sleep(0.1)
    Dir.glob(pat) do |name|
      log = File.read(name) rescue next
      case name
      when /\.crash\z/
        if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log
          File.unlink(name)
          File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil
          return log
        end
      when /\.ips\z/
        if /^ *"pid" *: *#{pid},/ =~ log
          File.unlink(name)
          return log
        end
      end
    end
  end
  nil
end

.failure_description(status, now, message = "", out = "") ⇒ Object



456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# File 'lib/envutil.rb', line 456

def self.failure_description(status, now, message = "", out = "")
  pid = status.pid
  if signo = status.termsig
    signame = Signal.signame(signo)
    sigdesc = "signal #{signo}"
  end
  log = diagnostic_reports(signame, pid, now)
  if signame
    sigdesc = "SIG#{signame} (#{sigdesc})"
  end
  if status.coredump?
    sigdesc = "#{sigdesc} (core dumped)"
  end
  full_message = ''.dup
  message = message.call if Proc === message
  if message and !message.empty?
    full_message << message << "\n"
  end
  full_message << "pid #{pid}"
  full_message << " exit #{status.exitstatus}" if status.exited?
  full_message << " killed by #{sigdesc}" if sigdesc
  if out and !out.empty?
    full_message << "\n" << out.b.gsub(/^/, '| ')
    full_message.sub!(/(?<!\n)\z/, "\n")
  end
  if log
    full_message << "Diagnostic reports:\n" << log.b.gsub(/^/, '| ')
  end
  full_message
end

.find_executable(cmd, *args) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# File 'lib/find_executable.rb', line 5

def find_executable(cmd, *args)
  exts = RbConfig::CONFIG["EXECUTABLE_EXTS"].split | [RbConfig::CONFIG["EXEEXT"]]
  ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
    next if path.empty?
    path = File.join(path, cmd)
    exts.each do |ext|
      cmdline = [path + ext, *args]
      begin
        return cmdline if yield(IO.popen(cmdline, "r", err: [:child, :out], &:read))
      rescue
        next
      end
    end
  end
  nil
end

.gc_stress_to_class?Boolean

Returns:

  • (Boolean)


487
488
489
490
491
492
493
# File 'lib/envutil.rb', line 487

def self.gc_stress_to_class?
  unless defined?(@gc_stress_to_class)
    _, _, status = invoke_ruby(["-e""exit GC.respond_to?(:add_stress_to_class)"])
    @gc_stress_to_class = status.success?
  end
  @gc_stress_to_class
end

.invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false, encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error, stdout_filter: nil, stderr_filter: nil, ios: nil, signal: :TERM, rubybin: EnvUtil.rubybin, precommand: nil, **opt) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/envutil.rb', line 204

def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false,
                encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error,
                stdout_filter: nil, stderr_filter: nil, ios: nil,
                signal: :TERM,
                rubybin: EnvUtil.rubybin, precommand: nil,
                **opt)
  timeout = apply_timeout_scale(timeout)

  in_c, in_p = IO.pipe
  out_p, out_c = IO.pipe if capture_stdout
  err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout
  opt[:in] = in_c
  opt[:out] = out_c if capture_stdout
  opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr
  if encoding
    out_p.set_encoding(encoding) if out_p
    err_p.set_encoding(encoding) if err_p
  end
  ios.each {|i, o = i|opt[i] = o} if ios

  c = "C"
  child_env = {}
  LANG_ENVS.each {|lc| child_env[lc] = c}
  if Array === args and Hash === args.first
    child_env.update(args.shift)
  end
  if RUBYLIB and lib = child_env["RUBYLIB"]
    child_env["RUBYLIB"] = [lib, RUBYLIB].join(File::PATH_SEPARATOR)
  end

  # remain env
  %w(ASAN_OPTIONS RUBY_ON_BUG).each{|name|
    child_env[name] = ENV[name] if !child_env.key?(name) and ENV.key?(name)
  }

  args = [args] if args.kind_of?(String)
  # use the same parser as current ruby
  if (args.none? { |arg| arg.start_with?("--parser=") } and
      /^ +--parser=/ =~ IO.popen([{"PAGER"=>nil, "RUBY_PAGER"=>nil}, rubybin, "--help", err: %i[child out]], &:read))
    args = ["--parser=#{current_parser}"] + args
  end
  pid = spawn(child_env, *precommand, rubybin, *args, opt)
  in_c.close
  out_c&.close
  out_c = nil
  err_c&.close
  err_c = nil
  if block_given?
    return yield in_p, out_p, err_p, pid
  else
    th_stdout = Thread.new { out_p.read } if capture_stdout
    th_stderr = Thread.new { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout
    in_p.write stdin_data.to_str unless stdin_data.empty?
    in_p.close
    if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout))
      timeout_error = nil
    else
      status = terminate(pid, signal, opt[:pgroup], reprieve)
      terminated = Time.now
    end
    stdout = th_stdout.value if capture_stdout
    stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout
    out_p.close if capture_stdout
    err_p.close if capture_stderr && capture_stderr != :merge_to_stdout
    status ||= Process.wait2(pid)[1]
    stdout = stdout_filter.call(stdout) if stdout_filter
    stderr = stderr_filter.call(stderr) if stderr_filter
    if timeout_error
      bt = caller_locations
      msg = "execution of #{bt.shift.label} expired timeout (#{timeout} sec)"
      msg = failure_description(status, terminated, msg, [stdout, stderr].join("\n"))
      raise timeout_error, msg, bt.map(&:to_s)
    end
    return stdout, stderr, status
  end
ensure
  [th_stdout, th_stderr].each do |th|
    th.kill if th
  end
  [in_c, in_p, out_c, out_p, err_c, err_p].each do |io|
    io&.close
  end
  [th_stdout, th_stderr].each do |th|
    th.join if th
  end
end

.labeled_class(name, superclass = Object, &block) ⇒ Object



405
406
407
408
409
410
411
412
413
414
# File 'lib/envutil.rb', line 405

def labeled_class(name, superclass = Object, &block)
  Class.new(superclass) do
    singleton_class.class_eval {
      define_method(:to_s) {name}
      alias inspect to_s
      alias name to_s
    }
    class_eval(&block) if block
  end
end

.labeled_module(name, &block) ⇒ Object



393
394
395
396
397
398
399
400
401
402
# File 'lib/envutil.rb', line 393

def labeled_module(name, &block)
  Module.new do
    singleton_class.class_eval {
      define_method(:to_s) {name}
      alias inspect to_s
      alias name to_s
    }
    class_eval(&block) if block
  end
end

.rubybinObject



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/envutil.rb', line 16

def rubybin
  if ruby = ENV["RUBY"]
    ruby
  elsif defined?(RbConfig.ruby)
    RbConfig.ruby
  else
    ruby = "ruby"
    exeext = RbConfig::CONFIG["EXEEXT"]
    rubyexe = (ruby + exeext if exeext and !exeext.empty?)
    3.times do
      if File.exist? ruby and File.executable? ruby and !File.directory? ruby
        return File.expand_path(ruby)
      end
      if rubyexe and File.exist? rubyexe and File.executable? rubyexe
        return File.expand_path(rubyexe)
      end
      ruby = File.join("..", ruby)
    end
    "ruby"
  end
end

.suppress_warningObject



337
338
339
340
341
342
# File 'lib/envutil.rb', line 337

def suppress_warning
  $VERBOSE = nil
  yield
ensure
  $VERBOSE = EnvUtil.original_verbose
end

.terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/envutil.rb', line 159

def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1)
  reprieve = apply_timeout_scale(reprieve) if reprieve

  signals = Array(signal).select do |sig|
    DEFAULT_SIGNALS[sig.to_s] or
      DEFAULT_SIGNALS[Signal.signame(sig)] rescue false
  end
  signals |= [:ABRT, :KILL]
  case pgroup
  when 0, true
    pgroup = -pid
  when nil, false
    pgroup = pid
  end

  dumped = false
  while signal = signals.shift

    if !dumped and [:ABRT, :KILL].include?(signal)
      Debugger.search&.dump(pid)
      dumped = true
    end

    begin
      Process.kill signal, pgroup
    rescue Errno::EINVAL
      next
    rescue Errno::ESRCH
      break
    end
    if signals.empty? or !reprieve
      Process.wait(pid)
    else
      begin
        Timeout.timeout(reprieve) {Process.wait(pid)}
      rescue Timeout::Error
      else
        break
      end
    end
  end
  $?
end

.timeout(sec, klass = nil, message = nil, &blk) ⇒ Object



83
84
85
86
87
# File 'lib/envutil.rb', line 83

def timeout(sec, klass = nil, message = nil, &blk)
  return yield(sec) if sec == nil or sec.zero?
  sec = apply_timeout_scale(sec)
  Timeout.timeout(sec, klass, message, &blk)
end

.under_gc_compact_stress(val = :empty, &block) ⇒ Object



353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/envutil.rb', line 353

def under_gc_compact_stress(val = :empty, &block)
  raise "compaction doesn't work well on s390x. Omit the test in the caller." if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077

  if GC.respond_to?(:auto_compact)
    auto_compact = GC.auto_compact
    GC.auto_compact = val
  end

  under_gc_stress(&block)
ensure
  GC.auto_compact = auto_compact if GC.respond_to?(:auto_compact)
end

.under_gc_stress(stress = true) ⇒ Object



345
346
347
348
349
350
# File 'lib/envutil.rb', line 345

def under_gc_stress(stress = true)
  stress, GC.stress = GC.stress, stress
  yield
ensure
  GC.stress = stress
end

.verbose_warningObject



298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/envutil.rb', line 298

def verbose_warning
  class << (stderr = "".dup)
    alias write concat
    def flush; end
  end
  stderr, $stderr = $stderr, stderr
  $VERBOSE = true
  yield stderr
  return $stderr
ensure
  stderr, $stderr = $stderr, stderr
  $VERBOSE = EnvUtil.original_verbose
  EnvUtil.original_warning&.each {|i, v| Warning[i] = v}
end

.with_default_external(enc = nil, of: nil) ⇒ Object



375
376
377
378
379
380
381
# File 'lib/envutil.rb', line 375

def with_default_external(enc = nil, of: nil)
  enc = of.encoding if defined?(of.encoding)
  suppress_warning { Encoding.default_external = enc }
  yield
ensure
  suppress_warning { Encoding.default_external = EnvUtil.original_external_encoding }
end

.with_default_internal(enc = nil, of: nil) ⇒ Object



384
385
386
387
388
389
390
# File 'lib/envutil.rb', line 384

def with_default_internal(enc = nil, of: nil)
  enc = of.encoding if defined?(of.encoding)
  suppress_warning { Encoding.default_internal = enc }
  yield
ensure
  suppress_warning { Encoding.default_internal = EnvUtil.original_internal_encoding }
end

.without_gcObject



367
368
369
370
371
372
# File 'lib/envutil.rb', line 367

def without_gc
  prev_disabled = GC.disable
  yield
ensure
  GC.enable unless prev_disabled
end