mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 08:02:24 +00:00
fix(ui): keep blocking host work off the UI thread and off gpui's pool
Five findings from review, all about where blocking work runs and what a stale handle is still pointing at. - `live_pane_count` ran a routed `List` — an SSH handshake, and on a WSL route as far as installing the server — straight from the Stop/Delete action handler. That is `guard_off_ui`'s debug abort in a dev build and a frozen window in a release one. It is now split into a UI-thread read and a background count, with the prompt raised through the window handle afterwards. - `teardown_workspace_forwards` blocked the UI thread on a daemon reply that waits for the SSH server to acknowledge `cancel_tcpip_forward`. On a machine that has gone unreachable — exactly when someone reaches for Stop Workspace — it never came. Backgrounded, and `on_workspace` now sets a read timeout so the thread is not parked forever either. - The file tree's and editor's watch subscriptions had no record of which host opened them. A reconnect inserts a fresh `RemoteHost` under the same `HostId`, so `set_dirs` failed on a dead `ControlClient`, was warned and dropped, and nothing opened a new one: after the first reconnect the tree stopped seeing remote changes for the life of the window, and the editor's external-change detection — what stops a save clobbering someone else's edit — was silently off. Both now compare the host by pointer and reopen when it differs. - Closing a remote window that was empty *because its machine could not be reached* deleted the workspace: its `RemoteRef`, cached layout and geometry, while its panes were still running over there. Only a machine that answered licenses dropping the entry. - `HostOps` ran blocking calls on gpui's background executor, which on Linux is a fixed pool with no blocking tier. Four stalled host calls on a four-core client took every worker, including the one the reconnect needed to clear the stall. They now run on their own elastic pool.
This commit is contained in:
+32
-1
@@ -41,7 +41,7 @@ use gpui_component::{
|
||||
};
|
||||
|
||||
use crate::ui::app::Tty7App;
|
||||
use crate::ui::host_ops::{HostOps, MTime, WatchSub};
|
||||
use crate::ui::host_ops::{HostOps, MTime, SharedHost, WatchSub};
|
||||
|
||||
/// Refuse to open files larger than this: the component's code editor is rated
|
||||
/// to ~50K lines, and a multi-megabyte blob is almost never what a terminal
|
||||
@@ -165,6 +165,15 @@ pub(crate) struct EditorPanelState {
|
||||
/// and a server-side watcher recreated every time a file is opened or
|
||||
/// closed. `Arc` because `set_dirs` is itself a host call.
|
||||
watch: Option<Arc<WatchSub>>,
|
||||
/// The host `watch` was opened against, kept so a subscription is never
|
||||
/// reused across a different one.
|
||||
///
|
||||
/// A `HostId` is not enough to tell them apart: reconnecting removes the
|
||||
/// dead `RemoteHost` and inserts a fresh one under the *same* id, so the id
|
||||
/// matches while the `ControlClient` behind the old subscription is gone.
|
||||
/// Compared by pointer, which distinguishes both that and an outright
|
||||
/// switch to another machine.
|
||||
watch_host: Option<SharedHost>,
|
||||
/// A subscription is being opened; keeps a burst of opens from asking for
|
||||
/// one each.
|
||||
watch_opening: bool,
|
||||
@@ -217,6 +226,7 @@ impl EditorPanelState {
|
||||
.detach();
|
||||
Self {
|
||||
watch: None,
|
||||
watch_host: None,
|
||||
watch_opening: false,
|
||||
watch_busy: false,
|
||||
watch_dirty: false,
|
||||
@@ -434,6 +444,25 @@ impl Tty7App {
|
||||
return;
|
||||
};
|
||||
|
||||
// Same rule as the file tree's: a subscription belongs to the host that
|
||||
// opened it. A reconnect inserts a fresh `RemoteHost` under the same
|
||||
// `HostId`, so the id matches while the `ControlClient` behind this
|
||||
// subscription is gone — `set_dirs` then fails, is warned and dropped,
|
||||
// and nothing opens a new one. The cost here is quieter and worse than
|
||||
// a stale tree: external-change detection is what stops a save
|
||||
// clobbering an edit made on the other side.
|
||||
if !self
|
||||
.editor
|
||||
.watch_host
|
||||
.as_ref()
|
||||
.is_some_and(|opened_with| Arc::ptr_eq(opened_with, &host))
|
||||
{
|
||||
self.editor.watch = None;
|
||||
self.editor.watch_host = None;
|
||||
self.editor.watch_busy = false;
|
||||
self.editor.watch_dirty = false;
|
||||
}
|
||||
|
||||
if let Some(sub) = self.editor.watch.clone() {
|
||||
if self.editor.watch_busy {
|
||||
self.editor.watch_dirty = true;
|
||||
@@ -462,6 +491,7 @@ impl Tty7App {
|
||||
return;
|
||||
}
|
||||
self.editor.watch_opening = true;
|
||||
let opened_host = Arc::clone(&host);
|
||||
let opened_with = self.editor.watched_dirs.clone();
|
||||
HostOps::run(
|
||||
host,
|
||||
@@ -481,6 +511,7 @@ impl Tty7App {
|
||||
};
|
||||
let events = sub.events().clone();
|
||||
app.editor.watch = Some(sub);
|
||||
app.editor.watch_host = Some(opened_host);
|
||||
cx.spawn(async move |app, cx| {
|
||||
while let Ok(batch) = events.recv().await {
|
||||
let ok = app.update(cx, |app, _cx| {
|
||||
|
||||
Reference in New Issue
Block a user