Class: Ibex::BoundedSubprocess
- Inherits:
-
Object
- Object
- Ibex::BoundedSubprocess
- Defined in:
- lib/ibex/bounded_subprocess.rb,
sig/ibex/bounded_subprocess.rbs
Overview
Captures one shell-free child process under wall-clock and output budgets.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- DEFAULT_TIMEOUT_SECONDS =
10- DEFAULT_MAX_OUTPUT_BYTES =
1_048_576- TERMINATION_GRACE_SECONDS =
0.25- POLL_SECONDS =
0.01
Instance Method Summary collapse
-
#initialize(timeout_seconds: DEFAULT_TIMEOUT_SECONDS, max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES) ⇒ BoundedSubprocess
constructor
A new instance of BoundedSubprocess.
- #monotonic_time ⇒ Float
- #output_exceeded?(stdout_path, stderr_path) ⇒ Boolean
- #process_group_alive?(pid) ⇒ Boolean
- #process_groups? ⇒ Boolean
- #process_status(pid) ⇒ Process::Status
- #read_bounded(path) ⇒ String
- #run(command, input:) ⇒ Result
- #run_with_files(command, input, stdin, stdout, stderr) ⇒ Result
- #signal_process_tree(signal, pid) ⇒ void
- #spawn_child(command, stdin, stdout, stderr) ⇒ Integer
- #terminate(pid) ⇒ Process::Status
- #terminate_descendants(pid) ⇒ void
- #wait(pid, stdout_path, stderr_path) ⇒ [ Process::Status, bool, bool ]
Constructor Details
#initialize(timeout_seconds: DEFAULT_TIMEOUT_SECONDS, max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES) ⇒ BoundedSubprocess
Returns a new instance of BoundedSubprocess.
35 36 37 38 39 40 41 42 |
# File 'lib/ibex/bounded_subprocess.rb', line 35 def initialize(timeout_seconds: DEFAULT_TIMEOUT_SECONDS, max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES) raise ArgumentError, "timeout_seconds must be positive" unless timeout_seconds.positive? raise ArgumentError, "max_output_bytes must be positive" unless max_output_bytes.positive? @timeout_seconds = timeout_seconds @max_output_bytes = max_output_bytes end |
Instance Method Details
#monotonic_time ⇒ Float
167 168 169 |
# File 'lib/ibex/bounded_subprocess.rb', line 167 def monotonic_time Process.clock_gettime(Process::CLOCK_MONOTONIC) end |
#output_exceeded?(stdout_path, stderr_path) ⇒ Boolean
157 158 159 |
# File 'lib/ibex/bounded_subprocess.rb', line 157 def output_exceeded?(stdout_path, stderr_path) File.size(stdout_path) > @max_output_bytes || File.size(stderr_path) > @max_output_bytes end |
#process_group_alive?(pid) ⇒ Boolean
134 135 136 137 138 139 140 141 |
# File 'lib/ibex/bounded_subprocess.rb', line 134 def process_group_alive?(pid) Process.kill(0, -pid) true rescue Errno::ESRCH false rescue Errno::EPERM true end |
#process_groups? ⇒ Boolean
144 145 146 |
# File 'lib/ibex/bounded_subprocess.rb', line 144 def process_groups? !RUBY_PLATFORM.match?(/mswin|mingw|cygwin/) end |
#process_status(pid) ⇒ Process::Status
149 150 151 152 153 154 |
# File 'lib/ibex/bounded_subprocess.rb', line 149 def process_status(pid) waited = Process.waitpid2(pid, Process::WNOHANG) return waited.last if waited raise Ibex::Error, "(subprocess):1:1: child #{pid} exited without a wait status" end |
#read_bounded(path) ⇒ String
162 163 164 |
# File 'lib/ibex/bounded_subprocess.rb', line 162 def read_bounded(path) File.binread(path, @max_output_bytes) end |
#run(command, input:) ⇒ Result
45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/ibex/bounded_subprocess.rb', line 45 def run(command, input:) raise ArgumentError, "command must not be empty" if command.empty? Tempfile.create("ibex-subprocess-input") do |stdin| Tempfile.create("ibex-subprocess-stdout") do |stdout| Tempfile.create("ibex-subprocess-stderr") do |stderr| run_with_files(command, input, stdin, stdout, stderr) end end end end |
#run_with_files(command, input, stdin, stdout, stderr) ⇒ Result
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/ibex/bounded_subprocess.rb', line 60 def run_with_files(command, input, stdin, stdout, stderr) stdin.binmode stdin.write(input) stdin.flush stdin.rewind pid = spawn_child(command, stdin, stdout, stderr) status, timed_out, output_limited = wait(pid, stdout.path, stderr.path) terminate_descendants(pid) Result.new( stdout: read_bounded(stdout.path), stderr: read_bounded(stderr.path), status: status, timed_out: timed_out, output_limited: output_limited ).freeze ensure terminate(pid) if pid && !status end |
#signal_process_tree(signal, pid) ⇒ void
This method returns an undefined value.
129 130 131 |
# File 'lib/ibex/bounded_subprocess.rb', line 129 def signal_process_tree(signal, pid) Process.kill(signal, process_groups? ? -pid : pid) end |
#spawn_child(command, stdin, stdout, stderr) ⇒ Integer
77 78 79 80 81 |
# File 'lib/ibex/bounded_subprocess.rb', line 77 def spawn_child(command, stdin, stdout, stderr) = { in: stdin, out: stdout, err: stderr } #: Hash[Symbol, Object?] [:pgroup] = true if process_groups? spawn(*command, **) end |
#terminate(pid) ⇒ Process::Status
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/ibex/bounded_subprocess.rb', line 99 def terminate(pid) signal_process_tree("TERM", pid) deadline = monotonic_time + TERMINATION_GRACE_SECONDS loop do waited = Process.waitpid2(pid, Process::WNOHANG) return waited.last if waited break if monotonic_time >= deadline sleep(POLL_SECONDS) end signal_process_tree("KILL", pid) Process.waitpid2(pid).last rescue Errno::ESRCH, Errno::ECHILD process_status(pid) end |
#terminate_descendants(pid) ⇒ void
This method returns an undefined value.
116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/ibex/bounded_subprocess.rb', line 116 def terminate_descendants(pid) return unless process_groups? return unless process_group_alive?(pid) deadline = monotonic_time + TERMINATION_GRACE_SECONDS Process.kill("TERM", -pid) sleep(POLL_SECONDS) while process_group_alive?(pid) && monotonic_time < deadline Process.kill("KILL", -pid) if process_group_alive?(pid) rescue Errno::ESRCH nil end |
#wait(pid, stdout_path, stderr_path) ⇒ [ Process::Status, bool, bool ]
85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/ibex/bounded_subprocess.rb', line 85 def wait(pid, stdout_path, stderr_path) deadline = monotonic_time + @timeout_seconds loop do waited = Process.waitpid2(pid, Process::WNOHANG) return [waited.last, false, false] if waited return [terminate(pid), false, true] if output_exceeded?(stdout_path, stderr_path) return [terminate(pid), true, false] if monotonic_time >= deadline sleep(POLL_SECONDS) end end |