Class: AgentsControl::Executor

Inherits:
Object
  • Object
show all
Defined in:
lib/agents_control/executor.rb

Overview

The single point through which the utility talks to the outside world.

Exists for the "adapter isolation" rule: osascript can hang dead when iTerm2 shows a beachball, and without a hard timeout that takes the whole daemon down with it. No adapter calls Open3 directly.

Tests swap this for FakeExecutor — no real terminal needed in CI.

Defined Under Namespace

Classes: Result

Constant Summary collapse

DEFAULT_TIMEOUT =
10
TIMEOUT_STATUS =

status 124 — coreutils timeout(1) convention, 127 — "command not found".

124
NOT_FOUND_STATUS =
127

Instance Method Summary collapse

Constructor Details

#initialize(timeout: DEFAULT_TIMEOUT) ⇒ Executor

Returns a new instance of Executor.



26
27
28
# File 'lib/agents_control/executor.rb', line 26

def initialize(timeout: DEFAULT_TIMEOUT)
  @timeout = timeout
end

Instance Method Details

#run(*argv, stdin: nil, timeout: @timeout) ⇒ Object

Always returns a Result — never raises. A failing external command must not take the daemon down with it.



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
# File 'lib/agents_control/executor.rb', line 32

def run(*argv, stdin: nil, timeout: @timeout)
  Open3.popen3(*argv) do |input, output, errors, wait_thread|
    write_stdin(input, stdin)

    # Read on separate threads: otherwise a process that fills the
    # stderr buffer blocks waiting to be read, while we block waiting
    # for it to exit. report_on_exception is off on purpose: on a
    # timeout, the reader threads find popen3's pipe already closed
    # and raise IOError. That's expected and handled below, but Ruby
    # prints that trace to stderr by default, where it looks like a
    # real crash in the utility's console.
    out_reader = Thread.new { output.read }
    err_reader = Thread.new { errors.read }
    [out_reader, err_reader].each { |thread| thread.report_on_exception = false }

    return kill(wait_thread, out_reader, err_reader, timeout) unless wait_thread.join(timeout)

    Result.new(
      stdout: out_reader.value.to_s,
      stderr: err_reader.value.to_s,
      status: wait_thread.value.exitstatus || -1
    )
  end
rescue Errno::ENOENT, Errno::EACCES => e
  Result.new(stdout: "", stderr: e.message, status: NOT_FOUND_STATUS)
end