@flowscripter/dynamic-cli-framework 9.0.4 → 9.1.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/src/cli/BaseCLI.d.ts.map +1 -1
- package/dist/src/cli/BaseCLI.js +2 -0
- package/dist/src/runtime/lifecycle/BackgroundTasks.d.ts +21 -0
- package/dist/src/runtime/lifecycle/BackgroundTasks.d.ts.map +1 -0
- package/dist/src/runtime/lifecycle/BackgroundTasks.js +69 -0
- package/dist/src/runtime/runner.d.ts +2 -2
- package/dist/src/runtime/runner.d.ts.map +1 -1
- package/dist/src/runtime/runner.js +4 -5
- package/dist/src/util/helpHelper.d.ts.map +1 -1
- package/dist/src/util/helpHelper.js +9 -6
- package/package.json +1 -1
- package/src/cli/BaseCLI.ts +2 -0
- package/src/runtime/lifecycle/BackgroundTasks.ts +96 -0
- package/src/runtime/runner.ts +4 -5
- package/src/util/helpHelper.ts +11 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BaseCLI.d.ts","sourceRoot":"","sources":["../../../src/cli/BaseCLI.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,yCAAyC,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAGvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAsBzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"BaseCLI.d.ts","sourceRoot":"","sources":["../../../src/cli/BaseCLI.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,yCAAyC,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAGvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAsBzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAU3E,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AAGpD,OAAO,KAAK,MAAM,MAAM,uBAAuB,CAAC;AAChD,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,qBAAqB,MAAM,4BAA4B,CAAC;AAqCpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,CAAC,OAAO,OAAO,OAAQ,YAAW,GAAG;;IAazC,YACE,SAAS,EAAE,SAAS,EACpB,oBAAoB,EAAE,cAAc,EACpC,oBAAoB,EAAE,cAAc,EACpC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,OAAO,EACtB,cAAc,EAAE,QAAQ,EACxB,cAAc,EAAE,QAAQ,EACxB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,OAAO,CAAC,EAAE,qBAAqB,EAmEhC;IAED;;;;;;;OAOG;IACI,kBAAkB,CAAC,eAAe,EAAE,eAAe,QAEzD;IAED;;;;;;;;OAQG;IACH;;;;;OAKG;IACI,cAAc,CAAC,IAAI,EAAE,WAAW,QAEtC;IAEM,UAAU,CAAC,OAAO,EAAE,OAAO,QAQjC;IAEK,GAAG,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAqQzD;CACF"}
|
package/dist/src/cli/BaseCLI.js
CHANGED
|
@@ -17,6 +17,7 @@ import KeyValueServiceProvider from "../service/configuration/KeyValueServicePro
|
|
|
17
17
|
import PrinterServiceProvider from "../service/printer/PrinterServiceProvider.js";
|
|
18
18
|
import TableGeneratorServiceProvider from "../service/tableGenerator/TableGeneratorServiceProvider.js";
|
|
19
19
|
import { run } from "../runtime/runner.js";
|
|
20
|
+
import { awaitBackgroundTasks } from "../runtime/lifecycle/BackgroundTasks.js";
|
|
20
21
|
import { printRuntimeError } from "../util/runnerHelper.js";
|
|
21
22
|
import { WritableStream } from "node:stream/web";
|
|
22
23
|
import DefaultPrinterService from "../service/printer/DefaultPrinterService.js";
|
|
@@ -322,6 +323,7 @@ export default class BaseCLI {
|
|
|
322
323
|
};
|
|
323
324
|
}
|
|
324
325
|
finally {
|
|
326
|
+
await awaitBackgroundTasks();
|
|
325
327
|
await ShutdownServiceProvider.shutdown();
|
|
326
328
|
}
|
|
327
329
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long, at shutdown, to wait for in-flight "background" startup tasks before abandoning them.
|
|
3
|
+
*/
|
|
4
|
+
export declare const BACKGROUND_TASK_GRACE_PERIOD_MS = 1000;
|
|
5
|
+
/**
|
|
6
|
+
* Start a background startup task without awaiting it, and track it so that
|
|
7
|
+
* {@link awaitBackgroundTasks} can give it a chance to finish at shutdown. A rejection is
|
|
8
|
+
* logged (at debug level) and never propagated.
|
|
9
|
+
*
|
|
10
|
+
* @param id the id of the startup task.
|
|
11
|
+
* @param start starts the task.
|
|
12
|
+
*/
|
|
13
|
+
export declare function trackBackgroundTask(id: string, start: () => Promise<unknown>): void;
|
|
14
|
+
/**
|
|
15
|
+
* Wait for the tracked background startup tasks, for at most `gracePeriodMs`. Tasks still running
|
|
16
|
+
* once it expires are abandoned. Returns immediately if no tasks are in flight. Never rejects.
|
|
17
|
+
*
|
|
18
|
+
* @param gracePeriodMs the maximum time to wait.
|
|
19
|
+
*/
|
|
20
|
+
export declare function awaitBackgroundTasks(gracePeriodMs?: number): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=BackgroundTasks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BackgroundTasks.d.ts","sourceRoot":"","sources":["../../../../src/runtime/lifecycle/BackgroundTasks.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,eAAO,MAAM,+BAA+B,OAAO,CAAC;AAcpD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAkBnF;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,aAAa,GAAE,MAAwC,GACtD,OAAO,CAAC,IAAI,CAAC,CAsCf"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import getLogger from "../../util/logger.js";
|
|
2
|
+
const logger = getLogger("BackgroundTasks");
|
|
3
|
+
/**
|
|
4
|
+
* How long, at shutdown, to wait for in-flight "background" startup tasks before abandoning them.
|
|
5
|
+
*/
|
|
6
|
+
export const BACKGROUND_TASK_GRACE_PERIOD_MS = 1000;
|
|
7
|
+
const inFlightTasks = new Set();
|
|
8
|
+
function elapsedMs(task) {
|
|
9
|
+
return Math.round(performance.now() - task.startedAt);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Start a background startup task without awaiting it, and track it so that
|
|
13
|
+
* {@link awaitBackgroundTasks} can give it a chance to finish at shutdown. A rejection is
|
|
14
|
+
* logged (at debug level) and never propagated.
|
|
15
|
+
*
|
|
16
|
+
* @param id the id of the startup task.
|
|
17
|
+
* @param start starts the task.
|
|
18
|
+
*/
|
|
19
|
+
export function trackBackgroundTask(id, start) {
|
|
20
|
+
const task = { id, startedAt: performance.now(), settled: Promise.resolve() };
|
|
21
|
+
logger.debug(() => `Background startup task '${id}' started`);
|
|
22
|
+
task.settled = new Promise((resolve) => resolve(start()))
|
|
23
|
+
.then(() => {
|
|
24
|
+
logger.debug(() => `Background startup task '${id}' completed in ${elapsedMs(task)}ms`);
|
|
25
|
+
}, (error) => {
|
|
26
|
+
logger.debug(() => `Background startup task '${id}' failed after ${elapsedMs(task)}ms: ${error}`);
|
|
27
|
+
})
|
|
28
|
+
.finally(() => {
|
|
29
|
+
inFlightTasks.delete(task);
|
|
30
|
+
});
|
|
31
|
+
inFlightTasks.add(task);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Wait for the tracked background startup tasks, for at most `gracePeriodMs`. Tasks still running
|
|
35
|
+
* once it expires are abandoned. Returns immediately if no tasks are in flight. Never rejects.
|
|
36
|
+
*
|
|
37
|
+
* @param gracePeriodMs the maximum time to wait.
|
|
38
|
+
*/
|
|
39
|
+
export async function awaitBackgroundTasks(gracePeriodMs = BACKGROUND_TASK_GRACE_PERIOD_MS) {
|
|
40
|
+
if (inFlightTasks.size === 0) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const pending = [...inFlightTasks];
|
|
44
|
+
logger.debug(() => `Waiting up to ${gracePeriodMs}ms for ${pending.length} in-flight background startup task(s)`);
|
|
45
|
+
for (const task of pending) {
|
|
46
|
+
logger.debug(() => `Background startup task '${task.id}' still in flight after ${elapsedMs(task)}ms`);
|
|
47
|
+
}
|
|
48
|
+
let timer;
|
|
49
|
+
const expired = new Promise((resolve) => {
|
|
50
|
+
timer = setTimeout(resolve, gracePeriodMs);
|
|
51
|
+
// must not keep the process alive
|
|
52
|
+
timer.unref?.();
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
await Promise.race([Promise.all(pending.map((task) => task.settled)), expired]);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// settled promises never reject; nothing to do
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
}
|
|
63
|
+
for (const task of pending) {
|
|
64
|
+
if (inFlightTasks.has(task)) {
|
|
65
|
+
inFlightTasks.delete(task);
|
|
66
|
+
logger.debug(() => `Background startup task '${task.id}' cancelled after ${elapsedMs(task)}ms: still running when the ${gracePeriodMs}ms grace period expired`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -39,8 +39,8 @@ export type RunnerResult = RunResult & {
|
|
|
39
39
|
* - Give the task a Context whose KeyValueService is bound permanently to its own scope
|
|
40
40
|
* (keyed by task.id), via KeyValueServiceProvider if provided.
|
|
41
41
|
* - Run the task: awaited before continuing if its mode is "blocking" (the default, and how
|
|
42
|
-
* every ServiceProvider's initService() behaves);
|
|
43
|
-
* (errors logged, not propagated).
|
|
42
|
+
* every ServiceProvider's initService() behaves); started without awaiting if "background"
|
|
43
|
+
* (errors logged, not propagated; the CLI gives it a short grace period to finish at shutdown).
|
|
44
44
|
* - Return immediately if a "blocking" task returned an exitRequest, using its runState and
|
|
45
45
|
* skipping the remaining tasks and the command.
|
|
46
46
|
* 2. Scan arguments for any GlobalModifierCommand clauses not provided by a StartupTask
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/runtime/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAiB/E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAEvF,OAAO,KAAK,4BAA4B,MAAM,0DAA0D,CAAC;AACzG,OAAO,KAAK,uBAAuB,MAAM,qDAAqD,CAAC;AAK/F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAKvE,OAAO,qBAAqB,MAAM,6CAA6C,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/runtime/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAiB/E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAEvF,OAAO,KAAK,4BAA4B,MAAM,0DAA0D,CAAC;AACzG,OAAO,KAAK,uBAAuB,MAAM,qDAAqD,CAAC;AAK/F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAKvE,OAAO,qBAAqB,MAAM,6CAA6C,CAAC;AAMhF;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG;IAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAkfzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AACH,wBAAsB,GAAG,CACvB,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,EAC3B,eAAe,EAAE,eAAe,EAChC,uBAAuB,EAAE,uBAAuB,EAChD,4BAA4B,EAAE,4BAA4B,GAAG,SAAS,EACtE,uBAAuB,EAAE,uBAAuB,GAAG,SAAS,EAC5D,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,OAAO,EACxB,cAAc,CAAC,EAAE,qBAAqB,GACrC,OAAO,CAAC,YAAY,CAAC,CAkKvB"}
|
|
@@ -7,6 +7,7 @@ import { parseGlobalCommandClause, parseSubCommandClause } from "./parser.js";
|
|
|
7
7
|
import { ARGUMENT_PROMPTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
8
8
|
import DefaultStartupService from "../service/startup/DefaultStartupService.js";
|
|
9
9
|
import { sortByPriority } from "./lifecycle/PriorityTaskList.js";
|
|
10
|
+
import { trackBackgroundTask } from "./lifecycle/BackgroundTasks.js";
|
|
10
11
|
const logger = getLogger("runner");
|
|
11
12
|
/**
|
|
12
13
|
* Wrap a {@link ServiceProvider}'s `initService()` call as a {@link StartupTask} so it can be
|
|
@@ -387,8 +388,8 @@ async function findAndExecuteDefaultNonModifierCommand(availableArgs, unusedArgs
|
|
|
387
388
|
* - Give the task a Context whose KeyValueService is bound permanently to its own scope
|
|
388
389
|
* (keyed by task.id), via KeyValueServiceProvider if provided.
|
|
389
390
|
* - Run the task: awaited before continuing if its mode is "blocking" (the default, and how
|
|
390
|
-
* every ServiceProvider's initService() behaves);
|
|
391
|
-
* (errors logged, not propagated).
|
|
391
|
+
* every ServiceProvider's initService() behaves); started without awaiting if "background"
|
|
392
|
+
* (errors logged, not propagated; the CLI gives it a short grace period to finish at shutdown).
|
|
392
393
|
* - Return immediately if a "blocking" task returned an exitRequest, using its runState and
|
|
393
394
|
* skipping the remaining tasks and the command.
|
|
394
395
|
* 2. Scan arguments for any GlobalModifierCommand clauses not provided by a StartupTask
|
|
@@ -486,9 +487,7 @@ export async function run(args, commandRegistry, serviceProviderRegistry, config
|
|
|
486
487
|
}
|
|
487
488
|
}
|
|
488
489
|
else {
|
|
489
|
-
|
|
490
|
-
logger.debug(() => `Background startup task '${task.id}' failed: ${error}`);
|
|
491
|
-
});
|
|
490
|
+
trackBackgroundTask(task.id, () => task.run(taskContext));
|
|
492
491
|
}
|
|
493
492
|
}
|
|
494
493
|
// using the GlobalModifierCommands instances which are NOT provided by a ServiceProvider...
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpHelper.d.ts","sourceRoot":"","sources":["../../../src/util/helpHelper.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAG7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAIvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAC;AAM5E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAG9E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"helpHelper.d.ts","sourceRoot":"","sources":["../../../src/util/helpHelper.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAG7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAIvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAC;AAM5E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AAG9E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAiCrF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CACxC;AAsDD,wBAAsB,iBAAiB,CACrC,cAAc,EAAE,cAAc,EAC9B,qBAAqB,EAAE,qBAAqB,EAC5C,QAAQ,EAAE,KAAK,CAAC,WAAW,CAAC,GAC3B,OAAO,CAAC,IAAI,CAAC,CA2Cf;AA2BD,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,SAAS,EACpB,cAAc,EAAE,OAAO,EACvB,aAAa,EAAE,aAAa,GAC3B,SAAS,CA6DX;AAED,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,UAAU,GAAG,MAAM,CA+C3E;AAkLD,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,SAAS,EACpB,cAAc,EAAE,OAAO,EACvB,UAAU,EAAE,UAAU,EACtB,kBAAkB,EAAE,OAAO,GAC1B,KAAK,CAAC,WAAW,CAAC,CAoBpB;AACD,wBAAgB,8BAA8B,CAC5C,cAAc,EAAE,cAAc,EAC9B,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,UAAU,GACrB,KAAK,CAAC,WAAW,CAAC,CAgCpB;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,EAChB,sBAAsB,EAAE,aAAa,CAAC,qBAAqB,CAAC,EAC5D,cAAc,EAAE,aAAa,CAAC,aAAa,CAAC,EAC5C,aAAa,EAAE,aAAa,CAAC,YAAY,CAAC,EAC1C,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,GACrC,MAAM,CA8IR;AAED,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,aAAa,CAAC,YAAY,CAAC,EAC1C,WAAW,EAAE,aAAa,CAAC,UAAU,CAAC,GACrC,MAAM,EAAE,CAkBV"}
|
|
@@ -10,6 +10,9 @@ const USER_INPUT_PATTERN = new RegExp(`${USER_INPUT_START}(.*?)${USER_INPUT_END}
|
|
|
10
10
|
function userInput(value) {
|
|
11
11
|
return `${USER_INPUT_START}${value}${USER_INPUT_END}`;
|
|
12
12
|
}
|
|
13
|
+
function userInputList(values, separator) {
|
|
14
|
+
return values.map((value) => userInput(`${value}`)).join(separator);
|
|
15
|
+
}
|
|
13
16
|
function renderDescription(printerService, description) {
|
|
14
17
|
let rendered = "";
|
|
15
18
|
let last = 0;
|
|
@@ -122,7 +125,7 @@ export function getGlobalArgumentHelpEntry(cliConfig, includeEnvVars, globalComm
|
|
|
122
125
|
const syntax = getGlobalCommandArgumentForm(argument);
|
|
123
126
|
const notesItems = [];
|
|
124
127
|
if (argument.allowableValues !== undefined && argument.allowableValues.length > 0) {
|
|
125
|
-
notesItems.push(`valid values: ${
|
|
128
|
+
notesItems.push(`valid values: ${userInputList(argument.allowableValues, "|")}`);
|
|
126
129
|
}
|
|
127
130
|
else {
|
|
128
131
|
switch (argument.type) {
|
|
@@ -235,7 +238,7 @@ function getOptionHelpEntry(cliConfig, includeEnvVars, command, option, subEntry
|
|
|
235
238
|
}
|
|
236
239
|
else {
|
|
237
240
|
if (option.allowableValues !== undefined && option.allowableValues.length > 0) {
|
|
238
|
-
notesItems.push(`valid values: ${
|
|
241
|
+
notesItems.push(`valid values: ${userInputList(option.allowableValues, "|")}`);
|
|
239
242
|
}
|
|
240
243
|
else {
|
|
241
244
|
switch (option.type) {
|
|
@@ -265,9 +268,9 @@ function getOptionHelpEntry(cliConfig, includeEnvVars, command, option, subEntry
|
|
|
265
268
|
notesItems.push(range);
|
|
266
269
|
}
|
|
267
270
|
if (option.defaultValue !== undefined) {
|
|
268
|
-
notesItems.push(`default: ${
|
|
269
|
-
?
|
|
270
|
-
: `${option.defaultValue}`)}`);
|
|
271
|
+
notesItems.push(`default: ${Array.isArray(option.defaultValue)
|
|
272
|
+
? userInputList(option.defaultValue, ", ")
|
|
273
|
+
: userInput(`${option.defaultValue}`)}`);
|
|
271
274
|
}
|
|
272
275
|
if (option.isOptional) {
|
|
273
276
|
notesItems.push("optional");
|
|
@@ -303,7 +306,7 @@ function getOptionHelpEntry(cliConfig, includeEnvVars, command, option, subEntry
|
|
|
303
306
|
function getPositionalHelpEntry(cliConfig, includeEnvVars, command, positional) {
|
|
304
307
|
const notesItems = [];
|
|
305
308
|
if (positional.allowableValues !== undefined && positional.allowableValues.length > 0) {
|
|
306
|
-
notesItems.push(`valid values: ${
|
|
309
|
+
notesItems.push(`valid values: ${userInputList(positional.allowableValues, "|")}`);
|
|
307
310
|
}
|
|
308
311
|
else {
|
|
309
312
|
switch (positional.type) {
|
package/package.json
CHANGED
package/src/cli/BaseCLI.ts
CHANGED
|
@@ -36,6 +36,7 @@ import KeyValueServiceProvider from "../service/configuration/KeyValueServicePro
|
|
|
36
36
|
import PrinterServiceProvider from "../service/printer/PrinterServiceProvider.ts";
|
|
37
37
|
import TableGeneratorServiceProvider from "../service/tableGenerator/TableGeneratorServiceProvider.ts";
|
|
38
38
|
import { run } from "../runtime/runner.ts";
|
|
39
|
+
import { awaitBackgroundTasks } from "../runtime/lifecycle/BackgroundTasks.ts";
|
|
39
40
|
import { printRuntimeError } from "../util/runnerHelper.ts";
|
|
40
41
|
|
|
41
42
|
import { WritableStream } from "node:stream/web";
|
|
@@ -510,6 +511,7 @@ export default class BaseCLI implements CLI {
|
|
|
510
511
|
error: error as Error,
|
|
511
512
|
};
|
|
512
513
|
} finally {
|
|
514
|
+
await awaitBackgroundTasks();
|
|
513
515
|
await ShutdownServiceProvider.shutdown();
|
|
514
516
|
}
|
|
515
517
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import getLogger from "../../util/logger.ts";
|
|
2
|
+
|
|
3
|
+
const logger = getLogger("BackgroundTasks");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How long, at shutdown, to wait for in-flight "background" startup tasks before abandoning them.
|
|
7
|
+
*/
|
|
8
|
+
export const BACKGROUND_TASK_GRACE_PERIOD_MS = 1000;
|
|
9
|
+
|
|
10
|
+
interface InFlightTask {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly startedAt: number;
|
|
13
|
+
settled: Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const inFlightTasks = new Set<InFlightTask>();
|
|
17
|
+
|
|
18
|
+
function elapsedMs(task: InFlightTask): number {
|
|
19
|
+
return Math.round(performance.now() - task.startedAt);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Start a background startup task without awaiting it, and track it so that
|
|
24
|
+
* {@link awaitBackgroundTasks} can give it a chance to finish at shutdown. A rejection is
|
|
25
|
+
* logged (at debug level) and never propagated.
|
|
26
|
+
*
|
|
27
|
+
* @param id the id of the startup task.
|
|
28
|
+
* @param start starts the task.
|
|
29
|
+
*/
|
|
30
|
+
export function trackBackgroundTask(id: string, start: () => Promise<unknown>): void {
|
|
31
|
+
const task: InFlightTask = { id, startedAt: performance.now(), settled: Promise.resolve() };
|
|
32
|
+
logger.debug(() => `Background startup task '${id}' started`);
|
|
33
|
+
task.settled = new Promise<unknown>((resolve) => resolve(start()))
|
|
34
|
+
.then(
|
|
35
|
+
() => {
|
|
36
|
+
logger.debug(() => `Background startup task '${id}' completed in ${elapsedMs(task)}ms`);
|
|
37
|
+
},
|
|
38
|
+
(error: unknown) => {
|
|
39
|
+
logger.debug(
|
|
40
|
+
() => `Background startup task '${id}' failed after ${elapsedMs(task)}ms: ${error}`,
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
.finally(() => {
|
|
45
|
+
inFlightTasks.delete(task);
|
|
46
|
+
});
|
|
47
|
+
inFlightTasks.add(task);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Wait for the tracked background startup tasks, for at most `gracePeriodMs`. Tasks still running
|
|
52
|
+
* once it expires are abandoned. Returns immediately if no tasks are in flight. Never rejects.
|
|
53
|
+
*
|
|
54
|
+
* @param gracePeriodMs the maximum time to wait.
|
|
55
|
+
*/
|
|
56
|
+
export async function awaitBackgroundTasks(
|
|
57
|
+
gracePeriodMs: number = BACKGROUND_TASK_GRACE_PERIOD_MS,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
if (inFlightTasks.size === 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const pending = [...inFlightTasks];
|
|
63
|
+
logger.debug(
|
|
64
|
+
() =>
|
|
65
|
+
`Waiting up to ${gracePeriodMs}ms for ${pending.length} in-flight background startup task(s)`,
|
|
66
|
+
);
|
|
67
|
+
for (const task of pending) {
|
|
68
|
+
logger.debug(
|
|
69
|
+
() => `Background startup task '${task.id}' still in flight after ${elapsedMs(task)}ms`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
74
|
+
const expired = new Promise<void>((resolve) => {
|
|
75
|
+
timer = setTimeout(resolve, gracePeriodMs);
|
|
76
|
+
// must not keep the process alive
|
|
77
|
+
(timer as { unref?: () => void }).unref?.();
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
await Promise.race([Promise.all(pending.map((task) => task.settled)), expired]);
|
|
81
|
+
} catch {
|
|
82
|
+
// settled promises never reject; nothing to do
|
|
83
|
+
} finally {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const task of pending) {
|
|
88
|
+
if (inFlightTasks.has(task)) {
|
|
89
|
+
inFlightTasks.delete(task);
|
|
90
|
+
logger.debug(
|
|
91
|
+
() =>
|
|
92
|
+
`Background startup task '${task.id}' cancelled after ${elapsedMs(task)}ms: still running when the ${gracePeriodMs}ms grace period expired`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/runtime/runner.ts
CHANGED
|
@@ -33,6 +33,7 @@ import type { StartupTask } from "@flowscripter/dynamic-cli-framework-api";
|
|
|
33
33
|
import type { ServiceProvider } from "@flowscripter/dynamic-cli-framework-api";
|
|
34
34
|
import DefaultStartupService from "../service/startup/DefaultStartupService.ts";
|
|
35
35
|
import { sortByPriority } from "./lifecycle/PriorityTaskList.ts";
|
|
36
|
+
import { trackBackgroundTask } from "./lifecycle/BackgroundTasks.ts";
|
|
36
37
|
|
|
37
38
|
const logger = getLogger("runner");
|
|
38
39
|
|
|
@@ -564,8 +565,8 @@ async function findAndExecuteDefaultNonModifierCommand(
|
|
|
564
565
|
* - Give the task a Context whose KeyValueService is bound permanently to its own scope
|
|
565
566
|
* (keyed by task.id), via KeyValueServiceProvider if provided.
|
|
566
567
|
* - Run the task: awaited before continuing if its mode is "blocking" (the default, and how
|
|
567
|
-
* every ServiceProvider's initService() behaves);
|
|
568
|
-
* (errors logged, not propagated).
|
|
568
|
+
* every ServiceProvider's initService() behaves); started without awaiting if "background"
|
|
569
|
+
* (errors logged, not propagated; the CLI gives it a short grace period to finish at shutdown).
|
|
569
570
|
* - Return immediately if a "blocking" task returned an exitRequest, using its runState and
|
|
570
571
|
* skipping the remaining tasks and the command.
|
|
571
572
|
* 2. Scan arguments for any GlobalModifierCommand clauses not provided by a StartupTask
|
|
@@ -698,9 +699,7 @@ export async function run(
|
|
|
698
699
|
return { runState: outcome.exitRequest.runState, exitRequested: true };
|
|
699
700
|
}
|
|
700
701
|
} else {
|
|
701
|
-
|
|
702
|
-
logger.debug(() => `Background startup task '${task.id}' failed: ${error}`);
|
|
703
|
-
});
|
|
702
|
+
trackBackgroundTask(task.id, () => task.run(taskContext));
|
|
704
703
|
}
|
|
705
704
|
}
|
|
706
705
|
|
package/src/util/helpHelper.ts
CHANGED
|
@@ -31,6 +31,10 @@ function userInput(value: string): string {
|
|
|
31
31
|
return `${USER_INPUT_START}${value}${USER_INPUT_END}`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function userInputList(values: ReadonlyArray<unknown>, separator: string): string {
|
|
35
|
+
return values.map((value) => userInput(`${value}`)).join(separator);
|
|
36
|
+
}
|
|
37
|
+
|
|
34
38
|
function renderDescription(printerService: PrinterService, description: string): string {
|
|
35
39
|
let rendered = "";
|
|
36
40
|
let last = 0;
|
|
@@ -201,7 +205,7 @@ export function getGlobalArgumentHelpEntry(
|
|
|
201
205
|
|
|
202
206
|
const notesItems: Array<string> = [];
|
|
203
207
|
if (argument.allowableValues !== undefined && argument.allowableValues.length > 0) {
|
|
204
|
-
notesItems.push(`valid values: ${
|
|
208
|
+
notesItems.push(`valid values: ${userInputList(argument.allowableValues, "|")}`);
|
|
205
209
|
} else {
|
|
206
210
|
switch (argument.type) {
|
|
207
211
|
case ValueTypeName.STRING:
|
|
@@ -332,7 +336,7 @@ function getOptionHelpEntry(
|
|
|
332
336
|
});
|
|
333
337
|
} else {
|
|
334
338
|
if (option.allowableValues !== undefined && option.allowableValues.length > 0) {
|
|
335
|
-
notesItems.push(`valid values: ${
|
|
339
|
+
notesItems.push(`valid values: ${userInputList(option.allowableValues, "|")}`);
|
|
336
340
|
} else {
|
|
337
341
|
switch (option.type) {
|
|
338
342
|
case ValueTypeName.STRING:
|
|
@@ -364,11 +368,11 @@ function getOptionHelpEntry(
|
|
|
364
368
|
|
|
365
369
|
if (option.defaultValue !== undefined) {
|
|
366
370
|
notesItems.push(
|
|
367
|
-
`default: ${
|
|
371
|
+
`default: ${
|
|
368
372
|
Array.isArray(option.defaultValue)
|
|
369
|
-
?
|
|
370
|
-
: `${option.defaultValue}
|
|
371
|
-
|
|
373
|
+
? userInputList(option.defaultValue, ", ")
|
|
374
|
+
: userInput(`${option.defaultValue}`)
|
|
375
|
+
}`,
|
|
372
376
|
);
|
|
373
377
|
}
|
|
374
378
|
|
|
@@ -417,7 +421,7 @@ function getPositionalHelpEntry(
|
|
|
417
421
|
): HelpEntry {
|
|
418
422
|
const notesItems: Array<string> = [];
|
|
419
423
|
if (positional.allowableValues !== undefined && positional.allowableValues.length > 0) {
|
|
420
|
-
notesItems.push(`valid values: ${
|
|
424
|
+
notesItems.push(`valid values: ${userInputList(positional.allowableValues, "|")}`);
|
|
421
425
|
} else {
|
|
422
426
|
switch (positional.type) {
|
|
423
427
|
case ValueTypeName.STRING:
|