Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 24 additions & 49 deletions src-tauri/src/pty/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,9 @@ pub struct TerminalInstance {
/// (persistence + parity with the desktop terminal). Capped at
/// `SCROLLBACK_CAP` bytes; oldest bytes are dropped first.
pub scrollback: Arc<RwLock<std::collections::VecDeque<u8>>>,
/// PTY slot token that holds the slot reservation. When dropped, the slot is automatically released.
/// This ensures deterministic cleanup even if other cleanup paths fail.
pub slot_token: Option<crate::pty::SlotToken>,
#[cfg(target_os = "windows")]
pub conpty_handles: Option<Arc<ParkingMutex<Option<ConPtyHandles>>>>,
}
Expand Down Expand Up @@ -688,44 +691,6 @@ struct TerminalExitEvent {
signal: Option<i32>,
}

struct TerminalSlotReservation {
active_slots: Arc<AtomicUsize>,
committed: bool,
}

impl TerminalSlotReservation {
fn try_acquire(active_slots: Arc<AtomicUsize>) -> Option<Self> {
loop {
let current = active_slots.load(Ordering::SeqCst);
if current >= GLOBAL_TERMINAL_LIMIT {
return None;
}

if active_slots
.compare_exchange(current, current + 1, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
return Some(Self {
active_slots,
committed: false,
});
}
}
}

fn commit(&mut self) {
self.committed = true;
}
}

impl Drop for TerminalSlotReservation {
fn drop(&mut self) {
if !self.committed {
self.active_slots.fetch_sub(1, Ordering::SeqCst);
}
}
}

/// Manages all PTY instances
pub struct PtyManager {
terminals: Arc<RwLock<HashMap<String, Arc<TerminalInstance>>>>,
Expand All @@ -742,6 +707,8 @@ pub struct PtyManager {
/// Set when the app window is minimized/hidden to prevent
/// ConPTY lifecycle issues on Windows.
is_hidden: Arc<AtomicBool>,
/// PTY slot manager for enforcing concurrent terminal limit
slot_manager: Arc<crate::pty::PtySlotManager>,
}

impl PtyManager {
Expand All @@ -752,6 +719,12 @@ impl PtyManager {
git_tracker: Arc<GitTracker>,
exit_code_tracker: Arc<ExitCodeTracker>,
) -> Self {
let slot_config = crate::pty::SlotManagerConfig {
max_slots: 20,
orphan_timeout: Duration::from_secs(300),
metrics_enabled: true,
};

Self {
terminals: Arc::new(RwLock::new(HashMap::new())),
active_terminal_slots: Arc::new(AtomicUsize::new(0)),
Expand All @@ -764,6 +737,7 @@ impl PtyManager {
cwd_tracker,
git_tracker,
exit_code_tracker,
slot_manager: Arc::new(crate::pty::PtySlotManager::with_config(slot_config)),
}
}

Expand Down Expand Up @@ -807,10 +781,6 @@ impl PtyManager {
}
}

fn try_reserve_terminal_slot(&self) -> Option<TerminalSlotReservation> {
TerminalSlotReservation::try_acquire(self.active_terminal_slots.clone())
}

fn release_terminal_slot(&self) {
self.active_terminal_slots.fetch_sub(1, Ordering::SeqCst);
}
Expand Down Expand Up @@ -911,9 +881,11 @@ impl PtyManager {
// Start orphan detection on first spawn (lazy initialization)
self.start_orphan_detection();

let mut slot_reservation = self
.try_reserve_terminal_slot()
.ok_or_else(|| "Global terminal limit reached".to_string())?;
// Reserve a PTY slot via the slot manager
let slot_token = self
.slot_manager
.try_reserve_slot()
.ok_or_else(|| "Terminal slot limit reached (max 20). Close some terminals or wait for orphans to be reaped.".to_string())?;

let id = self.generate_id();

Expand Down Expand Up @@ -1020,6 +992,7 @@ impl PtyManager {
rows: Arc::new(RwLock::new(rows)),
broadcast_tx: Arc::new(tokio::sync::broadcast::channel(TERM_BROADCAST_CAPACITY).0),
scrollback: Arc::new(RwLock::new(std::collections::VecDeque::new())),
slot_token: Some(slot_token),
conpty_handles: Some(Arc::new(ParkingMutex::new(Some(conpty_handles)))),
});

Expand Down Expand Up @@ -1073,8 +1046,6 @@ impl PtyManager {
self.git_tracker.initialize_terminal(&id, &cwd);
self.exit_code_tracker.initialize_terminal(&id);

slot_reservation.commit();

Ok(TerminalInfo {
id,
shell: shell_path,
Expand Down Expand Up @@ -1158,6 +1129,7 @@ impl PtyManager {
rows: Arc::new(RwLock::new(rows)),
broadcast_tx: Arc::new(tokio::sync::broadcast::channel(TERM_BROADCAST_CAPACITY).0),
scrollback: Arc::new(RwLock::new(std::collections::VecDeque::new())),
slot_token: Some(slot_token),
#[cfg(target_os = "windows")]
conpty_handles: None,
});
Expand Down Expand Up @@ -1206,8 +1178,6 @@ impl PtyManager {
self.git_tracker.initialize_terminal(&id, &cwd);
self.exit_code_tracker.initialize_terminal(&id);

slot_reservation.commit();

Ok(TerminalInfo {
id,
shell: shell_path,
Expand Down Expand Up @@ -1582,6 +1552,11 @@ impl PtyManager {
self.active_terminal_slots.load(Ordering::SeqCst) >= GLOBAL_TERMINAL_LIMIT
}

/// Get current PTY slot manager metrics
pub fn get_slot_metrics(&self) -> crate::pty::SlotMetrics {
self.slot_manager.get_metrics()
}

/// Kill all terminals (best-effort), used as app-exit safety net.
/// This is async because cleanup_terminal_resources_sync uses blocking_lock()
/// on AsyncMutex fields, which is forbidden inside tokio async runtime.
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/pty/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
pub mod da_filter;
pub mod env_refresh;
pub mod manager;
pub mod slot_manager;

#[cfg(target_os = "windows")]
pub mod windows;

pub use da_filter::DaFilter;
pub use manager::{PtyManager, SpawnOptions, TerminalInfo};
pub use slot_manager::{PtySlotManager, SlotToken, SlotManagerConfig, SlotMetrics};
Loading
Loading