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.
144 145 146 |
# File 'lib/clacky/tools/terminal/persistent_session.rb', line 144 def discard @mutex.synchronize { @session = nil } end |
#release(session) ⇒ Object
Put a session back into the persistent slot after a successful command. If the slot is already filled (concurrent call built another one), we just discard the extra to avoid leaks.
130 131 132 133 134 135 136 137 138 139 |
# File 'lib/clacky/tools/terminal/persistent_session.rb', line 130 def release(session) @mutex.synchronize do if @session.nil? && session_healthy?(session) @session = session else # Either we already have one, or this one looks unhealthy. # Let the caller's cleanup_session path handle teardown. end end end |
#shutdown! ⇒ Object
Shut the persistent session down (typically at_exit).
149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/clacky/tools/terminal/persistent_session.rb', line 149 def shutdown! @mutex.synchronize do sess = @session @session = nil next unless sess begin Process.kill("TERM", sess.pid) rescue StandardError # ignore end end end |