@elench/testkit 0.1.113 → 0.1.115
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/lib/cli/assistant/app.mjs +4 -2
- package/lib/cli/assistant/session.mjs +5 -1
- package/lib/cli/assistant/state.mjs +1 -2
- package/lib/cli/components/blocks/run-tree.mjs +7 -2
- package/lib/cli/components/hooks/use-element-layout.mjs +63 -0
- package/lib/cli/components/hooks/use-spinner-frame.mjs +26 -0
- package/lib/playwright/index.d.ts +1 -0
- package/lib/playwright/index.mjs +1 -0
- package/lib/runner/default-runtime-runner.mjs +5 -28
- package/lib/runner/lifecycle.mjs +2 -51
- package/lib/runner/managed-processes.mjs +2 -1
- package/lib/runner/playwright-config.mjs +13 -1
- package/lib/runner/playwright-runner.mjs +85 -15
- package/lib/runner/processes.mjs +59 -3
- package/lib/runner/subprocess.mjs +155 -0
- package/lib/shared/file-timeout.mjs +1 -1
- package/lib/ui/index.d.ts +2 -0
- package/lib/ui/index.mjs +1 -0
- package/node_modules/@alcalzone/ansi-tokenize/README.md +0 -5
- package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.d.ts +8 -0
- package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.js +10 -8
- package/node_modules/@alcalzone/ansi-tokenize/build/ansiCodes.js.map +1 -1
- package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.d.ts +1 -5
- package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.js +9 -45
- package/node_modules/@alcalzone/ansi-tokenize/build/tokenize.js.map +1 -1
- package/node_modules/@alcalzone/ansi-tokenize/package.json +1 -1
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/node_modules/cli-boxes/index.d.ts +95 -90
- package/node_modules/cli-boxes/index.js +5 -2
- package/node_modules/cli-boxes/package.json +6 -13
- package/node_modules/cli-boxes/readme.md +15 -3
- package/node_modules/cli-truncate/index.d.ts +1 -1
- package/node_modules/cli-truncate/package.json +4 -4
- package/node_modules/cli-truncate/readme.md +1 -0
- package/node_modules/ink/build/apply-styles.js +175 -0
- package/node_modules/ink/build/build-layout.js +77 -0
- package/node_modules/ink/build/calculate-wrapped-text.js +53 -0
- package/node_modules/ink/build/components/App.d.ts +1 -4
- package/node_modules/ink/build/components/App.js +22 -142
- package/node_modules/ink/build/components/App.js.map +1 -1
- package/node_modules/ink/build/components/AppContext.d.ts +3 -23
- package/node_modules/ink/build/components/AppContext.js +4 -7
- package/node_modules/ink/build/components/AppContext.js.map +1 -1
- package/node_modules/ink/build/components/Box.d.ts +3 -16
- package/node_modules/ink/build/components/Color.js +62 -0
- package/node_modules/ink/build/components/Cursor.d.ts +83 -0
- package/node_modules/ink/build/components/Cursor.js +53 -0
- package/node_modules/ink/build/components/Cursor.js.map +1 -0
- package/node_modules/ink/build/components/ErrorBoundary.d.ts +2 -2
- package/node_modules/ink/build/components/ErrorOverview.js +6 -6
- package/node_modules/ink/build/components/ErrorOverview.js.map +1 -1
- package/node_modules/ink/build/components/Static.js.map +1 -1
- package/node_modules/ink/build/components/StdinContext.d.ts +1 -7
- package/node_modules/ink/build/components/StdinContext.js +0 -1
- package/node_modules/ink/build/components/StdinContext.js.map +1 -1
- package/node_modules/ink/build/components/Text.d.ts +1 -1
- package/node_modules/ink/build/components/Text.js +1 -1
- package/node_modules/ink/build/components/Text.js.map +1 -1
- package/node_modules/ink/build/components/Transform.d.ts +1 -1
- package/node_modules/ink/build/devtools-window-polyfill.js +4 -7
- package/node_modules/ink/build/devtools-window-polyfill.js.map +1 -1
- package/node_modules/ink/build/devtools.js +6 -31
- package/node_modules/ink/build/devtools.js.map +1 -1
- package/node_modules/ink/build/dom.d.ts +1 -5
- package/node_modules/ink/build/dom.js +1 -20
- package/node_modules/ink/build/dom.js.map +1 -1
- package/node_modules/ink/build/experimental/apply-style.js +140 -0
- package/node_modules/ink/build/experimental/dom.js +123 -0
- package/node_modules/ink/build/experimental/output.js +91 -0
- package/node_modules/ink/build/experimental/reconciler.js +141 -0
- package/node_modules/ink/build/experimental/renderer.js +81 -0
- package/node_modules/ink/build/hooks/use-app.d.ts +1 -1
- package/node_modules/ink/build/hooks/use-app.js +1 -1
- package/node_modules/ink/build/hooks/use-cursor.d.ts +1 -1
- package/node_modules/ink/build/hooks/use-cursor.js +1 -1
- package/node_modules/ink/build/hooks/use-focus-manager.d.ts +2 -17
- package/node_modules/ink/build/hooks/use-focus-manager.js +1 -2
- package/node_modules/ink/build/hooks/use-focus-manager.js.map +1 -1
- package/node_modules/ink/build/hooks/use-focus.d.ts +1 -2
- package/node_modules/ink/build/hooks/use-focus.js +4 -5
- package/node_modules/ink/build/hooks/use-focus.js.map +1 -1
- package/node_modules/ink/build/hooks/use-input.d.ts +1 -2
- package/node_modules/ink/build/hooks/use-input.js +80 -82
- package/node_modules/ink/build/hooks/use-input.js.map +1 -1
- package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.d.ts +1 -2
- package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.js +1 -2
- package/node_modules/ink/build/hooks/use-is-screen-reader-enabled.js.map +1 -1
- package/node_modules/ink/build/hooks/use-stderr.d.ts +1 -1
- package/node_modules/ink/build/hooks/use-stderr.js +1 -1
- package/node_modules/ink/build/hooks/use-stdin.d.ts +2 -4
- package/node_modules/ink/build/hooks/use-stdin.js +1 -2
- package/node_modules/ink/build/hooks/use-stdin.js.map +1 -1
- package/node_modules/ink/build/hooks/use-stdout.d.ts +1 -1
- package/node_modules/ink/build/hooks/use-stdout.js +1 -1
- package/node_modules/ink/build/hooks/useInput.js +38 -0
- package/node_modules/ink/build/index.d.ts +1 -8
- package/node_modules/ink/build/index.js +0 -4
- package/node_modules/ink/build/index.js.map +1 -1
- package/node_modules/ink/build/ink.d.ts +3 -48
- package/node_modules/ink/build/ink.js +155 -325
- package/node_modules/ink/build/ink.js.map +1 -1
- package/node_modules/ink/build/input-parser.d.ts +1 -4
- package/node_modules/ink/build/input-parser.js +30 -70
- package/node_modules/ink/build/input-parser.js.map +1 -1
- package/node_modules/ink/build/instance.js +205 -0
- package/node_modules/ink/build/layout.d.ts +7 -0
- package/node_modules/ink/build/layout.js +33 -0
- package/node_modules/ink/build/layout.js.map +1 -0
- package/node_modules/ink/build/log-update.d.ts +0 -1
- package/node_modules/ink/build/log-update.js +1 -13
- package/node_modules/ink/build/log-update.js.map +1 -1
- package/node_modules/ink/build/measure-element.d.ts +0 -4
- package/node_modules/ink/build/measure-element.js +0 -4
- package/node_modules/ink/build/measure-element.js.map +1 -1
- package/node_modules/ink/build/options.d.ts +52 -0
- package/node_modules/ink/build/options.js +2 -0
- package/node_modules/ink/build/options.js.map +1 -0
- package/node_modules/ink/build/output.js +0 -25
- package/node_modules/ink/build/output.js.map +1 -1
- package/node_modules/ink/build/parse-keypress.d.ts +3 -1
- package/node_modules/ink/build/parse-keypress.js +17 -19
- package/node_modules/ink/build/parse-keypress.js.map +1 -1
- package/node_modules/ink/build/reconciler.js +27 -46
- package/node_modules/ink/build/reconciler.js.map +1 -1
- package/node_modules/ink/build/render-border.js +18 -29
- package/node_modules/ink/build/render-border.js.map +1 -1
- package/node_modules/ink/build/render-to-string.js +1 -2
- package/node_modules/ink/build/render-to-string.js.map +1 -1
- package/node_modules/ink/build/render.d.ts +2 -57
- package/node_modules/ink/build/render.js +11 -18
- package/node_modules/ink/build/render.js.map +1 -1
- package/node_modules/ink/build/screen-reader-update.d.ts +13 -0
- package/node_modules/ink/build/screen-reader-update.js +38 -0
- package/node_modules/ink/build/screen-reader-update.js.map +1 -0
- package/node_modules/ink/build/styles.d.ts +16 -78
- package/node_modules/ink/build/styles.js +31 -102
- package/node_modules/ink/build/styles.js.map +1 -1
- package/node_modules/ink/build/utils.d.ts +2 -9
- package/node_modules/ink/build/utils.js +3 -18
- package/node_modules/ink/build/utils.js.map +1 -1
- package/node_modules/ink/build/wrap-text.js +0 -7
- package/node_modules/ink/build/wrap-text.js.map +1 -1
- package/node_modules/ink/build/write-synchronized.d.ts +1 -1
- package/node_modules/ink/build/write-synchronized.js +2 -4
- package/node_modules/ink/build/write-synchronized.js.map +1 -1
- package/node_modules/ink/node_modules/emoji-regex/LICENSE-MIT.txt +20 -0
- package/node_modules/ink/node_modules/emoji-regex/README.md +107 -0
- package/node_modules/ink/node_modules/emoji-regex/index.d.ts +3 -0
- package/node_modules/ink/node_modules/emoji-regex/index.js +4 -0
- package/node_modules/ink/node_modules/emoji-regex/index.mjs +4 -0
- package/node_modules/ink/node_modules/emoji-regex/package.json +45 -0
- package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/index.d.ts +1 -1
- package/node_modules/ink/node_modules/wrap-ansi/index.js +222 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/index.js +82 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/license +9 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/package.json +64 -0
- package/node_modules/ink/node_modules/wrap-ansi/node_modules/string-width/readme.md +66 -0
- package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/package.json +11 -11
- package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/readme.md +0 -2
- package/node_modules/ink/package.json +98 -34
- package/node_modules/ink/readme.md +48 -554
- package/node_modules/slice-ansi/index.d.ts +1 -1
- package/node_modules/slice-ansi/index.js +89 -146
- package/node_modules/slice-ansi/package.json +5 -5
- package/node_modules/slice-ansi/readme.md +0 -1
- package/node_modules/slice-ansi/tokenize-ansi.js +1 -1
- package/package.json +11 -10
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +188 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +293 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -0
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +25 -0
- package/node_modules/@alcalzone/ansi-tokenize/build/consts.d.ts +0 -17
- package/node_modules/@alcalzone/ansi-tokenize/build/consts.js +0 -28
- package/node_modules/@alcalzone/ansi-tokenize/build/consts.js.map +0 -1
- package/node_modules/ink/build/components/AnimationContext.d.ts +0 -9
- package/node_modules/ink/build/components/AnimationContext.js +0 -13
- package/node_modules/ink/build/components/AnimationContext.js.map +0 -1
- package/node_modules/ink/build/hooks/use-animation.d.ts +0 -49
- package/node_modules/ink/build/hooks/use-animation.js +0 -87
- package/node_modules/ink/build/hooks/use-animation.js.map +0 -1
- package/node_modules/ink/build/hooks/use-box-metrics.d.ts +0 -59
- package/node_modules/ink/build/hooks/use-box-metrics.js +0 -88
- package/node_modules/ink/build/hooks/use-box-metrics.js.map +0 -1
- package/node_modules/ink/build/hooks/use-paste.d.ts +0 -35
- package/node_modules/ink/build/hooks/use-paste.js +0 -62
- package/node_modules/ink/build/hooks/use-paste.js.map +0 -1
- package/node_modules/ink/build/hooks/use-window-size.d.ts +0 -18
- package/node_modules/ink/build/hooks/use-window-size.js +0 -22
- package/node_modules/ink/build/hooks/use-window-size.js.map +0 -1
- package/node_modules/wrap-ansi/index.js +0 -468
- /package/node_modules/{wrap-ansi → ink/node_modules/wrap-ansi}/license +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { createElement, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Box, Text, useApp,
|
|
2
|
+
import { Box, Text, useApp, useCursor, useInput, useStdout } from "ink";
|
|
3
3
|
import { bold, cyan, dim, green, red, yellow } from "../terminal/colors.mjs";
|
|
4
4
|
import { RunTreeView } from "../components/blocks/run-tree.mjs";
|
|
5
|
+
import { useElementLayout } from "../components/hooks/use-element-layout.mjs";
|
|
5
6
|
import { colorCodeLine } from "./code-block.mjs";
|
|
6
7
|
import { getComposerDisplayModel } from "./composer.mjs";
|
|
7
8
|
import { MarkdownBlock } from "./markdown-block.mjs";
|
|
@@ -370,7 +371,8 @@ function colorCommandStatus(block, status) {
|
|
|
370
371
|
|
|
371
372
|
function ComposerBar({ view, busy }) {
|
|
372
373
|
const ref = useRef(null);
|
|
373
|
-
const
|
|
374
|
+
const { stdout } = useStdout();
|
|
375
|
+
const metrics = useElementLayout(ref, { stdout });
|
|
374
376
|
const { setCursorPosition } = useCursor();
|
|
375
377
|
const display = getComposerDisplayModel(
|
|
376
378
|
{
|
|
@@ -81,7 +81,11 @@ export async function runAssistantConversationTurn({
|
|
|
81
81
|
onEvent(event) {
|
|
82
82
|
appendProviderTrace(tracePath, event);
|
|
83
83
|
onProviderEvent?.(event);
|
|
84
|
-
if (
|
|
84
|
+
if (
|
|
85
|
+
event.display !== false &&
|
|
86
|
+
!event.transient &&
|
|
87
|
+
(event.type === "status" || event.type === "tool-start" || event.type === "tool-update" || event.type === "tool-end")
|
|
88
|
+
) {
|
|
85
89
|
onStatus?.(formatProviderEvent(event));
|
|
86
90
|
}
|
|
87
91
|
},
|
|
@@ -757,14 +757,13 @@ function createProviderTurnState() {
|
|
|
757
757
|
assistantMessageId: null,
|
|
758
758
|
assistantMessageIdsByProviderItem: new Map(),
|
|
759
759
|
providerToolMessageIdsByProviderItem: new Map(),
|
|
760
|
-
|
|
760
|
+
lastActivityText: null,
|
|
761
761
|
};
|
|
762
762
|
}
|
|
763
763
|
|
|
764
764
|
function handleProviderEvent(turn, event, { appendMessage, updateMessage, setStatus } = {}) {
|
|
765
765
|
if (!event) return;
|
|
766
766
|
if (event.transient || event.display === false) {
|
|
767
|
-
if (event.type === "status" && event.text) setStatus?.(event.text);
|
|
768
767
|
return;
|
|
769
768
|
}
|
|
770
769
|
if (event.type === "assistant-delta") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { createElement, useEffect, useMemo, useState } from "react";
|
|
2
|
-
import { Box, Text,
|
|
2
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
3
3
|
import figures from "figures";
|
|
4
4
|
import { formatDuration } from "../../../runner/formatting.mjs";
|
|
5
5
|
import {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { renderSummaryBox } from "../primitives/summary-box.mjs";
|
|
15
15
|
import { getTerminalWidth } from "../../terminal/layout.mjs";
|
|
16
16
|
import { renderFailureDetail, renderPassedDetail } from "../../renderers/run/inline-detail.mjs";
|
|
17
|
+
import { useSpinnerFrame } from "../hooks/use-spinner-frame.mjs";
|
|
17
18
|
|
|
18
19
|
const SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
19
20
|
|
|
@@ -27,7 +28,11 @@ export function RunTreeView({
|
|
|
27
28
|
const { exit } = useApp();
|
|
28
29
|
const controlledSnapshot = Boolean(snapshotOverride);
|
|
29
30
|
const [snapshot, setSnapshot] = useState(() => snapshotOverride || runState.getSnapshot());
|
|
30
|
-
const
|
|
31
|
+
const frame = useSpinnerFrame({
|
|
32
|
+
frameCount: SPINNER_FRAMES.length,
|
|
33
|
+
intervalMs: 80,
|
|
34
|
+
isActive: !controlledSnapshot && !snapshot.finished,
|
|
35
|
+
});
|
|
31
36
|
const spinnerFrame = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
|
32
37
|
|
|
33
38
|
useEffect(() => {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
const EMPTY_LAYOUT = Object.freeze({
|
|
4
|
+
width: 0,
|
|
5
|
+
height: 0,
|
|
6
|
+
left: 0,
|
|
7
|
+
top: 0,
|
|
8
|
+
hasMeasured: false,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function readElementLayout(ref) {
|
|
12
|
+
const node = ref?.current || null;
|
|
13
|
+
const rawLayout = node?.yogaNode?.getComputedLayout?.() || null;
|
|
14
|
+
if (!node || !rawLayout) return EMPTY_LAYOUT;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
width: normalizeLayoutNumber(rawLayout.width),
|
|
18
|
+
height: normalizeLayoutNumber(rawLayout.height),
|
|
19
|
+
left: normalizeLayoutNumber(rawLayout.left),
|
|
20
|
+
top: normalizeLayoutNumber(rawLayout.top),
|
|
21
|
+
hasMeasured: true,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function layoutsEqual(left, right) {
|
|
26
|
+
return (
|
|
27
|
+
left.width === right.width &&
|
|
28
|
+
left.height === right.height &&
|
|
29
|
+
left.left === right.left &&
|
|
30
|
+
left.top === right.top &&
|
|
31
|
+
left.hasMeasured === right.hasMeasured
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useElementLayout(ref, { stdout } = {}) {
|
|
36
|
+
const [layout, setLayout] = useState(EMPTY_LAYOUT);
|
|
37
|
+
|
|
38
|
+
const updateLayout = useCallback(() => {
|
|
39
|
+
const nextLayout = readElementLayout(ref);
|
|
40
|
+
setLayout((currentLayout) => layoutsEqual(currentLayout, nextLayout) ? currentLayout : nextLayout);
|
|
41
|
+
}, [ref]);
|
|
42
|
+
|
|
43
|
+
useEffect(updateLayout);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!stdout?.on) return undefined;
|
|
47
|
+
|
|
48
|
+
stdout.on("resize", updateLayout);
|
|
49
|
+
return () => {
|
|
50
|
+
if (stdout.off) {
|
|
51
|
+
stdout.off("resize", updateLayout);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
stdout.removeListener?.("resize", updateLayout);
|
|
55
|
+
};
|
|
56
|
+
}, [stdout, updateLayout]);
|
|
57
|
+
|
|
58
|
+
return useMemo(() => layout, [layout]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeLayoutNumber(value) {
|
|
62
|
+
return Number.isFinite(value) ? value : 0;
|
|
63
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function nextSpinnerFrame(frame, frameCount) {
|
|
4
|
+
const normalizedFrameCount = Number.isInteger(frameCount) && frameCount > 0 ? frameCount : 1;
|
|
5
|
+
return (frame + 1) % normalizedFrameCount;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function useSpinnerFrame({
|
|
9
|
+
frameCount,
|
|
10
|
+
intervalMs = 80,
|
|
11
|
+
isActive = true,
|
|
12
|
+
} = {}) {
|
|
13
|
+
const [frame, setFrame] = useState(0);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!isActive) return undefined;
|
|
17
|
+
|
|
18
|
+
const timer = setInterval(() => {
|
|
19
|
+
setFrame((currentFrame) => nextSpinnerFrame(currentFrame, frameCount));
|
|
20
|
+
}, intervalMs);
|
|
21
|
+
|
|
22
|
+
return () => clearInterval(timer);
|
|
23
|
+
}, [frameCount, intervalMs, isActive]);
|
|
24
|
+
|
|
25
|
+
return frame;
|
|
26
|
+
}
|
|
@@ -11,6 +11,7 @@ export declare function defineConfig<T extends Record<string, unknown>>(
|
|
|
11
11
|
config: T,
|
|
12
12
|
options?: PlaywrightConfigOptions
|
|
13
13
|
): T & {
|
|
14
|
+
globalTimeout?: number;
|
|
14
15
|
timeout: number;
|
|
15
16
|
expect: Record<string, unknown> & { timeout: number };
|
|
16
17
|
use: Record<string, unknown> & {
|
package/lib/playwright/index.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { execa } from "execa";
|
|
4
3
|
import { bundleK6File } from "../bundler/index.mjs";
|
|
5
4
|
import { resolveK6Binary } from "../config/binaries.mjs";
|
|
6
5
|
import {
|
|
@@ -17,7 +16,7 @@ import { RUNTIME_ARTIFACT_MARKER } from "../runtime-src/k6/artifacts.js";
|
|
|
17
16
|
import { readDatabaseUrl } from "./state-io.mjs";
|
|
18
17
|
import { buildTaskExecutionEnv } from "./template.mjs";
|
|
19
18
|
import { registerManagedProcess, unregisterManagedProcess } from "./managed-processes.mjs";
|
|
20
|
-
import {
|
|
19
|
+
import { settleManagedSubprocess, startManagedSubprocess } from "./subprocess.mjs";
|
|
21
20
|
|
|
22
21
|
export async function runHttpK6Task(targetConfig, task, lifecycle, lease, reporter = null) {
|
|
23
22
|
const baseUrl = targetConfig.testkit.local?.baseUrl;
|
|
@@ -88,7 +87,7 @@ export async function runDefaultRuntimeTask(
|
|
|
88
87
|
fs.mkdirSync(path.dirname(summaryFile), { recursive: true });
|
|
89
88
|
const startedAt = Date.now();
|
|
90
89
|
const fileTimeoutSeconds = targetConfig.testkit.execution.fileTimeoutSeconds;
|
|
91
|
-
const subprocess =
|
|
90
|
+
const subprocess = startManagedSubprocess(
|
|
92
91
|
k6Binary,
|
|
93
92
|
[...args.slice(0, 1), "--summary-export", summaryFile, ...args.slice(1)],
|
|
94
93
|
{
|
|
@@ -102,8 +101,6 @@ export async function runDefaultRuntimeTask(
|
|
|
102
101
|
},
|
|
103
102
|
process.env
|
|
104
103
|
),
|
|
105
|
-
reject: false,
|
|
106
|
-
forceKillAfterDelay: 5_000,
|
|
107
104
|
}
|
|
108
105
|
);
|
|
109
106
|
registerManagedProcess(lifecycle, subprocess, "SIGINT");
|
|
@@ -111,7 +108,9 @@ export async function runDefaultRuntimeTask(
|
|
|
111
108
|
let result;
|
|
112
109
|
let timedOut;
|
|
113
110
|
try {
|
|
114
|
-
({ result, timedOut } = await
|
|
111
|
+
({ result, timedOut } = await settleManagedSubprocess(subprocess, {
|
|
112
|
+
timeoutMs: fileTimeoutSeconds * 1000 + 1_000,
|
|
113
|
+
}));
|
|
115
114
|
} finally {
|
|
116
115
|
unregisterManagedProcess(lifecycle, subprocess);
|
|
117
116
|
}
|
|
@@ -170,28 +169,6 @@ export async function runDefaultRuntimeTask(
|
|
|
170
169
|
};
|
|
171
170
|
}
|
|
172
171
|
|
|
173
|
-
export async function settleSubprocess(subprocess, fileTimeoutSeconds) {
|
|
174
|
-
const timeoutMs = fileTimeoutSeconds * 1000 + 1_000;
|
|
175
|
-
let timeoutHandle = null;
|
|
176
|
-
let timedOut = false;
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
return await Promise.race([
|
|
180
|
-
subprocess.then((result) => ({ result, timedOut })),
|
|
181
|
-
new Promise((resolve) => {
|
|
182
|
-
timeoutHandle = setTimeout(async () => {
|
|
183
|
-
timedOut = true;
|
|
184
|
-
killChildProcess(subprocess, "SIGTERM");
|
|
185
|
-
const result = await subprocess.catch((error) => error);
|
|
186
|
-
resolve({ result, timedOut: true });
|
|
187
|
-
}, timeoutMs);
|
|
188
|
-
}),
|
|
189
|
-
]);
|
|
190
|
-
} finally {
|
|
191
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
172
|
export function buildDefaultRuntimeSummaryPath(lease, task) {
|
|
196
173
|
return path.join(lease.leaseDir, "default-runtime", `task-${task.id}.summary.json`);
|
|
197
174
|
}
|
package/lib/runner/lifecycle.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import crypto from "crypto";
|
|
4
|
-
import { execFileSync } from "child_process";
|
|
5
4
|
import { destroyRuntimeDatabase, cleanupOrphanedLocalInfrastructure } from "../database/index.mjs";
|
|
5
|
+
import { killProcessTree, listDescendantPids } from "./processes.mjs";
|
|
6
6
|
|
|
7
7
|
const RUN_SCHEMA_VERSION = 1;
|
|
8
8
|
const RUNS_DIRNAME = path.join(".testkit", "_runs");
|
|
@@ -297,22 +297,7 @@ async function terminateOwnedProcess(service) {
|
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
function killProcessGroup(pid, signal) {
|
|
300
|
-
|
|
301
|
-
process.kill(-pid, signal);
|
|
302
|
-
return;
|
|
303
|
-
} catch (error) {
|
|
304
|
-
if (error?.code !== "ESRCH") {
|
|
305
|
-
// Fall through and try the direct pid.
|
|
306
|
-
} else {
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
try {
|
|
312
|
-
process.kill(pid, signal);
|
|
313
|
-
} catch (error) {
|
|
314
|
-
if (error?.code !== "ESRCH") throw error;
|
|
315
|
-
}
|
|
300
|
+
killProcessTree(pid, signal);
|
|
316
301
|
}
|
|
317
302
|
|
|
318
303
|
function terminateDescendantProcesses(rootPid, signal) {
|
|
@@ -328,40 +313,6 @@ function terminateDescendantProcesses(rootPid, signal) {
|
|
|
328
313
|
}
|
|
329
314
|
}
|
|
330
315
|
|
|
331
|
-
function listDescendantPids(rootPid) {
|
|
332
|
-
try {
|
|
333
|
-
const output = execFileSync("ps", ["-eo", "pid=,ppid="], {
|
|
334
|
-
encoding: "utf8",
|
|
335
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
336
|
-
});
|
|
337
|
-
const childrenByParent = new Map();
|
|
338
|
-
for (const line of output.split(/\r?\n/)) {
|
|
339
|
-
const trimmed = line.trim();
|
|
340
|
-
if (!trimmed) continue;
|
|
341
|
-
const [pidRaw, parentRaw] = trimmed.split(/\s+/);
|
|
342
|
-
const pid = Number(pidRaw);
|
|
343
|
-
const parentPid = Number(parentRaw);
|
|
344
|
-
if (!Number.isInteger(pid) || !Number.isInteger(parentPid)) continue;
|
|
345
|
-
const siblings = childrenByParent.get(parentPid) || [];
|
|
346
|
-
siblings.push(pid);
|
|
347
|
-
childrenByParent.set(parentPid, siblings);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const descendants = [];
|
|
351
|
-
const stack = [...(childrenByParent.get(rootPid) || [])];
|
|
352
|
-
while (stack.length > 0) {
|
|
353
|
-
const pid = stack.pop();
|
|
354
|
-
descendants.push(pid);
|
|
355
|
-
for (const childPid of childrenByParent.get(pid) || []) {
|
|
356
|
-
stack.push(childPid);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
return descendants;
|
|
360
|
-
} catch {
|
|
361
|
-
return [];
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
316
|
async function waitForPidExit(pid, timeoutMs) {
|
|
366
317
|
const startedAt = Date.now();
|
|
367
318
|
while (Date.now() - startedAt < timeoutMs) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { killChildProcess } from "./processes.mjs";
|
|
2
|
+
import { terminateSubprocess } from "./subprocess.mjs";
|
|
2
3
|
|
|
3
4
|
export function registerManagedProcess(lifecycle, subprocess, signal = "SIGINT") {
|
|
4
5
|
lifecycle.registerProcess(subprocess, () => {
|
|
5
|
-
|
|
6
|
+
void terminateSubprocess(subprocess, { gracefulSignal: signal });
|
|
6
7
|
});
|
|
7
8
|
if (lifecycle.isStopRequested()) {
|
|
8
9
|
interruptManagedProcess(subprocess, signal);
|
|
@@ -3,7 +3,7 @@ import path from "path";
|
|
|
3
3
|
import { pathToFileURL } from "url";
|
|
4
4
|
import { normalizePathSeparators } from "./state.mjs";
|
|
5
5
|
|
|
6
|
-
export function ensurePlaywrightTestConfig(targetConfig, cwd, requestedFiles, lease) {
|
|
6
|
+
export function ensurePlaywrightTestConfig(targetConfig, cwd, requestedFiles, lease, options = {}) {
|
|
7
7
|
if (!lease?.leaseDir) {
|
|
8
8
|
throw new Error(
|
|
9
9
|
`Playwright task for service "${targetConfig.name}" requires a lease-scoped directory`
|
|
@@ -17,6 +17,7 @@ export function ensurePlaywrightTestConfig(targetConfig, cwd, requestedFiles, le
|
|
|
17
17
|
const configPath = path.join(stateDir, "playwright.testkit.config.mjs");
|
|
18
18
|
const baseConfigPath = findPlaywrightConfig(cwd);
|
|
19
19
|
const normalizedFiles = requestedFiles.map(normalizePathSeparators);
|
|
20
|
+
const globalTimeoutMs = normalizeOptionalPositiveInteger(options.globalTimeoutMs, "globalTimeoutMs");
|
|
20
21
|
|
|
21
22
|
let source = "";
|
|
22
23
|
if (baseConfigPath) {
|
|
@@ -28,6 +29,7 @@ export function ensurePlaywrightTestConfig(targetConfig, cwd, requestedFiles, le
|
|
|
28
29
|
` testDir: ${JSON.stringify(cwd)},\n` +
|
|
29
30
|
` testMatch: ${JSON.stringify(normalizedFiles)},\n` +
|
|
30
31
|
` outputDir: ${JSON.stringify(outputDir)},\n` +
|
|
32
|
+
(globalTimeoutMs ? ` globalTimeout: ${globalTimeoutMs},\n` : "") +
|
|
31
33
|
` workers: 1,\n` +
|
|
32
34
|
` fullyParallel: false,\n` +
|
|
33
35
|
` webServer: undefined,\n` +
|
|
@@ -38,6 +40,7 @@ export function ensurePlaywrightTestConfig(targetConfig, cwd, requestedFiles, le
|
|
|
38
40
|
` testDir: ${JSON.stringify(cwd)},\n` +
|
|
39
41
|
` testMatch: ${JSON.stringify(normalizedFiles)},\n` +
|
|
40
42
|
` outputDir: ${JSON.stringify(outputDir)},\n` +
|
|
43
|
+
(globalTimeoutMs ? ` globalTimeout: ${globalTimeoutMs},\n` : "") +
|
|
41
44
|
` workers: 1,\n` +
|
|
42
45
|
` fullyParallel: false,\n` +
|
|
43
46
|
`};\n`;
|
|
@@ -47,6 +50,15 @@ export function ensurePlaywrightTestConfig(targetConfig, cwd, requestedFiles, le
|
|
|
47
50
|
return configPath;
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
function normalizeOptionalPositiveInteger(value, label) {
|
|
54
|
+
if (value == null) return null;
|
|
55
|
+
const normalized = Number(value);
|
|
56
|
+
if (!Number.isInteger(normalized) || normalized <= 0) {
|
|
57
|
+
throw new Error(`${label} must be a positive integer.`);
|
|
58
|
+
}
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
export function resolvePlaywrightOutputDir(stateDir) {
|
|
51
63
|
return path.join(stateDir, "playwright-output");
|
|
52
64
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import
|
|
2
|
+
import fs from "fs";
|
|
3
3
|
import { parsePlaywrightJsonResults } from "../reporters/playwright.mjs";
|
|
4
4
|
import { resolveServiceCwd } from "../config/paths.mjs";
|
|
5
|
-
import { formatFileTimeoutBudgetError } from "../shared/file-timeout.mjs";
|
|
5
|
+
import { buildFileTimeoutEnv, formatFileTimeoutBudgetError } from "../shared/file-timeout.mjs";
|
|
6
6
|
import { persistTaskOutputArtifacts } from "./artifacts.mjs";
|
|
7
|
-
import { settleSubprocess } from "./default-runtime-runner.mjs";
|
|
8
7
|
import { ensurePlaywrightTestConfig } from "./playwright-config.mjs";
|
|
9
8
|
import { registerManagedProcess, unregisterManagedProcess } from "./managed-processes.mjs";
|
|
10
9
|
import { normalizePathSeparators } from "./state.mjs";
|
|
11
10
|
import { buildPlaywrightEnv } from "./template.mjs";
|
|
11
|
+
import { settleManagedSubprocess, startManagedSubprocess } from "./subprocess.mjs";
|
|
12
12
|
|
|
13
13
|
export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, reporter = null) {
|
|
14
14
|
const local = targetConfig.testkit.local;
|
|
@@ -20,20 +20,36 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, re
|
|
|
20
20
|
|
|
21
21
|
const cwd = resolveServiceCwd(targetConfig.productDir, local.cwd);
|
|
22
22
|
const requestedFile = path.relative(cwd, path.join(targetConfig.productDir, task.file));
|
|
23
|
-
const playwrightConfigPath = ensurePlaywrightTestConfig(targetConfig, cwd, [requestedFile], lease);
|
|
24
23
|
if (lifecycle.isStopRequested()) {
|
|
25
24
|
throw new Error(`testkit run interrupted before starting ${task.file}`);
|
|
26
25
|
}
|
|
27
26
|
const startedAt = Date.now();
|
|
28
27
|
const fileTimeoutSeconds = targetConfig.testkit.execution.fileTimeoutSeconds;
|
|
29
|
-
const
|
|
28
|
+
const playwrightConfigPath = ensurePlaywrightTestConfig(
|
|
29
|
+
targetConfig,
|
|
30
|
+
cwd,
|
|
31
|
+
[requestedFile],
|
|
32
|
+
lease,
|
|
33
|
+
{ globalTimeoutMs: fileTimeoutSeconds * 1000 }
|
|
34
|
+
);
|
|
35
|
+
const jsonReportPath = buildPlaywrightJsonReportPath(lease, task);
|
|
36
|
+
const subprocess = startManagedSubprocess(
|
|
30
37
|
"npx",
|
|
31
38
|
["playwright", "test", "--config", playwrightConfigPath, "--reporter=json"],
|
|
32
39
|
{
|
|
33
40
|
cwd,
|
|
34
|
-
env:
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
env: {
|
|
42
|
+
...buildPlaywrightEnv(
|
|
43
|
+
targetConfig,
|
|
44
|
+
local.baseUrl,
|
|
45
|
+
lease,
|
|
46
|
+
{
|
|
47
|
+
...process.env,
|
|
48
|
+
...buildFileTimeoutEnv(fileTimeoutSeconds, startedAt),
|
|
49
|
+
}
|
|
50
|
+
),
|
|
51
|
+
PLAYWRIGHT_JSON_OUTPUT_FILE: jsonReportPath,
|
|
52
|
+
},
|
|
37
53
|
}
|
|
38
54
|
);
|
|
39
55
|
registerManagedProcess(lifecycle, subprocess, "SIGINT");
|
|
@@ -41,17 +57,30 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, re
|
|
|
41
57
|
let result;
|
|
42
58
|
let timedOut;
|
|
43
59
|
try {
|
|
44
|
-
({ result, timedOut } = await
|
|
60
|
+
({ result, timedOut } = await settleManagedSubprocess(subprocess, {
|
|
61
|
+
timeoutMs: fileTimeoutSeconds * 1000 + 1_000,
|
|
62
|
+
}));
|
|
45
63
|
} finally {
|
|
46
64
|
unregisterManagedProcess(lifecycle, subprocess);
|
|
47
65
|
}
|
|
48
66
|
|
|
49
|
-
const
|
|
67
|
+
const jsonReport = readOptionalText(jsonReportPath);
|
|
68
|
+
const stdoutReport = jsonReport ? "" : extractJsonReportFromStdout(result.stdout || "");
|
|
69
|
+
const parsed = parsePlaywrightJsonResults(jsonReport || stdoutReport, cwd);
|
|
50
70
|
const finishedAt = Date.now();
|
|
51
71
|
const durationMs = finishedAt - startedAt;
|
|
52
72
|
const relativeFile = normalizePathSeparators(requestedFile);
|
|
53
73
|
const fileResult = parsed.fileResults.get(relativeFile);
|
|
54
74
|
const outputArtifacts = persistTaskOutputArtifacts(targetConfig.productDir, task, [
|
|
75
|
+
result.stdout
|
|
76
|
+
? {
|
|
77
|
+
name: "playwright-stdout",
|
|
78
|
+
kind: "runtime.output",
|
|
79
|
+
summary: result.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "captured stdout",
|
|
80
|
+
stream: "stdout",
|
|
81
|
+
text: result.stdout,
|
|
82
|
+
}
|
|
83
|
+
: null,
|
|
55
84
|
result.stderr
|
|
56
85
|
? {
|
|
57
86
|
name: "playwright-stderr",
|
|
@@ -62,16 +91,19 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, re
|
|
|
62
91
|
}
|
|
63
92
|
: null,
|
|
64
93
|
]);
|
|
65
|
-
const
|
|
94
|
+
const parsedError = stripAnsi(parsed.errors[0] || "");
|
|
95
|
+
const globalTimeoutError = isPlaywrightGlobalTimeoutError(parsedError);
|
|
96
|
+
const genericError = timedOut || globalTimeoutError
|
|
66
97
|
? formatFileTimeoutBudgetError(fileTimeoutSeconds)
|
|
67
98
|
: result.exitCode === 0
|
|
68
|
-
?
|
|
69
|
-
:
|
|
99
|
+
? parsedError || null
|
|
100
|
+
: parsedError || firstNonEmptyLine(result.stderr) || firstNonEmptyLine(result.stdout) || `Playwright exited with code ${result.exitCode}`;
|
|
101
|
+
const failed = timedOut || globalTimeoutError || (fileResult ? fileResult.status === "failed" : genericError !== null);
|
|
70
102
|
|
|
71
103
|
return {
|
|
72
104
|
task,
|
|
73
|
-
failed
|
|
74
|
-
status: timedOut ? "failed" : fileResult?.status || (
|
|
105
|
+
failed,
|
|
106
|
+
status: timedOut ? "failed" : fileResult?.status || (failed ? "failed" : "passed"),
|
|
75
107
|
error: timedOut ? genericError : fileResult?.error || genericError,
|
|
76
108
|
durationMs: fileResult?.durationMs > 0 ? fileResult.durationMs : durationMs,
|
|
77
109
|
startedAt,
|
|
@@ -80,3 +112,41 @@ export async function runPlaywrightTask(targetConfig, task, lifecycle, lease, re
|
|
|
80
112
|
failureDetails: timedOut ? [] : fileResult?.failureDetails || [],
|
|
81
113
|
};
|
|
82
114
|
}
|
|
115
|
+
|
|
116
|
+
export function buildPlaywrightJsonReportPath(lease, task) {
|
|
117
|
+
if (!lease?.leaseDir) {
|
|
118
|
+
throw new Error(`Playwright task ${task?.file || ""} requires a lease-scoped directory`);
|
|
119
|
+
}
|
|
120
|
+
const reportDir = path.join(lease.leaseDir, "playwright-reports");
|
|
121
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
122
|
+
return path.join(reportDir, `task-${task.id}.json`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readOptionalText(filePath) {
|
|
126
|
+
try {
|
|
127
|
+
return fs.readFileSync(filePath, "utf8");
|
|
128
|
+
} catch {
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function extractJsonReportFromStdout(stdout) {
|
|
134
|
+
const trimmed = String(stdout || "").trim();
|
|
135
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return "";
|
|
136
|
+
return trimmed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function firstNonEmptyLine(value) {
|
|
140
|
+
return stripAnsi(value)
|
|
141
|
+
.split(/\r?\n/)
|
|
142
|
+
.map((line) => line.trim())
|
|
143
|
+
.find(Boolean) || null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isPlaywrightGlobalTimeoutError(value) {
|
|
147
|
+
return /Timed out waiting \d+(?:\.\d+)?s for the test suite to run/i.test(value || "");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function stripAnsi(value) {
|
|
151
|
+
return String(value || "").replace(/\u001b\[[0-9;]*m/g, "");
|
|
152
|
+
}
|
package/lib/runner/processes.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
1
|
+
import { execFileSync, spawn } from "child_process";
|
|
2
2
|
|
|
3
3
|
export function normalizeServiceStartCommand(command) {
|
|
4
4
|
if (typeof command !== "string") {
|
|
@@ -36,9 +36,16 @@ export function startDetachedCommand(command, cwd, env) {
|
|
|
36
36
|
|
|
37
37
|
export function killChildProcess(child, signal) {
|
|
38
38
|
if (!child?.pid) return;
|
|
39
|
+
killProcessTree(child.pid, signal);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function killProcessTree(pid, signal) {
|
|
43
|
+
const normalizedPid = Number(pid);
|
|
44
|
+
if (!Number.isInteger(normalizedPid) || normalizedPid <= 0) return;
|
|
45
|
+
const descendants = listDescendantPids(normalizedPid);
|
|
39
46
|
|
|
40
47
|
try {
|
|
41
|
-
process.kill(-
|
|
48
|
+
process.kill(-normalizedPid, signal);
|
|
42
49
|
return;
|
|
43
50
|
} catch (error) {
|
|
44
51
|
if (error?.code !== "ESRCH") {
|
|
@@ -49,10 +56,18 @@ export function killChildProcess(child, signal) {
|
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
try {
|
|
52
|
-
|
|
59
|
+
process.kill(normalizedPid, signal);
|
|
53
60
|
} catch (error) {
|
|
54
61
|
if (error?.code !== "ESRCH") throw error;
|
|
55
62
|
}
|
|
63
|
+
|
|
64
|
+
for (const childPid of descendants) {
|
|
65
|
+
try {
|
|
66
|
+
process.kill(childPid, signal);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error?.code !== "ESRCH") throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
export function captureOutput(stream, options = {}) {
|
|
@@ -116,3 +131,44 @@ export async function stopChildProcess(child, outputDrains = []) {
|
|
|
116
131
|
export function sleep(ms) {
|
|
117
132
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
118
133
|
}
|
|
134
|
+
|
|
135
|
+
export function listDescendantPids(rootPid) {
|
|
136
|
+
const normalizedRootPid = Number(rootPid);
|
|
137
|
+
if (!Number.isInteger(normalizedRootPid) || normalizedRootPid <= 0) return [];
|
|
138
|
+
|
|
139
|
+
if (process.platform === "win32") {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const output = execFileSync("ps", ["-eo", "pid=,ppid="], {
|
|
145
|
+
encoding: "utf8",
|
|
146
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
147
|
+
});
|
|
148
|
+
const childrenByParent = new Map();
|
|
149
|
+
for (const line of output.split(/\r?\n/)) {
|
|
150
|
+
const trimmed = line.trim();
|
|
151
|
+
if (!trimmed) continue;
|
|
152
|
+
const [pidRaw, parentRaw] = trimmed.split(/\s+/);
|
|
153
|
+
const pid = Number(pidRaw);
|
|
154
|
+
const parentPid = Number(parentRaw);
|
|
155
|
+
if (!Number.isInteger(pid) || !Number.isInteger(parentPid)) continue;
|
|
156
|
+
const siblings = childrenByParent.get(parentPid) || [];
|
|
157
|
+
siblings.push(pid);
|
|
158
|
+
childrenByParent.set(parentPid, siblings);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const descendants = [];
|
|
162
|
+
const stack = [...(childrenByParent.get(normalizedRootPid) || [])];
|
|
163
|
+
while (stack.length > 0) {
|
|
164
|
+
const pid = stack.pop();
|
|
165
|
+
descendants.push(pid);
|
|
166
|
+
for (const childPid of childrenByParent.get(pid) || []) {
|
|
167
|
+
stack.push(childPid);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return descendants;
|
|
171
|
+
} catch {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
}
|