use crate::extensions::ssh_remote::{
command::parse_ssh_command,
connect::{self, ClientHandler, CommandOutput},
state::{Endpoint, Limits, RemoteConfig},
};
use russh::client::Handle;
use std::{
collections::HashMap,
sync::{
Arc, Mutex, OnceLock, RwLock,
atomic::{AtomicUsize, Ordering},
},
time::Duration,
};
use tokio::sync::Mutex as AsyncMutex;
const MAX_POOLED: usize = 8;
static CONNECT_LOCK: AsyncMutex<()> = AsyncMutex::const_new(());
static TURN_OUTPUT_BYTES: AtomicUsize = AtomicUsize::new(0);
static POOL: OnceLock<Mutex<HashMap<String, Arc<RemoteConn>>>> = OnceLock::new();
fn pool() -> &'static Mutex<HashMap<String, Arc<RemoteConn>>> {
POOL.get_or_init(|| Mutex::new(HashMap::new()))
}
pub struct RemoteConn {
endpoint: RwLock<Endpoint>,
pub handle: Arc<Handle<ClientHandler>>,
_jumps: Vec<Handle<ClientHandler>>,
}
impl std::fmt::Debug for RemoteConn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RemoteConn")
.field("endpoint", &self.endpoint().key())
.field("alive", &self.is_alive())
.finish()
}
}
impl RemoteConn {
pub fn is_alive(&self) -> bool {
!self.handle.is_closed()
}
pub fn endpoint(&self) -> Endpoint {
self.endpoint
.read()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
}
pub fn reset_turn_budget() {
TURN_OUTPUT_BYTES.store(0, Ordering::Relaxed);
}
pub fn take_turn_budget(wanted: usize) -> usize {
let remaining = limits().turn_max_bytes;
let used = TURN_OUTPUT_BYTES.load(Ordering::Relaxed);
let granted = wanted.min(remaining.saturating_sub(used));
TURN_OUTPUT_BYTES.fetch_add(granted, Ordering::Relaxed);
granted
}
pub fn limits() -> Limits {
RemoteConfig::load().limits
}
pub fn connected() -> Vec<Endpoint> {
let mut map = pool().lock().unwrap();
map.retain(|_, conn| conn.is_alive());
let mut out: Vec<Endpoint> = map.values().map(|conn| conn.endpoint()).collect();
out.sort_by_key(|e| e.key());
out
}
pub fn sync_endpoint(ep: &Endpoint) -> bool {
let mut map = pool().lock().unwrap();
map.retain(|_, conn| conn.is_alive());
let Some(conn) = map.get(&ep.key()) else {
return false;
};
*conn.endpoint.write().unwrap_or_else(|e| e.into_inner()) = ep.clone();
true
}
pub async fn ensure(ep: Endpoint) -> Result<Arc<RemoteConn>, String> {
let key = ep.key();
if let Some(conn) = lookup(&key) {
return Ok(conn);
}
let _guard = CONNECT_LOCK.lock().await;
if let Some(conn) = lookup(&key) {
return Ok(conn);
}
let conn = Arc::new(connect_now(ep).await?);
install(conn.clone());
Ok(conn)
}
pub async fn resolve_default() -> Result<Arc<RemoteConn>, String> {
let cfg = RemoteConfig::load();
match cfg.default {
Some(ep) => ensure(ep).await,
None => Err(
"no SSH target: pass `host` (`[user@]host[:port]` or a ~/.ssh/config alias) or set a \
default with `/ssh <target>`"
.to_string(),
),
}
}
pub async fn resolve_host(host: &str) -> Result<Arc<RemoteConn>, String> {
let spec = host.trim();
if spec.is_empty() {
return resolve_default().await;
}
if let Some(conn) = find_connected(spec) {
return Ok(conn);
}
let cfg = RemoteConfig::load();
if let Some(ep) = cfg.known.iter().find(|ep| matches_spec(ep, spec)) {
return ensure(ep.clone()).await;
}
let endpoint = parse_ssh_command(&format!("ssh {spec}"))?;
ensure(endpoint).await
}
fn find_connected(spec: &str) -> Option<Arc<RemoteConn>> {
let mut map = pool().lock().unwrap();
map.retain(|_, conn| conn.is_alive());
map.values()
.find(|conn| matches_spec(&conn.endpoint(), spec))
.cloned()
}
pub fn matches_spec(ep: &Endpoint, spec: &str) -> bool {
let spec = spec.trim();
if spec.is_empty() {
return false;
}
if ep.key() == spec || ep.host == spec || format!("{}:{}", ep.host, ep.port) == spec {
return true;
}
ep.command
.split_whitespace()
.any(|word| word == spec || word.ends_with(&format!("@{spec}")))
}
fn lookup(key: &str) -> Option<Arc<RemoteConn>> {
let mut map = pool().lock().unwrap();
match map.get(key) {
Some(conn) if conn.is_alive() => Some(conn.clone()),
Some(_) => {
map.remove(key);
None
}
None => None,
}
}
fn install(conn: Arc<RemoteConn>) {
let mut map = pool().lock().unwrap();
if map.len() >= MAX_POOLED {
let mut keys: Vec<String> = map.keys().cloned().collect();
keys.sort();
for key in keys.into_iter().take(MAX_POOLED / 2) {
map.remove(&key);
}
}
map.insert(conn.endpoint().key(), conn);
}
async fn connect_now(ep: Endpoint) -> Result<RemoteConn, String> {
let target = connect::Target {
host: ep.host.clone(),
port: ep.port,
user: ep.user.clone(),
identity: ep.identity.clone(),
password_file: ep.password_file.clone(),
jump: ep.jump.clone(),
};
let conn = connect::connect(&target).await?;
remember(&ep);
Ok(RemoteConn {
endpoint: RwLock::new(ep),
handle: Arc::new(conn.handle),
_jumps: conn.jump_handles,
})
}
fn remember(ep: &Endpoint) {
let mut cfg = RemoteConfig::load();
if cfg.remember(ep) {
_ = cfg.save();
}
}
async fn reconnect(ep: Endpoint) -> Result<Arc<RemoteConn>, String> {
let _guard = CONNECT_LOCK.lock().await;
let key = ep.key();
pool().lock().unwrap().remove(&key);
let conn = Arc::new(connect_now(ep).await?);
install(conn.clone());
Ok(conn)
}
pub async fn exec(
conn: &Arc<RemoteConn>,
command: &str,
cwd: Option<&str>,
timeout: Option<Duration>,
max_bytes: usize,
) -> Result<CommandOutput, String> {
let endpoint = conn.endpoint();
let cwd = cwd
.map(|c| c.to_string())
.unwrap_or_else(|| endpoint.cwd.clone());
let timeout = timeout.unwrap_or_else(|| Duration::from_secs(limits().timeout_seconds as u64));
let first = connect::exec_capture(&conn.handle, command, &cwd, max_bytes, timeout).await;
if first.is_ok() {
return first;
}
let first_err = first.expect_err("checked is_ok above");
let fresh = reconnect(endpoint)
.await
.map_err(|e| format!("{first_err}; reconnect failed: {e}"))?;
connect::exec_capture(&fresh.handle, command, &cwd, max_bytes, timeout)
.await
.map_err(|second| format!("{first_err}; retried after reconnect: {second}"))
}
pub fn disconnect(key: &str) -> bool {
pool().lock().unwrap().remove(key).is_some()
}
pub fn disconnect_all() -> usize {
let mut map = pool().lock().unwrap();
let count = map.len();
map.clear();
count
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spec_matching_accepts_key_host_and_command() {
let ep = Endpoint {
command: "ssh root@gpu1 -p 2202".to_string(),
host: "10.0.0.5".to_string(),
user: "root".to_string(),
port: 2202,
identity: None,
password_file: None,
jump: None,
note: String::new(),
cwd: "~".to_string(),
};
assert!(matches_spec(&ep, "root@10.0.0.5:2202"));
assert!(matches_spec(&ep, "10.0.0.5"));
assert!(matches_spec(&ep, "10.0.0.5:2202"));
assert!(matches_spec(&ep, "root@gpu1"));
assert!(matches_spec(&ep, "gpu1"), "别名不带用户也认");
assert!(!matches_spec(&ep, "gpu2"));
assert!(!matches_spec(&ep, "10.0.0"));
assert!(!matches_spec(&ep, "gpu"));
assert!(!matches_spec(&ep, ""), "空串不该命中任何端点");
}
#[test]
fn turn_budget_is_shared_and_capped() {
reset_turn_budget();
let cap = limits().turn_max_bytes;
let granted = take_turn_budget(cap * 2);
assert_eq!(granted, cap, "单次申请不应超过剩余预算");
assert_eq!(take_turn_budget(1), 0, "预算用尽后不再发放");
reset_turn_budget();
assert!(take_turn_budget(16) > 0, "归零后重新可用");
}
}