feat(appd): wrap runtime in systemd-run cgroup scope when user session is active

supervise() checks /systemd/private to detect an active
user systemd session. When present (and WEFT_DISABLE_CGROUP is unset),
the runtime binary is launched via:

  systemd-run --user --scope --wait --collect --slice=weft-apps.slice     -p CPUQuota=200% -p MemoryMax=512M -- <bin> ...

This places each app in a transient weft-apps.slice scope with default
resource limits from the blueprint. The --wait flag keeps systemd-run
alive so child.wait()/child.kill() remain correct.

When no user session is present the command is built directly as before.
WEFT_DISABLE_CGROUP=1 bypasses the wrapping unconditionally.
This commit is contained in:
Marco Allegretti 2026-03-11 15:25:04 +01:00
parent c5a47a05b4
commit 71b7bdf657

View file

@ -10,6 +10,18 @@ use crate::ipc::{AppStateKind, Response};
const READY_TIMEOUT: Duration = Duration::from_secs(30);
fn systemd_cgroup_available() -> bool {
if std::env::var("WEFT_DISABLE_CGROUP").is_ok() {
return false;
}
let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") else {
return false;
};
std::path::Path::new(&runtime_dir)
.join("systemd/private")
.exists()
}
fn resolve_preopens(app_id: &str) -> Vec<(String, String)> {
#[derive(serde::Deserialize)]
struct Pkg {
@ -92,7 +104,25 @@ pub(crate) async fn supervise(
}
};
let mut cmd = tokio::process::Command::new(&bin);
let mut cmd = if systemd_cgroup_available() {
let mut c = tokio::process::Command::new("systemd-run");
c.args([
"--user",
"--scope",
"--wait",
"--collect",
"--slice=weft-apps.slice",
"-p",
"CPUQuota=200%",
"-p",
"MemoryMax=512M",
"--",
&bin,
]);
c
} else {
tokio::process::Command::new(&bin)
};
cmd.arg(app_id)
.arg(session_id.to_string())
.stdout(std::process::Stdio::piped())