Module: EnvUtil

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

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



66
67
68
69
70
71
72
# File 'lib/envutil.rb', line 66

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

.default_warningObject



231
232
233
234
235
236
# File 'lib/envutil.rb', line 231

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

.diagnostic_reports(signame, pid, now) ⇒ Object



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/envutil.rb', line 318

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



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/envutil.rb', line 352

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)


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

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



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/envutil.rb', line 132

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)
  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



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

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



289
290
291
292
293
294
295
296
297
298
# File 'lib/envutil.rb', line 289

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



239
240
241
242
243
244
# File 'lib/envutil.rb', line 239

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

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



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/envutil.rb', line 82

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

  lldb = true if /darwin/ =~ RUBY_PLATFORM

  while signal = signals.shift

    if lldb and [:ABRT, :KILL].include?(signal)
      lldb = false
      # sudo -n: --non-interactive
      # lldb -p: attach
      #      -o: run command
      system(*%W[sudo -n lldb -p #{pid} --batch -o bt\ all -o call\ rb_vmdebug_stack_dump_all_threads() -o quit])
      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



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

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



255
256
257
258
259
260
261
262
# File 'lib/envutil.rb', line 255

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
  auto_compact = GC.auto_compact
  GC.auto_compact = val
  under_gc_stress(&block)
ensure
  GC.auto_compact = auto_compact
end

.under_gc_stress(stress = true) ⇒ Object



247
248
249
250
251
252
# File 'lib/envutil.rb', line 247

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

.verbose_warningObject



215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/envutil.rb', line 215

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) ⇒ Object



273
274
275
276
277
278
# File 'lib/envutil.rb', line 273

def with_default_external(enc)
  suppress_warning { Encoding.default_external = enc }
  yield
ensure
  suppress_warning { Encoding.default_external = EnvUtil.original_external_encoding }
end

.with_default_internal(enc) ⇒ Object



281
282
283
284
285
286
# File 'lib/envutil.rb', line 281

def with_default_internal(enc)
  suppress_warning { Encoding.default_internal = enc }
  yield
ensure
  suppress_warning { Encoding.default_internal = EnvUtil.original_internal_encoding }
end

.without_gcObject



265
266
267
268
269
270
# File 'lib/envutil.rb', line 265

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