@ix-xs/node-comfort 1.1.31 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +190 -0
- package/LICENSE +21 -0
- package/README.md +210 -748
- package/index.js +84 -13
- package/package.json +201 -21
- package/src/Arr.js +901 -0
- package/src/Async.js +585 -0
- package/src/Cache.js +329 -0
- package/src/Checker.js +979 -188
- package/src/Cli.js +1034 -0
- package/src/Color.js +415 -0
- package/src/Crypto.js +707 -0
- package/src/Emitter.js +289 -0
- package/src/Env.js +555 -0
- package/src/FS.js +1483 -768
- package/src/Func.js +692 -0
- package/src/Http.js +538 -0
- package/src/Id.js +418 -0
- package/src/Logger.js +739 -307
- package/src/Num.js +849 -0
- package/src/Obj.js +904 -0
- package/src/SQLite.js +1308 -994
- package/src/Schema.js +1586 -0
- package/src/Str.js +999 -0
- package/src/Sys.js +511 -0
- package/src/Time.js +1323 -0
- package/src/Utils.js +321 -325
- package/src/errors.js +292 -0
- package/types/index.d.ts +693 -120
- package/types/src/Arr.d.ts +562 -0
- package/types/src/Arr.d.ts.map +1 -0
- package/types/src/Async.d.ts +321 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +195 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +609 -81
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +582 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +294 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +534 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +170 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +382 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +709 -201
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +455 -0
- package/types/src/Func.d.ts.map +1 -0
- package/types/src/Http.d.ts +374 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +200 -0
- package/types/src/Id.d.ts.map +1 -0
- package/types/src/Logger.d.ts +374 -24
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +578 -0
- package/types/src/Num.d.ts.map +1 -0
- package/types/src/Obj.d.ts +475 -0
- package/types/src/Obj.d.ts.map +1 -0
- package/types/src/SQLite.d.ts +647 -305
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +940 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +696 -0
- package/types/src/Str.d.ts.map +1 -0
- package/types/src/Sys.d.ts +269 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +664 -0
- package/types/src/Time.d.ts.map +1 -0
- package/types/src/Utils.d.ts +154 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +292 -0
- package/types/src/errors.d.ts.map +1 -0
- package/tsconfig.json +0 -18
- package/types/index.d.ts.map +0 -1
package/src/Sys.js
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Processes and the system: run commands and get their output, find
|
|
5
|
+
* executables, shut down cleanly, open URLs, and learn about the machine.
|
|
6
|
+
* Behaves the same on Windows, macOS and Linux; on Windows,
|
|
7
|
+
* `run("npm", [...])` finds `npm.cmd` and escapes arguments safely.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const { stdout } = await sys.run("git", ["rev-parse", "HEAD"]);
|
|
11
|
+
* sys.onShutdown(async () => {
|
|
12
|
+
* await server.close();
|
|
13
|
+
* await db.close();
|
|
14
|
+
* });
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { spawn } = require("node:child_process");
|
|
18
|
+
const fs = require("node:fs");
|
|
19
|
+
const os = require("node:os");
|
|
20
|
+
const nodePath = require("node:path");
|
|
21
|
+
const { ProcessError, AbortError } = require("./errors.js");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Options for `run()` and `exec()`.
|
|
25
|
+
* @typedef {object} RunOptions
|
|
26
|
+
* @property {string} [cwd] Where to run the command.
|
|
27
|
+
* @property {Record<string, string | undefined>} [env] Extra environment variables, added to `process.env`.
|
|
28
|
+
* @property {boolean} [extendEnv] Set to `false` to pass only `env`, without `process.env`.
|
|
29
|
+
* @property {string | Buffer} [input] Written to the command's stdin.
|
|
30
|
+
* @property {number | string} [timeout] Kill the command after this long, like `"30s"`.
|
|
31
|
+
* @property {AbortSignal} [signal] Kills the command when aborted.
|
|
32
|
+
* @property {"pipe" | "inherit"} [stdio] `"inherit"` shows the output live instead of capturing it. Defaults to `"pipe"`.
|
|
33
|
+
* @property {(line: string) => void} [onStdout] Called for each line of stdout.
|
|
34
|
+
* @property {(line: string) => void} [onStderr] Called for each line of stderr.
|
|
35
|
+
* @property {boolean} [reject] Throw when the exit code isn't 0. Defaults to `true`.
|
|
36
|
+
* @property {boolean} [trim] Drop the final newline of the output. Defaults to `true`.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A finished command.
|
|
41
|
+
* @typedef {object} RunResult
|
|
42
|
+
* @property {string} command The command line that ran.
|
|
43
|
+
* @property {string} stdout
|
|
44
|
+
* @property {string} stderr
|
|
45
|
+
* @property {number | null} exitCode `null` if a signal killed it.
|
|
46
|
+
* @property {string | null} signal
|
|
47
|
+
* @property {number} duration In milliseconds.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Options for `onShutdown()`. They apply to all handlers.
|
|
52
|
+
* @typedef {object} ShutdownOptions
|
|
53
|
+
* @property {number | string} [timeout] How long the handlers get before the exit is forced. Defaults to 10 seconds.
|
|
54
|
+
* @property {number} [exitCode] Exit code after a clean shutdown. Defaults to `0`.
|
|
55
|
+
* @property {NodeJS.Signals[]} [signals] Signals that trigger it. Defaults to `SIGINT`, `SIGTERM` and `SIGHUP`.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returned by `info()`.
|
|
60
|
+
* @typedef {object} SystemInfo
|
|
61
|
+
* @property {NodeJS.Platform} platform `"win32"`, `"darwin"`, `"linux"`...
|
|
62
|
+
* @property {string} arch `"x64"`, `"arm64"`...
|
|
63
|
+
* @property {string} release OS release.
|
|
64
|
+
* @property {string} node Node.js version.
|
|
65
|
+
* @property {number} cpus Number of logical CPUs.
|
|
66
|
+
* @property {string} cpuModel
|
|
67
|
+
* @property {{ total: number, free: number, used: number }} memory System memory, in bytes.
|
|
68
|
+
* @property {number} uptime In seconds.
|
|
69
|
+
* @property {string} hostname
|
|
70
|
+
* @property {string} user
|
|
71
|
+
* @property {number} pid
|
|
72
|
+
* @property {boolean} ci Running in CI.
|
|
73
|
+
* @property {boolean} docker Running in a container.
|
|
74
|
+
* @property {boolean} wsl Running in WSL.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
const META = /([()\][%!^"`<>&|;, *?])/g;
|
|
78
|
+
|
|
79
|
+
/** @param {string} command */
|
|
80
|
+
const _escapeCommand = (command) => command.replace(META, "^$1");
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Escapes an argument for cmd.exe, like `cross-spawn` does.
|
|
84
|
+
* @param {string} arg
|
|
85
|
+
* @param {boolean} doubleEscape
|
|
86
|
+
*/
|
|
87
|
+
const _escapeArg = (arg, doubleEscape) => {
|
|
88
|
+
let out = String(arg).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1");
|
|
89
|
+
out = `"${out}"`.replace(META, "^$1");
|
|
90
|
+
return doubleEscape ? out.replace(META, "^$1") : out;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Handles Windows `.cmd` and `.bat` files.
|
|
95
|
+
* @param {string} file
|
|
96
|
+
* @param {string[]} args
|
|
97
|
+
* @returns {{ file: string, args: string[], verbatim: boolean }}
|
|
98
|
+
*/
|
|
99
|
+
const _prepare = (file, args) => {
|
|
100
|
+
if (process.platform !== "win32") return { file, args, verbatim: false };
|
|
101
|
+
const resolved = which(file) ?? file;
|
|
102
|
+
if (!/\.(cmd|bat)$/i.test(resolved)) return { file: resolved, args, verbatim: false };
|
|
103
|
+
const shim = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i.test(resolved);
|
|
104
|
+
const line = [_escapeCommand(nodePath.normalize(resolved)), ...args.map((a) => _escapeArg(a, shim))].join(" ");
|
|
105
|
+
return { file: process.env.comspec || "cmd.exe", args: ["/d", "/s", "/c", `"${line}"`], verbatim: true };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} file
|
|
110
|
+
* @param {string[]} args
|
|
111
|
+
* @param {boolean} shell
|
|
112
|
+
* @param {string} display
|
|
113
|
+
* @param {RunOptions} options
|
|
114
|
+
* @returns {Promise<RunResult>}
|
|
115
|
+
*/
|
|
116
|
+
const _spawn = (file, args, shell, display, options) =>
|
|
117
|
+
new Promise((resolve, reject) => {
|
|
118
|
+
const started = Date.now();
|
|
119
|
+
const inherit = options.stdio === "inherit";
|
|
120
|
+
const env = options.extendEnv === false ? options.env : { ...process.env, ...options.env };
|
|
121
|
+
const prepared = shell ? { file, args, verbatim: false } : _prepare(file, args);
|
|
122
|
+
const timeoutMs = options.timeout === undefined ? undefined : typeof options.timeout === "number" ? options.timeout : require("./Time.js").parseDuration(options.timeout) ?? undefined;
|
|
123
|
+
if (options.signal?.aborted) return reject(new AbortError(`Command aborted: ${display}`, { cause: options.signal.reason }));
|
|
124
|
+
|
|
125
|
+
const child = spawn(prepared.file, prepared.args, {
|
|
126
|
+
cwd: options.cwd,
|
|
127
|
+
env: /** @type {NodeJS.ProcessEnv} */ (env),
|
|
128
|
+
shell,
|
|
129
|
+
windowsHide: true,
|
|
130
|
+
windowsVerbatimArguments: prepared.verbatim,
|
|
131
|
+
stdio: [options.input !== undefined ? "pipe" : inherit ? "inherit" : "ignore", inherit ? "inherit" : "pipe", inherit ? "inherit" : "pipe"],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
let stdout = "";
|
|
135
|
+
let stderr = "";
|
|
136
|
+
let timedOut = false;
|
|
137
|
+
/** @type {NodeJS.Timeout | undefined} */
|
|
138
|
+
let killTimer;
|
|
139
|
+
/** @type {NodeJS.Timeout | undefined} */
|
|
140
|
+
let forceTimer;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {import("node:stream").Readable | null} stream
|
|
144
|
+
* @param {(chunk: string) => void} append
|
|
145
|
+
* @param {((line: string) => void) | undefined} onLine
|
|
146
|
+
*/
|
|
147
|
+
const collect = (stream, append, onLine) => {
|
|
148
|
+
if (!stream) return;
|
|
149
|
+
stream.setEncoding("utf8");
|
|
150
|
+
let pending = "";
|
|
151
|
+
stream.on("data", (chunk) => {
|
|
152
|
+
append(chunk);
|
|
153
|
+
if (!onLine) return;
|
|
154
|
+
pending += chunk;
|
|
155
|
+
const lines = pending.split(/\r?\n/);
|
|
156
|
+
pending = lines.pop() ?? "";
|
|
157
|
+
for (const line of lines) onLine(line);
|
|
158
|
+
});
|
|
159
|
+
stream.on("end", () => {
|
|
160
|
+
if (onLine && pending) onLine(pending);
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
collect(child.stdout, (c) => (stdout += c), options.onStdout);
|
|
164
|
+
collect(child.stderr, (c) => (stderr += c), options.onStderr);
|
|
165
|
+
|
|
166
|
+
if (options.input !== undefined && child.stdin) {
|
|
167
|
+
child.stdin.on("error", () => {});
|
|
168
|
+
child.stdin.end(options.input);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const kill = () => {
|
|
172
|
+
child.kill("SIGTERM");
|
|
173
|
+
forceTimer = setTimeout(() => child.kill("SIGKILL"), 2000);
|
|
174
|
+
forceTimer.unref();
|
|
175
|
+
};
|
|
176
|
+
if (timeoutMs !== undefined) {
|
|
177
|
+
killTimer = setTimeout(() => {
|
|
178
|
+
timedOut = true;
|
|
179
|
+
kill();
|
|
180
|
+
}, timeoutMs);
|
|
181
|
+
}
|
|
182
|
+
const onAbort = () => kill();
|
|
183
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
184
|
+
|
|
185
|
+
child.on("error", (error) => {
|
|
186
|
+
clearTimeout(killTimer);
|
|
187
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
188
|
+
reject(new ProcessError(`Failed to start "${display}": ${error.message}`, { command: display, cause: error }));
|
|
189
|
+
});
|
|
190
|
+
child.on("close", (exitCode, signal) => {
|
|
191
|
+
clearTimeout(killTimer);
|
|
192
|
+
clearTimeout(forceTimer);
|
|
193
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
194
|
+
const trim = options.trim ?? true;
|
|
195
|
+
const result = {
|
|
196
|
+
command: display,
|
|
197
|
+
stdout: trim ? stdout.replace(/\r?\n$/, "") : stdout,
|
|
198
|
+
stderr: trim ? stderr.replace(/\r?\n$/, "") : stderr,
|
|
199
|
+
exitCode,
|
|
200
|
+
signal,
|
|
201
|
+
duration: Date.now() - started,
|
|
202
|
+
};
|
|
203
|
+
if (options.signal?.aborted) return reject(new AbortError(`Command aborted: ${display}`, { cause: options.signal.reason }));
|
|
204
|
+
if (timedOut) return reject(new ProcessError(`Command timed out after ${timeoutMs}ms: ${display}`, { ...result, timedOut: true }));
|
|
205
|
+
if ((options.reject ?? true) && exitCode !== 0) {
|
|
206
|
+
const detail = result.stderr.trim().split(/\r?\n/).slice(-5).join("\n");
|
|
207
|
+
return reject(new ProcessError(`Command failed with ${exitCode === null ? `signal ${signal}` : `exit code ${exitCode}`}: ${display}${detail ? `\n${detail}` : ""}`, result));
|
|
208
|
+
}
|
|
209
|
+
resolve(result);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Runs a program without a shell, so arguments are passed as they are and
|
|
215
|
+
* user input can't inject anything. Resolves with the output; rejects with
|
|
216
|
+
* a `ProcessError` if the exit code isn't 0.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* const { stdout } = await sys.run("git", ["log", "-1", "--format=%s"]);
|
|
220
|
+
* await sys.run("npm", ["install"], { cwd: "./app", stdio: "inherit" });
|
|
221
|
+
* await sys.run("ffmpeg", ["-i", input, output], { timeout: "5m" });
|
|
222
|
+
*
|
|
223
|
+
* @param {string} file A name found in `PATH`, or a path.
|
|
224
|
+
* @param {string[]} [args=[]]
|
|
225
|
+
* @param {RunOptions} [options]
|
|
226
|
+
* @returns {Promise<RunResult>}
|
|
227
|
+
* @throws {ProcessError} If it can't start, fails (unless `reject: false`) or times out.
|
|
228
|
+
*/
|
|
229
|
+
function run(file, args = [], options = {}) {
|
|
230
|
+
const display = [file, ...args.map((a) => (/\s/.test(a) ? JSON.stringify(a) : a))].join(" ");
|
|
231
|
+
return _spawn(file, args.map(String), false, display, options);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Runs a command line through the shell, so pipes, `&&` and variables
|
|
236
|
+
* work. Don't build it from untrusted input; use `run()` for that.
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* const { stdout } = await sys.exec("git status --short | wc -l");
|
|
240
|
+
* const { exitCode } = await sys.exec("npm test", { reject: false, stdio: "inherit" });
|
|
241
|
+
*
|
|
242
|
+
* @param {string} command
|
|
243
|
+
* @param {RunOptions} [options]
|
|
244
|
+
* @returns {Promise<RunResult>}
|
|
245
|
+
* @throws {ProcessError} If it fails (unless `reject: false`) or times out.
|
|
246
|
+
*/
|
|
247
|
+
function exec(command, options = {}) {
|
|
248
|
+
return _spawn(command, [], true, command, options);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Finds an executable in `PATH`, like the `which` command.
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* sys.which("git"); // "/usr/bin/git"
|
|
256
|
+
* sys.which("nope"); // undefined
|
|
257
|
+
*
|
|
258
|
+
* @param {string} command
|
|
259
|
+
* @returns {string | undefined}
|
|
260
|
+
*/
|
|
261
|
+
function which(command) {
|
|
262
|
+
const isWin = process.platform === "win32";
|
|
263
|
+
const exts = isWin ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
264
|
+
const candidates = (/** @type {string} */ base) => (isWin && !nodePath.extname(base) ? exts.map((e) => base + e.toLowerCase()) : isWin ? [base, ...exts.map((e) => base + e.toLowerCase())] : [base]);
|
|
265
|
+
const usable = (/** @type {string} */ file) => {
|
|
266
|
+
try {
|
|
267
|
+
if (!fs.statSync(file).isFile()) return false;
|
|
268
|
+
if (!isWin) fs.accessSync(file, fs.constants.X_OK);
|
|
269
|
+
return true;
|
|
270
|
+
} catch {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
if (command.includes("/") || command.includes("\\")) {
|
|
275
|
+
return candidates(nodePath.resolve(command)).find(usable);
|
|
276
|
+
}
|
|
277
|
+
for (const dir of (process.env.PATH ?? process.env.Path ?? "").split(nodePath.delimiter)) {
|
|
278
|
+
if (!dir) continue;
|
|
279
|
+
const found = candidates(nodePath.join(dir.replace(/^"|"$/g, ""), command)).find(usable);
|
|
280
|
+
if (found) return found;
|
|
281
|
+
}
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Opens a URL, file or folder with the default app.
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* sys.open("http://localhost:3000");
|
|
290
|
+
*
|
|
291
|
+
* @param {string} target
|
|
292
|
+
* @returns {boolean} `true` if the opener started.
|
|
293
|
+
*/
|
|
294
|
+
function open(target) {
|
|
295
|
+
try {
|
|
296
|
+
const value = /^[a-z][a-z\d+.-]*:/i.test(target) ? target : nodePath.resolve(target);
|
|
297
|
+
const child =
|
|
298
|
+
process.platform === "win32"
|
|
299
|
+
? spawn(process.env.comspec || "cmd.exe", ["/d", "/s", "/c", `start "" ${_escapeArg(value, false)}`], { detached: true, stdio: "ignore", windowsHide: true, windowsVerbatimArguments: true })
|
|
300
|
+
: spawn(process.platform === "darwin" ? "open" : "xdg-open", [value], { detached: true, stdio: "ignore" });
|
|
301
|
+
child.on("error", () => {});
|
|
302
|
+
child.unref();
|
|
303
|
+
return true;
|
|
304
|
+
} catch {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** @type {Array<() => unknown>} */
|
|
310
|
+
const _shutdownHandlers = [];
|
|
311
|
+
let _shutdownInstalled = false;
|
|
312
|
+
let _shuttingDown = false;
|
|
313
|
+
/** @type {{ timeout: number, exitCode: number }} */
|
|
314
|
+
const _shutdownConfig = { timeout: 10_000, exitCode: 0 };
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Runs cleanup code when the process is asked to stop (Ctrl+C,
|
|
318
|
+
* `docker stop`, `kill`): close servers, flush logs, disconnect databases.
|
|
319
|
+
* Handlers run in reverse order, then the process exits. If they take
|
|
320
|
+
* longer than `timeout`, the exit is forced; a second Ctrl+C exits at once.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* const server = app.listen(3000);
|
|
324
|
+
* sys.onShutdown(() => new Promise((done) => server.close(done)));
|
|
325
|
+
* sys.onShutdown(() => db.close());
|
|
326
|
+
*
|
|
327
|
+
* @param {() => unknown} handler Can be async.
|
|
328
|
+
* @param {ShutdownOptions} [options]
|
|
329
|
+
* @returns {() => void} Removes the handler.
|
|
330
|
+
*/
|
|
331
|
+
function onShutdown(handler, options = {}) {
|
|
332
|
+
if (options.timeout !== undefined) {
|
|
333
|
+
_shutdownConfig.timeout = typeof options.timeout === "number" ? options.timeout : require("./Time.js").parseDuration(options.timeout) ?? 10_000;
|
|
334
|
+
}
|
|
335
|
+
if (options.exitCode !== undefined) _shutdownConfig.exitCode = options.exitCode;
|
|
336
|
+
_shutdownHandlers.push(handler);
|
|
337
|
+
if (!_shutdownInstalled) {
|
|
338
|
+
_shutdownInstalled = true;
|
|
339
|
+
const signals = options.signals ?? /** @type {NodeJS.Signals[]} */ (["SIGINT", "SIGTERM", "SIGHUP", ...(process.platform === "win32" ? ["SIGBREAK"] : [])]);
|
|
340
|
+
for (const signal of signals) {
|
|
341
|
+
try {
|
|
342
|
+
process.on(signal, () => {
|
|
343
|
+
void _shutdown(signal);
|
|
344
|
+
});
|
|
345
|
+
} catch {
|
|
346
|
+
// unsupported signal on this platform
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return () => {
|
|
351
|
+
const index = _shutdownHandlers.lastIndexOf(handler);
|
|
352
|
+
if (index !== -1) _shutdownHandlers.splice(index, 1);
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* @param {string} reason
|
|
358
|
+
*/
|
|
359
|
+
const _shutdown = async (reason) => {
|
|
360
|
+
if (_shuttingDown) {
|
|
361
|
+
process.exit(1);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
_shuttingDown = true;
|
|
365
|
+
const force = setTimeout(() => {
|
|
366
|
+
process.stderr.write(`[@ix-xs/node-comfort] Shutdown (${reason}) took longer than ${_shutdownConfig.timeout}ms, forcing exit\n`);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}, _shutdownConfig.timeout);
|
|
369
|
+
force.unref();
|
|
370
|
+
let failed = false;
|
|
371
|
+
for (const handler of [..._shutdownHandlers].reverse()) {
|
|
372
|
+
try {
|
|
373
|
+
await handler();
|
|
374
|
+
} catch (error) {
|
|
375
|
+
failed = true;
|
|
376
|
+
process.stderr.write(`[@ix-xs/node-comfort] Shutdown handler failed: ${error instanceof Error ? error.stack : String(error)}\n`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
clearTimeout(force);
|
|
380
|
+
process.exit(failed ? 1 : _shutdownConfig.exitCode);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Are we on Windows?
|
|
385
|
+
* @returns {boolean}
|
|
386
|
+
*/
|
|
387
|
+
function isWindows() {
|
|
388
|
+
return process.platform === "win32";
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Are we on macOS?
|
|
393
|
+
* @returns {boolean}
|
|
394
|
+
*/
|
|
395
|
+
function isMac() {
|
|
396
|
+
return process.platform === "darwin";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Are we on Linux? WSL counts.
|
|
401
|
+
* @returns {boolean}
|
|
402
|
+
*/
|
|
403
|
+
function isLinux() {
|
|
404
|
+
return process.platform === "linux";
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Are we running in CI? Detects GitHub Actions, GitLab, CircleCI, Jenkins,
|
|
409
|
+
* Azure, Vercel, Netlify and more.
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* if (sys.isCI()) nc.setLevel("warn");
|
|
413
|
+
*
|
|
414
|
+
* @returns {boolean}
|
|
415
|
+
*/
|
|
416
|
+
function isCI() {
|
|
417
|
+
const env = process.env;
|
|
418
|
+
if (env.CI === "false" || env.CI === "0") return false;
|
|
419
|
+
return Boolean(
|
|
420
|
+
env.CI || env.CONTINUOUS_INTEGRATION || env.BUILD_NUMBER || env.RUN_ID || env.GITHUB_ACTIONS || env.GITLAB_CI ||
|
|
421
|
+
env.CIRCLECI || env.TRAVIS || env.JENKINS_URL || env.TF_BUILD || env.BITBUCKET_BUILD_NUMBER || env.BUILDKITE ||
|
|
422
|
+
env.VERCEL || env.NETLIFY || env.CODEBUILD_BUILD_ID || env.TEAMCITY_VERSION,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Are we running inside a container?
|
|
428
|
+
*
|
|
429
|
+
* @returns {boolean}
|
|
430
|
+
*/
|
|
431
|
+
function isDocker() {
|
|
432
|
+
if (process.platform !== "linux") return false;
|
|
433
|
+
try {
|
|
434
|
+
if (fs.existsSync("/.dockerenv") || fs.existsSync("/run/.containerenv")) return true;
|
|
435
|
+
return /docker|kubepods|containerd/.test(fs.readFileSync("/proc/self/cgroup", "utf8"));
|
|
436
|
+
} catch {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Are we running in WSL?
|
|
443
|
+
*
|
|
444
|
+
* @returns {boolean}
|
|
445
|
+
*/
|
|
446
|
+
function isWSL() {
|
|
447
|
+
return process.platform === "linux" && (/microsoft/i.test(os.release()) || Boolean(process.env.WSL_DISTRO_NAME));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Facts about the machine and the runtime, handy for bug reports.
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* console.table(sys.info());
|
|
455
|
+
*
|
|
456
|
+
* @returns {SystemInfo}
|
|
457
|
+
*/
|
|
458
|
+
function info() {
|
|
459
|
+
const cpus = os.cpus();
|
|
460
|
+
let user = "";
|
|
461
|
+
try {
|
|
462
|
+
user = os.userInfo().username;
|
|
463
|
+
} catch {
|
|
464
|
+
user = process.env.USER ?? process.env.USERNAME ?? "";
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
platform: process.platform,
|
|
468
|
+
arch: process.arch,
|
|
469
|
+
release: os.release(),
|
|
470
|
+
node: process.version,
|
|
471
|
+
cpus: cpus.length,
|
|
472
|
+
cpuModel: cpus[0]?.model?.trim() ?? "unknown",
|
|
473
|
+
memory: { total: os.totalmem(), free: os.freemem(), used: os.totalmem() - os.freemem() },
|
|
474
|
+
uptime: os.uptime(),
|
|
475
|
+
hostname: os.hostname(),
|
|
476
|
+
user,
|
|
477
|
+
pid: process.pid,
|
|
478
|
+
ci: isCI(),
|
|
479
|
+
docker: isDocker(),
|
|
480
|
+
wsl: isWSL(),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Memory used by this process, in bytes.
|
|
486
|
+
*
|
|
487
|
+
* @example
|
|
488
|
+
* nc.num.formatBytes(sys.memory().heapUsed); // "42.1 MB"
|
|
489
|
+
*
|
|
490
|
+
* @returns {{ rss: number, heapTotal: number, heapUsed: number, external: number, arrayBuffers: number }}
|
|
491
|
+
*/
|
|
492
|
+
function memory() {
|
|
493
|
+
const usage = process.memoryUsage();
|
|
494
|
+
return { rss: usage.rss, heapTotal: usage.heapTotal, heapUsed: usage.heapUsed, external: usage.external, arrayBuffers: usage.arrayBuffers };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = {
|
|
498
|
+
run,
|
|
499
|
+
exec,
|
|
500
|
+
which,
|
|
501
|
+
open,
|
|
502
|
+
onShutdown,
|
|
503
|
+
isWindows,
|
|
504
|
+
isMac,
|
|
505
|
+
isLinux,
|
|
506
|
+
isCI,
|
|
507
|
+
isDocker,
|
|
508
|
+
isWSL,
|
|
509
|
+
info,
|
|
510
|
+
memory,
|
|
511
|
+
};
|