aboutsummaryrefslogtreecommitdiff
path: root/src/server/autodetect.rs
diff options
context:
space:
mode:
authorFranz Bettag <franz@bett.ag>2026-04-28 19:11:11 +0200
committerGitHub <noreply@github.com>2026-04-28 20:11:11 +0300
commit5baf231e076ed96bb2aaba245eb53ad0e4501d76 (patch)
treea89d28ad26b26f8b8e519106dc304d4e54d3b39e /src/server/autodetect.rs
parent22d7bef5680a1d55c28ff6246518d96487e95d5e (diff)
Add named persistent sessions (#57)
* feat: add named persistent sessions * feat: add session management commands * fix: tighten named session stop and override precedence * fix: handle session help and stop test race * test: isolate default session env test --------- Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/server/autodetect.rs')
-rw-r--r--src/server/autodetect.rs73
1 files changed, 62 insertions, 11 deletions
diff --git a/src/server/autodetect.rs b/src/server/autodetect.rs
index c4a07a2d..8f695467 100644
--- a/src/server/autodetect.rs
+++ b/src/server/autodetect.rs
@@ -12,6 +12,7 @@ use std::io;
use std::os::unix::net::UnixStream;
use std::os::unix::process::CommandExt;
use std::path::Path;
+use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
@@ -85,8 +86,9 @@ fn is_server_listening_at(socket_path: &Path) -> bool {
/// The server process is fully detached:
/// - Runs in its own session (setsid) so it survives the client exiting
/// - Stdin/stdout/stderr are redirected to /dev/null
-/// - Inherits all relevant environment variables
-/// (`HERDR_SOCKET_PATH`, `HERDR_CLIENT_SOCKET_PATH`, `XDG_CONFIG_HOME`, etc.)
+/// - Inherits relevant environment variables (`XDG_CONFIG_HOME`, `HERDR_SESSION`,
+/// socket overrides, etc.), except inherited socket overrides are cleared when
+/// this CLI invocation explicitly selected a session.
///
/// Returns the PID of the spawned server process.
pub fn spawn_server_daemon() -> io::Result<u32> {
@@ -99,7 +101,21 @@ pub fn spawn_server_daemon() -> io::Result<u32> {
info!(exe = %exe.display(), "spawning server daemon");
- let child = Command::new(&exe)
+ let mut command = build_server_daemon_command(exe);
+
+ let child = command.spawn().map_err(|err: io::Error| {
+ io::Error::new(err.kind(), format!("failed to spawn herdr server: {err}"))
+ })?;
+
+ let pid = child.id();
+ info!(pid, "server daemon spawned");
+
+ Ok(pid)
+}
+
+fn build_server_daemon_command(exe: PathBuf) -> Command {
+ let mut command = Command::new(&exe);
+ command
.arg("server")
// Create a new process group so the server survives the parent's exit
// and doesn't receive SIGHUP when the client's terminal closes.
@@ -107,16 +123,15 @@ pub fn spawn_server_daemon() -> io::Result<u32> {
// Redirect stdio to /dev/null
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
- .stderr(std::process::Stdio::null())
- .spawn()
- .map_err(|err: io::Error| {
- io::Error::new(err.kind(), format!("failed to spawn herdr server: {err}"))
- })?;
+ .stderr(std::process::Stdio::null());
- let pid = child.id();
- info!(pid, "server daemon spawned");
+ if crate::session::explicit_session_requested() {
+ command
+ .env_remove(crate::api::SOCKET_PATH_ENV_VAR)
+ .env_remove("HERDR_CLIENT_SOCKET_PATH");
+ }
- Ok(pid)
+ command
}
// ---------------------------------------------------------------------------
@@ -187,7 +202,14 @@ pub fn auto_detect_launch() -> io::Result<()> {
#[cfg(test)]
mod tests {
use super::*;
+ use std::ffi::OsStr;
use std::os::unix::net::UnixListener;
+ use std::sync::{Mutex, OnceLock};
+
+ fn env_lock() -> &'static Mutex<()> {
+ static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
+ LOCK.get_or_init(|| Mutex::new(()))
+ }
fn unique_test_dir(name: &str) -> std::path::PathBuf {
let nanos = std::time::SystemTime::now()
@@ -205,6 +227,35 @@ mod tests {
}
#[test]
+ fn server_daemon_command_clears_socket_overrides_for_explicit_session() {
+ let _guard = env_lock().lock().unwrap();
+ std::env::set_var(crate::api::SOCKET_PATH_ENV_VAR, "/tmp/inherited.sock");
+ std::env::set_var("HERDR_CLIENT_SOCKET_PATH", "/tmp/inherited-client.sock");
+ std::env::remove_var(crate::session::SESSION_ENV_VAR);
+ crate::session::clear_explicit_session_for_test();
+ let args = vec![
+ "herdr".to_string(),
+ "--session".to_string(),
+ "work".to_string(),
+ ];
+ crate::session::configure_from_args(&args).unwrap();
+
+ let command = build_server_daemon_command(PathBuf::from("/tmp/herdr-test"));
+ let envs: Vec<_> = command.get_envs().collect();
+
+ assert!(envs.iter().any(|(key, value)| {
+ *key == OsStr::new(crate::api::SOCKET_PATH_ENV_VAR) && value.is_none()
+ }));
+ assert!(envs.iter().any(|(key, value)| {
+ *key == OsStr::new("HERDR_CLIENT_SOCKET_PATH") && value.is_none()
+ }));
+ std::env::remove_var(crate::api::SOCKET_PATH_ENV_VAR);
+ std::env::remove_var("HERDR_CLIENT_SOCKET_PATH");
+ std::env::remove_var(crate::session::SESSION_ENV_VAR);
+ crate::session::clear_explicit_session_for_test();
+ }
+
+ #[test]
fn is_server_listening_returns_true_for_live_socket() {
let dir = unique_test_dir("live");
std::fs::create_dir_all(&dir).unwrap();