Class: Clacky::Tools::Terminal::SessionManager
- Inherits:
-
Object
- Object
- Clacky::Tools::Terminal::SessionManager
- Defined in:
- lib/clacky/tools/terminal/session_manager.rb
Overview
In-process registry of interactive PTY sessions.
Lifecycle: sessions die with the openclacky process because the child bash is a grandchild of openclacky (PTY.spawn forks then execs), and we also SIGKILL them on interpreter exit via an at_exit hook.
Thread-safety: all mutations go through a class-level Mutex. The reader thread writes to Session#log_io concurrently with the main thread reading log_file, but File IO is append-safe on POSIX so we don’t need to lock reads — we just pin them by byte offset.
Status values:
"starting" - PTY spawned, setup in progress
"running" - ready to receive commands
"exited" - child process ended
"killed" - we signalled it
Defined Under Namespace
Classes: Session
Class Method Summary collapse
- .advance_offset(id, new_offset) ⇒ Object
- .allocate_log_file ⇒ Object
-
.forget(id) ⇒ Object
Forget a session (after it has been killed/exited).
- .get(id) ⇒ Object
-
.kill(id, signal: "TERM") ⇒ Object
Send signal to child, mark as killed.
-
.kill_all! ⇒ Object
Kill every live session.
- .list ⇒ Object
- .log_dir ⇒ Object
-
.mark_running(id) ⇒ Object
Mark running (called by the Terminal action after setup completes).
- .refresh(id) ⇒ Object
- .refresh_all ⇒ Object
-
.register(pid:, command:, cwd:, log_file:, log_io:, reader:, writer:, reader_thread:, mode:, marker_token: nil, shell_name: nil) ⇒ Object
Register a new session.
-
.reset! ⇒ Object
Test-only: clear state without killing processes.
Class Method Details
.advance_offset(id, new_offset) ⇒ Object
156 157 158 159 160 161 |
# File 'lib/clacky/tools/terminal/session_manager.rb', line 156 def advance_offset(id, new_offset) @mutex.synchronize do s = @sessions[id] s.read_offset = new_offset if s end end |
.allocate_log_file ⇒ Object
171 172 173 174 175 176 |
# File 'lib/clacky/tools/terminal/session_manager.rb', line 171 def allocate_log_file @mutex.synchronize do next_id = @next_id + 1 File.join(log_dir, "#{next_id}.log") end end |
.forget(id) ⇒ Object
Forget a session (after it has been killed/exited). Does NOT kill the process — callers should kill first.
116 117 118 |
# File 'lib/clacky/tools/terminal/session_manager.rb', line 116 def forget(id) @mutex.synchronize { @sessions.delete(id) } end |
.get(id) ⇒ Object
85 86 87 |
# File 'lib/clacky/tools/terminal/session_manager.rb', line 85 def get(id) @mutex.synchronize { @sessions[id] } end |
.kill(id, signal: "TERM") ⇒ Object
Send signal to child, mark as killed. Returns the Session, or nil if unknown.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/clacky/tools/terminal/session_manager.rb', line 96 def kill(id, signal: "TERM") session = @mutex.synchronize { @sessions[id] } return nil unless session begin Process.kill(signal, session.pid) rescue Errno::ESRCH, Errno::EPERM # Already dead — fall through and mark killed. end @mutex.synchronize do if session.status == "starting" || session.status == "running" session.status = "killed" end end session end |
.kill_all! ⇒ Object
Kill every live session. Called from at_exit.
179 180 181 182 183 184 185 |
# File 'lib/clacky/tools/terminal/session_manager.rb', line 179 def kill_all! (@sessions.values rescue []).each do |s| next if s.status == "exited" || s.status == "killed" Process.kill("KILL", s.pid) rescue nil s.log_io&.close rescue nil end end |
.list ⇒ Object
89 90 91 92 |
# File 'lib/clacky/tools/terminal/session_manager.rb', line 89 def list refresh_all @mutex.synchronize { @sessions.values.sort_by(&:id) } end |
.log_dir ⇒ Object
163 164 165 166 167 168 169 |
# File 'lib/clacky/tools/terminal/session_manager.rb', line 163 def log_dir @log_dir ||= begin dir = File.join(Dir.tmpdir, "clacky-terminals-#{Process.pid}") FileUtils.mkdir_p(dir) dir end end |
.mark_running(id) ⇒ Object
Mark running (called by the Terminal action after setup completes).
149 150 151 152 153 154 |
# File 'lib/clacky/tools/terminal/session_manager.rb', line 149 def mark_running(id) @mutex.synchronize do session = @sessions[id] session.status = "running" if session && session.status == "starting" end end |
.refresh(id) ⇒ Object
140 141 142 143 144 145 146 |
# File 'lib/clacky/tools/terminal/session_manager.rb', line 140 def refresh(id) @mutex.synchronize do session = @sessions[id] refresh_locked(session) if session session end end |
.refresh_all ⇒ Object
134 135 136 137 138 |
# File 'lib/clacky/tools/terminal/session_manager.rb', line 134 def refresh_all @mutex.synchronize do @sessions.each_value { |s| refresh_locked(s) } end end |
.register(pid:, command:, cwd:, log_file:, log_io:, reader:, writer:, reader_thread:, mode:, marker_token: nil, shell_name: nil) ⇒ Object
Register a new session. Caller has already spawned the PTY and started the reader thread; we just record the metadata.
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 82 83 |
# File 'lib/clacky/tools/terminal/session_manager.rb', line 56 def register(pid:, command:, cwd:, log_file:, log_io:, reader:, writer:, reader_thread:, mode:, marker_token: nil, shell_name: nil) @mutex.synchronize do @next_id += 1 session = Session.new( id: @next_id, pid: pid, command: command, cwd: cwd, started_at: Time.now, log_file: log_file, log_io: log_io, reader: reader, writer: writer, reader_thread: reader_thread, status: "starting", exit_code: nil, mode: mode, marker_token: marker_token, marker_regex: marker_token ? /__CLACKY_DONE_#{marker_token}_(\d+)__/ : nil, read_offset: 0, mutex: Mutex.new, shell_name: shell_name ) @sessions[session.id] = session session end end |
.reset! ⇒ Object
Test-only: clear state without killing processes.
188 189 190 191 192 193 194 |
# File 'lib/clacky/tools/terminal/session_manager.rb', line 188 def reset! @mutex.synchronize do @sessions.clear @next_id = 0 @log_dir = nil end end |