openworkgraph 0.3.0 → 0.3.1
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/operations/daemon.js +8 -2
- package/dist/operations/update.d.ts +2 -0
- package/dist/operations/update.js +30 -11
- package/dist/service.d.ts +1 -0
- package/dist/service.js +21 -5
- package/dist/vendor/protocol/index.d.ts +2 -2
- package/dist/vendor/protocol/index.js +1 -1
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ import { waitForBackups, clearStaleResourceLeases } from './backups.js';
|
|
|
7
7
|
import { localCapacity } from './capacity.js';
|
|
8
8
|
import { registerLocalPlugin } from './plugins.js';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { isGlobalNpmInstallation, startWebUpdate } from './update.js';
|
|
10
|
+
import { isGlobalNpmInstallation, isNpxInstallation, startWebUpdate } from './update.js';
|
|
11
11
|
export function activeRunCount(db) {
|
|
12
12
|
return Number(db.prepare("SELECT count(*) AS n FROM runs WHERE status NOT IN ('accepted','queued','paused_restore','succeeded','failed','cancelled','interrupted')").get().n);
|
|
13
13
|
}
|
|
@@ -17,9 +17,11 @@ export async function serveManaged(dataDir, host, port, onStopped) {
|
|
|
17
17
|
let service;
|
|
18
18
|
const cliPath = fileURLToPath(new URL('../cli.js', import.meta.url));
|
|
19
19
|
const npmInstallation = isGlobalNpmInstallation(cliPath);
|
|
20
|
+
const npxInstallation = npmInstallation.then(global => global ? false : isNpxInstallation(cliPath));
|
|
20
21
|
try {
|
|
21
22
|
service = createService(lock.root, {
|
|
22
23
|
npmInstallation: () => npmInstallation,
|
|
24
|
+
npxInstallation: () => npxInstallation,
|
|
23
25
|
updateRuntime: async (onExit) => {
|
|
24
26
|
if (state !== 'running')
|
|
25
27
|
throw new Error('Runtime is stopping.');
|
|
@@ -40,10 +42,13 @@ export async function serveManaged(dataDir, host, port, onStopped) {
|
|
|
40
42
|
let stopping = false;
|
|
41
43
|
const drains = new Set();
|
|
42
44
|
let closing;
|
|
45
|
+
let listenAddress;
|
|
43
46
|
let recoveryAttempted = false;
|
|
44
47
|
const onSignal = () => { void stop(false).catch(() => log.append('shutdown_failed')); };
|
|
45
48
|
const removeSignals = () => { process.removeListener('SIGINT', onSignal); process.removeListener('SIGTERM', onSignal); };
|
|
46
|
-
|
|
49
|
+
// server.address() becomes null as soon as HTTP shutdown begins; local
|
|
50
|
+
// management must remain available until connections close and locks release.
|
|
51
|
+
const status = () => ({ state, activeRuns: closing ? 0 : activeRunCount(service.db), listenHost: listenAddress.address, ...listenerInfo(listenAddress, service.info.serviceId, service.instanceId) });
|
|
47
52
|
async function stop(interrupt) {
|
|
48
53
|
// Escalation remains available while an earlier drain waits indefinitely.
|
|
49
54
|
if (stopping && (!interrupt || state === 'interrupting'))
|
|
@@ -91,6 +96,7 @@ export async function serveManaged(dataDir, host, port, onStopped) {
|
|
|
91
96
|
clearStaleResourceLeases(service.db, lock);
|
|
92
97
|
await service.api.backups.recoverCreating(lock);
|
|
93
98
|
await new Promise((done, reject) => { service.server.once('error', reject); service.server.listen(port, host, done); });
|
|
99
|
+
listenAddress = service.server.address();
|
|
94
100
|
// Own the kernel lock before transferring scheduler epochs. Runtime.start
|
|
95
101
|
// resets acceptingRuns after a normal shutdown and starts its dispatch timer.
|
|
96
102
|
recoveryAttempted = true;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { readInstance, requestInstance, startBackground, waitUntilStopped } from './lifecycle.js';
|
|
3
|
+
/** Recognize the package actually running from npm exec/npx's cache, including custom cache roots. */
|
|
4
|
+
export declare function isNpxInstallation(cliPath: string, spawnProcess?: SpawnProcess): Promise<boolean>;
|
|
3
5
|
/** Compare the running package against npm's actual global package location. */
|
|
4
6
|
export declare function isGlobalNpmInstallation(cliPath: string, spawnProcess?: SpawnProcess): Promise<boolean>;
|
|
5
7
|
/** The updater must outlive the Runtime that it stops. */
|
|
@@ -4,20 +4,39 @@ import { join, relative, isAbsolute, sep } from 'node:path';
|
|
|
4
4
|
import { SERVICE_VERSION } from "../vendor/protocol/index.js";
|
|
5
5
|
import { readInstance, requestInstance, startBackground, waitUntilStopped } from './lifecycle.js';
|
|
6
6
|
const PACKAGE_NAME = 'openworkgraph';
|
|
7
|
+
async function npmPath(args, spawnProcess) {
|
|
8
|
+
const invocation = process.platform === 'win32'
|
|
9
|
+
? { command: process.env.ComSpec ?? process.env.COMSPEC ?? 'cmd.exe', args: ['/d', '/s', '/c', 'npm ' + args.join(' ')] }
|
|
10
|
+
: { command: 'npm', args };
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const child = spawnProcess(invocation.command, invocation.args, { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 });
|
|
13
|
+
let output = '';
|
|
14
|
+
child.stdout?.on('data', chunk => { if (output.length < 8192)
|
|
15
|
+
output += chunk.toString('utf8'); });
|
|
16
|
+
child.once('error', reject);
|
|
17
|
+
child.once('close', code => code === 0 ? resolve(output.trim()) : reject(new Error('npm path query failed')));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** Recognize the package actually running from npm exec/npx's cache, including custom cache roots. */
|
|
21
|
+
export async function isNpxInstallation(cliPath, spawnProcess = spawn) {
|
|
22
|
+
try {
|
|
23
|
+
const cache = await npmPath(['config', 'get', 'cache'], spawnProcess);
|
|
24
|
+
if (!cache)
|
|
25
|
+
return false;
|
|
26
|
+
const npxRoot = await realpath(join(cache, '_npx'));
|
|
27
|
+
const runningPath = await realpath(cliPath);
|
|
28
|
+
const inside = relative(npxRoot, runningPath).split(sep);
|
|
29
|
+
return inside.length === 5 && inside[0] !== '..' && inside[0] !== '' && !isAbsolute(inside[0])
|
|
30
|
+
&& inside[1] === 'node_modules' && inside[2] === PACKAGE_NAME && inside[3] === 'dist' && inside[4] === 'cli.js';
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
7
36
|
/** Compare the running package against npm's actual global package location. */
|
|
8
37
|
export async function isGlobalNpmInstallation(cliPath, spawnProcess = spawn) {
|
|
9
|
-
const invocation = process.platform === 'win32'
|
|
10
|
-
? { command: process.env.ComSpec ?? process.env.COMSPEC ?? 'cmd.exe', args: ['/d', '/s', '/c', 'npm root -g'] }
|
|
11
|
-
: { command: 'npm', args: ['root', '-g'] };
|
|
12
38
|
try {
|
|
13
|
-
const root = await
|
|
14
|
-
const child = spawnProcess(invocation.command, invocation.args, { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 });
|
|
15
|
-
let output = '';
|
|
16
|
-
child.stdout?.on('data', chunk => { if (output.length < 8192)
|
|
17
|
-
output += chunk.toString('utf8'); });
|
|
18
|
-
child.once('error', reject);
|
|
19
|
-
child.once('close', code => code === 0 ? resolve(output.trim()) : reject(new Error('npm root failed')));
|
|
20
|
-
});
|
|
39
|
+
const root = await npmPath(['root', '-g'], spawnProcess);
|
|
21
40
|
if (!root)
|
|
22
41
|
return false;
|
|
23
42
|
const installedPath = join(root, PACKAGE_NAME);
|
package/dist/service.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare function createService(dataDir?: string, options?: {
|
|
|
11
11
|
projectProbe?: ProjectProbe;
|
|
12
12
|
runtime?: WorkflowRuntimeOptions;
|
|
13
13
|
npmInstallation?: () => Promise<boolean>;
|
|
14
|
+
npxInstallation?: () => Promise<boolean>;
|
|
14
15
|
latestNpmVersion?: () => Promise<string>;
|
|
15
16
|
updateRuntime?: (onExit: () => void) => Promise<void>;
|
|
16
17
|
}): {
|
package/dist/service.js
CHANGED
|
@@ -58,6 +58,7 @@ export function createService(dataDir, options = {}) {
|
|
|
58
58
|
const events = new Events(db, serviceId, auth);
|
|
59
59
|
const instanceId = randomUUID();
|
|
60
60
|
const npmInstallation = options.npmInstallation ?? (async () => false);
|
|
61
|
+
const npxInstallation = options.npxInstallation ?? (async () => false);
|
|
61
62
|
let updateQueued = false;
|
|
62
63
|
const unavailable = {
|
|
63
64
|
status: "unavailable",
|
|
@@ -202,7 +203,7 @@ export function createService(dataDir, options = {}) {
|
|
|
202
203
|
const runtimeInfo = runtime.serviceInfo;
|
|
203
204
|
send({
|
|
204
205
|
...info,
|
|
205
|
-
installation: await npmInstallation() ? 'npm-global' : 'other',
|
|
206
|
+
installation: await npmInstallation() ? 'npm-global' : await npxInstallation() ? 'npx' : 'other',
|
|
206
207
|
imageRoutes: runtimeInfo.imageRoutes,
|
|
207
208
|
capabilities: {
|
|
208
209
|
...info.capabilities,
|
|
@@ -470,13 +471,29 @@ export function createService(dataDir, options = {}) {
|
|
|
470
471
|
db.prepare("INSERT INTO settings(key,value) VALUES('listener',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(JSON.stringify(listenerInfo(address, serviceId, instanceId)));
|
|
471
472
|
});
|
|
472
473
|
let closing;
|
|
474
|
+
async function closeHttpServer() {
|
|
475
|
+
if (!server.listening)
|
|
476
|
+
return;
|
|
477
|
+
await new Promise((resolve, reject) => {
|
|
478
|
+
// Runs and event streams have already drained. A browser's unfinished
|
|
479
|
+
// request must not hold the service's lifetime locks indefinitely.
|
|
480
|
+
const timeout = setTimeout(() => server.closeAllConnections(), 1000);
|
|
481
|
+
timeout.unref();
|
|
482
|
+
server.close(error => {
|
|
483
|
+
clearTimeout(timeout);
|
|
484
|
+
if (error)
|
|
485
|
+
reject(error);
|
|
486
|
+
else
|
|
487
|
+
resolve();
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
}
|
|
473
491
|
async function abortStartup() {
|
|
474
492
|
runtime.disposeUnstarted();
|
|
475
493
|
projectProbe.close();
|
|
476
494
|
events.close();
|
|
477
495
|
try {
|
|
478
|
-
|
|
479
|
-
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
496
|
+
await closeHttpServer();
|
|
480
497
|
}
|
|
481
498
|
finally {
|
|
482
499
|
db.close();
|
|
@@ -489,8 +506,7 @@ export function createService(dataDir, options = {}) {
|
|
|
489
506
|
projectProbe.close();
|
|
490
507
|
events.close();
|
|
491
508
|
try {
|
|
492
|
-
|
|
493
|
-
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
509
|
+
await closeHttpServer();
|
|
494
510
|
db.prepare("DELETE FROM settings WHERE key='listener' AND json_extract(value,'$.instanceId')=?").run(instanceId);
|
|
495
511
|
}
|
|
496
512
|
finally {
|
|
@@ -5,7 +5,7 @@ export * from './execution-chain.js';
|
|
|
5
5
|
export * from './graph-title.js';
|
|
6
6
|
export * from './graph-binary.js';
|
|
7
7
|
export declare const PROTOCOL_VERSION: '1.0';
|
|
8
|
-
export declare const SERVICE_VERSION: '0.3.
|
|
8
|
+
export declare const SERVICE_VERSION: '0.3.1';
|
|
9
9
|
/** Work Graph uploads use the UI's binary MB convention. */
|
|
10
10
|
export declare const WORKGRAPH_UPLOAD_MAX_BYTES: number;
|
|
11
11
|
export declare const WORKGRAPH_TRANSFER_TOTAL_BYTES: number;
|
|
@@ -57,7 +57,7 @@ export interface ServiceInfo {
|
|
|
57
57
|
serviceId: string;
|
|
58
58
|
version: string;
|
|
59
59
|
protocolVersion: string;
|
|
60
|
-
installation?: 'npm-global' | 'other';
|
|
60
|
+
installation?: 'npm-global' | 'npx' | 'other';
|
|
61
61
|
capabilities: Record<CoreCapabilityName, Capability> & Partial<Record<ProjectFileCapabilityName, Capability>>;
|
|
62
62
|
imageRoutes?: ImageRouteCapability[];
|
|
63
63
|
}
|
|
@@ -5,7 +5,7 @@ export * from './execution-chain.js';
|
|
|
5
5
|
export * from './graph-title.js';
|
|
6
6
|
export * from './graph-binary.js';
|
|
7
7
|
export const PROTOCOL_VERSION = '1.0';
|
|
8
|
-
export const SERVICE_VERSION = '0.3.
|
|
8
|
+
export const SERVICE_VERSION = '0.3.1';
|
|
9
9
|
/** Work Graph uploads use the UI's binary MB convention. */
|
|
10
10
|
export const WORKGRAPH_UPLOAD_MAX_BYTES = 300 * 1024 * 1024;
|
|
11
11
|
export const WORKGRAPH_TRANSFER_TOTAL_BYTES = 1024 * 1024 * 1024;
|
package/package.json
CHANGED