Class: Kward::LocalPtyCommandRunner
- Inherits:
-
Object
- Object
- Kward::LocalPtyCommandRunner
- Defined in:
- lib/kward/local_pty_command_runner.rb
Overview
Low-level pseudo-terminal command runner with bounded capture, timeout, cancellation, and optional output streaming. This gives child processes a TTY without trying to emulate an interactive terminal.
Constant Summary collapse
- Result =
LocalCommandRunner::Result
- READ_SIZE =
4096- DEFAULT_ROWS =
24- DEFAULT_COLUMNS =
80
Instance Method Summary collapse
-
#initialize(timeout_seconds:, max_output_bytes:, terminate_on_output_limit: false, window_size_provider: nil) ⇒ LocalPtyCommandRunner
constructor
A new instance of LocalPtyCommandRunner.
- #run(*command, env: {}, cwd: Dir.pwd, cancellation: nil, &block) ⇒ Object
Constructor Details
#initialize(timeout_seconds:, max_output_bytes:, terminate_on_output_limit: false, window_size_provider: nil) ⇒ LocalPtyCommandRunner
Returns a new instance of LocalPtyCommandRunner.
19 20 21 22 23 24 25 |
# File 'lib/kward/local_pty_command_runner.rb', line 19 def initialize(timeout_seconds:, max_output_bytes:, terminate_on_output_limit: false, window_size_provider: nil) @timeout_seconds = timeout_seconds.to_i.positive? ? timeout_seconds.to_i : 30 @max_output_bytes = max_output_bytes.to_i.positive? ? max_output_bytes.to_i : 128 * 1024 @terminate_on_output_limit = terminate_on_output_limit @window_size_provider = window_size_provider @window_size = nil end |
Instance Method Details
#run(*command, env: {}, cwd: Dir.pwd, cancellation: nil, &block) ⇒ Object
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/kward/local_pty_command_runner.rb', line 27 def run(*command, env: {}, cwd: Dir.pwd, cancellation: nil, &block) cancellation&.raise_if_cancelled! output = +"" captured_bytes = 0 truncated = false timed_out = false cancelled = false pid = nil status = nil PTY.spawn(env.to_h, *command, chdir: cwd.to_s) do |reader, _writer, child_pid| pid = child_pid update_window_size(reader, pid) cancellation&.on_cancel do cancelled = true terminate_process_group(pid) end begin deadline = Time.now + @timeout_seconds loop do cancellation&.raise_if_cancelled! raise Timeout::Error if Time.now >= deadline update_window_size(reader, pid) readable, = IO.select([reader], nil, nil, 0.02) next unless readable chunk = read_chunk(reader) break if chunk.nil? chunk = normalize_line_endings(chunk) captured_bytes, truncated, captured_chunk = capture_chunk(chunk, output, captured_bytes, truncated) block&.call(:stdout, captured_chunk) unless captured_chunk.empty? terminate_process_group(pid) if truncated && @terminate_on_output_limit end rescue Errno::EIO nil end status = wait_for_status(pid) end cancellation&.raise_if_cancelled! if cancelled Result.new(stdout: output, stderr: "", exit_status: exit_status(status), timed_out: false, truncated: truncated) rescue Timeout::Error timed_out = true terminate_process_group(pid) if pid wait_for_status(pid) if pid Result.new(stdout: output, stderr: "", exit_status: nil, timed_out: timed_out, truncated: truncated) rescue Cancellation::CancelledError terminate_process_group(pid) if pid wait_for_status(pid) if pid raise end |