feat(FN-5189): complete Step 3 — migrate supervised spawns

Fusion-Task-Id: FN-5189
Fusion-Task-Lineage: 4caa3f0a-af81-4c60-88e8-de229ed72e08
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 15:33:27 -07:00
committed by gsxdsm
parent 87ea63c7c3
commit 818db4a714
8 changed files with 33 additions and 38 deletions

View File

@@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
import { exec, execFile, spawn, type ChildProcess } from "node:child_process";
import type { Readable } from "node:stream";
import { promisify } from "node:util";
import { superviseSpawn } from "@fusion/core";
import { remoteTunnelLog } from "../logger.js";
import {
getTunnelProviderAdapter,
@@ -306,13 +307,15 @@ export class TunnelProcessManager extends EventEmitter implements TunnelManager
this.emitLog("info", "manager", `Starting ${provider} tunnel: ${command.redactedPreview}`);
const child = this.spawnImpl(command.command, command.args, {
const supervised = superviseSpawn(command.command, command.args, {
cwd: command.cwd,
env: command.env,
detached: process.platform !== "win32",
shell: false,
stdio: ["ignore", "pipe", "pipe"],
maxLifetimeMs: 24 * 60 * 60 * 1_000,
spawnImpl: this.spawnImpl,
});
const child = supervised.child;
this.processHandle = {
provider,

View File

@@ -146,7 +146,7 @@ describe("NativeSandboxBackend.runStreaming", () => {
child.emit("close", null, "SIGTERM");
await expect(promise).resolves.toMatchObject({ outcome: "timeout", timeoutMs: 100 });
expect(spawnMock).toHaveBeenCalledWith("sleep", expect.objectContaining({ detached: false }));
expect(spawnMock).toHaveBeenCalledWith("sleep", [], expect.objectContaining({ detached: false }));
});
it("returns spawn-error", async () => {

View File

@@ -1,5 +1,6 @@
import { exec, spawn } from "node:child_process";
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { superviseSpawn } from "@fusion/core";
import type {
SandboxBackend,
@@ -84,18 +85,18 @@ export class NativeSandboxBackend implements SandboxBackend {
}
return await new Promise((resolve) => {
const useProcessGroup = process.platform !== "win32";
const child = spawn(command, {
const supervised = superviseSpawn(command, [], {
cwd: options.cwd,
shell: true,
detached: useProcessGroup,
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
...(options.env ?? {}),
},
maxLifetimeMs: options.timeout + 6_000,
});
const child = supervised.child;
let stdout = "";
let stderr = "";
@@ -106,16 +107,7 @@ export class NativeSandboxBackend implements SandboxBackend {
let settled = false;
const killTree = (sig: NodeJS.Signals) => {
if (child.pid === undefined) return;
try {
if (useProcessGroup) {
process.kill(-child.pid, sig);
} else {
child.kill(sig);
}
} catch {
// group may already be gone
}
supervised.kill(sig);
};
const timer = setTimeout(() => {