use std::path::{Path, PathBuf};
use super::CommsPaths;
pub const DAEMON_BINARY_ENV: &str = "BASEMIND_DAEMON_BINARY";
fn absolute_daemon_binary(exe: PathBuf) -> std::io::Result<PathBuf> {
if exe.is_absolute() || exe.parent().is_none_or(|parent| parent.as_os_str().is_empty()) {
return Ok(exe);
}
Ok(std::env::current_dir()?.join(exe))
}
fn resolve_daemon_binary() -> std::io::Result<PathBuf> {
if let Some(explicit) = std::env::var_os(DAEMON_BINARY_ENV) {
return Ok(PathBuf::from(explicit));
}
daemon_binary_for(&std::env::current_exe()?)
}
fn daemon_binary_for(exe: &Path) -> std::io::Result<PathBuf> {
let stem = exe.file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or_default();
if stem == "basemind" {
return Ok(exe.to_path_buf());
}
if let Some(sibling) = exe
.parent()
.and_then(std::path::Path::parent)
.map(|dir| dir.join(if cfg!(windows) { "basemind.exe" } else { "basemind" }))
.filter(|candidate| candidate.is_file())
{
return Ok(sibling);
}
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"refusing to spawn a comms daemon by re-exec'ing {}: it is not a `basemind` binary. \
Re-exec'ing a test harness makes it re-run its own suite and spawn again, without \
bound. Set {DAEMON_BINARY_ENV} to a real `basemind` executable to spawn one from here.",
exe.display()
),
))
}
const INHERITED_IDENTITY_VARS: [&str; 3] = ["BASEMIND_AGENT_ID", "BASEMIND_PARENT_AGENT_ID", "BASEMIND_THREAD_ID"];
const INHERITED_CWD_VARS: [&str; 2] = ["PWD", "OLDPWD"];
pub const DAEMON_LOG_FILE: &str = "daemon.log";
pub const DAEMON_LOG_MAX_BYTES: u64 = 8 * 1024 * 1024;
pub fn daemon_log_path(comms_dir: &Path) -> PathBuf {
comms_dir.join(DAEMON_LOG_FILE)
}
fn open_daemon_log(comms_dir: &Path) -> Option<std::fs::File> {
let path = daemon_log_path(comms_dir);
if std::fs::metadata(&path).is_ok_and(|meta| meta.len() > DAEMON_LOG_MAX_BYTES) {
let _ = std::fs::rename(&path, comms_dir.join(format!("{DAEMON_LOG_FILE}.1")));
}
let mut options = std::fs::OpenOptions::new();
options.create(true).append(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
options.open(path).ok()
}
pub fn trim_daemon_log(comms_dir: &Path) {
let path = daemon_log_path(comms_dir);
if !std::fs::metadata(&path).is_ok_and(|meta| meta.len() > DAEMON_LOG_MAX_BYTES) {
return;
}
if std::fs::copy(&path, comms_dir.join(format!("{DAEMON_LOG_FILE}.1"))).is_ok() {
let _ = std::fs::OpenOptions::new()
.write(true)
.open(&path)
.and_then(|f| f.set_len(0));
}
}
fn daemon_command(exe: &Path, paths: &CommsPaths) -> std::process::Command {
let mut command = std::process::Command::new(exe);
command
.arg("comms")
.arg("daemon")
.current_dir(&paths.comms_dir)
.stdin(std::process::Stdio::null());
match open_daemon_log(&paths.comms_dir).and_then(|log| log.try_clone().ok().map(|dup| (log, dup))) {
Some((out, err)) => {
command.stdout(out).stderr(err);
}
None => {
command
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
}
}
for key in INHERITED_IDENTITY_VARS.iter().chain(INHERITED_CWD_VARS.iter()) {
command.env_remove(key);
}
command
}
pub fn spawn_detached_daemon(paths: &CommsPaths) -> std::io::Result<()> {
let exe = absolute_daemon_binary(resolve_daemon_binary()?)?;
let _ = std::fs::create_dir_all(&paths.comms_dir);
let mut command = daemon_command(&exe, paths);
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
unsafe {
command.pre_exec(|| {
let _ = detach_session();
Ok(())
});
}
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x0000_0008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
command.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW);
clear_std_handle_inheritance();
}
command.spawn()?;
Ok(())
}
#[cfg(windows)]
fn clear_std_handle_inheritance() {
const STD_INPUT_HANDLE: u32 = -10i32 as u32;
const STD_OUTPUT_HANDLE: u32 = -11i32 as u32;
const STD_ERROR_HANDLE: u32 = -12i32 as u32;
const HANDLE_FLAG_INHERIT: u32 = 0x0000_0001;
const INVALID_HANDLE_VALUE: isize = -1;
for selector in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
unsafe {
let handle = GetStdHandle(selector);
if handle != 0 && handle != INVALID_HANDLE_VALUE {
let _ = SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0);
}
}
}
}
#[cfg(windows)]
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetStdHandle(nstdhandle: u32) -> isize;
fn SetHandleInformation(hobject: isize, dwmask: u32, dwflags: u32) -> i32;
}
#[cfg(unix)]
fn detach_session() -> std::io::Result<()> {
let rc = unsafe { setsid() };
if rc == -1 {
Err(std::io::Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(unix)]
unsafe extern "C" {
fn setsid() -> i32;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_refuse_to_re_exec_a_binary_that_is_not_basemind() {
let harness = std::path::Path::new("/nowhere/target/debug/deps/git_history_daemon-17c2bf1b");
let error = daemon_binary_for(harness).expect_err("a test harness must never be re-exec'd");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
let message = error.to_string();
assert!(
message.contains("not a `basemind` binary") && message.contains(DAEMON_BINARY_ENV),
"the refusal must name the cause and the escape hatch; got: {message}"
);
}
#[test]
fn should_re_exec_a_real_basemind_binary() {
let real = std::path::Path::new("/usr/local/bin/basemind");
assert_eq!(daemon_binary_for(real).expect("a basemind binary resolves"), real);
}
#[test]
fn should_redirect_a_harness_to_the_sibling_basemind_instead_of_itself() {
let target = tempfile::tempdir().expect("tempdir");
let deps = target.path().join("deps");
std::fs::create_dir_all(&deps).expect("create deps dir");
let real = target
.path()
.join(if cfg!(windows) { "basemind.exe" } else { "basemind" });
std::fs::write(&real, b"").expect("write the sibling binary");
let harness = deps.join("comms_smoke-deadbeef");
assert_eq!(
daemon_binary_for(&harness).expect("the sibling resolves"),
real,
"the harness must be redirected to the real binary, never re-exec'd"
);
}
fn paths_at(comms_dir: &std::path::Path) -> CommsPaths {
CommsPaths {
comms_dir: comms_dir.to_path_buf(),
socket_path: comms_dir.join("comms.sock"),
}
}
#[test]
fn oversized_daemon_log_is_rotated_at_spawn_and_trimmed_in_place() {
let dir = tempfile::tempdir().expect("tempdir");
let log = daemon_log_path(dir.path());
let big = vec![b'x'; usize::try_from(DAEMON_LOG_MAX_BYTES).expect("fits") + 1];
std::fs::write(&log, &big).expect("seed");
drop(open_daemon_log(dir.path()).expect("open"));
assert_eq!(
std::fs::metadata(&log).expect("fresh log").len(),
0,
"spawn starts a fresh log"
);
assert!(dir.path().join("daemon.log.1").exists(), "the old log is kept as .1");
std::fs::write(&log, &big).expect("grow");
trim_daemon_log(dir.path());
assert_eq!(std::fs::metadata(&log).expect("trimmed").len(), 0);
assert!(std::fs::metadata(dir.path().join("daemon.log.1")).expect(".1").len() > DAEMON_LOG_MAX_BYTES);
}
#[test]
fn the_daemon_runs_in_the_comms_dir_not_the_spawner_cwd() {
let dir = tempfile::tempdir().expect("tempdir");
let command = daemon_command(std::path::Path::new("/usr/local/bin/basemind"), &paths_at(dir.path()));
assert_eq!(
command.get_current_dir(),
Some(dir.path()),
"the child must be placed in the comms dir, which is never a repository"
);
}
#[test]
fn the_child_environment_drops_identity_and_cwd_vars_and_keeps_everything_else() {
let dir = tempfile::tempdir().expect("tempdir");
let command = daemon_command(std::path::Path::new("/usr/local/bin/basemind"), &paths_at(dir.path()));
let removed: Vec<&std::ffi::OsStr> = command
.get_envs()
.filter(|(_, value)| value.is_none())
.map(|(key, _)| key)
.collect();
for key in INHERITED_IDENTITY_VARS.iter().chain(INHERITED_CWD_VARS.iter()) {
assert!(
removed.contains(&std::ffi::OsStr::new(key)),
"{key} must not reach the daemon; removed: {removed:?}"
);
}
assert!(
command.get_envs().all(|(_, value)| value.is_none()),
"only removals belong here — an override would mean the parent env is being rebuilt"
);
assert_eq!(
removed.len(),
INHERITED_IDENTITY_VARS.len() + INHERITED_CWD_VARS.len(),
"nothing the daemon needs (PATH, HOME, XDG, proxy, model vars) may be swept up: {removed:?}"
);
}
#[test]
fn a_relative_binary_is_absolutised_before_the_working_directory_changes() {
let relative = PathBuf::from("target/debug/basemind");
let resolved = absolute_daemon_binary(relative.clone()).expect("absolutised");
assert!(resolved.is_absolute(), "got {}", resolved.display());
assert!(resolved.ends_with(&relative));
let absolute = PathBuf::from(if cfg!(windows) {
r"C:\bin\basemind.exe"
} else {
"/usr/local/bin/basemind"
});
assert_eq!(absolute_daemon_binary(absolute.clone()).expect("unchanged"), absolute);
let bare = PathBuf::from("basemind");
assert_eq!(absolute_daemon_binary(bare.clone()).expect("unchanged"), bare);
}
#[test]
fn should_use_the_explicit_binary_override_when_set() {
let previous = std::env::var_os(DAEMON_BINARY_ENV);
let explicit = std::path::PathBuf::from("/opt/somewhere/basemind-wrapper");
unsafe { std::env::set_var(DAEMON_BINARY_ENV, &explicit) };
let resolved = resolve_daemon_binary();
match previous {
Some(value) => unsafe { std::env::set_var(DAEMON_BINARY_ENV, value) },
None => unsafe { std::env::remove_var(DAEMON_BINARY_ENV) },
}
assert_eq!(resolved.expect("the override resolves"), explicit);
}
}