use std::ffi::OsString;
use std::path::PathBuf;
#[cfg(target_os = "linux")]
use std::sync::OnceLock;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScopeKind {
Tool,
Daemon,
}
impl ScopeKind {
pub fn slice_key(self) -> &'static str {
match self {
ScopeKind::Tool => "tool_scope_slice",
ScopeKind::Daemon => "daemon_scope_slice",
}
}
}
fn slice_for(kind: ScopeKind, config: &crate::config::SynapsConfig) -> Option<String> {
match kind {
ScopeKind::Tool => config.tool_scope_slice.clone(),
ScopeKind::Daemon => config.daemon_scope_slice.clone(),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScopeLauncher {
systemd_run: PathBuf,
no_expand: bool,
slice: Option<String>,
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))] const EXPAND_ENVIRONMENT_SINCE: u32 = 254;
fn scope_options(description: &str, no_expand: bool, slice: Option<&str>) -> Vec<OsString> {
let mut argv: Vec<OsString> = vec![
"--user".into(),
"--scope".into(),
"--quiet".into(),
"--collect".into(),
];
if no_expand {
argv.push("--expand-environment=no".into());
}
if let Some(slice) = slice {
argv.push(format!("--slice={slice}").into());
}
argv.push(format!("--description={description}").into());
argv
}
impl ScopeLauncher {
pub fn wrap(
&self,
description: &str,
program: impl Into<OsString>,
args: impl IntoIterator<Item = OsString>,
) -> (OsString, Vec<OsString>) {
let mut argv = scope_options(description, self.no_expand, self.slice.as_deref());
argv.push("--".into());
argv.push(program.into());
argv.extend(args);
(self.systemd_run.clone().into_os_string(), argv)
}
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))] fn parse_systemd_version(output: &str) -> Option<u32> {
let rest = output.lines().next()?.trim().strip_prefix("systemd ")?;
let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
digits.parse().ok()
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))] fn no_expand_for_version(version: Option<u32>) -> Option<bool> {
version.map(|v| v >= EXPAND_ENVIRONMENT_SINCE)
}
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 has_manager_env(env: &[(OsString, OsString)]) -> bool {
env.iter()
.any(|(k, v)| (k == "XDG_RUNTIME_DIR" || k == "DBUS_SESSION_BUS_ADDRESS") && !v.is_empty())
}
#[cfg(target_os = "linux")]
fn probe(
systemd_run: &std::path::Path,
no_expand: bool,
env: &[(OsString, OsString)],
cwd: Option<&std::path::Path>,
slice: Option<&str>,
) -> bool {
if !has_manager_env(env) {
return false;
}
let mut cmd = std::process::Command::new(systemd_run);
cmd.env_clear().envs(env.iter().cloned());
if let Some(cwd) = cwd {
cmd.current_dir(cwd);
}
cmd.args(scope_options("synaps scope probe", no_expand, slice))
.args(["--", "/bin/true"])
.stdout(std::process::Stdio::null());
run_bounded(cmd).is_some_and(|(status, _)| status.success())
}
#[cfg(target_os = "linux")]
fn systemd_run_version(systemd_run: &std::path::Path) -> Option<u32> {
let mut cmd = std::process::Command::new(systemd_run);
cmd.env_clear()
.env("LC_ALL", "C")
.env("SYSTEMD_PAGER", "")
.arg("--version")
.stdout(std::process::Stdio::piped());
let (status, stdout) = run_bounded(cmd)?;
status.success().then_some(())?;
parse_systemd_version(&String::from_utf8_lossy(&stdout))
}
#[cfg(target_os = "linux")]
fn run_bounded(mut cmd: std::process::Command) -> Option<(std::process::ExitStatus, Vec<u8>)> {
use std::io::Read;
use std::os::unix::process::CommandExt;
use std::time::{Duration, Instant};
unsafe {
cmd.pre_exec(|| {
if libc::setsid() < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
let mut child = cmd
.stdin(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.ok()?;
let deadline = Instant::now() + Duration::from_secs(3);
loop {
match child.try_wait() {
Ok(Some(status)) => {
let mut out = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
let _ = stdout.by_ref().take(64 * 1024).read_to_end(&mut out);
}
return Some((status, out));
}
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 None;
}
}
}
}
#[cfg(test)]
fn test_fake_systemd_run(env: &[(OsString, OsString)]) -> Option<PathBuf> {
env.iter()
.find(|(k, _)| k == "SYNAPS_TEST_FAKE_SYSTEMD_RUN")
.map(|(_, v)| PathBuf::from(v))
}
pub fn launcher_for_effective_env(
env: &[(OsString, OsString)],
cwd: Option<&std::path::Path>,
kind: ScopeKind,
) -> Option<ScopeLauncher> {
#[cfg(test)]
let fake = test_fake_systemd_run(env);
#[cfg(not(test))]
let fake: Option<PathBuf> = None;
#[cfg(any(test, feature = "testing"))]
if fake.is_none() && std::env::var("SYNAPS_TEST_REAL_SCOPES").as_deref() != Ok("1") {
return None;
}
let config = crate::config::load_config();
if !enabled_from(
std::env::var("SYNAPS_PROCESS_SCOPES").ok().as_deref(),
config.process_scopes,
) {
return None;
}
let slice = slice_for(kind, &config);
#[cfg(target_os = "linux")]
{
if let Some(fake) = fake {
return launcher_with_binary(&fake, false, env, cwd, kind, slice);
}
static BINARY: OnceLock<Option<PathBuf>> = OnceLock::new();
let binary = BINARY.get_or_init(find_systemd_run).as_ref()?;
static NO_EXPAND: OnceLock<bool> = OnceLock::new();
let no_expand = match NO_EXPAND.get() {
Some(v) => *v,
None => {
let v = no_expand_for_version(systemd_run_version(binary))?;
*NO_EXPAND.get_or_init(|| v)
}
};
launcher_with_binary(binary, no_expand, env, cwd, kind, slice)
}
#[cfg(not(target_os = "linux"))]
{
let _ = (env, cwd, fake, slice);
None
}
}
#[cfg(target_os = "linux")]
fn launcher_with_binary(
binary: &std::path::Path,
no_expand: bool,
env: &[(OsString, OsString)],
cwd: Option<&std::path::Path>,
kind: ScopeKind,
slice: Option<String>,
) -> Option<ScopeLauncher> {
if probe(binary, no_expand, env, cwd, slice.as_deref()) {
if let Some(slice) = slice.as_deref() {
if first_limits_check(slice) {
spawn_slice_limits_check(binary, env, kind, slice);
}
}
return Some(ScopeLauncher {
systemd_run: binary.to_owned(),
no_expand,
slice,
});
}
if let Some(slice) = slice.filter(|_| has_manager_env(env)) {
if first_refusal(&slice) {
tracing::warn!(key = kind.slice_key(), slice = %slice, "{}", probe_failed_warning(&slice));
}
}
None
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))] fn probe_failed_warning(slice: &str) -> String {
format!(
"process scope: scope probe failed (user manager unreachable, or slice {slice} refused); these launches start without a scope"
)
}
#[cfg(target_os = "linux")]
fn first_in(seen: &std::sync::Mutex<Vec<String>>, name: &str) -> bool {
let mut seen = seen.lock().unwrap_or_else(|e| e.into_inner());
if seen.iter().any(|s| s == name) {
return false;
}
seen.push(name.to_owned());
true
}
#[cfg(target_os = "linux")]
fn first_refusal(slice: &str) -> bool {
static WARNED: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
first_in(&WARNED, slice)
}
#[cfg(target_os = "linux")]
fn first_limits_check(slice: &str) -> bool {
static CHECKED: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
first_in(&CHECKED, slice)
}
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, ScopeKind::Tool)
}
const SLICE_LIMIT_PROPERTIES: &str = "MemoryHigh,MemoryMax,MemorySwapMax,CPUWeight,CPUQuotaPerSecUSec,TasksMax";
#[cfg_attr(not(target_os = "linux"), allow(dead_code))] fn slice_without_limits(show: &str) -> Option<bool> {
let mut limits_seen = 0;
let mut unit_file = false;
for line in show.lines() {
let Some((key, value)) = line.split_once('=') else {
continue;
};
if key == "FragmentPath" {
unit_file = !value.is_empty();
} else if SLICE_LIMIT_PROPERTIES.split(',').any(|p| p == key) {
if !matches!(value, "" | "infinity" | "[not set]" | "18446744073709551615") {
return None;
}
limits_seen += 1;
}
}
(limits_seen == SLICE_LIMIT_PROPERTIES.split(',').count()).then_some(unit_file)
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))] fn no_limits_warning(slice: &str, unit_file: bool) -> String {
let why = if unit_file {
"its unit sets none"
} else {
"no unit file defines it: a typo, or a new unit not yet loaded with `systemctl --user daemon-reload`?"
};
format!(
"process scope: slice {slice} has no MemoryHigh, MemoryMax, MemorySwapMax, CPUWeight, CPUQuota or TasksMax set ({why}); scopes in it run uncapped"
)
}
#[cfg(target_os = "linux")]
fn query_slice_without_limits(
systemctl: &std::path::Path,
env: &[(OsString, OsString)],
slice: &str,
) -> Option<bool> {
let mut cmd = std::process::Command::new(systemctl);
cmd.env_clear()
.envs(env.iter().cloned())
.env("LC_ALL", "C")
.env("SYSTEMD_PAGER", "")
.args(["--user", "show", "-p", SLICE_LIMIT_PROPERTIES, "-p", "FragmentPath", "--", slice])
.stdout(std::process::Stdio::piped());
let (status, stdout) = run_bounded(cmd)?;
status.success().then_some(())?;
slice_without_limits(&String::from_utf8_lossy(&stdout))
}
#[cfg(target_os = "linux")]
fn spawn_slice_limits_check(systemd_run: &std::path::Path, env: &[(OsString, OsString)], kind: ScopeKind, slice: &str) {
let systemctl = systemd_run.with_file_name("systemctl");
let env = env.to_vec();
let slice = slice.to_owned();
let _ = std::thread::Builder::new()
.name("synaps-slice-limits".into())
.spawn(move || {
if let Some(unit_file) = query_slice_without_limits(&systemctl, &env, &slice) {
tracing::warn!(key = kind.slice_key(), slice = %slice, "{}", no_limits_warning(&slice, unit_file));
}
});
}
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());
}
}
}
#[cfg(not(unix))]
let _ = (env, cwd);
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("program not found in child PATH: {program:?}"),
))
}
#[cfg(test)]
mod tests {
use super::*;
fn wrapped(no_expand: bool, script: &str) -> (OsString, Vec<String>) {
let l = ScopeLauncher {
systemd_run: PathBuf::from("/usr/bin/systemd-run"),
no_expand,
slice: None,
};
let (program, args) = l.wrap("synaps tool: bash", "bash", ["-c".into(), script.into()]);
let args = args
.iter()
.map(|a| a.to_string_lossy().into_owned())
.collect();
(program, args)
}
#[test]
fn wrap_disables_expansion_when_systemd_run_supports_it() {
let (program, args) = wrapped(true, "echo 'D:${x}' $HOME");
assert_eq!(program, OsString::from("/usr/bin/systemd-run"));
assert_eq!(
args,
[
"--user",
"--scope",
"--quiet",
"--collect",
"--expand-environment=no",
"--description=synaps tool: bash",
"--",
"bash",
"-c",
"echo 'D:${x}' $HOME",
]
);
}
#[test]
fn wrap_never_passes_the_flag_to_a_systemd_run_without_it() {
let (program, args) = wrapped(false, "echo hi");
assert_eq!(program, OsString::from("/usr/bin/systemd-run"));
assert_eq!(
args,
[
"--user",
"--scope",
"--quiet",
"--collect",
"--description=synaps tool: bash",
"--",
"bash",
"-c",
"echo hi",
]
);
assert!(!args.iter().any(|a| a.starts_with("--expand-environment")));
}
#[test]
fn version_decides_the_flag_and_unknown_versions_decide_nothing() {
assert_eq!(
parse_systemd_version("systemd 259 (259.5-0ubuntu3.4)\n+PAM +AUDIT"),
Some(259)
);
assert_eq!(parse_systemd_version("systemd 254 (254.5-1)"), Some(254));
assert_eq!(parse_systemd_version("systemd 249\n"), Some(249));
assert_eq!(parse_systemd_version("systemd 219"), Some(219));
assert_eq!(parse_systemd_version(""), None);
assert_eq!(parse_systemd_version("something else 300"), None);
assert_eq!(parse_systemd_version("systemd x"), None);
assert_eq!(no_expand_for_version(Some(259)), Some(true));
assert_eq!(no_expand_for_version(Some(254)), Some(true));
assert_eq!(no_expand_for_version(Some(253)), Some(false));
assert_eq!(no_expand_for_version(Some(219)), Some(false));
assert_eq!(no_expand_for_version(None), None);
}
#[cfg(target_os = "linux")]
#[test]
fn version_query_and_probe_use_exactly_the_launch_options() {
let dir = tempfile::tempdir().unwrap();
let binary = fake_script(
dir.path(),
"systemd-run",
r#"
[ "$1" = --version ] && { echo 'systemd 249 (249.11-0ubuntu3)'; exit 0; }
for a in "$@"; do case "$a" in --expand-environment=*) exit 1;; --) break;; esac; done
exit 0
"#,
);
assert_eq!(systemd_run_version(&binary), Some(249));
let env = vec![("DBUS_SESSION_BUS_ADDRESS".into(), "fake:good".into())];
assert!(launcher_with_binary(&binary, false, &env, None, ScopeKind::Tool, None).is_some());
assert!(launcher_with_binary(&binary, true, &env, None, ScopeKind::Tool, None).is_none());
fake_script(dir.path(), "systemd-run", "exit 1");
assert_eq!(systemd_run_version(&binary), None);
fake_script(dir.path(), "systemd-run", "echo 'systemd 258 (258)'");
assert_eq!(systemd_run_version(&binary), Some(258));
}
#[cfg(target_os = "linux")]
#[test]
fn real_scope_passes_dollar_braces_through_verbatim() {
if std::env::var("SYNAPS_PROCESS_SCOPES").is_ok_and(|v| !enabled_from(Some(&v), true)) {
eprintln!("skip: SYNAPS_PROCESS_SCOPES is off");
return;
}
let Some(binary) = find_systemd_run() else {
eprintln!("skip: no systemd-run");
return;
};
let Some(no_expand) = no_expand_for_version(systemd_run_version(&binary)) else {
eprintln!("skip: systemd-run --version unusable");
return;
};
let mut env = inherited_env();
set_env(&mut env, "x", "EXPANDED");
let Some(launcher) = launcher_with_binary(&binary, no_expand, &env, None, ScopeKind::Tool, None) else {
eprintln!("skip: no reachable systemd user manager");
return;
};
let payload = ["D:${x}", "$x", "'${x:-dflt}'", "`${a}${b}`", "$$", "${}"];
let mut args: Vec<OsString> =
vec!["-c".into(), "printf '%s\\n' \"$@\"".into(), "sh".into()];
args.extend(payload.iter().map(OsString::from));
let (program, argv) = launcher.wrap("synaps test: no expand", "/bin/sh", args);
let out = std::process::Command::new(program)
.args(argv)
.env_clear()
.envs(env)
.stdin(std::process::Stdio::null())
.output()
.unwrap();
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).unwrap();
assert_eq!(stdout.lines().collect::<Vec<_>>(), payload);
assert!(
!String::from_utf8_lossy(&out.stderr).contains("environment variable"),
"{}",
String::from_utf8_lossy(&out.stderr)
);
}
#[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, ScopeKind::Tool).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, true, &env, Some(dir.path()), ScopeKind::Tool, None).is_some());
set_env(&mut env, "DBUS_SESSION_BUS_ADDRESS", "fake:stale");
assert!(launcher_with_binary(&binary, true, &env, Some(dir.path()), ScopeKind::Tool, None).is_none());
set_env(&mut env, "DBUS_SESSION_BUS_ADDRESS", "fake:good");
assert!(launcher_with_binary(&binary, true, &env, Some(dir.path()), ScopeKind::Tool, None).is_some());
fake_script(dir.path(), "systemd-run", "exit 1");
assert!(launcher_with_binary(&binary, true, &env, Some(dir.path()), ScopeKind::Tool, None).is_none());
env.retain(|(k, _)| k != "DBUS_SESSION_BUS_ADDRESS");
assert!(!probe(&binary, true, &env, Some(dir.path()), None));
}
#[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, true, &env, Some(dir.path()), ScopeKind::Tool, None).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("daemon_launch(").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()"));
}
fn args_of(args: &[OsString]) -> Vec<String> {
args.iter().map(|a| a.to_string_lossy().into_owned()).collect()
}
#[test]
fn wrap_passes_the_slice_before_the_description_when_set() {
let l = ScopeLauncher {
systemd_run: PathBuf::from("/usr/bin/systemd-run"),
no_expand: false,
slice: Some("synaps_tools.slice".into()),
};
let (program, args) = l.wrap("synaps tool: bash", "bash", ["-c".into(), "echo hi".into()]);
assert_eq!(program, OsString::from("/usr/bin/systemd-run"));
assert_eq!(
args_of(&args),
[
"--user",
"--scope",
"--quiet",
"--collect",
"--slice=synaps_tools.slice",
"--description=synaps tool: bash",
"--",
"bash",
"-c",
"echo hi",
]
);
}
#[test]
fn each_kind_reads_its_own_slice_key() {
let mut c = crate::config::SynapsConfig::default();
assert_eq!(slice_for(ScopeKind::Tool, &c), None);
assert_eq!(slice_for(ScopeKind::Daemon, &c), None);
c.tool_scope_slice = Some("t_tools.slice".into());
assert_eq!(slice_for(ScopeKind::Tool, &c).as_deref(), Some("t_tools.slice"));
assert_eq!(slice_for(ScopeKind::Daemon, &c), None);
c.daemon_scope_slice = Some("d_daemon.slice".into());
assert_eq!(slice_for(ScopeKind::Daemon, &c).as_deref(), Some("d_daemon.slice"));
assert_eq!(slice_for(ScopeKind::Tool, &c).as_deref(), Some("t_tools.slice"));
assert_eq!(ScopeKind::Tool.slice_key(), "tool_scope_slice");
assert_eq!(ScopeKind::Daemon.slice_key(), "daemon_scope_slice");
}
#[cfg(target_os = "linux")]
#[test]
fn the_probe_runs_with_the_launch_slice_and_a_refused_slice_launches_direct() {
let dir = tempfile::tempdir().unwrap();
let fake = test_support::fake_systemd_run(dir.path());
let env = vec![("DBUS_SESSION_BUS_ADDRESS".into(), "fake:good".into())];
assert!(launcher_with_binary(&fake, false, &env, Some(dir.path()), ScopeKind::Tool, None).is_some());
assert_eq!(
test_support::recorded(dir.path(), "probe.args"),
["--user", "--scope", "--quiet", "--collect", "--description=synaps scope probe", "--", "/bin/true"]
);
let slice = Some("synaps_tools.slice".to_string());
let l = launcher_with_binary(&fake, false, &env, Some(dir.path()), ScopeKind::Tool, slice).unwrap();
assert_eq!(
test_support::recorded(dir.path(), "probe.args"),
[
"--user",
"--scope",
"--quiet",
"--collect",
"--slice=synaps_tools.slice",
"--description=synaps scope probe",
"--",
"/bin/true",
]
);
assert!(args_of(&l.wrap("t", "/bin/echo", []).1).contains(&"--slice=synaps_tools.slice".to_string()));
let refusing = fake_script(
dir.path(),
"refusing-systemd-run",
r#"for a in "$@"; do case "$a" in --slice=*) exit 1;; esac; done
exit 0"#,
);
let refused = Some("refused-probe-test.slice".to_string());
assert!(launcher_with_binary(&refusing, false, &env, Some(dir.path()), ScopeKind::Tool, refused).is_none());
assert!(launcher_with_binary(&refusing, false, &env, Some(dir.path()), ScopeKind::Tool, None).is_some());
}
#[cfg(target_os = "linux")]
#[test]
fn a_refused_slice_warns_once_per_name() {
assert!(first_refusal("warn-once-test-a.slice"));
assert!(!first_refusal("warn-once-test-a.slice"));
assert!(first_refusal("warn-once-test-b.slice"));
}
#[cfg(target_os = "linux")]
#[test]
#[serial_test::serial(synaps_base_dir)]
fn each_kind_launches_in_its_configured_slice() {
let base = crate::test_env::BaseDirGuard::new();
let _scopes = crate::test_env::EnvVarGuard::remove("SYNAPS_PROCESS_SCOPES");
let dir = tempfile::tempdir().unwrap();
let fake = test_support::fake_systemd_run(dir.path());
let env: Vec<(OsString, OsString)> = test_support::fake_env(&fake)
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect();
let slices = |kind| {
launcher_for_effective_env(&env, Some(dir.path()), kind).map(|l| {
args_of(&l.wrap("t", "/bin/echo", []).1)
.into_iter()
.filter(|a| a.starts_with("--slice"))
.collect::<Vec<_>>()
})
};
assert_eq!(slices(ScopeKind::Tool), Some(vec![]));
assert_eq!(slices(ScopeKind::Daemon), Some(vec![]));
let config = base.path().join("config");
std::fs::write(&config, "tool_scope_slice = t_tools.slice\ndaemon_scope_slice = d_daemon.slice\n").unwrap();
assert_eq!(slices(ScopeKind::Tool), Some(vec!["--slice=t_tools.slice".to_string()]));
assert_eq!(slices(ScopeKind::Daemon), Some(vec!["--slice=d_daemon.slice".to_string()]));
std::fs::write(&config, "process_scopes = off\ntool_scope_slice = t_tools.slice\n").unwrap();
assert_eq!(slices(ScopeKind::Tool), None);
}
#[test]
fn the_fake_systemd_run_seam_is_test_only() {
let compact: String = include_str!("process_scope.rs")
.chars()
.filter(|c| !c.is_whitespace())
.collect();
let name = ["test_fake", "_systemd_run"].concat();
let gate = "#[cfg(test)]";
let definitions: Vec<_> = compact.match_indices(&format!("fn{name}(")).collect();
assert_eq!(definitions.len(), 1, "one definition of the seam");
assert!(compact[..definitions[0].0].ends_with(gate), "the definition is cfg(test)");
let calls: Vec<_> = compact.match_indices(&format!("={name}(")).collect();
assert_eq!(calls.len(), 1, "one call of the seam");
assert!(
compact[..calls[0].0].ends_with(&format!("{gate}letfake")),
"the call is cfg(test)"
);
let production = compact.split("#[cfg(test)]modtests{").next().unwrap();
let env_name = ["SYNAPS_TEST_FAKE", "_SYSTEMD_RUN"].concat();
assert_eq!(production.matches(&format!("\"{env_name}\"")).count(), 1, "named only in the seam");
}
#[test]
fn a_failed_probe_warning_does_not_blame_the_slice_alone() {
let w = probe_failed_warning("synaps_tools.slice");
assert!(w.contains("user manager unreachable"), "{w}");
assert!(w.contains("or slice synaps_tools.slice refused"), "{w}");
assert!(w.contains("start without a scope"), "{w}");
}
#[test]
fn a_slice_with_no_limit_set_is_reported_and_one_limit_is_enough() {
let missing = "LoadState=loaded\nFragmentPath=\nCPUWeight=[not set]\nCPUQuotaPerSecUSec=infinity\n\
MemoryHigh=infinity\nMemoryMax=infinity\nMemorySwapMax=infinity\nTasksMax=infinity\n";
assert_eq!(slice_without_limits(missing), Some(false));
let with_unit = missing.replace("FragmentPath=\n", "FragmentPath=/home/u/.config/systemd/user/synaps_tools.slice\n");
assert_eq!(slice_without_limits(&with_unit), Some(true));
let old = missing.replace("[not set]", "18446744073709551615").replace("TasksMax=infinity", "TasksMax=");
assert_eq!(slice_without_limits(&old), Some(false));
for (from, to) in [
("CPUWeight=[not set]", "CPUWeight=20"),
("CPUQuotaPerSecUSec=infinity", "CPUQuotaPerSecUSec=2s"),
("MemoryHigh=infinity", "MemoryHigh=8589934592"),
("MemoryMax=infinity", "MemoryMax=12884901888"),
("MemorySwapMax=infinity", "MemorySwapMax=0"),
("TasksMax=infinity", "TasksMax=512"),
] {
assert_eq!(slice_without_limits(&missing.replace(from, to)), None, "{to}");
}
assert_eq!(slice_without_limits(""), None);
assert_eq!(slice_without_limits(&missing.replace("TasksMax=infinity\n", "")), None);
let w = no_limits_warning("synaps_tools.slice", false);
assert!(w.contains("synaps_tools.slice") && w.contains("typo") && w.contains("daemon-reload"), "{w}");
assert!(no_limits_warning("synaps_tools.slice", true).contains("its unit sets none"));
}
#[cfg(target_os = "linux")]
#[test]
fn slice_limits_are_looked_up_once_per_name_off_the_launch_path() {
assert!(first_limits_check("limits-once-test-a.slice"));
assert!(!first_limits_check("limits-once-test-a.slice"));
assert!(first_limits_check("limits-once-test-b.slice"));
assert!(first_refusal("limits-once-test-a.slice"));
}
#[cfg(target_os = "linux")]
#[test]
fn a_launch_with_a_slice_asks_the_manager_for_its_limits_once() {
let dir = tempfile::tempdir().unwrap();
let fake = test_support::fake_systemd_run(dir.path());
let log = dir.path().join("systemctl.log");
fake_script(
dir.path(),
"systemctl",
&format!(
"printf '%s %s\\n' \"$DBUS_SESSION_BUS_ADDRESS\" \"$*\" >> '{}'\n\
printf 'FragmentPath=\\nCPUWeight=[not set]\\nCPUQuotaPerSecUSec=infinity\\n'\n\
printf 'MemoryHigh=infinity\\nMemoryMax=infinity\\nMemorySwapMax=infinity\\nTasksMax=infinity\\n'",
log.display()
),
);
let env = vec![("DBUS_SESSION_BUS_ADDRESS".into(), "fake:limits".into())];
let read_log = || std::fs::read_to_string(&log).unwrap_or_default();
assert!(launcher_with_binary(&fake, false, &env, None, ScopeKind::Tool, None).is_some());
let slice = "limits-e2e-test.slice".to_string();
for _ in 0..3 {
assert!(launcher_with_binary(&fake, false, &env, None, ScopeKind::Tool, Some(slice.clone())).is_some());
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while read_log().is_empty() && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(10));
}
std::thread::sleep(std::time::Duration::from_millis(200));
assert_eq!(
read_log().lines().collect::<Vec<_>>(),
[format!("fake:limits --user show -p {SLICE_LIMIT_PROPERTIES} -p FragmentPath -- {slice}")]
);
let systemctl = dir.path().join("systemctl");
assert_eq!(query_slice_without_limits(&systemctl, &env, &slice), Some(false));
fake_script(dir.path(), "systemctl", "exit 1");
assert_eq!(query_slice_without_limits(&systemctl, &env, &slice), None);
}
}
#[cfg(all(test, target_os = "linux"))]
pub(crate) mod test_support {
use std::path::{Path, PathBuf};
pub(crate) fn fake_systemd_run(dir: &Path) -> PathBuf {
use std::os::unix::fs::PermissionsExt;
let path = dir.join("systemd-run");
let body = format!(
"#!/bin/sh\nfor a in \"$@\"; do last=$a; done\n\
if [ \"$last\" = /bin/true ]; then printf '%s\\n' \"$@\" > '{probe}'; exit 0; fi\n\
printf '%s\\n' \"$@\" > '{launch}'\n\
while [ \"$1\" != -- ]; do shift; done\nshift\nexec \"$@\"\n",
probe = dir.join("probe.args").display(),
launch = dir.join("launch.args").display(),
);
std::fs::write(&path, body).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).unwrap();
path
}
pub(crate) fn recorded(dir: &Path, which: &str) -> Vec<String> {
std::fs::read_to_string(dir.join(which))
.unwrap_or_default()
.lines()
.map(str::to_owned)
.collect()
}
pub(crate) fn fake_env(fake: &Path) -> Vec<(String, String)> {
vec![
("DBUS_SESSION_BUS_ADDRESS".into(), "fake:slice-test".into()),
("SYNAPS_TEST_FAKE_SYSTEMD_RUN".into(), fake.display().to_string()),
]
}
}