Persist only named/prewarmed sessions, not implicit ones
Implicit sessions (dosh host with no --session) get a random
term-<millis>-<pid> name you can never reattach to, so persisting them just
left unreattachable holder processes lingering across restarts. The server
now only uses a persistent holder for prewarmed or explicitly-named
sessions; implicit ones use the in-process shell that dies on restart and
reaps on disconnect, as before. Adds a shared
protocol::{generate,is}_implicit_session_name as the single source of truth
for the name shape, used by both client (generate) and server (detect).
Server-only behavior change; wire VERSION unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,39 @@ use crate::crypto;
|
||||
use crate::native::{EnvVar, NativeAuthOk, NativeClientHello, NativeServerHello, NativeUserAuth};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// Generate a name for an implicit, ephemeral terminal session — the kind
|
||||
/// created by `dosh host` with no `--session`. Single source of truth shared by
|
||||
/// the client (which generates it) and the server (which recognizes it via
|
||||
/// [`is_implicit_session_name`] to decide a session is NOT worth persisting).
|
||||
pub fn generate_implicit_session_name() -> String {
|
||||
let millis = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis();
|
||||
format!("term-{millis}-{}", std::process::id())
|
||||
}
|
||||
|
||||
/// Whether `name` is a client-generated implicit session name
|
||||
/// (`term-<digits>-<digits>`). Such sessions are ephemeral: the user can never
|
||||
/// reattach by name, so the server must not persist them across restarts.
|
||||
/// Explicitly-named (`--session work`) and prewarmed sessions are not implicit.
|
||||
pub fn is_implicit_session_name(name: &str) -> bool {
|
||||
let Some(rest) = name.strip_prefix("term-") else {
|
||||
return false;
|
||||
};
|
||||
let mut parts = rest.split('-');
|
||||
match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some(millis), Some(pid), None) => {
|
||||
!millis.is_empty()
|
||||
&& millis.bytes().all(|b| b.is_ascii_digit())
|
||||
&& !pid.is_empty()
|
||||
&& pid.bytes().all(|b| b.is_ascii_digit())
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub const MAGIC: &[u8; 4] = b"DOSH";
|
||||
// v3: added `ForwardingKind::Agent` (SSH-agent forwarding). The new variant rides
|
||||
@@ -496,3 +529,41 @@ impl ReplayWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod session_name_tests {
|
||||
use super::{generate_implicit_session_name, is_implicit_session_name};
|
||||
|
||||
#[test]
|
||||
fn generated_names_are_recognized_as_implicit() {
|
||||
let name = generate_implicit_session_name();
|
||||
assert!(is_implicit_session_name(&name), "generated: {name}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_and_prewarm_names_are_not_implicit() {
|
||||
for name in [
|
||||
"default",
|
||||
"work",
|
||||
"logs",
|
||||
"term",
|
||||
"term-",
|
||||
"term-abc-1",
|
||||
"term-1-x",
|
||||
"term-1",
|
||||
"term-1-2-3",
|
||||
"",
|
||||
] {
|
||||
assert!(
|
||||
!is_implicit_session_name(name),
|
||||
"should not be implicit: {name:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_shape_matches() {
|
||||
assert!(is_implicit_session_name("term-1781470634216-76685"));
|
||||
assert!(is_implicit_session_name("term-0-0"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user