ugly-app 0.1.983 → 0.1.984
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/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +18 -41
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/inspect/ui/panels/ConsolePanel.d.ts.map +1 -1
- package/dist/inspect/ui/panels/ConsolePanel.js +15 -3
- package/dist/inspect/ui/panels/ConsolePanel.js.map +1 -1
- package/dist/inspect/ui/panels/ConsolePanel.test.d.ts +2 -0
- package/dist/inspect/ui/panels/ConsolePanel.test.d.ts.map +1 -0
- package/dist/inspect/ui/panels/ConsolePanel.test.js +57 -0
- package/dist/inspect/ui/panels/ConsolePanel.test.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/dev.ts +18 -42
- package/src/cli/version.ts +1 -1
- package/src/inspect/ui/panels/ConsolePanel.test.tsx +76 -0
- package/src/inspect/ui/panels/ConsolePanel.tsx +23 -5
- package/dist/cli/devReap.d.ts +0 -57
- package/dist/cli/devReap.d.ts.map +0 -1
- package/dist/cli/devReap.js +0 -173
- package/dist/cli/devReap.js.map +0 -1
- package/src/cli/devReap.test.ts +0 -154
- package/src/cli/devReap.ts +0 -207
package/dist/cli/dev.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"AAkDA,wBAAsB,MAAM,CAC1B,IAAI,GAAE;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAqC,GAC5E,OAAO,CAAC,IAAI,CAAC,CAkQf"}
|
package/dist/cli/dev.js
CHANGED
|
@@ -39,7 +39,6 @@ import { readUglyAppConfig } from './uglyappConfig.js';
|
|
|
39
39
|
import { lintSummaryAsync } from './lint.js';
|
|
40
40
|
import { checkDevLock, releaseDevLock, setAllowMultiple, stopHint, writeDevLock, } from './devLock.js';
|
|
41
41
|
import { backgroundWait } from '../server/backgroundWait.js';
|
|
42
|
-
import { killSubtree, startOrphanWatch } from './devReap.js';
|
|
43
42
|
export async function runDev(opts = { watch: false, verbose: false }) {
|
|
44
43
|
// ── Set universal dev env vars ─────────────────────────────────────────────
|
|
45
44
|
if (!process.env['NODE_ENV'])
|
|
@@ -225,51 +224,29 @@ export async function runDev(opts = { watch: false, verbose: false }) {
|
|
|
225
224
|
console.log(` [${c.name}] ${c.command}`);
|
|
226
225
|
}
|
|
227
226
|
}
|
|
228
|
-
const { result } = concurrently(finalCommands, {
|
|
227
|
+
const { result, commands: runningCommands } = concurrently(finalCommands, {
|
|
229
228
|
killOthersOn: ['failure'],
|
|
230
229
|
handleInput: true,
|
|
231
230
|
raw: !isWin,
|
|
232
231
|
});
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return;
|
|
249
|
-
reaped = true;
|
|
250
|
-
const forced = killSubtree(process.pid);
|
|
251
|
-
if (forced.length > 0 && verbose) {
|
|
252
|
-
console.log(`[ugly-app] SIGKILLed ${forced.length} stuck child(ren)`);
|
|
232
|
+
// Self-reap when orphaned. `concurrently` forwards Ctrl-C and cross-kills on
|
|
233
|
+
// failure, but nothing here notices the *launching* process dying: when the
|
|
234
|
+
// terminal / session that started `ugly-app dev` exits, the OS re-parents us
|
|
235
|
+
// to PID 1 and we'd otherwise keep tsx/tsc/minio watching files forever.
|
|
236
|
+
// macOS has no PR_SET_PDEATHSIG, so poll process.ppid and shut our own child
|
|
237
|
+
// watchers down the moment it flips to 1. Only meaningful in watch mode.
|
|
238
|
+
if (opts.watch && !isWin) {
|
|
239
|
+
const bornPpid = process.ppid; // guard: never fire if we started orphaned
|
|
240
|
+
const orphanWatch = setInterval(() => {
|
|
241
|
+
if (bornPpid !== 1 && process.ppid === 1) {
|
|
242
|
+
clearInterval(orphanWatch);
|
|
243
|
+
console.error('[ugly-app] launching process exited — stopping dev watchers');
|
|
244
|
+
for (const c of runningCommands)
|
|
245
|
+
c.kill('SIGTERM');
|
|
246
|
+
setTimeout(() => process.exit(0), 2000).unref();
|
|
253
247
|
}
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// Self-reap when the session that launched us goes away. macOS has no
|
|
257
|
-
// PR_SET_PDEATHSIG, so poll instead — and poll the whole ancestor *chain*,
|
|
258
|
-
// not just our ppid. `npx ugly-app dev` runs us under an `npm exec`
|
|
259
|
-
// wrapper that outlives the terminal, so our ppid stays valid forever and
|
|
260
|
-
// a ppid===1 test never fires (that is how a ugly-bot dev stack and its
|
|
261
|
-
// minio stayed up for two days). This runs in non-watch mode too: a plain
|
|
262
|
-
// `ugly-app dev` leaks a `tsx server/index.ts` the same way, and two of
|
|
263
|
-
// those were found 3 and 5.5 days old.
|
|
264
|
-
//
|
|
265
|
-
// Set UGLY_APP_NO_ORPHAN_REAP=1 to deliberately outlive your launcher.
|
|
266
|
-
if (!process.env['UGLY_APP_NO_ORPHAN_REAP']) {
|
|
267
|
-
startOrphanWatch((deadAncestor) => {
|
|
268
|
-
console.error(`[ugly-app] launching process (pid ${deadAncestor}) exited — stopping the dev stack`);
|
|
269
|
-
reapChildren();
|
|
270
|
-
process.exit(0);
|
|
271
|
-
});
|
|
272
|
-
}
|
|
248
|
+
}, 3000);
|
|
249
|
+
orphanWatch.unref();
|
|
273
250
|
}
|
|
274
251
|
try {
|
|
275
252
|
await result;
|
package/dist/cli/dev.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,YAAY,MAAM,cAAc,CAAC;AACxC;;;;;GAKG;AACH,SAAS,eAAe,CAAC,IAAqB;IAC5C,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,MAAM,GAAG,GAAG,QAAQ,CAClB,2BAA2B,IAAI,sBAAsB,EACrD,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAC3D,CAAC,IAAI,EAAE,CAAC;YACT,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAChD,IAAI,CAAC,SAAS;gBAAE,OAAO,IAAI,CAAC;YAC5B,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC;YAChD,OAAO,GAAG,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;QACzC,CAAC;QACD,MAAM,GAAG,GAAG,QAAQ,CAAC,YAAY,IAAI,eAAe,EAAE;YACpD,QAAQ,EAAE,OAAO;YACjB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;SACpC,CAAC,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/C,OAAO,QAAQ,IAAI,IAAI,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,0EAA0E;QAC1E,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EACL,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,QAAQ,EACR,YAAY,GACb,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/cli/dev.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.984";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConsolePanel.d.ts","sourceRoot":"","sources":["../../../../src/inspect/ui/panels/ConsolePanel.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"ConsolePanel.d.ts","sourceRoot":"","sources":["../../../../src/inspect/ui/panels/ConsolePanel.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAwBrD,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,QAAQ,GACT,EAAE;IACD,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB,+BAsHA"}
|
|
@@ -15,12 +15,24 @@ export function ConsolePanel({ client, targetId, }) {
|
|
|
15
15
|
const [repl, setRepl] = useState([]);
|
|
16
16
|
const [input, setInput] = useState('');
|
|
17
17
|
const scrollRef = useRef(null);
|
|
18
|
+
// ⚠️ NOT `seq` — THAT IS NOT UNIQUE HERE. `seq` counts inside the page's
|
|
19
|
+
// console agent, and a fresh document gets a fresh agent, so it RESTARTS AT 1
|
|
20
|
+
// on every navigation. This list spans navigations (the target is a tab, and
|
|
21
|
+
// the tab did not change), so keying rows by seq gave two `c1`s: React warned
|
|
22
|
+
// that children "may be duplicated and/or omitted" and then omitted them —
|
|
23
|
+
// reported as "I cannot scroll to the end of the console", because the end was
|
|
24
|
+
// what stopped rendering. This counter is monotonic for the life of the list,
|
|
25
|
+
// which is the scope the keys have to be unique in.
|
|
26
|
+
const uid = useRef(0);
|
|
18
27
|
useEffect(() => {
|
|
19
28
|
let live = true;
|
|
20
29
|
setEntries([]);
|
|
21
30
|
setRepl([]);
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
const row = (e) => ({ uid: uid.current++, e });
|
|
32
|
+
void client
|
|
33
|
+
.consoleBuffer(targetId)
|
|
34
|
+
.then((b) => live && setEntries(b.map(row)));
|
|
35
|
+
const off = client.onConsole(targetId, (e) => setEntries((cur) => [...cur, row(e)].slice(-2000)));
|
|
24
36
|
return () => {
|
|
25
37
|
live = false;
|
|
26
38
|
off();
|
|
@@ -48,7 +60,7 @@ export function ConsolePanel({ client, targetId, }) {
|
|
|
48
60
|
: [...cur, { kind: 'error', text: res.exception?.text ?? 'error' }]);
|
|
49
61
|
};
|
|
50
62
|
const expand = (objectId) => client.expand(targetId, objectId);
|
|
51
|
-
return (_jsxs("div", { className: "ins-panel", children: [_jsxs("div", { className: "ins-scroll", ref: scrollRef, children: [entries.length === 0 && repl.length === 0 && (_jsxs("div", { className: "ins-empty", children: [_jsx("b", { children: "Console" }), "console output streams here \u00B7 type below to evaluate"] })), entries.map((e) => (_jsxs("div", { className: "ins-log-row", "data-level": e.level, children: [_jsx("span", { className: "ins-log-gutter", children: GUTTER[e.level] ?? '·' }), _jsx("span", { className: "ins-log-body", children: e.text }), e.source?.url && (_jsxs("span", { className: "ins-log-src", children: [shortUrl(e.source.url), ":", e.source.line] }))] }, `c${
|
|
63
|
+
return (_jsxs("div", { className: "ins-panel", children: [_jsxs("div", { className: "ins-scroll", ref: scrollRef, children: [entries.length === 0 && repl.length === 0 && (_jsxs("div", { className: "ins-empty", children: [_jsx("b", { children: "Console" }), "console output streams here \u00B7 type below to evaluate"] })), entries.map(({ uid, e }) => (_jsxs("div", { className: "ins-log-row", "data-level": e.level, children: [_jsx("span", { className: "ins-log-gutter", children: GUTTER[e.level] ?? '·' }), _jsx("span", { className: "ins-log-body", children: e.text }), e.source?.url && (_jsxs("span", { className: "ins-log-src", children: [shortUrl(e.source.url), ":", e.source.line] }))] }, `c${uid}`))), repl.map((line, i) => (_jsxs("div", { className: "ins-log-row", "data-level": line.kind === 'error' ? 'error' : 'log', children: [_jsx("span", { className: "ins-log-gutter", children: line.kind === 'input' ? '›' : line.kind === 'error' ? '✕' : '‹' }), _jsx("span", { className: "ins-log-body", children: line.value ? (_jsx(RemoteValueView, { value: line.value, expand: expand, defaultOpen: true })) : (line.text) })] }, `r${i}`)))] }), _jsx("form", { className: "ins-repl", onSubmit: (e) => {
|
|
52
64
|
e.preventDefault();
|
|
53
65
|
void run();
|
|
54
66
|
}, children: _jsx("input", { value: input, onChange: (e) => setInput(e.target.value), placeholder: "evaluate expression\u2026", spellCheck: false, autoComplete: "off" }) })] }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConsolePanel.js","sourceRoot":"","sources":["../../../../src/inspect/ui/panels/ConsolePanel.tsx"],"names":[],"mappings":";AAAA,4EAA4E;AAC5E,gFAAgF;AAEhF,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGpD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,MAAM,MAAM,GAA2B;IACrC,GAAG,EAAE,GAAG;IACR,IAAI,EAAE,GAAG;IACT,IAAI,EAAE,GAAG;IACT,KAAK,EAAE,GAAG;IACV,KAAK,EAAE,GAAG;CACX,CAAC;
|
|
1
|
+
{"version":3,"file":"ConsolePanel.js","sourceRoot":"","sources":["../../../../src/inspect/ui/panels/ConsolePanel.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConsolePanel.test.d.ts","sourceRoot":"","sources":["../../../../src/inspect/ui/panels/ConsolePanel.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// @vitest-environment jsdom
|
|
3
|
+
import { cleanup, render, act } from '@testing-library/react';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { ConsolePanel } from './ConsolePanel.js';
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
const entry = (seq, text) => ({ seq, level: 'log', text, ts: 0, args: [] });
|
|
8
|
+
/** A client whose live console stream we drive by hand. */
|
|
9
|
+
function fakeClient(buffer = []) {
|
|
10
|
+
let emit = () => { };
|
|
11
|
+
const client = {
|
|
12
|
+
consoleBuffer: vi.fn(async () => buffer),
|
|
13
|
+
onConsole: vi.fn((_t, h) => {
|
|
14
|
+
emit = h;
|
|
15
|
+
return () => { };
|
|
16
|
+
}),
|
|
17
|
+
eval: vi.fn(),
|
|
18
|
+
expand: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
return { client, push: (e) => act(() => emit(e)) };
|
|
21
|
+
}
|
|
22
|
+
describe('ConsolePanel', () => {
|
|
23
|
+
/**
|
|
24
|
+
* `seq` is a closure counter inside the page's console agent, so it RESTARTS
|
|
25
|
+
* AT 1 on every navigation — a fresh document gets a fresh agent. The panel
|
|
26
|
+
* keeps accumulating across those navigations (the target id, a tab, never
|
|
27
|
+
* changed), so a keyed-by-seq list ends up with two `c1`s, React warns that
|
|
28
|
+
* children "may be duplicated and/or omitted", and the tail of the log stops
|
|
29
|
+
* rendering: "I cannot scroll to the end of the console."
|
|
30
|
+
*/
|
|
31
|
+
it('renders every entry after a navigation resets the agent seq', async () => {
|
|
32
|
+
const warn = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
33
|
+
const { client, push } = fakeClient([entry(1, 'before-nav')]);
|
|
34
|
+
const { container } = render(_jsx(ConsolePanel, { client: client, targetId: "tab-1" }));
|
|
35
|
+
await act(async () => { });
|
|
36
|
+
// The page navigates; the new document's agent starts counting from 1 again.
|
|
37
|
+
push(entry(1, 'after-nav-a'));
|
|
38
|
+
push(entry(2, 'after-nav-b'));
|
|
39
|
+
const rows = Array.from(container.querySelectorAll('.ins-log-row'), (r) => r.querySelector('.ins-log-body')?.textContent);
|
|
40
|
+
expect(rows).toEqual(['before-nav', 'after-nav-a', 'after-nav-b']);
|
|
41
|
+
// React reports duplicate keys through console.error — none may be raised.
|
|
42
|
+
const dupWarning = warn.mock.calls
|
|
43
|
+
.flat()
|
|
44
|
+
.some((a) => String(a).includes('same key'));
|
|
45
|
+
expect(dupWarning, 'React warned about duplicate keys').toBe(false);
|
|
46
|
+
warn.mockRestore();
|
|
47
|
+
});
|
|
48
|
+
it('keeps the newest entry last so the pinned scroll lands on it', async () => {
|
|
49
|
+
const { client, push } = fakeClient([entry(7, 'old')]);
|
|
50
|
+
const { container } = render(_jsx(ConsolePanel, { client: client, targetId: "tab-1" }));
|
|
51
|
+
await act(async () => { });
|
|
52
|
+
push(entry(1, 'newest'));
|
|
53
|
+
const rows = Array.from(container.querySelectorAll('.ins-log-body'), (r) => r.textContent);
|
|
54
|
+
expect(rows.at(-1)).toBe('newest');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
//# sourceMappingURL=ConsolePanel.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConsolePanel.test.js","sourceRoot":"","sources":["../../../../src/inspect/ui/panels/ConsolePanel.test.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ugly-app",
|
|
3
3
|
"packageManager": "pnpm@10.34.5",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.984",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"comment:files": "Allowlist what ships to npm. dist = runtime; src = sourcemap targets (dist/*.js.map reference ../../src/); templates = CLI scaffold. Everything else at repo root (.pgdata local Postgres, coverage, assets/icons sources, test/, test-results/) is excluded by omission. The !negations strip the scaffold's installed deps + cruft (templates/node_modules is 200MB+ and must never ship). package.json/README/LICENSE ship automatically.",
|
|
7
7
|
"files": [
|
package/src/cli/dev.ts
CHANGED
|
@@ -47,7 +47,6 @@ import {
|
|
|
47
47
|
writeDevLock,
|
|
48
48
|
} from './devLock.js';
|
|
49
49
|
import { backgroundWait } from '../server/backgroundWait.js';
|
|
50
|
-
import { killSubtree, startOrphanWatch } from './devReap.js';
|
|
51
50
|
|
|
52
51
|
export async function runDev(
|
|
53
52
|
opts: { watch: boolean; verbose: boolean } = { watch: false, verbose: false },
|
|
@@ -269,54 +268,31 @@ export async function runDev(
|
|
|
269
268
|
}
|
|
270
269
|
}
|
|
271
270
|
|
|
272
|
-
const { result } = concurrently(finalCommands, {
|
|
271
|
+
const { result, commands: runningCommands } = concurrently(finalCommands, {
|
|
273
272
|
killOthersOn: ['failure'],
|
|
274
273
|
handleInput: true,
|
|
275
274
|
raw: !isWin,
|
|
276
275
|
});
|
|
277
276
|
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (!isWin) {
|
|
290
|
-
let reaped = false;
|
|
291
|
-
const reapChildren = (): void => {
|
|
292
|
-
if (reaped) return;
|
|
293
|
-
reaped = true;
|
|
294
|
-
const forced = killSubtree(process.pid);
|
|
295
|
-
if (forced.length > 0 && verbose) {
|
|
296
|
-
console.log(`[ugly-app] SIGKILLed ${forced.length} stuck child(ren)`);
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
process.on('exit', reapChildren);
|
|
300
|
-
|
|
301
|
-
// Self-reap when the session that launched us goes away. macOS has no
|
|
302
|
-
// PR_SET_PDEATHSIG, so poll instead — and poll the whole ancestor *chain*,
|
|
303
|
-
// not just our ppid. `npx ugly-app dev` runs us under an `npm exec`
|
|
304
|
-
// wrapper that outlives the terminal, so our ppid stays valid forever and
|
|
305
|
-
// a ppid===1 test never fires (that is how a ugly-bot dev stack and its
|
|
306
|
-
// minio stayed up for two days). This runs in non-watch mode too: a plain
|
|
307
|
-
// `ugly-app dev` leaks a `tsx server/index.ts` the same way, and two of
|
|
308
|
-
// those were found 3 and 5.5 days old.
|
|
309
|
-
//
|
|
310
|
-
// Set UGLY_APP_NO_ORPHAN_REAP=1 to deliberately outlive your launcher.
|
|
311
|
-
if (!process.env['UGLY_APP_NO_ORPHAN_REAP']) {
|
|
312
|
-
startOrphanWatch((deadAncestor) => {
|
|
277
|
+
// Self-reap when orphaned. `concurrently` forwards Ctrl-C and cross-kills on
|
|
278
|
+
// failure, but nothing here notices the *launching* process dying: when the
|
|
279
|
+
// terminal / session that started `ugly-app dev` exits, the OS re-parents us
|
|
280
|
+
// to PID 1 and we'd otherwise keep tsx/tsc/minio watching files forever.
|
|
281
|
+
// macOS has no PR_SET_PDEATHSIG, so poll process.ppid and shut our own child
|
|
282
|
+
// watchers down the moment it flips to 1. Only meaningful in watch mode.
|
|
283
|
+
if (opts.watch && !isWin) {
|
|
284
|
+
const bornPpid = process.ppid; // guard: never fire if we started orphaned
|
|
285
|
+
const orphanWatch = setInterval(() => {
|
|
286
|
+
if (bornPpid !== 1 && process.ppid === 1) {
|
|
287
|
+
clearInterval(orphanWatch);
|
|
313
288
|
console.error(
|
|
314
|
-
|
|
289
|
+
'[ugly-app] launching process exited — stopping dev watchers',
|
|
315
290
|
);
|
|
316
|
-
|
|
317
|
-
process.exit(0);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
291
|
+
for (const c of runningCommands) c.kill('SIGTERM');
|
|
292
|
+
setTimeout(() => process.exit(0), 2000).unref();
|
|
293
|
+
}
|
|
294
|
+
}, 3000);
|
|
295
|
+
orphanWatch.unref();
|
|
320
296
|
}
|
|
321
297
|
|
|
322
298
|
try {
|
package/src/cli/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by prebuild — do not edit manually
|
|
2
|
-
export const CLI_VERSION = "0.1.
|
|
2
|
+
export const CLI_VERSION = "0.1.984";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { cleanup, render, act } from '@testing-library/react';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { ConsolePanel } from './ConsolePanel.js';
|
|
5
|
+
import type { InspectClient } from '../../client.js';
|
|
6
|
+
import type { ConsoleEntry } from '../../protocol.js';
|
|
7
|
+
|
|
8
|
+
afterEach(cleanup);
|
|
9
|
+
|
|
10
|
+
const entry = (seq: number, text: string): ConsoleEntry =>
|
|
11
|
+
({ seq, level: 'log', text, ts: 0, args: [] }) as unknown as ConsoleEntry;
|
|
12
|
+
|
|
13
|
+
/** A client whose live console stream we drive by hand. */
|
|
14
|
+
function fakeClient(buffer: ConsoleEntry[] = []) {
|
|
15
|
+
let emit: (e: ConsoleEntry) => void = () => {};
|
|
16
|
+
const client = {
|
|
17
|
+
consoleBuffer: vi.fn(async () => buffer),
|
|
18
|
+
onConsole: vi.fn((_t: string, h: (e: ConsoleEntry) => void) => {
|
|
19
|
+
emit = h;
|
|
20
|
+
return () => {};
|
|
21
|
+
}),
|
|
22
|
+
eval: vi.fn(),
|
|
23
|
+
expand: vi.fn(),
|
|
24
|
+
} as unknown as InspectClient;
|
|
25
|
+
return { client, push: (e: ConsoleEntry) => act(() => emit(e)) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('ConsolePanel', () => {
|
|
29
|
+
/**
|
|
30
|
+
* `seq` is a closure counter inside the page's console agent, so it RESTARTS
|
|
31
|
+
* AT 1 on every navigation — a fresh document gets a fresh agent. The panel
|
|
32
|
+
* keeps accumulating across those navigations (the target id, a tab, never
|
|
33
|
+
* changed), so a keyed-by-seq list ends up with two `c1`s, React warns that
|
|
34
|
+
* children "may be duplicated and/or omitted", and the tail of the log stops
|
|
35
|
+
* rendering: "I cannot scroll to the end of the console."
|
|
36
|
+
*/
|
|
37
|
+
it('renders every entry after a navigation resets the agent seq', async () => {
|
|
38
|
+
const warn = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
39
|
+
const { client, push } = fakeClient([entry(1, 'before-nav')]);
|
|
40
|
+
const { container } = render(
|
|
41
|
+
<ConsolePanel client={client} targetId="tab-1" />,
|
|
42
|
+
);
|
|
43
|
+
await act(async () => {});
|
|
44
|
+
|
|
45
|
+
// The page navigates; the new document's agent starts counting from 1 again.
|
|
46
|
+
push(entry(1, 'after-nav-a'));
|
|
47
|
+
push(entry(2, 'after-nav-b'));
|
|
48
|
+
|
|
49
|
+
const rows = Array.from(
|
|
50
|
+
container.querySelectorAll('.ins-log-row'),
|
|
51
|
+
(r) => r.querySelector('.ins-log-body')?.textContent,
|
|
52
|
+
);
|
|
53
|
+
expect(rows).toEqual(['before-nav', 'after-nav-a', 'after-nav-b']);
|
|
54
|
+
|
|
55
|
+
// React reports duplicate keys through console.error — none may be raised.
|
|
56
|
+
const dupWarning = warn.mock.calls
|
|
57
|
+
.flat()
|
|
58
|
+
.some((a) => String(a).includes('same key'));
|
|
59
|
+
expect(dupWarning, 'React warned about duplicate keys').toBe(false);
|
|
60
|
+
warn.mockRestore();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('keeps the newest entry last so the pinned scroll lands on it', async () => {
|
|
64
|
+
const { client, push } = fakeClient([entry(7, 'old')]);
|
|
65
|
+
const { container } = render(
|
|
66
|
+
<ConsolePanel client={client} targetId="tab-1" />,
|
|
67
|
+
);
|
|
68
|
+
await act(async () => {});
|
|
69
|
+
push(entry(1, 'newest'));
|
|
70
|
+
const rows = Array.from(
|
|
71
|
+
container.querySelectorAll('.ins-log-body'),
|
|
72
|
+
(r) => r.textContent,
|
|
73
|
+
);
|
|
74
|
+
expect(rows.at(-1)).toBe('newest');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -20,6 +20,12 @@ interface ReplLine {
|
|
|
20
20
|
value?: RemoteValue | undefined;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** A captured entry plus a key that is unique in THIS list — see `uid` below. */
|
|
24
|
+
interface Row {
|
|
25
|
+
uid: number;
|
|
26
|
+
e: ConsoleEntry;
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
export function ConsolePanel({
|
|
24
30
|
client,
|
|
25
31
|
targetId,
|
|
@@ -27,18 +33,30 @@ export function ConsolePanel({
|
|
|
27
33
|
client: InspectClient;
|
|
28
34
|
targetId: string;
|
|
29
35
|
}) {
|
|
30
|
-
const [entries, setEntries] = useState<
|
|
36
|
+
const [entries, setEntries] = useState<Row[]>([]);
|
|
31
37
|
const [repl, setRepl] = useState<ReplLine[]>([]);
|
|
32
38
|
const [input, setInput] = useState('');
|
|
33
39
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
40
|
+
// ⚠️ NOT `seq` — THAT IS NOT UNIQUE HERE. `seq` counts inside the page's
|
|
41
|
+
// console agent, and a fresh document gets a fresh agent, so it RESTARTS AT 1
|
|
42
|
+
// on every navigation. This list spans navigations (the target is a tab, and
|
|
43
|
+
// the tab did not change), so keying rows by seq gave two `c1`s: React warned
|
|
44
|
+
// that children "may be duplicated and/or omitted" and then omitted them —
|
|
45
|
+
// reported as "I cannot scroll to the end of the console", because the end was
|
|
46
|
+
// what stopped rendering. This counter is monotonic for the life of the list,
|
|
47
|
+
// which is the scope the keys have to be unique in.
|
|
48
|
+
const uid = useRef(0);
|
|
34
49
|
|
|
35
50
|
useEffect(() => {
|
|
36
51
|
let live = true;
|
|
37
52
|
setEntries([]);
|
|
38
53
|
setRepl([]);
|
|
39
|
-
|
|
54
|
+
const row = (e: ConsoleEntry): Row => ({ uid: uid.current++, e });
|
|
55
|
+
void client
|
|
56
|
+
.consoleBuffer(targetId)
|
|
57
|
+
.then((b) => live && setEntries(b.map(row)));
|
|
40
58
|
const off = client.onConsole(targetId, (e) =>
|
|
41
|
-
setEntries((cur) => [...cur, e].slice(-2000)),
|
|
59
|
+
setEntries((cur) => [...cur, row(e)].slice(-2000)),
|
|
42
60
|
);
|
|
43
61
|
return () => {
|
|
44
62
|
live = false;
|
|
@@ -81,8 +99,8 @@ export function ConsolePanel({
|
|
|
81
99
|
console output streams here · type below to evaluate
|
|
82
100
|
</div>
|
|
83
101
|
)}
|
|
84
|
-
{entries.map((e) => (
|
|
85
|
-
<div className="ins-log-row" data-level={e.level} key={`c${
|
|
102
|
+
{entries.map(({ uid, e }) => (
|
|
103
|
+
<div className="ins-log-row" data-level={e.level} key={`c${uid}`}>
|
|
86
104
|
<span className="ins-log-gutter">{GUTTER[e.level] ?? '·'}</span>
|
|
87
105
|
<span className="ins-log-body">{e.text}</span>
|
|
88
106
|
{e.source?.url && (
|
package/dist/cli/devReap.d.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
export interface ProcRow {
|
|
2
|
-
pid: number;
|
|
3
|
-
ppid: number;
|
|
4
|
-
}
|
|
5
|
-
/** Parse `ps -Ao pid=,ppid=` output. Malformed lines are skipped, not thrown. */
|
|
6
|
-
export declare function parsePsTable(out: string): ProcRow[];
|
|
7
|
-
export declare function readProcTable(): ProcRow[];
|
|
8
|
-
/**
|
|
9
|
-
* Descendants of `root`, deepest first, excluding `root` itself. Killing in
|
|
10
|
-
* this order stops a supervisor (tsx watch) from respawning a child we already
|
|
11
|
-
* killed. Cycle-safe: a pid is only ever visited once, so a recycled-pid loop
|
|
12
|
-
* in the table cannot hang the caller.
|
|
13
|
-
*/
|
|
14
|
-
export declare function descendantsOf(root: number, rows: ProcRow[]): number[];
|
|
15
|
-
/**
|
|
16
|
-
* The ancestor chain of `pid`, nearest first, stopping before pid 1. Empty when
|
|
17
|
-
* the process is already an orphan — which is exactly when an orphan watch must
|
|
18
|
-
* not arm itself.
|
|
19
|
-
*/
|
|
20
|
-
export declare function ancestorsOf(pid: number, rows: ProcRow[]): number[];
|
|
21
|
-
/** Signal 0 tests for existence without delivering anything. */
|
|
22
|
-
export declare function isAlive(pid: number): boolean;
|
|
23
|
-
export interface KillSubtreeOptions {
|
|
24
|
-
/** How long to let SIGTERM work before escalating. */
|
|
25
|
-
graceMs?: number;
|
|
26
|
-
/** Injected in tests. */
|
|
27
|
-
procTable?: () => ProcRow[];
|
|
28
|
-
sleep?: (ms: number) => void;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Kill every descendant of `root` — never `root` itself, so a caller can use
|
|
32
|
-
* this on its own pid without racing its own exit. SIGTERM first (tsx and minio
|
|
33
|
-
* flush and exit cleanly given the chance), SIGKILL for whatever ignored it.
|
|
34
|
-
*
|
|
35
|
-
* Returns the pids that had to be SIGKILLed, which is worth logging: it is the
|
|
36
|
-
* signal that a child is not shutting down properly.
|
|
37
|
-
*/
|
|
38
|
-
export declare function killSubtree(root: number, opts?: KillSubtreeOptions): number[];
|
|
39
|
-
export interface OrphanWatchOptions {
|
|
40
|
-
/** Poll period. */
|
|
41
|
-
intervalMs?: number;
|
|
42
|
-
procTable?: () => ProcRow[];
|
|
43
|
-
alive?: (pid: number) => boolean;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Call `onOrphan` once the chain that launched us breaks — i.e. any ancestor
|
|
47
|
-
* recorded at startup has died. Watching the *whole* chain (not just ppid) is
|
|
48
|
-
* what catches wrapper processes: `npm exec` outlives the terminal that ran it,
|
|
49
|
-
* so our own ppid stays valid forever while the session it belonged to is long
|
|
50
|
-
* gone.
|
|
51
|
-
*
|
|
52
|
-
* Returns a stop function. No-ops (returning a no-op stopper) when we started
|
|
53
|
-
* with no ancestors, so a deliberately detached dev server is never reaped by
|
|
54
|
-
* its own watchdog on the first tick.
|
|
55
|
-
*/
|
|
56
|
-
export declare function startOrphanWatch(onOrphan: (deadAncestor: number) => void, opts?: OrphanWatchOptions): () => void;
|
|
57
|
-
//# sourceMappingURL=devReap.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"devReap.d.ts","sourceRoot":"","sources":["../../src/cli/devReap.ts"],"names":[],"mappings":"AA2BA,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,EAAE,CAQnD;AAED,wBAAgB,aAAa,IAAI,OAAO,EAAE,CAazC;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CAmBrE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CAWlE;AAED,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQ5C;AAQD,MAAM,WAAW,kBAAkB;IACjC,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,OAAO,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,kBAAuB,GAC5B,MAAM,EAAE,CAyBV;AAED,MAAM,WAAW,kBAAkB;IACjC,mBAAmB;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,OAAO,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CAClC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,EACxC,IAAI,GAAE,kBAAuB,GAC5B,MAAM,IAAI,CAeZ"}
|
package/dist/cli/devReap.js
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Making `ugly-app dev` take its whole process tree down with it.
|
|
3
|
-
*
|
|
4
|
-
* A dev stack is `ugly-app dev` -> tsx watch -> esbuild, plus `sh -c tsc
|
|
5
|
-
* --watch` -> tsc, plus minio. None of those exit on their own: tsx watch
|
|
6
|
-
* deliberately survives a crashed server, tsc --watch runs forever, minio
|
|
7
|
-
* serves forever. So the stack only ever goes away because something kills it,
|
|
8
|
-
* and two things kept stopping that from happening:
|
|
9
|
-
*
|
|
10
|
-
* 1. The self-reap watchdog in dev.ts fired on `process.ppid === 1`. When
|
|
11
|
-
* `ugly-app dev` is launched through a wrapper — `npx ugly-app dev`, which
|
|
12
|
-
* leaves an `npm exec` process in between — our parent is that wrapper,
|
|
13
|
-
* which never exits, so our ppid never becomes 1 no matter how long ago
|
|
14
|
-
* the terminal that started it went away. `ancestorsOf` + a liveness poll
|
|
15
|
-
* watches the whole chain instead of one link of it. (2026-08-03: a
|
|
16
|
-
* ugly-bot stack and its minio had been up 2 days this way.)
|
|
17
|
-
*
|
|
18
|
-
* 2. Shutdown sent SIGTERM to concurrently's direct children only. tsx and
|
|
19
|
-
* tsc routinely survive SIGTERM (the machine-level reaper's log is full of
|
|
20
|
-
* "survived TERM"), and esbuild/minio are grandchildren nothing signalled
|
|
21
|
-
* at all. `killSubtree` walks the real process table and escalates.
|
|
22
|
-
*
|
|
23
|
-
* Everything here is sync and dependency-free so it can also run from a
|
|
24
|
-
* `process.on('exit')` handler, where async work never completes.
|
|
25
|
-
*/
|
|
26
|
-
import { execSync } from 'child_process';
|
|
27
|
-
/** Parse `ps -Ao pid=,ppid=` output. Malformed lines are skipped, not thrown. */
|
|
28
|
-
export function parsePsTable(out) {
|
|
29
|
-
const rows = [];
|
|
30
|
-
for (const line of out.split('\n')) {
|
|
31
|
-
const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
32
|
-
if (!m)
|
|
33
|
-
continue;
|
|
34
|
-
rows.push({ pid: Number(m[1]), ppid: Number(m[2]) });
|
|
35
|
-
}
|
|
36
|
-
return rows;
|
|
37
|
-
}
|
|
38
|
-
export function readProcTable() {
|
|
39
|
-
try {
|
|
40
|
-
return parsePsTable(execSync('ps -Ao pid=,ppid=', {
|
|
41
|
-
encoding: 'utf-8',
|
|
42
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
43
|
-
}));
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
// No `ps` (or it failed): callers degrade to "no descendants / no
|
|
47
|
-
// ancestors", which is always safer than guessing.
|
|
48
|
-
return [];
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Descendants of `root`, deepest first, excluding `root` itself. Killing in
|
|
53
|
-
* this order stops a supervisor (tsx watch) from respawning a child we already
|
|
54
|
-
* killed. Cycle-safe: a pid is only ever visited once, so a recycled-pid loop
|
|
55
|
-
* in the table cannot hang the caller.
|
|
56
|
-
*/
|
|
57
|
-
export function descendantsOf(root, rows) {
|
|
58
|
-
const byParent = new Map();
|
|
59
|
-
for (const r of rows) {
|
|
60
|
-
const kids = byParent.get(r.ppid);
|
|
61
|
-
if (kids)
|
|
62
|
-
kids.push(r.pid);
|
|
63
|
-
else
|
|
64
|
-
byParent.set(r.ppid, [r.pid]);
|
|
65
|
-
}
|
|
66
|
-
const seen = new Set([root]);
|
|
67
|
-
const ordered = [];
|
|
68
|
-
const walk = (pid) => {
|
|
69
|
-
for (const kid of byParent.get(pid) ?? []) {
|
|
70
|
-
if (seen.has(kid))
|
|
71
|
-
continue;
|
|
72
|
-
seen.add(kid);
|
|
73
|
-
walk(kid); // depth first, so children land before their parent
|
|
74
|
-
ordered.push(kid);
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
walk(root);
|
|
78
|
-
return ordered;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* The ancestor chain of `pid`, nearest first, stopping before pid 1. Empty when
|
|
82
|
-
* the process is already an orphan — which is exactly when an orphan watch must
|
|
83
|
-
* not arm itself.
|
|
84
|
-
*/
|
|
85
|
-
export function ancestorsOf(pid, rows) {
|
|
86
|
-
const parentOf = new Map(rows.map((r) => [r.pid, r.ppid]));
|
|
87
|
-
const chain = [];
|
|
88
|
-
const seen = new Set([pid]);
|
|
89
|
-
let cur = parentOf.get(pid);
|
|
90
|
-
while (cur !== undefined && cur > 1 && !seen.has(cur)) {
|
|
91
|
-
chain.push(cur);
|
|
92
|
-
seen.add(cur);
|
|
93
|
-
cur = parentOf.get(cur);
|
|
94
|
-
}
|
|
95
|
-
return chain;
|
|
96
|
-
}
|
|
97
|
-
/** Signal 0 tests for existence without delivering anything. */
|
|
98
|
-
export function isAlive(pid) {
|
|
99
|
-
try {
|
|
100
|
-
process.kill(pid, 0);
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
catch (err) {
|
|
104
|
-
// EPERM means it exists but belongs to someone else — still alive.
|
|
105
|
-
return err.code === 'EPERM';
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/** Block the thread without a subprocess, so this is usable from `exit`. */
|
|
109
|
-
function sleepSync(ms) {
|
|
110
|
-
const shared = new Int32Array(new SharedArrayBuffer(4));
|
|
111
|
-
Atomics.wait(shared, 0, 0, ms);
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Kill every descendant of `root` — never `root` itself, so a caller can use
|
|
115
|
-
* this on its own pid without racing its own exit. SIGTERM first (tsx and minio
|
|
116
|
-
* flush and exit cleanly given the chance), SIGKILL for whatever ignored it.
|
|
117
|
-
*
|
|
118
|
-
* Returns the pids that had to be SIGKILLed, which is worth logging: it is the
|
|
119
|
-
* signal that a child is not shutting down properly.
|
|
120
|
-
*/
|
|
121
|
-
export function killSubtree(root, opts = {}) {
|
|
122
|
-
const { graceMs = 2000, procTable = readProcTable, sleep = sleepSync } = opts;
|
|
123
|
-
const targets = descendantsOf(root, procTable());
|
|
124
|
-
if (targets.length === 0)
|
|
125
|
-
return [];
|
|
126
|
-
for (const pid of targets) {
|
|
127
|
-
try {
|
|
128
|
-
process.kill(pid, 'SIGTERM');
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
/* already gone between listing and signalling */
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
sleep(graceMs);
|
|
135
|
-
const survivors = targets.filter(isAlive);
|
|
136
|
-
for (const pid of survivors) {
|
|
137
|
-
try {
|
|
138
|
-
process.kill(pid, 'SIGKILL');
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
/* raced us to the exit */
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return survivors;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Call `onOrphan` once the chain that launched us breaks — i.e. any ancestor
|
|
148
|
-
* recorded at startup has died. Watching the *whole* chain (not just ppid) is
|
|
149
|
-
* what catches wrapper processes: `npm exec` outlives the terminal that ran it,
|
|
150
|
-
* so our own ppid stays valid forever while the session it belonged to is long
|
|
151
|
-
* gone.
|
|
152
|
-
*
|
|
153
|
-
* Returns a stop function. No-ops (returning a no-op stopper) when we started
|
|
154
|
-
* with no ancestors, so a deliberately detached dev server is never reaped by
|
|
155
|
-
* its own watchdog on the first tick.
|
|
156
|
-
*/
|
|
157
|
-
export function startOrphanWatch(onOrphan, opts = {}) {
|
|
158
|
-
const { intervalMs = 3000, procTable = readProcTable, alive = isAlive } = opts;
|
|
159
|
-
const chain = ancestorsOf(process.pid, procTable());
|
|
160
|
-
if (chain.length === 0)
|
|
161
|
-
return () => undefined;
|
|
162
|
-
const timer = setInterval(() => {
|
|
163
|
-
const dead = chain.find((pid) => !alive(pid));
|
|
164
|
-
if (dead === undefined)
|
|
165
|
-
return;
|
|
166
|
-
clearInterval(timer);
|
|
167
|
-
onOrphan(dead);
|
|
168
|
-
}, intervalMs);
|
|
169
|
-
// Never hold the event loop open on account of the watchdog.
|
|
170
|
-
timer.unref();
|
|
171
|
-
return () => clearInterval(timer);
|
|
172
|
-
}
|
|
173
|
-
//# sourceMappingURL=devReap.js.map
|
package/dist/cli/devReap.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"devReap.js","sourceRoot":"","sources":["../../src/cli/devReap.ts"],"names":[],"mappings":"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"}
|
package/src/cli/devReap.test.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { spawn } from 'child_process';
|
|
3
|
-
import {
|
|
4
|
-
ancestorsOf,
|
|
5
|
-
descendantsOf,
|
|
6
|
-
isAlive,
|
|
7
|
-
killSubtree,
|
|
8
|
-
parsePsTable,
|
|
9
|
-
readProcTable,
|
|
10
|
-
startOrphanWatch,
|
|
11
|
-
type ProcRow,
|
|
12
|
-
} from './devReap.js';
|
|
13
|
-
|
|
14
|
-
/** A dev stack as `ps` actually reports it, plus unrelated processes. */
|
|
15
|
-
const STACK: ProcRow[] = [
|
|
16
|
-
{ pid: 1, ppid: 0 },
|
|
17
|
-
{ pid: 100, ppid: 1 }, // npm exec wrapper (outlives the terminal)
|
|
18
|
-
{ pid: 200, ppid: 100 }, // ugly-app dev
|
|
19
|
-
{ pid: 210, ppid: 200 }, // minio
|
|
20
|
-
{ pid: 220, ppid: 200 }, // tsx watch
|
|
21
|
-
{ pid: 221, ppid: 220 }, // esbuild
|
|
22
|
-
{ pid: 230, ppid: 200 }, // sh -c tsc --watch
|
|
23
|
-
{ pid: 231, ppid: 230 }, // tsc
|
|
24
|
-
{ pid: 900, ppid: 1 }, // somebody else's process
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
describe('parsePsTable', () => {
|
|
28
|
-
it('reads pid/ppid pairs and ignores anything malformed', () => {
|
|
29
|
-
expect(parsePsTable(' 1 0\n 502 1\n\ngarbage\n')).toEqual([
|
|
30
|
-
{ pid: 1, ppid: 0 },
|
|
31
|
-
{ pid: 502, ppid: 1 },
|
|
32
|
-
]);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe('descendantsOf', () => {
|
|
37
|
-
it('returns the whole tree deepest-first, excluding the root', () => {
|
|
38
|
-
const got = descendantsOf(200, STACK);
|
|
39
|
-
|
|
40
|
-
expect(new Set(got)).toEqual(new Set([210, 220, 221, 230, 231]));
|
|
41
|
-
expect(got).not.toContain(200);
|
|
42
|
-
// Grandchildren must be signalled before the supervisor that would respawn
|
|
43
|
-
// them — esbuild before tsx, tsc before its `sh -c` wrapper.
|
|
44
|
-
expect(got.indexOf(221)).toBeLessThan(got.indexOf(220));
|
|
45
|
-
expect(got.indexOf(231)).toBeLessThan(got.indexOf(230));
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('never reaches processes outside the tree', () => {
|
|
49
|
-
expect(descendantsOf(200, STACK)).not.toContain(900);
|
|
50
|
-
expect(descendantsOf(200, STACK)).not.toContain(100);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('terminates on a cyclic table rather than hanging', () => {
|
|
54
|
-
const cyclic: ProcRow[] = [
|
|
55
|
-
{ pid: 5, ppid: 6 },
|
|
56
|
-
{ pid: 6, ppid: 5 },
|
|
57
|
-
];
|
|
58
|
-
expect(descendantsOf(5, cyclic)).toEqual([6]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('is empty for a leaf', () => {
|
|
62
|
-
expect(descendantsOf(221, STACK)).toEqual([]);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe('ancestorsOf', () => {
|
|
67
|
-
it('walks past the wrapper the old ppid===1 check stopped at', () => {
|
|
68
|
-
// This is the ugly-bot leak: pid 200's own parent (100) is alive forever,
|
|
69
|
-
// so watching ppid alone can never notice the session dying. The chain is
|
|
70
|
-
// what makes 100's own launcher observable.
|
|
71
|
-
expect(ancestorsOf(200, STACK)).toEqual([100]);
|
|
72
|
-
expect(ancestorsOf(221, STACK)).toEqual([220, 200, 100]);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('excludes pid 1 so launchd is never treated as a live launcher', () => {
|
|
76
|
-
expect(ancestorsOf(100, STACK)).toEqual([]);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('terminates on a cyclic table', () => {
|
|
80
|
-
expect(
|
|
81
|
-
ancestorsOf(5, [
|
|
82
|
-
{ pid: 5, ppid: 6 },
|
|
83
|
-
{ pid: 6, ppid: 5 },
|
|
84
|
-
]),
|
|
85
|
-
).toEqual([6]);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe('startOrphanWatch', () => {
|
|
90
|
-
it('does not arm when the process is already an orphan', () => {
|
|
91
|
-
let fired = false;
|
|
92
|
-
const stop = startOrphanWatch(() => (fired = true), {
|
|
93
|
-
intervalMs: 1,
|
|
94
|
-
procTable: () => [{ pid: process.pid, ppid: 1 }],
|
|
95
|
-
alive: () => false,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
stop();
|
|
99
|
-
expect(fired).toBe(false);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('fires when an ancestor above the immediate parent dies', async () => {
|
|
103
|
-
const dead = new Set<number>();
|
|
104
|
-
let reported: number | null = null;
|
|
105
|
-
const stop = startOrphanWatch((pid) => (reported = pid), {
|
|
106
|
-
intervalMs: 1,
|
|
107
|
-
procTable: () => [
|
|
108
|
-
{ pid: process.pid, ppid: 100 },
|
|
109
|
-
{ pid: 100, ppid: 400 }, // wrapper, stays alive
|
|
110
|
-
{ pid: 400, ppid: 1 }, // the launching session
|
|
111
|
-
],
|
|
112
|
-
alive: (pid) => !dead.has(pid),
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
await new Promise((r) => setTimeout(r, 20));
|
|
116
|
-
expect(reported).toBeNull(); // chain intact — nothing to do
|
|
117
|
-
|
|
118
|
-
dead.add(400); // the terminal / agent session exits
|
|
119
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
120
|
-
stop();
|
|
121
|
-
|
|
122
|
-
expect(reported).toBe(400);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe('killSubtree', () => {
|
|
127
|
-
it('leaves the root alone and reports nothing when there is no tree', () => {
|
|
128
|
-
const killed = killSubtree(200, {
|
|
129
|
-
procTable: () => [{ pid: 200, ppid: 1 }],
|
|
130
|
-
sleep: () => undefined,
|
|
131
|
-
});
|
|
132
|
-
expect(killed).toEqual([]);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('kills a real grandchild, which is what the old shutdown missed', async () => {
|
|
136
|
-
// `sh -c 'sleep 30'` reproduces the shape that leaked: `sleep` is a
|
|
137
|
-
// grandchild, exactly like esbuild under tsx or tsc under its `sh -c`
|
|
138
|
-
// wrapper. Signalling only the direct child left those running.
|
|
139
|
-
// The trailing `; true` matters: with a single command `sh` execs it and
|
|
140
|
-
// there is no grandchild to find.
|
|
141
|
-
const child = spawn('sh', ['-c', 'sleep 30; true'], { stdio: 'ignore' });
|
|
142
|
-
const shPid = child.pid as number;
|
|
143
|
-
await new Promise((r) => setTimeout(r, 400));
|
|
144
|
-
|
|
145
|
-
const [sleepPid] = descendantsOf(shPid, readProcTable());
|
|
146
|
-
expect(sleepPid).toBeTypeOf('number');
|
|
147
|
-
|
|
148
|
-
killSubtree(shPid, { graceMs: 500 });
|
|
149
|
-
|
|
150
|
-
expect(isAlive(sleepPid as number)).toBe(false);
|
|
151
|
-
expect(isAlive(shPid)).toBe(true); // the root is never signalled
|
|
152
|
-
child.kill('SIGKILL');
|
|
153
|
-
}, 20_000);
|
|
154
|
-
});
|
package/src/cli/devReap.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Making `ugly-app dev` take its whole process tree down with it.
|
|
3
|
-
*
|
|
4
|
-
* A dev stack is `ugly-app dev` -> tsx watch -> esbuild, plus `sh -c tsc
|
|
5
|
-
* --watch` -> tsc, plus minio. None of those exit on their own: tsx watch
|
|
6
|
-
* deliberately survives a crashed server, tsc --watch runs forever, minio
|
|
7
|
-
* serves forever. So the stack only ever goes away because something kills it,
|
|
8
|
-
* and two things kept stopping that from happening:
|
|
9
|
-
*
|
|
10
|
-
* 1. The self-reap watchdog in dev.ts fired on `process.ppid === 1`. When
|
|
11
|
-
* `ugly-app dev` is launched through a wrapper — `npx ugly-app dev`, which
|
|
12
|
-
* leaves an `npm exec` process in between — our parent is that wrapper,
|
|
13
|
-
* which never exits, so our ppid never becomes 1 no matter how long ago
|
|
14
|
-
* the terminal that started it went away. `ancestorsOf` + a liveness poll
|
|
15
|
-
* watches the whole chain instead of one link of it. (2026-08-03: a
|
|
16
|
-
* ugly-bot stack and its minio had been up 2 days this way.)
|
|
17
|
-
*
|
|
18
|
-
* 2. Shutdown sent SIGTERM to concurrently's direct children only. tsx and
|
|
19
|
-
* tsc routinely survive SIGTERM (the machine-level reaper's log is full of
|
|
20
|
-
* "survived TERM"), and esbuild/minio are grandchildren nothing signalled
|
|
21
|
-
* at all. `killSubtree` walks the real process table and escalates.
|
|
22
|
-
*
|
|
23
|
-
* Everything here is sync and dependency-free so it can also run from a
|
|
24
|
-
* `process.on('exit')` handler, where async work never completes.
|
|
25
|
-
*/
|
|
26
|
-
import { execSync } from 'child_process';
|
|
27
|
-
|
|
28
|
-
export interface ProcRow {
|
|
29
|
-
pid: number;
|
|
30
|
-
ppid: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Parse `ps -Ao pid=,ppid=` output. Malformed lines are skipped, not thrown. */
|
|
34
|
-
export function parsePsTable(out: string): ProcRow[] {
|
|
35
|
-
const rows: ProcRow[] = [];
|
|
36
|
-
for (const line of out.split('\n')) {
|
|
37
|
-
const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
|
|
38
|
-
if (!m) continue;
|
|
39
|
-
rows.push({ pid: Number(m[1]), ppid: Number(m[2]) });
|
|
40
|
-
}
|
|
41
|
-
return rows;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function readProcTable(): ProcRow[] {
|
|
45
|
-
try {
|
|
46
|
-
return parsePsTable(
|
|
47
|
-
execSync('ps -Ao pid=,ppid=', {
|
|
48
|
-
encoding: 'utf-8',
|
|
49
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
50
|
-
}),
|
|
51
|
-
);
|
|
52
|
-
} catch {
|
|
53
|
-
// No `ps` (or it failed): callers degrade to "no descendants / no
|
|
54
|
-
// ancestors", which is always safer than guessing.
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Descendants of `root`, deepest first, excluding `root` itself. Killing in
|
|
61
|
-
* this order stops a supervisor (tsx watch) from respawning a child we already
|
|
62
|
-
* killed. Cycle-safe: a pid is only ever visited once, so a recycled-pid loop
|
|
63
|
-
* in the table cannot hang the caller.
|
|
64
|
-
*/
|
|
65
|
-
export function descendantsOf(root: number, rows: ProcRow[]): number[] {
|
|
66
|
-
const byParent = new Map<number, number[]>();
|
|
67
|
-
for (const r of rows) {
|
|
68
|
-
const kids = byParent.get(r.ppid);
|
|
69
|
-
if (kids) kids.push(r.pid);
|
|
70
|
-
else byParent.set(r.ppid, [r.pid]);
|
|
71
|
-
}
|
|
72
|
-
const seen = new Set<number>([root]);
|
|
73
|
-
const ordered: number[] = [];
|
|
74
|
-
const walk = (pid: number): void => {
|
|
75
|
-
for (const kid of byParent.get(pid) ?? []) {
|
|
76
|
-
if (seen.has(kid)) continue;
|
|
77
|
-
seen.add(kid);
|
|
78
|
-
walk(kid); // depth first, so children land before their parent
|
|
79
|
-
ordered.push(kid);
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
walk(root);
|
|
83
|
-
return ordered;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* The ancestor chain of `pid`, nearest first, stopping before pid 1. Empty when
|
|
88
|
-
* the process is already an orphan — which is exactly when an orphan watch must
|
|
89
|
-
* not arm itself.
|
|
90
|
-
*/
|
|
91
|
-
export function ancestorsOf(pid: number, rows: ProcRow[]): number[] {
|
|
92
|
-
const parentOf = new Map(rows.map((r) => [r.pid, r.ppid]));
|
|
93
|
-
const chain: number[] = [];
|
|
94
|
-
const seen = new Set<number>([pid]);
|
|
95
|
-
let cur = parentOf.get(pid);
|
|
96
|
-
while (cur !== undefined && cur > 1 && !seen.has(cur)) {
|
|
97
|
-
chain.push(cur);
|
|
98
|
-
seen.add(cur);
|
|
99
|
-
cur = parentOf.get(cur);
|
|
100
|
-
}
|
|
101
|
-
return chain;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Signal 0 tests for existence without delivering anything. */
|
|
105
|
-
export function isAlive(pid: number): boolean {
|
|
106
|
-
try {
|
|
107
|
-
process.kill(pid, 0);
|
|
108
|
-
return true;
|
|
109
|
-
} catch (err) {
|
|
110
|
-
// EPERM means it exists but belongs to someone else — still alive.
|
|
111
|
-
return (err as NodeJS.ErrnoException).code === 'EPERM';
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Block the thread without a subprocess, so this is usable from `exit`. */
|
|
116
|
-
function sleepSync(ms: number): void {
|
|
117
|
-
const shared = new Int32Array(new SharedArrayBuffer(4));
|
|
118
|
-
Atomics.wait(shared, 0, 0, ms);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export interface KillSubtreeOptions {
|
|
122
|
-
/** How long to let SIGTERM work before escalating. */
|
|
123
|
-
graceMs?: number;
|
|
124
|
-
/** Injected in tests. */
|
|
125
|
-
procTable?: () => ProcRow[];
|
|
126
|
-
sleep?: (ms: number) => void;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Kill every descendant of `root` — never `root` itself, so a caller can use
|
|
131
|
-
* this on its own pid without racing its own exit. SIGTERM first (tsx and minio
|
|
132
|
-
* flush and exit cleanly given the chance), SIGKILL for whatever ignored it.
|
|
133
|
-
*
|
|
134
|
-
* Returns the pids that had to be SIGKILLed, which is worth logging: it is the
|
|
135
|
-
* signal that a child is not shutting down properly.
|
|
136
|
-
*/
|
|
137
|
-
export function killSubtree(
|
|
138
|
-
root: number,
|
|
139
|
-
opts: KillSubtreeOptions = {},
|
|
140
|
-
): number[] {
|
|
141
|
-
const { graceMs = 2000, procTable = readProcTable, sleep = sleepSync } = opts;
|
|
142
|
-
|
|
143
|
-
const targets = descendantsOf(root, procTable());
|
|
144
|
-
if (targets.length === 0) return [];
|
|
145
|
-
|
|
146
|
-
for (const pid of targets) {
|
|
147
|
-
try {
|
|
148
|
-
process.kill(pid, 'SIGTERM');
|
|
149
|
-
} catch {
|
|
150
|
-
/* already gone between listing and signalling */
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
sleep(graceMs);
|
|
155
|
-
|
|
156
|
-
const survivors = targets.filter(isAlive);
|
|
157
|
-
for (const pid of survivors) {
|
|
158
|
-
try {
|
|
159
|
-
process.kill(pid, 'SIGKILL');
|
|
160
|
-
} catch {
|
|
161
|
-
/* raced us to the exit */
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return survivors;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export interface OrphanWatchOptions {
|
|
168
|
-
/** Poll period. */
|
|
169
|
-
intervalMs?: number;
|
|
170
|
-
procTable?: () => ProcRow[];
|
|
171
|
-
alive?: (pid: number) => boolean;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Call `onOrphan` once the chain that launched us breaks — i.e. any ancestor
|
|
176
|
-
* recorded at startup has died. Watching the *whole* chain (not just ppid) is
|
|
177
|
-
* what catches wrapper processes: `npm exec` outlives the terminal that ran it,
|
|
178
|
-
* so our own ppid stays valid forever while the session it belonged to is long
|
|
179
|
-
* gone.
|
|
180
|
-
*
|
|
181
|
-
* Returns a stop function. No-ops (returning a no-op stopper) when we started
|
|
182
|
-
* with no ancestors, so a deliberately detached dev server is never reaped by
|
|
183
|
-
* its own watchdog on the first tick.
|
|
184
|
-
*/
|
|
185
|
-
export function startOrphanWatch(
|
|
186
|
-
onOrphan: (deadAncestor: number) => void,
|
|
187
|
-
opts: OrphanWatchOptions = {},
|
|
188
|
-
): () => void {
|
|
189
|
-
const {
|
|
190
|
-
intervalMs = 3000,
|
|
191
|
-
procTable = readProcTable,
|
|
192
|
-
alive = isAlive,
|
|
193
|
-
} = opts;
|
|
194
|
-
|
|
195
|
-
const chain = ancestorsOf(process.pid, procTable());
|
|
196
|
-
if (chain.length === 0) return () => undefined;
|
|
197
|
-
|
|
198
|
-
const timer = setInterval(() => {
|
|
199
|
-
const dead = chain.find((pid) => !alive(pid));
|
|
200
|
-
if (dead === undefined) return;
|
|
201
|
-
clearInterval(timer);
|
|
202
|
-
onOrphan(dead);
|
|
203
|
-
}, intervalMs);
|
|
204
|
-
// Never hold the event loop open on account of the watchdog.
|
|
205
|
-
timer.unref();
|
|
206
|
-
return () => clearInterval(timer);
|
|
207
|
-
}
|