fix(tunnel): preserve successor cloudflared PID
Make PID cleanup conditional on the exiting child still owning the PID file so a stale exit cannot erase a replacement tunnel's PID. Only null the in-memory process when the exiting child is current. Explicit disable keeps unconditional cleanup.
This commit is contained in:
@@ -239,8 +239,8 @@ export async function spawnCloudflared(tunnelToken) {
|
||||
});
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
cloudflaredProcess = null;
|
||||
clearPid();
|
||||
if (cloudflaredProcess === child) cloudflaredProcess = null;
|
||||
clearPid(child.pid);
|
||||
const wasConnected = resolved; // true = already connected successfully
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
@@ -372,8 +372,8 @@ export async function spawnQuickTunnel(localPort, onUrlUpdate) {
|
||||
});
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
cloudflaredProcess = null;
|
||||
clearPid();
|
||||
if (cloudflaredProcess === child) cloudflaredProcess = null;
|
||||
clearPid(child.pid);
|
||||
// Deliberate kill (restart/disable) — exit silently, no error noise
|
||||
if (intentionalKill) {
|
||||
intentionalKill = false;
|
||||
|
||||
@@ -16,8 +16,12 @@ export function loadPid() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function clearPid() {
|
||||
export function clearPid(expectedPid = null) {
|
||||
try {
|
||||
if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
|
||||
if (!fs.existsSync(PID_FILE)) return false;
|
||||
if (expectedPid !== null && loadPid() !== expectedPid) return false;
|
||||
fs.unlinkSync(PID_FILE);
|
||||
return true;
|
||||
} catch { /* ignore */ }
|
||||
return false;
|
||||
}
|
||||
|
||||
39
tests/unit/tunnel-pid-ownership.test.js
Normal file
39
tests/unit/tunnel-pid-ownership.test.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("cloudflared PID ownership", () => {
|
||||
let dataDir;
|
||||
|
||||
beforeEach(() => {
|
||||
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "9router-tunnel-pid-"));
|
||||
process.env.DATA_DIR = dataDir;
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.DATA_DIR;
|
||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("does not let an old child clear its successor PID", async () => {
|
||||
const { clearPid, loadPid, savePid } = await import("../../src/lib/tunnel/cloudflare/pid.js");
|
||||
|
||||
savePid(100);
|
||||
savePid(200);
|
||||
clearPid(100);
|
||||
|
||||
expect(loadPid()).toBe(200);
|
||||
|
||||
clearPid(200);
|
||||
expect(loadPid()).toBeNull();
|
||||
});
|
||||
|
||||
it("releases PID and process ownership for the exiting child only", () => {
|
||||
const source = fs.readFileSync(new URL("../../src/lib/tunnel/cloudflare/cloudflared.js", import.meta.url), "utf8");
|
||||
|
||||
expect(source.match(/clearPid\(child\.pid\)/g)).toHaveLength(2);
|
||||
expect(source.match(/cloudflaredProcess === child/g)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user