Files
rustdesk/terminal.md
RustDesk 5faf0ad3cf terminal works basically. (#12189)
* terminal works basically.
todo:
- persistent
- sessions restore
- web
- mobile

* missed terminal persistent option change

* android sdk 34 -> 35

* +#![cfg_attr(lt_1_77, feature(c_str_literals))]

* fixing ci

* fix ci

* fix ci for android

* try "Fix Android SDK Platform 35"

* fix android 34

* revert flutter_plugin_android_lifecycle to 2.0.17 which used in rustdesk 1.4.0

* refactor, but break something of desktop terminal (new tab showing loading)

* fix connecting...
2025-07-01 13:12:55 +08:00

14 KiB

RustDesk Terminal Service Implementation

Overview

The RustDesk terminal service provides remote terminal/shell access with support for multiple concurrent terminal sessions per connection. It features persistence support, allowing terminal sessions to survive connection drops and be resumed later.

Architecture

Client-Side (Flutter)

Terminal Connection Management

  • TerminalConnectionManager (flutter/lib/desktop/pages/terminal_connection_manager.dart)
    • Manages one FFI instance per peer (shared across all terminal tabs)
    • Tracks persistence settings per peer
    • Handles connection reference counting

Terminal Models

  • TerminalModel (flutter/lib/models/terminal_model.dart)
    • One instance per terminal tab
    • Handles terminal I/O and display using xterm package
    • Manages terminal state (opened, size, buffer)

UI Components

  • TerminalTabPage (flutter/lib/desktop/pages/terminal_tab_page.dart)
    • Manages multiple terminal tabs
    • Right-click menu for persistence toggle
    • Keyboard shortcuts (Cmd/Ctrl+Shift+T for new terminal)

Server-Side (Rust)

Terminal Service Structure

TerminalService {
    conn_id: i32,
    service_id: String,  // "tmp_{uuid}" or "persist_{uuid}"
    persist: bool,
}

PersistentTerminalService {
    service_id: String,
    sessions: HashMap<i32, TerminalSession>,  // terminal_id -> session
    next_terminal_id: i32,
    created_at: Instant,
    last_activity: Instant,
}

TerminalSession {
    terminal_id: i32,
    pty_pair: PtyPair,
    child: Box<dyn Child>,
    writer: Box<dyn Write>,
    reader: Box<dyn Read>,
    output_buffer: OutputBuffer,  // For reconnection
    rows: u16,
    cols: u16,
}

Message Protocol

Client → Server Messages

  1. Open Terminal
TerminalAction {
    open: OpenTerminal {
        terminal_id: i32,
        rows: u32,
        cols: u32,
    }
}
  1. Send Input
TerminalAction {
    data: TerminalData {
        terminal_id: i32,
        data: bytes,
    }
}
  1. Resize Terminal
TerminalAction {
    resize: ResizeTerminal {
        terminal_id: i32,
        rows: u32,
        cols: u32,
    }
}
  1. Close Terminal
TerminalAction {
    close: CloseTerminal {
        terminal_id: i32,
        force: bool,
    }
}

Server → Client Messages

  1. Terminal Opened
TerminalResponse {
    opened: TerminalOpened {
        terminal_id: i32,
        success: bool,
        message: string,
        pid: u32,
    }
}
  1. Terminal Output
TerminalResponse {
    data: TerminalData {
        terminal_id: i32,
        data: bytes,  // Base64 encoded in Flutter
    }
}
  1. Terminal Closed
TerminalResponse {
    closed: TerminalClosed {
        terminal_id: i32,
        exit_code: i32,
    }
}

Persistence Design

Service ID Convention

  • Temporary: "tmp_{uuid}" - Cleaned up after idle timeout
  • Persistent: "persist_{uuid}" - Survives disconnections

Persistence Flow

  1. User right-clicks terminal tab → "Enable terminal persistence"
  2. Client stores persistence preference in TerminalConnectionManager
  3. New terminals created with appropriate service ID prefix
  4. Service ID saved for future reconnection (TODO: implement storage)

Cleanup Rules

  • Temporary services (tmp_):

    • Removed after 1 hour idle time
    • Immediately removed when service loop exits
  • Persistent services:

    • Removed after 2 hours idle time IF empty
    • Survive connection drops
    • Can be reconnected using saved service ID

Cleanup Implementation

1. Automatic Background Cleanup

// Runs every 5 minutes
fn ensure_cleanup_task() {
    tokio::spawn(async {
        let mut interval = tokio::time::interval(Duration::from_secs(300));
        loop {
            interval.tick().await;
            cleanup_inactive_services();
        }
    });
}

2. Cleanup Logic

fn cleanup_inactive_services() {
    let now = Instant::now();
    
    for (service_id, service) in services.iter() {
        // Temporary services: clean up after 1 hour idle
        if service_id.starts_with("tmp_") && 
           now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT {
            to_remove.push(service_id);
        }
        // Persistent services: clean up after 2 hours IF empty
        else if !service_id.starts_with("tmp_") && 
                svc.sessions.is_empty() && 
                now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT * 2 {
            to_remove.push(service_id);
        }
    }
}

3. Service Loop Exit Cleanup

fn run(sp: EmptyExtraFieldService, _conn_id: i32, service_id: String) {
    // Service loop
    while sp.ok() {
        // Read and send terminal outputs...
    }
    
    // Clean up temporary services immediately on exit
    if service_id.starts_with("tmp_") {
        remove_service(&service_id);
    }
}

4. Session Cleanup Within Service

When a terminal is closed:

  • PTY process is terminated
  • Terminal session removed from service's HashMap
  • Resources (file descriptors, buffers) are freed
  • Service continues running for other terminals

5. Connection Drop Behavior

impl Drop for Connection {
    fn drop(&mut self) {
        if self.terminal {
            // Unsubscribe from terminal service
            server.subscribe(&service_name, self.inner.clone(), false);
        }
    }
}
  • Connection unsubscribes from service
  • Service loop continues if other subscribers exist
  • If no subscribers remain, sp.ok() returns false → service loop exits

6. Activity Tracking

last_activity is updated when:

  • New terminal opened
  • Input sent to terminal
  • Terminal resized
  • Output read from terminal
  • Any terminal operation occurs

7. Two-Phase Cleanup Process

// Collect services to remove (while holding lock)
let mut to_remove = Vec::new();
for (id, service) in services.iter() {
    if should_remove(service) {
        to_remove.push(id);
    }
}

// Remove services (after releasing lock)
drop(services);
for id in to_remove {
    remove_service(&id);
}

This prevents deadlock when removing services.

Key Features

Multiple Terminals per Connection

  • Single FFI connection shared by all terminal tabs
  • Each terminal has unique ID within the service
  • Independent PTY sessions per terminal

Output Buffering

  • Last 1MB of output buffered per terminal
  • Allows showing recent history on reconnection
  • Ring buffer with line-based storage

Cross-Platform Support

  • Unix/Linux/macOS: Uses default shell from $SHELL or /bin/bash
  • Windows: Uses %COMSPEC% or cmd.exe
  • PTY implementation via portable_pty crate

Non-Blocking I/O

  • PTY readers set to non-blocking mode (Unix)
  • Output polled at ~33fps for responsive display
  • Prevents blocking when no data available

Current Limitations

  1. Service ID Storage: Client doesn't persist service IDs yet
  2. Reconnection UI: No UI to recover previous sessions
  3. Authentication: No per-service authentication for reconnection
  4. Resource Limits: No configurable limits on terminals per service

Future Enhancements

  1. Proper Reconnection Flow:

    • Store service IDs in peer config
    • UI to list and recover previous sessions
    • Show buffered output on reconnection
  2. Security:

    • Authentication token for service recovery
    • Encryption of buffered output
    • Access control per terminal
  3. Advanced Features:

    • Terminal sharing between users
    • Session recording/playback
    • File transfer via terminal
    • Custom shell/command configuration

Code Locations

  • Server Implementation: src/server/terminal_service.rs
  • Connection Handler: src/server/connection.rs (handle_terminal_action)
  • Client Interface: src/ui_session_interface.rs (terminal methods)
  • Flutter FFI: src/flutter_ffi.rs (session_open_terminal, etc.)
  • Flutter Models: flutter/lib/models/terminal_model.dart
  • Flutter UI: flutter/lib/desktop/pages/terminal_*.dart

Usage

  1. Start Terminal Session:

    • Click terminal icon or use Ctrl/Cmd+Shift+T
    • Terminal opens with default shell
  2. Enable Persistence:

    • Right-click any terminal tab
    • Select "Enable terminal persistence"
    • All terminals for that peer become persistent
  3. Multiple Terminals:

    • Click "+" button or Ctrl/Cmd+Shift+T
    • Each terminal is independent
  4. Reconnection (TODO):

    • Connect to same peer
    • Previous terminals automatically restored
    • Recent output displayed

Implementation Issues & TODOs

Critical Missing Features

  1. Service ID Storage & Recovery

    • Need to store service_id in peer config when persistence enabled
    • Pass service_id in LoginRequest for reconnection
    • Handle service_id in server login flow
    • Return terminal list in LoginResponse
  2. Protocol Extensions Needed

    // In LoginRequest
    message Terminal {
        string service_id = 1;  // For reconnection
        bool persistent = 2;    // Request persistence
    }
    
    // In LoginResponse
    message TerminalServiceInfo {
        string service_id = 1;
        repeated TerminalSessionInfo sessions = 2;
    }
    
  3. Terminal Recovery Flow

    • Add RecoverTerminal action to restore specific terminal
    • Send buffered output on reconnection
    • Handle terminal size on recovery
    • UI to show available terminals

Current Design Issues

  1. Service Pattern Mismatch

    • Terminal service forced into broadcast service pattern
    • Should be direct connection resource, not shared service
    • Complex routing through service registry unnecessary
  2. Global State Management

    • TERMINAL_SERVICES static HashMap may cause issues
    • No proper service discovery mechanism
    • Cleanup task is global, not per-connection
  3. Resource Limits Missing

    • No limit on terminals per service
    • No limit on buffer size per terminal
    • No limit on total services
    • Could lead to resource exhaustion
  4. Security Concerns

    • No authentication for service recovery
    • Service IDs are predictable (just UUID)
    • No encryption of buffered terminal output
    • No access control between users

Performance Optimizations Needed

  1. Output Reading

    • Currently polls at 33fps regardless of activity
    • Should use event-driven I/O (epoll/kqueue)
    • Batch small outputs to reduce messages
  2. Buffer Management

    • Ring buffer could be more efficient
    • Consider compression for stored output
    • Implement smart truncation (keep last N complete lines)
  3. Message Overhead

    • Each output chunk creates new protobuf message
    • Could batch multiple terminal outputs
    • Consider streaming protocol for continuous output

Platform-Specific Issues

  1. Windows

    • ConPTY support needs testing
    • Non-blocking I/O handled differently
    • Shell detection could be improved
  2. Mobile (Android/iOS)

    • Terminal feature disabled by conditional compilation
    • Need to evaluate mobile terminal support
    • Touch keyboard integration needed

Testing Requirements

  1. Unit Tests Needed

    • Terminal service lifecycle
    • Cleanup logic edge cases
    • Buffer management
    • Message serialization
  2. Integration Tests

    • Multi-terminal scenarios
    • Reconnection flows
    • Cleanup timing
    • Resource limits
  3. Stress Tests

    • Many terminals per connection
    • Large output volumes
    • Rapid connect/disconnect
    • Long-running sessions

Alternative Designs to Consider

  1. Direct Terminal Management

    // In Connection struct
    terminals: HashMap<i32, TerminalSession>,
    
    // No service pattern, direct management
    async fn handle_terminal_action(&mut self, action) {
        match action {
            Open => self.open_terminal(),
            Data => self.terminal_input(),
            // etc
        }
    }
    
  2. Actor-Based Design

    • Each terminal as an actor
    • Message passing for I/O
    • Better isolation and error handling
  3. Session Manager Service

    • One global terminal manager
    • Connections request terminals from manager
    • Cleaner separation of concerns

Documentation Gaps

  1. API Documentation

    • Document all public methods
    • Add examples for common operations
    • Document error conditions
  2. Configuration

    • Document all timeouts and limits
    • How to configure shell/terminal
    • Platform-specific settings
  3. Troubleshooting Guide

    • Common issues and solutions
    • Debug logging interpretation
    • Performance tuning

Future Feature Ideas

  1. Advanced Terminal Features

    • Terminal sharing (multiple users, one terminal)
    • Session recording and playback
    • File transfer through terminal (zmodem)
    • Custom color schemes
    • Font configuration
  2. Integration Features

    • SSH key forwarding
    • Environment variable injection
    • Working directory synchronization
    • Shell integration (prompt markers, etc)
  3. Management Features

    • Terminal session monitoring
    • Usage statistics
    • Audit logging
    • Rate limiting

Refactoring Suggestions

  1. Separate Concerns

    • Split terminal_service.rs into multiple files
    • Separate PTY management from service logic
    • Extract buffer management to own module
  2. Improve Error Handling

    • Use proper error types, not strings
    • Add error recovery mechanisms
    • Better error reporting to client
  3. Configuration Management

    • Make timeouts configurable
    • Add feature flags for experimental features
    • Environment-based configuration