Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3364a7eb7b | ||
|
|
970d54b991 | ||
|
|
b8b56c0f32 |
Generated
+1
-1
@@ -436,7 +436,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dosh"
|
name = "dosh"
|
||||||
version = "1.0.0-rc43"
|
version = "1.0.0-rc44"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dosh"
|
name = "dosh"
|
||||||
version = "1.0.0-rc43"
|
version = "1.0.0-rc44"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|||||||
+99
-10
@@ -398,6 +398,10 @@ struct Session {
|
|||||||
holder_control: Option<StdUnixStream>,
|
holder_control: Option<StdUnixStream>,
|
||||||
/// Whether this session's shell lives in a holder process (persistent).
|
/// Whether this session's shell lives in a holder process (persistent).
|
||||||
persistent: bool,
|
persistent: bool,
|
||||||
|
/// The holder survived a server restart but no client has reattached yet.
|
||||||
|
/// Such sessions need the full reconnect window because the client may be
|
||||||
|
/// asleep during an unattended server update.
|
||||||
|
restart_orphaned: bool,
|
||||||
/// Bytes of session output since the screen was last mirrored to disk, used
|
/// Bytes of session output since the screen was last mirrored to disk, used
|
||||||
/// to throttle the (atomic) screen-persistence writes.
|
/// to throttle the (atomic) screen-persistence writes.
|
||||||
bytes_since_persist: usize,
|
bytes_since_persist: usize,
|
||||||
@@ -569,6 +573,7 @@ impl ServerState {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: control,
|
holder_control: control,
|
||||||
persistent,
|
persistent,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now() - SCREEN_PERSIST_MAX_AGE,
|
last_screen_persist_at: Instant::now() - SCREEN_PERSIST_MAX_AGE,
|
||||||
@@ -714,6 +719,7 @@ impl ServerState {
|
|||||||
empty_since: Some(Instant::now()),
|
empty_since: Some(Instant::now()),
|
||||||
holder_control: Some(control),
|
holder_control: Some(control),
|
||||||
persistent: true,
|
persistent: true,
|
||||||
|
restart_orphaned: true,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: output_seq,
|
last_persisted_seq: output_seq,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -732,6 +738,7 @@ impl ServerState {
|
|||||||
if let Some(session) = self.sessions.get_mut(session_name) {
|
if let Some(session) = self.sessions.get_mut(session_name) {
|
||||||
session.clients.insert(client_id, client);
|
session.clients.insert(client_id, client);
|
||||||
session.empty_since = None;
|
session.empty_since = None;
|
||||||
|
session.restart_orphaned = false;
|
||||||
self.client_index
|
self.client_index
|
||||||
.insert(client_id, session_name.to_string());
|
.insert(client_id, session_name.to_string());
|
||||||
}
|
}
|
||||||
@@ -3271,15 +3278,25 @@ async fn run_exec_stream_service(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let output = TokioCommand::new("sh")
|
let shell = {
|
||||||
.arg("-lc")
|
state
|
||||||
|
.lock()
|
||||||
|
.expect("server state poisoned")
|
||||||
|
.config
|
||||||
|
.shell
|
||||||
|
.clone()
|
||||||
|
};
|
||||||
|
let output = TokioCommand::new(&shell)
|
||||||
|
.arg("-c")
|
||||||
.arg(&request.command)
|
.arg(&request.command)
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("run command {:?}", request.command))?;
|
.with_context(|| {
|
||||||
|
format!("run command {:?} with shell {shell}", request.command)
|
||||||
|
})?;
|
||||||
for chunk in output.stdout.chunks(CHUNK_SIZE) {
|
for chunk in output.stdout.chunks(CHUNK_SIZE) {
|
||||||
send_exec_response_to_client(
|
send_exec_response_to_client(
|
||||||
&state,
|
&state,
|
||||||
@@ -4447,7 +4464,8 @@ fn cleanup_disconnected_clients(state: &Arc<Mutex<ServerState>>) {
|
|||||||
.filter(|(name, session)| {
|
.filter(|(name, session)| {
|
||||||
!prewarm.contains(name.as_str())
|
!prewarm.contains(name.as_str())
|
||||||
&& session.empty_since.is_some_and(|since| {
|
&& session.empty_since.is_some_and(|since| {
|
||||||
now.duration_since(since) >= empty_session_timeout(name, timeout)
|
now.duration_since(since)
|
||||||
|
>= empty_session_timeout(name, timeout, session.restart_orphaned)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.map(|(name, _)| name.clone())
|
.map(|(name, _)| name.clone())
|
||||||
@@ -4467,8 +4485,12 @@ fn cleanup_disconnected_clients(state: &Arc<Mutex<ServerState>>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn empty_session_timeout(name: &str, configured_timeout: Duration) -> Duration {
|
fn empty_session_timeout(
|
||||||
if protocol::is_implicit_session_name(name) {
|
name: &str,
|
||||||
|
configured_timeout: Duration,
|
||||||
|
restart_orphaned: bool,
|
||||||
|
) -> Duration {
|
||||||
|
if protocol::is_implicit_session_name(name) && !restart_orphaned {
|
||||||
configured_timeout.min(Duration::from_secs(IMPLICIT_EMPTY_SESSION_GRACE_SECS))
|
configured_timeout.min(Duration::from_secs(IMPLICIT_EMPTY_SESSION_GRACE_SECS))
|
||||||
} else {
|
} else {
|
||||||
configured_timeout
|
configured_timeout
|
||||||
@@ -4613,6 +4635,7 @@ mod tests {
|
|||||||
empty_since: Some(Instant::now()),
|
empty_since: Some(Instant::now()),
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: true,
|
persistent: true,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 7,
|
last_persisted_seq: 7,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -4640,6 +4663,7 @@ mod tests {
|
|||||||
empty_since: Some(Instant::now()),
|
empty_since: Some(Instant::now()),
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: true,
|
persistent: true,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 7,
|
last_persisted_seq: 7,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -4687,6 +4711,7 @@ mod tests {
|
|||||||
empty_since: Some(Instant::now()),
|
empty_since: Some(Instant::now()),
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: true,
|
persistent: true,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 7,
|
last_persisted_seq: 7,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -5036,25 +5061,76 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn implicit_empty_session_timeout_is_bounded_for_update_reconnect() {
|
fn implicit_empty_session_timeout_preserves_restart_orphans() {
|
||||||
let configured = Duration::from_secs(2_592_000);
|
let configured = Duration::from_secs(2_592_000);
|
||||||
let implicit = protocol::generate_implicit_session_name();
|
let implicit = protocol::generate_implicit_session_name();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
empty_session_timeout(&implicit, configured),
|
empty_session_timeout(&implicit, configured, false),
|
||||||
Duration::from_secs(IMPLICIT_EMPTY_SESSION_GRACE_SECS)
|
Duration::from_secs(IMPLICIT_EMPTY_SESSION_GRACE_SECS)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
empty_session_timeout("work", configured),
|
empty_session_timeout(&implicit, configured, true),
|
||||||
|
configured,
|
||||||
|
"a sleeping client must retain its restart-surviving shell"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
empty_session_timeout("work", configured, false),
|
||||||
configured,
|
configured,
|
||||||
"named sessions keep the normal long timeout"
|
"named sessions keep the normal long timeout"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
empty_session_timeout(&implicit, Duration::from_secs(1)),
|
empty_session_timeout(&implicit, Duration::from_secs(1), false),
|
||||||
Duration::from_secs(1),
|
Duration::from_secs(1),
|
||||||
"tests/admins can still configure a shorter timeout"
|
"tests/admins can still configure a shorter timeout"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cleanup_keeps_restart_orphan_then_reaps_after_reattach_disconnect() {
|
||||||
|
let (pty_tx, _pty_rx) = mpsc::channel(PTY_OUTPUT_QUEUE_CAPACITY);
|
||||||
|
let mut state = ServerState::new(
|
||||||
|
ServerConfig {
|
||||||
|
client_timeout_secs: 2_592_000,
|
||||||
|
prewarm_sessions: Vec::new(),
|
||||||
|
..ServerConfig::default()
|
||||||
|
},
|
||||||
|
[0u8; 32],
|
||||||
|
pty_tx,
|
||||||
|
);
|
||||||
|
let implicit = protocol::generate_implicit_session_name();
|
||||||
|
state
|
||||||
|
.ensure_session(&implicit, 80, 24, "forward-only", &[])
|
||||||
|
.unwrap();
|
||||||
|
{
|
||||||
|
let session = state.sessions.get_mut(&implicit).unwrap();
|
||||||
|
session.empty_since = Some(
|
||||||
|
Instant::now()
|
||||||
|
- Duration::from_secs(IMPLICIT_EMPTY_SESSION_GRACE_SECS + 10),
|
||||||
|
);
|
||||||
|
session.restart_orphaned = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = Arc::new(Mutex::new(state));
|
||||||
|
cleanup_disconnected_clients(&state);
|
||||||
|
assert!(
|
||||||
|
state.lock().unwrap().sessions.contains_key(&implicit),
|
||||||
|
"restart orphan was reaped before the reconnect timeout"
|
||||||
|
);
|
||||||
|
|
||||||
|
state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.sessions
|
||||||
|
.get_mut(&implicit)
|
||||||
|
.unwrap()
|
||||||
|
.restart_orphaned = false;
|
||||||
|
cleanup_disconnected_clients(&state);
|
||||||
|
assert!(
|
||||||
|
!state.lock().unwrap().sessions.contains_key(&implicit),
|
||||||
|
"ordinary abandoned implicit session was not reaped"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn forged_plaintext_detach_does_not_remove_client() {
|
async fn forged_plaintext_detach_does_not_remove_client() {
|
||||||
let (pty_tx, _pty_rx) = mpsc::channel(PTY_OUTPUT_QUEUE_CAPACITY);
|
let (pty_tx, _pty_rx) = mpsc::channel(PTY_OUTPUT_QUEUE_CAPACITY);
|
||||||
@@ -5136,6 +5212,7 @@ mod tests {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -5201,6 +5278,7 @@ mod tests {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -5278,6 +5356,7 @@ mod tests {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -5337,6 +5416,7 @@ mod tests {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -5514,6 +5594,7 @@ mod tests {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -5618,6 +5699,7 @@ mod tests {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -5683,6 +5765,7 @@ mod tests {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -5740,6 +5823,7 @@ mod tests {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -5796,6 +5880,7 @@ mod tests {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -5843,6 +5928,7 @@ mod tests {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -5906,6 +5992,7 @@ mod tests {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -5982,6 +6069,7 @@ mod tests {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
@@ -6089,6 +6177,7 @@ mod tests {
|
|||||||
empty_since: None,
|
empty_since: None,
|
||||||
holder_control: None,
|
holder_control: None,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
|
restart_orphaned: false,
|
||||||
bytes_since_persist: 0,
|
bytes_since_persist: 0,
|
||||||
last_persisted_seq: 0,
|
last_persisted_seq: 0,
|
||||||
last_screen_persist_at: Instant::now(),
|
last_screen_persist_at: Instant::now(),
|
||||||
|
|||||||
@@ -1355,6 +1355,22 @@ fn native_exec_command_smoke() {
|
|||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let port = free_udp_port();
|
let port = free_udp_port();
|
||||||
let config = write_server_config(&dir, port);
|
let config = write_server_config(&dir, port);
|
||||||
|
let shell_log = dir.path().join("exec-shell.log");
|
||||||
|
let shell = dir.path().join("exec-shell");
|
||||||
|
fs::write(
|
||||||
|
&shell,
|
||||||
|
format!(
|
||||||
|
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{}'\nexec /bin/sh \"$@\"\n",
|
||||||
|
shell_log.display()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fs::set_permissions(&shell, fs::Permissions::from_mode(0o700)).unwrap();
|
||||||
|
let raw = fs::read_to_string(&config).unwrap().replace(
|
||||||
|
"shell = \"/bin/sh\"",
|
||||||
|
&format!("shell = {:?}", shell.display().to_string()),
|
||||||
|
);
|
||||||
|
fs::write(&config, raw).unwrap();
|
||||||
write_native_client_auth(&dir, &config);
|
write_native_client_auth(&dir, &config);
|
||||||
let mut server = start_server(&dir, &config);
|
let mut server = start_server(&dir, &config);
|
||||||
let client_bin = env!("CARGO_BIN_EXE_dosh-client");
|
let client_bin = env!("CARGO_BIN_EXE_dosh-client");
|
||||||
@@ -1380,6 +1396,19 @@ fn native_exec_command_smoke() {
|
|||||||
);
|
);
|
||||||
assert_eq!(String::from_utf8_lossy(&output.stdout), "out");
|
assert_eq!(String::from_utf8_lossy(&output.stdout), "out");
|
||||||
assert_eq!(String::from_utf8_lossy(&output.stderr), "err");
|
assert_eq!(String::from_utf8_lossy(&output.stderr), "err");
|
||||||
|
let shell_invocations = fs::read_to_string(shell_log).unwrap();
|
||||||
|
assert!(
|
||||||
|
shell_invocations
|
||||||
|
.lines()
|
||||||
|
.any(|line| line.starts_with("-c ")),
|
||||||
|
"configured shell was not used for exec: {shell_invocations:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!shell_invocations
|
||||||
|
.lines()
|
||||||
|
.any(|line| line.starts_with("-lc ")),
|
||||||
|
"exec must not start a login shell: {shell_invocations:?}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user