@travetto/runtime 7.1.1 → 7.1.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/README.md +2 -9
- package/package.json +3 -3
- package/src/context.ts +0 -5
- package/src/exec.ts +7 -19
- package/src/shutdown.ts +59 -53
- package/src/trv.d.ts +0 -5
- package/src/watch.ts +10 -35
package/README.md
CHANGED
|
@@ -51,8 +51,6 @@ class $Runtime {
|
|
|
51
51
|
get monoRoot(): boolean;
|
|
52
52
|
/** Main source path */
|
|
53
53
|
get mainSourcePath(): string;
|
|
54
|
-
/** Get trv entrypoint */
|
|
55
|
-
get trvEntryPoint(): string;
|
|
56
54
|
/** Produce a workspace relative path */
|
|
57
55
|
workspaceRelative(...parts: string[]): string;
|
|
58
56
|
/** Strip off the workspace path from a file */
|
|
@@ -108,11 +106,6 @@ interface EnvData {
|
|
|
108
106
|
* @default 2s
|
|
109
107
|
*/
|
|
110
108
|
TRV_SHUTDOWN_WAIT: TimeSpan | number;
|
|
111
|
-
/**
|
|
112
|
-
* The time to wait for stdout to drain during shutdown
|
|
113
|
-
* @default 0s
|
|
114
|
-
*/
|
|
115
|
-
TRV_SHUTDOWN_STDOUT_WAIT: TimeSpan | number;
|
|
116
109
|
/**
|
|
117
110
|
* The desired runtime module
|
|
118
111
|
*/
|
|
@@ -328,7 +321,7 @@ export class TimeUtil {
|
|
|
328
321
|
```
|
|
329
322
|
|
|
330
323
|
## Process Execution
|
|
331
|
-
[ExecUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/exec.ts#
|
|
324
|
+
[ExecUtil](https://github.com/travetto/travetto/tree/main/module/runtime/src/exec.ts#L42) exposes `getResult` as a means to wrap [child_process](https://nodejs.org/api/child_process.html)'s process object. This wrapper allows for a promise-based resolution of the subprocess with the ability to capture the stderr/stdout.
|
|
332
325
|
|
|
333
326
|
A simple example would be:
|
|
334
327
|
|
|
@@ -353,7 +346,7 @@ As a registered shutdown handler, you can do.
|
|
|
353
346
|
import { ShutdownManager } from '@travetto/runtime';
|
|
354
347
|
|
|
355
348
|
export function registerShutdownHandler() {
|
|
356
|
-
ShutdownManager.
|
|
349
|
+
ShutdownManager.signal.addEventListener('abort', () => {
|
|
357
350
|
// Do important work, the framework will wait until all async
|
|
358
351
|
// operations are completed before finishing shutdown
|
|
359
352
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/runtime",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime for travetto applications.",
|
|
6
6
|
"keywords": [
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"directory": "module/runtime"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/manifest": "^7.1.
|
|
29
|
+
"@travetto/manifest": "^7.1.2",
|
|
30
30
|
"@types/debug": "^4.1.12",
|
|
31
31
|
"debug": "^4.4.3"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@travetto/transformer": "^7.1.
|
|
34
|
+
"@travetto/transformer": "^7.1.2"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@travetto/transformer": {
|
package/src/context.ts
CHANGED
|
@@ -68,11 +68,6 @@ class $Runtime {
|
|
|
68
68
|
return this.#idx.mainModule.sourcePath;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/** Get trv entrypoint */
|
|
72
|
-
get trvEntryPoint(): string {
|
|
73
|
-
return this.workspaceRelative('node_modules', '.bin', 'trv');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
71
|
/** Produce a workspace relative path */
|
|
77
72
|
workspaceRelative(...parts: string[]): string {
|
|
78
73
|
return path.resolve(this.workspace.path, ...parts);
|
package/src/exec.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { type ChildProcess } from 'node:child_process';
|
|
1
|
+
import { type ChildProcess, spawn, type SpawnOptions } from 'node:child_process';
|
|
2
2
|
import type { Readable } from 'node:stream';
|
|
3
3
|
import { createInterface } from 'node:readline/promises';
|
|
4
4
|
|
|
5
5
|
import { castTo } from './types.ts';
|
|
6
|
+
import { RuntimeIndex } from './manifest-index.ts';
|
|
6
7
|
|
|
7
8
|
const ResultSymbol = Symbol();
|
|
8
9
|
|
|
@@ -39,24 +40,6 @@ type ExecutionBaseResult = Omit<ExecutionResult, 'stdout' | 'stderr'>;
|
|
|
39
40
|
*/
|
|
40
41
|
export class ExecUtil {
|
|
41
42
|
|
|
42
|
-
/**
|
|
43
|
-
* Defer control to subprocess execution, mainly used for nested execution
|
|
44
|
-
*/
|
|
45
|
-
static async deferToSubprocess(child: ChildProcess): Promise<ExecutionResult> {
|
|
46
|
-
const messageToChild = (value: unknown): void => { child.send(value!); };
|
|
47
|
-
const messageToParent = (value: unknown): void => { process.send?.(value); };
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
process.on('message', messageToChild);
|
|
51
|
-
child.on('message', messageToParent);
|
|
52
|
-
const result = await this.getResult(child, { catch: true });
|
|
53
|
-
process.exitCode = child.exitCode;
|
|
54
|
-
return result;
|
|
55
|
-
} finally {
|
|
56
|
-
process.off('message', messageToChild);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
43
|
/**
|
|
61
44
|
* Take a child process, and some additional options, and produce a promise that
|
|
62
45
|
* represents the entire execution. On successful completion the promise will resolve, and
|
|
@@ -128,4 +111,9 @@ export class ExecUtil {
|
|
|
128
111
|
await handler(item);
|
|
129
112
|
}
|
|
130
113
|
}
|
|
114
|
+
|
|
115
|
+
/** Spawn a package command */
|
|
116
|
+
static spawnPackageCommand(cmd: string, args: string[], config: SpawnOptions = {}): ChildProcess {
|
|
117
|
+
return spawn(process.argv0, [RuntimeIndex.resolvePackageCommand(cmd), ...args], config);
|
|
118
|
+
}
|
|
131
119
|
}
|
package/src/shutdown.ts
CHANGED
|
@@ -1,99 +1,105 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
|
|
1
3
|
import { Env } from './env.ts';
|
|
2
4
|
import { Util } from './util.ts';
|
|
3
5
|
import { TimeUtil } from './time.ts';
|
|
4
6
|
|
|
7
|
+
const MAPPING = [['restart', 200], ['error', 1], ['quit', 0]] as const;
|
|
8
|
+
export type ShutdownReason = typeof MAPPING[number][0];
|
|
9
|
+
|
|
10
|
+
const REASON_TO_CODE = new Map<ShutdownReason, number>(MAPPING);
|
|
11
|
+
const CODE_TO_REASON = new Map<number, ShutdownReason>(MAPPING.map(([k, v]) => [v, k]));
|
|
12
|
+
|
|
5
13
|
type Handler = (event: Event) => unknown;
|
|
14
|
+
type ShutdownEvent = { reason?: ShutdownReason, mode?: 'exit' | 'interrupt' };
|
|
15
|
+
|
|
16
|
+
const isShutdownEvent = (event: unknown): event is ShutdownEvent =>
|
|
17
|
+
typeof event === 'object' && event !== null && 'type' in event && event.type === 'shutdown';
|
|
18
|
+
|
|
19
|
+
const wrapped = async (handler: Handler): Promise<void> => {
|
|
20
|
+
try {
|
|
21
|
+
await handler(new Event('abort'));
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error('Error during shutdown handler', err);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
6
26
|
|
|
7
27
|
/**
|
|
8
28
|
* Shutdown manager, allowing for listening for graceful shutdowns
|
|
9
29
|
*/
|
|
10
30
|
export class ShutdownManager {
|
|
11
|
-
|
|
12
31
|
static #shouldIgnoreInterrupt = false;
|
|
13
|
-
static #registered = new
|
|
14
|
-
static #pending: Function[] = [];
|
|
32
|
+
static #registered = new Set<Handler>();
|
|
15
33
|
static #controller = new AbortController();
|
|
16
|
-
static #startedAt: number = 0;
|
|
17
|
-
static #addListener = this.#controller.signal.addEventListener.bind(this.#controller.signal);
|
|
18
|
-
static #removeListener = this.#controller.signal.removeEventListener.bind(this.#controller.signal);
|
|
19
34
|
|
|
20
35
|
static {
|
|
21
|
-
this.#controller.signal.addEventListener = (
|
|
22
|
-
this.#controller.signal.removeEventListener = (
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
this.#controller.signal.addEventListener = (_: 'abort', listener: Handler): void => { this.#registered.add(listener); };
|
|
37
|
+
this.#controller.signal.removeEventListener = (_: 'abort', listener: Handler): void => { this.#registered.delete(listener); };
|
|
38
|
+
try {
|
|
39
|
+
process
|
|
40
|
+
.on('message', event => { isShutdownEvent(event) && this.shutdown(event); })
|
|
41
|
+
.on('SIGINT', () => this.shutdown({ mode: 'interrupt' }))
|
|
42
|
+
.on('SIGTERM', () => this.shutdown());
|
|
43
|
+
} catch { }
|
|
25
44
|
}
|
|
26
45
|
|
|
27
46
|
static get signal(): AbortSignal {
|
|
28
47
|
return this.#controller.signal;
|
|
29
48
|
}
|
|
30
49
|
|
|
31
|
-
|
|
50
|
+
/** Disable SIGINT interrupt handling */
|
|
51
|
+
static disableInterrupt(): typeof ShutdownManager {
|
|
32
52
|
this.#shouldIgnoreInterrupt = true;
|
|
53
|
+
return this;
|
|
33
54
|
}
|
|
34
55
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.on('SIGUSR2', () => this.shutdown('SIGUSR2'))
|
|
40
|
-
.on('SIGTERM', () => this.shutdown('SIGTERM'));
|
|
41
|
-
}
|
|
56
|
+
/** Convert exit code to a reason string */
|
|
57
|
+
static reasonForExitCode(code: number): ShutdownReason {
|
|
58
|
+
return CODE_TO_REASON.get(code) ?? 'error';
|
|
59
|
+
}
|
|
42
60
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
/** Trigger a watch signal signal to a subprocess */
|
|
62
|
+
static async shutdownChild(subprocess: ChildProcess, config?: ShutdownEvent): Promise<void> {
|
|
63
|
+
subprocess?.send!({ type: 'shutdown', ...config });
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
/**
|
|
49
67
|
* Shutdown the application gracefully
|
|
50
68
|
*/
|
|
51
|
-
static async shutdown(
|
|
52
|
-
if (
|
|
69
|
+
static async shutdown({ reason = 'quit', mode = undefined }: ShutdownEvent = {}): Promise<void> {
|
|
70
|
+
if ((mode === 'interrupt' && this.#shouldIgnoreInterrupt) || this.#controller.signal.aborted) {
|
|
53
71
|
return;
|
|
54
72
|
}
|
|
55
|
-
if (this.#controller.signal.aborted) {
|
|
56
|
-
if (this.#startedAt && (Date.now() - this.#startedAt) > 500) {
|
|
57
|
-
console.warn('Shutdown already in progress, exiting immediately', { source });
|
|
58
|
-
process.exit(0); // Quit immediately
|
|
59
|
-
} else {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
73
|
|
|
64
|
-
|
|
74
|
+
process // Allow shutdown if anything is still listening
|
|
75
|
+
.removeAllListeners('message')
|
|
76
|
+
.removeAllListeners('SIGINT')
|
|
77
|
+
.removeAllListeners('SIGTERM');
|
|
78
|
+
|
|
79
|
+
if (mode === 'interrupt' && process.stdout.isTTY) {
|
|
65
80
|
process.stdout.write('\n');
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
process.
|
|
69
|
-
|
|
70
|
-
const context = source ? [{ source, pid: process.pid }] : [{ pid: process.pid }];
|
|
71
|
-
this.#startedAt = Date.now();
|
|
72
|
-
this.#controller.abort('Shutdown started');
|
|
73
|
-
await Util.queueMacroTask(); // Force the event loop to wait one cycle
|
|
74
|
-
|
|
75
|
-
console.debug('Shutdown started', ...context, { pending: this.#pending.length });
|
|
83
|
+
process.exitCode ??= REASON_TO_CODE.get(reason);
|
|
76
84
|
|
|
77
85
|
const timeout = TimeUtil.fromValue(Env.TRV_SHUTDOWN_WAIT.value) ?? 2000;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
})));
|
|
82
|
-
|
|
83
|
-
const winner = await Promise.race([timeoutTasks, allPendingTasks]);
|
|
86
|
+
const context = { reason, mode, pid: process.pid, timeout, pending: this.#registered.size };
|
|
87
|
+
this.#controller.abort('Shutdown started');
|
|
88
|
+
console.debug('Shutdown started', context);
|
|
84
89
|
|
|
85
|
-
|
|
90
|
+
const winner = await Promise.race([
|
|
91
|
+
Util.nonBlockingTimeout(timeout).then(() => this),
|
|
92
|
+
Promise.all([...this.#registered].map(wrapped))
|
|
93
|
+
]);
|
|
86
94
|
|
|
87
95
|
if (winner !== this) {
|
|
88
|
-
console.debug('Shutdown completed',
|
|
96
|
+
console.debug('Shutdown completed', context);
|
|
89
97
|
} else {
|
|
90
|
-
console.warn('Shutdown timed out',
|
|
91
|
-
process.exit(1); // Indicate error on timeout
|
|
98
|
+
console.warn('Shutdown timed out', context);
|
|
92
99
|
}
|
|
93
100
|
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
await Util.blockingTimeout(stdoutDrain);
|
|
101
|
+
if (mode === 'exit') {
|
|
102
|
+
process.exit();
|
|
97
103
|
}
|
|
98
104
|
}
|
|
99
105
|
}
|
package/src/trv.d.ts
CHANGED
|
@@ -31,11 +31,6 @@ declare module "@travetto/runtime" {
|
|
|
31
31
|
* @default 2s
|
|
32
32
|
*/
|
|
33
33
|
TRV_SHUTDOWN_WAIT: TimeSpan | number;
|
|
34
|
-
/**
|
|
35
|
-
* The time to wait for stdout to drain during shutdown
|
|
36
|
-
* @default 0s
|
|
37
|
-
*/
|
|
38
|
-
TRV_SHUTDOWN_STDOUT_WAIT: TimeSpan | number;
|
|
39
34
|
/**
|
|
40
35
|
* The desired runtime module
|
|
41
36
|
*/
|
package/src/watch.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import type { ChildProcess } from 'node:child_process';
|
|
2
|
-
|
|
3
1
|
import type { CompilerEventPayload, CompilerEventType } from '@travetto/compiler';
|
|
4
2
|
|
|
5
3
|
import { AppError } from './error.ts';
|
|
6
4
|
import { Util } from './util.ts';
|
|
7
5
|
import { RuntimeIndex } from './manifest-index.ts';
|
|
8
|
-
import { ShutdownManager } from './shutdown.ts';
|
|
6
|
+
import { ShutdownManager, type ShutdownReason } from './shutdown.ts';
|
|
9
7
|
import { castTo } from './types.ts';
|
|
10
8
|
|
|
11
9
|
type RetryRunState = {
|
|
12
10
|
iteration: number;
|
|
13
11
|
startTime: number;
|
|
14
12
|
errorIterations: number;
|
|
15
|
-
result?:
|
|
13
|
+
result?: ShutdownReason;
|
|
16
14
|
};
|
|
17
15
|
|
|
18
16
|
type RetryRunConfig = {
|
|
@@ -27,34 +25,6 @@ type RetryRunConfig = {
|
|
|
27
25
|
*/
|
|
28
26
|
export class WatchUtil {
|
|
29
27
|
|
|
30
|
-
static #RESTART_EXIT_CODE = 200;
|
|
31
|
-
|
|
32
|
-
/** Convert exit code to a result type */
|
|
33
|
-
static exitCodeToResult(code: number): RetryRunState['result'] {
|
|
34
|
-
return code === this.#RESTART_EXIT_CODE ? 'restart' : (code !== null && code > 0) ? 'error' : 'stop';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Exit with a restart exit code */
|
|
38
|
-
static exitWithRestart(): void {
|
|
39
|
-
ShutdownManager.shutdown('SIGTERM', this.#RESTART_EXIT_CODE);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Listen for restart signals */
|
|
43
|
-
static listenForSignals(): void {
|
|
44
|
-
ShutdownManager.disableInterrupt();
|
|
45
|
-
process.on('message', event => {
|
|
46
|
-
switch (event) {
|
|
47
|
-
case 'WATCH_RESTART': this.exitWithRestart(); break;
|
|
48
|
-
case 'WATCH_SHUTDOWN': ShutdownManager.shutdown('SIGTERM', 0); break;
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Trigger a watch signal signal to a subprocess */
|
|
54
|
-
static triggerSignal(subprocess: ChildProcess, signal: 'WATCH_RESTART' | 'WATCH_SHUTDOWN'): void {
|
|
55
|
-
subprocess.connected && subprocess.send?.(signal);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
28
|
/** Compute the delay before restarting */
|
|
59
29
|
static computeRestartDelay(state: RetryRunState, config: RetryRunConfig): number {
|
|
60
30
|
return state.result === 'error'
|
|
@@ -65,7 +35,7 @@ export class WatchUtil {
|
|
|
65
35
|
/**
|
|
66
36
|
* Run with restart capability
|
|
67
37
|
*/
|
|
68
|
-
static async runWithRetry(run: (state: RetryRunState & { signal: AbortSignal }) => Promise<
|
|
38
|
+
static async runWithRetry(run: (state: RetryRunState & { signal: AbortSignal }) => Promise<ShutdownReason>, options?: Partial<RetryRunConfig>): Promise<void> {
|
|
69
39
|
let retryExhausted = false;
|
|
70
40
|
|
|
71
41
|
const state: RetryRunState = {
|
|
@@ -89,7 +59,7 @@ export class WatchUtil {
|
|
|
89
59
|
|
|
90
60
|
state.result = await run({ ...state, signal: ShutdownManager.signal }).catch(() => 'error' as const);
|
|
91
61
|
switch (state.result) {
|
|
92
|
-
case '
|
|
62
|
+
case 'quit': break outer;
|
|
93
63
|
case 'error': state.errorIterations += 1; break;
|
|
94
64
|
case 'restart': {
|
|
95
65
|
state.startTime = Date.now();
|
|
@@ -114,7 +84,12 @@ export class WatchUtil {
|
|
|
114
84
|
options?: Partial<RetryRunConfig>,
|
|
115
85
|
): Promise<void> {
|
|
116
86
|
const { CompilerClient } = await import('@travetto/compiler/src/server/client.ts');
|
|
117
|
-
const client = new CompilerClient(RuntimeIndex.manifest,
|
|
87
|
+
const client = new CompilerClient(RuntimeIndex.manifest, {
|
|
88
|
+
debug: (...args: unknown[]): void => console.debug(...args),
|
|
89
|
+
info: (...args: unknown[]): void => console.info(...args),
|
|
90
|
+
warn: (...args: unknown[]): void => console.warn(...args),
|
|
91
|
+
error: (...args: unknown[]): void => console.error(...args),
|
|
92
|
+
});
|
|
118
93
|
|
|
119
94
|
return this.runWithRetry(async ({ signal }) => {
|
|
120
95
|
await client.waitForState(['compile-end', 'watch-start'], undefined, signal);
|