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:
+42
-1
@@ -472,6 +472,20 @@ impl ServerState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether a session named `name` should be backed by a persistent holder.
|
||||
///
|
||||
/// Persistence is only worthwhile for sessions a user can actually reattach
|
||||
/// to by name: prewarmed sessions and explicitly-named ones (`--session`).
|
||||
/// Implicit `term-<millis>-<pid>` names (from `dosh host` with no session)
|
||||
/// are ephemeral — persisting them just leaves unreattachable holders
|
||||
/// lingering across restarts, so they use the in-process (dies-on-restart)
|
||||
/// shell that reaps normally on disconnect.
|
||||
fn should_persist_session(&self, name: &str) -> bool {
|
||||
self.config.persist_sessions
|
||||
&& (self.config.prewarm_sessions.iter().any(|s| s == name)
|
||||
|| !protocol::is_implicit_session_name(name))
|
||||
}
|
||||
|
||||
/// Spawn (or, in the persistent case, spawn-and-adopt) a PTY for a session.
|
||||
///
|
||||
/// With `persist_sessions` on, the shell is launched in a detached holder
|
||||
@@ -488,7 +502,7 @@ impl ServerState {
|
||||
) -> Result<(PtyHandle, Option<StdUnixStream>, bool)> {
|
||||
let cols = cols.max(1);
|
||||
let rows = rows.max(1);
|
||||
if self.config.persist_sessions {
|
||||
if self.should_persist_session(name) {
|
||||
match self.spawn_persistent_pty(name, cols, rows, env) {
|
||||
Ok(pair) => return Ok((pair.0, Some(pair.1), true)),
|
||||
Err(err) => {
|
||||
@@ -2911,6 +2925,33 @@ fn parse_env_pairs(raw: &[String]) -> Result<Vec<(String, String)>> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn persists_named_and_prewarmed_but_not_implicit_sessions() {
|
||||
let (pty_tx, _rx) = mpsc::unbounded_channel();
|
||||
let config = ServerConfig {
|
||||
persist_sessions: true,
|
||||
prewarm_sessions: vec!["default".to_string()],
|
||||
..ServerConfig::default()
|
||||
};
|
||||
let state = ServerState::new(config, [0u8; 32], pty_tx);
|
||||
assert!(state.should_persist_session("default")); // prewarmed
|
||||
assert!(state.should_persist_session("work")); // explicitly named
|
||||
assert!(!state.should_persist_session("term-1781470634216-76685")); // implicit
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persist_disabled_never_persists() {
|
||||
let (pty_tx, _rx) = mpsc::unbounded_channel();
|
||||
let config = ServerConfig {
|
||||
persist_sessions: false,
|
||||
prewarm_sessions: vec!["default".to_string()],
|
||||
..ServerConfig::default()
|
||||
};
|
||||
let state = ServerState::new(config, [0u8; 32], pty_tx);
|
||||
assert!(!state.should_persist_session("default"));
|
||||
assert!(!state.should_persist_session("work"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_auth_rate_limiter_throttles_per_ip_and_refills() {
|
||||
let mut limiter = NativeAuthRateLimiter::new(3);
|
||||
|
||||
Reference in New Issue
Block a user