@anhlp/alp-client 0.0.0-beta.2 → 0.0.1-beta.3
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/dist/index.d.ts +16 -1
- package/dist/index.js +83 -5
- package/dist/supervise.js +18 -12
- package/package.json +3 -3
- package/src/index.js +75 -4
- package/src/supervise.js +17 -13
package/dist/index.d.ts
CHANGED
|
@@ -68,17 +68,32 @@ export function connect(socketPath: string, client?: {
|
|
|
68
68
|
name: string;
|
|
69
69
|
version: string;
|
|
70
70
|
}, options?: import("./rpc/client.js").AlpRpcClientOptions & import("./ipc.js").SocketTransportOptions): Promise<AlpClient>;
|
|
71
|
+
export function readDaemonLifecycle(home: any): Promise<{
|
|
72
|
+
token: string;
|
|
73
|
+
pid: number;
|
|
74
|
+
bootTime: number;
|
|
75
|
+
action: string;
|
|
76
|
+
} | undefined>;
|
|
77
|
+
/** Serializes requested starts and stops, including the time before alpd has a lock.
|
|
78
|
+
* Rename publishes a fully populated directory: an existing nonempty directory cannot
|
|
79
|
+
* be replaced. Reaping/release unlinks only its unique owner token, then rmdir; even
|
|
80
|
+
* racing stale reapers cannot remove a successor's nonempty directory.
|
|
81
|
+
*/
|
|
82
|
+
export function withDaemonLifecycle(home: any, action: any, run: any, { joinStart }?: {
|
|
83
|
+
joinStart?: boolean | undefined;
|
|
84
|
+
}): Promise<any>;
|
|
71
85
|
/**
|
|
72
86
|
* Returns the socket of a ready daemon, starting one when none runs.
|
|
73
87
|
* `entry` is the daemon script; it is started through its own detaching launcher so the
|
|
74
88
|
* daemon outlives the client that started it.
|
|
75
89
|
*/
|
|
76
|
-
export function ensureDaemon({ home, entry, env, execPath, timeoutMs }?: {
|
|
90
|
+
export function ensureDaemon({ home, entry, env, execPath, timeoutMs, lifecycleLocked }?: {
|
|
77
91
|
home?: string | undefined;
|
|
78
92
|
entry?: string | undefined;
|
|
79
93
|
env?: NodeJS.ProcessEnv | undefined;
|
|
80
94
|
execPath?: string | undefined;
|
|
81
95
|
timeoutMs?: number | undefined;
|
|
96
|
+
lifecycleLocked?: boolean | undefined;
|
|
82
97
|
}): Promise<any>;
|
|
83
98
|
export function discoverIntegration(name?: string, options?: {}): Promise<string | undefined>;
|
|
84
99
|
export function startIntegration(nameOrOptions?: string): Promise<any>;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// packages/client/src/index.js
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { open, readFile, realpath, stat, lstat, mkdir, readlink, unlink } from "node:fs/promises";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { open, readFile, writeFile, realpath, stat, lstat, mkdir, readlink, unlink, readdir, rename, rmdir, link } from "node:fs/promises";
|
|
5
5
|
import net from "node:net";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
@@ -197,10 +197,86 @@ async function tail(file, lines = 30) {
|
|
|
197
197
|
return "";
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
+
async function lifecycleOwners(home) {
|
|
201
|
+
const directory = path.join(home, "state", "alpd.starting");
|
|
202
|
+
const names = await readdir(directory).catch((error) => {
|
|
203
|
+
if (error.code === "ENOENT") return [];
|
|
204
|
+
throw error;
|
|
205
|
+
});
|
|
206
|
+
return names.map((token) => {
|
|
207
|
+
const match = /^owner-(\d+)-(\d+)-(start|stop)-[a-f0-9-]+$/.exec(token);
|
|
208
|
+
if (!match) throw new Error("Invalid alpd startup admission owner");
|
|
209
|
+
return { token, pid: Number(match[1]), bootTime: Number(match[2]), action: match[3] };
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
async function readDaemonLifecycle(home) {
|
|
213
|
+
return (await lifecycleOwners(home)).find((owner) => lockAlive(owner));
|
|
214
|
+
}
|
|
215
|
+
async function withDaemonLifecycle(home, action, run, { joinStart = false } = {}) {
|
|
216
|
+
try {
|
|
217
|
+
await assertPrivateDirectory(home, { create: true });
|
|
218
|
+
} catch (cause) {
|
|
219
|
+
throw Object.assign(new Error(`ALP_HOME_UNUSABLE, alpd failed to start while preparing its home: ${cause.message}`, { cause }), { code: "ALP_HOME_UNUSABLE" });
|
|
220
|
+
}
|
|
221
|
+
const state = path.join(home, "state");
|
|
222
|
+
await assertPrivateDirectory(state, { create: true });
|
|
223
|
+
const directory = path.join(state, "alpd.starting");
|
|
224
|
+
const token = `owner-${process.pid}-${bootTime()}-${action}-${randomUUID()}`;
|
|
225
|
+
const candidate = path.join(state, `alpd.starting-${token}`);
|
|
226
|
+
await mkdir(candidate, { mode: 448 });
|
|
227
|
+
await writeFile(path.join(candidate, token), JSON.stringify({ pid: process.pid, bootTime: bootTime(), action }), { mode: 384 });
|
|
228
|
+
try {
|
|
229
|
+
let joined = false;
|
|
230
|
+
if (joinStart) {
|
|
231
|
+
const owner = await readDaemonLifecycle(home);
|
|
232
|
+
if (owner?.action === "start") {
|
|
233
|
+
try {
|
|
234
|
+
await link(path.join(directory, owner.token), path.join(directory, token));
|
|
235
|
+
joined = true;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (error.code !== "ENOENT") throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
while (!joined) {
|
|
242
|
+
try {
|
|
243
|
+
await rename(candidate, directory);
|
|
244
|
+
break;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (!["EEXIST", "ENOTEMPTY", "EPERM", "EACCES"].includes(error.code)) throw error;
|
|
247
|
+
for (const owner of await lifecycleOwners(home)) {
|
|
248
|
+
if (!lockAlive(owner)) await unlink(path.join(directory, owner.token)).catch((error2) => {
|
|
249
|
+
if (error2.code !== "ENOENT") throw error2;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
await rmdir(directory).catch((error2) => {
|
|
253
|
+
if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error2.code)) throw error2;
|
|
254
|
+
});
|
|
255
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
return await run();
|
|
260
|
+
} finally {
|
|
261
|
+
await unlink(path.join(directory, token));
|
|
262
|
+
await rmdir(directory).catch((error) => {
|
|
263
|
+
if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code)) throw error;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
} finally {
|
|
267
|
+
await unlink(path.join(candidate, token)).catch((error) => {
|
|
268
|
+
if (error.code !== "ENOENT") throw error;
|
|
269
|
+
});
|
|
270
|
+
await rmdir(candidate).catch((error) => {
|
|
271
|
+
if (error.code !== "ENOENT") throw error;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
200
275
|
async function ensureDaemon({ home = alpHome(), entry = (
|
|
201
276
|
/** @type {string | undefined} */
|
|
202
277
|
void 0
|
|
203
|
-
), env = process.env, execPath = process.execPath, timeoutMs = 15e3 } = {}) {
|
|
278
|
+
), env = process.env, execPath = process.execPath, timeoutMs = 15e3, lifecycleLocked = false } = {}) {
|
|
279
|
+
if (!lifecycleLocked) return withDaemonLifecycle(home, "start", () => ensureDaemon({ home, entry, env, execPath, timeoutMs, lifecycleLocked: true }));
|
|
204
280
|
const ready = async () => {
|
|
205
281
|
const lock = await readLock(home);
|
|
206
282
|
return lockAlive(lock) && lock.ready ? lock.socket : void 0;
|
|
@@ -213,7 +289,7 @@ async function ensureDaemon({ home = alpHome(), entry = (
|
|
|
213
289
|
entry ??= await findDaemonEntry({ home, env });
|
|
214
290
|
if (!entry) throw new Error("ALP daemon is not running and alpd.js was not found. Install the ALP CLI and run `alp daemon start`, or set ALP_DAEMON_ENTRY to alpd.js");
|
|
215
291
|
await new Promise((resolve, reject) => {
|
|
216
|
-
const launcher = spawn(execPath, [entry, "--detach"], {
|
|
292
|
+
const launcher = spawn(execPath, [entry, "--detach", `--startup-timeout=${timeoutMs}`], {
|
|
217
293
|
env: { ...env, ALP_HOME: home, ELECTRON_RUN_AS_NODE: "1" },
|
|
218
294
|
stdio: ["ignore", "ignore", "pipe"],
|
|
219
295
|
windowsHide: true
|
|
@@ -415,8 +491,10 @@ export {
|
|
|
415
491
|
ensureDaemon,
|
|
416
492
|
findDaemonEntry,
|
|
417
493
|
lockAlive,
|
|
494
|
+
readDaemonLifecycle,
|
|
418
495
|
readLock,
|
|
419
496
|
redactUrl,
|
|
420
497
|
releaseDaemonLock,
|
|
421
|
-
startIntegration
|
|
498
|
+
startIntegration,
|
|
499
|
+
withDaemonLifecycle
|
|
422
500
|
};
|
package/dist/supervise.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// packages/client/src/supervise.js
|
|
2
2
|
import { mkdir, unlink, writeFile, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { ensureDaemon, lockAlive, readLock } from "./index.js";
|
|
4
|
+
import { ensureDaemon, lockAlive, readLock, withDaemonLifecycle } from "./index.js";
|
|
5
5
|
import { installedProgram, serviceFor, startService } from "./service.js";
|
|
6
6
|
var holdFile = (home) => path.join(home, "state", "alpd.held");
|
|
7
7
|
async function holdDaemon(home, by = "alp daemon stop") {
|
|
@@ -24,16 +24,20 @@ async function whenReady(home, timeoutMs) {
|
|
|
24
24
|
}
|
|
25
25
|
throw new Error(`alpd did not become ready within ${timeoutMs / 1e3} s; see ${path.join(home, "logs")}`);
|
|
26
26
|
}
|
|
27
|
-
async function startDaemon({ home, entry, env = process.env, timeoutMs = 15e3 }) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
async function startDaemon({ home, entry, env = process.env, timeoutMs = 15e3, automatic = false }) {
|
|
28
|
+
return withDaemonLifecycle(home, "start", async () => {
|
|
29
|
+
if (automatic) {
|
|
30
|
+
if (await daemonHeld(home)) return;
|
|
31
|
+
} else await releaseHold(home);
|
|
32
|
+
const lock = await readLock(home);
|
|
33
|
+
if (lockAlive(lock)) return lock.ready ? lock.socket : whenReady(home, timeoutMs);
|
|
34
|
+
const service = serviceFor({ home, env });
|
|
35
|
+
if (service && await installedProgram(service)) {
|
|
36
|
+
await startService(service);
|
|
37
|
+
return whenReady(home, timeoutMs);
|
|
38
|
+
}
|
|
39
|
+
return ensureDaemon({ home, entry, env, timeoutMs, lifecycleLocked: true });
|
|
40
|
+
});
|
|
37
41
|
}
|
|
38
42
|
function superviseDaemon({ home, entry, env = process.env, intervalMs = 5e3, misses = 2, log = () => {
|
|
39
43
|
} }) {
|
|
@@ -41,7 +45,9 @@ function superviseDaemon({ home, entry, env = process.env, intervalMs = 5e3, mis
|
|
|
41
45
|
let missed = 0;
|
|
42
46
|
let starting;
|
|
43
47
|
const start = (reason) => {
|
|
44
|
-
starting ??= startDaemon({ home, entry: typeof entry === "function" ? entry() : entry, env
|
|
48
|
+
starting ??= startDaemon({ home, entry: typeof entry === "function" ? entry() : entry, env, automatic: reason !== "started with Paseo" }).then((socket) => {
|
|
49
|
+
if (socket) log(`alpd ${reason}`);
|
|
50
|
+
}, (error) => log(`could not start alpd: ${error?.message ?? error}`)).finally(() => {
|
|
45
51
|
starting = void 0;
|
|
46
52
|
missed = 0;
|
|
47
53
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anhlp/alp-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1-beta.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ALP client: portable JSON-RPC SDK, WebSocket and Node IPC transports, daemon discovery and setup",
|
|
6
6
|
"files": [
|
|
@@ -142,8 +142,8 @@
|
|
|
142
142
|
"node": ">=22.12"
|
|
143
143
|
},
|
|
144
144
|
"dependencies": {
|
|
145
|
-
"@anhlp/alp-core": "0.0.
|
|
146
|
-
"@anhlp/alp-protocol": "0.0.
|
|
145
|
+
"@anhlp/alp-core": "0.0.1-beta.3",
|
|
146
|
+
"@anhlp/alp-protocol": "0.0.1-beta.3"
|
|
147
147
|
},
|
|
148
148
|
"scripts": {
|
|
149
149
|
"build": "node ../../scripts/build-workspace.mjs client",
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import { open, readFile, realpath, stat, lstat, mkdir, readlink, unlink } from 'node:fs/promises';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { open, readFile, writeFile, realpath, stat, lstat, mkdir, readlink, unlink, readdir, rename, rmdir, link } from 'node:fs/promises';
|
|
4
4
|
import net from 'node:net';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
@@ -242,12 +242,83 @@ async function tail(file, lines = 30) {
|
|
|
242
242
|
try { return scrubText((await readFile(file, 'utf8')).split('\n').slice(-lines).join('\n'), { max: 8000 }); } catch { return ''; }
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
/** Immutable admission identities live in filenames, including for service hard links. */
|
|
246
|
+
async function lifecycleOwners(home) {
|
|
247
|
+
const directory = path.join(home, 'state', 'alpd.starting');
|
|
248
|
+
const names = await readdir(directory).catch(error => { if (error.code === 'ENOENT') return []; throw error; });
|
|
249
|
+
return names.map(token => {
|
|
250
|
+
const match = /^owner-(\d+)-(\d+)-(start|stop)-[a-f0-9-]+$/.exec(token);
|
|
251
|
+
if (!match) throw new Error('Invalid alpd startup admission owner');
|
|
252
|
+
return { token, pid: Number(match[1]), bootTime: Number(match[2]), action: match[3] };
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function readDaemonLifecycle(home) {
|
|
257
|
+
return (await lifecycleOwners(home)).find(owner => lockAlive(owner));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Serializes requested starts and stops, including the time before alpd has a lock.
|
|
261
|
+
* Rename publishes a fully populated directory: an existing nonempty directory cannot
|
|
262
|
+
* be replaced. Reaping/release unlinks only its unique owner token, then rmdir; even
|
|
263
|
+
* racing stale reapers cannot remove a successor's nonempty directory.
|
|
264
|
+
*/
|
|
265
|
+
export async function withDaemonLifecycle(home, action, run, { joinStart = false } = {}) {
|
|
266
|
+
try {
|
|
267
|
+
await assertPrivateDirectory(home, { create: true });
|
|
268
|
+
} catch (cause) {
|
|
269
|
+
// Admission now prepares the home before the launcher can report this failure.
|
|
270
|
+
// Preserve the operator-facing startup code at this earlier boundary.
|
|
271
|
+
throw Object.assign(new Error(`ALP_HOME_UNUSABLE, alpd failed to start while preparing its home: ${cause.message}`, { cause }), { code: 'ALP_HOME_UNUSABLE' });
|
|
272
|
+
}
|
|
273
|
+
const state = path.join(home, 'state');
|
|
274
|
+
await assertPrivateDirectory(state, { create: true });
|
|
275
|
+
const directory = path.join(state, 'alpd.starting');
|
|
276
|
+
const token = `owner-${process.pid}-${bootTime()}-${action}-${randomUUID()}`;
|
|
277
|
+
const candidate = path.join(state, `alpd.starting-${token}`);
|
|
278
|
+
await mkdir(candidate, { mode: 0o700 });
|
|
279
|
+
await writeFile(path.join(candidate, token), JSON.stringify({ pid: process.pid, bootTime: bootTime(), action }), { mode: 0o600 });
|
|
280
|
+
try {
|
|
281
|
+
let joined = false;
|
|
282
|
+
if (joinStart) {
|
|
283
|
+
const owner = await readDaemonLifecycle(home);
|
|
284
|
+
if (owner?.action === 'start') {
|
|
285
|
+
try {
|
|
286
|
+
// Linking the unique source token both validates this generation and
|
|
287
|
+
// pins its directory until this service has finished starting.
|
|
288
|
+
await link(path.join(directory, owner.token), path.join(directory, token));
|
|
289
|
+
joined = true;
|
|
290
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
while (!joined) {
|
|
294
|
+
try { await rename(candidate, directory); break; }
|
|
295
|
+
catch (error) {
|
|
296
|
+
if (!['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES'].includes(error.code)) throw error;
|
|
297
|
+
for (const owner of await lifecycleOwners(home)) {
|
|
298
|
+
if (!lockAlive(owner)) await unlink(path.join(directory, owner.token)).catch(error => { if (error.code !== 'ENOENT') throw error; });
|
|
299
|
+
}
|
|
300
|
+
await rmdir(directory).catch(error => { if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code)) throw error; });
|
|
301
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
try { return await run(); }
|
|
305
|
+
finally {
|
|
306
|
+
await unlink(path.join(directory, token));
|
|
307
|
+
await rmdir(directory).catch(error => { if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code)) throw error; });
|
|
308
|
+
}
|
|
309
|
+
} finally {
|
|
310
|
+
await unlink(path.join(candidate, token)).catch(error => { if (error.code !== 'ENOENT') throw error; });
|
|
311
|
+
await rmdir(candidate).catch(error => { if (error.code !== 'ENOENT') throw error; });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
245
315
|
/**
|
|
246
316
|
* Returns the socket of a ready daemon, starting one when none runs.
|
|
247
317
|
* `entry` is the daemon script; it is started through its own detaching launcher so the
|
|
248
318
|
* daemon outlives the client that started it.
|
|
249
319
|
*/
|
|
250
|
-
export async function ensureDaemon({ home = alpHome(), entry = /** @type {string | undefined} */ (undefined), env = process.env, execPath = process.execPath, timeoutMs = 15_000 } = {}) {
|
|
320
|
+
export async function ensureDaemon({ home = alpHome(), entry = /** @type {string | undefined} */ (undefined), env = process.env, execPath = process.execPath, timeoutMs = 15_000, lifecycleLocked = false } = {}) {
|
|
321
|
+
if (!lifecycleLocked) return withDaemonLifecycle(home, 'start', () => ensureDaemon({ home, entry, env, execPath, timeoutMs, lifecycleLocked: true }));
|
|
251
322
|
const ready = async () => {
|
|
252
323
|
const lock = await readLock(home);
|
|
253
324
|
return lockAlive(lock) && lock.ready ? lock.socket : undefined;
|
|
@@ -260,7 +331,7 @@ export async function ensureDaemon({ home = alpHome(), entry = /** @type {string
|
|
|
260
331
|
entry ??= await findDaemonEntry({ home, env });
|
|
261
332
|
if (!entry) throw new Error('ALP daemon is not running and alpd.js was not found. Install the ALP CLI and run `alp daemon start`, or set ALP_DAEMON_ENTRY to alpd.js');
|
|
262
333
|
await new Promise((resolve, reject) => {
|
|
263
|
-
const launcher = spawn(execPath, [entry, '--detach'], {
|
|
334
|
+
const launcher = spawn(execPath, [entry, '--detach', `--startup-timeout=${timeoutMs}`], {
|
|
264
335
|
env: { ...env, ALP_HOME: home, ELECTRON_RUN_AS_NODE: '1' },
|
|
265
336
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
266
337
|
windowsHide: true,
|
package/src/supervise.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, unlink, writeFile, stat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { ensureDaemon, lockAlive, readLock } from './index.js';
|
|
3
|
+
import { ensureDaemon, lockAlive, readLock, withDaemonLifecycle } from './index.js';
|
|
4
4
|
import { installedProgram, serviceFor, startService } from './service.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -41,16 +41,20 @@ async function whenReady(home, timeoutMs) {
|
|
|
41
41
|
* Starts alpd for `home` unless it runs, and lifts a hold: someone asked for it.
|
|
42
42
|
* Returns its socket.
|
|
43
43
|
*/
|
|
44
|
-
export async function startDaemon({ home, entry, env = process.env, timeoutMs = 15_000 }) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
export async function startDaemon({ home, entry, env = process.env, timeoutMs = 15_000, automatic = false }) {
|
|
45
|
+
return withDaemonLifecycle(home, 'start', async () => {
|
|
46
|
+
if (automatic) {
|
|
47
|
+
if (await daemonHeld(home)) return;
|
|
48
|
+
} else await releaseHold(home);
|
|
49
|
+
const lock = await readLock(home);
|
|
50
|
+
if (lockAlive(lock)) return lock.ready ? lock.socket : whenReady(home, timeoutMs);
|
|
51
|
+
const service = serviceFor({ home, env });
|
|
52
|
+
if (service && await installedProgram(service)) {
|
|
53
|
+
await startService(service);
|
|
54
|
+
return whenReady(home, timeoutMs);
|
|
55
|
+
}
|
|
56
|
+
return ensureDaemon({ home, entry, env, timeoutMs, lifecycleLocked: true });
|
|
57
|
+
});
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
/**
|
|
@@ -64,8 +68,8 @@ export function superviseDaemon({ home, entry, env = process.env, intervalMs = 5
|
|
|
64
68
|
let missed = 0;
|
|
65
69
|
let starting;
|
|
66
70
|
const start = reason => {
|
|
67
|
-
starting ??= startDaemon({ home, entry: typeof entry === 'function' ? entry() : entry, env })
|
|
68
|
-
.then(()
|
|
71
|
+
starting ??= startDaemon({ home, entry: typeof entry === 'function' ? entry() : entry, env, automatic: reason !== 'started with Paseo' })
|
|
72
|
+
.then(socket => { if (socket) log(`alpd ${reason}`); }, error => log(`could not start alpd: ${error?.message ?? error}`))
|
|
69
73
|
.finally(() => { starting = undefined; missed = 0; });
|
|
70
74
|
return starting;
|
|
71
75
|
};
|