fix(supervisor): keep service-managed children attached (#38463, thanks @spirittechie)

Co-authored-by: Jesse Paul <drzin69@gmail.com>
This commit is contained in:
Peter Steinberger
2026-03-07 21:36:07 +00:00
parent cc7e61612a
commit e20f445099
3 changed files with 29 additions and 5 deletions

View File

@@ -282,6 +282,7 @@ Docs: https://docs.openclaw.ai
- Config/env substitution degraded mode: convert missing `${VAR}` resolution in config reads from hard-fail to warning-backed degraded behavior, while preventing unresolved placeholders from being accepted as gateway credentials. (#39050) Thanks @akz142857.
- Discord inbound listener non-blocking dispatch: make `MESSAGE_CREATE` listener handoff asynchronous (no per-listener queue blocking), so long runs no longer stall unrelated incoming events. (#39154) Thanks @yaseenkadlemakki.
- Daemon/Windows PATH freeze fix: stop persisting install-time `PATH` snapshots into Scheduled Task scripts so runtime tool lookup follows current host PATH updates; also refresh local TUI history on silent local finals. (#39139) Thanks @Narcooo.
- Gateway/systemd service restart hardening: clear stale gateway listeners by explicit run-port before service bind, add restart stale-pid port-override support, tune systemd start/stop/exit handling, and disable detached child mode only in service-managed runtime so cgroup stop semantics clean up descendants reliably. (#38463) Thanks @spirittechie.
## 2026.3.2

View File

@@ -49,6 +49,8 @@ async function createAdapterHarness(params?: {
}
describe("createChildAdapter", () => {
const originalServiceMarker = process.env.OPENCLAW_SERVICE_MARKER;
beforeAll(async () => {
({ createChildAdapter } = await import("./child.js"));
});
@@ -56,6 +58,11 @@ describe("createChildAdapter", () => {
beforeEach(() => {
spawnWithFallbackMock.mockClear();
killProcessTreeMock.mockClear();
if (originalServiceMarker === undefined) {
delete process.env.OPENCLAW_SERVICE_MARKER;
} else {
process.env.OPENCLAW_SERVICE_MARKER = originalServiceMarker;
}
});
it("uses process-tree kill for default SIGKILL", async () => {
@@ -90,6 +97,19 @@ describe("createChildAdapter", () => {
expect(killMock).toHaveBeenCalledWith("SIGTERM");
});
it("disables detached mode in service-managed runtime", async () => {
process.env.OPENCLAW_SERVICE_MARKER = "openclaw";
await createAdapterHarness({ pid: 7777 });
const spawnArgs = spawnWithFallbackMock.mock.calls[0]?.[0] as {
options?: { detached?: boolean };
fallbacks?: Array<{ options?: { detached?: boolean } }>;
};
expect(spawnArgs.options?.detached).toBe(false);
expect(spawnArgs.fallbacks ?? []).toEqual([]);
});
it("keeps inherited env when no override env is provided", async () => {
await createAdapterHarness({
pid: 3333,

View File

@@ -21,6 +21,10 @@ function resolveCommand(command: string): string {
export type ChildAdapter = SpawnProcessAdapter<NodeJS.Signals | null>;
function isServiceManagedRuntime(): boolean {
return Boolean(process.env.OPENCLAW_SERVICE_MARKER?.trim());
}
export async function createChildAdapter(params: {
argv: string[];
cwd?: string;
@@ -34,11 +38,10 @@ export async function createChildAdapter(params: {
const stdinMode = params.stdinMode ?? (params.input !== undefined ? "pipe-closed" : "inherit");
// On Windows, `detached: true` creates a new process group and can prevent
// stdout/stderr pipes from connecting when running under a Scheduled Task
// (headless, no console). Default to `detached: false` on Windows; on
// POSIX systems keep `detached: true` so the child survives parent exit.
const useDetached = process.platform !== "win32";
// In service-managed mode keep children attached so systemd/launchd can
// stop the full process tree reliably. Outside service mode preserve the
// existing POSIX detached behavior.
const useDetached = process.platform !== "win32" && !isServiceManagedRuntime();
const options: SpawnOptions = {
cwd: params.cwd,