Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
237ad52bef | ||
|
|
a8ba852f16 | ||
|
|
f1a3de7730 | ||
|
|
80699dcf9d |
Generated
+1
-1
@@ -436,7 +436,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dosh"
|
name = "dosh"
|
||||||
version = "0.1.13"
|
version = "0.1.17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dosh"
|
name = "dosh"
|
||||||
version = "0.1.13"
|
version = "0.1.17"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: build test fmt clippy release-check install package-release package-release-linux package-release-windows publish-release bench-report bench-local bench-local-json bench-docker-ssh bench-docker-mosh fuzz-smoke fuzz-deep soak-local tui-harness
|
.PHONY: build test fmt clippy release-check 1.0-check reconnect-check hostile-network-check persistence-check package-check install package-release package-release-linux package-release-windows publish-release bench-report bench-local bench-local-json bench-docker-ssh bench-docker-mosh fuzz-smoke fuzz-deep soak-local tui-harness
|
||||||
|
|
||||||
build:
|
build:
|
||||||
cargo build --release
|
cargo build --release
|
||||||
@@ -19,6 +19,24 @@ release-check:
|
|||||||
cargo test
|
cargo test
|
||||||
$(MAKE) tui-harness
|
$(MAKE) tui-harness
|
||||||
|
|
||||||
|
1.0-check: release-check reconnect-check hostile-network-check persistence-check package-check
|
||||||
|
|
||||||
|
reconnect-check:
|
||||||
|
cargo test --test integration_smoke resume_updates_udp_endpoint_for_roaming -- --nocapture
|
||||||
|
DOSH_SOAK_SECONDS=$${DOSH_SOAK_SECONDS:-300} cargo test --test integration_smoke sleep_roaming_soak_30m -- --ignored --nocapture
|
||||||
|
|
||||||
|
hostile-network-check:
|
||||||
|
cargo test --test hostile_network -- --nocapture
|
||||||
|
|
||||||
|
persistence-check:
|
||||||
|
cargo test --test integration_smoke session_survives_server_restart_same_shell_and_screen -- --nocapture
|
||||||
|
cargo test --test integration_smoke multiple_persistent_named_sessions_survive_restart_independently -- --nocapture
|
||||||
|
|
||||||
|
package-check:
|
||||||
|
$(MAKE) package-release-linux
|
||||||
|
$(MAKE) package-release-windows
|
||||||
|
sh scripts/verify-release-artifacts.sh
|
||||||
|
|
||||||
install:
|
install:
|
||||||
sh install.sh --from-current
|
sh install.sh --from-current
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -426,7 +426,7 @@ Wants=network-online.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=$bindir/dosh-server serve
|
ExecStart=$bindir/dosh-server serve
|
||||||
Restart=on-failure
|
Restart=always
|
||||||
RestartSec=1
|
RestartSec=1
|
||||||
KillMode=process
|
KillMode=process
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Wants=network-online.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=%h/.local/bin/dosh-server serve
|
ExecStart=%h/.local/bin/dosh-server serve
|
||||||
Restart=on-failure
|
Restart=always
|
||||||
RestartSec=1
|
RestartSec=1
|
||||||
KillMode=process
|
KillMode=process
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
repo_root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||||
|
cd "$repo_root"
|
||||||
|
|
||||||
|
version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | sed -n '1p')"
|
||||||
|
out_dir="${DOSH_PACKAGE_DIR:-target/dosh-release}"
|
||||||
|
|
||||||
|
artifact_version() {
|
||||||
|
artifact="$1"
|
||||||
|
case "$artifact" in
|
||||||
|
*.tar.gz)
|
||||||
|
tar -xOf "$artifact" dosh/VERSION 2>/dev/null | tr -d '\r\n'
|
||||||
|
;;
|
||||||
|
*.zip)
|
||||||
|
unzip -p "$artifact" dosh/VERSION 2>/dev/null | tr -d '\r\n'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
for artifact in \
|
||||||
|
"$out_dir/dosh-linux-x86_64.tar.gz" \
|
||||||
|
"$out_dir/dosh-macos-aarch64.tar.gz" \
|
||||||
|
"$out_dir/dosh-windows-x86_64.zip"; do
|
||||||
|
if [ ! -f "$artifact" ]; then
|
||||||
|
echo "missing release artifact: $artifact" >&2
|
||||||
|
failed=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [ ! -f "$artifact.sha256" ]; then
|
||||||
|
echo "missing release checksum: $artifact.sha256" >&2
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
actual="$(artifact_version "$artifact" || true)"
|
||||||
|
if [ "$actual" != "$version" ]; then
|
||||||
|
echo "artifact version mismatch: $artifact has ${actual:-unknown}, expected $version" >&2
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
exit "$failed"
|
||||||
+123
-19
@@ -4271,7 +4271,7 @@ async fn run_terminal(
|
|||||||
let mut status_tick = tokio::time::interval(Duration::from_secs(1));
|
let mut status_tick = tokio::time::interval(Duration::from_secs(1));
|
||||||
let mut resize_tick = tokio::time::interval(Duration::from_millis(250));
|
let mut resize_tick = tokio::time::interval(Duration::from_millis(250));
|
||||||
let mut stream_retransmit_tick = tokio::time::interval(Duration::from_millis(200));
|
let mut stream_retransmit_tick = tokio::time::interval(Duration::from_millis(200));
|
||||||
let mut frame_gap_tick = tokio::time::interval(Duration::from_millis(100));
|
let mut frame_gap_tick = tokio::time::interval(Duration::from_millis(250));
|
||||||
let mut last_size = terminal_size();
|
let mut last_size = terminal_size();
|
||||||
// React to terminal resize the instant it happens via SIGWINCH (mosh-style),
|
// React to terminal resize the instant it happens via SIGWINCH (mosh-style),
|
||||||
// instead of waiting up to one `resize_tick`. The 250ms poll below stays as a
|
// instead of waiting up to one `resize_tick`. The 250ms poll below stays as a
|
||||||
@@ -4500,7 +4500,7 @@ async fn run_terminal(
|
|||||||
maybe_send_resize(&socket, addr, &cred, &mut send_seq, &mut last_size).await?;
|
maybe_send_resize(&socket, addr, &cred, &mut send_seq, &mut last_size).await?;
|
||||||
}
|
}
|
||||||
_ = frame_gap_tick.tick() => {
|
_ = frame_gap_tick.tick() => {
|
||||||
if frame_buffer.gap_wait_elapsed(Duration::from_millis(FRAME_GAP_RESYNC_AFTER_MS))
|
if frame_buffer.resync_due()
|
||||||
&& let Some(frame) = reconnect(
|
&& let Some(frame) = reconnect(
|
||||||
&socket,
|
&socket,
|
||||||
&mut cred,
|
&mut cred,
|
||||||
@@ -4610,6 +4610,22 @@ async fn run_terminal(
|
|||||||
}
|
}
|
||||||
PacketKind::Pong => {
|
PacketKind::Pong => {
|
||||||
last_packet_at = Instant::now();
|
last_packet_at = Instant::now();
|
||||||
|
if should_flush_terminal_input_after_contact(
|
||||||
|
forward_only,
|
||||||
|
&cred.mode,
|
||||||
|
pending_user_input.is_empty(),
|
||||||
|
) {
|
||||||
|
flush_pending_user_input(
|
||||||
|
&socket,
|
||||||
|
addr,
|
||||||
|
&cred,
|
||||||
|
&mut send_seq,
|
||||||
|
&mut predictor,
|
||||||
|
&mut pending_user_input,
|
||||||
|
&mut pending_user_input_bytes,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
PacketKind::Rekey => {
|
PacketKind::Rekey => {
|
||||||
// Server-initiated transport rekey (spec §11). The Rekey is
|
// Server-initiated transport rekey (spec §11). The Rekey is
|
||||||
@@ -5439,6 +5455,14 @@ fn should_hold_during_startup_gate(
|
|||||||
mode == StartupGateMode::HoldAll || already_queued || should_hold_post_submit_input(bytes)
|
mode == StartupGateMode::HoldAll || already_queued || should_hold_post_submit_input(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_flush_terminal_input_after_contact(
|
||||||
|
forward_only: bool,
|
||||||
|
mode: &str,
|
||||||
|
pending_empty: bool,
|
||||||
|
) -> bool {
|
||||||
|
!forward_only && !pending_empty && mode != "view-only" && mode != "forward-only"
|
||||||
|
}
|
||||||
|
|
||||||
async fn flush_pending_user_input(
|
async fn flush_pending_user_input(
|
||||||
socket: &UdpSocket,
|
socket: &UdpSocket,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
@@ -6592,18 +6616,22 @@ fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
|
|||||||
.any(|window| window == needle)
|
.any(|window| window == needle)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FRAME_GAP_RESYNC_AFTER_MS: u64 = 500;
|
const FRAME_GAP_RESYNC_AFTER_MS: u64 = 2_500;
|
||||||
|
const FRAME_GAP_RESYNC_COOLDOWN_MS: u64 = 5_000;
|
||||||
|
const FRAME_GAP_PENDING_RESYNC_THRESHOLD: usize = 128;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct FrameBuffer {
|
struct FrameBuffer {
|
||||||
pending: BTreeMap<u64, Frame>,
|
pending: BTreeMap<u64, Frame>,
|
||||||
gap_since: Option<Instant>,
|
gap_since: Option<Instant>,
|
||||||
|
last_resync_at: Option<Instant>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FrameBuffer {
|
impl FrameBuffer {
|
||||||
fn clear(&mut self) {
|
fn clear(&mut self) {
|
||||||
self.pending.clear();
|
self.pending.clear();
|
||||||
self.gap_since = None;
|
self.gap_since = None;
|
||||||
|
self.last_resync_at = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept(&mut self, frame: Frame, last_rendered_seq: &mut u64) -> Vec<Frame> {
|
fn accept(&mut self, frame: Frame, last_rendered_seq: &mut u64) -> Vec<Frame> {
|
||||||
@@ -6613,6 +6641,7 @@ impl FrameBuffer {
|
|||||||
}
|
}
|
||||||
self.pending.clear();
|
self.pending.clear();
|
||||||
self.gap_since = None;
|
self.gap_since = None;
|
||||||
|
self.last_resync_at = None;
|
||||||
*last_rendered_seq = frame.output_seq;
|
*last_rendered_seq = frame.output_seq;
|
||||||
return vec![frame];
|
return vec![frame];
|
||||||
}
|
}
|
||||||
@@ -6639,11 +6668,28 @@ impl FrameBuffer {
|
|||||||
self.gap_since.get_or_insert_with(Instant::now);
|
self.gap_since.get_or_insert_with(Instant::now);
|
||||||
} else {
|
} else {
|
||||||
self.gap_since = None;
|
self.gap_since = None;
|
||||||
|
self.last_resync_at = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gap_wait_elapsed(&self, wait: Duration) -> bool {
|
fn resync_due(&mut self) -> bool {
|
||||||
self.gap_since.is_some_and(|since| since.elapsed() >= wait)
|
let Some(gap_since) = self.gap_since else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let now = Instant::now();
|
||||||
|
let waited = now.duration_since(gap_since);
|
||||||
|
if waited < Duration::from_millis(FRAME_GAP_RESYNC_AFTER_MS)
|
||||||
|
&& self.pending.len() < FRAME_GAP_PENDING_RESYNC_THRESHOLD
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if self.last_resync_at.is_some_and(|last| {
|
||||||
|
now.duration_since(last) < Duration::from_millis(FRAME_GAP_RESYNC_COOLDOWN_MS)
|
||||||
|
}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.last_resync_at = Some(now);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -7073,11 +7119,11 @@ mod tests {
|
|||||||
release_tag_download_url, release_tag_from_effective_url, release_version_from_tag,
|
release_tag_download_url, release_tag_from_effective_url, release_version_from_tag,
|
||||||
render_status_clear, render_status_overlay, requested_env, resolved_startup_command,
|
render_status_clear, render_status_overlay, requested_env, resolved_startup_command,
|
||||||
retransmit_stream_opens, rewrite_forward_command, selected_predict_mode, selected_udp_host,
|
retransmit_stream_opens, rewrite_forward_command, selected_predict_mode, selected_udp_host,
|
||||||
server_version_mismatch, should_hold_during_startup_gate, should_hold_post_submit_input,
|
server_version_mismatch, should_flush_terminal_input_after_contact,
|
||||||
split_after_command_submit, ssh_destination_host, ssh_username, ssh_with_user,
|
should_hold_during_startup_gate, should_hold_post_submit_input, split_after_command_submit,
|
||||||
startup_command, status_ssh_target, strip_stale_mouse_reports, toml_bare_key_or_quoted,
|
ssh_destination_host, ssh_username, ssh_with_user, startup_command, status_ssh_target,
|
||||||
update_check_requested, update_version_status, upsert_managed_block, valid_forward_host,
|
strip_stale_mouse_reports, toml_bare_key_or_quoted, update_check_requested,
|
||||||
vscode_safe_alias,
|
update_version_status, upsert_managed_block, valid_forward_host, vscode_safe_alias,
|
||||||
};
|
};
|
||||||
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
||||||
use dosh::native::EnvVar;
|
use dosh::native::EnvVar;
|
||||||
@@ -7483,14 +7529,12 @@ mod tests {
|
|||||||
|
|
||||||
assert!(buffer.accept(test_frame(12, false), &mut last).is_empty());
|
assert!(buffer.accept(test_frame(12, false), &mut last).is_empty());
|
||||||
assert_eq!(last, 10);
|
assert_eq!(last, 10);
|
||||||
assert!(!buffer.gap_wait_elapsed(Duration::from_secs(60)));
|
|
||||||
|
|
||||||
let ready = buffer.accept(test_frame(11, false), &mut last);
|
let ready = buffer.accept(test_frame(11, false), &mut last);
|
||||||
assert_eq!(ready.len(), 2);
|
assert_eq!(ready.len(), 2);
|
||||||
assert_eq!(ready[0].output_seq, 11);
|
assert_eq!(ready[0].output_seq, 11);
|
||||||
assert_eq!(ready[1].output_seq, 12);
|
assert_eq!(ready[1].output_seq, 12);
|
||||||
assert_eq!(last, 12);
|
assert_eq!(last, 12);
|
||||||
assert!(!buffer.gap_wait_elapsed(Duration::from_secs(0)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -7509,28 +7553,60 @@ mod tests {
|
|||||||
let mut last = 10;
|
let mut last = 10;
|
||||||
|
|
||||||
assert!(buffer.accept(test_frame(12, false), &mut last).is_empty());
|
assert!(buffer.accept(test_frame(12, false), &mut last).is_empty());
|
||||||
buffer.backdate_gap_for_test(Duration::from_secs(10));
|
|
||||||
assert!(buffer.gap_wait_elapsed(Duration::from_millis(1)));
|
|
||||||
let ready = buffer.accept(test_frame(20, true), &mut last);
|
let ready = buffer.accept(test_frame(20, true), &mut last);
|
||||||
|
|
||||||
assert_eq!(ready.len(), 1);
|
assert_eq!(ready.len(), 1);
|
||||||
assert_eq!(ready[0].output_seq, 20);
|
assert_eq!(ready[0].output_seq, 20);
|
||||||
assert_eq!(last, 20);
|
assert_eq!(last, 20);
|
||||||
assert!(!buffer.gap_wait_elapsed(Duration::from_millis(1)));
|
|
||||||
assert!(buffer.accept(test_frame(12, false), &mut last).is_empty());
|
assert!(buffer.accept(test_frame(12, false), &mut last).is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn frame_buffer_marks_gap_ready_for_resync_after_wait() {
|
fn frame_buffer_keeps_gap_buffered_without_snapshot_resync() {
|
||||||
let mut buffer = FrameBuffer::default();
|
let mut buffer = FrameBuffer::default();
|
||||||
let mut last = 10;
|
let mut last = 10;
|
||||||
|
|
||||||
assert!(buffer.accept(test_frame(13, false), &mut last).is_empty());
|
assert!(buffer.accept(test_frame(13, false), &mut last).is_empty());
|
||||||
assert_eq!(last, 10);
|
assert_eq!(last, 10);
|
||||||
assert!(!buffer.gap_wait_elapsed(Duration::from_secs(60)));
|
assert!(buffer.accept(test_frame(14, false), &mut last).is_empty());
|
||||||
|
assert_eq!(last, 10);
|
||||||
|
assert!(!buffer.resync_due());
|
||||||
|
|
||||||
buffer.backdate_gap_for_test(Duration::from_secs(1));
|
let ready = buffer.accept(test_frame(11, false), &mut last);
|
||||||
assert!(buffer.gap_wait_elapsed(Duration::from_millis(FRAME_GAP_RESYNC_AFTER_MS)));
|
assert_eq!(ready.len(), 1);
|
||||||
|
assert_eq!(ready[0].output_seq, 11);
|
||||||
|
assert_eq!(last, 11);
|
||||||
|
|
||||||
|
let ready = buffer.accept(test_frame(12, false), &mut last);
|
||||||
|
assert_eq!(ready.len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
ready
|
||||||
|
.iter()
|
||||||
|
.map(|frame| frame.output_seq)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![12, 13, 14]
|
||||||
|
);
|
||||||
|
assert_eq!(last, 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_buffer_resyncs_old_gap_with_cooldown() {
|
||||||
|
let mut buffer = FrameBuffer::default();
|
||||||
|
let mut last = 10;
|
||||||
|
|
||||||
|
assert!(buffer.accept(test_frame(13, false), &mut last).is_empty());
|
||||||
|
buffer.backdate_gap_for_test(Duration::from_millis(FRAME_GAP_RESYNC_AFTER_MS + 10));
|
||||||
|
assert!(buffer.resync_due());
|
||||||
|
assert!(
|
||||||
|
!buffer.resync_due(),
|
||||||
|
"resync must be rate-limited so snapshots cannot churn the TUI"
|
||||||
|
);
|
||||||
|
|
||||||
|
let ready = buffer.accept(test_frame(20, true), &mut last);
|
||||||
|
assert_eq!(ready.len(), 1);
|
||||||
|
assert_eq!(ready[0].output_seq, 20);
|
||||||
|
assert_eq!(last, 20);
|
||||||
|
assert!(!buffer.resync_due());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -7827,6 +7903,34 @@ mod tests {
|
|||||||
assert_eq!(post_submit_hold_duration(b"x"), POST_SUBMIT_ALL_INPUT_HOLD);
|
assert_eq!(post_submit_hold_duration(b"x"), POST_SUBMIT_ALL_INPUT_HOLD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn contact_flushes_pending_terminal_input_for_interactive_sessions() {
|
||||||
|
assert!(should_flush_terminal_input_after_contact(
|
||||||
|
false, "normal", false
|
||||||
|
));
|
||||||
|
assert!(should_flush_terminal_input_after_contact(
|
||||||
|
false,
|
||||||
|
"read-write",
|
||||||
|
false
|
||||||
|
));
|
||||||
|
assert!(!should_flush_terminal_input_after_contact(
|
||||||
|
false, "normal", true
|
||||||
|
));
|
||||||
|
assert!(!should_flush_terminal_input_after_contact(
|
||||||
|
true, "normal", false
|
||||||
|
));
|
||||||
|
assert!(!should_flush_terminal_input_after_contact(
|
||||||
|
false,
|
||||||
|
"view-only",
|
||||||
|
false
|
||||||
|
));
|
||||||
|
assert!(!should_flush_terminal_input_after_contact(
|
||||||
|
false,
|
||||||
|
"forward-only",
|
||||||
|
false
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reconnect_refreshes_live_send_address_from_credentials() {
|
fn reconnect_refreshes_live_send_address_from_credentials() {
|
||||||
let mut addr = "127.0.0.1:50000".parse().unwrap();
|
let mut addr = "127.0.0.1:50000".parse().unwrap();
|
||||||
|
|||||||
@@ -3519,11 +3519,9 @@ async fn retransmit_pending(
|
|||||||
if pending.output_seq <= client.last_acked {
|
if pending.output_seq <= client.last_acked {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if now.duration_since(pending.last_sent) >= Duration::from_millis(200)
|
if now.duration_since(pending.last_sent) >= Duration::from_millis(200) {
|
||||||
&& pending.attempts < 8
|
|
||||||
{
|
|
||||||
pending.last_sent = now;
|
pending.last_sent = now;
|
||||||
pending.attempts += 1;
|
pending.attempts = pending.attempts.saturating_add(1);
|
||||||
sends.push((client.endpoint, pending.packet.clone()));
|
sends.push((client.endpoint, pending.packet.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-11
@@ -1392,18 +1392,10 @@ fn server_retransmits_unacked_output_frames() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut seen = Vec::new();
|
let mut seen = Vec::new();
|
||||||
let mut duplicate = None;
|
let deadline = std::time::Instant::now() + Duration::from_secs(4);
|
||||||
let deadline = std::time::Instant::now() + Duration::from_secs(3);
|
|
||||||
while std::time::Instant::now() < deadline {
|
while std::time::Instant::now() < deadline {
|
||||||
if let Some((header, frame)) = recv_frame(&socket, &bootstrap.session_key) {
|
if let Some((header, frame)) = recv_frame(&socket, &bootstrap.session_key) {
|
||||||
if String::from_utf8_lossy(&frame.bytes).contains("DOSH_RETRANSMIT") {
|
if String::from_utf8_lossy(&frame.bytes).contains("DOSH_RETRANSMIT") {
|
||||||
if seen
|
|
||||||
.iter()
|
|
||||||
.any(|(_, output_seq)| *output_seq == frame.output_seq)
|
|
||||||
{
|
|
||||||
duplicate = Some((header.seq, frame.output_seq));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
seen.push((header.seq, frame.output_seq));
|
seen.push((header.seq, frame.output_seq));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1412,9 +1404,18 @@ fn server_retransmits_unacked_output_frames() {
|
|||||||
let _ = server.kill();
|
let _ = server.kill();
|
||||||
let _ = server.wait();
|
let _ = server.wait();
|
||||||
|
|
||||||
|
let max_same_output_seq = seen
|
||||||
|
.iter()
|
||||||
|
.map(|(_, output_seq)| {
|
||||||
|
seen.iter()
|
||||||
|
.filter(|(_, candidate)| candidate == output_seq)
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
assert!(
|
assert!(
|
||||||
duplicate.is_some(),
|
max_same_output_seq >= 10,
|
||||||
"expected retransmitted same output seq, seen={seen:?}"
|
"expected retransmission to continue past the old 8-attempt ceiling, seen={seen:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ fn systemd_service_does_not_sandbox_remote_shells() {
|
|||||||
let install = include_str!("../install.sh");
|
let install = include_str!("../install.sh");
|
||||||
for raw in [service, install] {
|
for raw in [service, install] {
|
||||||
assert!(raw.contains("KillMode=process"));
|
assert!(raw.contains("KillMode=process"));
|
||||||
|
assert!(
|
||||||
|
raw.contains("Restart=always"),
|
||||||
|
"dosh-server should come back after accidental SIGTERM while preserving child shells"
|
||||||
|
);
|
||||||
for directive in [
|
for directive in [
|
||||||
"NoNewPrivileges=",
|
"NoNewPrivileges=",
|
||||||
"PrivateTmp=",
|
"PrivateTmp=",
|
||||||
|
|||||||
Reference in New Issue
Block a user