Class: ShellSession

Inherits:
Object
  • Object
show all
Defined in:
lib/shell_session.rb

Overview

Persistent shell session backed by a PTY with FIFO-based stderr separation. Commands share working directory, environment variables, and shell history within a conversation. Multiple tools share the same session.

Examples:

session = ShellSession.new(session_id: 42)
session.run("cd /tmp")
session.run("pwd")
# => {stdout: "/tmp", stderr: "", exit_code: 0}
session.finalize

Constant Summary collapse

COMMAND_TIMEOUT =
30
MAX_OUTPUT_BYTES =
100_000

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(session_id:) ⇒ ShellSession

Returns a new instance of ShellSession.

Parameters:

  • session_id (Integer, String)

    unique identifier for logging/diagnostics



26
27
28
29
30
31
32
33
34
35
# File 'lib/shell_session.rb', line 26

def initialize(session_id:)
  @session_id = session_id
  @mutex = Mutex.new
  @fifo_path = File.join(Dir.tmpdir, "anima-stderr-#{Process.pid}-#{SecureRandom.hex(8)}")
  @alive = false
  @pwd = nil
  self.class.cleanup_orphans
  start
  self.class.register(self)
end

Instance Attribute Details

#pwdString? (readonly)

Returns current working directory of the shell process.

Returns:

  • (String, nil)

    current working directory of the shell process



23
24
25
# File 'lib/shell_session.rb', line 23

def pwd
  @pwd
end

Class Method Details

.cleanup_allObject

Finalize all live sessions. Called automatically via at_exit.



77
78
79
80
81
82
# File 'lib/shell_session.rb', line 77

def cleanup_all
  @sessions_mutex.synchronize do
    @sessions.each { |session| session.send(:shutdown) }
    @sessions.clear
  end
end

.cleanup_orphansObject

Remove stale FIFO files left by crashed processes. FIFO naming format: anima-stderr-pid-hex



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/shell_session.rb', line 86

def cleanup_orphans
  Dir.glob(File.join(Dir.tmpdir, "anima-stderr-*")).each do |path|
    match = File.basename(path).match(/\Aanima-stderr-(\d+)-/)
    next unless match

    pid = match[1].to_i
    next if pid <= 0

    begin
      Process.kill(0, pid)
    rescue Errno::ESRCH
      begin
        File.delete(path)
      rescue SystemCallError
        # Best-effort cleanup
      end
    rescue Errno::EPERM
      # Process exists but we can't signal it — leave it
    end
  end
end

.register(session) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



67
68
69
# File 'lib/shell_session.rb', line 67

def register(session)
  @sessions_mutex.synchronize { @sessions << session }
end

.unregister(session) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



72
73
74
# File 'lib/shell_session.rb', line 72

def unregister(session)
  @sessions_mutex.synchronize { @sessions.delete(session) }
end

Instance Method Details

#alive?Boolean

Returns whether the shell process is still running.

Returns:

  • (Boolean)

    whether the shell process is still running



56
57
58
# File 'lib/shell_session.rb', line 56

def alive?
  @mutex.synchronize { @alive }
end

#finalizeObject

Clean up PTY, FIFO, and child process.



50
51
52
53
# File 'lib/shell_session.rb', line 50

def finalize
  @mutex.synchronize { shutdown }
  self.class.unregister(self)
end

#run(command) ⇒ Hash

Execute a command in the persistent shell.

Parameters:

  • command (String)

    bash command to execute

Returns:

  • (Hash)

    with :stdout, :stderr, :exit_code keys on success

  • (Hash)

    with :error key on failure



42
43
44
45
46
47
# File 'lib/shell_session.rb', line 42

def run(command)
  @mutex.synchronize do
    return {error: "Shell session is not running"} unless @alive
    execute_in_pty(command)
  end
end