@f5-sales-demo/pi-utils 19.51.2
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/package.json +61 -0
- package/src/abortable.ts +86 -0
- package/src/async.ts +50 -0
- package/src/cli.ts +432 -0
- package/src/color.ts +204 -0
- package/src/dirs.ts +480 -0
- package/src/env.ts +108 -0
- package/src/format.ts +106 -0
- package/src/frontmatter.ts +118 -0
- package/src/fs-error.ts +56 -0
- package/src/glob.ts +189 -0
- package/src/hook-fetch.ts +30 -0
- package/src/i18n.test.ts +213 -0
- package/src/i18n.ts +90 -0
- package/src/index.ts +63 -0
- package/src/json.ts +10 -0
- package/src/logger.ts +204 -0
- package/src/mermaid-ascii.ts +31 -0
- package/src/mermaid-color.ts +231 -0
- package/src/mime.ts +159 -0
- package/src/models-yml.ts +162 -0
- package/src/peek-file.ts +114 -0
- package/src/postmortem.ts +197 -0
- package/src/procmgr.ts +326 -0
- package/src/prompt.ts +412 -0
- package/src/ptree.ts +426 -0
- package/src/ring.ts +169 -0
- package/src/snowflake.ts +136 -0
- package/src/stream.ts +394 -0
- package/src/tab-spacing.ts +312 -0
- package/src/temp.ts +77 -0
- package/src/type-guards.ts +11 -0
- package/src/which.ts +232 -0
- package/src/xcsh-context-paths.ts +23 -0
- package/src/xcsh-context-resolver.ts +333 -0
- package/src/xcsh-env-names.ts +69 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleanup and postmortem handler utilities.
|
|
3
|
+
*
|
|
4
|
+
* This module provides a system for registering and running cleanup callbacks
|
|
5
|
+
* in response to process exit, signals, or fatal exceptions. It is intended to
|
|
6
|
+
* allow reliably releasing resources or shutting down subprocesses, files, sockets, etc.
|
|
7
|
+
*/
|
|
8
|
+
import inspector from "node:inspector";
|
|
9
|
+
import { isMainThread } from "node:worker_threads";
|
|
10
|
+
import { logger } from ".";
|
|
11
|
+
|
|
12
|
+
// Cleanup reasons, in order of priority/meaning.
|
|
13
|
+
export enum Reason {
|
|
14
|
+
PRE_EXIT = "pre_exit", // Pre-exit phase (not used by default)
|
|
15
|
+
EXIT = "exit", // Normal process exit
|
|
16
|
+
SIGINT = "sigint", // Ctrl-C or SIGINT
|
|
17
|
+
SIGTERM = "sigterm", // SIGTERM
|
|
18
|
+
SIGHUP = "sighup", // SIGHUP
|
|
19
|
+
UNCAUGHT_EXCEPTION = "uncaught_exception", // Fatal exception
|
|
20
|
+
UNHANDLED_REJECTION = "unhandled_rejection", // Unhandled promise rejection
|
|
21
|
+
MANUAL = "manual", // Manual cleanup (not triggered by process)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Internal list of active cleanup callbacks (in registration order)
|
|
25
|
+
const callbackList: ((reason: Reason) => Promise<void> | void)[] = [];
|
|
26
|
+
// Tracks cleanup run state (to prevent recursion/reentry issues)
|
|
27
|
+
let cleanupStage: "idle" | "running" | "complete" = "idle";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Internal: runs all registered cleanup callbacks for the given reason.
|
|
31
|
+
* Ensures each callback is invoked at most once. Handles errors and prevents reentrancy.
|
|
32
|
+
*
|
|
33
|
+
* Returns a Promise that settles after all cleanups complete or error out.
|
|
34
|
+
*/
|
|
35
|
+
function runCleanup(reason: Reason): Promise<void> {
|
|
36
|
+
switch (cleanupStage) {
|
|
37
|
+
case "idle":
|
|
38
|
+
cleanupStage = "running";
|
|
39
|
+
break;
|
|
40
|
+
case "running":
|
|
41
|
+
logger.error("Cleanup invoked recursively", { stack: new Error().stack });
|
|
42
|
+
return Promise.resolve();
|
|
43
|
+
case "complete":
|
|
44
|
+
return Promise.resolve();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Call .cleanup() for each callback that is still "armed".
|
|
48
|
+
// Use Promise.try to handle sync/async, but only those armed.
|
|
49
|
+
// Create a copy to avoid mutating the original array with reverse()
|
|
50
|
+
const promises = [...callbackList].reverse().map(callback => {
|
|
51
|
+
return Promise.try(() => callback(reason));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return Promise.allSettled(promises).then(results => {
|
|
55
|
+
for (const result of results) {
|
|
56
|
+
if (result.status === "rejected") {
|
|
57
|
+
const err = result.reason instanceof Error ? result.reason : new Error(String(result.reason));
|
|
58
|
+
logger.error("Cleanup callback failed", { err, stack: err.stack });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
cleanupStage = "complete";
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Register signal and error event handlers to trigger cleanup before exit.
|
|
66
|
+
// Main thread: full signal handling (SIGINT, SIGTERM, SIGHUP) + exceptions + exit
|
|
67
|
+
// Worker thread: exit only (workers use self.addEventListener for exceptions)
|
|
68
|
+
let inspectorOpened = false;
|
|
69
|
+
|
|
70
|
+
function formatFatalError(label: string, err: Error): string {
|
|
71
|
+
const name = err.name || "Error";
|
|
72
|
+
const message = err.message || "(no message)";
|
|
73
|
+
const stack = err.stack || "";
|
|
74
|
+
const stackLines = stack.split("\n").slice(1);
|
|
75
|
+
const formattedStack = stackLines.length > 0 ? `\n${stackLines.join("\n")}` : "";
|
|
76
|
+
return `\n[${label}] ${name}: ${message}${formattedStack}\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isMainThread) {
|
|
80
|
+
process
|
|
81
|
+
.on("SIGINT", async () => {
|
|
82
|
+
await runCleanup(Reason.SIGINT);
|
|
83
|
+
process.exit(130); // 128 + SIGINT (2)
|
|
84
|
+
})
|
|
85
|
+
.on("SIGUSR1", () => {
|
|
86
|
+
if (inspectorOpened) return;
|
|
87
|
+
inspectorOpened = true;
|
|
88
|
+
inspector.open(undefined, undefined, false);
|
|
89
|
+
const url = inspector.url();
|
|
90
|
+
process.stderr.write(`Inspector opened: ${url}\n`);
|
|
91
|
+
})
|
|
92
|
+
.on("uncaughtException", async err => {
|
|
93
|
+
process.stderr.write(formatFatalError("Uncaught Exception", err));
|
|
94
|
+
logger.error("Uncaught exception", { err, stack: err.stack });
|
|
95
|
+
await runCleanup(Reason.UNCAUGHT_EXCEPTION);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
})
|
|
98
|
+
.on("unhandledRejection", async reason => {
|
|
99
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
100
|
+
process.stderr.write(formatFatalError("Unhandled Rejection", err));
|
|
101
|
+
logger.error("Unhandled rejection", { err, stack: err.stack });
|
|
102
|
+
await runCleanup(Reason.UNHANDLED_REJECTION);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
})
|
|
105
|
+
.on("exit", async () => {
|
|
106
|
+
void runCleanup(Reason.EXIT); // fire and forget (exit imminent)
|
|
107
|
+
})
|
|
108
|
+
.on("SIGTERM", async () => {
|
|
109
|
+
await runCleanup(Reason.SIGTERM);
|
|
110
|
+
process.exit(143); // 128 + SIGTERM (15)
|
|
111
|
+
})
|
|
112
|
+
.on("SIGHUP", async () => {
|
|
113
|
+
await runCleanup(Reason.SIGHUP);
|
|
114
|
+
process.exit(129); // 128 + SIGHUP (1)
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
// Worker thread: only register exit handler for cleanup.
|
|
118
|
+
// DO NOT register uncaughtException/unhandledRejection handlers here -
|
|
119
|
+
// they would swallow errors before the worker's own handlers (self.addEventListener)
|
|
120
|
+
// can report failures back to the parent thread.
|
|
121
|
+
process.on("exit", () => {
|
|
122
|
+
void runCleanup(Reason.EXIT);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Register a process cleanup callback, to be run on shutdown, signal, or fatal error.
|
|
128
|
+
*
|
|
129
|
+
* Returns a Callback instance that can be used to cancel (unregister) or manually clean up.
|
|
130
|
+
* If register is called after cleanup already began, invokes callback on a microtask.
|
|
131
|
+
*/
|
|
132
|
+
export function register(id: string, callback: (reason: Reason) => void | Promise<void>): () => void {
|
|
133
|
+
let done = false;
|
|
134
|
+
const exec = (reason: Reason) => {
|
|
135
|
+
if (done) return;
|
|
136
|
+
done = true;
|
|
137
|
+
try {
|
|
138
|
+
return callback(reason);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
141
|
+
logger.error("Cleanup callback failed", { err, id, stack: err.stack });
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const cancel = () => {
|
|
146
|
+
const index = callbackList.indexOf(exec);
|
|
147
|
+
if (index >= 0) {
|
|
148
|
+
callbackList.splice(index, 1);
|
|
149
|
+
}
|
|
150
|
+
done = true;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (cleanupStage !== "idle") {
|
|
154
|
+
// If cleanup is already running/completed, warn and run on microtask.
|
|
155
|
+
logger.warn("Cleanup invoked recursively", { id });
|
|
156
|
+
try {
|
|
157
|
+
callback(Reason.MANUAL);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
160
|
+
logger.error("Cleanup callback failed", { err, id, stack: err.stack });
|
|
161
|
+
}
|
|
162
|
+
return () => {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Register callback as "armed" (active).
|
|
166
|
+
callbackList.push(exec);
|
|
167
|
+
return cancel;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Runs all cleanup callbacks without exiting.
|
|
172
|
+
* Use this in workers or when you need to clean up but continue execution.
|
|
173
|
+
*/
|
|
174
|
+
export function cleanup(): Promise<void> {
|
|
175
|
+
return runCleanup(Reason.MANUAL);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Runs all cleanup callbacks and exits.
|
|
180
|
+
*
|
|
181
|
+
* In main thread: waits for stdout drain, then calls process.exit().
|
|
182
|
+
* In workers: runs cleanup only (process.exit would kill entire process).
|
|
183
|
+
*/
|
|
184
|
+
export async function quit(code: number = 0): Promise<void> {
|
|
185
|
+
await runCleanup(Reason.MANUAL);
|
|
186
|
+
|
|
187
|
+
if (!isMainThread) {
|
|
188
|
+
return; // Workers: cleanup done, let worker exit naturally
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (process.stdout.writableLength > 0) {
|
|
192
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
193
|
+
process.stdout.once("drain", resolve);
|
|
194
|
+
await Promise.race([promise, Bun.sleep(5000)]);
|
|
195
|
+
}
|
|
196
|
+
process.exit(code);
|
|
197
|
+
}
|
package/src/procmgr.ts
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import * as timers from "node:timers";
|
|
4
|
+
import type { Subprocess } from "bun";
|
|
5
|
+
import { $env } from "./env";
|
|
6
|
+
import { $which } from "./which";
|
|
7
|
+
|
|
8
|
+
export interface ShellConfig {
|
|
9
|
+
shell: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
env: Record<string, string>;
|
|
12
|
+
prefix: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let cachedShellConfig: ShellConfig | null = null;
|
|
16
|
+
|
|
17
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
18
|
+
const TERM_SIGNAL = IS_WINDOWS ? undefined : "SIGTERM";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a shell binary is executable.
|
|
22
|
+
*/
|
|
23
|
+
function isExecutable(path: string): boolean {
|
|
24
|
+
try {
|
|
25
|
+
fs.accessSync(path, fs.constants.X_OK);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build the spawn environment (cached).
|
|
34
|
+
*/
|
|
35
|
+
function buildSpawnEnv(shell: string): Record<string, string> {
|
|
36
|
+
const noCI = $env.PI_BASH_NO_CI || $env.CLAUDE_BASH_NO_CI;
|
|
37
|
+
return {
|
|
38
|
+
...Bun.env,
|
|
39
|
+
SHELL: shell,
|
|
40
|
+
GIT_EDITOR: "true",
|
|
41
|
+
GPG_TTY: "not a tty",
|
|
42
|
+
OMPCODE: "1",
|
|
43
|
+
CLAUDECODE: "1",
|
|
44
|
+
...(noCI ? {} : { CI: "true" }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get shell args, optionally including login shell flag.
|
|
50
|
+
* Supports PI_BASH_NO_LOGIN and CLAUDE_BASH_NO_LOGIN to skip -l.
|
|
51
|
+
*/
|
|
52
|
+
function getShellArgs(): string[] {
|
|
53
|
+
const noLogin = $env.PI_BASH_NO_LOGIN || $env.CLAUDE_BASH_NO_LOGIN;
|
|
54
|
+
return noLogin ? ["-c"] : ["-l", "-c"];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get shell prefix for wrapping commands (profilers, strace, etc.).
|
|
59
|
+
*/
|
|
60
|
+
function getShellPrefix(): string | undefined {
|
|
61
|
+
return $env.PI_SHELL_PREFIX || $env.CLAUDE_CODE_SHELL_PREFIX;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build full shell config from a shell path.
|
|
66
|
+
*/
|
|
67
|
+
function buildConfig(shell: string): ShellConfig {
|
|
68
|
+
return {
|
|
69
|
+
shell,
|
|
70
|
+
args: getShellArgs(),
|
|
71
|
+
env: buildSpawnEnv(shell),
|
|
72
|
+
prefix: getShellPrefix(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a basic shell (bash or sh) as fallback.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveBasicShell(): string | undefined {
|
|
80
|
+
for (const name of ["bash", "bash.exe", "sh", "sh.exe"]) {
|
|
81
|
+
const resolved = $which(name);
|
|
82
|
+
if (resolved) return resolved;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (process.platform !== "win32") {
|
|
86
|
+
const searchPaths = ["/bin", "/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"];
|
|
87
|
+
const candidates = ["bash", "sh"];
|
|
88
|
+
|
|
89
|
+
for (const name of candidates) {
|
|
90
|
+
for (const dir of searchPaths) {
|
|
91
|
+
const fullPath = path.join(dir, name);
|
|
92
|
+
if (fs.existsSync(fullPath)) return fullPath;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get shell configuration based on platform.
|
|
102
|
+
* Resolution order:
|
|
103
|
+
* 1. User-specified shellPath in settings.json
|
|
104
|
+
* 2. On Windows: Git Bash in known locations, then bash on PATH
|
|
105
|
+
* 3. On Unix: $SHELL if bash/zsh, then fallback paths
|
|
106
|
+
* 4. Fallback: sh
|
|
107
|
+
*/
|
|
108
|
+
export function getShellConfig(customShellPath?: string): ShellConfig {
|
|
109
|
+
if (cachedShellConfig) {
|
|
110
|
+
return cachedShellConfig;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 1. Check user-specified shell path
|
|
114
|
+
if (customShellPath) {
|
|
115
|
+
if (fs.existsSync(customShellPath)) {
|
|
116
|
+
cachedShellConfig = buildConfig(customShellPath);
|
|
117
|
+
return cachedShellConfig;
|
|
118
|
+
}
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Custom shell path not found: ${customShellPath}\nPlease update shellPath in ~/.xcsh/agent/settings.json`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (process.platform === "win32") {
|
|
125
|
+
// 2. Try Git Bash in known locations
|
|
126
|
+
const paths: string[] = [];
|
|
127
|
+
const programFiles = Bun.env.ProgramFiles;
|
|
128
|
+
if (programFiles) {
|
|
129
|
+
paths.push(`${programFiles}\\Git\\bin\\bash.exe`);
|
|
130
|
+
}
|
|
131
|
+
const programFilesX86 = Bun.env["ProgramFiles(x86)"];
|
|
132
|
+
if (programFilesX86) {
|
|
133
|
+
paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const path of paths) {
|
|
137
|
+
if (fs.existsSync(path)) {
|
|
138
|
+
cachedShellConfig = buildConfig(path);
|
|
139
|
+
return cachedShellConfig;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)
|
|
144
|
+
const bashOnPath = $which("bash.exe");
|
|
145
|
+
if (bashOnPath) {
|
|
146
|
+
cachedShellConfig = buildConfig(bashOnPath);
|
|
147
|
+
return cachedShellConfig;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
throw new Error(
|
|
151
|
+
`No bash shell found. Options:\n` +
|
|
152
|
+
` 1. Install Git for Windows: https://git-scm.com/download/win\n` +
|
|
153
|
+
` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n` +
|
|
154
|
+
` 3. Set shellPath in ~/.xcsh/agent/settings.json\n\n` +
|
|
155
|
+
`Searched Git Bash in:\n${paths.map(p => ` ${p}`).join("\n")}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Unix: prefer user's shell from $SHELL if it's bash/zsh and executable
|
|
160
|
+
const userShell = Bun.env.SHELL;
|
|
161
|
+
const isValidShell = userShell && (userShell.includes("bash") || userShell.includes("zsh"));
|
|
162
|
+
if (isValidShell && isExecutable(userShell)) {
|
|
163
|
+
cachedShellConfig = buildConfig(userShell);
|
|
164
|
+
return cachedShellConfig;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 4. Fallback: use basic shell
|
|
168
|
+
const basicShell = resolveBasicShell();
|
|
169
|
+
if (basicShell) {
|
|
170
|
+
cachedShellConfig = buildConfig(basicShell);
|
|
171
|
+
return cachedShellConfig;
|
|
172
|
+
}
|
|
173
|
+
cachedShellConfig = buildConfig("sh");
|
|
174
|
+
return cachedShellConfig;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Function signature for native process tree killing.
|
|
179
|
+
* Returns the number of processes killed.
|
|
180
|
+
*/
|
|
181
|
+
export type KillTreeFn = (pid: number, signal: number) => number;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Global native kill tree function, injected by pi-natives when loaded.
|
|
185
|
+
* Falls back to platform-specific behavior if not set.
|
|
186
|
+
*/
|
|
187
|
+
export let nativeKillTree: KillTreeFn | undefined;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Set the native kill tree function. Called by pi-natives on load.
|
|
191
|
+
*/
|
|
192
|
+
export function setNativeKillTree(fn: KillTreeFn): void {
|
|
193
|
+
nativeKillTree = fn;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Options for terminating a process and all its descendants.
|
|
198
|
+
*/
|
|
199
|
+
export interface TerminateOptions {
|
|
200
|
+
/** The process to terminate */
|
|
201
|
+
target: Subprocess | number;
|
|
202
|
+
/** Whether to terminate the process tree (all descendants) */
|
|
203
|
+
group?: boolean;
|
|
204
|
+
/** Timeout in milliseconds */
|
|
205
|
+
timeout?: number;
|
|
206
|
+
/** Abort signal */
|
|
207
|
+
signal?: AbortSignal;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Check if a process is running.
|
|
212
|
+
*/
|
|
213
|
+
export function isPidRunning(pid: number | Subprocess): boolean {
|
|
214
|
+
try {
|
|
215
|
+
if (typeof pid === "number") {
|
|
216
|
+
process.kill(pid, 0);
|
|
217
|
+
} else {
|
|
218
|
+
if (pid.killed) return false;
|
|
219
|
+
if (pid.exitCode !== null) return false;
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function joinSignals(...sigs: (AbortSignal | null | undefined)[]): AbortSignal | undefined {
|
|
228
|
+
const nn = sigs.filter(Boolean) as AbortSignal[];
|
|
229
|
+
if (nn.length === 0) return undefined;
|
|
230
|
+
if (nn.length === 1) return nn[0];
|
|
231
|
+
return AbortSignal.any(nn);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function onProcessExit(proc: Subprocess | number, abortSignal?: AbortSignal): Promise<boolean> {
|
|
235
|
+
if (typeof proc !== "number") {
|
|
236
|
+
return proc.exited.then(
|
|
237
|
+
() => true,
|
|
238
|
+
() => true,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!isPidRunning(proc)) {
|
|
243
|
+
return Promise.resolve(true);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const { promise, resolve, reject } = Promise.withResolvers<boolean>();
|
|
247
|
+
const localAbortController = new AbortController();
|
|
248
|
+
|
|
249
|
+
const timer = timers.promises.setInterval(300, null, {
|
|
250
|
+
signal: joinSignals(abortSignal, localAbortController.signal),
|
|
251
|
+
});
|
|
252
|
+
void (async () => {
|
|
253
|
+
try {
|
|
254
|
+
for await (const _ of timer) {
|
|
255
|
+
if (!isPidRunning(proc)) {
|
|
256
|
+
resolve(true);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return reject(error);
|
|
262
|
+
} finally {
|
|
263
|
+
localAbortController.abort();
|
|
264
|
+
}
|
|
265
|
+
resolve(false);
|
|
266
|
+
})();
|
|
267
|
+
|
|
268
|
+
return promise;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Terminate a process and all its descendants.
|
|
273
|
+
*/
|
|
274
|
+
export async function terminate(options: TerminateOptions): Promise<boolean> {
|
|
275
|
+
const { target, group = false, timeout = 5000, signal } = options;
|
|
276
|
+
|
|
277
|
+
const abortController = new AbortController();
|
|
278
|
+
try {
|
|
279
|
+
const abortSignal = joinSignals(signal, abortController.signal);
|
|
280
|
+
|
|
281
|
+
// Determine PID
|
|
282
|
+
let pid: number | undefined;
|
|
283
|
+
const exitPromise = onProcessExit(target, abortSignal);
|
|
284
|
+
if (typeof target === "number") {
|
|
285
|
+
pid = target;
|
|
286
|
+
} else {
|
|
287
|
+
pid = target.pid;
|
|
288
|
+
if (target.killed) return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Give it a moment to exit gracefully first.
|
|
292
|
+
try {
|
|
293
|
+
if (typeof target === "number") {
|
|
294
|
+
process.kill(target, TERM_SIGNAL);
|
|
295
|
+
} else {
|
|
296
|
+
target.kill(TERM_SIGNAL);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (exitPromise) {
|
|
300
|
+
const exited = await Promise.race([Bun.sleep(1000).then(() => false), exitPromise]);
|
|
301
|
+
if (exited) return true;
|
|
302
|
+
}
|
|
303
|
+
} catch {}
|
|
304
|
+
|
|
305
|
+
if (nativeKillTree) {
|
|
306
|
+
nativeKillTree(pid, 9);
|
|
307
|
+
} else {
|
|
308
|
+
if (group && !IS_WINDOWS) {
|
|
309
|
+
try {
|
|
310
|
+
process.kill(-pid, "SIGKILL");
|
|
311
|
+
} catch {}
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
if (typeof target === "number") {
|
|
315
|
+
process.kill(target, "SIGKILL");
|
|
316
|
+
} else {
|
|
317
|
+
target.kill("SIGKILL");
|
|
318
|
+
}
|
|
319
|
+
} catch {}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return await Promise.race([Bun.sleep(timeout).then(() => false), exitPromise]);
|
|
323
|
+
} finally {
|
|
324
|
+
abortController.abort();
|
|
325
|
+
}
|
|
326
|
+
}
|