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.



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