@midscene/shared 1.12.1 → 1.12.2-beta-20260827052510.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/es/cli/interrupt.mjs +47 -21
- package/dist/es/cli/record-command.mjs +11 -5
- package/dist/es/cli/verbose.mjs +2 -1
- package/dist/es/env/parse-model-config.mjs +1 -1
- package/dist/lib/cli/interrupt.js +49 -20
- package/dist/lib/cli/record-command.js +10 -4
- package/dist/lib/cli/verbose.js +2 -1
- package/dist/lib/env/parse-model-config.js +1 -1
- package/dist/types/cli/interrupt.d.ts +23 -6
- package/package.json +1 -1
- package/src/cli/interrupt.ts +73 -27
- package/src/cli/record-command.ts +11 -5
- package/src/cli/verbose.ts +5 -2
|
@@ -14,28 +14,54 @@ function registerCliInterruptWaiter(source) {
|
|
|
14
14
|
function hasActiveCliInterruptWaiter(source = process) {
|
|
15
15
|
return (activeInterruptWaiters.get(source) ?? 0) > 0;
|
|
16
16
|
}
|
|
17
|
-
function
|
|
17
|
+
function createCliInterruptWaiter(watchdogMs, source = process) {
|
|
18
18
|
const unregisterWaiter = registerCliInterruptWaiter(source);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
unregisterWaiter();
|
|
35
|
-
reject(error);
|
|
36
|
-
return;
|
|
19
|
+
let timer;
|
|
20
|
+
let finished = false;
|
|
21
|
+
let disposed = false;
|
|
22
|
+
let resolveResult;
|
|
23
|
+
let rejectResult;
|
|
24
|
+
const result = new Promise((resolve, reject)=>{
|
|
25
|
+
resolveResult = resolve;
|
|
26
|
+
rejectResult = reject;
|
|
27
|
+
});
|
|
28
|
+
const finish = (reason)=>{
|
|
29
|
+
if (finished) return;
|
|
30
|
+
finished = true;
|
|
31
|
+
if (timer) {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
timer = void 0;
|
|
37
34
|
}
|
|
35
|
+
resolveResult(reason);
|
|
36
|
+
};
|
|
37
|
+
const onSigint = ()=>finish('sigint');
|
|
38
|
+
const onSigterm = ()=>finish('sigterm');
|
|
39
|
+
const dispose = ()=>{
|
|
40
|
+
if (disposed) return;
|
|
41
|
+
disposed = true;
|
|
42
|
+
source.removeListener('SIGINT', onSigint);
|
|
43
|
+
source.removeListener('SIGTERM', onSigterm);
|
|
44
|
+
if (timer) {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
timer = void 0;
|
|
47
|
+
}
|
|
48
|
+
unregisterWaiter();
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
source.on('SIGINT', onSigint);
|
|
52
|
+
source.on('SIGTERM', onSigterm);
|
|
38
53
|
if (watchdogMs > 0) timer = setTimeout(()=>finish('watchdog'), watchdogMs);
|
|
39
|
-
})
|
|
54
|
+
} catch (error) {
|
|
55
|
+
dispose();
|
|
56
|
+
rejectResult(error);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
result,
|
|
60
|
+
dispose
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function waitForCliInterrupt(watchdogMs, source = process) {
|
|
64
|
+
const waiter = createCliInterruptWaiter(watchdogMs, source);
|
|
65
|
+
return waiter.result.finally(waiter.dispose);
|
|
40
66
|
}
|
|
41
|
-
export { hasActiveCliInterruptWaiter, waitForCliInterrupt };
|
|
67
|
+
export { createCliInterruptWaiter, hasActiveCliInterruptWaiter, waitForCliInterrupt };
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { getErrorMessage } from "../agent-tools/error-formatter.mjs";
|
|
3
3
|
import { resolveObservationArtifactAdapter } from "../agent-tools/observation-artifact.mjs";
|
|
4
4
|
import { writeUIObservationRecord } from "../agent-tools/observation-record.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { createCliInterruptWaiter } from "./interrupt.mjs";
|
|
6
6
|
import { attachCliVerboseDumpListener, emitCliVerboseEvent } from "./verbose.mjs";
|
|
7
7
|
const recordCliMetadata = {
|
|
8
8
|
positionals: [
|
|
@@ -55,7 +55,7 @@ function createErrorResult(message) {
|
|
|
55
55
|
function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata) {
|
|
56
56
|
return {
|
|
57
57
|
name: 'record',
|
|
58
|
-
description: 'Record the page/screen in the foreground until Ctrl+C, then save the ordered frame window for a later assert command.',
|
|
58
|
+
description: 'Record the page/screen in the foreground until Ctrl+C or a termination signal, then save the ordered frame window for a later assert command.',
|
|
59
59
|
schema: {
|
|
60
60
|
action: z.literal('start').describe('Start a foreground recording. Press Ctrl+C to finish.'),
|
|
61
61
|
output: z.string().optional().describe('Path for the JSON observation manifest. Frames are stored in an adjacent <name>.frames directory. Defaults to a generated path under midscene_run/output.'),
|
|
@@ -80,6 +80,7 @@ function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata
|
|
|
80
80
|
toolName: 'record'
|
|
81
81
|
});
|
|
82
82
|
let observer;
|
|
83
|
+
let interruptWaiter;
|
|
83
84
|
try {
|
|
84
85
|
const watchdogMs = args.watchdogMs ?? 300000;
|
|
85
86
|
observer = await agent.startObserving({
|
|
@@ -87,12 +88,13 @@ function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata
|
|
|
87
88
|
maxFrames: args.maxFrames,
|
|
88
89
|
watchdogMs
|
|
89
90
|
});
|
|
91
|
+
interruptWaiter = createCliInterruptWaiter(watchdogMs);
|
|
90
92
|
emitCliVerboseEvent({
|
|
91
93
|
event: 'recording_ready',
|
|
92
94
|
tool: 'record',
|
|
93
95
|
watchdogMs
|
|
94
96
|
});
|
|
95
|
-
const stopReason = await
|
|
97
|
+
const stopReason = await interruptWaiter.result;
|
|
96
98
|
emitCliVerboseEvent({
|
|
97
99
|
event: 'recording_stopping',
|
|
98
100
|
tool: 'record',
|
|
@@ -110,8 +112,12 @@ function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata
|
|
|
110
112
|
]
|
|
111
113
|
};
|
|
112
114
|
} finally{
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
try {
|
|
116
|
+
await observer?.dispose?.();
|
|
117
|
+
} finally{
|
|
118
|
+
interruptWaiter?.dispose();
|
|
119
|
+
unsubscribeVerbose();
|
|
120
|
+
}
|
|
115
121
|
}
|
|
116
122
|
} catch (error) {
|
|
117
123
|
const errorMessage = getErrorMessage(error);
|
package/dist/es/cli/verbose.mjs
CHANGED
|
@@ -305,7 +305,8 @@ function renderCliVerboseEventText(event, context) {
|
|
|
305
305
|
case 'recording_ready':
|
|
306
306
|
return '[Midscene] Recording. Press Ctrl+C to stop and save.';
|
|
307
307
|
case 'recording_stopping':
|
|
308
|
-
|
|
308
|
+
if ('watchdog' === event.reason) return '[Midscene] Recording watchdog reached; finalizing and saving.';
|
|
309
|
+
return 'sigterm' === event.reason ? '[Midscene] SIGTERM received; finalizing and saving.' : '[Midscene] Ctrl+C received; finalizing and saving.';
|
|
309
310
|
case 'dump_update':
|
|
310
311
|
{
|
|
311
312
|
if (isActVerboseEvent(command, tool)) return;
|
|
@@ -5,7 +5,7 @@ import { assert } from "../utils.mjs";
|
|
|
5
5
|
import { maskConfig, parseJson } from "./helper.mjs";
|
|
6
6
|
import { initDebugConfig } from "./init-debug.mjs";
|
|
7
7
|
const MODEL_CONFIG_DOC_URL = 'https://midscenejs.com/model-common-config.html';
|
|
8
|
-
const getCurrentVersion = ()=>"1.12.
|
|
8
|
+
const getCurrentVersion = ()=>"1.12.2-beta-20260827052510.0";
|
|
9
9
|
const getInvalidModelFamilyMessage = (modelFamily)=>`Invalid MIDSCENE_MODEL_FAMILY value: ${modelFamily}. Current version v${getCurrentVersion()} accepts the following model families: ${MODEL_FAMILY_VALUES.join(', ')}. You can also visit ${MODEL_CONFIG_DOC_URL} for the latest configuration information.`;
|
|
10
10
|
const KEYS_MAP = {
|
|
11
11
|
insight: INSIGHT_MODEL_CONFIG_KEYS,
|
|
@@ -24,6 +24,7 @@ var __webpack_require__ = {};
|
|
|
24
24
|
var __webpack_exports__ = {};
|
|
25
25
|
__webpack_require__.r(__webpack_exports__);
|
|
26
26
|
__webpack_require__.d(__webpack_exports__, {
|
|
27
|
+
createCliInterruptWaiter: ()=>createCliInterruptWaiter,
|
|
27
28
|
hasActiveCliInterruptWaiter: ()=>hasActiveCliInterruptWaiter,
|
|
28
29
|
waitForCliInterrupt: ()=>waitForCliInterrupt
|
|
29
30
|
});
|
|
@@ -43,33 +44,61 @@ function registerCliInterruptWaiter(source) {
|
|
|
43
44
|
function hasActiveCliInterruptWaiter(source = process) {
|
|
44
45
|
return (activeInterruptWaiters.get(source) ?? 0) > 0;
|
|
45
46
|
}
|
|
46
|
-
function
|
|
47
|
+
function createCliInterruptWaiter(watchdogMs, source = process) {
|
|
47
48
|
const unregisterWaiter = registerCliInterruptWaiter(source);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
unregisterWaiter();
|
|
64
|
-
reject(error);
|
|
65
|
-
return;
|
|
49
|
+
let timer;
|
|
50
|
+
let finished = false;
|
|
51
|
+
let disposed = false;
|
|
52
|
+
let resolveResult;
|
|
53
|
+
let rejectResult;
|
|
54
|
+
const result = new Promise((resolve, reject)=>{
|
|
55
|
+
resolveResult = resolve;
|
|
56
|
+
rejectResult = reject;
|
|
57
|
+
});
|
|
58
|
+
const finish = (reason)=>{
|
|
59
|
+
if (finished) return;
|
|
60
|
+
finished = true;
|
|
61
|
+
if (timer) {
|
|
62
|
+
clearTimeout(timer);
|
|
63
|
+
timer = void 0;
|
|
66
64
|
}
|
|
65
|
+
resolveResult(reason);
|
|
66
|
+
};
|
|
67
|
+
const onSigint = ()=>finish('sigint');
|
|
68
|
+
const onSigterm = ()=>finish('sigterm');
|
|
69
|
+
const dispose = ()=>{
|
|
70
|
+
if (disposed) return;
|
|
71
|
+
disposed = true;
|
|
72
|
+
source.removeListener('SIGINT', onSigint);
|
|
73
|
+
source.removeListener('SIGTERM', onSigterm);
|
|
74
|
+
if (timer) {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
timer = void 0;
|
|
77
|
+
}
|
|
78
|
+
unregisterWaiter();
|
|
79
|
+
};
|
|
80
|
+
try {
|
|
81
|
+
source.on('SIGINT', onSigint);
|
|
82
|
+
source.on('SIGTERM', onSigterm);
|
|
67
83
|
if (watchdogMs > 0) timer = setTimeout(()=>finish('watchdog'), watchdogMs);
|
|
68
|
-
})
|
|
84
|
+
} catch (error) {
|
|
85
|
+
dispose();
|
|
86
|
+
rejectResult(error);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
result,
|
|
90
|
+
dispose
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function waitForCliInterrupt(watchdogMs, source = process) {
|
|
94
|
+
const waiter = createCliInterruptWaiter(watchdogMs, source);
|
|
95
|
+
return waiter.result.finally(waiter.dispose);
|
|
69
96
|
}
|
|
97
|
+
exports.createCliInterruptWaiter = __webpack_exports__.createCliInterruptWaiter;
|
|
70
98
|
exports.hasActiveCliInterruptWaiter = __webpack_exports__.hasActiveCliInterruptWaiter;
|
|
71
99
|
exports.waitForCliInterrupt = __webpack_exports__.waitForCliInterrupt;
|
|
72
100
|
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
101
|
+
"createCliInterruptWaiter",
|
|
73
102
|
"hasActiveCliInterruptWaiter",
|
|
74
103
|
"waitForCliInterrupt"
|
|
75
104
|
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
@@ -83,7 +83,7 @@ function createErrorResult(message) {
|
|
|
83
83
|
function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata) {
|
|
84
84
|
return {
|
|
85
85
|
name: 'record',
|
|
86
|
-
description: 'Record the page/screen in the foreground until Ctrl+C, then save the ordered frame window for a later assert command.',
|
|
86
|
+
description: 'Record the page/screen in the foreground until Ctrl+C or a termination signal, then save the ordered frame window for a later assert command.',
|
|
87
87
|
schema: {
|
|
88
88
|
action: external_zod_namespaceObject.z.literal('start').describe('Start a foreground recording. Press Ctrl+C to finish.'),
|
|
89
89
|
output: external_zod_namespaceObject.z.string().optional().describe('Path for the JSON observation manifest. Frames are stored in an adjacent <name>.frames directory. Defaults to a generated path under midscene_run/output.'),
|
|
@@ -108,6 +108,7 @@ function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata
|
|
|
108
108
|
toolName: 'record'
|
|
109
109
|
});
|
|
110
110
|
let observer;
|
|
111
|
+
let interruptWaiter;
|
|
111
112
|
try {
|
|
112
113
|
const watchdogMs = args.watchdogMs ?? 300000;
|
|
113
114
|
observer = await agent.startObserving({
|
|
@@ -115,12 +116,13 @@ function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata
|
|
|
115
116
|
maxFrames: args.maxFrames,
|
|
116
117
|
watchdogMs
|
|
117
118
|
});
|
|
119
|
+
interruptWaiter = (0, external_interrupt_js_namespaceObject.createCliInterruptWaiter)(watchdogMs);
|
|
118
120
|
(0, external_verbose_js_namespaceObject.emitCliVerboseEvent)({
|
|
119
121
|
event: 'recording_ready',
|
|
120
122
|
tool: 'record',
|
|
121
123
|
watchdogMs
|
|
122
124
|
});
|
|
123
|
-
const stopReason = await
|
|
125
|
+
const stopReason = await interruptWaiter.result;
|
|
124
126
|
(0, external_verbose_js_namespaceObject.emitCliVerboseEvent)({
|
|
125
127
|
event: 'recording_stopping',
|
|
126
128
|
tool: 'record',
|
|
@@ -138,8 +140,12 @@ function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata
|
|
|
138
140
|
]
|
|
139
141
|
};
|
|
140
142
|
} finally{
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
try {
|
|
144
|
+
await observer?.dispose?.();
|
|
145
|
+
} finally{
|
|
146
|
+
interruptWaiter?.dispose();
|
|
147
|
+
unsubscribeVerbose();
|
|
148
|
+
}
|
|
143
149
|
}
|
|
144
150
|
} catch (error) {
|
|
145
151
|
const errorMessage = (0, error_formatter_js_namespaceObject.getErrorMessage)(error);
|
package/dist/lib/cli/verbose.js
CHANGED
|
@@ -342,7 +342,8 @@ function renderCliVerboseEventText(event, context) {
|
|
|
342
342
|
case 'recording_ready':
|
|
343
343
|
return '[Midscene] Recording. Press Ctrl+C to stop and save.';
|
|
344
344
|
case 'recording_stopping':
|
|
345
|
-
|
|
345
|
+
if ('watchdog' === event.reason) return '[Midscene] Recording watchdog reached; finalizing and saving.';
|
|
346
|
+
return 'sigterm' === event.reason ? '[Midscene] SIGTERM received; finalizing and saving.' : '[Midscene] Ctrl+C received; finalizing and saving.';
|
|
346
347
|
case 'dump_update':
|
|
347
348
|
{
|
|
348
349
|
if (isActVerboseEvent(command, tool)) return;
|
|
@@ -37,7 +37,7 @@ const external_utils_js_namespaceObject = require("../utils.js");
|
|
|
37
37
|
const external_helper_js_namespaceObject = require("./helper.js");
|
|
38
38
|
const external_init_debug_js_namespaceObject = require("./init-debug.js");
|
|
39
39
|
const MODEL_CONFIG_DOC_URL = 'https://midscenejs.com/model-common-config.html';
|
|
40
|
-
const getCurrentVersion = ()=>"1.12.
|
|
40
|
+
const getCurrentVersion = ()=>"1.12.2-beta-20260827052510.0";
|
|
41
41
|
const getInvalidModelFamilyMessage = (modelFamily)=>`Invalid MIDSCENE_MODEL_FAMILY value: ${modelFamily}. Current version v${getCurrentVersion()} accepts the following model families: ${external_types_js_namespaceObject.MODEL_FAMILY_VALUES.join(', ')}. You can also visit ${MODEL_CONFIG_DOC_URL} for the latest configuration information.`;
|
|
42
42
|
const KEYS_MAP = {
|
|
43
43
|
insight: external_constants_js_namespaceObject.INSIGHT_MODEL_CONFIG_KEYS,
|
|
@@ -1,13 +1,30 @@
|
|
|
1
|
-
export type CliInterruptReason = 'sigint' | 'watchdog';
|
|
1
|
+
export type CliInterruptReason = 'sigint' | 'sigterm' | 'watchdog';
|
|
2
|
+
type CliInterruptSignal = 'SIGINT' | 'SIGTERM';
|
|
2
3
|
export interface CliInterruptSource {
|
|
3
|
-
|
|
4
|
-
removeListener(event:
|
|
4
|
+
on(event: CliInterruptSignal, listener: () => void): unknown;
|
|
5
|
+
removeListener(event: CliInterruptSignal, listener: () => void): unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface CliInterruptWaiter {
|
|
8
|
+
/** Resolves on the first stop signal or watchdog timeout. */
|
|
9
|
+
readonly result: Promise<CliInterruptReason>;
|
|
10
|
+
/** Release signal handlers after asynchronous finalization has completed. */
|
|
11
|
+
dispose(): void;
|
|
5
12
|
}
|
|
6
13
|
/** Whether a foreground CLI command is currently waiting for this source. */
|
|
7
14
|
export declare function hasActiveCliInterruptWaiter(source?: CliInterruptSource): boolean;
|
|
8
15
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* Keep graceful-stop handlers installed until the caller has finished saving.
|
|
17
|
+
*
|
|
18
|
+
* Package runners such as pnpm can deliver SIGINT to the foreground child and
|
|
19
|
+
* immediately follow it with SIGTERM while shutting down their own process.
|
|
20
|
+
* Resolving on the first signal is not enough: removing the handlers at that
|
|
21
|
+
* point lets the forwarded SIGTERM kill the child during asynchronous artifact
|
|
22
|
+
* finalization.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createCliInterruptWaiter(watchdogMs: number, source?: CliInterruptSource): CliInterruptWaiter;
|
|
25
|
+
/**
|
|
26
|
+
* Wait for one stop request and release the handlers immediately afterwards.
|
|
27
|
+
* Long-running finalizers should use {@link createCliInterruptWaiter} instead.
|
|
12
28
|
*/
|
|
13
29
|
export declare function waitForCliInterrupt(watchdogMs: number, source?: CliInterruptSource): Promise<CliInterruptReason>;
|
|
30
|
+
export {};
|
package/package.json
CHANGED
package/src/cli/interrupt.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
export type CliInterruptReason = 'sigint' | 'watchdog';
|
|
1
|
+
export type CliInterruptReason = 'sigint' | 'sigterm' | 'watchdog';
|
|
2
|
+
|
|
3
|
+
type CliInterruptSignal = 'SIGINT' | 'SIGTERM';
|
|
2
4
|
|
|
3
5
|
export interface CliInterruptSource {
|
|
4
|
-
|
|
5
|
-
removeListener(event:
|
|
6
|
+
on(event: CliInterruptSignal, listener: () => void): unknown;
|
|
7
|
+
removeListener(event: CliInterruptSignal, listener: () => void): unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CliInterruptWaiter {
|
|
11
|
+
/** Resolves on the first stop signal or watchdog timeout. */
|
|
12
|
+
readonly result: Promise<CliInterruptReason>;
|
|
13
|
+
/** Release signal handlers after asynchronous finalization has completed. */
|
|
14
|
+
dispose(): void;
|
|
6
15
|
}
|
|
7
16
|
|
|
8
17
|
const activeInterruptWaiters = new WeakMap<object, number>();
|
|
@@ -33,39 +42,76 @@ export function hasActiveCliInterruptWaiter(
|
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
45
|
+
* Keep graceful-stop handlers installed until the caller has finished saving.
|
|
46
|
+
*
|
|
47
|
+
* Package runners such as pnpm can deliver SIGINT to the foreground child and
|
|
48
|
+
* immediately follow it with SIGTERM while shutting down their own process.
|
|
49
|
+
* Resolving on the first signal is not enough: removing the handlers at that
|
|
50
|
+
* point lets the forwarded SIGTERM kill the child during asynchronous artifact
|
|
51
|
+
* finalization.
|
|
39
52
|
*/
|
|
40
|
-
export function
|
|
53
|
+
export function createCliInterruptWaiter(
|
|
41
54
|
watchdogMs: number,
|
|
42
55
|
source: CliInterruptSource = process,
|
|
43
|
-
):
|
|
56
|
+
): CliInterruptWaiter {
|
|
44
57
|
const unregisterWaiter = registerCliInterruptWaiter(source);
|
|
58
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
59
|
+
let finished = false;
|
|
60
|
+
let disposed = false;
|
|
61
|
+
let resolveResult!: (reason: CliInterruptReason) => void;
|
|
62
|
+
let rejectResult!: (error: unknown) => void;
|
|
45
63
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
const result = new Promise<CliInterruptReason>((resolve, reject) => {
|
|
65
|
+
resolveResult = resolve;
|
|
66
|
+
rejectResult = reject;
|
|
67
|
+
});
|
|
49
68
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
const finish = (reason: CliInterruptReason) => {
|
|
70
|
+
if (finished) return;
|
|
71
|
+
finished = true;
|
|
72
|
+
if (timer) {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
timer = undefined;
|
|
75
|
+
}
|
|
76
|
+
resolveResult(reason);
|
|
77
|
+
};
|
|
78
|
+
const onSigint = () => finish('sigint');
|
|
79
|
+
const onSigterm = () => finish('sigterm');
|
|
59
80
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
const dispose = () => {
|
|
82
|
+
if (disposed) return;
|
|
83
|
+
disposed = true;
|
|
84
|
+
source.removeListener('SIGINT', onSigint);
|
|
85
|
+
source.removeListener('SIGTERM', onSigterm);
|
|
86
|
+
if (timer) {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
timer = undefined;
|
|
66
89
|
}
|
|
90
|
+
unregisterWaiter();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
source.on('SIGINT', onSigint);
|
|
95
|
+
source.on('SIGTERM', onSigterm);
|
|
67
96
|
if (watchdogMs > 0) {
|
|
68
97
|
timer = setTimeout(() => finish('watchdog'), watchdogMs);
|
|
69
98
|
}
|
|
70
|
-
})
|
|
99
|
+
} catch (error) {
|
|
100
|
+
dispose();
|
|
101
|
+
rejectResult(error);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { result, dispose };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Wait for one stop request and release the handlers immediately afterwards.
|
|
109
|
+
* Long-running finalizers should use {@link createCliInterruptWaiter} instead.
|
|
110
|
+
*/
|
|
111
|
+
export function waitForCliInterrupt(
|
|
112
|
+
watchdogMs: number,
|
|
113
|
+
source: CliInterruptSource = process,
|
|
114
|
+
): Promise<CliInterruptReason> {
|
|
115
|
+
const waiter = createCliInterruptWaiter(watchdogMs, source);
|
|
116
|
+
return waiter.result.finally(waiter.dispose);
|
|
71
117
|
}
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
ToolResult,
|
|
10
10
|
ToolSchema,
|
|
11
11
|
} from '../agent-tools/types';
|
|
12
|
-
import {
|
|
12
|
+
import { type CliInterruptWaiter, createCliInterruptWaiter } from './interrupt';
|
|
13
13
|
import { attachCliVerboseDumpListener, emitCliVerboseEvent } from './verbose';
|
|
14
14
|
|
|
15
15
|
const recordCliMetadata: ToolCliMetadata = {
|
|
@@ -62,7 +62,7 @@ export function createRecordCliCommand(
|
|
|
62
62
|
return {
|
|
63
63
|
name: 'record',
|
|
64
64
|
description:
|
|
65
|
-
'Record the page/screen in the foreground until Ctrl+C, then save the ordered frame window for a later assert command.',
|
|
65
|
+
'Record the page/screen in the foreground until Ctrl+C or a termination signal, then save the ordered frame window for a later assert command.',
|
|
66
66
|
schema: {
|
|
67
67
|
action: z
|
|
68
68
|
.literal('start')
|
|
@@ -125,6 +125,7 @@ export function createRecordCliCommand(
|
|
|
125
125
|
let observer:
|
|
126
126
|
| Awaited<ReturnType<NonNullable<BaseAgent['startObserving']>>>
|
|
127
127
|
| undefined;
|
|
128
|
+
let interruptWaiter: CliInterruptWaiter | undefined;
|
|
128
129
|
try {
|
|
129
130
|
const watchdogMs = (args.watchdogMs as number | undefined) ?? 300_000;
|
|
130
131
|
observer = await agent.startObserving({
|
|
@@ -132,12 +133,13 @@ export function createRecordCliCommand(
|
|
|
132
133
|
maxFrames: args.maxFrames as number | undefined,
|
|
133
134
|
watchdogMs,
|
|
134
135
|
});
|
|
136
|
+
interruptWaiter = createCliInterruptWaiter(watchdogMs);
|
|
135
137
|
emitCliVerboseEvent({
|
|
136
138
|
event: 'recording_ready',
|
|
137
139
|
tool: 'record',
|
|
138
140
|
watchdogMs,
|
|
139
141
|
});
|
|
140
|
-
const stopReason = await
|
|
142
|
+
const stopReason = await interruptWaiter.result;
|
|
141
143
|
emitCliVerboseEvent({
|
|
142
144
|
event: 'recording_stopping',
|
|
143
145
|
tool: 'record',
|
|
@@ -158,8 +160,12 @@ export function createRecordCliCommand(
|
|
|
158
160
|
],
|
|
159
161
|
};
|
|
160
162
|
} finally {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
try {
|
|
164
|
+
await observer?.dispose?.();
|
|
165
|
+
} finally {
|
|
166
|
+
interruptWaiter?.dispose();
|
|
167
|
+
unsubscribeVerbose();
|
|
168
|
+
}
|
|
163
169
|
}
|
|
164
170
|
} catch (error: unknown) {
|
|
165
171
|
const errorMessage = getErrorMessage(error);
|
package/src/cli/verbose.ts
CHANGED
|
@@ -587,8 +587,11 @@ function renderCliVerboseEventText(
|
|
|
587
587
|
case 'recording_ready':
|
|
588
588
|
return '[Midscene] Recording. Press Ctrl+C to stop and save.';
|
|
589
589
|
case 'recording_stopping':
|
|
590
|
-
|
|
591
|
-
|
|
590
|
+
if (event.reason === 'watchdog') {
|
|
591
|
+
return '[Midscene] Recording watchdog reached; finalizing and saving.';
|
|
592
|
+
}
|
|
593
|
+
return event.reason === 'sigterm'
|
|
594
|
+
? '[Midscene] SIGTERM received; finalizing and saving.'
|
|
592
595
|
: '[Midscene] Ctrl+C received; finalizing and saving.';
|
|
593
596
|
case 'dump_update': {
|
|
594
597
|
if (isActVerboseEvent(command, tool)) {
|