fix(app): abort the old reader on reconnect (fixes doubled keystroke echo)
reconnect() spawned a new reader/writer but left the previous reader task
running. A reconnect triggered while the old connection was still alive (e.g.
a request timing out during a slow daemon start) left TWO live connections;
the daemon broadcast Output to both, so every byte — including input echo —
arrived twice ("ccucurcurl"). The bridge now stores the reader's JoinHandle
and aborts it before establishing the new connection, guaranteeing a single
live reader.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,9 @@ pub struct Bridge {
|
||||
tx: Mutex<mpsc::Sender<Envelope>>,
|
||||
/// Serializes reconnect attempts.
|
||||
reconnect_lock: Mutex<()>,
|
||||
/// The current reader task; aborted on reconnect so a stale connection can't
|
||||
/// keep delivering duplicate output (which doubled keystroke echo).
|
||||
reader: Mutex<tokio::task::JoinHandle<()>>,
|
||||
/// Pending request id → reply slot.
|
||||
pending: Arc<Mutex<HashMap<u64, oneshot::Sender<Envelope>>>>,
|
||||
/// surface id → output channel into the webview.
|
||||
@@ -102,13 +105,13 @@ async fn spawn_connection(
|
||||
app: &AppHandle,
|
||||
pending: Arc<Mutex<HashMap<u64, oneshot::Sender<Envelope>>>>,
|
||||
out_channels: Arc<Mutex<HashMap<String, Channel<Vec<u8>>>>>,
|
||||
) -> Result<mpsc::Sender<Envelope>> {
|
||||
) -> Result<(mpsc::Sender<Envelope>, tokio::task::JoinHandle<()>)> {
|
||||
let stream = ensure_daemon(sock).await?;
|
||||
let (read_half, write_half) = stream.into_split();
|
||||
let (tx, rx) = mpsc::channel::<Envelope>(256);
|
||||
spawn_writer(write_half, rx);
|
||||
spawn_reader(read_half, app.clone(), pending, out_channels);
|
||||
Ok(tx)
|
||||
let reader = spawn_reader(read_half, app.clone(), pending, out_channels);
|
||||
Ok((tx, reader))
|
||||
}
|
||||
|
||||
impl Bridge {
|
||||
@@ -116,7 +119,7 @@ impl Bridge {
|
||||
let sock = socket_path()?;
|
||||
let pending: Arc<Mutex<HashMap<u64, oneshot::Sender<Envelope>>>> = Arc::default();
|
||||
let out_channels: Arc<Mutex<HashMap<String, Channel<Vec<u8>>>>> = Arc::default();
|
||||
let tx = spawn_connection(&sock, &app, pending.clone(), out_channels.clone()).await?;
|
||||
let (tx, reader) = spawn_connection(&sock, &app, pending.clone(), out_channels.clone()).await?;
|
||||
Ok(Self {
|
||||
next_id: AtomicU64::new(1),
|
||||
app,
|
||||
@@ -124,6 +127,7 @@ impl Bridge {
|
||||
gen: AtomicU64::new(0),
|
||||
tx: Mutex::new(tx),
|
||||
reconnect_lock: Mutex::new(()),
|
||||
reader: Mutex::new(reader),
|
||||
pending,
|
||||
out_channels,
|
||||
})
|
||||
@@ -139,8 +143,12 @@ impl Bridge {
|
||||
}
|
||||
// Drop in-flight reply slots — their connection is gone; they'll error out.
|
||||
self.pending.lock().await.clear();
|
||||
let new_tx = spawn_connection(&self.sock, &self.app, self.pending.clone(), self.out_channels.clone()).await?;
|
||||
// Kill the old reader FIRST so it can't keep delivering output on a stale
|
||||
// connection alongside the new one (the cause of doubled keystroke echo).
|
||||
self.reader.lock().await.abort();
|
||||
let (new_tx, new_reader) = spawn_connection(&self.sock, &self.app, self.pending.clone(), self.out_channels.clone()).await?;
|
||||
*self.tx.lock().await = new_tx;
|
||||
*self.reader.lock().await = new_reader;
|
||||
self.gen.fetch_add(1, Ordering::Release);
|
||||
let _ = self.app.emit("spacesh:reconnected", ());
|
||||
Ok(())
|
||||
@@ -205,7 +213,7 @@ fn spawn_reader(
|
||||
app: AppHandle,
|
||||
pending: Arc<Mutex<HashMap<u64, oneshot::Sender<Envelope>>>>,
|
||||
out_channels: Arc<Mutex<HashMap<String, Channel<Vec<u8>>>>>,
|
||||
) {
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match read_frame(&mut read_half).await {
|
||||
@@ -232,7 +240,7 @@ fn spawn_reader(
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// ---- Tauri commands ----
|
||||
|
||||
Reference in New Issue
Block a user