Class: PiAgent::Transport::Subprocess

Inherits:
Object
  • Object
show all
Defined in:
lib/pi_agent/transport/subprocess.rb

Overview

Runs pi --mode rpc as a local child process and speaks NDJSON over its stdio.

One reader thread per pipe; stdout lines are JSON-parsed and dispatched to on_message; stderr lines are forwarded raw to on_stderr. Writes are serialized through a mutex so concurrent senders don't interleave JSON payloads on stdin.

Constant Summary collapse

DEFAULT_CHUNK_SIZE =
4096
DEFAULT_CLOSE_TIMEOUT =
5
EXIT_DRAIN_TIMEOUT =

How long to let the stdout reader drain buffered output after the child exits before reporting the death (or closing) anyway — a descendant that inherited the pipe can hold it open indefinitely.

1

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(command:, env: {}, cwd: nil, on_message: nil, on_stderr: nil, on_close: nil) ⇒ Subprocess

cwd sets the child's working directory — pi's built-in tools (bash/read/edit/...) operate relative to it. nil leaves the child in this process's working directory.

on_close, when given, is invoked exactly once with a short human-readable reason when the child exits on its own (see watch_exit). A caller-initiated #close is not reported.



32
33
34
35
36
37
38
39
40
41
# File 'lib/pi_agent/transport/subprocess.rb', line 32

def initialize(command:, env: {}, cwd: nil, on_message: nil, on_stderr: nil, on_close: nil)
  @command = Array(command)
  @env = env.transform_keys(&:to_s)
  @cwd = cwd
  @on_message = on_message
  @on_stderr = on_stderr
  @on_close = on_close
  @write_mutex = Mutex.new
  @owner_closed = false
end

Instance Attribute Details

#pidObject (readonly)

Returns the value of attribute pid.



23
24
25
# File 'lib/pi_agent/transport/subprocess.rb', line 23

def pid
  @pid
end

Instance Method Details

#alive?Boolean

Returns:

  • (Boolean)


82
83
84
# File 'lib/pi_agent/transport/subprocess.rb', line 82

def alive?
  !!@wait_thr&.alive?
end

#close(timeout: DEFAULT_CLOSE_TIMEOUT) ⇒ Object



67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/pi_agent/transport/subprocess.rb', line 67

def close(timeout: DEFAULT_CLOSE_TIMEOUT)
  return if mark_owner_closed!

  safe_close(@stdin)
  wait_for_exit(timeout)
  readers = [@stdout_thread, @stderr_thread].compact
  # Bounded drain: a descendant that inherited a pipe can hold it
  # open past the child's exit; force the readers out by closing
  # the pipes after the window rather than hanging the close.
  readers.each { |t| t.join(EXIT_DRAIN_TIMEOUT) }
  safe_close(@stdout)
  safe_close(@stderr)
  (readers + [@exit_watch_thread]).compact.each(&:join)
end

#exit_statusObject



86
87
88
# File 'lib/pi_agent/transport/subprocess.rb', line 86

def exit_status
  @wait_thr&.value
end

#startObject



43
44
45
46
47
48
49
50
51
52
53
# File 'lib/pi_agent/transport/subprocess.rb', line 43

def start
  @stdin, @stdout, @stderr, @wait_thr = Open3.popen3(*spawn_args)
  @pid = @wait_thr.pid
  @stdin.binmode
  @stdout.binmode
  @stderr.binmode
  @stdout_thread = Thread.new { read_loop(@stdout, :stdout) }
  @stderr_thread = Thread.new { read_loop(@stderr, :stderr) }
  @exit_watch_thread = Thread.new { watch_exit }
  self
end

#write(obj) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
# File 'lib/pi_agent/transport/subprocess.rb', line 55

def write(obj)
  payload = "#{JSON.generate(obj)}\n"
  @write_mutex.synchronize do
    raise ProtocolError, "Transport closed" if @owner_closed

    @stdin.write(payload)
    @stdin.flush
  end
rescue Errno::EPIPE
  raise ProtocolError, "Broken pipe writing to subprocess (process may have exited)"
end