Class: RubyClaude::Runner
- Inherits:
-
Object
- Object
- RubyClaude::Runner
- Defined in:
- lib/ruby_claude/runner.rb
Overview
Owns every subprocess concern: spawning claude via Open3, writing the prompt to stdin, enforcing the timeout by killing the child, capturing output, and translating spawn failures into BinaryNotFoundError.
The runner is stateless, so a single instance is safe to share across threads. The Client accepts an injected runner so tests never spawn.
Constant Summary collapse
- KILL_GRACE =
Seconds to wait after
SIGTERMbefore escalating toSIGKILL. 2
Instance Method Summary collapse
-
#run(argv:, env:, cwd:, timeout:, stdin: nil) ⇒ RunResult
Run the command to completion and capture its output.
-
#stream(argv:, env:, cwd:, timeout:, stdin: nil) {|line| ... } ⇒ RunResult
Run the command and yield each non-empty stdout line as it arrives.
Instance Method Details
#run(argv:, env:, cwd:, timeout:, stdin: nil) ⇒ RunResult
Run the command to completion and capture its output.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/ruby_claude/runner.rb', line 36 def run(argv:, env:, cwd:, timeout:, stdin: nil) spawn(argv, env, cwd) do |stdin_io, stdout_io, stderr_io, wait_thr| out_reader = Thread.new { stdout_io.read } err_reader = Thread.new { stderr_io.read } write_stdin(stdin_io, stdin) if wait_thr.join(timeout).nil? terminate(wait_thr) out_reader.kill err_reader.kill raise TimeoutError, "claude did not finish within #{timeout}s; the process was killed" end RunResult.new( stdout: out_reader.value, stderr: err_reader.value, exit_status: wait_thr.value.exitstatus ) end end |
#stream(argv:, env:, cwd:, timeout:, stdin: nil) {|line| ... } ⇒ RunResult
Run the command and yield each non-empty stdout line as it arrives.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/ruby_claude/runner.rb', line 64 def stream(argv:, env:, cwd:, timeout:, stdin: nil) spawn(argv, env, cwd) do |stdin_io, stdout_io, stderr_io, wait_thr| err_reader = Thread.new { stderr_io.read } # Write stdin on its own thread so a prompt larger than the OS pipe # buffer can't deadlock against stdout we haven't started reading yet. writer = Thread.new { write_stdin(stdin_io, stdin) } timed_out = false watchdog = Thread.new do sleep(timeout) timed_out = true terminate(wait_thr) end begin stdout_io.each_line do |line| chomped = line.chomp yield chomped unless chomped.empty? end ensure watchdog.kill writer.join end raise TimeoutError, "claude streaming exceeded #{timeout}s; the process was killed" if timed_out RunResult.new(stdout: nil, stderr: err_reader.value, exit_status: wait_thr.value.exitstatus) end end |