Skip to content

Commit 4fb4049

Browse files
fix(daemon): handle systemctl is-enabled exit 4 (not-found) on Ubuntu (#33634)
Merged via squash. Prepared head SHA: 67dffc3 Co-authored-by: Yuandiaodiaodiao <[email protected]> Co-authored-by: shakkernerd <[email protected]> Reviewed-by: @shakkernerd
1 parent 3fa43ec commit 4fb4049

File tree

4 files changed

+22
-3
lines changed

4 files changed

+22
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
1717
### Fixes
1818

1919
- Models/custom provider headers: propagate `models.providers.<name>.headers` across inline, fallback, and registry-found model resolution so header-authenticated proxies consistently receive configured request headers. (#27490) thanks @Sid-Qin.
20+
- Daemon/systemd install robustness: treat `systemctl --user is-enabled` exit-code-4 `not-found` responses as not-enabled by combining stderr/stdout detail parsing, so Ubuntu fresh installs no longer fail with `systemctl is-enabled unavailable`. (#33634) Thanks @Yuandiaodiaodiao.
2021
- Slack/system-event session routing: resolve reaction/member/pin/interaction system-event session keys through channel/account bindings (with sender-aware DM routing) so inbound Slack events target the correct agent session in multi-account setups instead of defaulting to `agent:main`. (#34045) Thanks @paulomcg, @daht-mad and @vincentkoc.
2122
- Gateway/HTTP tools invoke media compatibility: preserve raw media payload access for direct `/tools/invoke` clients by allowing media `nodes` invoke commands only in HTTP tool context, while keeping agent-context media invoke blocking to prevent base64 prompt bloat. (#34365) Thanks @obviyus.
2223
- Agents/Nodes media outputs: add dedicated `photos_latest` action handling, block media-returning `nodes invoke` commands, keep metadata-only `camera.list` invoke allowed, and normalize empty `photos_latest` results to a consistent response shape to prevent base64 context bloat. (#34332) Thanks @obviyus.

src/daemon/systemd.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,21 @@ describe("isSystemdServiceEnabled", () => {
9090
"systemctl is-enabled unavailable: Failed to connect to bus",
9191
);
9292
});
93+
94+
it("returns false when systemctl is-enabled exits with code 4 (not-found)", async () => {
95+
const { isSystemdServiceEnabled } = await import("./systemd.js");
96+
execFileMock.mockImplementationOnce((_cmd, _args, _opts, cb) => {
97+
// On Ubuntu 24.04, `systemctl --user is-enabled <unit>` exits with
98+
// code 4 and prints "not-found" to stdout when the unit doesn't exist.
99+
const err = new Error(
100+
"Command failed: systemctl --user is-enabled openclaw-gateway.service",
101+
) as Error & { code?: number };
102+
err.code = 4;
103+
cb(err, "not-found\n", "");
104+
});
105+
const result = await isSystemdServiceEnabled({ env: {} });
106+
expect(result).toBe(false);
107+
});
93108
});
94109

95110
describe("systemd runtime parsing", () => {

src/daemon/systemd.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,10 @@ async function execSystemctl(
143143
}
144144

145145
function readSystemctlDetail(result: { stdout: string; stderr: string }): string {
146-
return (result.stderr || result.stdout || "").trim();
146+
// Concatenate both streams so pattern matchers (isSystemdUnitNotEnabled,
147+
// isSystemctlMissing) can see the unit status from stdout even when
148+
// execFileUtf8 populates stderr with the Node error message fallback.
149+
return `${result.stderr} ${result.stdout}`.trim();
147150
}
148151

149152
function isSystemctlMissing(detail: string): boolean {

src/plugin-sdk/root-alias.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ describe("plugin-sdk root alias", () => {
2727
expect(parsed.success).toBe(false);
2828
});
2929

30-
it("loads legacy root exports lazily through the proxy", () => {
30+
it("loads legacy root exports lazily through the proxy", { timeout: 240_000 }, () => {
3131
expect(typeof rootSdk.resolveControlCommandGate).toBe("function");
3232
expect(typeof rootSdk.default).toBe("object");
3333
expect(rootSdk.default).toBe(rootSdk);
3434
expect(rootSdk.__esModule).toBe(true);
3535
});
3636

37-
it("preserves reflection semantics for lazily resolved exports", () => {
37+
it("preserves reflection semantics for lazily resolved exports", { timeout: 240_000 }, () => {
3838
expect("resolveControlCommandGate" in rootSdk).toBe(true);
3939
const keys = Object.keys(rootSdk);
4040
expect(keys).toContain("resolveControlCommandGate");

0 commit comments

Comments
 (0)