From c00b46f781f7f8a425bbc7655d6d6997d2751fbf Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Thu, 28 May 2026 02:09:26 +0000 Subject: [PATCH] fix: replace nc -z -U probe with Node net.Socket.connect child process (PILOT-186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit probeDaemonLiveSync used BSD nc(1) which returns exit 1 on macOS even for connectable UNIX sockets. Combined with the ESM-incompatible inline require('node:child_process'), this could cause the seeder to falsely believe no daemon is running — and overwrite a live binary. Mirror the async probe's net.Socket.connect approach via a synchronous Node child process. This is consistent across platforms and avoids the require-in-ESM hazard by importing spawnSync at the top of the module. --- src/runtime.ts | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/runtime.ts b/src/runtime.ts index 15c24c2..03b7e89 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -30,6 +30,7 @@ import { accessSync, constants as fsConstants, } from 'node:fs'; +import { spawnSync } from 'node:child_process'; import { homedir, arch as osArch, platform as osPlatform } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -210,20 +211,36 @@ async function probeDaemonLive(timeoutMs = 200): Promise { }); } -/** Synchronous probe used by the seeder. Loops on a short setImmediate. */ +/** Synchronous probe used by the seeder. Mirrors the async probe's + * net.Socket.connect logic via a Node child process so the result is + * consistent across platforms. The old `nc -z -U` probe returned exit + * status 1 on macOS even for connectable sockets, causing the seeder to + * incorrectly believe the daemon was down. (PILOT-186) */ function probeDaemonLiveSync(): boolean { const sockPath = readSocketPath(); if (!existsSync(sockPath)) return false; - // Best-effort sync: try connecting via a child process. Falls back to - // "assume not running" if we can't decide quickly. + + // Use the same Node.js runtime and net.Socket.connect semantics as the + // async path — a child process is the only safe way to get a synchronous + // socket-connect result without relying on platform-specific nc(1). try { - const { spawnSync } = require('node:child_process') as typeof import('node:child_process'); - // `nc -z -U ` is the cleanest sync probe; fall back to true if nc is missing. - const r = spawnSync('nc', ['-z', '-U', sockPath], { timeout: 250 }); - if (r.error) return existsSync(sockPath); // nc missing — be conservative + const escaped = sockPath.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + const r = spawnSync( + process.execPath, + [ + '-e', + `var n=require("net"),s=new n.Socket;` + + `s.setTimeout(250);` + + `s.on("error",()=>process.exit(1));` + + `s.on("connect",()=>{s.destroy();process.exit(0)});` + + `s.connect('${escaped}')`, + ], + { timeout: 500 }, + ); + if (r.error) return existsSync(sockPath); // node missing — conservative fallback return r.status === 0; } catch { - // Conservative: if a socket file is present, assume the daemon is up. + // spawnSync itself threw (shouldn't happen). Conservative fallback. return existsSync(sockPath); } }