Skip to main content

PTY Daemon Architecture

The PTY Daemon (pty-daemon-rust) is a standalone Rust binary that manages PTY sessions. It survives app crashes and enables session recovery.

Overview

┌─────────────────────────────────────────────────────────────────────┐
│                    ARCHITECTURE OVERVIEW                            │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   ┌───────────────────────────────────────────────────────────────┐ │
│   │                    Tauri App Process                          │ │
│   │                                                               │ │
│   │   ┌─────────────┐    MPSC     ┌─────────────────────────────┐ │ │
│   │   │ IPC Handler │───Channel──→│      SessionActor           │ │ │
│   │   │ (invoke)    │             │                             │ │ │
│   │   └─────────────┘             └─────────────────────────────┘ │ │
│   │                                                               │ │
│   └───────────────────────────────────────────────────────────────┘ │
│                                                                     │
│   ┌───────────────────────────────────────────────────────────────┐ │
│   │              PTY Daemon (Detached Process)                    │ │
│   │              pty-daemon-rust (953KB)                          │ │
│   │                                                               │ │
│   │   ┌──────────────────┐      ┌──────────────────────────────┐  │ │
│   │   │ Control Socket   │      │ portable-pty                 │  │ │
│   │   │ daemon-control-  │      │ (Pure Rust PTY)              │  │ │
│   │   │ {PID}.sock       │      └──────────────────────────────┘  │ │
│   │   └──────────────────┘                                        │ │
│   │   ┌──────────────────┐                                        │ │
│   │   │ Session Sockets  │  ← One per terminal session            │ │
│   │   │ session-{ID}.sock│                                        │ │
│   │   └──────────────────┘                                        │ │
│   │                                                               │ │
│   │   App Crash → Daemon Survives → Sessions Recoverable          │ │
│   └───────────────────────────────────────────────────────────────┘ │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Key Features

Crash Resilience

The daemon runs as a separate process from the main app:
  • App crashes don’t kill the daemon
  • Terminal sessions continue running
  • Sessions can be recovered on app restart

Unix Socket Communication

Each session has its own Unix socket:
/tmp/monolex-pty/
├── daemon-control-{PID}.sock    # Control channel
├── session-abc123.sock          # Session 1
├── session-def456.sock          # Session 2
└── session-ghi789.sock          # Session 3

Pure Rust PTY

Uses portable-pty crate for cross-platform PTY management:
  • No node-pty (native module issues)
  • Pure Rust implementation
  • macOS, Linux, Windows support

Protocol

Control Socket Commands

CommandDescription
CREATECreate new PTY session
LISTList active sessions
KILLTerminate a session
SHUTDOWNGraceful daemon shutdown

Session Socket Protocol

Binary protocol for data transfer:
┌─────────────────────────────────────────────────────────────────────┐
│  SESSION SOCKET PROTOCOL                                            │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Client → Daemon (Write to PTY):                                    │
│  ┌────────────┬─────────────────────────────────────────────────┐   │
│  │ Length (4B)│ Data (variable)                                 │   │
│  └────────────┴─────────────────────────────────────────────────┘   │
│                                                                     │
│  Daemon → Client (PTY Output):                                      │
│  ┌────────────┬─────────────────────────────────────────────────┐   │
│  │ Length (4B)│ Raw PTY output (binary + ANSI)                  │   │
│  └────────────┴─────────────────────────────────────────────────┘   │
│                                                                     │
│  Note: 4KB chunk size for Unix socket efficiency                    │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Lifecycle

Startup

// lib.rs - start_daemon command
#[tauri::command]
async fn start_daemon(state: State<'_, AppState>) -> Result<(), String> {
    // 1. Check if daemon already running
    if is_daemon_running(&state.pty_client).await {
        return Ok(());
    }

    // 2. Spawn daemon as sidecar
    let sidecar = app.shell()
        .sidecar("pty-daemon-rust")?
        .spawn()?;

    // 3. Wait for control socket
    wait_for_socket(&control_socket_path).await?;

    // 4. Connect PtyClient
    state.pty_client.connect().await?;

    Ok(())
}

Session Creation

1. Frontend: invoke("create_session", { cols: 120, rows: 40 })
2. SessionActor: Sends CREATE to daemon control socket
3. Daemon: Spawns PTY, creates session socket
4. Daemon: Returns session ID
5. SessionActor: Connects to session socket
6. SessionActor: Spawns GridWorker for this session
7. Frontend: Receives session ID, ready to use

Crash Recovery

1. App crashes
2. Daemon continues running (separate process)
3. PTY sessions stay alive
4. App restarts
5. Frontend: invoke("list_sessions")
6. Daemon: Returns list of active session IDs
7. Frontend: Reconnects to existing sessions
8. User sees their terminal sessions restored

Configuration

Binary Location

EnvironmentPath
Developmentsrc-tauri/binaries/pty-daemon-rust-{triple}
ProductionMacOS/pty-daemon-rust

Tauri Configuration

{
  "bundle": {
    "externalBin": [
      "binaries/pty-daemon-rust"
    ]
  }
}

Target Triple Suffix

PlatformSuffix
macOS ARMaarch64-apple-darwin
macOS Intelx86_64-apple-darwin
Linuxx86_64-unknown-linux-gnu
Windowsx86_64-pc-windows-msvc.exe

Logging

Daemon logs to /tmp/pty-daemon.log:
# View daemon logs
tail -f /tmp/pty-daemon.log

# Example output
[2024-01-15T10:30:00Z] INFO  Starting PTY daemon
[2024-01-15T10:30:00Z] INFO  Control socket: /tmp/monolex-pty/daemon-control-12345.sock
[2024-01-15T10:30:01Z] INFO  Session created: abc123
[2024-01-15T10:30:01Z] INFO  Session socket: /tmp/monolex-pty/session-abc123.sock

Troubleshooting

Check Daemon Status

# Check if daemon is running
ps aux | grep pty-daemon-rust

# Check socket files
ls -la /tmp/monolex-pty/

# View recent logs
tail -50 /tmp/pty-daemon.log

Kill Stale Daemon

# Kill daemon process
pkill -f pty-daemon-rust

# Clean up socket files
rm -rf /tmp/monolex-pty/

Code Signing (macOS)

Daemon binary MUST be signed or macOS will SIGKILL it:
# Sign the binary
codesign --force --sign "Developer ID Application: ..." \
  src-tauri/binaries/pty-daemon-rust-aarch64-apple-darwin

# Verify signature
codesign -dv src-tauri/binaries/pty-daemon-rust-aarch64-apple-darwin

Characteristics

MetricValue
Binary size~953KB
StartupFast
Memory per sessionLow
CommunicationUnix sockets

SMPC/OFAC Applied

PrincipleApplication
SMPCSingle responsibility: PTY management only
Simple protocol: length-prefixed binary
One socket per session: clear isolation
OFACCrash resilience emerged from separate process
Recovery pattern emerged from socket persistence