Class: ShellSession
- Inherits:
-
Object
- Object
- ShellSession
- Defined in:
- lib/shell_session.rb
Overview
Persistent shell session backed by a PTY with FIFO-based stderr separation. Commands share working directory, environment variables, and shell history within a conversation. Multiple tools share the same session.
Auto-recovers from timeouts and crashes: if the shell dies, the next command transparently respawns a fresh shell and restores the working directory.
Uses IO.select-based deadlines instead of Timeout.timeout for all PTY reads. Timeout.timeout is unsafe with PTY I/O — it uses Thread.raise which can corrupt mutex state, leave resources inconsistent, and cause exceptions to fire outside handler blocks when nested.
Instance Attribute Summary collapse
-
#pwd ⇒ String?
readonly
Current working directory of the shell process.
Class Method Summary collapse
-
.cleanup_all ⇒ Object
Finalize all live sessions.
-
.cleanup_orphans ⇒ Object
Remove stale FIFO files left by crashed processes.
- .register(session) ⇒ Object private
- .unregister(session) ⇒ Object private
Instance Method Summary collapse
-
#alive? ⇒ Boolean
Whether the shell process is still running.
-
#finalize ⇒ Object
Clean up PTY, FIFO, and child process.
-
#initialize(session_id:) ⇒ ShellSession
constructor
A new instance of ShellSession.
-
#run(command, timeout: nil, interrupt_check: nil) ⇒ Hash
Execute a command in the persistent shell.
Constructor Details
#initialize(session_id:) ⇒ ShellSession
Returns a new instance of ShellSession.
31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/shell_session.rb', line 31 def initialize(session_id:) @session_id = session_id @mutex = Mutex.new @fifo_path = File.join(Dir.tmpdir, "anima-stderr-#{Process.pid}-#{SecureRandom.hex(8)}") @alive = false @finalized = false @pwd = nil @read_buffer = +"" self.class.cleanup_orphans start self.class.register(self) end |
Instance Attribute Details
#pwd ⇒ String? (readonly)
Returns current working directory of the shell process.
28 29 30 |
# File 'lib/shell_session.rb', line 28 def pwd @pwd end |
Class Method Details
.cleanup_all ⇒ Object
Finalize all live sessions. Called automatically via at_exit.
98 99 100 101 102 103 |
# File 'lib/shell_session.rb', line 98 def cleanup_all @sessions_mutex.synchronize do @sessions.each { |session| session.send(:teardown) } @sessions.clear end end |
.cleanup_orphans ⇒ Object
Remove stale FIFO files left by crashed processes. FIFO naming format: anima-stderr-pid-hex
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/shell_session.rb', line 107 def cleanup_orphans Dir.glob(File.join(Dir.tmpdir, "anima-stderr-*")).each do |path| match = File.basename(path).match(/\Aanima-stderr-(\d+)-/) next unless match pid = match[1].to_i next if pid <= 0 begin Process.kill(0, pid) rescue Errno::ESRCH begin File.delete(path) rescue SystemCallError # Best-effort cleanup end rescue Errno::EPERM # Process exists but we can't signal it — leave it end end end |
.register(session) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
88 89 90 |
# File 'lib/shell_session.rb', line 88 def register(session) @sessions_mutex.synchronize { @sessions << session } end |
.unregister(session) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
93 94 95 |
# File 'lib/shell_session.rb', line 93 def unregister(session) @sessions_mutex.synchronize { @sessions.delete(session) } end |
Instance Method Details
#alive? ⇒ Boolean
Returns whether the shell process is still running.
77 78 79 |
# File 'lib/shell_session.rb', line 77 def alive? @mutex.synchronize { @alive } end |
#finalize ⇒ Object
Clean up PTY, FIFO, and child process. Permanent — the session will not auto-respawn after this call.
68 69 70 71 72 73 74 |
# File 'lib/shell_session.rb', line 68 def finalize @mutex.synchronize do @finalized = true teardown end self.class.unregister(self) end |
#run(command, timeout: nil, interrupt_check: nil) ⇒ Hash
Execute a command in the persistent shell. Respawns the shell automatically if the previous session died (timeout, crash, etc.).
56 57 58 59 60 61 62 63 64 |
# File 'lib/shell_session.rb', line 56 def run(command, timeout: nil, interrupt_check: nil) @mutex.synchronize do return {error: "Shell session is not running"} if @finalized restart unless @alive execute_in_pty(command, timeout: timeout, interrupt_check: interrupt_check) end rescue => error # rubocop:disable Lint/RescueException -- LLM must always get a result hash, never a stack trace {error: "#{error.class}: #{error.}"} end |