| import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
| import { join } from "node:path";
|
| import { resolveDataDir } from "../data-dir.mjs";
|
|
|
| const SERVICES = ["server", "mitm", "tunnel/cloudflared", "tunnel/tailscale"];
|
|
|
| function getServicePidPath(service) {
|
| return join(resolveDataDir(), service, ".pid");
|
| }
|
|
|
| export function writePidFile(service, pid) {
|
| try {
|
| const dir = join(resolveDataDir(), service);
|
| mkdirSync(dir, { recursive: true });
|
| writeFileSync(getServicePidPath(service), String(pid), "utf8");
|
| return true;
|
| } catch {
|
| return false;
|
| }
|
| }
|
|
|
| export function readPidFile(service) {
|
| try {
|
| const file = getServicePidPath(service);
|
| if (!existsSync(file)) return null;
|
| const pid = parseInt(readFileSync(file, "utf8").trim(), 10);
|
| return Number.isFinite(pid) ? pid : null;
|
| } catch {
|
| return null;
|
| }
|
| }
|
|
|
| export function cleanupPidFile(service) {
|
| try {
|
| unlinkSync(getServicePidPath(service));
|
| } catch {}
|
| }
|
|
|
| export function killAllSubprocesses() {
|
| for (const service of SERVICES) {
|
| const pid = readPidFile(service);
|
| if (!pid) continue;
|
| try {
|
| process.kill(pid, "SIGTERM");
|
| } catch {}
|
| cleanupPidFile(service);
|
| }
|
| }
|
|
|
| export function isPidRunning(pid) {
|
| if (!pid) return false;
|
| try {
|
| process.kill(pid, 0);
|
| return true;
|
| } catch {
|
| return false;
|
| }
|
| }
|
|
|
| export function sleep(ms) {
|
| return new Promise((resolve) => setTimeout(resolve, ms));
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| export async function waitForServer(port, timeout = 60000) {
|
| const start = Date.now();
|
| let tcpListeningSince = null;
|
| while (Date.now() - start < timeout) {
|
| try {
|
| const res = await fetch(`http://localhost:${port}/api/monitoring/health`, {
|
| signal: AbortSignal.timeout(2000),
|
| });
|
| if (res.ok) return true;
|
|
|
|
|
| if (tcpListeningSince === null) tcpListeningSince = Date.now();
|
| } catch {
|
| const listening = await isPortListening(port).catch(() => false);
|
| if (listening) {
|
| if (tcpListeningSince === null) tcpListeningSince = Date.now();
|
| if (Date.now() - tcpListeningSince >= 3000) return true;
|
| } else {
|
| tcpListeningSince = null;
|
| }
|
| }
|
| await sleep(500);
|
| }
|
| return false;
|
| }
|
|
|
| async function isPortListening(port) {
|
| const net = await import("node:net");
|
| return new Promise((resolve) => {
|
| const socket = net.connect({ host: "127.0.0.1", port, timeout: 1000 });
|
| const finish = (ok) => {
|
| try {
|
| socket.destroy();
|
| } catch {}
|
| resolve(ok);
|
| };
|
| socket.once("connect", () => finish(true));
|
| socket.once("error", () => finish(false));
|
| socket.once("timeout", () => finish(false));
|
| });
|
| }
|
|
|