Class: Clacky::Tools::Terminal::PersistentSessionPool
- Inherits:
-
Object
- Object
- Clacky::Tools::Terminal::PersistentSessionPool
- Defined in:
- lib/clacky/tools/terminal/persistent_session.rb
Overview
Holds (at most) ONE long-lived PTY shell session that is reused across multiple terminal calls. Reusing the session eliminates the ~1s cold-start cost of ‘zsh -l -i` / `bash -l -i` on every command.
Reuse rules:
- Only non-background, non-dedicated calls take from the persistent
slot. background / env-overridden calls spawn a fresh session.
- Before each call we diff rc-file mtime(s); if changed, we
`source` them once inside the live shell so the user sees freshly
installed PATH / functions / aliases on the very next command.
- If a command leaves the session in a non-clean state (marker not
hit — i.e. the program is still running and interactive), the
session is "donated" to the caller as a dedicated session_id and
the persistent slot is cleared (next call rebuilds a fresh one).
- If cleanup fails or a spawn fails, we transparently fall back to
the old one-shot `bash --noprofile --norc -i` spawn.
Thread safety:
- Each persistent session has its own mutex (Session#mutex) that
serialises PTY writes (unchanged).
- The PersistentSessionPool itself is guarded by a class-level
mutex so concurrent terminal calls don't race on acquire/release.
Class Method Summary collapse
Instance Method Summary collapse
-
#acquire(runner:, cwd: nil, env: nil) ⇒ Object
Acquire a persistent session for a new command.
-
#discard ⇒ Object
The caller has decided the session is unusable (e.g. command left an interactive program running).
-
#initialize ⇒ PersistentSessionPool
constructor
A new instance of PersistentSessionPool.
-
#release(session) ⇒ Object
Put a session back into the persistent slot after a successful command.
-
#shutdown! ⇒ Object
Shut the persistent session down (typically at_exit).
Constructor Details
#initialize ⇒ PersistentSessionPool
Returns a new instance of PersistentSessionPool.
48 49 50 51 52 53 54 |
# File 'lib/clacky/tools/terminal/persistent_session.rb', line 48 def initialize @mutex = Mutex.new @session = nil # currently-idle persistent session, or nil @rc_fingerprint = nil # mtime snapshot used to detect rc changes @last_env_keys = [] # keys we exported last time; unset them on env change @disabled = false # set to true after a spawn failure to stop retrying end |
Class Method Details
.instance ⇒ Object
32 33 34 |
# File 'lib/clacky/tools/terminal/persistent_session.rb', line 32 def instance @instance ||= new end |
.reset! ⇒ Object
36 37 38 39 40 41 42 43 44 45 |
# File 'lib/clacky/tools/terminal/persistent_session.rb', line 36 def reset! if @instance begin @instance.shutdown! rescue StandardError # swallow — best-effort during tests / shutdown end end @instance = nil end |
Instance Method Details
#acquire(runner:, cwd: nil, env: nil) ⇒ Object
Acquire a persistent session for a new command.
Returns [session, reused:] where ‘session` is a running PTY session ready to accept a command (no concurrent command in flight). Raises SpawnFailed if we can’t build one.
‘reused:` is true when an existing session was handed out; false when we had to spawn a fresh one.
Side effects when reusing:
- Sources rc files if their mtimes changed.
- `cd`s to `cwd` if given.
- Resets env vars that were exported last time and exports the
new ones (only when `env` is non-nil).
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/clacky/tools/terminal/persistent_session.rb', line 70 def acquire(runner:, cwd: nil, env: nil) @mutex.synchronize do return [nil, false] if @disabled # 1) Make sure the stored session is still healthy. if @session unless session_healthy?(@session) drop_locked end end # 2) Spawn a fresh one if we don't have anything warm. unless @session begin @session = runner.spawn_persistent_session rescue StandardError => e @disabled = true raise SpawnFailed, e. end @rc_fingerprint = current_rc_fingerprint @last_env_keys = [] reused = false else reused = true end # 3) If rc files changed since last use, re-source them once. if reused && rc_changed? runner.source_rc_in_session(@session, rc_files_for_shell(@session.shell_name)) @rc_fingerprint = current_rc_fingerprint end # 4) Reset env — unset old, export new. if env && !env.empty? new_keys = env.keys.map(&:to_s) to_unset = @last_env_keys - new_keys runner.reset_env_in_session(@session, unset_keys: to_unset, set_env: env) @last_env_keys = new_keys elsif !@last_env_keys.empty? runner.reset_env_in_session(@session, unset_keys: @last_env_keys, set_env: {}) @last_env_keys = [] end # 5) cd to the requested directory. if cwd && Dir.exist?(cwd.to_s) runner.cd_in_session(@session, cwd.to_s) end session = @session # Remove it from the slot for the duration of the command so # a concurrent caller can't grab the same shell mid-run. @session = nil [session, reused] end end |
#discard ⇒ Object
The caller has decided the session is unusable (e.g. command left an interactive program running). Forget it without killing — the caller is keeping the PTY alive for their own use.
145 146 147 |
# File 'lib/clacky/tools/terminal/persistent_session.rb', line 145 def discard @mutex.synchronize { @session = nil } end |
#release(session) ⇒ Object
Put a session back into the persistent slot after a successful command. Returns true if stored (caller keeps the session), false if the slot was already filled or the session is unhealthy (caller MUST clean up the session — fds and process — itself).
131 132 133 134 135 136 137 138 139 140 |
# File 'lib/clacky/tools/terminal/persistent_session.rb', line 131 def release(session) @mutex.synchronize do if @session.nil? && session_healthy?(session) @session = session true else false end end end |
#shutdown! ⇒ Object
Shut the persistent session down (typically at_exit).
150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/clacky/tools/terminal/persistent_session.rb', line 150 def shutdown! @mutex.synchronize do sess = @session @session = nil next unless sess begin Process.kill("TERM", sess.pid) rescue StandardError # ignore end close_fds(sess) end end |