fix: latency cache staleness, reduce duplicate probes, fix CI arch detection and nfpm compression
Release / Build aarch64-unknown-linux-gnu (push) Has been cancelled
Release / Build aarch64-apple-darwin (push) Has been cancelled
Release / Build aarch64-pc-windows-msvc (push) Has been cancelled
Release / Build x86_64-unknown-linux-gnu (push) Has been cancelled
Release / Build x86_64-apple-darwin (push) Has been cancelled
Release / Build x86_64-pc-windows-msvc (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled

This commit is contained in:
2026-06-03 21:16:42 +08:00
parent 285a2049cc
commit 721b659b4b
6 changed files with 77 additions and 24 deletions
+1 -4
View File
@@ -126,12 +126,9 @@ jobs:
uses: Minionguyjpro/Inno-Setup-Action@v1.2.8 uses: Minionguyjpro/Inno-Setup-Action@v1.2.8
env: env:
SHELL_VERSION: ${{ env.SHELL_VERSION }} SHELL_VERSION: ${{ env.SHELL_VERSION }}
SSH_TARGET: ${{ matrix.target }}
with: with:
path: sshell.iss path: sshell.iss
options: >-
/DMyArch=${{ matrix.target == 'x86_64-pc-windows-msvc' && 'x64compatible' || 'arm64' }}
/DMyArchAllowed=${{ matrix.target == 'x86_64-pc-windows-msvc' && 'x64compatible' || 'arm64' }}
/DMyArchInstall64=${{ matrix.target == 'x86_64-pc-windows-msvc' && 'x64compatible' || 'arm64' }}
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
+2 -2
View File
@@ -19,10 +19,10 @@ contents:
mode: 0755 mode: 0755
deb: deb:
compression: gz compression: gzip
rpm: rpm:
compression: gz compression: gzip
archlinux: archlinux:
pkgbase: sshell pkgbase: sshell
+11 -1
View File
@@ -16,9 +16,19 @@ pub enum LatencyStatus {
Unknown, Unknown,
} }
/// A cached probe result with a timestamp.
#[derive(Debug, Clone)]
pub struct CacheEntry {
pub status: LatencyStatus,
pub checked_at: Instant,
}
/// How long before a cached entry is considered stale and re-probed.
pub const STALE_SECS: u64 = 30;
/// Shared latency cache keyed by "host:port" strings. /// Shared latency cache keyed by "host:port" strings.
/// Background threads write, draw reads. /// Background threads write, draw reads.
pub type LatencyCache = Arc<Mutex<HashMap<String, LatencyStatus>>>; pub type LatencyCache = Arc<Mutex<HashMap<String, CacheEntry>>>;
/// Perform a TCP connect to measure latency. /// Perform a TCP connect to measure latency.
/// Timeout is 3 seconds. Called from spawned threads. /// Timeout is 3 seconds. Called from spawned threads.
+47 -11
View File
@@ -8,7 +8,6 @@ use crossterm::{
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
}; };
use ratatui::{Terminal, backend::CrosstermBackend}; use ratatui::{Terminal, backend::CrosstermBackend};
use std::collections::HashSet;
use std::io; use std::io;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration; use std::time::Duration;
@@ -33,12 +32,12 @@ pub fn run() -> Result<()> {
if app.session.should_quit { if app.session.should_quit {
break; break;
} }
spawn_latency_probes(&app);
if event::poll(Duration::from_millis(200))? if event::poll(Duration::from_millis(200))?
&& let Event::Key(key) = event::read()? && let Event::Key(key) = event::read()?
&& key.kind == KeyEventKind::Press && key.kind == KeyEventKind::Press
{ {
handle_key(&mut app, key)?; handle_key(&mut app, key)?;
spawn_latency_probes(&app);
} }
} }
@@ -90,25 +89,62 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Result<()> {
} }
fn spawn_latency_probes(app: &App) { fn spawn_latency_probes(app: &App) {
let cache = app.session.latency.lock().unwrap(); use crate::app::latency::{CacheEntry, STALE_SECS};
let existing: HashSet<String> = cache.keys().cloned().collect(); use std::time::Instant;
drop(cache);
let stale_duration = Duration::from_secs(STALE_SECS);
let now = Instant::now();
// Collect keys that need probing (missing or stale)
let cache = app.session.latency.lock().unwrap();
let mut to_probe: Vec<String> = Vec::new();
for (_, profile) in app.entries() { for (_, profile) in app.entries() {
if let ConnectionType::Ssh { host, port, .. } = &profile.kind { if let ConnectionType::Ssh { host, port, .. } = &profile.kind {
let key = format!("{host}:{port}"); let key = format!("{host}:{port}");
if existing.contains(&key) { let needs_probe = match cache.get(&key) {
None => true,
Some(entry) => now.duration_since(entry.checked_at) >= stale_duration,
};
if needs_probe && !to_probe.contains(&key) {
to_probe.push(key);
}
}
}
drop(cache);
for key in to_probe {
// Re-check under lock to avoid duplicate spawns
{
let cache = app.session.latency.lock().unwrap();
if let Some(entry) = cache.get(&key) {
if now.duration_since(entry.checked_at) < stale_duration {
continue; continue;
} }
}
}
// Mark as "in-flight" by inserting a fresh entry
{
let mut cache = app.session.latency.lock().unwrap();
cache.insert(key.clone(), CacheEntry {
status: crate::app::latency::LatencyStatus::Unknown,
checked_at: now,
});
}
let cache_clone = app.session.latency.clone(); let cache_clone = app.session.latency.clone();
let host = host.clone(); let host_port = key.clone();
let port = *port;
std::thread::spawn(move || { std::thread::spawn(move || {
let status = crate::app::latency::probe(&host, port); let parts: Vec<&str> = host_port.splitn(2, ':').collect();
let (host, port) = match parts.as_slice() {
[h, p] => (*h, p.parse::<u16>().unwrap_or(22)),
_ => return,
};
let status = crate::app::latency::probe(host, port);
if let Ok(mut cache) = cache_clone.lock() { if let Ok(mut cache) = cache_clone.lock() {
cache.insert(key, status); cache.insert(host_port, CacheEntry {
status,
checked_at: Instant::now(),
});
} }
}); });
} }
} }
}
+1 -1
View File
@@ -269,7 +269,7 @@ fn connection_row(
ConnectionType::Ssh { host, port, .. } => { ConnectionType::Ssh { host, port, .. } => {
let key = format!("{host}:{port}"); let key = format!("{host}:{port}");
let cache = app.session.latency.lock().unwrap(); let cache = app.session.latency.lock().unwrap();
match cache.get(&key) { match cache.get(&key).map(|e| &e.status) {
Some(LatencyStatus::Reachable { ms }) => { Some(LatencyStatus::Reachable { ms }) => {
let text = format!("{ms} ms"); let text = format!("{ms} ms");
let color = if *ms < 100 { let color = if *ms < 100 {
+10
View File
@@ -7,6 +7,16 @@
#define MyAppURL "https://github.com/Rain-Bus/sshell" #define MyAppURL "https://github.com/Rain-Bus/sshell"
#define MyAppExeName "sshell.exe" #define MyAppExeName "sshell.exe"
#if GetEnv("SSH_TARGET") == "aarch64-pc-windows-msvc"
#define MyArch "arm64"
#define MyArchAllowed "arm64"
#define MyArchInstall64 "arm64"
#else
#define MyArch "x64compatible"
#define MyArchAllowed "x64compatible"
#define MyArchInstall64 "x64compatible"
#endif
[Setup] [Setup]
AppId={{sshell-2024-1} AppId={{sshell-2024-1}
AppName={#MyAppName} AppName={#MyAppName}