refactor(daemon): share runtime and service probe helpers

This commit is contained in:
Peter Steinberger
2026-02-22 21:18:30 +00:00
parent e029f78447
commit 06b0a60bef
12 changed files with 241 additions and 107 deletions

View File

@@ -8,6 +8,7 @@ import {
resolvePairingPaths,
writeJsonAtomic,
} from "./pairing-files.js";
import { rejectPendingPairingRequest } from "./pairing-pending.js";
import { generatePairingToken, verifyPairingToken } from "./pairing-token.js";
export type DevicePairingPendingRequest = {
@@ -382,14 +383,13 @@ export async function rejectDevicePairing(
baseDir?: string,
): Promise<{ requestId: string; deviceId: string } | null> {
return await withLock(async () => {
const state = await loadState(baseDir);
const pending = state.pendingById[requestId];
if (!pending) {
return null;
}
delete state.pendingById[requestId];
await persistState(state, baseDir);
return { requestId, deviceId: pending.deviceId };
return await rejectPendingPairingRequest({
requestId,
idKey: "deviceId",
loadState: () => loadState(baseDir),
persistState: (state) => persistState(state, baseDir),
getId: (pending) => pending.deviceId,
});
});
}

View File

@@ -7,6 +7,7 @@ import {
upsertPendingPairingRequest,
writeJsonAtomic,
} from "./pairing-files.js";
import { rejectPendingPairingRequest } from "./pairing-pending.js";
import { generatePairingToken, verifyPairingToken } from "./pairing-token.js";
export type NodePairingPendingRequest = {
@@ -194,14 +195,13 @@ export async function rejectNodePairing(
baseDir?: string,
): Promise<{ requestId: string; nodeId: string } | null> {
return await withLock(async () => {
const state = await loadState(baseDir);
const pending = state.pendingById[requestId];
if (!pending) {
return null;
}
delete state.pendingById[requestId];
await persistState(state, baseDir);
return { requestId, nodeId: pending.nodeId };
return await rejectPendingPairingRequest({
requestId,
idKey: "nodeId",
loadState: () => loadState(baseDir),
persistState: (state) => persistState(state, baseDir),
getId: (pending) => pending.nodeId,
});
});
}

View File

@@ -0,0 +1,27 @@
type PendingState<TPending> = {
pendingById: Record<string, TPending>;
};
export async function rejectPendingPairingRequest<
TPending,
TState extends PendingState<TPending>,
TIdKey extends string,
>(params: {
requestId: string;
idKey: TIdKey;
loadState: () => Promise<TState>;
persistState: (state: TState) => Promise<void>;
getId: (pending: TPending) => string;
}): Promise<({ requestId: string } & Record<TIdKey, string>) | null> {
const state = await params.loadState();
const pending = state.pendingById[params.requestId];
if (!pending) {
return null;
}
delete state.pendingById[params.requestId];
await params.persistState(state);
return {
requestId: params.requestId,
[params.idKey]: params.getId(pending),
} as { requestId: string } & Record<TIdKey, string>;
}

View File

@@ -1,8 +1,8 @@
import net from "node:net";
import { runCommandWithTimeout } from "../process/exec.js";
import { isErrno } from "./errors.js";
import { buildPortHints } from "./ports-format.js";
import { resolveLsofCommand } from "./ports-lsof.js";
import { tryListenOnPort } from "./ports-probe.js";
import type { PortListener, PortUsage, PortUsageStatus } from "./ports-types.js";
type CommandResult = {
@@ -227,15 +227,7 @@ async function readWindowsListeners(
async function tryListenOnHost(port: number, host: string): Promise<PortUsageStatus | "skip"> {
try {
await new Promise<void>((resolve, reject) => {
const tester = net
.createServer()
.once("error", (err) => reject(err))
.once("listening", () => {
tester.close(() => resolve());
})
.listen({ port, host, exclusive: true });
});
await tryListenOnPort({ port, host, exclusive: true });
return "free";
} catch (err) {
if (isErrno(err) && err.code === "EADDRINUSE") {

24
src/infra/ports-probe.ts Normal file
View File

@@ -0,0 +1,24 @@
import net from "node:net";
export async function tryListenOnPort(params: {
port: number;
host?: string;
exclusive?: boolean;
}): Promise<void> {
const listenOptions: net.ListenOptions = { port: params.port };
if (params.host) {
listenOptions.host = params.host;
}
if (typeof params.exclusive === "boolean") {
listenOptions.exclusive = params.exclusive;
}
await new Promise<void>((resolve, reject) => {
const tester = net
.createServer()
.once("error", (err) => reject(err))
.once("listening", () => {
tester.close(() => resolve());
})
.listen(listenOptions);
});
}

View File

@@ -1,4 +1,3 @@
import net from "node:net";
import { danger, info, shouldLogVerbose, warn } from "../globals.js";
import { logDebug } from "../logger.js";
import type { RuntimeEnv } from "../runtime.js";
@@ -6,6 +5,7 @@ import { defaultRuntime } from "../runtime.js";
import { isErrno } from "./errors.js";
import { formatPortDiagnostics } from "./ports-format.js";
import { inspectPortUsage } from "./ports-inspect.js";
import { tryListenOnPort } from "./ports-probe.js";
import type { PortListener, PortListenerKind, PortUsage, PortUsageStatus } from "./ports-types.js";
class PortInUseError extends Error {
@@ -31,15 +31,7 @@ export async function describePortOwner(port: number): Promise<string | undefine
export async function ensurePortAvailable(port: number): Promise<void> {
// Detect EADDRINUSE early with a friendly message.
try {
await new Promise<void>((resolve, reject) => {
const tester = net
.createServer()
.once("error", (err) => reject(err))
.once("listening", () => {
tester.close(() => resolve());
})
.listen(port);
});
await tryListenOnPort({ port });
} catch (err) {
if (isErrno(err) && err.code === "EADDRINUSE") {
throw new PortInUseError(port);