Compare commits

..
Author SHA1 Message Date
DuProcess 630b8dab30 Bump version to 1.0.0-rc18
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-11 11:29:56 -04:00
DuProcess b87f277e2d Use native installer for Windows updates 2026-07-11 11:29:41 -04:00
DuProcess 732f53f2d0 Keep release clippy gate clean
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-10 23:11:16 -04:00
DuProcess 6aa03cca63 Make package artifact checks deterministic
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-10 23:07:50 -04:00
8 changed files with 255 additions and 29 deletions
Generated
+1 -1
View File
@@ -436,7 +436,7 @@ dependencies = [
[[package]]
name = "dosh"
version = "1.0.0-rc17"
version = "1.0.0-rc18"
dependencies = [
"anyhow",
"base64",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "dosh"
version = "1.0.0-rc17"
version = "1.0.0-rc18"
edition = "2024"
license = "MIT"
+3 -1
View File
@@ -36,7 +36,9 @@ persistence-check:
package-check:
$(MAKE) package-release-linux
$(MAKE) package-release-windows
sh scripts/verify-release-artifacts.sh
sh scripts/verify-release-artifacts.sh \
target/dosh-release/dosh-linux-x86_64.tar.gz \
target/dosh-release/dosh-windows-x86_64.zip
install:
sh install.sh --from-current
+1
View File
@@ -44,6 +44,7 @@ dosh setup HOST
dosh HOST
dosh HOST COMMAND
dosh update
dosh update --client|--server|--both
```
Useful commands:
+7 -4
View File
@@ -22,11 +22,14 @@ artifact_version() {
esac
}
if [ "$#" -gt 0 ]; then
artifacts="$*"
else
artifacts="$out_dir/dosh-linux-x86_64.tar.gz $out_dir/dosh-macos-aarch64.tar.gz $out_dir/dosh-windows-x86_64.zip"
fi
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
for artifact in $artifacts; do
if [ ! -f "$artifact" ]; then
echo "missing release artifact: $artifact" >&2
failed=1
+215 -17
View File
@@ -2501,7 +2501,6 @@ fn download_file(
let mut file = if offset > 0 {
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.append(true)
.open(local)
.with_context(|| format!("open {}", local.display()))?;
@@ -2865,6 +2864,18 @@ impl UpdateRole {
UpdateRole::Both => "both",
}
}
fn installer_arg_for_os(self, os: &str) -> Result<&'static str> {
if os == "windows" {
return match self {
UpdateRole::Auto | UpdateRole::Client => Ok("client"),
UpdateRole::Server | UpdateRole::Both => {
Err(anyhow!("Windows installs support client updates only"))
}
};
}
Ok(self.installer_arg())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -2933,6 +2944,18 @@ fn release_artifact_name() -> &'static str {
}
}
fn update_installer_name(os: &str) -> &'static str {
if os == "windows" {
"install.ps1"
} else {
"install.sh"
}
}
fn update_installer_url(raw_base: &str, os: &str) -> String {
format!("{raw_base}/raw/branch/main/{}", update_installer_name(os))
}
fn latest_release_download_url(repo: &str, artifact: &str) -> Option<String> {
let web = repo
.strip_suffix(".git")
@@ -3045,9 +3068,10 @@ fn run_update(config: &dosh::config::ClientConfig, options: UpdateOptions) -> Re
.clone()
.unwrap_or_else(|| "https://git.palav.dev/Palav/dosh.git".to_string());
let raw_base = raw_base_from_repo(&repo);
let installer = format!("{raw_base}/raw/branch/main/install.sh");
let os = std::env::consts::OS;
let installer = update_installer_url(&raw_base, os);
let artifact = release_artifact_name();
let installer_role = options.role.installer_arg();
let installer_role = options.role.installer_arg_for_os(os)?;
if options.check_only {
let local_version = env!("CARGO_PKG_VERSION");
let latest_tag = latest_release_tag(&repo)?;
@@ -3090,13 +3114,78 @@ fn run_update(config: &dosh::config::ClientConfig, options: UpdateOptions) -> Re
.join("dosh")
.join("source");
let use_prebuilt = std::env::var("DOSH_USE_PREBUILT").unwrap_or_else(|_| "1".to_string());
let status = run_update_installer(
config,
&repo,
&installer,
installer_role,
&update_cache.display().to_string(),
&use_prebuilt,
)?;
if !status.success() {
return Err(anyhow!("dosh update failed with status {status}"));
}
Ok(())
}
fn run_update_installer(
config: &dosh::config::ClientConfig,
repo: &str,
installer: &str,
installer_role: &str,
update_cache: &str,
use_prebuilt: &str,
) -> Result<std::process::ExitStatus> {
if cfg!(windows) {
let script = windows_update_script(
config,
repo,
installer,
installer_role,
update_cache,
use_prebuilt,
);
return Command::new("powershell.exe")
.arg("-NoProfile")
.arg("-ExecutionPolicy")
.arg("Bypass")
.arg("-Command")
.arg(script)
.stdin(Stdio::null())
.status()
.context("run dosh update installer");
}
let script = unix_update_script(
config,
repo,
installer,
installer_role,
update_cache,
use_prebuilt,
);
Command::new("sh")
.arg("-c")
.arg(script)
.stdin(Stdio::null())
.status()
.context("run dosh update installer")
}
fn unix_update_script(
config: &dosh::config::ClientConfig,
repo: &str,
installer: &str,
installer_role: &str,
update_cache: &str,
use_prebuilt: &str,
) -> String {
let mut script = format!(
"curl -fsSL {} | DOSH_REPO={} DOSH_PORT={} DOSH_UPDATE_CACHE={} DOSH_UPDATE_QUIET=1 DOSH_USE_PREBUILT={} sh -s -- {}",
shell_word(&installer),
shell_word(&repo),
shell_word(installer),
shell_word(repo),
config.update_port.unwrap_or(config.dosh_port),
shell_word(&update_cache.display().to_string()),
shell_word(&use_prebuilt),
shell_word(update_cache),
shell_word(use_prebuilt),
shell_word(installer_role)
);
if config.server != "user@example.com" {
@@ -3105,16 +3194,47 @@ fn run_update(config: &dosh::config::ClientConfig, options: UpdateOptions) -> Re
if let Some(host) = &config.dosh_host {
script.push_str(&format!(" --dosh-host {}", shell_word(host)));
}
let status = Command::new("sh")
.arg("-c")
.arg(script)
.stdin(Stdio::null())
.status()
.context("run dosh update installer")?;
if !status.success() {
return Err(anyhow!("dosh update failed with status {status}"));
script
}
fn windows_update_script(
config: &dosh::config::ClientConfig,
repo: &str,
installer: &str,
installer_role: &str,
update_cache: &str,
use_prebuilt: &str,
) -> String {
let mut script = String::from("$ErrorActionPreference='Stop';");
script.push_str("$ProgressPreference='SilentlyContinue';");
for (name, value) in [
("DOSH_REPO", repo),
("DOSH_ROLE", installer_role),
(
"DOSH_PORT",
&config.update_port.unwrap_or(config.dosh_port).to_string(),
),
("DOSH_UPDATE_CACHE", update_cache),
("DOSH_UPDATE_QUIET", "1"),
("DOSH_USE_PREBUILT", use_prebuilt),
] {
script.push_str(&format!("$env:{name}={};", powershell_string(value)));
}
Ok(())
if config.server != "user@example.com" {
script.push_str(&format!(
"$env:DOSH_SERVER={};",
powershell_string(&config.server)
));
}
if let Some(host) = &config.dosh_host {
script.push_str(&format!("$env:DOSH_HOST={};", powershell_string(host)));
}
script.push_str(&format!("irm {} | iex", powershell_string(installer)));
script
}
fn powershell_string(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -7655,7 +7775,8 @@ mod tests {
should_repaint_idle_alternate_screen, split_after_command_submit, ssh_destination_host,
ssh_username, ssh_with_user, startup_command, status_ssh_target, strip_stale_mouse_reports,
strip_terminal_focus_reports, terminal_private_mode_transition, toml_bare_key_or_quoted,
update_version_status, upsert_managed_block, valid_forward_host, vscode_safe_alias,
unix_update_script, update_installer_url, update_version_status, upsert_managed_block,
valid_forward_host, vscode_safe_alias, windows_update_script,
};
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
use dosh::native::EnvVar;
@@ -8802,6 +8923,83 @@ mod tests {
assert!(parse_update_options(&["--client".to_string(), "--server".to_string()]).is_err());
}
#[test]
fn update_installer_uses_platform_native_script() {
assert_eq!(
update_installer_url("https://git.palav.dev/Palav/dosh", "linux"),
"https://git.palav.dev/Palav/dosh/raw/branch/main/install.sh"
);
assert_eq!(
update_installer_url("https://git.palav.dev/Palav/dosh", "macos"),
"https://git.palav.dev/Palav/dosh/raw/branch/main/install.sh"
);
assert_eq!(
update_installer_url("https://git.palav.dev/Palav/dosh", "windows"),
"https://git.palav.dev/Palav/dosh/raw/branch/main/install.ps1"
);
}
#[test]
fn windows_update_roles_are_client_only() {
assert_eq!(
UpdateRole::Auto.installer_arg_for_os("windows").unwrap(),
"client"
);
assert_eq!(
UpdateRole::Client.installer_arg_for_os("windows").unwrap(),
"client"
);
assert!(UpdateRole::Server.installer_arg_for_os("windows").is_err());
assert!(UpdateRole::Both.installer_arg_for_os("windows").is_err());
assert_eq!(
UpdateRole::Both.installer_arg_for_os("linux").unwrap(),
"both"
);
}
#[test]
fn update_scripts_are_native_to_the_platform() {
let config = ClientConfig {
server: "palav".to_string(),
dosh_host: Some("dosh.example.com".to_string()),
dosh_port: 50000,
update_port: Some(50001),
..ClientConfig::default()
};
let unix = unix_update_script(
&config,
"https://git.palav.dev/Palav/dosh.git",
"https://git.palav.dev/Palav/dosh/raw/branch/main/install.sh",
"both",
"/tmp/dosh-cache",
"1",
);
assert!(unix.contains("curl -fsSL"));
assert!(unix.contains("install.sh"));
assert!(unix.contains("sh -s -- 'both'"));
assert!(unix.contains("--server 'palav'"));
assert!(unix.contains("--dosh-host 'dosh.example.com'"));
let windows = windows_update_script(
&config,
"https://git.palav.dev/Palav/dosh.git",
"https://git.palav.dev/Palav/dosh/raw/branch/main/install.ps1",
"client",
"C:\\Users\\palav\\AppData\\Local\\dosh\\source",
"1",
);
assert!(
windows.contains(
"irm 'https://git.palav.dev/Palav/dosh/raw/branch/main/install.ps1' | iex"
)
);
assert!(windows.contains("$env:DOSH_ROLE='client';"));
assert!(windows.contains("$env:DOSH_SERVER='palav';"));
assert!(windows.contains("$env:DOSH_HOST='dosh.example.com';"));
assert!(!windows.contains("install.sh"));
assert!(!windows.contains(" sh -s "));
}
#[test]
fn status_uses_host_alias_and_user() {
let host = HostConfig {
+4 -5
View File
@@ -1743,10 +1743,10 @@ async fn handle_detach(state: &Arc<Mutex<ServerState>>, packet: &protocol::Packe
let (key, _) = find_client_decrypt_key(state, &packet.header)?;
let _ = protocol::decrypt_body(packet, &key, CLIENT_TO_SERVER)?;
let mut locked = state.lock().expect("server state poisoned");
if let Some(client) = locked.client_mut(&packet.header.conn_id) {
if !client.replay.accept(packet.header.seq) {
return Ok(());
}
if let Some(client) = locked.client_mut(&packet.header.conn_id)
&& !client.replay.accept(packet.header.seq)
{
return Ok(());
}
locked.remove_client_everywhere(&packet.header.conn_id);
Ok(())
@@ -2681,7 +2681,6 @@ async fn handle_file_request(
let (file, temp_path, written, atomic) = if resume && final_path.exists() {
let mut file = fs::OpenOptions::new()
.read(true)
.write(true)
.append(true)
.open(&final_path)
.with_context(|| format!("open {}", final_path.display()))?;
+23
View File
@@ -19,6 +19,29 @@ fn release_scripts_skip_stale_artifacts_when_auto_discovering() {
assert!(publish.contains("actual=\"$(artifact_version \"$artifact\" || true)\""));
}
#[test]
fn package_check_verifies_only_artifacts_it_builds() {
let makefile = include_str!("../Makefile");
let verify = include_str!("../scripts/verify-release-artifacts.sh");
assert!(
verify.contains("if [ \"$#\" -gt 0 ]; then"),
"release verifier must accept an explicit artifact list"
);
let package_check = makefile
.split("package-check:")
.nth(1)
.and_then(|tail| tail.split("\ninstall:").next())
.expect("package-check target");
assert!(package_check.contains("package-release-linux"));
assert!(package_check.contains("package-release-windows"));
assert!(package_check.contains("dosh-linux-x86_64.tar.gz"));
assert!(package_check.contains("dosh-windows-x86_64.zip"));
assert!(
!package_check.contains("dosh-macos-aarch64.tar.gz"),
"Linux package-check does not build macOS artifacts, so it must not depend on one"
);
}
#[test]
fn systemd_service_does_not_sandbox_remote_shells() {
let service = include_str!("../packaging/systemd/dosh-server.service");