mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 13:00:29 +00:00
fix(daemon): skip machine-scope fallback on permission-denied bus errors (#62337)
* fix(daemon): skip machine-scope fallback on permission-denied bus errors; fall back to --user when sudo machine scope fails When systemctl --user fails with "Failed to connect to bus: Permission denied", the machine-scope fallback is now skipped. A Permission denied error means the bus socket exists but the process cannot connect to it, so --machine user@ would hit the same wall. Additionally, the sudo path in execSystemctlUser now tries machine scope first but falls through to a direct --user attempt if it fails, instead of returning the error immediately. Fixes #61959 * fix(daemon): guard against double machine-scope call when sudo path already tried it When SUDO_USER is set and machine scope fails with a non-permission-denied bus error, execution falls through to the direct --user attempt. If that also fails with a bus-unavailable message, shouldFallbackToMachineUserScope returns true and machine scope is tried a second time -- a redundant exec that was never reachable before this PR opened the fallthrough path. Add machineScopeAlreadyTried flag and include it in the bottom-fallback guard condition so the second call is skipped when machine scope was already attempted in the sudo branch. Add regression test asserting exactly 2 execFile calls in this scenario. * fix: keep sudo systemctl scoped --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -169,6 +169,40 @@ describe("systemd availability", () => {
|
||||
|
||||
await expect(isSystemdUserServiceAvailable({ USER: "debian" })).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("does not fall back to machine scope when --user fails with permission denied", async () => {
|
||||
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
expect(args).toEqual(["--user", "status"]);
|
||||
cb(
|
||||
createExecFileError("Failed to connect to bus: Permission denied", {
|
||||
stderr: "Failed to connect to bus: Permission denied",
|
||||
code: 1,
|
||||
}),
|
||||
"",
|
||||
"",
|
||||
);
|
||||
});
|
||||
// Only one call should be made: no machine-scope fallback for permission denied errors.
|
||||
await expect(isSystemdUserServiceAvailable({ USER: "debian" })).resolves.toBe(false);
|
||||
expect(execFileMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not fall back to direct --user when machine scope fails under sudo", async () => {
|
||||
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
|
||||
assertMachineUserSystemctlArgs(args, "ai", "status");
|
||||
cb(
|
||||
createExecFileError("Failed to connect to bus: No such file or directory", {
|
||||
stderr: "Failed to connect to bus: No such file or directory",
|
||||
code: 1,
|
||||
}),
|
||||
"",
|
||||
"",
|
||||
);
|
||||
});
|
||||
|
||||
await expect(isSystemdUserServiceAvailable({ SUDO_USER: "ai" })).resolves.toBe(false);
|
||||
expect(execFileMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSystemdServiceEnabled", () => {
|
||||
|
||||
@@ -351,7 +351,13 @@ function resolveSystemctlMachineUserScopeArgs(user: string): string[] {
|
||||
}
|
||||
|
||||
function shouldFallbackToMachineUserScope(detail: string): boolean {
|
||||
return isSystemdUserBusUnavailableDetail(detail);
|
||||
if (!isSystemdUserBusUnavailableDetail(detail)) {
|
||||
return false;
|
||||
}
|
||||
// "Permission denied" means the bus socket exists but this process cannot connect to it.
|
||||
// The machine-scope approach targets the same bus infrastructure and will also fail,
|
||||
// so do not trigger the fallback in this case.
|
||||
return !detail.toLowerCase().includes("permission denied");
|
||||
}
|
||||
|
||||
async function execSystemctlUser(
|
||||
@@ -361,10 +367,11 @@ async function execSystemctlUser(
|
||||
const machineUser = resolveSystemctlMachineScopeUser(env);
|
||||
const sudoUser = env.SUDO_USER?.trim();
|
||||
|
||||
// Under sudo, prefer the invoking non-root user's scope directly.
|
||||
// Under sudo, prefer the invoking non-root user's scope directly via machine scope.
|
||||
if (sudoUser && sudoUser !== "root" && machineUser) {
|
||||
const machineScopeArgs = resolveSystemctlMachineUserScopeArgs(machineUser);
|
||||
if (machineScopeArgs.length > 0) {
|
||||
// Do not fall through to bare --user: under sudo that can target root's user manager.
|
||||
return await execSystemctl([...machineScopeArgs, ...args]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user