mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 09:58:38 +00:00
fix: handle intentional signal daemon shutdown on abort (#23379) (thanks @frankekn)
This commit is contained in:
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
- Gateway/Restart: fix restart-loop edge cases by keeping `openclaw.mjs -> dist/entry.js` bootstrap detection explicit, reacquiring the gateway lock for in-process restart fallback paths, and tightening restart-loop regression coverage. (#23416) Thanks @jeffwnli.
|
- Gateway/Restart: fix restart-loop edge cases by keeping `openclaw.mjs -> dist/entry.js` bootstrap detection explicit, reacquiring the gateway lock for in-process restart fallback paths, and tightening restart-loop regression coverage. (#23416) Thanks @jeffwnli.
|
||||||
|
- Signal/Monitor: treat user-initiated abort shutdowns as clean exits when auto-started `signal-cli` is terminated, while still surfacing unexpected daemon exits as startup/runtime failures. (#23379) Thanks @frankekn.
|
||||||
- Channels/Dedupe: centralize plugin dedupe primitives in plugin SDK (memory + persistent), move Feishu inbound dedupe to a namespace-scoped persistent store, and reuse shared dedupe cache logic for Zalo webhook replay + Tlon processed-message tracking to reduce duplicate handling during reconnect/replay paths.
|
- Channels/Dedupe: centralize plugin dedupe primitives in plugin SDK (memory + persistent), move Feishu inbound dedupe to a namespace-scoped persistent store, and reuse shared dedupe cache logic for Zalo webhook replay + Tlon processed-message tracking to reduce duplicate handling during reconnect/replay paths.
|
||||||
- ACP/Gateway: wait for gateway hello before opening ACP requests, and fail fast on pre-hello connect failures to avoid startup hangs and early `gateway not connected` request races. (#23390) Thanks @janckerchen.
|
- ACP/Gateway: wait for gateway hello before opening ACP requests, and fail fast on pre-hello connect failures to avoid startup hangs and early `gateway not connected` request races. (#23390) Thanks @janckerchen.
|
||||||
- Security/Audit: add `openclaw security audit` detection for open group policies that expose runtime/filesystem tools without sandbox/workspace guards (`security.exposure.open_groups_with_runtime_or_fs`).
|
- Security/Audit: add `openclaw security audit` detection for open group policies that expose runtime/filesystem tools without sandbox/workspace guards (`security.exposure.open_groups_with_runtime_or_fs`).
|
||||||
|
|||||||
@@ -246,6 +246,43 @@ describe("monitorSignalProvider tool results", () => {
|
|||||||
).rejects.toThrow(/signal daemon exited/i);
|
).rejects.toThrow(/signal daemon exited/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats daemon exit after user abort as clean shutdown", async () => {
|
||||||
|
const runtime = createMonitorRuntime();
|
||||||
|
setSignalAutoStartConfig();
|
||||||
|
const abortController = new AbortController();
|
||||||
|
let exited = false;
|
||||||
|
let resolveExit!: (value: { code: number | null; signal: NodeJS.Signals | null }) => void;
|
||||||
|
const exitedPromise = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
|
||||||
|
(resolve) => {
|
||||||
|
resolveExit = resolve;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const stop = vi.fn(() => {
|
||||||
|
if (exited) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exited = true;
|
||||||
|
resolveExit({ code: null, signal: "SIGTERM" });
|
||||||
|
});
|
||||||
|
spawnSignalDaemonMock.mockReturnValueOnce({
|
||||||
|
stop,
|
||||||
|
exited: exitedPromise,
|
||||||
|
isExited: () => exited,
|
||||||
|
});
|
||||||
|
streamMock.mockImplementationOnce(async () => {
|
||||||
|
abortController.abort(new Error("stop"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runMonitorWithMocks({
|
||||||
|
autoStart: true,
|
||||||
|
baseUrl: SIGNAL_BASE_URL,
|
||||||
|
runtime,
|
||||||
|
abortSignal: abortController.signal,
|
||||||
|
}),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("skips tool summaries with responsePrefix", async () => {
|
it("skips tool summaries with responsePrefix", async () => {
|
||||||
replyMock.mockResolvedValue({ text: "final reply" });
|
replyMock.mockResolvedValue({ text: "final reply" });
|
||||||
|
|
||||||
|
|||||||
@@ -330,6 +330,11 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi
|
|||||||
const daemonAbortController = new AbortController();
|
const daemonAbortController = new AbortController();
|
||||||
const mergedAbort = mergeAbortSignals(opts.abortSignal, daemonAbortController.signal);
|
const mergedAbort = mergeAbortSignals(opts.abortSignal, daemonAbortController.signal);
|
||||||
let daemonHandle: ReturnType<typeof spawnSignalDaemon> | null = null;
|
let daemonHandle: ReturnType<typeof spawnSignalDaemon> | null = null;
|
||||||
|
let daemonStopRequested = false;
|
||||||
|
const stopDaemon = () => {
|
||||||
|
daemonStopRequested = true;
|
||||||
|
daemonHandle?.stop();
|
||||||
|
};
|
||||||
|
|
||||||
if (autoStart) {
|
if (autoStart) {
|
||||||
const cliPath = opts.cliPath ?? accountInfo.config.cliPath ?? "signal-cli";
|
const cliPath = opts.cliPath ?? accountInfo.config.cliPath ?? "signal-cli";
|
||||||
@@ -347,6 +352,9 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi
|
|||||||
runtime,
|
runtime,
|
||||||
});
|
});
|
||||||
void daemonHandle.exited.then((exit) => {
|
void daemonHandle.exited.then((exit) => {
|
||||||
|
if (daemonStopRequested || opts.abortSignal?.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
daemonExitError = new Error(
|
daemonExitError = new Error(
|
||||||
`signal daemon exited (code=${String(exit.code ?? "null")} signal=${String(exit.signal ?? "null")})`,
|
`signal daemon exited (code=${String(exit.code ?? "null")} signal=${String(exit.signal ?? "null")})`,
|
||||||
);
|
);
|
||||||
@@ -357,7 +365,7 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onAbort = () => {
|
const onAbort = () => {
|
||||||
daemonHandle?.stop();
|
stopDaemon();
|
||||||
};
|
};
|
||||||
opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
||||||
|
|
||||||
@@ -426,6 +434,6 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi
|
|||||||
} finally {
|
} finally {
|
||||||
mergedAbort.dispose();
|
mergedAbort.dispose();
|
||||||
opts.abortSignal?.removeEventListener("abort", onAbort);
|
opts.abortSignal?.removeEventListener("abort", onAbort);
|
||||||
daemonHandle?.stop();
|
stopDaemon();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user