fix: handle missing systemctl in containers (#26089) (#26699)

* Daemon: handle missing systemctl in containers

* Daemon: harden missing-systemctl detection

* Daemon tests: cover systemctl spawn failure path

* Changelog: note container systemctl service-check fix

* Update CHANGELOG.md

* Daemon: fail closed on unknown systemctl is-enabled errors

* Daemon tests: cover is-enabled unknown-error path

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Sahil Satralkar
2026-03-02 11:18:06 +05:30
committed by GitHub
parent 5d78fcf1b5
commit cda119b052
3 changed files with 96 additions and 7 deletions

View File

@@ -142,6 +142,39 @@ async function execSystemctl(
return await execFileUtf8("systemctl", args);
}
function readSystemctlDetail(result: { stdout: string; stderr: string }): string {
return (result.stderr || result.stdout || "").trim();
}
function isSystemctlMissing(detail: string): boolean {
if (!detail) {
return false;
}
const normalized = detail.toLowerCase();
return (
normalized.includes("not found") ||
normalized.includes("no such file or directory") ||
normalized.includes("spawn systemctl enoent") ||
normalized.includes("spawn systemctl eacces")
);
}
function isSystemdUnitNotEnabled(detail: string): boolean {
if (!detail) {
return false;
}
const normalized = detail.toLowerCase();
return (
normalized.includes("disabled") ||
normalized.includes("static") ||
normalized.includes("indirect") ||
normalized.includes("masked") ||
normalized.includes("not-found") ||
normalized.includes("could not be found") ||
normalized.includes("failed to get unit file state")
);
}
export async function isSystemdUserServiceAvailable(): Promise<boolean> {
const res = await execSystemctl(["--user", "status"]);
if (res.code === 0) {
@@ -174,8 +207,8 @@ async function assertSystemdAvailable() {
if (res.code === 0) {
return;
}
const detail = res.stderr || res.stdout;
if (detail.toLowerCase().includes("not found")) {
const detail = readSystemctlDetail(res);
if (isSystemctlMissing(detail)) {
throw new Error("systemctl not available; systemd user services are required on Linux.");
}
throw new Error(`systemctl --user unavailable: ${detail || "unknown error"}`.trim());
@@ -312,11 +345,17 @@ export async function restartSystemdService({
}
export async function isSystemdServiceEnabled(args: GatewayServiceEnvArgs): Promise<boolean> {
await assertSystemdAvailable();
const serviceName = resolveSystemdServiceName(args.env ?? {});
const unitName = `${serviceName}.service`;
const res = await execSystemctl(["--user", "is-enabled", unitName]);
return res.code === 0;
if (res.code === 0) {
return true;
}
const detail = readSystemctlDetail(res);
if (isSystemctlMissing(detail) || isSystemdUnitNotEnabled(detail)) {
return false;
}
throw new Error(`systemctl is-enabled unavailable: ${detail || "unknown error"}`.trim());
}
export async function readSystemdServiceRuntime(
@@ -327,7 +366,7 @@ export async function readSystemdServiceRuntime(
} catch (err) {
return {
status: "unknown",
detail: String(err),
detail: err instanceof Error ? err.message : String(err),
};
}
const serviceName = resolveSystemdServiceName(env);
@@ -373,8 +412,7 @@ async function isSystemctlAvailable(): Promise<boolean> {
if (res.code === 0) {
return true;
}
const detail = (res.stderr || res.stdout).toLowerCase();
return !detail.includes("not found");
return !isSystemctlMissing(readSystemctlDetail(res));
}
export async function findLegacySystemdUnits(env: GatewayServiceEnv): Promise<LegacySystemdUnit[]> {