Class: RubyClaude::Runner

Inherits:
Object
  • Object
show all
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 SIGTERM before escalating to SIGKILL.

2

Instance Method Summary collapse

Instance Method Details

#run(argv:, env:, cwd:, timeout:, stdin: nil) ⇒ RunResult

Run the command to completion and capture its output.

Parameters:

  • argv (Array<String>)

    the command and its arguments

  • env (Hash)

    environment overrides (nil values unset a variable)

  • cwd (String, nil)

    working directory

  • timeout (Numeric)

    seconds before the child is killed

  • stdin (String, nil) (defaults to: nil)

    data to write to the child’s stdin

Returns:

Raises:



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.

Parameters:

  • argv (Array<String>)

    the command and its arguments

  • env (Hash)

    environment overrides (nil values unset a variable)

  • cwd (String, nil)

    working directory

  • timeout (Numeric)

    seconds before the child is killed

  • stdin (String, nil) (defaults to: nil)

    data to write to the child’s stdin

Yield Parameters:

  • line (String)

    one chomped, non-empty stdout line

Returns:

  • (RunResult)

    with stdout nil (it was streamed, not captured)

Raises:



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