mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
fix(native): require verified remote process exit receipts (#17799)
Require fresh ESRCH or an exact matching zombie generation before treating a remote Runner as stopped. Preserve uncertain ownership through adoption and transport close, retire local controller resources after failed close, and check Node before remote launch. Bind the shared transport change in Copilot profile 62 while retaining historical profile decoding. Add red-green process, permission, launch, adoption and cleanup regressions. Thanks to @hsm-lv for the original investigation in #17073. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
051b425fdf
commit
718c73dafe
16 files changed
+448
-37
No files matched your search
@@ -61,7 +61,7 @@ import { createHash, generateKeyPairSync, randomUUID } from "node:crypto";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { nativeSha256 } from "./canonical.js";
|
||||
import { resolveRemoteProviderPackAuthority } from "./remote-provider-pack-authority.js";
|
||||
import { RemoteRunnerRecoveryError, readRemoteRunnerRecoveryError } from "./remote-runner-recovery-diagnostics.js";
|
||||
import { REMOTE_RECOVERY_PROCESS_CHECK_SCRIPT, RemoteRunnerRecoveryError, readRemoteRunnerRecoveryError } from "./remote-runner-recovery-diagnostics.js";
|
||||
import { agentDirectoryWorkingCopyService } from "../agent-directory-working-copies.js";
|
||||
import { probeAgentDirectory } from "../agent-directory-probe.js";
|
||||
import { AGENT_FILES_CONTRACT } from "../agent-file-store.js";
|
||||
@@ -529,6 +529,10 @@ describe("remote controller restart adoption", () => {
|
||||
exitCode: 4,
|
||||
timedOut: false,
|
||||
});
|
||||
await expect(adopted.isAlive()).rejects.toMatchObject({ remoteRunnerRecovery: {
|
||||
stage: "process_verification", reason: "probe_failed",
|
||||
} });
|
||||
execute.mockResolvedValueOnce({ stdout: "", stderr: "", exitCode: 45, timedOut: false });
|
||||
expect(await adopted.isAlive()).toBe(false);
|
||||
});
|
||||
it.each([
|
||||
@@ -724,7 +728,9 @@ describe("remote controller restart adoption", () => {
|
||||
if (change === "start ticks") await writeFile(join(proc, String(pid), "stat"), stat(101));
|
||||
else if (change === "boot identity") await writeFile(join(proc, "sys/kernel/random/boot_id"), "abcd-5678\n");
|
||||
else await rm(join(proc, String(pid), "stat"));
|
||||
expect(await adopted.isAlive()).toBe(false);
|
||||
await expect(adopted.isAlive()).rejects.toMatchObject({ remoteRunnerRecovery: {
|
||||
stage: "process_verification", reason: change === "fingerprint probe" ? "fingerprint_unavailable" : "process_generation_changed",
|
||||
} });
|
||||
await expect(verifyRemoteRunnerReattachment({ claim, target: target as never, identity, runId: "run", normalizedSessionId: "session" }))
|
||||
.rejects.toMatchObject({ message: "runner_remote_process_identity_unavailable", remoteRunnerRecovery: {
|
||||
stage: "process_verification", reason: change === "fingerprint probe" ? "fingerprint_unavailable" : "process_generation_changed",
|
||||
@@ -778,6 +784,81 @@ describe("remote controller restart adoption", () => {
|
||||
|
||||
|
||||
describe("remote runner launch fingerprint compatibility", () => {
|
||||
it("verifies the generation emitted by the actual launch writer", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "runner-generation-roundtrip-"));
|
||||
const marker = join(root, "identity");
|
||||
const statPath = join(root, "stat");
|
||||
const bootPath = join(root, "boot_id");
|
||||
const commandPath = join(root, "cmdline");
|
||||
const stat = (pid: number) => `${pid} (runner (child)) ${["S", ...Array(18).fill("0"), 100].join(" ")}\n`;
|
||||
await writeFile(statPath, stat(1));
|
||||
await writeFile(bootPath, "abcd-1234\n");
|
||||
await writeFile(commandPath, "runner\0--runner-id\0writer-verifier\0");
|
||||
const writer = REMOTE_RUNNER_CHILD_LAUNCH_SCRIPT
|
||||
.replaceAll('"/proc/"+process.argv[1]+"/stat"', JSON.stringify(statPath))
|
||||
.replaceAll("/proc/sys/kernel/random/boot_id", bootPath);
|
||||
const child = spawn("/bin/sh", ["-c", writer, "writer", marker, "nonce", "writer-verifier", join(root, "diagnostics"),
|
||||
process.execPath, "-e", 'process.stdout.write("ready");setInterval(() => {}, 1000)', "--", "--runner-id", "writer-verifier"], { stdio: ["ignore", "pipe", "pipe"] });
|
||||
const exited = new Promise<void>(resolve => child.once("exit", () => resolve()));
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
child.stdout.once("data", () => resolve());
|
||||
child.once("error", reject);
|
||||
child.once("exit", code => reject(new Error(`launch writer exited before readiness: ${code}`)));
|
||||
});
|
||||
await writeFile(statPath, stat(child.pid!));
|
||||
const generation = (await readFile(marker, "utf8")).split("\n")[4]!;
|
||||
expect(generation).not.toBe("");
|
||||
const verifier = REMOTE_RECOVERY_PROCESS_CHECK_SCRIPT
|
||||
.replaceAll('"/proc/" + pid + "/cmdline"', JSON.stringify(commandPath))
|
||||
.replaceAll('"/proc/" + pid + "/stat"', JSON.stringify(statPath))
|
||||
.replaceAll('"/proc/sys/kernel/random/boot_id"', JSON.stringify(bootPath));
|
||||
expect(execFileSync("sh", ["-c", verifier, "verifier", marker, "nonce", "writer-verifier", String(child.pid), generation], { encoding: "utf8" })).toBe("");
|
||||
expect(() => execFileSync("sh", ["-c", verifier, "verifier", marker, "nonce", "writer-verifier", String(child.pid), `${generation}-changed`], { stdio: "pipe" }))
|
||||
.toThrow();
|
||||
expect(() => process.kill(child.pid!, 0)).not.toThrow();
|
||||
} finally {
|
||||
child.kill("SIGKILL");
|
||||
await exited;
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["missing", "broken", "working"] as const)("checks the Node probe before creating launch state (%s)", async runtime => {
|
||||
const root = await mkdtemp(join(tmpdir(), "runner-launch-probe-"));
|
||||
const bin = join(root, "bin");
|
||||
const marker = join(root, "state", "identity");
|
||||
const detached = join(root, "detached");
|
||||
let launch: { command: string; args?: string[] } | undefined;
|
||||
const execute = vi.fn(async (request: { command: string; args?: string[] }) => {
|
||||
if (request.args?.[2] === "paperclip-runner-launch") launch = request;
|
||||
return { exitCode: 1, timedOut: false, stdout: "", stderr: "" };
|
||||
});
|
||||
try {
|
||||
await mkdir(bin);
|
||||
for (const name of ["dirname", "mkdir", "sh"]) await symlink(execFileSync("sh", ["-c", `command -v ${name}`], { encoding: "utf8" }).trim(), join(bin, name));
|
||||
await writeFile(join(bin, "nohup"), `#!/bin/sh\nprintf detached > '${detached}'\n`, { mode: 0o755 });
|
||||
if (runtime === "working") await symlink(process.execPath, join(bin, "node"));
|
||||
if (runtime === "broken") await writeFile(join(bin, "node"), "#!/bin/sh\nexit 7\n", { mode: 0o755 });
|
||||
const handle = createRemoteRunnerProcessLauncher({
|
||||
target: { kind: "remote", transport: "sandbox", environmentId: "environment", leaseId: "lease", remoteCwd: root },
|
||||
runner: { execute } as never, remoteBinary: "/runtime/runnerd", processIdentityPath: marker,
|
||||
stateDirectory: root, diagnosticsDirectory: join(root, "diagnostics"), runnerInstanceId: "runner-launch",
|
||||
})({ command: "/runtime/runnerd", args: [], cwd: root, environment: {} });
|
||||
await expect(handle.completion).rejects.toThrow("runner_remote_process_launch_failed");
|
||||
expect(launch).toBeDefined();
|
||||
let status = 0;
|
||||
try { execFileSync(launch!.command, launch!.args, { env: { ...process.env, PATH: bin }, stdio: "pipe" }); }
|
||||
catch (error) { status = (error as { status: number }).status; }
|
||||
if (status === 0) await vi.waitFor(async () => expect(await readFile(detached, "utf8")).toBe("detached"));
|
||||
expect(status === 0).toBe(runtime === "working");
|
||||
if (runtime !== "working") {
|
||||
await expect(access(join(root, "state"))).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(access(detached)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
}
|
||||
} finally { await rm(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it.each([true, false])("launches with the guaranteed Node runtime when boot identity is available: %s", async (hasBootIdentity) => {
|
||||
const root = await mkdtemp(join(tmpdir(), "remote-launch-generation-"));
|
||||
try {
|
||||
@@ -803,6 +884,77 @@ describe("remote runner launch fingerprint compatibility", () => {
|
||||
});
|
||||
|
||||
describe("remote runner process supervision", () => {
|
||||
it.each(["exit", "unreadable", "changed generation"] as const)("requires an exit receipt when process metadata changes during supervision (%s)", async failure => {
|
||||
const root = await mkdtemp(join(tmpdir(), "runner-exit-receipt-"));
|
||||
const marker = join(root, "identity");
|
||||
const child = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)", "--", "--runner-id", "runner-exit"], { stdio: "ignore" });
|
||||
const exited = new Promise<void>(resolve => child.once("exit", () => resolve()));
|
||||
const proc = join(root, "proc");
|
||||
const statPath = join(proc, String(child.pid), "stat");
|
||||
const hook = join(root, "probe-hook.cjs");
|
||||
await mkdir(join(proc, String(child.pid)), { recursive: true });
|
||||
await mkdir(join(proc, "sys/kernel/random"), { recursive: true });
|
||||
await writeFile(statPath, `${child.pid} (runner (child)) ${["S", ...Array(18).fill("0"), failure === "changed generation" ? 101 : 100].join(" ")}\n`);
|
||||
await writeFile(join(proc, String(child.pid), "cmdline"), "runner\0--runner-id\0runner-exit\0");
|
||||
await writeFile(join(proc, "sys/kernel/random/boot_id"), "abcd-1234\n");
|
||||
await writeFile(hook, `const fs = require("node:fs");
|
||||
const read = fs.readFileSync;
|
||||
fs.readFileSync = function(path, ...args) {
|
||||
if (path === ${JSON.stringify(statPath)}) {
|
||||
if (${JSON.stringify(failure)} === "unreadable") throw Object.assign(new Error("denied"), { code: "EACCES" });
|
||||
if (${JSON.stringify(failure)} === "exit") {
|
||||
process.kill(${child.pid}, "SIGKILL");
|
||||
const until = Date.now() + 5000;
|
||||
while (true) {
|
||||
try { process.kill(${child.pid}, 0); }
|
||||
catch (error) { if (error.code === "ESRCH") break; throw error; }
|
||||
if (Date.now() > until) throw new Error("fixture child was not reaped");
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
||||
}
|
||||
fs.rmSync(path);
|
||||
}
|
||||
}
|
||||
return read.call(this, path, ...args);
|
||||
};`);
|
||||
let savedMarker = "";
|
||||
const ok = { stdout: "", stderr: "", exitCode: 0, signal: null, timedOut: false };
|
||||
const execute = vi.fn(async (request: { command: string; args?: string[] }) => {
|
||||
if (request.args?.[2] === "paperclip-runner-launch") {
|
||||
savedMarker = `${request.args[4]}\n${child.pid}\n2026-10-26T00:00:00.000Z\nrunner-exit\nlinux:abcd-1234:100\n`;
|
||||
await writeFile(marker, savedMarker);
|
||||
return ok;
|
||||
}
|
||||
const args = request.args?.map(arg => arg.replaceAll("/proc/", `${proc}/`)) ?? [];
|
||||
return new Promise<typeof ok>(resolve => execFile(request.command, args, {
|
||||
env: { ...process.env, NODE_OPTIONS: `--require=${hook}` }, timeout: 8_000,
|
||||
}, (error, stdout, stderr) => resolve({ ...ok, stdout, stderr,
|
||||
exitCode: error ? typeof error.code === "number" ? error.code : -1 : 0, timedOut: !!error?.killed })));
|
||||
});
|
||||
const handle = createRemoteRunnerProcessLauncher({
|
||||
target: { kind: "remote", transport: "sandbox", environmentId: "environment", leaseId: "lease", remoteCwd: root },
|
||||
runner: { execute } as never, remoteBinary: "/runtime/runnerd", processIdentityPath: marker,
|
||||
stateDirectory: root, diagnosticsDirectory: join(root, "diagnostics"), runnerInstanceId: "runner-exit",
|
||||
})({ command: "/runtime/runnerd", args: [], cwd: root, environment: {} });
|
||||
try {
|
||||
if (failure === "exit") {
|
||||
await expect(handle.completion).resolves.toMatchObject({ code: null });
|
||||
expect(handle.isExitConfirmed?.()).toBe(true);
|
||||
} else {
|
||||
expect(await handle.completion.catch(readRemoteRunnerRecoveryError)).toEqual({ stage: "process_verification",
|
||||
reason: failure === "unreadable" ? "fingerprint_unavailable" : "process_generation_changed" });
|
||||
expect(handle.isExitConfirmed?.()).toBe(false);
|
||||
expect(() => process.kill(child.pid!, 0)).not.toThrow();
|
||||
}
|
||||
expect(await readFile(marker, "utf8")).toBe(savedMarker);
|
||||
expect(execute.mock.calls.filter(([request]) => request.args?.[2] === "paperclip-runner-launch")).toHaveLength(1);
|
||||
} finally {
|
||||
child.kill("SIGKILL");
|
||||
await exited;
|
||||
await handle.completion.catch(() => undefined);
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 15_000);
|
||||
|
||||
it("preserves a real listening process and its marker when a health RPC times out", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "runner-health-listener-"));
|
||||
const marker = join(root, "identity");
|
||||
|
||||
@@ -11217,7 +11217,7 @@ export async function verifyRemoteRunnerReattachment(input: {
|
||||
command: "sh",
|
||||
args: [
|
||||
"-c",
|
||||
`${REMOTE_RECOVERY_PROCESS_CHECK_SCRIPT}; if ${REMOTE_RUNNER_PROCESS_FINGERPRINT_SCRIPT}; then :; else exit 46; fi; test "$process_fingerprint" = "$5" || exit 47` +
|
||||
REMOTE_RECOVERY_PROCESS_CHECK_SCRIPT +
|
||||
(signal ? '; kill -"$6" "$pid"' : ""),
|
||||
"paperclip-runner-recovery-check",
|
||||
identityPath,
|
||||
@@ -11230,8 +11230,12 @@ export async function verifyRemoteRunnerReattachment(input: {
|
||||
bypassSession: true,
|
||||
},
|
||||
});
|
||||
return { alive: result.exitCode === 0 && !result.timedOut,
|
||||
reason: remoteRecoveryProbeReason("process_verification", result.exitCode, result.timedOut) };
|
||||
const alive = result.exitCode === 0 && !result.timedOut;
|
||||
const reason = remoteRecoveryProbeReason("process_verification", result.exitCode, result.timedOut);
|
||||
if (!alive && reason !== "process_not_running") {
|
||||
throw new RemoteRunnerRecoveryError({ stage: "process_verification", reason });
|
||||
}
|
||||
return { alive, reason };
|
||||
};
|
||||
const verified = await check(undefined, probeDeadline);
|
||||
if (!verified.alive) throw new RemoteRunnerRecoveryError({ stage: "process_verification", reason: verified.reason });
|
||||
@@ -11417,7 +11421,7 @@ export function createRemoteRunnerProcessLauncher(input: {
|
||||
command: "sh",
|
||||
args: [
|
||||
"-c",
|
||||
'set -eu; identity_path=$1; identity_nonce=$2; runner_instance_id=$3; child_script=$4; shift 4; umask 077; identity_dir=$(dirname -- "$identity_path"); mkdir -p -- "$identity_dir"; if command -v setsid >/dev/null 2>&1; then nohup setsid sh -c "$child_script" paperclip-runner-child "$identity_path" "$identity_nonce" "$runner_instance_id" "$@" </dev/null >/dev/null 2>&1 & else nohup sh -c "$child_script" paperclip-runner-child "$identity_path" "$identity_nonce" "$runner_instance_id" "$@" </dev/null >/dev/null 2>&1 & fi',
|
||||
'set -eu; node -e "process.kill(process.pid, 0)"; identity_path=$1; identity_nonce=$2; runner_instance_id=$3; child_script=$4; shift 4; umask 077; identity_dir=$(dirname -- "$identity_path"); mkdir -p -- "$identity_dir"; if command -v setsid >/dev/null 2>&1; then nohup setsid sh -c "$child_script" paperclip-runner-child "$identity_path" "$identity_nonce" "$runner_instance_id" "$@" </dev/null >/dev/null 2>&1 & else nohup sh -c "$child_script" paperclip-runner-child "$identity_path" "$identity_nonce" "$runner_instance_id" "$@" </dev/null >/dev/null 2>&1 & fi',
|
||||
"paperclip-runner-launch",
|
||||
input.processIdentityPath,
|
||||
identityNonce,
|
||||
@@ -11496,9 +11500,7 @@ export function createRemoteRunnerProcessLauncher(input: {
|
||||
command: "sh",
|
||||
args: [
|
||||
"-c",
|
||||
REMOTE_RECOVERY_PROCESS_CHECK_SCRIPT + (identity.processStartFingerprint
|
||||
? `; if ${REMOTE_RUNNER_PROCESS_FINGERPRINT_SCRIPT}; then :; else exit 46; fi; test "$process_fingerprint" = "$5" || exit 47`
|
||||
: ""),
|
||||
REMOTE_RECOVERY_PROCESS_CHECK_SCRIPT,
|
||||
"paperclip-runner-monitor",
|
||||
input.processIdentityPath,
|
||||
identityNonce,
|
||||
|
||||
@@ -18,6 +18,40 @@ function shellExit(script: string, args: string[]) {
|
||||
}
|
||||
|
||||
describe("remote recovery diagnostic shell receipts", () => {
|
||||
it("keeps permission-denied liveness uncertain", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "remote-probe-denied-"));
|
||||
const marker = join(root, "marker");
|
||||
const hook = join(root, "denied.cjs");
|
||||
try {
|
||||
await writeFile(marker, `nonce\n${process.pid}\n2026-10-26T00:00:00.000Z\nrunner\n`);
|
||||
await writeFile(hook, 'process.kill = () => { throw Object.assign(new Error("denied"), { code: "EPERM" }); };');
|
||||
const script = `NODE_OPTIONS="--require=${hook}"; export NODE_OPTIONS; kill() { return 1; }; ` + REMOTE_RECOVERY_PROCESS_CHECK_SCRIPT;
|
||||
expect(shellExit(script, [marker, "nonce", "runner", String(process.pid)])).toBe(46);
|
||||
expect(() => process.kill(process.pid, 0)).not.toThrow();
|
||||
} finally { await rm(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ fingerprint: "linux:abcd-1234:100", code: 45 },
|
||||
{ fingerprint: "linux:abcd-1234:101", code: 47 },
|
||||
{ fingerprint: "", code: 44 },
|
||||
])("requires the saved generation before accepting a zombie receipt (%j)", async ({ fingerprint, code }) => {
|
||||
const root = await mkdtemp(join(tmpdir(), "remote-probe-zombie-"));
|
||||
const marker = join(root, "marker");
|
||||
try {
|
||||
await writeFile(marker, `nonce\n${process.pid}\n2026-10-26T00:00:00.000Z\nrunner\n`);
|
||||
await writeFile(join(root, "cmdline"), "");
|
||||
await writeFile(join(root, "stat"), `${process.pid} (runner (child)) ${["Z", ...Array(18).fill("0"), 100].join(" ")}\n`);
|
||||
await writeFile(join(root, "boot_id"), "abcd-1234\n");
|
||||
const script = REMOTE_RECOVERY_PROCESS_CHECK_SCRIPT
|
||||
.replaceAll('"/proc/$pid/cmdline"', JSON.stringify(join(root, "cmdline")))
|
||||
.replaceAll('"/proc/" + pid + "/cmdline"', JSON.stringify(join(root, "cmdline")))
|
||||
.replaceAll('"/proc/" + pid + "/stat"', JSON.stringify(join(root, "stat")))
|
||||
.replaceAll('"/proc/sys/kernel/random/boot_id"', JSON.stringify(join(root, "boot_id")));
|
||||
expect(shellExit(script, [marker, "nonce", "runner", String(process.pid), fingerprint])).toBe(code);
|
||||
} finally { await rm(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
it("distinguishes missing, unsafe and valid marker reads without exposing their contents", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "remote-recovery-read-"));
|
||||
const marker = join(root, "marker");
|
||||
|
||||
@@ -69,8 +69,45 @@ export const REMOTE_RECOVERY_PROCESS_CHECK_SCRIPT = [
|
||||
'test -f "$identity_path" || exit 41',
|
||||
'{ IFS= read -r nonce; IFS= read -r pid; IFS= read -r started_at; IFS= read -r runner_id; } < "$identity_path" || exit 43',
|
||||
'test "$nonce" = "$expected_nonce" && test "$runner_id" = "$expected_runner_id" && test "$pid" = "$expected_pid" && test -n "$started_at" || exit 44',
|
||||
'kill -0 "$pid" 2>/dev/null || exit 45',
|
||||
'if test -r "/proc/$pid/cmdline"; then command_line=$(tr "\\000" "\\n" < "/proc/$pid/cmdline"); printf "%s\\n" "$command_line" | grep -Fqx -- "--runner-id" || exit 44; printf "%s\\n" "$command_line" | grep -Fqx -- "$expected_runner_id" || exit 44; fi',
|
||||
String.raw`node -e '
|
||||
const fs = require("node:fs");
|
||||
const [pid, runnerId, expectedGeneration] = process.argv.slice(1);
|
||||
if (!/^[1-9][0-9]*$/.test(pid) || !Number.isSafeInteger(Number(pid))) process.exit(44);
|
||||
function requireLiveProcess() {
|
||||
try { process.kill(Number(pid), 0); }
|
||||
catch (error) { process.exit(error.code === "ESRCH" ? 45 : 46); }
|
||||
}
|
||||
function verifyGeneration() {
|
||||
try {
|
||||
const stat = fs.readFileSync("/proc/" + pid + "/stat", "utf8");
|
||||
const end = stat.lastIndexOf(") ");
|
||||
const fields = stat.slice(end + 2).trim().split(/\s+/);
|
||||
const ticks = fields[19];
|
||||
const boot = fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
||||
if (end < 0 || !stat.startsWith(pid + " (") || !/^[0-9]+$/.test(ticks || "") || !/^[-a-fA-F0-9]+$/.test(boot)) process.exit(46);
|
||||
if ("linux:" + boot + ":" + ticks !== expectedGeneration) process.exit(47);
|
||||
if (fields[0] === "Z") process.exit(45);
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") requireLiveProcess();
|
||||
process.exit(46);
|
||||
}
|
||||
}
|
||||
requireLiveProcess();
|
||||
const commandPath = "/proc/" + pid + "/cmdline";
|
||||
if (process.platform !== "linux" && !expectedGeneration && !fs.existsSync(commandPath)) process.exit(0);
|
||||
let args;
|
||||
try { args = fs.readFileSync(commandPath, "utf8").split("\0"); }
|
||||
catch (error) {
|
||||
requireLiveProcess();
|
||||
if (error.code === "ENOENT" && expectedGeneration) verifyGeneration();
|
||||
process.exit(46);
|
||||
}
|
||||
if (expectedGeneration) verifyGeneration();
|
||||
if (!args.includes("--runner-id") || !args.includes(runnerId)) {
|
||||
requireLiveProcess();
|
||||
process.exit(44);
|
||||
}
|
||||
' "$pid" "$expected_runner_id"` + ' "${5:-}"',
|
||||
].join("; ");
|
||||
|
||||
export function remoteRecoveryProbeReason(stage: RemoteRunnerRecoveryDiagnostic["stage"], exitCode: number | null, timedOut: boolean): RemoteRunnerRecoveryDiagnostic["reason"] {
|
||||
|
||||
Reference in new issue
Block a user