run402 4.58.0 → 4.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/git-remote-run402.mjs +159 -834
- package/gitvault-surface.json +1 -1
- package/lib/daemon-path.mjs +99 -0
- package/lib/doctor.mjs +45 -0
- package/lib/gitvault-daemon-run.mjs +22 -0
- package/lib/gitvault-daemon.mjs +263 -0
- package/lib/remote-helper-session.mjs +867 -0
- package/lib/repos.mjs +76 -0
- package/package.json +2 -1
- package/sdk/dist/namespaces/pay.d.ts.map +1 -1
- package/sdk/dist/namespaces/pay.js +12 -1
- package/sdk/dist/namespaces/pay.js.map +1 -1
- package/sdk/dist/node/gitvault-prewarm.d.ts +1 -0
- package/sdk/dist/node/gitvault-prewarm.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-prewarm.js +30 -6
- package/sdk/dist/node/gitvault-prewarm.js.map +1 -1
- package/sdk/dist/node/http-dispatcher.d.ts +26 -0
- package/sdk/dist/node/http-dispatcher.d.ts.map +1 -0
- package/sdk/dist/node/http-dispatcher.js +209 -0
- package/sdk/dist/node/http-dispatcher.js.map +1 -0
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +5 -1
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
- package/sdk/dist/node/paid-fetch.js +10 -8
- package/sdk/dist/node/paid-fetch.js.map +1 -1
package/gitvault-surface.json
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon identity + transport addressing (gitvault-persistent-helper D4/D5)
|
|
3
|
+
* — the ONE module the thin client's fast path may import beside node
|
|
4
|
+
* builtins (pinned by the import-graph gate test), so it must stay tiny.
|
|
5
|
+
*
|
|
6
|
+
* The socket lives INSIDE the client configuration directory — the same
|
|
7
|
+
* 0700 trust boundary that already holds the allowance private key and the
|
|
8
|
+
* gitvault keystore — and its path is keyed by CLI version, so a CLI
|
|
9
|
+
* upgrade resolves a NEW path: the new client never reaches the old daemon,
|
|
10
|
+
* which idles out on its own. Windows uses a named pipe whose name carries
|
|
11
|
+
* a hash of the config dir (pipes have no filesystem home) plus the same
|
|
12
|
+
* version key.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { homedir, tmpdir, userInfo } from "node:os";
|
|
16
|
+
import { join, dirname } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
|
|
20
|
+
/** Mirrors core config's base-dir resolution without importing its graph. */
|
|
21
|
+
export function configBaseDir() {
|
|
22
|
+
return process.env.RUN402_CONFIG_DIR || join(homedir(), ".config", "run402");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let cachedVersion = null;
|
|
26
|
+
export function cliVersion() {
|
|
27
|
+
if (cachedVersion) return cachedVersion;
|
|
28
|
+
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"));
|
|
29
|
+
cachedVersion = String(pkg.version);
|
|
30
|
+
return cachedVersion;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Bump when the client↔daemon frame protocol changes shape. */
|
|
34
|
+
export const DAEMON_PROTOCOL_VERSION = 1;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Unix domain sockets cap `sun_path` around 104 bytes (macOS) — a long
|
|
38
|
+
* RUN402_CONFIG_DIR (test tmpdirs, deep homes) makes the config-dir socket
|
|
39
|
+
* un-listenable, which the fallback would silently mask forever. So the
|
|
40
|
+
* home is the config dir when the path FITS, else an owner-only per-user
|
|
41
|
+
* directory under tmpdir whose socket NAME still keys on the config dir —
|
|
42
|
+
* the trust boundary (0700 dir the daemon creates and verifies) travels
|
|
43
|
+
* with it either way.
|
|
44
|
+
*/
|
|
45
|
+
const SUN_PATH_BUDGET = 100;
|
|
46
|
+
|
|
47
|
+
function fallbackDir() {
|
|
48
|
+
let who;
|
|
49
|
+
try {
|
|
50
|
+
who = String(userInfo().uid ?? userInfo().username);
|
|
51
|
+
} catch {
|
|
52
|
+
who = "u";
|
|
53
|
+
}
|
|
54
|
+
return join(tmpdir(), `run402-gvd-${who}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The socket's full address — preferred home when it fits the budget, else the owner-only fallback with a config-keyed name. */
|
|
58
|
+
function resolveSocket() {
|
|
59
|
+
const version = cliVersion().replace(/[^0-9A-Za-z.]/g, "_");
|
|
60
|
+
if (process.platform === "win32") {
|
|
61
|
+
const key = createHash("sha256").update(configBaseDir()).digest("hex").slice(0, 12);
|
|
62
|
+
return { dir: null, path: `\\\\.\\pipe\\run402-gvd-${key}-${version}` };
|
|
63
|
+
}
|
|
64
|
+
const preferredDir = join(configBaseDir(), "daemon");
|
|
65
|
+
const preferred = join(preferredDir, `gv-${version}.sock`);
|
|
66
|
+
if (Buffer.byteLength(preferred) <= SUN_PATH_BUDGET) return { dir: preferredDir, path: preferred };
|
|
67
|
+
const dir = fallbackDir();
|
|
68
|
+
const key = createHash("sha256").update(configBaseDir()).digest("hex").slice(0, 12);
|
|
69
|
+
return { dir, path: join(dir, `g${key}${version.replace(/\./g, "")}.sock`) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Directory holding the socket (created 0700 by the daemon). */
|
|
73
|
+
export function daemonDir() {
|
|
74
|
+
return resolveSocket().dir;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function daemonSocketPath() {
|
|
78
|
+
return resolveSocket().path;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function daemonRunnerPath() {
|
|
82
|
+
return join(dirname(fileURLToPath(import.meta.url)), "gitvault-daemon-run.mjs");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The env allowlist a session forwards (D1): everything run402- or
|
|
87
|
+
* git-shaped, plus the locale pair git itself respects. Deliberately NOT
|
|
88
|
+
* the whole environment — the daemon runs with its own HOME/PATH (same
|
|
89
|
+
* user, same machine), and forwarding arbitrary env would make the daemon's
|
|
90
|
+
* behavior depend on whichever client spoke last in ways nothing re-reads.
|
|
91
|
+
*/
|
|
92
|
+
export function forwardableEnv(env) {
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const [k, v] of Object.entries(env)) {
|
|
95
|
+
if (v === undefined) continue;
|
|
96
|
+
if (k.startsWith("RUN402_") || k.startsWith("GIT_") || k === "LC_ALL" || k === "LANG") out[k] = v;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
package/lib/doctor.mjs
CHANGED
|
@@ -602,6 +602,50 @@ export async function run(sub, args = []) {
|
|
|
602
602
|
// all of that (the resolver's own top tier), same as every other gitvault
|
|
603
603
|
// verb's `--project`.
|
|
604
604
|
if (wanted("gitvault")) {
|
|
605
|
+
// gitvault-persistent-helper: a bounded LOCAL probe of the resident
|
|
606
|
+
// helper engine — {running:false} is a fine answer, never a finding
|
|
607
|
+
// (the daemon is an accelerator, not a dependency).
|
|
608
|
+
const daemonInfo = await (async () => {
|
|
609
|
+
try {
|
|
610
|
+
const { daemonSocketPath } = await import("./daemon-path.mjs");
|
|
611
|
+
const { connect: netConnect } = await import("node:net");
|
|
612
|
+
return await new Promise((resolve) => {
|
|
613
|
+
let settled = false;
|
|
614
|
+
const done = (v) => {
|
|
615
|
+
if (!settled) {
|
|
616
|
+
settled = true;
|
|
617
|
+
resolve(v);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
const socket = netConnect(daemonSocketPath());
|
|
621
|
+
const timer = setTimeout(() => {
|
|
622
|
+
socket.destroy();
|
|
623
|
+
done({ running: false });
|
|
624
|
+
}, 500);
|
|
625
|
+
let data = "";
|
|
626
|
+
socket.on("data", (c) => {
|
|
627
|
+
data += c.toString("utf8");
|
|
628
|
+
const nl = data.indexOf("\n");
|
|
629
|
+
if (nl === -1) return;
|
|
630
|
+
clearTimeout(timer);
|
|
631
|
+
try {
|
|
632
|
+
const { t: _t, ...rest } = JSON.parse(data.slice(0, nl));
|
|
633
|
+
done({ running: true, ...rest });
|
|
634
|
+
} catch {
|
|
635
|
+
done({ running: false });
|
|
636
|
+
}
|
|
637
|
+
socket.end();
|
|
638
|
+
});
|
|
639
|
+
socket.once("error", () => {
|
|
640
|
+
clearTimeout(timer);
|
|
641
|
+
done({ running: false });
|
|
642
|
+
});
|
|
643
|
+
socket.once("connect", () => socket.write('{"t":"status"}\n'));
|
|
644
|
+
});
|
|
645
|
+
} catch {
|
|
646
|
+
return { running: false };
|
|
647
|
+
}
|
|
648
|
+
})();
|
|
605
649
|
const target = await resolveGitvaultTarget({ repoDir: process.cwd(), explicitProjectId: projectOverride ?? undefined });
|
|
606
650
|
const projectId = target.project_id ?? null;
|
|
607
651
|
const repoId = target.repo_id ?? null;
|
|
@@ -633,6 +677,7 @@ export async function run(sub, args = []) {
|
|
|
633
677
|
// already proved a second covering recipient, so the hint switches
|
|
634
678
|
// to the durability sentence instead of the terminal-loss claim.
|
|
635
679
|
covering_recipients: gv.covering_recipients ?? null,
|
|
680
|
+
daemon: daemonInfo,
|
|
636
681
|
};
|
|
637
682
|
const gaps = [];
|
|
638
683
|
// The one that actually breaks the next deploy: the project demands a
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon process entry (gitvault-persistent-helper) — spawned detached by
|
|
3
|
+
* the thin client's fallback path; never invoked by users directly. Enables
|
|
4
|
+
* the compile cache (the daemon compiles the heavy graph exactly once, so
|
|
5
|
+
* this mostly benefits the NEXT daemon after an upgrade), then runs the
|
|
6
|
+
* listener. A second copy losing the listen race exits quietly — the socket
|
|
7
|
+
* winner serves everyone.
|
|
8
|
+
*/
|
|
9
|
+
import * as nodeModule from "node:module";
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
nodeModule.enableCompileCache?.();
|
|
13
|
+
} catch {
|
|
14
|
+
/* silent by contract */
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { runDaemon } = await import("./gitvault-daemon.mjs");
|
|
18
|
+
try {
|
|
19
|
+
await runDaemon();
|
|
20
|
+
} catch {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The resident helper engine (gitvault-persistent-helper D2–D6).
|
|
3
|
+
*
|
|
4
|
+
* Holds the WARM things — the loaded session module (whose top-level await
|
|
5
|
+
* pulls the whole SDK graph), the owned dispatcher's live h2 connection,
|
|
6
|
+
* signer precomputation, the paid stack — and re-reads the SMALL things
|
|
7
|
+
* (keystore, pins, wallet binding, allowance, env-derived config) fresh per
|
|
8
|
+
* forwarded session, so no cache-invalidation class exists (D2).
|
|
9
|
+
*
|
|
10
|
+
* SINGLE-SESSION BY DESIGN (D6 as applied): a session is {cwd, env}
|
|
11
|
+
* applied to THIS process (`process.chdir` + an env allowlist swap), and
|
|
12
|
+
* the SDK reads both at call time throughout — two concurrent sessions
|
|
13
|
+
* with different cwd/env would race process-global state. Concurrent git
|
|
14
|
+
* helpers are rare; a `busy` rejection makes the second client fall back
|
|
15
|
+
* in-process, which is exactly today's behavior. Correct and boring beats
|
|
16
|
+
* clever here.
|
|
17
|
+
*
|
|
18
|
+
* Lifecycle (D4): idle exit after 15 minutes; the socket path is keyed by
|
|
19
|
+
* CLI version so an upgraded client never reaches this daemon; a `hello`
|
|
20
|
+
* carrying a different version or protocol is rejected (the client falls
|
|
21
|
+
* back and spawns a fresh daemon at ITS path). Trust boundary (D5): the
|
|
22
|
+
* socket lives in the 0700 config dir, 0600 where the OS honors socket
|
|
23
|
+
* modes; no network listener exists or ever will in this module.
|
|
24
|
+
*/
|
|
25
|
+
import net from "node:net";
|
|
26
|
+
import { mkdirSync, unlinkSync, chmodSync } from "node:fs";
|
|
27
|
+
import { daemonDir, daemonSocketPath, DAEMON_PROTOCOL_VERSION, cliVersion, forwardableEnv } from "./daemon-path.mjs";
|
|
28
|
+
import { PassThrough } from "node:stream";
|
|
29
|
+
|
|
30
|
+
const IDLE_EXIT_MS = 15 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
function frame(obj) {
|
|
33
|
+
return `${JSON.stringify(obj)}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Swap the allowlisted env to the session's view; returns a restore fn. */
|
|
37
|
+
function applySessionEnv(sessionEnv) {
|
|
38
|
+
const mine = forwardableEnv(process.env);
|
|
39
|
+
const keys = new Set([...Object.keys(mine), ...Object.keys(sessionEnv)]);
|
|
40
|
+
for (const k of keys) {
|
|
41
|
+
if (k in sessionEnv) process.env[k] = sessionEnv[k];
|
|
42
|
+
else delete process.env[k];
|
|
43
|
+
}
|
|
44
|
+
return () => {
|
|
45
|
+
for (const k of new Set([...keys, ...Object.keys(forwardableEnv(process.env))])) {
|
|
46
|
+
if (k in mine) process.env[k] = mine[k];
|
|
47
|
+
else delete process.env[k];
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function runDaemon() {
|
|
53
|
+
// Load the heavy module ONCE — this is the entire point of residency.
|
|
54
|
+
// Its top-level await pulls the SDK graph; the prewarm dials the API
|
|
55
|
+
// origin so the first forwarded session rides a warm h2 connection.
|
|
56
|
+
const { prewarmGitvaultConnection } = await import("../sdk/dist/node/gitvault-prewarm.js");
|
|
57
|
+
prewarmGitvaultConnection();
|
|
58
|
+
const { runHelperSession } = await import("./remote-helper-session.mjs");
|
|
59
|
+
|
|
60
|
+
const socketPath = daemonSocketPath();
|
|
61
|
+
if (process.platform !== "win32") {
|
|
62
|
+
mkdirSync(daemonDir(), { recursive: true, mode: 0o700 });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let busy = false;
|
|
66
|
+
let sessionsServed = 0;
|
|
67
|
+
const startedAt = Date.now();
|
|
68
|
+
let idleTimer = null;
|
|
69
|
+
const armIdle = () => {
|
|
70
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
71
|
+
idleTimer = setTimeout(() => {
|
|
72
|
+
if (busy) {
|
|
73
|
+
// Never exit under a live session — re-arm and check again later.
|
|
74
|
+
armIdle();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
server.close();
|
|
79
|
+
if (process.platform !== "win32") unlinkSync(socketPath);
|
|
80
|
+
} catch {
|
|
81
|
+
/* exiting anyway */
|
|
82
|
+
}
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}, IDLE_EXIT_MS);
|
|
85
|
+
idleTimer.unref?.();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const server = net.createServer((socket) => {
|
|
89
|
+
socket.setNoDelay(true);
|
|
90
|
+
armIdle();
|
|
91
|
+
let buf = "";
|
|
92
|
+
let session = null; // { stdin, restoreEnv, restoreCwd, restoreWrites }
|
|
93
|
+
|
|
94
|
+
const send = (obj) => {
|
|
95
|
+
try {
|
|
96
|
+
socket.write(frame(obj));
|
|
97
|
+
} catch {
|
|
98
|
+
/* client gone — session teardown happens on 'close' */
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const teardown = () => {
|
|
103
|
+
if (!session) return;
|
|
104
|
+
const s = session;
|
|
105
|
+
session = null;
|
|
106
|
+
busy = false;
|
|
107
|
+
try {
|
|
108
|
+
s.stdin.end();
|
|
109
|
+
} catch {
|
|
110
|
+
/* already ended */
|
|
111
|
+
}
|
|
112
|
+
s.restoreWrites();
|
|
113
|
+
s.restoreEnv();
|
|
114
|
+
s.restoreCwd();
|
|
115
|
+
armIdle();
|
|
116
|
+
};
|
|
117
|
+
socket.on("close", teardown);
|
|
118
|
+
socket.on("error", () => socket.destroy());
|
|
119
|
+
|
|
120
|
+
socket.on("data", (chunk) => {
|
|
121
|
+
buf += chunk.toString("utf8");
|
|
122
|
+
for (;;) {
|
|
123
|
+
const nl = buf.indexOf("\n");
|
|
124
|
+
if (nl === -1) return;
|
|
125
|
+
const line = buf.slice(0, nl);
|
|
126
|
+
buf = buf.slice(nl + 1);
|
|
127
|
+
let msg;
|
|
128
|
+
try {
|
|
129
|
+
msg = JSON.parse(line);
|
|
130
|
+
} catch {
|
|
131
|
+
socket.destroy();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (msg.t === "hello") {
|
|
135
|
+
if (msg.proto !== DAEMON_PROTOCOL_VERSION || msg.version !== cliVersion()) {
|
|
136
|
+
send({ t: "reject", reason: "version" });
|
|
137
|
+
socket.end();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (busy) {
|
|
141
|
+
// D6: one session at a time — the client falls back in-process,
|
|
142
|
+
// which is exactly the pre-daemon behavior.
|
|
143
|
+
send({ t: "reject", reason: "busy" });
|
|
144
|
+
socket.end();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
busy = true;
|
|
148
|
+
sessionsServed += 1;
|
|
149
|
+
const stdin = new PassThrough();
|
|
150
|
+
const restoreEnv = applySessionEnv(msg.env ?? {});
|
|
151
|
+
const prevCwd = process.cwd();
|
|
152
|
+
let restoreCwd = () => {};
|
|
153
|
+
try {
|
|
154
|
+
process.chdir(msg.cwd);
|
|
155
|
+
restoreCwd = () => {
|
|
156
|
+
try {
|
|
157
|
+
process.chdir(prevCwd);
|
|
158
|
+
} catch {
|
|
159
|
+
/* prev dir may be gone; daemon cwd is inert between sessions */
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
} catch {
|
|
163
|
+
restoreEnv();
|
|
164
|
+
busy = false;
|
|
165
|
+
send({ t: "reject", reason: "cwd" });
|
|
166
|
+
socket.end();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Redirect BOTH write streams for the session's duration: the
|
|
170
|
+
// session's own out()/note() and every SDK trace/warn line reach
|
|
171
|
+
// the CLIENT, exactly as they reach a standalone helper's pipes.
|
|
172
|
+
const realOut = process.stdout.write.bind(process.stdout);
|
|
173
|
+
const realErr = process.stderr.write.bind(process.stderr);
|
|
174
|
+
const toBuffer = (data, rest) => (typeof data === "string" ? Buffer.from(data, typeof rest[0] === "string" ? rest[0] : "utf8") : Buffer.from(data));
|
|
175
|
+
process.stdout.write = (data, ...rest) => {
|
|
176
|
+
send({ t: "out", d: toBuffer(data, rest).toString("base64") });
|
|
177
|
+
rest.find((a) => typeof a === "function")?.();
|
|
178
|
+
return true;
|
|
179
|
+
};
|
|
180
|
+
process.stderr.write = (data, ...rest) => {
|
|
181
|
+
send({ t: "err", d: toBuffer(data, rest).toString("base64") });
|
|
182
|
+
rest.find((a) => typeof a === "function")?.();
|
|
183
|
+
return true;
|
|
184
|
+
};
|
|
185
|
+
const restoreWrites = () => {
|
|
186
|
+
process.stdout.write = realOut;
|
|
187
|
+
process.stderr.write = realErr;
|
|
188
|
+
};
|
|
189
|
+
session = { stdin, restoreEnv, restoreCwd, restoreWrites };
|
|
190
|
+
send({ t: "ready" });
|
|
191
|
+
runHelperSession(Array.isArray(msg.argv) ? msg.argv : [], { stdin })
|
|
192
|
+
.then((code) => {
|
|
193
|
+
teardown();
|
|
194
|
+
send({ t: "exit", code });
|
|
195
|
+
socket.end();
|
|
196
|
+
})
|
|
197
|
+
.catch(() => {
|
|
198
|
+
teardown();
|
|
199
|
+
send({ t: "exit", code: 1 });
|
|
200
|
+
socket.end();
|
|
201
|
+
});
|
|
202
|
+
} else if (msg.t === "in") {
|
|
203
|
+
session?.stdin.write(Buffer.from(msg.d, "base64"));
|
|
204
|
+
} else if (msg.t === "in_end") {
|
|
205
|
+
session?.stdin.end();
|
|
206
|
+
} else if (msg.t === "status") {
|
|
207
|
+
send({ t: "status", version: cliVersion(), proto: DAEMON_PROTOCOL_VERSION, pid: process.pid, started_at: startedAt, sessions_served: sessionsServed, busy });
|
|
208
|
+
socket.end();
|
|
209
|
+
} else if (msg.t === "stop") {
|
|
210
|
+
send({ t: "stopping" });
|
|
211
|
+
socket.end();
|
|
212
|
+
try {
|
|
213
|
+
server.close();
|
|
214
|
+
if (process.platform !== "win32") unlinkSync(socketPath);
|
|
215
|
+
} catch {
|
|
216
|
+
/* exiting anyway */
|
|
217
|
+
}
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await new Promise((resolve, reject) => {
|
|
225
|
+
const tryListen = (attempt) => {
|
|
226
|
+
server.once("error", (err) => {
|
|
227
|
+
if (err.code === "EADDRINUSE" && attempt === 0 && process.platform !== "win32") {
|
|
228
|
+
// A live daemon OR a stale socket file from a crash. Probe it: a
|
|
229
|
+
// refused connection means stale — unlink and take the address.
|
|
230
|
+
const probe = net.connect(socketPath);
|
|
231
|
+
probe.once("connect", () => {
|
|
232
|
+
probe.destroy();
|
|
233
|
+
reject(new Error("daemon already running"));
|
|
234
|
+
});
|
|
235
|
+
probe.once("error", () => {
|
|
236
|
+
try {
|
|
237
|
+
unlinkSync(socketPath);
|
|
238
|
+
} catch {
|
|
239
|
+
/* raced */
|
|
240
|
+
}
|
|
241
|
+
tryListen(1);
|
|
242
|
+
});
|
|
243
|
+
} else {
|
|
244
|
+
reject(err);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
server.listen(socketPath, () => {
|
|
248
|
+
if (process.platform !== "win32") {
|
|
249
|
+
try {
|
|
250
|
+
chmodSync(socketPath, 0o600);
|
|
251
|
+
} catch {
|
|
252
|
+
/* the 0700 parent dir is the real boundary */
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
resolve();
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
tryListen(0);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
armIdle();
|
|
262
|
+
return server;
|
|
263
|
+
}
|