Compare commits

...
5 Commits
Author SHA1 Message Date
DuProcess b393c0e5d5 Allow netlink for terminal network monitors
ci / test (push) Has been cancelled
ci / fuzz-smoke (push) Has been cancelled
ci / windows-client (push) Has been cancelled
ci / package-release (linux-x86_64, ubuntu-latest) (push) Has been cancelled
ci / package-release (macos-aarch64, macos-14) (push) Has been cancelled
ci / package-release (macos-x86_64, macos-13) (push) Has been cancelled
ci / package-release (windows-x86_64, windows-latest) (push) Has been cancelled
ci / remote-bench (push) Has been cancelled
2026-07-02 19:40:30 -04:00
DuProcess d5bc8e3c64 Recover terminal output gaps with resume repaint
ci / test (push) Has been cancelled
ci / fuzz-smoke (push) Has been cancelled
ci / windows-client (push) Has been cancelled
ci / package-release (linux-x86_64, ubuntu-latest) (push) Has been cancelled
ci / package-release (macos-aarch64, macos-14) (push) Has been cancelled
ci / package-release (macos-x86_64, macos-13) (push) Has been cancelled
ci / package-release (windows-x86_64, windows-latest) (push) Has been cancelled
ci / remote-bench (push) Has been cancelled
2026-07-02 19:29:20 -04:00
DuProcess c40f5459ba Keep live TUI output raw under retransmit pressure
ci / test (push) Has been cancelled
ci / fuzz-smoke (push) Has been cancelled
ci / windows-client (push) Has been cancelled
ci / package-release (linux-x86_64, ubuntu-latest) (push) Has been cancelled
ci / package-release (macos-aarch64, macos-14) (push) Has been cancelled
ci / package-release (macos-x86_64, macos-13) (push) Has been cancelled
ci / package-release (windows-x86_64, windows-latest) (push) Has been cancelled
ci / remote-bench (push) Has been cancelled
2026-07-02 19:21:43 -04:00
DuProcess 601510687e Prepare Dosh 0.1.9 release
ci / test (push) Has been cancelled
ci / fuzz-smoke (push) Has been cancelled
ci / windows-client (push) Has been cancelled
ci / package-release (linux-x86_64, ubuntu-latest) (push) Has been cancelled
ci / package-release (macos-aarch64, macos-14) (push) Has been cancelled
ci / package-release (macos-x86_64, macos-13) (push) Has been cancelled
ci / package-release (windows-x86_64, windows-latest) (push) Has been cancelled
ci / remote-bench (push) Has been cancelled
2026-07-02 19:07:46 -04:00
DuProcess 60387e9222 Install Dosh binaries atomically
ci / test (push) Has been cancelled
ci / fuzz-smoke (push) Has been cancelled
ci / windows-client (push) Has been cancelled
ci / package-release (linux-x86_64, ubuntu-latest) (push) Has been cancelled
ci / package-release (macos-aarch64, macos-14) (push) Has been cancelled
ci / package-release (macos-x86_64, macos-13) (push) Has been cancelled
ci / package-release (windows-x86_64, windows-latest) (push) Has been cancelled
ci / remote-bench (push) Has been cancelled
2026-07-02 18:41:02 -04:00
10 changed files with 223 additions and 34 deletions
Generated
+1 -1
View File
@@ -436,7 +436,7 @@ dependencies = [
[[package]] [[package]]
name = "dosh" name = "dosh"
version = "0.1.8" version = "0.1.12"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "dosh" name = "dosh"
version = "0.1.8" version = "0.1.12"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
+19 -5
View File
@@ -141,6 +141,20 @@ function Verify-ArchiveVersion($ExtractDir, $Url) {
} }
} }
function Install-Binary($Source, $Destination) {
$dir = Split-Path -Parent $Destination
$name = Split-Path -Leaf $Destination
$tmp = Join-Path $dir ".$name.tmp.$PID"
Copy-Item $Source $tmp -Force
try {
Move-Item $tmp $Destination -Force
}
catch {
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
throw
}
}
$bindir = Join-Path $Prefix "bin" $bindir = Join-Path $Prefix "bin"
$configDir = Join-Path $HOME ".config\dosh" $configDir = Join-Path $HOME ".config\dosh"
New-Item -ItemType Directory -Force -Path $bindir, $configDir | Out-Null New-Item -ItemType Directory -Force -Path $bindir, $configDir | Out-Null
@@ -168,9 +182,9 @@ function Install-Prebuilt {
if (-not $found) { if (-not $found) {
throw "prebuilt archive missing $bin" throw "prebuilt archive missing $bin"
} }
Copy-Item $found.FullName (Join-Path $bindir $bin) -Force Install-Binary $found.FullName (Join-Path $bindir $bin)
} }
Copy-Item (Join-Path $bindir "dosh-client.exe") (Join-Path $bindir "dosh.exe") -Force Install-Binary (Join-Path $bindir "dosh-client.exe") (Join-Path $bindir "dosh.exe")
return $true return $true
} }
catch { catch {
@@ -202,9 +216,9 @@ function Install-FromSource {
try { try {
Push-Location $src Push-Location $src
cargo build --release --bin dosh-client --bin dosh-bench cargo build --release --bin dosh-client --bin dosh-bench
Copy-Item "target\release\dosh-client.exe" (Join-Path $bindir "dosh-client.exe") -Force Install-Binary "target\release\dosh-client.exe" (Join-Path $bindir "dosh-client.exe")
Copy-Item "target\release\dosh-client.exe" (Join-Path $bindir "dosh.exe") -Force Install-Binary "target\release\dosh-client.exe" (Join-Path $bindir "dosh.exe")
Copy-Item "target\release\dosh-bench.exe" (Join-Path $bindir "dosh-bench.exe") -Force Install-Binary "target\release\dosh-bench.exe" (Join-Path $bindir "dosh-bench.exe")
} }
finally { finally {
Pop-Location Pop-Location
+20 -7
View File
@@ -226,13 +226,26 @@ find_extracted_binary() {
find "$1" -type f -name "$2" 2>/dev/null | sed -n '1p' find "$1" -type f -name "$2" 2>/dev/null | sed -n '1p'
} }
install_binary() {
src="$1"
dst="$2"
dst_dir="$(dirname "$dst")"
dst_base="$(basename "$dst")"
tmp="$dst_dir/.$dst_base.tmp.$$"
install -m 0755 "$src" "$tmp"
if ! mv -f "$tmp" "$dst"; then
rm -f "$tmp"
return 1
fi
}
install_extracted_binary() { install_extracted_binary() {
found="$(find_extracted_binary "$1" "$2")" found="$(find_extracted_binary "$1" "$2")"
if [ -z "$found" ]; then if [ -z "$found" ]; then
echo "prebuilt archive missing $2" >&2 echo "prebuilt archive missing $2" >&2
return 1 return 1
fi fi
install -m 0755 "$found" "$3" install_binary "$found" "$3"
} }
sha256_file() { sha256_file() {
@@ -329,7 +342,7 @@ try_install_prebuilt() {
install_extracted_binary "$tmpdir/extract" dosh-auth "$bindir/dosh-auth" || return 1 install_extracted_binary "$tmpdir/extract" dosh-auth "$bindir/dosh-auth" || return 1
fi fi
if found_bench="$(find_extracted_binary "$tmpdir/extract" dosh-bench)" && [ -n "$found_bench" ]; then if found_bench="$(find_extracted_binary "$tmpdir/extract" dosh-bench)" && [ -n "$found_bench" ]; then
install -m 0755 "$found_bench" "$bindir/dosh-bench" install_binary "$found_bench" "$bindir/dosh-bench"
fi fi
return 0 return 0
} }
@@ -388,14 +401,14 @@ install_from_source() {
fi fi
fi fi
install -m 0755 target/release/dosh-client "$bindir/dosh-client" install_binary target/release/dosh-client "$bindir/dosh-client"
ln -sf dosh-client "$bindir/dosh" ln -sf dosh-client "$bindir/dosh"
if [ "$role" = "server" ] || [ "$role" = "both" ]; then if [ "$role" = "server" ] || [ "$role" = "both" ]; then
install -m 0755 target/release/dosh-server "$bindir/dosh-server" install_binary target/release/dosh-server "$bindir/dosh-server"
install -m 0755 target/release/dosh-auth "$bindir/dosh-auth" install_binary target/release/dosh-auth "$bindir/dosh-auth"
fi fi
if [ -f target/release/dosh-bench ]; then if [ -f target/release/dosh-bench ]; then
install -m 0755 target/release/dosh-bench "$bindir/dosh-bench" install_binary target/release/dosh-bench "$bindir/dosh-bench"
fi fi
} }
@@ -421,7 +434,7 @@ PrivateTmp=true
ProtectSystem=strict ProtectSystem=strict
ProtectHome=read-only ProtectHome=read-only
ReadWritePaths=$config_dir $data_dir ReadWritePaths=$config_dir $data_dir
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
RestrictRealtime=true RestrictRealtime=true
LockPersonality=true LockPersonality=true
MemoryDenyWriteExecute=true MemoryDenyWriteExecute=true
+1 -1
View File
@@ -14,7 +14,7 @@ PrivateTmp=true
ProtectSystem=strict ProtectSystem=strict
ProtectHome=read-only ProtectHome=read-only
ReadWritePaths=%h/.config/dosh %h/.local/share/dosh ReadWritePaths=%h/.config/dosh %h/.local/share/dosh
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
RestrictRealtime=true RestrictRealtime=true
LockPersonality=true LockPersonality=true
MemoryDenyWriteExecute=true MemoryDenyWriteExecute=true
+1
View File
@@ -9,6 +9,7 @@ for test_name in \
tui_control_sequences_survive_transport_verbatim \ tui_control_sequences_survive_transport_verbatim \
unicode_output_survives_transport \ unicode_output_survives_transport \
tui_graph_glyphs_survive_fast_repaints_without_snapshot_substitution \ tui_graph_glyphs_survive_fast_repaints_without_snapshot_substitution \
btop_style_graph_output_stays_raw_when_retransmit_history_overflows \
large_tui_paint_is_delivered_in_mtu_safe_frames \ large_tui_paint_is_delivered_in_mtu_safe_frames \
resume_snapshot_preserves_alternate_screen_mode resume_snapshot_preserves_alternate_screen_mode
do do
+80 -4
View File
@@ -4271,6 +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 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
@@ -4498,6 +4499,36 @@ async fn run_terminal(
_ = resize_tick.tick() => { _ = resize_tick.tick() => {
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() => {
if frame_buffer.gap_wait_elapsed(Duration::from_millis(FRAME_GAP_RESYNC_AFTER_MS))
&& let Some(frame) = reconnect(
&socket,
&mut cred,
&mut send_seq,
last_size,
&mut frame_buffer,
&mut predictor,
)
.await?
{
refresh_live_addr(&mut addr, &cred)?;
if !forward_only {
render_frame(&frame)?;
predictor.observe_output(&frame.bytes);
}
last_packet_at = Instant::now();
flush_pending_user_input(
&socket,
addr,
&cred,
&mut send_seq,
&mut predictor,
&mut pending_user_input,
&mut pending_user_input_bytes,
)
.await?;
}
}
recv = socket.recv_from(&mut recv_buf) => { recv = socket.recv_from(&mut recv_buf) => {
let (n, _) = recv?; let (n, _) = recv?;
let Ok(packet) = protocol::decode(&recv_buf[..n]) else { let Ok(packet) = protocol::decode(&recv_buf[..n]) else {
@@ -6561,14 +6592,18 @@ fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
.any(|window| window == needle) .any(|window| window == needle)
} }
const FRAME_GAP_RESYNC_AFTER_MS: u64 = 500;
#[derive(Default)] #[derive(Default)]
struct FrameBuffer { struct FrameBuffer {
pending: BTreeMap<u64, Frame>, pending: BTreeMap<u64, Frame>,
gap_since: Option<Instant>,
} }
impl FrameBuffer { impl FrameBuffer {
fn clear(&mut self) { fn clear(&mut self) {
self.pending.clear(); self.pending.clear();
self.gap_since = 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> {
@@ -6577,6 +6612,7 @@ impl FrameBuffer {
return Vec::new(); return Vec::new();
} }
self.pending.clear(); self.pending.clear();
self.gap_since = None;
*last_rendered_seq = frame.output_seq; *last_rendered_seq = frame.output_seq;
return vec![frame]; return vec![frame];
} }
@@ -6590,8 +6626,30 @@ impl FrameBuffer {
*last_rendered_seq = frame.output_seq; *last_rendered_seq = frame.output_seq;
ready.push(frame); ready.push(frame);
} }
self.refresh_gap(*last_rendered_seq);
ready ready
} }
fn refresh_gap(&mut self, last_rendered_seq: u64) {
let waiting_on_missing = self
.pending
.first_key_value()
.is_some_and(|(&seq, _)| seq > last_rendered_seq.saturating_add(1));
if waiting_on_missing {
self.gap_since.get_or_insert_with(Instant::now);
} else {
self.gap_since = None;
}
}
fn gap_wait_elapsed(&self, wait: Duration) -> bool {
self.gap_since.is_some_and(|since| since.elapsed() >= wait)
}
#[cfg(test)]
fn backdate_gap_for_test(&mut self, elapsed: Duration) {
self.gap_since = Some(Instant::now() - elapsed);
}
} }
/// Read the current terminal size and, if it changed since `last_size`, send a /// Read the current terminal size and, if it changed since `last_size`, send a
@@ -7003,10 +7061,10 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
CachedCredential, DisconnectStatus, DynamicForward, FrameBuffer, LocalForward, CachedCredential, DisconnectStatus, DynamicForward, FRAME_GAP_RESYNC_AFTER_MS, FrameBuffer,
MAX_PENDING_USER_INPUT_BYTES, POST_SUBMIT_ALL_INPUT_HOLD, PendingStreamOpen, PredictMode, LocalForward, MAX_PENDING_USER_INPUT_BYTES, POST_SUBMIT_ALL_INPUT_HOLD, PendingStreamOpen,
Predictor, RESTART_STATUS_SCRIPT, RemoteForward, STARTUP_INPUT_HOLD, SshConfig, PredictMode, Predictor, RESTART_STATUS_SCRIPT, RemoteForward, STARTUP_INPUT_HOLD,
StartupGateMode, StatusAction, auth_allows, cache_key, cache_server_prefix, SshConfig, StartupGateMode, StatusAction, auth_allows, cache_key, cache_server_prefix,
clear_cached_credentials, ensure_tui_safe_status_overlay, input_matches_escape, clear_cached_credentials, ensure_tui_safe_status_overlay, input_matches_escape,
is_local_status_target, is_resume_response_for_client, latest_release_download_url, is_local_status_target, is_resume_response_for_client, latest_release_download_url,
load_first_native_identity_with_prompt, parse_dynamic_forward, parse_escape_key, load_first_native_identity_with_prompt, parse_dynamic_forward, parse_escape_key,
@@ -7425,12 +7483,14 @@ 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]
@@ -7449,14 +7509,30 @@ 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]
fn frame_buffer_marks_gap_ready_for_resync_after_wait() {
let mut buffer = FrameBuffer::default();
let mut last = 10;
assert!(buffer.accept(test_frame(13, false), &mut last).is_empty());
assert_eq!(last, 10);
assert!(!buffer.gap_wait_elapsed(Duration::from_secs(60)));
buffer.backdate_gap_for_test(Duration::from_secs(1));
assert!(buffer.gap_wait_elapsed(Duration::from_millis(FRAME_GAP_RESYNC_AFTER_MS)));
}
#[test] #[test]
fn frame_buffer_ignores_stale_snapshot() { fn frame_buffer_ignores_stale_snapshot() {
let mut buffer = FrameBuffer::default(); let mut buffer = FrameBuffer::default();
+10 -15
View File
@@ -3336,7 +3336,7 @@ async fn broadcast_output(
let sends = { let sends = {
let mut locked = state.lock().expect("server state poisoned"); let mut locked = state.lock().expect("server state poisoned");
let scrollback = locked.config.scrollback; let scrollback = locked.config.scrollback;
let retransmit_window = locked.config.retransmit_window; let retransmit_window = locked.config.retransmit_window.max(1);
let session = locked let session = locked
.sessions .sessions
.get_mut(&output.session) .get_mut(&output.session)
@@ -3374,20 +3374,18 @@ async fn broadcast_output(
client.send_seq += 1; client.send_seq += 1;
// Count traffic toward the packet-count rekey trigger (spec §11). // Count traffic toward the packet-count rekey trigger (spec §11).
client.epoch_packets = client.epoch_packets.saturating_add(1); client.epoch_packets = client.epoch_packets.saturating_add(1);
// Only materialize a screen snapshot when this client has fallen too // Live terminal bytes are never rewritten into vt100 snapshots. A
// far behind; the common case sends the raw output bytes and must not // snapshot is useful for attach/resume, but substituting it during a
// clone the whole vt100 grid per client per packet. // running TUI can lose graph/background-cell details that apps like
let (bytes, snapshot) = if client.pending.len() >= retransmit_window { // btop rely on. If retransmit history is full, prune only history.
client.pending.clear(); while client.pending.len() >= retransmit_window {
(screen_snapshot(session.parser.screen()), true) client.pending.pop_front();
} else { }
(output.bytes.clone(), false)
};
let frame = Frame { let frame = Frame {
session: output.session.clone(), session: output.session.clone(),
output_seq, output_seq,
bytes, bytes: output.bytes.clone(),
snapshot, snapshot: false,
closed: false, closed: false,
}; };
let body = protocol::to_body(&frame)?; let body = protocol::to_body(&frame)?;
@@ -3400,9 +3398,6 @@ async fn broadcast_output(
SERVER_TO_CLIENT, SERVER_TO_CLIENT,
&body, &body,
)?; )?;
while client.pending.len() >= retransmit_window {
client.pending.pop_front();
}
client.pending.push_back(PendingFrame { client.pending.push_back(PendingFrame {
output_seq, output_seq,
packet: packet.clone(), packet: packet.clone(),
+78
View File
@@ -112,6 +112,21 @@ fn write_server_config_with_output_interval(
config config
} }
fn write_server_config_with_retransmit_window(
dir: &tempfile::TempDir,
port: u16,
retransmit_window: u64,
) -> std::path::PathBuf {
let config = write_server_config(dir, port);
let mut raw = fs::read_to_string(&config).unwrap();
raw = raw.replace(
"retransmit_window = 256\n",
&format!("retransmit_window = {retransmit_window}\n"),
);
fs::write(&config, raw).unwrap();
config
}
fn start_server(dir: &tempfile::TempDir, config: &std::path::Path) -> Child { fn start_server(dir: &tempfile::TempDir, config: &std::path::Path) -> Child {
let server = env!("CARGO_BIN_EXE_dosh-server"); let server = env!("CARGO_BIN_EXE_dosh-server");
let child = Command::new(server) let child = Command::new(server)
@@ -1694,6 +1709,69 @@ fn tui_graph_glyphs_survive_fast_repaints_without_snapshot_substitution() {
); );
} }
#[test]
fn btop_style_graph_output_stays_raw_when_retransmit_history_overflows() {
let dir = tempfile::tempdir().unwrap();
let port = free_udp_port();
let config = write_server_config_with_retransmit_window(&dir, port, 3);
let mut server = start_server(&dir, &config);
let (socket, bootstrap, ok) = direct_attach(&config, port, "read-write");
let graph = "DOSH_BTOP_NET ⠀⠁⠃⠇⡇⣇⣧⣷⣿";
let input = Input {
bytes: format!(
"stty -echo; \
printf '\\033[?1049h\\033[?2026h\\033[?25l'; \
for i in 1 2 3 4 5 6 7 8 9 10; do \
printf '\\033[6;4H\\033[38;2;80;220;140m{} %s\\033[0m' \"$i\"; \
done; \
printf '\\033[?25h\\033[?2026l\\033[?1049l'\n",
graph
)
.into_bytes(),
};
send_encrypted(
&socket,
port,
PacketKind::Input,
ok.client_id,
2,
0,
&bootstrap.session_key,
&protocol::to_body(&input).unwrap(),
);
let mut text = String::new();
let mut snapshots = 0usize;
let deadline = std::time::Instant::now() + Duration::from_secs(3);
while std::time::Instant::now() < deadline
&& !(text.matches("DOSH_BTOP_NET").count() >= 10 && text.contains("\x1b[?2026l"))
{
if let Some((_header, frame)) = recv_frame(&socket, &bootstrap.session_key) {
if frame.snapshot {
snapshots += 1;
}
text.push_str(&String::from_utf8_lossy(&frame.bytes));
}
}
let _ = server.kill();
let _ = server.wait();
assert_eq!(
snapshots, 0,
"live btop-style graph output must not be replaced by snapshots; got {snapshots}"
);
assert!(
text.matches("DOSH_BTOP_NET").count() >= 10
&& text.contains("")
&& text.contains("\x1b[38;2;80;220;140m")
&& text.contains("\x1b[?2026h")
&& text.contains("\x1b[?2026l"),
"expected raw btop-style graph output to survive under retransmit pressure, got {text:?}"
);
}
#[test] #[test]
fn large_tui_paint_is_delivered_in_mtu_safe_frames() { fn large_tui_paint_is_delivered_in_mtu_safe_frames() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
+12
View File
@@ -18,3 +18,15 @@ fn release_scripts_skip_stale_artifacts_when_auto_discovering() {
assert!(publish.contains("skipping stale artifact")); assert!(publish.contains("skipping stale artifact"));
assert!(publish.contains("actual=\"$(artifact_version \"$artifact\" || true)\"")); assert!(publish.contains("actual=\"$(artifact_version \"$artifact\" || true)\""));
} }
#[test]
fn systemd_sandbox_allows_netlink_for_terminal_tools() {
let service = include_str!("../packaging/systemd/dosh-server.service");
let install = include_str!("../install.sh");
for raw in [service, install] {
assert!(
raw.contains("RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK"),
"dosh sessions must allow AF_NETLINK so tools like btop can enumerate network interfaces"
);
}
}