mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
fix: probe the recorded port for live-server liveness
greptile-apps[bot] re-raised the residual with a repro: a stale server.json pid reused by an unrelated node process passed the command-name check. The decisive signal is the recorded PORT: a real helper is listening on it, a pid squatter is not. hasLiveServer now probes 127.0.0.1:<port> (bash /dev/tcp, sync, ~ms, win32-guarded with the previous behavior); the multi-app preference test runs a real listener instead of faking liveness with a bare pid. This work was produced with AI assistance (Claude Code). Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Code
parent
26f54d15c2
commit
7fa25da98e
@@ -296,19 +296,33 @@ function readPointerEntries(repoRoot) {
|
||||
*/
|
||||
function hasLiveServer(appRoot) {
|
||||
let pid;
|
||||
let port;
|
||||
try {
|
||||
const info = JSON.parse(fs.readFileSync(path.join(appRoot, '.impeccable', 'live', 'server.json'), 'utf-8'));
|
||||
if (!info || typeof info.pid !== 'number') return false;
|
||||
pid = info.pid;
|
||||
port = Number(info.port);
|
||||
process.kill(pid, 0);
|
||||
} catch (err) {
|
||||
// EPERM: the process exists but is not signalable by this user.
|
||||
if (err?.code !== 'EPERM') return false;
|
||||
}
|
||||
if (process.platform === 'win32') return true; // no cheap portable command check
|
||||
// Liveness alone misclassifies a REUSED pid (helper died without removing
|
||||
// server.json, the OS handed the pid to something else, even another node
|
||||
// process). The decisive signal is the recorded PORT: a real helper is
|
||||
// listening on it, a pid squatter is not.
|
||||
if (Number.isInteger(port) && port > 0 && process.platform !== 'win32') {
|
||||
try {
|
||||
execFileSync('bash', ['-c', `exec 3<>/dev/tcp/127.0.0.1/${port}`], { timeout: 1500, stdio: 'ignore' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (process.platform === 'win32') return true; // no cheap portable probe
|
||||
try {
|
||||
const command = execFileSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf-8' });
|
||||
return /\b(node|bun|live-server)\b/.test(command);
|
||||
return /live-server|\b(node|bun)\b/.test(command);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { spawnSync } from 'node:child_process';
|
||||
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { createServer } from 'node:net';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
discoverAppCandidates,
|
||||
@@ -198,7 +199,7 @@ describe('review regressions: walk bounds', () => {
|
||||
});
|
||||
|
||||
describe('review regressions: multi-app pointer', () => {
|
||||
it('prefers the app whose live server is running over the last boot', () => {
|
||||
it('prefers the app whose live server is running over the last boot', async () => {
|
||||
const repo = realpathSync(mkdtempSync(join(tmpdir(), 'impeccable-roots-multi-')));
|
||||
try {
|
||||
mkdirSync(join(repo, '.git'), { recursive: true });
|
||||
@@ -211,13 +212,22 @@ describe('review regressions: multi-app pointer', () => {
|
||||
writeRootsManifest(a);
|
||||
writeRootsManifest(b); // B booted last: a naive pointer now points at B
|
||||
|
||||
// A's helper server is the one alive (this test process's pid).
|
||||
write(repo, 'siteA/.impeccable/live/server.json', JSON.stringify({ pid: process.pid, port: 1, token: 't' }));
|
||||
write(repo, 'siteB/.impeccable/live/server.json', JSON.stringify({ pid: 999999999, port: 2, token: 't' }));
|
||||
// A's helper server is the one alive: a real listener on a real port
|
||||
// (the liveness check probes the recorded port, so a bare pid is not
|
||||
// enough to count as running).
|
||||
const srv = createServer();
|
||||
await new Promise((resolve) => srv.listen(0, '127.0.0.1', resolve));
|
||||
try {
|
||||
const livePort = srv.address().port;
|
||||
write(repo, 'siteA/.impeccable/live/server.json', JSON.stringify({ pid: process.pid, port: livePort, token: 't' }));
|
||||
write(repo, 'siteB/.impeccable/live/server.json', JSON.stringify({ pid: 999999999, port: 2, token: 't' }));
|
||||
|
||||
const resolved = resolveLiveRoots(repo);
|
||||
assert.equal(resolved.source, 'pointer');
|
||||
assert.equal(resolved.manifest.appRoot, join(repo, 'siteA'));
|
||||
const resolved = resolveLiveRoots(repo);
|
||||
assert.equal(resolved.source, 'pointer');
|
||||
assert.equal(resolved.manifest.appRoot, join(repo, 'siteA'));
|
||||
} finally {
|
||||
await new Promise((resolve) => srv.close(resolve));
|
||||
}
|
||||
} finally {
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user