@dinoxx/dinox-cli 2.13.0 → 2.14.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/dist/cli.js +5 -2
- package/dist/commands/auth/index.d.ts +2 -7
- package/dist/commands/auth/index.js +26 -51
- package/dist/commands/doctor.d.ts +4 -1
- package/dist/commands/doctor.js +20 -1
- package/dist/commands/graph/index.js +2 -1
- package/dist/commands/notes/contentPatch.js +2 -1
- package/dist/commands/notes/export.js +3 -2
- package/dist/commands/runtime.d.ts +8 -0
- package/dist/commands/runtime.js +49 -1
- package/dist/commands/schema/globalOptions.d.ts +1 -0
- package/dist/commands/schema/globalOptions.js +1 -0
- package/dist/commands/schema/renderSkills.js +1 -0
- package/dist/commands/schema/spec.js +9 -1
- package/dist/commands/storage/repo/operations.js +3 -1
- package/dist/commands/sync.d.ts +2 -1
- package/dist/commands/sync.js +24 -5
- package/dist/daemon/client.js +32 -2
- package/dist/daemon/connectivity.d.ts +20 -0
- package/dist/daemon/connectivity.js +42 -0
- package/dist/daemon/ensure.js +48 -12
- package/dist/daemon/forward.d.ts +34 -0
- package/dist/daemon/forward.js +146 -0
- package/dist/daemon/fresh.d.ts +4 -0
- package/dist/daemon/fresh.js +10 -0
- package/dist/daemon/index.d.ts +11 -6
- package/dist/daemon/index.js +90 -25
- package/dist/daemon/launch.d.ts +13 -1
- package/dist/daemon/launch.js +31 -2
- package/dist/daemon/lifecycle.d.ts +3 -1
- package/dist/daemon/lifecycle.js +27 -3
- package/dist/daemon/pid.d.ts +8 -0
- package/dist/daemon/pid.js +20 -0
- package/dist/daemon/route.js +2 -1
- package/dist/daemon/server.d.ts +44 -0
- package/dist/daemon/server.js +175 -3
- package/dist/daemon/supervise.d.ts +6 -6
- package/dist/daemon/supervise.js +12 -10
- package/dist/daemon/supervisor.d.ts +21 -5
- package/dist/daemon/supervisor.js +125 -6
- package/dist/daemon/types.d.ts +16 -0
- package/dist/dinox.js +41 -64
- package/dist/gateway/mcpCommandTools.d.ts +9 -0
- package/dist/gateway/mcpCommandTools.js +40 -10
- package/dist/powersync/runtime.d.ts +2 -0
- package/dist/powersync/runtime.js +3 -1
- package/dist/requests/store.d.ts +10 -0
- package/dist/requests/store.js +190 -0
- package/dist/runCli.d.ts +2 -0
- package/dist/runCli.js +64 -0
- package/dist/utils/argValue.js +2 -1
- package/dist/utils/errorCodes.d.ts +20 -0
- package/dist/utils/errorCodes.js +5 -0
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +31 -7
- package/dist/utils/requestContext.d.ts +24 -0
- package/dist/utils/requestContext.js +13 -0
- package/package.json +2 -1
package/dist/daemon/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, watch, mkdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getConfigFilePath } from '../config/paths.js';
|
|
2
4
|
import { DaemonServer } from './server.js';
|
|
3
5
|
import { DEFAULT_DAEMON_PORT } from './types.js';
|
|
4
6
|
import { acquireDaemonStartLock, getDaemonSocketPath, isProcessRunning, readPid, removePid, } from './pid.js';
|
|
@@ -13,6 +15,7 @@ import { hydrateResolvedAuthIdentity } from '../auth/userInfo.js';
|
|
|
13
15
|
import { withAsyncRelease } from '../utils/asyncRelease.js';
|
|
14
16
|
import { remainingDeadlineMs } from '../utils/deadline.js';
|
|
15
17
|
import { getPackageVersion } from '../utils/version.js';
|
|
18
|
+
import { matchesRuntimeTokenBinding } from './fresh.js';
|
|
16
19
|
import { DAEMON_CHILD_ENV, daemonServiceSpec, envTokenOverridesLogin, launchDaemon } from './launch.js';
|
|
17
20
|
import { getDaemonLogPath, rotateDaemonLog, writeDaemonLogLine } from './log.js';
|
|
18
21
|
import { DAEMON_SERVICE_ENV, getDaemonService } from './supervisor.js';
|
|
@@ -99,14 +102,6 @@ async function expectedDaemonIdentity(baseConfig, timeoutMs) {
|
|
|
99
102
|
runtimeConfigFingerprint: fingerprintDaemonRuntimeConfig(config),
|
|
100
103
|
};
|
|
101
104
|
}
|
|
102
|
-
function matchesRuntimeTokenBinding(info, version, config) {
|
|
103
|
-
return Boolean(config.auth.authorization
|
|
104
|
-
&& info.authenticated
|
|
105
|
-
&& info.version === version
|
|
106
|
-
&& info.tokenFingerprint === (config.auth.tokenFingerprint ?? null)
|
|
107
|
-
&& info.runtimeConfigFingerprint === fingerprintDaemonRuntimeConfig(config)
|
|
108
|
-
&& (!config.auth.userId || info.userId === config.auth.userId));
|
|
109
|
-
}
|
|
110
105
|
export async function startDaemonLocked(port, detached, options = {}) {
|
|
111
106
|
const maximumTimeoutMs = options.timeoutMs === undefined
|
|
112
107
|
? undefined
|
|
@@ -142,7 +137,7 @@ export async function startDaemonLocked(port, detached, options = {}) {
|
|
|
142
137
|
}
|
|
143
138
|
const expected = await expectedDaemonIdentity(baseConfig, remainingStartTimeoutMs('auth-hydration'));
|
|
144
139
|
if (detached) {
|
|
145
|
-
const launch = await launchDaemon(port);
|
|
140
|
+
const launch = await launchDaemon(port, { takeover: options.takeover });
|
|
146
141
|
const ready = await waitForFreshDaemon(expected.version, expected.userId, expected.tokenFingerprint, expected.runtimeConfigFingerprint, remainingStartTimeoutMs('ready-poll'));
|
|
147
142
|
if (!ready) {
|
|
148
143
|
launch.abort();
|
|
@@ -172,7 +167,16 @@ async function serveDaemonInForeground(port, options) {
|
|
|
172
167
|
if (!options.quiet) {
|
|
173
168
|
printHumanReadableLine(`Starting daemon on ${getDaemonSocketPath()}...`);
|
|
174
169
|
}
|
|
175
|
-
const
|
|
170
|
+
const stopAndExit = async (reason, exitCode, details) => {
|
|
171
|
+
writeDaemonLogLine('dinox-daemon', 'stopping', { reason, ...details });
|
|
172
|
+
const forceExit = setTimeout(() => process.exit(exitCode), DAEMON_CRASH_STOP_TIMEOUT_MS);
|
|
173
|
+
forceExit.unref();
|
|
174
|
+
await server.stop().catch(() => undefined);
|
|
175
|
+
process.exit(exitCode);
|
|
176
|
+
};
|
|
177
|
+
const server = new DaemonServer({
|
|
178
|
+
onStopRequested: (reason, details) => stopAndExit(reason, typeof details?.exitCode === 'number' ? details.exitCode : 0, details),
|
|
179
|
+
});
|
|
176
180
|
try {
|
|
177
181
|
await server.initialize();
|
|
178
182
|
await server.start(port);
|
|
@@ -189,13 +193,6 @@ async function serveDaemonInForeground(port, options) {
|
|
|
189
193
|
version: getPackageVersion(),
|
|
190
194
|
supervisor: process.env[DAEMON_SERVICE_ENV] ?? null,
|
|
191
195
|
});
|
|
192
|
-
const stopAndExit = async (reason, exitCode) => {
|
|
193
|
-
writeDaemonLogLine('dinox-daemon', 'stopping', { reason });
|
|
194
|
-
const forceExit = setTimeout(() => process.exit(exitCode), DAEMON_CRASH_STOP_TIMEOUT_MS);
|
|
195
|
-
forceExit.unref();
|
|
196
|
-
await server.stop().catch(() => undefined);
|
|
197
|
-
process.exit(exitCode);
|
|
198
|
-
};
|
|
199
196
|
process.on('SIGINT', () => {
|
|
200
197
|
if (!options.quiet) {
|
|
201
198
|
printHumanReadableLine('');
|
|
@@ -247,11 +244,17 @@ export async function stopDaemonLocked() {
|
|
|
247
244
|
await shutdownLegacyDaemon(info, DAEMON_STOP_TIMEOUT_MS);
|
|
248
245
|
return { stopped: true, wasRunning: true, stalePidFile: false };
|
|
249
246
|
}
|
|
247
|
+
const service = getDaemonService();
|
|
248
|
+
const supervised = Boolean(service?.isInstalled() && info.supervisor === service.kind);
|
|
250
249
|
try {
|
|
251
|
-
|
|
250
|
+
// Through the manager: with KeepAlive a plain socket shutdown is respawned.
|
|
251
|
+
if (supervised)
|
|
252
|
+
await service.stop();
|
|
253
|
+
if (!supervised || service.kind === 'schtasks')
|
|
254
|
+
await requestDaemonShutdown(info.instanceId, 2_000);
|
|
252
255
|
}
|
|
253
256
|
catch (error) {
|
|
254
|
-
throw new DinoxError(`Failed to stop daemon through its private socket: ${error.message}`, {
|
|
257
|
+
throw new DinoxError(`Failed to stop daemon ${supervised ? `through ${service.kind}` : 'through its private socket'}: ${error.message}`, {
|
|
255
258
|
code: 'DAEMON_STOP_FAILED',
|
|
256
259
|
suggestion: 'The daemon PID was not signaled because its process identity could not be verified safely.',
|
|
257
260
|
});
|
|
@@ -317,6 +320,20 @@ async function daemonStatus() {
|
|
|
317
320
|
}
|
|
318
321
|
}
|
|
319
322
|
export async function restartDaemonLocked(port = DEFAULT_DAEMON_PORT, options = {}) {
|
|
323
|
+
const info = await getDaemonInfo();
|
|
324
|
+
const service = getDaemonService();
|
|
325
|
+
if (info && service?.isInstalled() && info.supervisor === service.kind
|
|
326
|
+
&& service.isCurrent(daemonServiceSpec(service, port))) {
|
|
327
|
+
const expected = await expectedDaemonIdentity();
|
|
328
|
+
await service.restart();
|
|
329
|
+
if (service.kind === 'schtasks')
|
|
330
|
+
await requestDaemonShutdown(info.instanceId, 2_000);
|
|
331
|
+
const ready = await waitForFreshDaemon(expected.version, expected.userId, expected.tokenFingerprint, expected.runtimeConfigFingerprint, DAEMON_SERVICE_READY_TIMEOUT_MS);
|
|
332
|
+
if (!ready)
|
|
333
|
+
throw new DinoxError('Restarted service did not become ready', { code: 'DAEMON_START_FAILED' });
|
|
334
|
+
return { stopped: true, running: true, port, socketPath: getDaemonSocketPath(), transport: 'socket',
|
|
335
|
+
detached: true, alreadyRunning: false, service: service.kind };
|
|
336
|
+
}
|
|
320
337
|
const stopResult = await stopDaemonLocked();
|
|
321
338
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
322
339
|
const startResult = await startDaemonLocked(port, true, options);
|
|
@@ -388,6 +405,8 @@ export async function uninstallDaemonService() {
|
|
|
388
405
|
const info = await getDaemonInfo();
|
|
389
406
|
if (info?.supervisor === service.kind)
|
|
390
407
|
await stopDaemonLocked();
|
|
408
|
+
else
|
|
409
|
+
await service.stop();
|
|
391
410
|
}
|
|
392
411
|
return {
|
|
393
412
|
uninstalled: await service.uninstall(),
|
|
@@ -456,17 +475,63 @@ function printDaemonUninstallResult(result) {
|
|
|
456
475
|
? `Removed ${result.service} service; the daemon is stopped.`
|
|
457
476
|
: 'The daemon service is not installed.');
|
|
458
477
|
}
|
|
478
|
+
export async function waitForSavedDaemonLogin(overrides = {}) {
|
|
479
|
+
const hasAuth = overrides.hasAuth ?? (async () => Boolean(normalizeAuthToken((await loadConfig()).auth?.authorization)));
|
|
480
|
+
const exit = overrides.exit ?? ((code) => process.exit(code));
|
|
481
|
+
const onSignal = overrides.onSignal ?? ((handler) => {
|
|
482
|
+
process.on('SIGTERM', handler);
|
|
483
|
+
process.on('SIGINT', handler);
|
|
484
|
+
return () => { process.off('SIGTERM', handler); process.off('SIGINT', handler); };
|
|
485
|
+
});
|
|
486
|
+
const watchConfig = overrides.watchConfig ?? ((check) => {
|
|
487
|
+
const dir = path.dirname(getConfigFilePath());
|
|
488
|
+
mkdirSync(dir, { recursive: true });
|
|
489
|
+
try {
|
|
490
|
+
const watcher = watch(dir, check);
|
|
491
|
+
return () => watcher.close();
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return () => undefined;
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
writeDaemonLogLine('dinox-daemon', 'not signed in; waiting for saved login');
|
|
498
|
+
await new Promise((resolve) => {
|
|
499
|
+
let done = false;
|
|
500
|
+
let closeWatch = () => undefined;
|
|
501
|
+
let closeSignals = () => undefined;
|
|
502
|
+
const finish = (code) => {
|
|
503
|
+
if (done)
|
|
504
|
+
return;
|
|
505
|
+
done = true;
|
|
506
|
+
clearInterval(timer);
|
|
507
|
+
closeWatch();
|
|
508
|
+
closeSignals();
|
|
509
|
+
exit(code);
|
|
510
|
+
resolve();
|
|
511
|
+
};
|
|
512
|
+
const check = () => {
|
|
513
|
+
void hasAuth().then((authorized) => {
|
|
514
|
+
if (authorized && !done) {
|
|
515
|
+
writeDaemonLogLine('dinox-daemon', 'saved login found; restarting');
|
|
516
|
+
finish(75);
|
|
517
|
+
}
|
|
518
|
+
}).catch(() => undefined);
|
|
519
|
+
};
|
|
520
|
+
const timer = setInterval(check, overrides.pollMs ?? 60_000);
|
|
521
|
+
closeWatch = watchConfig(check);
|
|
522
|
+
closeSignals = onSignal(() => finish(0));
|
|
523
|
+
check();
|
|
524
|
+
});
|
|
525
|
+
}
|
|
459
526
|
async function runDaemonStart(context) {
|
|
460
527
|
const port = parsePort(context.options.port);
|
|
461
528
|
const structured = Boolean(getRequestedStructuredOutputFormat());
|
|
462
529
|
if (context.options.detach === false && process.env[DAEMON_SERVICE_ENV]
|
|
463
530
|
&& !resolveConfig(await loadConfig()).auth.authorization) {
|
|
464
|
-
|
|
465
|
-
// service does not retry; the next command after `dino auth login` starts it.
|
|
466
|
-
writeDaemonLogLine('dinox-daemon', 'not signed in; staying stopped until `dino auth login`');
|
|
531
|
+
await waitForSavedDaemonLogin();
|
|
467
532
|
return;
|
|
468
533
|
}
|
|
469
|
-
const result = await startDaemon(port, context.options.detach !== false, { quiet: structured });
|
|
534
|
+
const result = await startDaemon(port, context.options.detach !== false, { quiet: structured, takeover: true });
|
|
470
535
|
if (!result.running) {
|
|
471
536
|
throw new DinoxError('Failed to start daemon', { code: 'DAEMON_START_FAILED' });
|
|
472
537
|
}
|
|
@@ -513,7 +578,7 @@ async function runDaemonUninstall() {
|
|
|
513
578
|
}
|
|
514
579
|
async function runDaemonRestart(context) {
|
|
515
580
|
const structured = Boolean(getRequestedStructuredOutputFormat());
|
|
516
|
-
const result = await restartDaemon(parsePort(context.options.port), { quiet: structured });
|
|
581
|
+
const result = await restartDaemon(parsePort(context.options.port), { quiet: structured, takeover: true });
|
|
517
582
|
if (structured) {
|
|
518
583
|
printSuccess(result);
|
|
519
584
|
return;
|
package/dist/daemon/launch.d.ts
CHANGED
|
@@ -22,10 +22,22 @@ export declare function envTokenOverridesLogin(raw: DinoxConfig, env?: NodeJS.Pr
|
|
|
22
22
|
export declare function daemonServiceSpec(service: DaemonService, port: number): DaemonServiceSpec;
|
|
23
23
|
/** The installed service when this caller can use it, else null. */
|
|
24
24
|
export declare function usableDaemonService(): Promise<DaemonService | null>;
|
|
25
|
+
/**
|
|
26
|
+
* The entrypoint of another dino install (e.g. a global install next to a
|
|
27
|
+
* source checkout) that the service definition runs, or null when it runs
|
|
28
|
+
* this CLI or an install that no longer exists. Automatic paths must not
|
|
29
|
+
* rewrite or restart a service another install owns: each install would
|
|
30
|
+
* otherwise keep switching it to its own version.
|
|
31
|
+
*/
|
|
32
|
+
export declare function foreignServiceOwner(service: DaemonService, entrypoint?: string | undefined): string | null;
|
|
25
33
|
/**
|
|
26
34
|
* Starts a daemon. Caller must hold the daemon start lock and have stopped any
|
|
27
35
|
* stale daemon. Uses the installed service when there is one (refreshing its
|
|
28
36
|
* definition if this CLI's program, arguments or environment changed, e.g.
|
|
29
37
|
* after `dino update`), otherwise a detached child with a clean environment.
|
|
38
|
+
* A service another install owns is only started, never rewritten, unless
|
|
39
|
+
* `takeover` is set by an explicit `dino daemon start/restart`.
|
|
30
40
|
*/
|
|
31
|
-
export declare function launchDaemon(port: number
|
|
41
|
+
export declare function launchDaemon(port: number, options?: {
|
|
42
|
+
takeover?: boolean;
|
|
43
|
+
}): Promise<DaemonLaunch>;
|
package/dist/daemon/launch.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import { loadConfig } from '../config/store.js';
|
|
4
5
|
import { DINOX_TOKEN_ENV_VAR, normalizeAuthToken, readEnvAuthToken } from '../utils/authToken.js';
|
|
@@ -52,6 +53,32 @@ export async function usableDaemonService() {
|
|
|
52
53
|
return null;
|
|
53
54
|
return service;
|
|
54
55
|
}
|
|
56
|
+
function samePath(a, b) {
|
|
57
|
+
const resolve = (value) => {
|
|
58
|
+
try {
|
|
59
|
+
return realpathSync(value);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return resolve(a) === resolve(b);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The entrypoint of another dino install (e.g. a global install next to a
|
|
69
|
+
* source checkout) that the service definition runs, or null when it runs
|
|
70
|
+
* this CLI or an install that no longer exists. Automatic paths must not
|
|
71
|
+
* rewrite or restart a service another install owns: each install would
|
|
72
|
+
* otherwise keep switching it to its own version.
|
|
73
|
+
*/
|
|
74
|
+
export function foreignServiceOwner(service, entrypoint = process.argv[1]) {
|
|
75
|
+
const command = service.installedCommand();
|
|
76
|
+
const daemonIndex = command?.indexOf('daemon') ?? -1;
|
|
77
|
+
const owner = daemonIndex > 0 ? command[daemonIndex - 1] : null;
|
|
78
|
+
if (!owner || !entrypoint || samePath(owner, entrypoint) || !existsSync(owner))
|
|
79
|
+
return null;
|
|
80
|
+
return owner;
|
|
81
|
+
}
|
|
55
82
|
function spawnDetached(port) {
|
|
56
83
|
const { program, args } = daemonCommandLine(port);
|
|
57
84
|
const log = openDaemonLogForChild();
|
|
@@ -76,14 +103,16 @@ function spawnDetached(port) {
|
|
|
76
103
|
* stale daemon. Uses the installed service when there is one (refreshing its
|
|
77
104
|
* definition if this CLI's program, arguments or environment changed, e.g.
|
|
78
105
|
* after `dino update`), otherwise a detached child with a clean environment.
|
|
106
|
+
* A service another install owns is only started, never rewritten, unless
|
|
107
|
+
* `takeover` is set by an explicit `dino daemon start/restart`.
|
|
79
108
|
*/
|
|
80
|
-
export async function launchDaemon(port) {
|
|
109
|
+
export async function launchDaemon(port, options = {}) {
|
|
81
110
|
const service = await usableDaemonService();
|
|
82
111
|
if (!service)
|
|
83
112
|
return spawnDetached(port);
|
|
84
113
|
ensureDaemonDir();
|
|
85
114
|
const spec = daemonServiceSpec(service, port);
|
|
86
|
-
if (!service.isCurrent(spec)) {
|
|
115
|
+
if (!service.isCurrent(spec) && (options.takeover || !foreignServiceOwner(service))) {
|
|
87
116
|
await service.install(spec);
|
|
88
117
|
}
|
|
89
118
|
else {
|
|
@@ -3,7 +3,7 @@ import { printHumanReadableWarning } from '../utils/output.js';
|
|
|
3
3
|
import { getPackageVersion } from '../utils/version.js';
|
|
4
4
|
import { getDaemonInfo, shutdownDaemonForExclusiveAccess } from './client.js';
|
|
5
5
|
import { startDaemonLocked } from './index.js';
|
|
6
|
-
import { acquireDaemonStartLock, readPid, type DaemonPidInfo } from './pid.js';
|
|
6
|
+
import { acquireDaemonStartLock, acquireDaemonExclusiveIntentLock, readPid, type DaemonPidInfo } from './pid.js';
|
|
7
7
|
import { type DaemonPingResult } from './types.js';
|
|
8
8
|
export type DaemonSnapshot = {
|
|
9
9
|
info: DaemonPingResult | null;
|
|
@@ -21,10 +21,12 @@ export type DaemonExclusiveAccessControl = {
|
|
|
21
21
|
};
|
|
22
22
|
type DaemonLifecycleDependencies = {
|
|
23
23
|
acquireStartLock: typeof acquireDaemonStartLock;
|
|
24
|
+
acquireIntentLock: typeof acquireDaemonExclusiveIntentLock;
|
|
24
25
|
getDaemonInfo: typeof getDaemonInfo;
|
|
25
26
|
readPid: typeof readPid;
|
|
26
27
|
shutdownDaemon: typeof shutdownDaemonForExclusiveAccess;
|
|
27
28
|
startDaemonLocked: typeof startDaemonLocked;
|
|
29
|
+
startInstalledService: () => Promise<boolean>;
|
|
28
30
|
getPackageVersion: typeof getPackageVersion;
|
|
29
31
|
printWarning: typeof printHumanReadableWarning;
|
|
30
32
|
};
|
package/dist/daemon/lifecycle.js
CHANGED
|
@@ -6,15 +6,27 @@ import { DinoxError } from '../utils/errors.js';
|
|
|
6
6
|
import { printHumanReadableWarning } from '../utils/output.js';
|
|
7
7
|
import { getPackageVersion } from '../utils/version.js';
|
|
8
8
|
import { getDaemonInfo, isDaemonFresh, shutdownDaemonForExclusiveAccess, } from './client.js';
|
|
9
|
+
import { getDaemonService } from './supervisor.js';
|
|
9
10
|
import { startDaemonLocked } from './index.js';
|
|
10
|
-
import { acquireDaemonStartLock, getDaemonSocketPath, readPid, } from './pid.js';
|
|
11
|
+
import { acquireDaemonStartLock, acquireDaemonExclusiveIntentLock, getDaemonSocketPath, readPid, } from './pid.js';
|
|
11
12
|
import { DEFAULT_DAEMON_PORT } from './types.js';
|
|
13
|
+
/** Matches the service readiness budget; a cold daemon start takes a few seconds. */
|
|
14
|
+
const DAEMON_RESTORE_TIMEOUT_MS = 20_000;
|
|
12
15
|
const daemonLifecycleDependencies = {
|
|
13
16
|
acquireStartLock: acquireDaemonStartLock,
|
|
17
|
+
acquireIntentLock: acquireDaemonExclusiveIntentLock,
|
|
14
18
|
getDaemonInfo,
|
|
15
19
|
readPid,
|
|
16
20
|
shutdownDaemon: shutdownDaemonForExclusiveAccess,
|
|
17
21
|
startDaemonLocked,
|
|
22
|
+
async startInstalledService() {
|
|
23
|
+
const service = getDaemonService();
|
|
24
|
+
if (!service?.isInstalled())
|
|
25
|
+
return false;
|
|
26
|
+
// Not restart(): the manager may already be bringing it back.
|
|
27
|
+
await service.start();
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
18
30
|
getPackageVersion,
|
|
19
31
|
printWarning: printHumanReadableWarning,
|
|
20
32
|
};
|
|
@@ -78,13 +90,17 @@ export async function suspendDaemonForExclusiveAccessLocked(config, timeoutMs, o
|
|
|
78
90
|
}
|
|
79
91
|
/** Caller must hold the daemon start lock. */
|
|
80
92
|
export async function restoreSuspendedDaemonLocked(suspended, overrides = {}, options = {}) {
|
|
81
|
-
if (!suspended.wasStopped
|
|
93
|
+
if (!suspended.wasStopped) {
|
|
82
94
|
return false;
|
|
83
95
|
}
|
|
84
96
|
const deps = dependencies(overrides);
|
|
97
|
+
if (!suspended.wasFresh) {
|
|
98
|
+
return Boolean(suspended.snapshot.info?.supervisor && await deps.startInstalledService());
|
|
99
|
+
}
|
|
85
100
|
await deps.startDaemonLocked(suspended.port, true, {
|
|
86
101
|
quiet: true,
|
|
87
|
-
|
|
102
|
+
// Restoring a background process must not consume the default five-minute sync budget.
|
|
103
|
+
timeoutMs: Math.min(options.timeoutMs ?? DAEMON_RESTORE_TIMEOUT_MS, DAEMON_RESTORE_TIMEOUT_MS),
|
|
88
104
|
});
|
|
89
105
|
return true;
|
|
90
106
|
}
|
|
@@ -94,6 +110,7 @@ export async function withDaemonExclusiveAccess(config, timeoutMs, run, override
|
|
|
94
110
|
const remainingOperationTimeoutMs = (phase, action) => requireRemainingDeadlineMs(operationDeadlineAtMs, timeoutMs, phase, action);
|
|
95
111
|
const remainingCleanupTimeoutMs = () => Math.max(1, remainingDeadlineMs(options.deadlineAtMs, timeoutMs));
|
|
96
112
|
const releaseStartLock = await deps.acquireStartLock(remainingOperationTimeoutMs('daemon-lock-acquire', 'acquiring the daemon lifecycle lock'));
|
|
113
|
+
let releaseIntent;
|
|
97
114
|
let suspended = null;
|
|
98
115
|
let restoreAllowed = true;
|
|
99
116
|
let cleanupError;
|
|
@@ -104,6 +121,7 @@ export async function withDaemonExclusiveAccess(config, timeoutMs, run, override
|
|
|
104
121
|
},
|
|
105
122
|
};
|
|
106
123
|
try {
|
|
124
|
+
releaseIntent = await deps.acquireIntentLock(remainingOperationTimeoutMs('daemon-intent-lock', 'acquiring exclusive intent'));
|
|
107
125
|
suspended = await suspendDaemonForExclusiveAccessLocked(config, remainingOperationTimeoutMs('daemon-suspend', 'suspending the background daemon'), {
|
|
108
126
|
deadlineAtMs: operationDeadlineAtMs,
|
|
109
127
|
dependencies: deps,
|
|
@@ -119,6 +137,12 @@ export async function withDaemonExclusiveAccess(config, timeoutMs, run, override
|
|
|
119
137
|
outcome = { ok: false, error };
|
|
120
138
|
}
|
|
121
139
|
finally {
|
|
140
|
+
try {
|
|
141
|
+
await releaseIntent?.();
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
cleanupError = error;
|
|
145
|
+
}
|
|
122
146
|
if (suspended && restoreAllowed) {
|
|
123
147
|
const suspendedDaemon = suspended;
|
|
124
148
|
try {
|
package/dist/daemon/pid.d.ts
CHANGED
|
@@ -21,3 +21,11 @@ export declare function removePid(expectedInstanceId?: string, options?: {
|
|
|
21
21
|
export declare function isProcessRunning(pid: number): boolean;
|
|
22
22
|
export declare function isLegacyDaemonProcess(pid: number, daemonUptimeMs: number, timeoutMs?: number): Promise<boolean>;
|
|
23
23
|
export declare function acquireDaemonStartLock(timeoutMs?: number): Promise<() => Promise<void>>;
|
|
24
|
+
/** Present while `dino daemon stop` wants the Windows supervisor to stay down. */
|
|
25
|
+
export declare function getSupervisorStopMarkerPath(): string;
|
|
26
|
+
/**
|
|
27
|
+
* Held by a command that needs the PowerSync database in-process, from before
|
|
28
|
+
* it stops the daemon until it has closed the database. A daemon the service
|
|
29
|
+
* manager respawns in between waits for it (see waitForDaemonExclusiveIntent).
|
|
30
|
+
*/
|
|
31
|
+
export declare function acquireDaemonExclusiveIntentLock(timeoutMs?: number): Promise<() => Promise<void>>;
|
package/dist/daemon/pid.js
CHANGED
|
@@ -5,6 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
6
6
|
import { getDataDir } from '../config/paths.js';
|
|
7
7
|
import { acquireExclusiveSqliteLease } from '../utils/sqliteLease.js';
|
|
8
|
+
import { SUPERVISOR_STOP_MARKER } from './supervisor.js';
|
|
8
9
|
const DAEMON_START_LOCK_TIMEOUT_MS = 10_000;
|
|
9
10
|
const LEGACY_DAEMON_UPTIME_TOLERANCE_MS = 60_000;
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
@@ -190,3 +191,22 @@ export async function acquireDaemonStartLock(timeoutMs = DAEMON_START_LOCK_TIMEO
|
|
|
190
191
|
createBusyError: () => new Error('Timed out waiting for daemon start lock'),
|
|
191
192
|
});
|
|
192
193
|
}
|
|
194
|
+
/** Present while `dino daemon stop` wants the Windows supervisor to stay down. */
|
|
195
|
+
export function getSupervisorStopMarkerPath() {
|
|
196
|
+
return join(getDaemonDir(), SUPERVISOR_STOP_MARKER);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Held by a command that needs the PowerSync database in-process, from before
|
|
200
|
+
* it stops the daemon until it has closed the database. A daemon the service
|
|
201
|
+
* manager respawns in between waits for it (see waitForDaemonExclusiveIntent).
|
|
202
|
+
*/
|
|
203
|
+
export async function acquireDaemonExclusiveIntentLock(timeoutMs = 30_000) {
|
|
204
|
+
ensureDaemonDir();
|
|
205
|
+
return acquireExclusiveSqliteLease({
|
|
206
|
+
lockPath: join(getDaemonDir(), 'daemon.exclusive.lock'),
|
|
207
|
+
timeoutMs,
|
|
208
|
+
maxWaitMs: timeoutMs,
|
|
209
|
+
pollIntervalMs: 50,
|
|
210
|
+
createBusyError: () => Object.assign(new Error('Timed out waiting for daemon exclusive intent lock'), { code: 'DAEMON_EXCLUSIVE_BUSY' }),
|
|
211
|
+
});
|
|
212
|
+
}
|
package/dist/daemon/route.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sendDaemonRequest } from './client.js';
|
|
2
2
|
import { ensureDaemonRunning } from './ensure.js';
|
|
3
3
|
import { DinoxError } from '../utils/errors.js';
|
|
4
|
+
import { localCommandExecution, requestContext } from '../utils/requestContext.js';
|
|
4
5
|
/**
|
|
5
6
|
* Default online reads use the long-lived daemon as the canonical DB owner. The
|
|
6
7
|
* process-local path is only for explicit modes that require opening the cache here:
|
|
@@ -17,7 +18,7 @@ export function shouldUseDaemon(globals) {
|
|
|
17
18
|
* receives a thin sender bound to the daemon; `localRun` opens its own db/sync path.
|
|
18
19
|
*/
|
|
19
20
|
export async function routeRead(globals, daemonMethod, daemonParams, localRun) {
|
|
20
|
-
if (!shouldUseDaemon(globals)) {
|
|
21
|
+
if (requestContext.getStore()?.daemon || localCommandExecution.getStore() || !shouldUseDaemon(globals)) {
|
|
21
22
|
return localRun();
|
|
22
23
|
}
|
|
23
24
|
try {
|
package/dist/daemon/server.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
1
2
|
import type { DaemonRequest, DaemonResponse } from './types.js';
|
|
3
|
+
import { type ConnectivityChange } from './connectivity.js';
|
|
4
|
+
import type { ResolvedDinoxConfig } from '../config/types.js';
|
|
5
|
+
import { connectPowerSync } from '../powersync/runtime.js';
|
|
6
|
+
import { acquireDaemonExclusiveIntentLock } from './pid.js';
|
|
7
|
+
/** EX_TEMPFAIL: the service manager restarts the daemon. */
|
|
8
|
+
export declare const DAEMON_RESTART_EXIT_CODE = 75;
|
|
2
9
|
export declare class DaemonServer {
|
|
10
|
+
private readonly options;
|
|
11
|
+
private config;
|
|
3
12
|
private db;
|
|
4
13
|
private server;
|
|
5
14
|
private startTime;
|
|
@@ -12,11 +21,46 @@ export declare class DaemonServer {
|
|
|
12
21
|
private ownsArtifacts;
|
|
13
22
|
private releaseOwner;
|
|
14
23
|
private stopPromise;
|
|
24
|
+
private healthTimer;
|
|
25
|
+
private connectivityTimer;
|
|
26
|
+
private reconnect;
|
|
27
|
+
private reconnecting;
|
|
28
|
+
private syncUnconfirmedSince;
|
|
15
29
|
private gateways;
|
|
30
|
+
constructor(options?: {
|
|
31
|
+
runtime?: {
|
|
32
|
+
db: AbstractPowerSyncDatabase;
|
|
33
|
+
config: ResolvedDinoxConfig;
|
|
34
|
+
};
|
|
35
|
+
acquireIntentLock?: typeof acquireDaemonExclusiveIntentLock;
|
|
36
|
+
connect?: typeof connectPowerSync;
|
|
37
|
+
onStopRequested?: (reason: string, details?: Record<string, unknown>) => void | Promise<void>;
|
|
38
|
+
});
|
|
16
39
|
initialize(): Promise<void>;
|
|
17
40
|
handleRequest(req: DaemonRequest): Promise<DaemonResponse>;
|
|
18
41
|
private isCacheStale;
|
|
19
42
|
start(port: number): Promise<void>;
|
|
20
43
|
stop(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* PowerSync reconnects on its own; a connection that stays failed for this
|
|
46
|
+
* long usually needs a fresh process (for example a rotated credential), so
|
|
47
|
+
* exit for the service manager to restart it.
|
|
48
|
+
*/
|
|
49
|
+
private startHealthCheck;
|
|
50
|
+
private startConnectivityWatch;
|
|
51
|
+
/**
|
|
52
|
+
* After a wake from sleep or a network change the stream may be a dead
|
|
53
|
+
* connection that still reports "connected". Reconnect, and treat the cache
|
|
54
|
+
* as unconfirmed until a checkpoint completes on the new stream.
|
|
55
|
+
*/
|
|
56
|
+
handleConnectivityChange(change: ConnectivityChange): Promise<void>;
|
|
57
|
+
/** The reconnect time while no checkpoint has completed since; null once confirmed. */
|
|
58
|
+
currentSyncUnconfirmedSince(): string | null;
|
|
21
59
|
private stopInternal;
|
|
22
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Blocks until no exclusive command holds the intent lock. A respawned
|
|
63
|
+
* daemon must not take the database from the command that just stopped it;
|
|
64
|
+
* the lease is released by the OS if that command dies.
|
|
65
|
+
*/
|
|
66
|
+
export declare function waitForDaemonExclusiveIntent(acquire?: typeof acquireDaemonExclusiveIntentLock): Promise<void>;
|