Class: Clacky::Tools::Terminal::PersistentSessionPool

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

Constructor Details

#initializePersistentSessionPool

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

.instanceObject



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.message
      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

#discardObject

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