Compare commits

...

4 Commits

Author SHA1 Message Date
DuProcess 35d9c9c6de Bump version to 0.1.6
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-06-28 09:31:50 -04:00
DuProcess 51247576f3 Harden server install restart path 2026-06-28 09:31:06 -04:00
DuProcess 255f584545 Add release check gate 2026-06-28 09:27:34 -04:00
DuProcess d947ccd934 Clarify update checks for releases 2026-06-28 09:24:20 -04:00
6 changed files with 142 additions and 23 deletions
Generated
+1 -1
View File
@@ -436,7 +436,7 @@ dependencies = [
[[package]] [[package]]
name = "dosh" name = "dosh"
version = "0.1.5" version = "0.1.6"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "dosh" name = "dosh"
version = "0.1.5" version = "0.1.6"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
+11 -1
View File
@@ -1,4 +1,4 @@
.PHONY: build test fmt 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 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
@@ -9,6 +9,16 @@ test:
fmt: fmt:
cargo fmt cargo fmt
clippy:
cargo clippy --all-targets -- -D warnings
release-check:
cargo fmt --check
sh -n install.sh scripts/*.sh
cargo clippy --all-targets -- -D warnings
cargo test
$(MAKE) tui-harness
install: install:
sh install.sh --from-current sh install.sh --from-current
+5 -5
View File
@@ -54,11 +54,11 @@ dosh recover HOST # clear cached attach state and re-check
Examples: Examples:
```sh ```sh
dosh palav dosh server
dosh palav tm dosh server tm
dosh forward palav -L 8080:127.0.0.1:80 dosh forward server -L 8080:127.0.0.1:80
dosh forward palav -D 1080 dosh forward server -D 1080
dosh forward palav -R 2222:127.0.0.1:22 dosh forward server -R 2222:127.0.0.1:22
``` ```
Agent forwarding is opt-in with `-A` and must be enabled on the server. Agent forwarding is opt-in with `-A` and must be enabled on the server.
+14 -4
View File
@@ -410,7 +410,7 @@ write_systemd_service() {
else else
cat >"$systemd_user_dir/dosh-server.service" <<EOF cat >"$systemd_user_dir/dosh-server.service" <<EOF
[Unit] [Unit]
Description=Dosh server Description=dosh dormant shell server
After=network-online.target After=network-online.target
Wants=network-online.target Wants=network-online.target
@@ -420,6 +420,15 @@ ExecStart=$bindir/dosh-server serve
Restart=on-failure Restart=on-failure
RestartSec=1 RestartSec=1
KillMode=process KillMode=process
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=$config_dir $data_dir
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictRealtime=true
LockPersonality=true
MemoryDenyWriteExecute=true
[Install] [Install]
WantedBy=default.target WantedBy=default.target
@@ -479,7 +488,8 @@ EOF
mkdir -p "$systemd_user_dir" mkdir -p "$systemd_user_dir"
write_systemd_service write_systemd_service
if [ "$start_server" -eq 1 ] && systemctl --user daemon-reload >/dev/null 2>&1; then if [ "$start_server" -eq 1 ] && systemctl --user daemon-reload >/dev/null 2>&1; then
systemctl --user enable --now dosh-server.service systemctl --user enable dosh-server.service
systemctl --user restart dosh-server.service
fi fi
elif [ "$start_server" -eq 1 ]; then elif [ "$start_server" -eq 1 ]; then
nohup "$bindir/dosh-server" serve >"$data_dir/dosh-server.log" 2>&1 & nohup "$bindir/dosh-server" serve >"$data_dir/dosh-server.log" 2>&1 &
@@ -547,8 +557,8 @@ EOF
fi fi
cat >"$hosts_config" <<EOF cat >"$hosts_config" <<EOF
# Example: # Example:
# [homelab] # [server]
# ssh = "homelab" # ssh = "server"
# dosh_host = "server.example.com" # dosh_host = "server.example.com"
# port = 50000 # port = 50000
# default_command = "tm" # default_command = "tm"
+110 -11
View File
@@ -1503,7 +1503,54 @@ fn latest_release_download_url(repo: &str, artifact: &str) -> Option<String> {
Some(format!("{web}/releases/latest/download/{artifact}")) Some(format!("{web}/releases/latest/download/{artifact}"))
} }
fn latest_release_tag_download_url(repo: &str, artifact: &str) -> Result<Option<String>> { fn release_tag_from_effective_url(web: &str, effective: &str) -> Option<String> {
let prefix = format!("{web}/releases/tag/");
let tag = effective.strip_prefix(&prefix)?;
if tag.is_empty() {
None
} else {
Some(tag.to_string())
}
}
fn release_version_from_tag(tag: &str) -> &str {
tag.strip_prefix('v').unwrap_or(tag)
}
fn update_version_status(local: &str, latest: &str) -> &'static str {
match compare_dotted_versions(latest, local) {
std::cmp::Ordering::Equal => "current",
std::cmp::Ordering::Greater => "update available",
std::cmp::Ordering::Less => "different",
}
}
fn compare_dotted_versions(left: &str, right: &str) -> std::cmp::Ordering {
let left = parse_dotted_version(left);
let right = parse_dotted_version(right);
let width = left.len().max(right.len());
for index in 0..width {
let ordering = left
.get(index)
.copied()
.unwrap_or(0)
.cmp(&right.get(index).copied().unwrap_or(0));
if ordering != std::cmp::Ordering::Equal {
return ordering;
}
}
std::cmp::Ordering::Equal
}
fn parse_dotted_version(version: &str) -> Vec<u64> {
version
.split(|byte: char| !byte.is_ascii_digit())
.filter(|part| !part.is_empty())
.map(|part| part.parse::<u64>().unwrap_or(0))
.collect()
}
fn latest_release_tag(repo: &str) -> Result<Option<String>> {
let web = repo let web = repo
.strip_suffix(".git") .strip_suffix(".git")
.unwrap_or(repo) .unwrap_or(repo)
@@ -1525,14 +1572,18 @@ fn latest_release_tag_download_url(repo: &str, artifact: &str) -> Result<Option<
return Ok(None); return Ok(None);
} }
let effective = String::from_utf8_lossy(&output.stdout); let effective = String::from_utf8_lossy(&output.stdout);
let prefix = format!("{web}/releases/tag/"); Ok(release_tag_from_effective_url(web, &effective))
let Some(tag) = effective.strip_prefix(&prefix) else { }
return Ok(None);
}; fn release_tag_download_url(repo: &str, tag: &str, artifact: &str) -> Option<String> {
if tag.is_empty() { let web = repo
return Ok(None); .strip_suffix(".git")
.unwrap_or(repo)
.trim_end_matches('/');
if !web.starts_with("http://") && !web.starts_with("https://") {
return None;
} }
Ok(Some(format!("{web}/releases/download/{tag}/{artifact}"))) Some(format!("{web}/releases/download/{tag}/{artifact}"))
} }
fn url_reachable(url: &str) -> Result<bool> { fn url_reachable(url: &str) -> Result<bool> {
@@ -1556,13 +1607,27 @@ fn run_update(config: &dosh::config::ClientConfig, check_only: bool) -> Result<(
let installer = format!("{raw_base}/raw/branch/main/install.sh"); let installer = format!("{raw_base}/raw/branch/main/install.sh");
let artifact = release_artifact_name(); let artifact = release_artifact_name();
if check_only { if check_only {
println!("dosh {}", env!("CARGO_PKG_VERSION")); let local_version = env!("CARGO_PKG_VERSION");
let latest_tag = latest_release_tag(&repo)?;
println!("dosh {local_version}");
println!("repo: {repo}"); println!("repo: {repo}");
println!("installer: {installer}"); println!("installer: {installer}");
match latest_tag.as_deref() {
Some(tag) => {
let latest_version = release_version_from_tag(tag);
println!(
"latest: {latest_version} ({})",
update_version_status(local_version, latest_version)
);
}
None => println!("latest: unknown"),
}
if let Some(latest_url) = latest_release_download_url(&repo, artifact) { if let Some(latest_url) = latest_release_download_url(&repo, artifact) {
let mut status = "missing"; let mut status = "missing";
let mut display_url = latest_url.clone(); let mut display_url = latest_url.clone();
if let Some(tag_url) = latest_release_tag_download_url(&repo, artifact)? if let Some(tag_url) = latest_tag
.as_deref()
.and_then(|tag| release_tag_download_url(&repo, tag, artifact))
&& url_reachable(&tag_url)? && url_reachable(&tag_url)?
{ {
status = "available"; status = "available";
@@ -5557,12 +5622,13 @@ mod tests {
load_first_native_identity_with_prompt, parse_dynamic_forward, parse_escape_key, load_first_native_identity_with_prompt, parse_dynamic_forward, parse_escape_key,
parse_local_forward, parse_remote_forward, parse_ssh_config, post_submit_hold_duration, parse_local_forward, parse_remote_forward, parse_ssh_config, post_submit_hold_duration,
queue_pending_user_input, raw_contains_host_table, recv_response_until, refresh_live_addr, queue_pending_user_input, raw_contains_host_table, recv_response_until, refresh_live_addr,
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_hold_during_startup_gate, should_hold_post_submit_input,
split_after_command_submit, ssh_destination_host, ssh_username, ssh_with_user, split_after_command_submit, ssh_destination_host, ssh_username, ssh_with_user,
startup_command, status_ssh_target, strip_stale_mouse_reports, toml_bare_key_or_quoted, startup_command, status_ssh_target, strip_stale_mouse_reports, toml_bare_key_or_quoted,
update_check_requested, valid_forward_host, update_check_requested, update_version_status, valid_forward_host,
}; };
use dosh::config::{ClientConfig, CommandExtension, HostConfig}; use dosh::config::{ClientConfig, CommandExtension, HostConfig};
use dosh::native::EnvVar; use dosh::native::EnvVar;
@@ -6435,6 +6501,39 @@ mod tests {
); );
} }
#[test]
fn release_tag_parses_effective_latest_url() {
assert_eq!(
release_tag_from_effective_url(
"https://example.com/owner/dosh",
"https://example.com/owner/dosh/releases/tag/v1.0.0"
)
.as_deref(),
Some("v1.0.0")
);
assert_eq!(release_version_from_tag("v1.0.0"), "1.0.0");
assert_eq!(release_version_from_tag("2026.06.28"), "2026.06.28");
assert_eq!(
release_tag_download_url(
"https://example.com/owner/dosh.git",
"v1.0.0",
"dosh-linux-x86_64.tar.gz"
)
.as_deref(),
Some(
"https://example.com/owner/dosh/releases/download/v1.0.0/dosh-linux-x86_64.tar.gz"
)
);
assert!(release_tag_from_effective_url("https://example.com/owner/dosh", "").is_none());
}
#[test]
fn update_version_status_compares_numeric_parts() {
assert_eq!(update_version_status("0.1.5", "0.1.5"), "current");
assert_eq!(update_version_status("0.1.9", "0.1.10"), "update available");
assert_eq!(update_version_status("1.0.0", "0.9.9"), "different");
}
#[test] #[test]
fn resolved_startup_command_expands_global_extension() { fn resolved_startup_command_expands_global_extension() {
let mut config = ClientConfig::default(); let mut config = ClientConfig::default();