polyphony 1.5 → 1.6

Sign up to get free protection for your applications and to get access to all the features.
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