client: recover relay launch after binary replacement

This commit is contained in:
Brad Stein 2026-05-15 08:03:56 -03:00
parent 80062c1b4b
commit b2e7a4cb38
6 changed files with 157 additions and 9 deletions

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lesavka_client"
version = "0.22.35"
version = "0.22.36"
dependencies = [
"anyhow",
"async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]]
name = "lesavka_common"
version = "0.22.35"
version = "0.22.36"
dependencies = [
"anyhow",
"base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]]
name = "lesavka_server"
version = "0.22.35"
version = "0.22.36"
dependencies = [
"anyhow",
"base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package]
name = "lesavka_client"
version = "0.22.35"
version = "0.22.36"
edition = "2024"
[dependencies]

View File

@ -15,6 +15,9 @@ use serial_test::serial;
use std::{
cell::RefCell,
collections::BTreeMap,
fs,
path::PathBuf,
process,
rc::Rc,
time::{Duration, Instant},
};
@ -35,6 +38,93 @@ fn rail_button_text(button: &gtk::Button) -> Option<String> {
.map(|label| label.text().to_string())
}
fn unique_test_path(label: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock before unix epoch")
.as_nanos();
std::env::temp_dir().join(format!(
"lesavka-relay-child-{label}-{}-{nanos}",
process::id()
))
}
fn unique_test_file(label: &str) -> PathBuf {
let path = unique_test_path(label);
fs::write(&path, b"test executable placeholder").expect("write relay child test file");
path
}
fn unique_deleted_exe_path(label: &str) -> PathBuf {
let path = unique_test_path(label);
let deleted_path = path.with_file_name(format!(
"{} (deleted)",
path.file_name()
.expect("test path has file name")
.to_string_lossy()
));
fs::write(&deleted_path, b"test executable placeholder")
.expect("write deleted relay child test file");
deleted_path
}
#[test]
fn relay_child_exe_uses_current_exe_when_still_runnable() {
let current = unique_test_file("current");
let installed = unique_test_file("installed");
let resolved = resolve_relay_child_exe_from(None, &current, &installed)
.expect("resolve relay child executable");
assert_eq!(resolved, current);
}
#[test]
fn relay_child_exe_falls_back_when_launcher_exe_was_replaced() {
let current = unique_deleted_exe_path("current");
let installed = unique_test_file("installed");
let resolved = resolve_relay_child_exe_from(None, &current, &installed)
.expect("resolve relay child executable");
assert_eq!(resolved, installed);
}
#[test]
fn relay_child_exe_falls_back_when_current_exe_path_is_missing() {
let current = unique_test_path("missing-current");
let installed = unique_test_file("installed");
let resolved = resolve_relay_child_exe_from(None, &current, &installed)
.expect("resolve relay child executable");
assert_eq!(resolved, installed);
}
#[test]
fn relay_child_exe_honors_valid_env_override() {
let override_path = unique_test_file("override");
let current = unique_test_file("current");
let installed = unique_test_file("installed");
let resolved = resolve_relay_child_exe_from(Some(&override_path), &current, &installed)
.expect("resolve relay child executable");
assert_eq!(resolved, override_path);
}
#[test]
fn relay_child_exe_rejects_invalid_env_override() {
let override_path = unique_test_path("missing-override");
let current = unique_test_file("current");
let installed = unique_test_file("installed");
let err = resolve_relay_child_exe_from(Some(&override_path), &current, &installed)
.expect_err("invalid override should fail loudly");
assert!(err.to_string().contains(CLIENT_BIN_ENV));
}
#[test]
fn local_test_detail_mentions_idle_and_running_modes() {
assert!(local_test_detail(false, false, false, false).contains("idle"));

View File

@ -1,3 +1,6 @@
const CLIENT_BIN_ENV: &str = "LESAVKA_CLIENT_BIN";
const INSTALLED_CLIENT_BIN: &str = "/usr/local/bin/lesavka-client";
pub fn spawn_client_process(
server_addr: &str,
state: &LauncherState,
@ -6,8 +9,8 @@ pub fn spawn_client_process(
input_state_path: &Path,
input_toggle_control_path: &Path,
) -> Result<RelayChild> {
let exe = std::env::current_exe()?;
let mut command = Command::new(exe);
let exe = resolve_relay_child_exe()?;
let mut command = Command::new(&exe);
command.arg("--no-launcher");
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
@ -54,7 +57,62 @@ pub fn spawn_client_process(
for (key, value) in runtime_env_vars(state) {
command.env(key, value);
}
Ok(command.spawn()?)
let child = command.spawn().map_err(|err| {
anyhow::anyhow!("starting relay child {} failed: {err}", exe.display())
})?;
Ok(child)
}
fn resolve_relay_child_exe() -> Result<PathBuf> {
let env_override = std::env::var_os(CLIENT_BIN_ENV).map(PathBuf::from);
let current_exe = std::env::current_exe().map_err(|err| {
anyhow::anyhow!("resolving launcher executable for relay child failed: {err}")
})?;
resolve_relay_child_exe_from(
env_override.as_deref(),
&current_exe,
Path::new(INSTALLED_CLIENT_BIN),
)
}
fn resolve_relay_child_exe_from(
env_override: Option<&Path>,
current_exe: &Path,
installed_client: &Path,
) -> Result<PathBuf> {
if let Some(override_path) = env_override
&& !override_path.as_os_str().is_empty()
{
if override_path.is_file() {
return Ok(override_path.to_path_buf());
}
return Err(anyhow::anyhow!(
"{CLIENT_BIN_ENV} points at {}, but that file is not runnable",
override_path.display()
));
}
if relay_exe_path_is_runnable(current_exe) {
return Ok(current_exe.to_path_buf());
}
if installed_client.is_file() {
return Ok(installed_client.to_path_buf());
}
Err(anyhow::anyhow!(
"launcher executable {} is no longer runnable and fallback {} is missing; restart Lesavka or reinstall the client",
current_exe.display(),
installed_client.display()
))
}
fn relay_exe_path_is_runnable(path: &Path) -> bool {
path.is_file()
&& !path
.as_os_str()
.to_string_lossy()
.ends_with(" (deleted)")
}
pub fn attach_child_log_streams(child: &mut RelayChild, tx: Sender<String>) {

View File

@ -1,6 +1,6 @@
[package]
name = "lesavka_common"
version = "0.22.35"
version = "0.22.36"
edition = "2024"
build = "build.rs"

View File

@ -10,7 +10,7 @@ bench = false
[package]
name = "lesavka_server"
version = "0.22.35"
version = "0.22.36"
edition = "2024"
autobins = false