Skip to content

Commit c2d0bec

Browse files
fix(Mountain): Defer sky://terminal/create event to fix race condition
The terminal workbench's `LocalTerminalBackend.createProcess` flow has a timing vulnerability: when `sky://terminal/create` emits synchronously during step 1 (RPC in-flight), the Tauri event arrives before step 3 (`_ptys.set(id, pty)`) completes. This causes `_ptys.get(id)` to return `undefined`, skipping `handleReady` and preventing `processManager._onProcessReady` from firing. Since `ptyProcessReady` never resolves, every subsequent `processManager.write(data)` call hangs indefinitely—users see the terminal panel but all keystrokes are silently dropped. Fix by emitting on a deferred tokio task with a 120ms delay, giving the RPC response roundtrip + `_ptys.set` sufficient headroom. Also switch to `LogSkyEmit` for visibility under `[DEV:SKY-EMIT]` histogram tracking. This complements the existing `AppendTerminalOutput` replay buffer for data (added in c99ccbe), completing the terminal race-condition fix for both create and data events.
1 parent c99ccbe commit c2d0bec

1 file changed

Lines changed: 54 additions & 16 deletions

File tree

Source/Environment/TerminalProvider.rs

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ use tauri::Emitter;
113113
use tokio::sync::mpsc as TokioMPSC;
114114

115115
use super::{MountainEnvironment::MountainEnvironment, Utility};
116-
use crate::{ApplicationState::DTO::TerminalStateDTO::TerminalStateDTO, dev_log};
116+
use crate::{ApplicationState::DTO::TerminalStateDTO::TerminalStateDTO, IPC::SkyEmit::LogSkyEmit, dev_log};
117117

118118
// Per-terminal recent-output buffer. The PTY reader task races SkyBridge's
119119
// `listen("sky://terminal/data", ...)` install: in the bundled-electron
@@ -418,21 +418,59 @@ impl TerminalProvider for MountainEnvironment {
418418
// BATCH-19 Part B: let Sky render the new terminal panel without
419419
// waiting for Cocoon to round-trip a notification. The `sky://` event
420420
// channel is already how ShowTerminal / HideTerminal talk to the UI.
421-
if let Err(Error) = self.ApplicationHandle.emit(
422-
SkyEvent::TerminalCreate.AsStr(),
423-
json!({
424-
"id": TerminalIdentifier,
425-
"name": Name,
426-
"pid": TerminalState.OSProcessIdentifier,
427-
}),
428-
) {
429-
dev_log!(
430-
"terminal",
431-
"warn: [TerminalProvider] sky://terminal/create emit failed for ID {}: {}",
432-
TerminalIdentifier,
433-
Error
434-
);
435-
}
421+
//
422+
// RACE FIX: emit on a deferred tokio task (~120 ms) instead of
423+
// synchronously. The workbench's `LocalTerminalBackend.createProcess`
424+
// flow is:
425+
// 1. await this._proxy.createProcess(...) // RPC IN-FLIGHT
426+
// 2. const pty = new LocalPty(id, …) // POST-await
427+
// 3. this._ptys.set(id, pty) // POST-await
428+
// The patched `_connectToDirectProxy` listener for
429+
// `_localPtyService.onProcessReady` does
430+
// `this._ptys.get(e.id)?.handleReady(e.event)`. If we emit
431+
// synchronously while CreateTerminal is still inside step (1),
432+
// the Tauri event fires before step (3) - `_ptys.get(id)` returns
433+
// `undefined`, `handleReady` is skipped, `BasePty._onProcessReady`
434+
// never fires, `processManager._onProcessReady` never fires,
435+
// `ptyProcessReady` never resolves - and every `processManager.
436+
// write(data)` call (which `terminalInstance._handleOnData`
437+
// `await`s) hangs forever. The user sees the panel render but
438+
// every keystroke is silently dropped because `LocalPty.input`
439+
// is never reached. A 120 ms delay gives the RPC response
440+
// roundtrip + `_ptys.set` plenty of headroom on real hardware.
441+
// Same race applies to `sky://terminal/data` for the shell's
442+
// first prompt - the existing `AppendTerminalOutput` replay
443+
// buffer covers data, but the create event needs explicit
444+
// deferral because there's no replay path for ready.
445+
let CreateAppHandle = self.ApplicationHandle.clone();
446+
let CreateTermId = TerminalIdentifier;
447+
let CreateName = Name.clone();
448+
let CreatePid = TerminalState.OSProcessIdentifier;
449+
tokio::spawn(async move {
450+
tokio::time::sleep(std::time::Duration::from_millis(120)).await;
451+
let CreatePayload = json!({
452+
"id": CreateTermId,
453+
"name": CreateName,
454+
"pid": CreatePid,
455+
});
456+
// `LogSkyEmit` makes the deferred emit visible under
457+
// `[DEV:SKY-EMIT]` so the next log dissection can confirm
458+
// the deferral landed (and how many `localPty:input` calls
459+
// arrived afterwards). The bare `.emit()` we replaced was
460+
// invisible to the histogram.
461+
if let Err(Error) = LogSkyEmit(
462+
&CreateAppHandle,
463+
SkyEvent::TerminalCreate.AsStr(),
464+
CreatePayload,
465+
) {
466+
dev_log!(
467+
"terminal",
468+
"warn: [TerminalProvider] sky://terminal/create emit failed for ID {}: {}",
469+
CreateTermId,
470+
Error
471+
);
472+
}
473+
});
436474

437475
dev_log!(
438476
"terminal",

0 commit comments

Comments
 (0)