feat: harden local shell, SFTP, and desktop session reliability

Finder-launched terminals now inherit a login PATH so tools like ollama resolve, destructive SFTP/editor actions require confirmation, and terminal search plus window dragging work again. SSH forwards and RDP sessions keep listeners and survive reconnect without FastPath crashes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Logan.LiandCursor committed 2026-08-16 16:08:11 +08:00
1 parent 188f3556ed
commit 08696896f9
223 files changed
+13493 -3423

No files matched your search

+87 -24
View File
@@ -1,5 +1,5 @@
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
io::{self, Read, Write},
net::{Shutdown, TcpStream},
sync::{
@@ -19,7 +19,7 @@ use crate::domain::ssh::{
ShellDataEvent, SshConnectRequest,
};
use super::tunnel::TunnelManager;
use super::tunnel::{connection_fingerprint, TunnelManager};
mod auth;
mod host_metrics;
@@ -50,13 +50,27 @@ struct Shell {
pending_output: Arc<Mutex<Vec<u8>>>,
}
/// Dedicated SSH transport + persistent SFTP channel for one workload.
/// Each UI SFTP tab owns a browsing channel and may lazily open a separate
/// transfer channel; neither shares the pooled session used by exec/keepalive.
struct ManagedSftp {
connection_id: String,
session: Session,
sftp: ssh2::Sftp,
}
#[derive(Default)]
pub struct SshService {
connections: Mutex<HashMap<String, Connection>>,
shells: Mutex<HashMap<String, Shell>>,
sftps: Mutex<HashMap<String, String>>,
/// Interactive browsing channel for each SFTP tab.
sftps: Mutex<HashMap<String, Arc<Mutex<ManagedSftp>>>>,
/// Lazily-created transfer channel, isolated from browsing/file operations.
sftp_transfers: Mutex<HashMap<String, Arc<Mutex<ManagedSftp>>>>,
/// Per-connection cancel flag for in-flight `ssh_exec` (Agent 停止).
active_execs: Mutex<HashMap<String, Arc<AtomicBool>>>,
/// Per-SFTP-session cancel for the active upload/download copy loop.
sftp_transfer_cancels: Mutex<HashMap<String, Arc<AtomicBool>>>,
tunnels: TunnelManager,
}
@@ -65,15 +79,30 @@ impl SshService {
validate_config(&config)?;
let session = establish_session(&config)?;
let id = Uuid::new_v4().to_string();
self.connections
.lock()
.map_err(lock_error)?
.insert(id.clone(), Connection { session, config });
let fingerprint = connection_fingerprint(&config);
let live = {
let mut connections = self.connections.lock().map_err(lock_error)?;
connections.insert(id.clone(), Connection { session, config });
connections.keys().cloned().collect::<HashSet<_>>()
};
// Reattach listeners that survived a reconnect (same SSH identity,
// previous connection id is gone). Runtime is never persisted.
self.tunnels.adopt_orphans(&id, &fingerprint, &live)?;
Ok(id)
}
pub fn disconnect(&self, app: &AppHandle, connection_id: &str) -> Result<(), String> {
self.tunnels.stop_connection(connection_id)?;
pub fn disconnect(
&self,
app: &AppHandle,
connection_id: &str,
retain_forwards: bool,
) -> Result<(), String> {
// Dedicated tunnel sessions keep listening across terminal flaps when
// retain_forwards is true. Explicit teardown (tab close / panel disconnect)
// must still close real Listeners.
if !retain_forwards {
let _ = self.tunnels.stop_connection(connection_id);
}
if let Some(flag) = self
.active_execs
.lock()
@@ -97,10 +126,22 @@ impl SshService {
let _ = shell.session.disconnect(None, "closed by user", None);
}
}
self.sftps
.lock()
.map_err(lock_error)?
.retain(|_, id| id != connection_id);
let sftp_ids = {
let sftps = self.sftps.lock().map_err(lock_error)?;
sftps
.iter()
.filter_map(|(id, handle)| {
handle
.lock()
.ok()
.filter(|sftp| sftp.connection_id == connection_id)
.map(|_| id.clone())
})
.collect::<Vec<_>>()
};
for sftp_id in sftp_ids {
let _ = self.close_sftp(&sftp_id);
}
if let Some(connection) = self
.connections
.lock()
@@ -256,8 +297,8 @@ impl SshService {
.map(|_| ())
.map_err(|error| format!("SSH 保活失败: {error}"))?;
// Interactive panes use dedicated SSH sessions so a busy SFTP/exec
// channel cannot stall terminal IO. Keep those transports alive too.
// Interactive panes / SFTP use dedicated SSH sessions so a busy
// SFTP/exec channel cannot stall terminal IO. Keep those alive too.
let shell_sessions = self
.shells
.lock()
@@ -271,6 +312,21 @@ impl SshService {
.keepalive_send()
.map_err(|error| format!("Shell SSH 保活失败: {error}"))?;
}
let sftp_sessions = {
let sftps = self.sftps.lock().map_err(lock_error)?;
sftps
.values()
.filter_map(|handle| {
let guard = handle.lock().ok()?;
(guard.connection_id == connection_id).then(|| guard.session.clone())
})
.collect::<Vec<_>>()
};
for session in sftp_sessions {
session
.keepalive_send()
.map_err(|error| format!("SFTP SSH 保活失败: {error}"))?;
}
Ok(())
}
@@ -393,6 +449,7 @@ impl SshService {
&self,
connection_id: &str,
forward_id: Option<String>,
local_bind_host: String,
local_port: u16,
remote_host: String,
remote_port: u16,
@@ -402,6 +459,7 @@ impl SshService {
forward_id,
connection_id,
config,
local_bind_host,
local_port,
remote_host,
remote_port,
@@ -423,6 +481,7 @@ impl SshService {
&self,
connection_id: &str,
forward_id: Option<String>,
remote_bind_host: String,
remote_port: u16,
local_host: String,
local_port: u16,
@@ -432,6 +491,7 @@ impl SshService {
forward_id,
connection_id,
config,
remote_bind_host,
remote_port,
local_host,
local_port,
@@ -481,7 +541,7 @@ pub(super) fn establish_session(config: &SshConnectRequest) -> Result<Session, S
session
.handshake()
.map_err(|error| format!("SSH 握手失败 ({address}): {error}"))?;
session.set_keepalive(config.keep_alive.unwrap_or(true), 30);
session.set_keepalive(config.keep_alive.unwrap_or(true), 15);
verify_host_key(&session, &config.host, config.port)?;
authenticate(&session, config)?;
Ok(session)
@@ -782,7 +842,10 @@ mod tests {
target.seek(SeekFrom::Start(3)).unwrap();
let mut progress = Vec::new();
copy_with_progress(&mut source, &mut target, 3, 6, |value| progress.push(value)).unwrap();
copy_with_progress(&mut source, &mut target, 3, 6, None, |value| {
progress.push(value)
})
.unwrap();
assert_eq!(target.into_inner(), b"abcdef");
assert_eq!(progress.first(), Some(&3));
@@ -835,23 +898,23 @@ mod tests {
let remote_path = format!("/tmp/opsxshell-sftp-{probe_id}.txt");
let payload = format!("OPSX_SFTP_ROUNDTRIP_{probe_id}\n").into_bytes();
fs::write(&local_source, &payload).expect("write local SFTP fixture");
let sftp = service.sftp(&sftp_id).expect("SFTP channel");
let transfer_result = (|| -> Result<(), String> {
upload_file(&sftp, &local_source, &remote_path, |_, _| {})?;
download_file(&sftp, &remote_path, &local_download, |_, _| {})?;
let transfer_result = service.with_sftp(&sftp_id, |sftp| {
upload_file(sftp, &local_source, &remote_path, None, |_, _| {})?;
download_file(sftp, &remote_path, &local_download, None, |_, _| {})?;
let downloaded = fs::read(&local_download)
.map_err(|error| format!("read downloaded probe: {error}"))?;
if downloaded != payload {
return Err("SFTP round-trip content mismatch".to_string());
}
let _ = sftp.unlink(Path::new(&remote_path));
let _ = sftp.unlink(Path::new(&format!("{remote_path}.opsx-part")));
Ok(())
})();
let _ = sftp.unlink(Path::new(&remote_path));
let _ = sftp.unlink(Path::new(&format!("{remote_path}.opsx-part")));
});
let _ = fs::remove_file(&local_source);
let _ = fs::remove_file(&local_download);
let _ = fs::remove_file(local_partial_path(&local_download));
transfer_result.expect("SFTP upload/download round trip");
service.close_sftp(&sftp_id).expect("close SFTP");
let connection = service.connection(&connection_id).unwrap();
let shell_session = establish_session(&connection.config).expect("shell SSH session");
let mut shell = shell_session.channel_session().expect("shell channel");