1# -*- coding: us-ascii -*-
2# frozen_string_literal: true
3require "open3"
4require "timeout"
5require_relative "find_executable"
6begin
7  require 'rbconfig'
8rescue LoadError
9end
10begin
11  require "rbconfig/sizeof"
12rescue LoadError
13end
14
15module EnvUtil
16  def rubybin
17    if ruby = ENV["RUBY"]
18      return ruby
19    end
20    ruby = "ruby"
21    exeext = RbConfig::CONFIG["EXEEXT"]
22    rubyexe = (ruby + exeext if exeext and !exeext.empty?)
23    3.times do
24      if File.exist? ruby and File.executable? ruby and !File.directory? ruby
25        return File.expand_path(ruby)
26      end
27      if rubyexe and File.exist? rubyexe and File.executable? rubyexe
28        return File.expand_path(rubyexe)
29      end
30      ruby = File.join("..", ruby)
31    end
32    if defined?(RbConfig.ruby)
33      RbConfig.ruby
34    else
35      "ruby"
36    end
37  end
38  module_function :rubybin
39
40  LANG_ENVS = %w"LANG LC_ALL LC_CTYPE"
41
42  DEFAULT_SIGNALS = Signal.list
43  DEFAULT_SIGNALS.delete("TERM") if /mswin|mingw/ =~ RUBY_PLATFORM
44
45  RUBYLIB = ENV["RUBYLIB"]
46
47  class << self
48    attr_accessor :subprocess_timeout_scale
49    attr_reader :original_internal_encoding, :original_external_encoding,
50                :original_verbose
51
52    def capture_global_values
53      @original_internal_encoding = Encoding.default_internal
54      @original_external_encoding = Encoding.default_external
55      @original_verbose = $VERBOSE
56    end
57  end
58
59  def apply_timeout_scale(t)
60    if scale = EnvUtil.subprocess_timeout_scale
61      t * scale
62    else
63      t
64    end
65  end
66  module_function :apply_timeout_scale
67
68  def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false,
69                  encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error,
70                  stdout_filter: nil, stderr_filter: nil,
71                  signal: :TERM,
72                  rubybin: EnvUtil.rubybin, precommand: nil,
73                  **opt)
74    timeout = apply_timeout_scale(timeout)
75    reprieve = apply_timeout_scale(reprieve) if reprieve
76
77    in_c, in_p = IO.pipe
78    out_p, out_c = IO.pipe if capture_stdout
79    err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout
80    opt[:in] = in_c
81    opt[:out] = out_c if capture_stdout
82    opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr
83    if encoding
84      out_p.set_encoding(encoding) if out_p
85      err_p.set_encoding(encoding) if err_p
86    end
87    c = "C"
88    child_env = {}
89    LANG_ENVS.each {|lc| child_env[lc] = c}
90    if Array === args and Hash === args.first
91      child_env.update(args.shift)
92    end
93    if RUBYLIB and lib = child_env["RUBYLIB"]
94      child_env["RUBYLIB"] = [lib, RUBYLIB].join(File::PATH_SEPARATOR)
95    end
96    args = [args] if args.kind_of?(String)
97    pid = spawn(child_env, *precommand, rubybin, *args, **opt)
98    in_c.close
99    out_c.close if capture_stdout
100    err_c.close if capture_stderr && capture_stderr != :merge_to_stdout
101    if block_given?
102      return yield in_p, out_p, err_p, pid
103    else
104      th_stdout = Thread.new { out_p.read } if capture_stdout
105      th_stderr = Thread.new { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout
106      in_p.write stdin_data.to_str unless stdin_data.empty?
107      in_p.close
108      if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout))
109        timeout_error = nil
110      else
111        signals = Array(signal).select do |sig|
112          DEFAULT_SIGNALS[sig.to_s] or
113            DEFAULT_SIGNALS[Signal.signame(sig)] rescue false
114        end
115        signals |= [:ABRT, :KILL]
116        case pgroup = opt[:pgroup]
117        when 0, true
118          pgroup = -pid
119        when nil, false
120          pgroup = pid
121        end
122        while signal = signals.shift
123          begin
124            Process.kill signal, pgroup
125          rescue Errno::EINVAL
126            next
127          rescue Errno::ESRCH
128            break
129          end
130          if signals.empty? or !reprieve
131            Process.wait(pid)
132          else
133            begin
134              Timeout.timeout(reprieve) {Process.wait(pid)}
135            rescue Timeout::Error
136            end
137          end
138        end
139        status = $?
140      end
141      stdout = th_stdout.value if capture_stdout
142      stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout
143      out_p.close if capture_stdout
144      err_p.close if capture_stderr && capture_stderr != :merge_to_stdout
145      status ||= Process.wait2(pid)[1]
146      stdout = stdout_filter.call(stdout) if stdout_filter
147      stderr = stderr_filter.call(stderr) if stderr_filter
148      if timeout_error
149        bt = caller_locations
150        msg = "execution of #{bt.shift.label} expired timeout (#{timeout} sec)"
151        msg = Test::Unit::Assertions::FailDesc[status, msg, [stdout, stderr].join("\n")].()
152        raise timeout_error, msg, bt.map(&:to_s)
153      end
154      return stdout, stderr, status
155    end
156  ensure
157    [th_stdout, th_stderr].each do |th|
158      th.kill if th
159    end
160    [in_c, in_p, out_c, out_p, err_c, err_p].each do |io|
161      io&.close
162    end
163    [th_stdout, th_stderr].each do |th|
164      th.join if th
165    end
166  end
167  module_function :invoke_ruby
168
169  alias rubyexec invoke_ruby
170  class << self
171    alias rubyexec invoke_ruby
172  end
173
174  def verbose_warning
175    class << (stderr = "".dup)
176      alias write concat
177      def flush; end
178    end
179    stderr, $stderr = $stderr, stderr
180    $VERBOSE = true
181    yield stderr
182    return $stderr
183  ensure
184    stderr, $stderr = $stderr, stderr
185    $VERBOSE = EnvUtil.original_verbose
186  end
187  module_function :verbose_warning
188
189  def default_warning
190    $VERBOSE = false
191    yield
192  ensure
193    $VERBOSE = EnvUtil.original_verbose
194  end
195  module_function :default_warning
196
197  def suppress_warning
198    $VERBOSE = nil
199    yield
200  ensure
201    $VERBOSE = EnvUtil.original_verbose
202  end
203  module_function :suppress_warning
204
205  def under_gc_stress(stress = true)
206    stress, GC.stress = GC.stress, stress
207    yield
208  ensure
209    GC.stress = stress
210  end
211  module_function :under_gc_stress
212
213  def with_default_external(enc)
214    suppress_warning { Encoding.default_external = enc }
215    yield
216  ensure
217    suppress_warning { Encoding.default_external = EnvUtil.original_external_encoding }
218  end
219  module_function :with_default_external
220
221  def with_default_internal(enc)
222    suppress_warning { Encoding.default_internal = enc }
223    yield
224  ensure
225    suppress_warning { Encoding.default_internal = EnvUtil.original_internal_encoding }
226  end
227  module_function :with_default_internal
228
229  def labeled_module(name, &block)
230    Module.new do
231      singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s}
232      class_eval(&block) if block
233    end
234  end
235  module_function :labeled_module
236
237  def labeled_class(name, superclass = Object, &block)
238    Class.new(superclass) do
239      singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s}
240      class_eval(&block) if block
241    end
242  end
243  module_function :labeled_class
244
245  if /darwin/ =~ RUBY_PLATFORM
246    DIAGNOSTIC_REPORTS_PATH = File.expand_path("~/Library/Logs/DiagnosticReports")
247    DIAGNOSTIC_REPORTS_TIMEFORMAT = '%Y-%m-%d-%H%M%S'
248    @ruby_install_name = RbConfig::CONFIG['RUBY_INSTALL_NAME']
249
250    def self.diagnostic_reports(signame, pid, now)
251      return unless %w[ABRT QUIT SEGV ILL TRAP].include?(signame)
252      cmd = File.basename(rubybin)
253      cmd = @ruby_install_name if "ruby-runner#{RbConfig::CONFIG["EXEEXT"]}" == cmd
254      path = DIAGNOSTIC_REPORTS_PATH
255      timeformat = DIAGNOSTIC_REPORTS_TIMEFORMAT
256      pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.crash"
257      first = true
258      30.times do
259        first ? (first = false) : sleep(0.1)
260        Dir.glob(pat) do |name|
261          log = File.read(name) rescue next
262          if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log
263            File.unlink(name)
264            File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil
265            return log
266          end
267        end
268      end
269      nil
270    end
271  else
272    def self.diagnostic_reports(signame, pid, now)
273    end
274  end
275
276  def self.gc_stress_to_class?
277    unless defined?(@gc_stress_to_class)
278      _, _, status = invoke_ruby(["-e""exit GC.respond_to?(:add_stress_to_class)"])
279      @gc_stress_to_class = status.success?
280    end
281    @gc_stress_to_class
282  end
283end
284
285if defined?(RbConfig)
286  module RbConfig
287    @ruby = EnvUtil.rubybin
288    class << self
289      undef ruby if method_defined?(:ruby)
290      attr_reader :ruby
291    end
292    dir = File.dirname(ruby)
293    CONFIG['bindir'] = dir
294    Gem::ConfigMap[:bindir] = dir if defined?(Gem::ConfigMap)
295  end
296end
297
298EnvUtil.capture_global_values
299