polyphony 1.5 → 1.6

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +14 -0
  4. data/TODO.md +0 -4
  5. data/ext/polyphony/backend_io_uring.c +34 -1
  6. data/ext/polyphony/backend_io_uring_context.c +24 -18
  7. data/ext/polyphony/backend_io_uring_context.h +4 -2
  8. data/ext/polyphony/backend_libev.c +4 -7
  9. data/ext/polyphony/event.c +21 -0
  10. data/ext/polyphony/extconf.rb +20 -18
  11. data/ext/polyphony/fiber.c +0 -2
  12. data/ext/polyphony/polyphony.c +2 -0
  13. data/ext/polyphony/polyphony.h +5 -0
  14. data/ext/polyphony/ring_buffer.c +1 -0
  15. data/ext/polyphony/runqueue_ring_buffer.c +1 -0
  16. data/ext/polyphony/thread.c +63 -0
  17. data/lib/polyphony/adapters/open3.rb +190 -0
  18. data/lib/polyphony/core/sync.rb +83 -13
  19. data/lib/polyphony/core/timer.rb +7 -25
  20. data/lib/polyphony/extensions/exception.rb +15 -0
  21. data/lib/polyphony/extensions/fiber.rb +14 -13
  22. data/lib/polyphony/extensions/io.rb +56 -14
  23. data/lib/polyphony/extensions/kernel.rb +1 -1
  24. data/lib/polyphony/extensions/object.rb +1 -13
  25. data/lib/polyphony/extensions/process.rb +76 -1
  26. data/lib/polyphony/extensions/thread.rb +19 -27
  27. data/lib/polyphony/version.rb +1 -1
  28. data/lib/polyphony.rb +11 -5
  29. data/test/helper.rb +46 -4
  30. data/test/open3/envutil.rb +380 -0
  31. data/test/open3/find_executable.rb +24 -0
  32. data/test/stress.rb +11 -7
  33. data/test/test_backend.rb +7 -2
  34. data/test/test_event.rb +10 -3
  35. data/test/test_ext.rb +2 -1
  36. data/test/test_fiber.rb +16 -4
  37. data/test/test_global_api.rb +13 -12
  38. data/test/test_io.rb +39 -0
  39. data/test/test_kernel.rb +2 -2
  40. data/test/test_monitor.rb +356 -0
  41. data/test/test_open3.rb +338 -0
  42. data/test/test_signal.rb +5 -1
  43. data/test/test_socket.rb +6 -3
  44. data/test/test_sync.rb +46 -0
  45. data/test/test_thread.rb +10 -1
  46. data/test/test_thread_pool.rb +5 -0
  47. data/test/test_throttler.rb +1 -1
  48. data/test/test_timer.rb +8 -2
  49. data/test/test_trace.rb +2 -0
  50. data/vendor/liburing/.github/workflows/build.yml +8 -0
  51. data/vendor/liburing/.gitignore +1 -0
  52. data/vendor/liburing/CHANGELOG +8 -0
  53. data/vendor/liburing/configure +17 -25
  54. data/vendor/liburing/debian/liburing-dev.manpages +2 -0
  55. data/vendor/liburing/debian/rules +2 -1
  56. data/vendor/liburing/examples/Makefile +2 -1
  57. data/vendor/liburing/examples/io_uring-udp.c +11 -3
  58. data/vendor/liburing/examples/rsrc-update-bench.c +100 -0
  59. data/vendor/liburing/liburing.spec +1 -1
  60. data/vendor/liburing/make-debs.sh +4 -2
  61. data/vendor/liburing/src/Makefile +5 -5
  62. data/vendor/liburing/src/arch/aarch64/lib.h +1 -1
  63. data/vendor/liburing/src/include/liburing/io_uring.h +41 -16
  64. data/vendor/liburing/src/include/liburing.h +86 -11
  65. data/vendor/liburing/src/int_flags.h +1 -0
  66. data/vendor/liburing/src/liburing-ffi.map +12 -0
  67. data/vendor/liburing/src/liburing.map +8 -0
  68. data/vendor/liburing/src/register.c +7 -2
  69. data/vendor/liburing/src/setup.c +373 -81
  70. data/vendor/liburing/test/232c93d07b74.c +3 -3
  71. data/vendor/liburing/test/Makefile +10 -3
  72. data/vendor/liburing/test/accept.c +2 -1
  73. data/vendor/liburing/test/buf-ring.c +35 -75
  74. data/vendor/liburing/test/connect-rep.c +204 -0
  75. data/vendor/liburing/test/coredump.c +59 -0
  76. data/vendor/liburing/test/fallocate.c +9 -0
  77. data/vendor/liburing/test/fd-pass.c +34 -3
  78. data/vendor/liburing/test/file-verify.c +27 -6
  79. data/vendor/liburing/test/helpers.c +3 -1
  80. data/vendor/liburing/test/io_uring_register.c +25 -28
  81. data/vendor/liburing/test/io_uring_setup.c +1 -1
  82. data/vendor/liburing/test/poll-cancel-all.c +29 -5
  83. data/vendor/liburing/test/poll-race-mshot.c +6 -22
  84. data/vendor/liburing/test/read-write.c +53 -0
  85. data/vendor/liburing/test/recv-msgall.c +21 -23
  86. data/vendor/liburing/test/reg-fd-only.c +55 -0
  87. data/vendor/liburing/test/reg-hint.c +56 -0
  88. data/vendor/liburing/test/regbuf-merge.c +91 -0
  89. data/vendor/liburing/test/ringbuf-read.c +2 -10
  90. data/vendor/liburing/test/send_recvmsg.c +5 -16
  91. data/vendor/liburing/test/shutdown.c +2 -1
  92. data/vendor/liburing/test/socket-io-cmd.c +215 -0
  93. data/vendor/liburing/test/socket-rw-eagain.c +2 -1
  94. data/vendor/liburing/test/socket-rw-offset.c +2 -1
  95. data/vendor/liburing/test/socket-rw.c +2 -1
  96. data/vendor/liburing/test/timeout.c +276 -0
  97. data/vendor/liburing/test/xattr.c +38 -25
  98. metadata +14 -3
  99. data/vendor/liburing/test/timeout-overflow.c +0 -204
@@ -2,6 +2,81 @@
2
2
 
3
3
  # Overrides for Process module
4
4
  module ::Process
5
+ module StatusExtensions
6
+ def coredump?
7
+ @status ? nil : super
8
+ end
9
+
10
+ def exited?
11
+ @status ? WIFEXITED(@status[1]) : super
12
+ end
13
+
14
+ def exitstatus
15
+ @status ? WEXITSTATUS(@status[1]) : super
16
+ end
17
+
18
+ def inspect
19
+ @status ? "#<Process::Status: pid #{@status[0]} exit #{@status[1]}>" : super
20
+ end
21
+
22
+ def pid
23
+ @status ? @status[0] : super
24
+ end
25
+
26
+ def signaled?
27
+ @status ? WIFSIGNALED(@status[1]) : super
28
+ end
29
+
30
+ def stopped?
31
+ @status ? WIFSTOPPED(@status[1]) : super
32
+ end
33
+
34
+ def stopsig
35
+ @status ? WIFSTOPPED(@status[1]) && WEXITSTATUS(@status[1]) : super
36
+ end
37
+
38
+ def success?
39
+ @status ? @status[1] == 0 : super
40
+ end
41
+
42
+ def termsig
43
+ @status ? WIFSIGNALED(@status[1]) && WTERMSIG(@status[1]) : super
44
+ end
45
+
46
+ private
47
+
48
+ # The following helper methods are translated from the C source:
49
+ # https://github.com/ruby/ruby/blob/v3_2_0/process.c
50
+
51
+ def WIFEXITED(w)
52
+ (w & 0xff) == 0
53
+ end
54
+
55
+ def WEXITSTATUS(w)
56
+ (w >> 8) & 0xff
57
+ end
58
+
59
+ def WIFSIGNALED(w)
60
+ (w & 0x7f) > 0 && ((w & 0x7f) < 0x7f)
61
+ end
62
+
63
+ def WIFSTOPPED(w)
64
+ (w & 0xff) == 0x7f
65
+ end
66
+
67
+ def WTERMSIG(w)
68
+ w & 0x7f
69
+ end
70
+ end
71
+
72
+ class Status
73
+ prepend StatusExtensions
74
+
75
+ def self.from_status_array(arr)
76
+ allocate.tap { |s| s.instance_variable_set(:@status, arr) }
77
+ end
78
+ end
79
+
5
80
  class << self
6
81
  # @!visibility private
7
82
  alias_method :orig_detach, :detach
@@ -11,7 +86,7 @@ module ::Process
11
86
  # @param pid [Integer] child pid
12
87
  # @return [Fiber] new fiber waiting on pid
13
88
  def detach(pid)
14
- fiber = spin { Polyphony.backend_waitpid(pid) }
89
+ fiber = spin { ::Process::Status.from_status_array(Polyphony.backend_waitpid(pid)) }
15
90
  fiber.define_singleton_method(:pid) { pid }
16
91
  fiber
17
92
  end
@@ -14,7 +14,6 @@ class ::Thread
14
14
  # @param args [Array] arguments to pass to thread block
15
15
  def initialize(*args, &block)
16
16
  @join_wait_queue = []
17
- @finalization_mutex = Mutex.new
18
17
  @args = args
19
18
  @block = block
20
19
  orig_initialize { execute }
@@ -41,16 +40,7 @@ class ::Thread
41
40
  # @param timeout [Number] timeout interval
42
41
  # @return [any] thread's return value
43
42
  def join(timeout = nil)
44
- watcher = Fiber.current.auto_watcher
45
-
46
- @finalization_mutex.synchronize do
47
- if @terminated
48
- @result.is_a?(Exception) ? (raise @result) : (return @result)
49
- else
50
- @join_wait_queue << watcher
51
- end
52
- end
53
- timeout ? move_on_after(timeout) { watcher.await } : watcher.await
43
+ timeout ? move_on_after(timeout) { await_done } : await_done
54
44
  end
55
45
  alias_method :await, :join
56
46
 
@@ -62,13 +52,14 @@ class ::Thread
62
52
  #
63
53
  # @param error [Exception, Class, nil] exception spec
64
54
  def raise(error = nil)
65
- Thread.pass until @main_fiber
66
- error = RuntimeError.new if error.nil?
67
- error = RuntimeError.new(error) if error.is_a?(String)
68
- error = error.new if error.is_a?(Class)
69
-
70
55
  sleep 0.0001 until @ready
71
- main_fiber&.raise(error)
56
+
57
+ error = Exception.instantiate(error)
58
+ if Thread.current == self
59
+ Kernel.raise(error)
60
+ else
61
+ @main_fiber&.raise(error)
62
+ end
72
63
  end
73
64
 
74
65
  # @!visibility private
@@ -77,11 +68,7 @@ class ::Thread
77
68
  # Terminates the thread.
78
69
  #
79
70
  # @return [Thread] self
80
- def kill
81
- return self if @terminated
82
-
83
- raise Polyphony::Terminate
84
- end
71
+ alias_method :kill, :kill_safe
85
72
 
86
73
  # @!visibility private
87
74
  alias_method :orig_inspect, :inspect
@@ -128,6 +115,11 @@ class ::Thread
128
115
  backend.idle_proc = block
129
116
  end
130
117
 
118
+ def value
119
+ join
120
+ @result.is_a?(Exception) ? raise(@result) : @result
121
+ end
122
+
131
123
  private
132
124
 
133
125
  # Runs the thread's block, handling any uncaught exceptions.
@@ -159,13 +151,13 @@ class ::Thread
159
151
  #
160
152
  # @param result [any] thread's return value
161
153
  def finalize(result)
154
+ # We need to make sure the fiber is not on the runqueue. This, in order to
155
+ # prevent a race condition between #finalize and #kill.
156
+ fiber_unschedule(Fiber.current)
162
157
  Fiber.current.shutdown_all_children if !Fiber.current.children.empty?
163
158
 
164
- @finalization_mutex.synchronize do
165
- @terminated = true
166
- @result = result
167
- signal_waiters(result)
168
- end
159
+ @result = result
160
+ mark_as_done(result)
169
161
  @backend&.finalize
170
162
  end
171
163
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Polyphony
4
4
  # @!visibility private
5
- VERSION = '1.5'
5
+ VERSION = '1.6'
6
6
  end
data/lib/polyphony.rb CHANGED
@@ -14,6 +14,7 @@ require_relative './polyphony/core/sync'
14
14
  require_relative './polyphony/core/timer'
15
15
  require_relative './polyphony/net'
16
16
  require_relative './polyphony/adapters/process'
17
+ require_relative './polyphony/adapters/open3'
17
18
 
18
19
  # Polyphony API
19
20
  module Polyphony
@@ -119,10 +120,10 @@ module Polyphony
119
120
  # processes,) we use a separate mechanism to terminate fibers in forked
120
121
  # processes (see Polyphony.fork).
121
122
  at_exit do
122
- next unless @original_pid == ::Process.pid
123
-
124
- terminate_threads
125
- Fiber.current.shutdown_all_children
123
+ if @original_pid == ::Process.pid
124
+ terminate_threads
125
+ Fiber.current.shutdown_all_children
126
+ end
126
127
  end
127
128
  end
128
129
  end
@@ -131,12 +132,17 @@ module Polyphony
131
132
  verbose = $VERBOSE
132
133
  $VERBOSE = nil
133
134
  Object.const_set(:Queue, Polyphony::Queue)
135
+ Thread.const_set(:Queue, Polyphony::Queue)
136
+
134
137
  Object.const_set(:Mutex, Polyphony::Mutex)
138
+ Thread.const_set(:Mutex, Polyphony::Mutex)
135
139
 
136
140
  require 'monitor'
137
- Object.const_set(:Monitor, Polyphony::Mutex)
141
+ Object.const_set(:Monitor, Polyphony::Monitor)
138
142
 
139
143
  Object.const_set(:ConditionVariable, Polyphony::ConditionVariable)
144
+ Thread.const_set(:ConditionVariable, Polyphony::ConditionVariable)
145
+
140
146
  $VERBOSE = verbose
141
147
 
142
148
  install_terminating_signal_handlers
data/test/helper.rb CHANGED
@@ -17,6 +17,14 @@ require 'minitest/autorun'
17
17
  IS_LINUX = RUBY_PLATFORM =~ /linux/
18
18
 
19
19
  module ::Kernel
20
+ def debug(**h)
21
+ k, v = h.first
22
+ h.delete(k)
23
+
24
+ rest = h.inject(+'') { |s, (k, v)| s << " #{k}: #{v.inspect}\n" }
25
+ STDOUT.orig_write("#{k}=>#{v} #{caller[0]}\n#{rest}")
26
+ end
27
+
20
28
  def trace(*args)
21
29
  STDOUT.orig_write(format_trace(args))
22
30
  end
@@ -41,20 +49,24 @@ end
41
49
  class MiniTest::Test
42
50
  def setup
43
51
  # trace "* setup #{self.name}"
44
- Fiber.current.setup_main_fiber
52
+ @__stamp = Time.now
45
53
  Thread.current.backend.finalize
46
54
  Thread.current.backend = Polyphony::Backend.new
47
- sleep 0.001
48
- @__stamp = Time.now
55
+ Fiber.current.setup_main_fiber
56
+ Fiber.current.instance_variable_set(:@auto_watcher, nil)
57
+ sleep 0.0001
49
58
  end
50
59
 
51
60
  def teardown
52
- # trace "* teardown #{self.name} (#{Time.now - @__stamp}s)"
61
+ Polyphony::ThreadPool.reset
62
+
63
+ # trace "* teardown #{self.name} (#{@__stamp ? (Time.now - @__stamp) : '?'}s)"
53
64
  Fiber.current.shutdown_all_children
54
65
  if Fiber.current.children.size > 0
55
66
  puts "Children left after #{self.name}: #{Fiber.current.children.inspect}"
56
67
  exit!
57
68
  end
69
+ # trace "* teardown done"
58
70
  rescue => e
59
71
  puts e
60
72
  puts e.backtrace.join("\n")
@@ -79,6 +91,36 @@ module Minitest::Assertions
79
91
  msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
80
92
  assert exp_range.include?(act), msg
81
93
  end
94
+
95
+ def assert_join_threads(threads, message = nil)
96
+ errs = []
97
+ values = []
98
+ while th = threads.shift
99
+ begin
100
+ values << th.value
101
+ rescue Exception
102
+ errs << [th, $!]
103
+ th = nil
104
+ end
105
+ end
106
+ values
107
+ ensure
108
+ if th&.alive?
109
+ th.raise(Timeout::Error.new)
110
+ th.join rescue errs << [th, $!]
111
+ end
112
+ if !errs.empty?
113
+ msg = "exceptions on #{errs.length} threads:\n" +
114
+ errs.map {|t, err|
115
+ "#{t.inspect}:\n" +
116
+ (err.respond_to?(:full_message) ? err.full_message(highlight: false, order: :top) : err.message)
117
+ }.join("\n---\n")
118
+ if message
119
+ msg = "#{message}\n#{msg}"
120
+ end
121
+ raise MiniTest::Assertion, msg
122
+ end
123
+ end
82
124
  end
83
125
 
84
126
  puts "Polyphony backend: #{Thread.current.backend.kind}"
@@ -0,0 +1,380 @@
1
+ # Adapted from https://github.com/ruby/open3/blob/master/test/lib/envutil.rb
2
+
3
+ # -*- coding: us-ascii -*-
4
+ # frozen_string_literal: true
5
+ require_relative "find_executable"
6
+ begin
7
+ require 'rbconfig'
8
+ rescue LoadError
9
+ end
10
+ begin
11
+ require "rbconfig/sizeof"
12
+ rescue LoadError
13
+ end
14
+
15
+ module 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 :timeout_scale
49
+ attr_reader :original_internal_encoding, :original_external_encoding,
50
+ :original_verbose, :original_warning
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
+ @original_warning = defined?(Warning.[]) ? %i[deprecated experimental].to_h {|i| [i, Warning[i]]} : nil
57
+ end
58
+ end
59
+
60
+ def apply_timeout_scale(t)
61
+ if scale = EnvUtil.timeout_scale
62
+ t * scale
63
+ else
64
+ t
65
+ end
66
+ end
67
+ module_function :apply_timeout_scale
68
+
69
+ def timeout(sec, klass = nil, message = nil, &blk)
70
+ return yield(sec) if sec == nil or sec.zero?
71
+ sec = apply_timeout_scale(sec)
72
+ Timeout.timeout(sec, klass, message, &blk)
73
+ end
74
+ module_function :timeout
75
+
76
+ def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1)
77
+ reprieve = apply_timeout_scale(reprieve) if reprieve
78
+
79
+ signals = Array(signal).select do |sig|
80
+ DEFAULT_SIGNALS[sig.to_s] or
81
+ DEFAULT_SIGNALS[Signal.signame(sig)] rescue false
82
+ end
83
+ signals |= [:ABRT, :KILL]
84
+ case pgroup
85
+ when 0, true
86
+ pgroup = -pid
87
+ when nil, false
88
+ pgroup = pid
89
+ end
90
+
91
+ lldb = true if /darwin/ =~ RUBY_PLATFORM
92
+
93
+ while signal = signals.shift
94
+
95
+ if lldb and [:ABRT, :KILL].include?(signal)
96
+ lldb = false
97
+ # sudo -n: --non-interactive
98
+ # lldb -p: attach
99
+ # -o: run command
100
+ system(*%W[sudo -n lldb -p #{pid} --batch -o bt\ all -o call\ rb_vmdebug_stack_dump_all_threads() -o quit])
101
+ true
102
+ end
103
+
104
+ begin
105
+ Process.kill signal, pgroup
106
+ rescue Errno::EINVAL
107
+ next
108
+ rescue Errno::ESRCH
109
+ break
110
+ end
111
+ if signals.empty? or !reprieve
112
+ Process.wait(pid)
113
+ else
114
+ begin
115
+ Timeout.timeout(reprieve) {Process.wait(pid)}
116
+ rescue Timeout::Error
117
+ else
118
+ break
119
+ end
120
+ end
121
+ end
122
+ $?
123
+ end
124
+ module_function :terminate
125
+
126
+ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false,
127
+ encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error,
128
+ stdout_filter: nil, stderr_filter: nil, ios: nil,
129
+ signal: :TERM,
130
+ rubybin: EnvUtil.rubybin, precommand: nil,
131
+ **opt)
132
+ timeout = apply_timeout_scale(timeout)
133
+
134
+ in_c, in_p = IO.pipe
135
+ out_p, out_c = IO.pipe if capture_stdout
136
+ err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout
137
+ opt[:in] = in_c
138
+ opt[:out] = out_c if capture_stdout
139
+ opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr
140
+ if encoding
141
+ out_p.set_encoding(encoding) if out_p
142
+ err_p.set_encoding(encoding) if err_p
143
+ end
144
+ ios.each {|i, o = i|opt[i] = o} if ios
145
+
146
+ c = "C"
147
+ child_env = {}
148
+ LANG_ENVS.each {|lc| child_env[lc] = c}
149
+ if Array === args and Hash === args.first
150
+ child_env.update(args.shift)
151
+ end
152
+ if RUBYLIB and lib = child_env["RUBYLIB"]
153
+ child_env["RUBYLIB"] = [lib, RUBYLIB].join(File::PATH_SEPARATOR)
154
+ end
155
+
156
+ # remain env
157
+ %w(ASAN_OPTIONS RUBY_ON_BUG).each{|name|
158
+ child_env[name] = ENV[name] if ENV[name]
159
+ }
160
+
161
+ args = [args] if args.kind_of?(String)
162
+ pid = spawn(child_env, *precommand, rubybin, *args, opt)
163
+ in_c.close
164
+ out_c&.close
165
+ out_c = nil
166
+ err_c&.close
167
+ err_c = nil
168
+ if block_given?
169
+ return yield in_p, out_p, err_p, pid
170
+ else
171
+ th_stdout = spin { out_p.read } if capture_stdout
172
+ th_stderr = spin { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout
173
+ in_p.write stdin_data.to_str unless stdin_data.empty?
174
+ in_p.close
175
+ if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout))
176
+ timeout_error = nil
177
+ else
178
+ status = terminate(pid, signal, opt[:pgroup], reprieve)
179
+ terminated = Time.now
180
+ end
181
+ stdout = th_stdout.value if capture_stdout
182
+ stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout
183
+ out_p.close if capture_stdout
184
+ err_p.close if capture_stderr && capture_stderr != :merge_to_stdout
185
+ status ||= Process.wait2(pid)[1]
186
+ stdout = stdout_filter.call(stdout) if stdout_filter
187
+ stderr = stderr_filter.call(stderr) if stderr_filter
188
+ if timeout_error
189
+ bt = caller_locations
190
+ msg = "execution of #{bt.shift.label} expired timeout (#{timeout} sec)"
191
+ msg = failure_description(status, terminated, msg, [stdout, stderr].join("\n"))
192
+ raise timeout_error, msg, bt.map(&:to_s)
193
+ end
194
+ return stdout, stderr, status
195
+ end
196
+ ensure
197
+ [th_stdout, th_stderr].each do |th|
198
+ th.kill if th
199
+ end
200
+ [in_c, in_p, out_c, out_p, err_c, err_p].each do |io|
201
+ io&.close
202
+ end
203
+ [th_stdout, th_stderr].each do |th|
204
+ th.join if th
205
+ end
206
+ end
207
+ module_function :invoke_ruby
208
+
209
+ def verbose_warning
210
+ class << (stderr = "".dup)
211
+ alias write concat
212
+ def flush; end
213
+ end
214
+ stderr, $stderr = $stderr, stderr
215
+ $VERBOSE = true
216
+ yield stderr
217
+ return $stderr
218
+ ensure
219
+ stderr, $stderr = $stderr, stderr
220
+ $VERBOSE = EnvUtil.original_verbose
221
+ EnvUtil.original_warning&.each {|i, v| Warning[i] = v}
222
+ end
223
+ module_function :verbose_warning
224
+
225
+ def default_warning
226
+ $VERBOSE = false
227
+ yield
228
+ ensure
229
+ $VERBOSE = EnvUtil.original_verbose
230
+ end
231
+ module_function :default_warning
232
+
233
+ def suppress_warning
234
+ $VERBOSE = nil
235
+ yield
236
+ ensure
237
+ $VERBOSE = EnvUtil.original_verbose
238
+ end
239
+ module_function :suppress_warning
240
+
241
+ def under_gc_stress(stress = true)
242
+ stress, GC.stress = GC.stress, stress
243
+ yield
244
+ ensure
245
+ GC.stress = stress
246
+ end
247
+ module_function :under_gc_stress
248
+
249
+ def with_default_external(enc)
250
+ suppress_warning { Encoding.default_external = enc }
251
+ yield
252
+ ensure
253
+ suppress_warning { Encoding.default_external = EnvUtil.original_external_encoding }
254
+ end
255
+ module_function :with_default_external
256
+
257
+ def with_default_internal(enc)
258
+ suppress_warning { Encoding.default_internal = enc }
259
+ yield
260
+ ensure
261
+ suppress_warning { Encoding.default_internal = EnvUtil.original_internal_encoding }
262
+ end
263
+ module_function :with_default_internal
264
+
265
+ def labeled_module(name, &block)
266
+ Module.new do
267
+ singleton_class.class_eval {
268
+ define_method(:to_s) {name}
269
+ alias inspect to_s
270
+ alias name to_s
271
+ }
272
+ class_eval(&block) if block
273
+ end
274
+ end
275
+ module_function :labeled_module
276
+
277
+ def labeled_class(name, superclass = Object, &block)
278
+ Class.new(superclass) do
279
+ singleton_class.class_eval {
280
+ define_method(:to_s) {name}
281
+ alias inspect to_s
282
+ alias name to_s
283
+ }
284
+ class_eval(&block) if block
285
+ end
286
+ end
287
+ module_function :labeled_class
288
+
289
+ if /darwin/ =~ RUBY_PLATFORM
290
+ DIAGNOSTIC_REPORTS_PATH = File.expand_path("~/Library/Logs/DiagnosticReports")
291
+ DIAGNOSTIC_REPORTS_TIMEFORMAT = '%Y-%m-%d-%H%M%S'
292
+ @ruby_install_name = RbConfig::CONFIG['RUBY_INSTALL_NAME']
293
+
294
+ def self.diagnostic_reports(signame, pid, now)
295
+ return unless %w[ABRT QUIT SEGV ILL TRAP].include?(signame)
296
+ cmd = File.basename(rubybin)
297
+ cmd = @ruby_install_name if "ruby-runner#{RbConfig::CONFIG["EXEEXT"]}" == cmd
298
+ path = DIAGNOSTIC_REPORTS_PATH
299
+ timeformat = DIAGNOSTIC_REPORTS_TIMEFORMAT
300
+ pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.{crash,ips}"
301
+ first = true
302
+ 30.times do
303
+ first ? (first = false) : sleep(0.1)
304
+ Dir.glob(pat) do |name|
305
+ log = File.read(name) rescue next
306
+ case name
307
+ when /\.crash\z/
308
+ if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log
309
+ File.unlink(name)
310
+ File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil
311
+ return log
312
+ end
313
+ when /\.ips\z/
314
+ if /^ *"pid" *: *#{pid},/ =~ log
315
+ File.unlink(name)
316
+ return log
317
+ end
318
+ end
319
+ end
320
+ end
321
+ nil
322
+ end
323
+ else
324
+ def self.diagnostic_reports(signame, pid, now)
325
+ end
326
+ end
327
+
328
+ def self.failure_description(status, now, message = "", out = "")
329
+ pid = status.pid
330
+ if signo = status.termsig
331
+ signame = Signal.signame(signo)
332
+ sigdesc = "signal #{signo}"
333
+ end
334
+ log = diagnostic_reports(signame, pid, now)
335
+ if signame
336
+ sigdesc = "SIG#{signame} (#{sigdesc})"
337
+ end
338
+ if status.coredump?
339
+ sigdesc = "#{sigdesc} (core dumped)"
340
+ end
341
+ full_message = ''.dup
342
+ message = message.call if Proc === message
343
+ if message and !message.empty?
344
+ full_message << message << "\n"
345
+ end
346
+ full_message << "pid #{pid}"
347
+ full_message << " exit #{status.exitstatus}" if status.exited?
348
+ full_message << " killed by #{sigdesc}" if sigdesc
349
+ if out and !out.empty?
350
+ full_message << "\n" << out.b.gsub(/^/, '| ')
351
+ full_message.sub!(/(?<!\n)\z/, "\n")
352
+ end
353
+ if log
354
+ full_message << "Diagnostic reports:\n" << log.b.gsub(/^/, '| ')
355
+ end
356
+ full_message
357
+ end
358
+
359
+ def self.gc_stress_to_class?
360
+ unless defined?(@gc_stress_to_class)
361
+ _, _, status = invoke_ruby(["-e""exit GC.respond_to?(:add_stress_to_class)"])
362
+ @gc_stress_to_class = status.success?
363
+ end
364
+ @gc_stress_to_class
365
+ end
366
+ end
367
+
368
+ if defined?(RbConfig)
369
+ module RbConfig
370
+ @ruby = EnvUtil.rubybin
371
+ class << self
372
+ undef ruby if method_defined?(:ruby)
373
+ attr_reader :ruby
374
+ end
375
+ dir = File.dirname(ruby)
376
+ CONFIG['bindir'] = dir
377
+ end
378
+ end
379
+
380
+ EnvUtil.capture_global_values
@@ -0,0 +1,24 @@
1
+ # Adapted from https://github.com/ruby/open3/blob/master/test/lib/find_executable.rb
2
+
3
+ # frozen_string_literal: true
4
+ require "rbconfig"
5
+
6
+ module EnvUtil
7
+ def find_executable(cmd, *args)
8
+ exts = RbConfig::CONFIG["EXECUTABLE_EXTS"].split | [RbConfig::CONFIG["EXEEXT"]]
9
+ ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
10
+ next if path.empty?
11
+ path = File.join(path, cmd)
12
+ exts.each do |ext|
13
+ cmdline = [path + ext, *args]
14
+ begin
15
+ return cmdline if yield(IO.popen(cmdline, "r", err: [:child, :out], &:read))
16
+ rescue
17
+ next
18
+ end
19
+ end
20
+ end
21
+ nil
22
+ end
23
+ module_function :find_executable
24
+ end