use std::ffi::OsString;
use std::path::PathBuf;
#[cfg(target_os = "linux")]
use std::sync::OnceLock;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScopeLauncher {
systemd_run: PathBuf,
}
impl ScopeLauncher {
pub fn wrap(
&self,
description: &str,
program: impl Into<OsString>,
args: impl IntoIterator<Item = OsString>,
) -> (OsString, Vec<OsString>) {
let mut argv: Vec<OsString> = vec![
"--user".into(),
"--scope".into(),
"--quiet".into(),
"--collect".into(),
format!("--description={description}").into(),
"--".into(),
program.into(),
];
argv.extend(args);
(self.systemd_run.clone().into_os_string(), argv)
}
}
fn enabled_from(env_value: Option<&str>, config_value: bool) -> bool {
match env_value.map(|v| v.trim().to_ascii_lowercase()) {
Some(v) if matches!(v.as_str(), "0" | "off" | "false" | "no") => false,
Some(v) if matches!(v.as_str(), "1" | "on" | "true" | "yes" | "auto") => true,
_ => config_value,
}
}
pub fn inherited_env() -> Vec<(OsString, OsString)> {
std::env::vars_os().collect()
}
pub fn set_env(env: &mut Vec<(OsString, OsString)>, key: &str, value: impl Into<OsString>) {
env.retain(|(k, _)| k != key);
env.push((key.into(), value.into()));
}
#[cfg(target_os = "linux")]
fn find_systemd_run() -> Option<PathBuf> {
["/usr/bin/systemd-run", "/bin/systemd-run"]
.into_iter()
.map(PathBuf::from)
.find(|p| p.is_file())
}
#[cfg(target_os = "linux")]
fn probe(
systemd_run: &std::path::Path,
env: &[(OsString, OsString)],
cwd: Option<&std::path::Path>,
) -> bool {
use std::time::{Duration, Instant};
if !env
.iter()
.any(|(k, v)| (k == "XDG_RUNTIME_DIR" || k == "DBUS_SESSION_BUS_ADDRESS") && !v.is_empty())
{
return false;
}
use std::os::unix::process::CommandExt;
let mut cmd = std::process::Command::new(systemd_run);
unsafe {
cmd.pre_exec(|| {
if libc::setsid() < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
cmd.env_clear().envs(env.iter().cloned());
if let Some(cwd) = cwd {
cmd.current_dir(cwd);
}
let Ok(mut child) = cmd
.args([
"--user",
"--scope",
"--quiet",
"--collect",
"--description=synaps scope probe",
"--",
"/bin/true",
])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
else {
return false;
};
let deadline = Instant::now() + Duration::from_secs(3);
loop {
match child.try_wait() {
Ok(Some(status)) => return status.success(),
Ok(None) if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(5)),
_ => {
unsafe {
libc::killpg(child.id() as libc::pid_t, libc::SIGKILL);
}
let _ = child.kill();
let cleanup_deadline = Instant::now() + Duration::from_millis(100);
while matches!(child.try_wait(), Ok(None)) && Instant::now() < cleanup_deadline {
std::thread::sleep(Duration::from_millis(5));
}
std::thread::spawn(move || {
let _ = child.wait();
});
return false;
}
}
}
}
pub fn launcher_for_effective_env(
env: &[(OsString, OsString)],
cwd: Option<&std::path::Path>,
) -> Option<ScopeLauncher> {
#[cfg(any(test, feature = "testing"))]
if std::env::var("SYNAPS_TEST_REAL_SCOPES").as_deref() != Ok("1") {
return None;
}
if !enabled_from(
std::env::var("SYNAPS_PROCESS_SCOPES").ok().as_deref(),
crate::config::load_config().process_scopes,
) {
return None;
}
#[cfg(target_os = "linux")]
{
static BINARY: OnceLock<Option<PathBuf>> = OnceLock::new();
let binary = BINARY.get_or_init(find_systemd_run).as_ref()?;
launcher_with_binary(binary, env, cwd)
}
#[cfg(not(target_os = "linux"))]
{
let _ = cwd;
None
}
}
#[cfg(target_os = "linux")]
fn launcher_with_binary(
binary: &std::path::Path,
env: &[(OsString, OsString)],
cwd: Option<&std::path::Path>,
) -> Option<ScopeLauncher> {
probe(binary, env, cwd).then(|| ScopeLauncher {
systemd_run: binary.to_owned(),
})
}
pub fn launcher_for_env(env: Option<&[(String, String)]>) -> Option<ScopeLauncher> {
let env = env
.map(|e| e.iter().map(|(k, v)| (k.into(), v.into())).collect())
.unwrap_or_else(inherited_env);
launcher_for_effective_env(&env, None)
}
pub fn resolve_program(
program: OsString,
env: &[(OsString, OsString)],
cwd: Option<&std::path::Path>,
) -> std::io::Result<OsString> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let base = cwd
.map(PathBuf::from)
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let base = if base.is_absolute() {
base
} else {
std::env::current_dir().unwrap_or_default().join(base)
};
if std::path::Path::new(&program).components().count() > 1 {
return Ok(base.join(program).into_os_string());
}
let path = env
.iter()
.find(|(k, _)| k == "PATH")
.map(|(_, v)| v.clone())
.unwrap_or_else(|| "/bin:/usr/bin".into());
for dir in std::env::split_paths(&path) {
let candidate = base.join(dir).join(&program);
if candidate
.metadata()
.is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
{
return Ok(candidate.into_os_string());
}
}
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("program not found in child PATH: {program:?}"),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wrap_puts_the_command_after_systemd_run_options() {
let l = ScopeLauncher {
systemd_run: PathBuf::from("/usr/bin/systemd-run"),
};
let (program, args) = l.wrap("synaps tool: bash", "bash", ["-c".into(), "echo hi".into()]);
assert_eq!(program, OsString::from("/usr/bin/systemd-run"));
let args: Vec<String> = args
.iter()
.map(|a| a.to_string_lossy().into_owned())
.collect();
assert_eq!(
args,
[
"--user",
"--scope",
"--quiet",
"--collect",
"--description=synaps tool: bash",
"--",
"bash",
"-c",
"echo hi",
]
);
}
#[test]
fn ordinary_test_build_does_not_probe_host_manager() {
if std::env::var("SYNAPS_TEST_REAL_SCOPES").as_deref() == Ok("1") {
return;
}
let env = vec![(
"DBUS_SESSION_BUS_ADDRESS".into(),
"fake:must-not-probe".into(),
)];
assert!(launcher_for_effective_env(&env, None).is_none());
}
#[test]
fn env_variable_overrides_config_and_unknown_values_fall_back_to_config() {
assert!(enabled_from(None, true));
assert!(!enabled_from(None, false));
for off in ["0", "off", "OFF", " false ", "no"] {
assert!(!enabled_from(Some(off), true), "{off:?}");
}
for on in ["1", "on", "true", "yes", "auto"] {
assert!(enabled_from(Some(on), false), "{on:?}");
}
assert!(enabled_from(Some("maybe"), true));
assert!(!enabled_from(Some("maybe"), false));
}
#[cfg(target_os = "linux")]
fn fake_script(dir: &std::path::Path, name: &str, body: &str) -> PathBuf {
use std::os::unix::fs::PermissionsExt;
let path = dir.join(name);
std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).unwrap();
path
}
#[cfg(target_os = "linux")]
#[test]
fn fresh_probe_uses_exact_env_cwd_and_observes_availability_changes() {
let dir = tempfile::tempdir().unwrap();
let binary = fake_script(
dir.path(),
"systemd-run",
r#"
[ "$DBUS_SESSION_BUS_ADDRESS" = fake:good ] || exit 1
[ "$TERM" = user-term ] || exit 2
[ "$SYNAPS_DAEMON" = 1 ] || exit 3
[ "$SYNAPS_DAEMON_READY_FD" = 987 ] || exit 4
[ "${HOME+x}" != x ] || exit 5
[ "$PWD" = "$EXPECTED_CWD" ] || exit 6
while [ "$1" != -- ]; do shift; done
shift
[ "$1" = /bin/true ] || exit 7
exit 0
"#,
);
let mut env = vec![];
for (k, v) in [
("DBUS_SESSION_BUS_ADDRESS", "fake:good"),
("TERM", "user-term"),
("SYNAPS_DAEMON", "1"),
("SYNAPS_DAEMON_READY_FD", "987"),
("PATH", "/nonexistent"),
] {
set_env(&mut env, k, v);
}
set_env(&mut env, "EXPECTED_CWD", dir.path().as_os_str());
assert!(launcher_with_binary(&binary, &env, Some(dir.path())).is_some());
set_env(&mut env, "DBUS_SESSION_BUS_ADDRESS", "fake:stale");
assert!(launcher_with_binary(&binary, &env, Some(dir.path())).is_none());
set_env(&mut env, "DBUS_SESSION_BUS_ADDRESS", "fake:good");
assert!(launcher_with_binary(&binary, &env, Some(dir.path())).is_some());
fake_script(dir.path(), "systemd-run", "exit 1");
assert!(launcher_with_binary(&binary, &env, Some(dir.path())).is_none());
env.retain(|(k, _)| k != "DBUS_SESSION_BUS_ADDRESS");
assert!(!probe(&binary, &env, Some(dir.path())));
}
#[cfg(target_os = "linux")]
#[test]
fn post_probe_failure_never_replays_and_exec_preserves_pid_and_stdio() {
use std::io::Write;
use std::process::{Command, Stdio};
let dir = tempfile::tempdir().unwrap();
let command = fake_script(
dir.path(),
"command",
"printf 'command=%s\\n' \"$$\"; read line; printf '%s\\n' \"$line\"; exit 23",
);
let env = vec![("DBUS_SESSION_BUS_ADDRESS".into(), "fake:good".into())];
for started in [false, true] {
let binary = fake_script(
dir.path(),
"systemd-run",
&format!(
r#"
while [ "$1" != -- ]; do shift; done
shift
[ "$1" = /bin/true ] && exit 0
printf 'wrapper=%s\n' "$$"
{}
"#,
if started { "exec \"$@\"" } else { "exit 42" }
),
);
let launcher = launcher_with_binary(&binary, &env, Some(dir.path())).unwrap();
let (program, args) = launcher.wrap("test", command.as_os_str(), []);
let mut child = Command::new(program)
.args(args)
.env_clear()
.envs(env.clone())
.current_dir(dir.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
let pid = child.id();
if started {
child.stdin.take().unwrap().write_all(b"input\n").unwrap();
}
let output = child.wait_with_output().unwrap();
let text = String::from_utf8(output.stdout).unwrap();
assert_eq!(
text,
if started {
format!("wrapper={pid}\ncommand={pid}\ninput\n")
} else {
format!("wrapper={pid}\n")
}
);
assert_eq!(output.status.code(), Some(if started { 23 } else { 42 }));
}
}
#[cfg(unix)]
#[test]
fn missing_child_path_program_is_an_error_not_launcher_path_fallback() {
let dir = tempfile::tempdir().unwrap();
let env = vec![("PATH".into(), dir.path().as_os_str().to_owned())];
assert!(resolve_program("bash".into(), &env, Some(dir.path())).is_err());
}
#[test]
fn host_policy_is_not_selected_from_the_child_environment() {
let source = include_str!("process_scope.rs");
let selection = source
.split("pub fn launcher_for_effective_env")
.nth(1)
.unwrap()
.split("/// Compatibility convenience")
.next()
.unwrap();
assert!(selection.contains("std::env::var(\"SYNAPS_PROCESS_SCOPES\")"));
assert!(!selection.contains("env.iter()"));
assert!(!enabled_from(Some("off"), true));
}
#[test]
fn production_callers_snapshot_overrides_and_do_not_retry_wrapped_launches() {
let pty = include_str!("tools/shell/pty.rs");
let spawn = pty
.split("pub(crate) fn prepare_scope")
.nth(1)
.unwrap()
.split("// 4.")
.next()
.unwrap();
assert!(
spawn.find("set_env(&mut effective_env, \"TERM\"").unwrap()
< spawn.find("for (k, v) in env").unwrap()
);
assert!(
spawn.find("for (k, v) in env").unwrap()
< spawn.find("launcher_for_effective_env").unwrap()
);
assert!(spawn.contains("cmd.env_clear();"));
assert_eq!(spawn.matches(".spawn_command(cmd)").count(), 1);
let daemon = include_str!("daemon/mod.rs");
let spawn = daemon.split("pub fn spawn_detached(").nth(1).unwrap();
assert!(
spawn.find("launcher_for_effective_env").unwrap() < spawn.find("libc::pipe(").unwrap()
);
assert!(
spawn.find("libc::pipe(").unwrap()
< spawn
.find("set_env(&mut effective_env, READY_FD_ENV")
.unwrap()
);
assert!(spawn.find("child.kill()").unwrap() < spawn.find("take(64 * 1024)").unwrap());
assert!(spawn.contains("cmd.env_clear().envs(effective_env)"));
let bash = include_str!("tools/bash.rs");
let spawn = bash
.split("pub(crate) async fn run_shell_command")
.nth(1)
.unwrap()
.split("#[cfg(test)]")
.next()
.unwrap();
assert_eq!(spawn.matches("cmd.spawn()").count(), 1);
assert!(spawn.contains("_ = cancel.cancelled()"));
}
}