Class: Clacky::Tools::Terminal::SessionManager

Inherits:
Object
  • Object
show all
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

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_fileObject



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

.listObject



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_dirObject



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_allObject



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