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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +14 -0
- data/TODO.md +0 -4
- data/ext/polyphony/backend_io_uring.c +34 -1
- data/ext/polyphony/backend_io_uring_context.c +24 -18
- data/ext/polyphony/backend_io_uring_context.h +4 -2
- data/ext/polyphony/backend_libev.c +4 -7
- data/ext/polyphony/event.c +21 -0
- data/ext/polyphony/extconf.rb +20 -18
- data/ext/polyphony/fiber.c +0 -2
- data/ext/polyphony/polyphony.c +2 -0
- data/ext/polyphony/polyphony.h +5 -0
- data/ext/polyphony/ring_buffer.c +1 -0
- data/ext/polyphony/runqueue_ring_buffer.c +1 -0
- data/ext/polyphony/thread.c +63 -0
- data/lib/polyphony/adapters/open3.rb +190 -0
- data/lib/polyphony/core/sync.rb +83 -13
- data/lib/polyphony/core/timer.rb +7 -25
- data/lib/polyphony/extensions/exception.rb +15 -0
- data/lib/polyphony/extensions/fiber.rb +14 -13
- data/lib/polyphony/extensions/io.rb +56 -14
- data/lib/polyphony/extensions/kernel.rb +1 -1
- data/lib/polyphony/extensions/object.rb +1 -13
- data/lib/polyphony/extensions/process.rb +76 -1
- data/lib/polyphony/extensions/thread.rb +19 -27
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony.rb +11 -5
- data/test/helper.rb +46 -4
- data/test/open3/envutil.rb +380 -0
- data/test/open3/find_executable.rb +24 -0
- data/test/stress.rb +11 -7
- data/test/test_backend.rb +7 -2
- data/test/test_event.rb +10 -3
- data/test/test_ext.rb +2 -1
- data/test/test_fiber.rb +16 -4
- data/test/test_global_api.rb +13 -12
- data/test/test_io.rb +39 -0
- data/test/test_kernel.rb +2 -2
- data/test/test_monitor.rb +356 -0
- data/test/test_open3.rb +338 -0
- data/test/test_signal.rb +5 -1
- data/test/test_socket.rb +6 -3
- data/test/test_sync.rb +46 -0
- data/test/test_thread.rb +10 -1
- data/test/test_thread_pool.rb +5 -0
- data/test/test_throttler.rb +1 -1
- data/test/test_timer.rb +8 -2
- data/test/test_trace.rb +2 -0
- data/vendor/liburing/.github/workflows/build.yml +8 -0
- data/vendor/liburing/.gitignore +1 -0
- data/vendor/liburing/CHANGELOG +8 -0
- data/vendor/liburing/configure +17 -25
- data/vendor/liburing/debian/liburing-dev.manpages +2 -0
- data/vendor/liburing/debian/rules +2 -1
- data/vendor/liburing/examples/Makefile +2 -1
- data/vendor/liburing/examples/io_uring-udp.c +11 -3
- data/vendor/liburing/examples/rsrc-update-bench.c +100 -0
- data/vendor/liburing/liburing.spec +1 -1
- data/vendor/liburing/make-debs.sh +4 -2
- data/vendor/liburing/src/Makefile +5 -5
- data/vendor/liburing/src/arch/aarch64/lib.h +1 -1
- data/vendor/liburing/src/include/liburing/io_uring.h +41 -16
- data/vendor/liburing/src/include/liburing.h +86 -11
- data/vendor/liburing/src/int_flags.h +1 -0
- data/vendor/liburing/src/liburing-ffi.map +12 -0
- data/vendor/liburing/src/liburing.map +8 -0
- data/vendor/liburing/src/register.c +7 -2
- data/vendor/liburing/src/setup.c +373 -81
- data/vendor/liburing/test/232c93d07b74.c +3 -3
- data/vendor/liburing/test/Makefile +10 -3
- data/vendor/liburing/test/accept.c +2 -1
- data/vendor/liburing/test/buf-ring.c +35 -75
- data/vendor/liburing/test/connect-rep.c +204 -0
- data/vendor/liburing/test/coredump.c +59 -0
- data/vendor/liburing/test/fallocate.c +9 -0
- data/vendor/liburing/test/fd-pass.c +34 -3
- data/vendor/liburing/test/file-verify.c +27 -6
- data/vendor/liburing/test/helpers.c +3 -1
- data/vendor/liburing/test/io_uring_register.c +25 -28
- data/vendor/liburing/test/io_uring_setup.c +1 -1
- data/vendor/liburing/test/poll-cancel-all.c +29 -5
- data/vendor/liburing/test/poll-race-mshot.c +6 -22
- data/vendor/liburing/test/read-write.c +53 -0
- data/vendor/liburing/test/recv-msgall.c +21 -23
- data/vendor/liburing/test/reg-fd-only.c +55 -0
- data/vendor/liburing/test/reg-hint.c +56 -0
- data/vendor/liburing/test/regbuf-merge.c +91 -0
- data/vendor/liburing/test/ringbuf-read.c +2 -10
- data/vendor/liburing/test/send_recvmsg.c +5 -16
- data/vendor/liburing/test/shutdown.c +2 -1
- data/vendor/liburing/test/socket-io-cmd.c +215 -0
- data/vendor/liburing/test/socket-rw-eagain.c +2 -1
- data/vendor/liburing/test/socket-rw-offset.c +2 -1
- data/vendor/liburing/test/socket-rw.c +2 -1
- data/vendor/liburing/test/timeout.c +276 -0
- data/vendor/liburing/test/xattr.c +38 -25
- metadata +14 -3
- 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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
@
|
165
|
-
|
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
|
|
data/lib/polyphony/version.rb
CHANGED
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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::
|
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
|
-
|
52
|
+
@__stamp = Time.now
|
45
53
|
Thread.current.backend.finalize
|
46
54
|
Thread.current.backend = Polyphony::Backend.new
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|