* 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...
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
- Open Terminal
TerminalAction {
open: OpenTerminal {
terminal_id: i32,
rows: u32,
cols: u32,
}
}
- Send Input
TerminalAction {
data: TerminalData {
terminal_id: i32,
data: bytes,
}
}
- Resize Terminal
TerminalAction {
resize: ResizeTerminal {
terminal_id: i32,
rows: u32,
cols: u32,
}
}
- Close Terminal
TerminalAction {
close: CloseTerminal {
terminal_id: i32,
force: bool,
}
}
Server → Client Messages
- Terminal Opened
TerminalResponse {
opened: TerminalOpened {
terminal_id: i32,
success: bool,
message: string,
pid: u32,
}
}
- Terminal Output
TerminalResponse {
data: TerminalData {
terminal_id: i32,
data: bytes, // Base64 encoded in Flutter
}
}
- 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
- User right-clicks terminal tab → "Enable terminal persistence"
- Client stores persistence preference in
TerminalConnectionManager - New terminals created with appropriate service ID prefix
- 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
$SHELLor/bin/bash - Windows: Uses
%COMSPEC%orcmd.exe - PTY implementation via
portable_ptycrate
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
- Service ID Storage: Client doesn't persist service IDs yet
- Reconnection UI: No UI to recover previous sessions
- Authentication: No per-service authentication for reconnection
- Resource Limits: No configurable limits on terminals per service
Future Enhancements
-
Proper Reconnection Flow:
- Store service IDs in peer config
- UI to list and recover previous sessions
- Show buffered output on reconnection
-
Security:
- Authentication token for service recovery
- Encryption of buffered output
- Access control per terminal
-
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
-
Start Terminal Session:
- Click terminal icon or use Ctrl/Cmd+Shift+T
- Terminal opens with default shell
-
Enable Persistence:
- Right-click any terminal tab
- Select "Enable terminal persistence"
- All terminals for that peer become persistent
-
Multiple Terminals:
- Click "+" button or Ctrl/Cmd+Shift+T
- Each terminal is independent
-
Reconnection (TODO):
- Connect to same peer
- Previous terminals automatically restored
- Recent output displayed
Implementation Issues & TODOs
Critical Missing Features
-
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
-
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; } -
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
-
Service Pattern Mismatch
- Terminal service forced into broadcast service pattern
- Should be direct connection resource, not shared service
- Complex routing through service registry unnecessary
-
Global State Management
- TERMINAL_SERVICES static HashMap may cause issues
- No proper service discovery mechanism
- Cleanup task is global, not per-connection
-
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
-
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
-
Output Reading
- Currently polls at 33fps regardless of activity
- Should use event-driven I/O (epoll/kqueue)
- Batch small outputs to reduce messages
-
Buffer Management
- Ring buffer could be more efficient
- Consider compression for stored output
- Implement smart truncation (keep last N complete lines)
-
Message Overhead
- Each output chunk creates new protobuf message
- Could batch multiple terminal outputs
- Consider streaming protocol for continuous output
Platform-Specific Issues
-
Windows
- ConPTY support needs testing
- Non-blocking I/O handled differently
- Shell detection could be improved
-
Mobile (Android/iOS)
- Terminal feature disabled by conditional compilation
- Need to evaluate mobile terminal support
- Touch keyboard integration needed
Testing Requirements
-
Unit Tests Needed
- Terminal service lifecycle
- Cleanup logic edge cases
- Buffer management
- Message serialization
-
Integration Tests
- Multi-terminal scenarios
- Reconnection flows
- Cleanup timing
- Resource limits
-
Stress Tests
- Many terminals per connection
- Large output volumes
- Rapid connect/disconnect
- Long-running sessions
Alternative Designs to Consider
-
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 } } -
Actor-Based Design
- Each terminal as an actor
- Message passing for I/O
- Better isolation and error handling
-
Session Manager Service
- One global terminal manager
- Connections request terminals from manager
- Cleaner separation of concerns
Documentation Gaps
-
API Documentation
- Document all public methods
- Add examples for common operations
- Document error conditions
-
Configuration
- Document all timeouts and limits
- How to configure shell/terminal
- Platform-specific settings
-
Troubleshooting Guide
- Common issues and solutions
- Debug logging interpretation
- Performance tuning
Future Feature Ideas
-
Advanced Terminal Features
- Terminal sharing (multiple users, one terminal)
- Session recording and playback
- File transfer through terminal (zmodem)
- Custom color schemes
- Font configuration
-
Integration Features
- SSH key forwarding
- Environment variable injection
- Working directory synchronization
- Shell integration (prompt markers, etc)
-
Management Features
- Terminal session monitoring
- Usage statistics
- Audit logging
- Rate limiting
Refactoring Suggestions
-
Separate Concerns
- Split terminal_service.rs into multiple files
- Separate PTY management from service logic
- Extract buffer management to own module
-
Improve Error Handling
- Use proper error types, not strings
- Add error recovery mechanisms
- Better error reporting to client
-
Configuration Management
- Make timeouts configurable
- Add feature flags for experimental features
- Environment-based configuration