@ix-xs/node-comfort 2.0.0 → 2.1.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 +150 -0
- package/README.md +32 -30
- package/index.js +3 -3
- package/package.json +8 -7
- package/src/Arr.js +222 -58
- package/src/Async.js +322 -90
- package/src/Cache.js +148 -44
- package/src/Checker.js +257 -26
- package/src/Cli.js +404 -145
- package/src/Color.js +224 -83
- package/src/Crypto.js +481 -106
- package/src/Emitter.js +104 -37
- package/src/Env.js +176 -42
- package/src/FS.js +203 -60
- package/src/Func.js +226 -76
- package/src/Http.js +311 -149
- package/src/Id.js +281 -96
- package/src/Logger.js +302 -78
- package/src/Num.js +182 -60
- package/src/Obj.js +340 -125
- package/src/SQLite.js +426 -105
- package/src/Schema.js +602 -87
- package/src/Str.js +316 -86
- package/src/Sys.js +197 -54
- package/src/Time.js +572 -317
- package/src/Utils.js +62 -41
- package/src/errors.js +54 -11
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +59 -17
- package/types/src/Arr.d.ts +78 -0
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +52 -1
- package/types/src/Async.d.ts.map +1 -1
- package/types/src/Cache.d.ts +33 -7
- package/types/src/Cache.d.ts.map +1 -1
- package/types/src/Checker.d.ts +155 -2
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +97 -15
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +7 -1
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +99 -13
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +36 -17
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +31 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +36 -5
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +147 -19
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +113 -43
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts +60 -8
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +53 -2
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +48 -7
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +67 -4
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +123 -14
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +256 -7
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +49 -1
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +51 -6
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +130 -34
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +15 -11
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +27 -3
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
package/src/Sys.js
CHANGED
|
@@ -14,21 +14,26 @@
|
|
|
14
14
|
* });
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
const { spawn } = require("node:child_process");
|
|
18
17
|
const fs = require("node:fs");
|
|
19
18
|
const os = require("node:os");
|
|
20
19
|
const nodePath = require("node:path");
|
|
21
20
|
const { ProcessError, AbortError } = require("./errors.js");
|
|
21
|
+
const { toMs } = require("./internal/shared.js");
|
|
22
|
+
|
|
23
|
+
// Loaded on first use: most programs never start another one.
|
|
24
|
+
/** @type {typeof import("node:child_process") | undefined} */
|
|
25
|
+
let _childProcess;
|
|
26
|
+
const _cp = () => (_childProcess ??= require("node:child_process"));
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
29
|
* Options for `run()` and `exec()`.
|
|
25
30
|
* @typedef {object} RunOptions
|
|
26
31
|
* @property {string} [cwd] Where to run the command.
|
|
27
|
-
* @property {Record<string, string | undefined>} [env] Extra environment variables, added to `process.env`.
|
|
32
|
+
* @property {Record<string, string | undefined>} [env] Extra environment variables, added to `process.env`. `undefined` removes one.
|
|
28
33
|
* @property {boolean} [extendEnv] Set to `false` to pass only `env`, without `process.env`.
|
|
29
34
|
* @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.
|
|
35
|
+
* @property {number | string} [timeout] Kill the command after this long, like `"30s"`. On Windows, what it started is stopped with it.
|
|
36
|
+
* @property {AbortSignal} [signal] Kills the command when aborted. On Windows, what it started is stopped with it.
|
|
32
37
|
* @property {"pipe" | "inherit"} [stdio] `"inherit"` shows the output live instead of capturing it. Defaults to `"pipe"`.
|
|
33
38
|
* @property {(line: string) => void} [onStdout] Called for each line of stdout.
|
|
34
39
|
* @property {(line: string) => void} [onStderr] Called for each line of stderr.
|
|
@@ -55,6 +60,12 @@ const { ProcessError, AbortError } = require("./errors.js");
|
|
|
55
60
|
* @property {NodeJS.Signals[]} [signals] Signals that trigger it. Defaults to `SIGINT`, `SIGTERM` and `SIGHUP`.
|
|
56
61
|
*/
|
|
57
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Why the process is stopping: the signal it got, or `"shutdown"` when
|
|
65
|
+
* `sys.shutdown()` was called.
|
|
66
|
+
* @typedef {NodeJS.Signals | "shutdown"} ShutdownReason
|
|
67
|
+
*/
|
|
68
|
+
|
|
58
69
|
/**
|
|
59
70
|
* Returned by `info()`.
|
|
60
71
|
* @typedef {object} SystemInfo
|
|
@@ -105,6 +116,23 @@ const _prepare = (file, args) => {
|
|
|
105
116
|
return { file: process.env.comspec || "cmd.exe", args: ["/d", "/s", "/c", `"${line}"`], verbatim: true };
|
|
106
117
|
};
|
|
107
118
|
|
|
119
|
+
/**
|
|
120
|
+
* @param {RunOptions} options
|
|
121
|
+
* @returns {Record<string, string | undefined> | undefined}
|
|
122
|
+
*/
|
|
123
|
+
const _env = (options) => {
|
|
124
|
+
if (options.extendEnv === false) return options.env;
|
|
125
|
+
if (!options.env) return process.env;
|
|
126
|
+
/** @type {Record<string, string | undefined>} */
|
|
127
|
+
const merged = { ...process.env };
|
|
128
|
+
if (process.platform === "win32") {
|
|
129
|
+
// Windows reads PATH and Path as one variable: ours has to replace it, not sit next to it.
|
|
130
|
+
const given = new Set(Object.keys(options.env).map((key) => key.toUpperCase()));
|
|
131
|
+
for (const key of Object.keys(merged)) if (given.has(key.toUpperCase())) delete merged[key];
|
|
132
|
+
}
|
|
133
|
+
return Object.assign(merged, options.env);
|
|
134
|
+
};
|
|
135
|
+
|
|
108
136
|
/**
|
|
109
137
|
* @param {string} file
|
|
110
138
|
* @param {string[]} args
|
|
@@ -117,14 +145,13 @@ const _spawn = (file, args, shell, display, options) =>
|
|
|
117
145
|
new Promise((resolve, reject) => {
|
|
118
146
|
const started = Date.now();
|
|
119
147
|
const inherit = options.stdio === "inherit";
|
|
120
|
-
const env = options.extendEnv === false ? options.env : { ...process.env, ...options.env };
|
|
121
148
|
const prepared = shell ? { file, args, verbatim: false } : _prepare(file, args);
|
|
122
|
-
const timeoutMs = options.timeout === undefined ? undefined :
|
|
149
|
+
const timeoutMs = options.timeout === undefined ? undefined : toMs(options.timeout, "timeout");
|
|
123
150
|
if (options.signal?.aborted) return reject(new AbortError(`Command aborted: ${display}`, { cause: options.signal.reason }));
|
|
124
151
|
|
|
125
|
-
const child = spawn(prepared.file, prepared.args, {
|
|
152
|
+
const child = _cp().spawn(prepared.file, prepared.args, {
|
|
126
153
|
cwd: options.cwd,
|
|
127
|
-
env: /** @type {NodeJS.ProcessEnv} */ (
|
|
154
|
+
env: /** @type {NodeJS.ProcessEnv} */ (_env(options)),
|
|
128
155
|
shell,
|
|
129
156
|
windowsHide: true,
|
|
130
157
|
windowsVerbatimArguments: prepared.verbatim,
|
|
@@ -168,12 +195,20 @@ const _spawn = (file, args, shell, display, options) =>
|
|
|
168
195
|
child.stdin.end(options.input);
|
|
169
196
|
}
|
|
170
197
|
|
|
198
|
+
let stopped = false;
|
|
171
199
|
const kill = () => {
|
|
200
|
+
stopped = true;
|
|
201
|
+
if (process.platform === "win32" && child.pid !== undefined) {
|
|
202
|
+
// On Windows, child.kill() ends cmd.exe and leaves what it started running.
|
|
203
|
+
const killer = _cp().spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
204
|
+
killer.on("error", () => child.kill());
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
172
207
|
child.kill("SIGTERM");
|
|
173
208
|
forceTimer = setTimeout(() => child.kill("SIGKILL"), 2000);
|
|
174
209
|
forceTimer.unref();
|
|
175
210
|
};
|
|
176
|
-
if (timeoutMs !== undefined) {
|
|
211
|
+
if (timeoutMs !== undefined && timeoutMs !== Infinity) {
|
|
177
212
|
killTimer = setTimeout(() => {
|
|
178
213
|
timedOut = true;
|
|
179
214
|
kill();
|
|
@@ -187,6 +222,13 @@ const _spawn = (file, args, shell, display, options) =>
|
|
|
187
222
|
options.signal?.removeEventListener("abort", onAbort);
|
|
188
223
|
reject(new ProcessError(`Failed to start "${display}": ${error.message}`, { command: display, cause: error }));
|
|
189
224
|
});
|
|
225
|
+
child.on("exit", () => {
|
|
226
|
+
// What the command started may outlive it and keep its output open:
|
|
227
|
+
// once the command was stopped on purpose, there is nothing more to wait for.
|
|
228
|
+
if (!stopped) return;
|
|
229
|
+
child.stdout?.destroy();
|
|
230
|
+
child.stderr?.destroy();
|
|
231
|
+
});
|
|
190
232
|
child.on("close", (exitCode, signal) => {
|
|
191
233
|
clearTimeout(killTimer);
|
|
192
234
|
clearTimeout(forceTimer);
|
|
@@ -248,6 +290,20 @@ function exec(command, options = {}) {
|
|
|
248
290
|
return _spawn(command, [], true, command, options);
|
|
249
291
|
}
|
|
250
292
|
|
|
293
|
+
/** @type {Map<string, string>} */
|
|
294
|
+
const _found = new Map();
|
|
295
|
+
|
|
296
|
+
/** @param {string} file */
|
|
297
|
+
const _runnable = (file) => {
|
|
298
|
+
try {
|
|
299
|
+
if (!fs.statSync(file).isFile()) return false;
|
|
300
|
+
if (process.platform !== "win32") fs.accessSync(file, fs.constants.X_OK);
|
|
301
|
+
return true;
|
|
302
|
+
} catch {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
251
307
|
/**
|
|
252
308
|
* Finds an executable in `PATH`, like the `which` command.
|
|
253
309
|
*
|
|
@@ -259,27 +315,33 @@ function exec(command, options = {}) {
|
|
|
259
315
|
* @returns {string | undefined}
|
|
260
316
|
*/
|
|
261
317
|
function which(command) {
|
|
318
|
+
if (typeof command !== "string" || !command) return undefined;
|
|
262
319
|
const isWin = process.platform === "win32";
|
|
320
|
+
const searchPath = process.env.PATH ?? process.env.Path ?? "";
|
|
321
|
+
const key = `${command}\0${searchPath}\0${process.cwd()}`;
|
|
322
|
+
const known = _found.get(key);
|
|
323
|
+
// A found program is checked again with one call, in case it was removed since.
|
|
324
|
+
if (known !== undefined && _runnable(known)) return known;
|
|
263
325
|
const exts = isWin ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
264
326
|
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
|
-
|
|
266
|
-
|
|
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
|
-
};
|
|
327
|
+
/** @type {string | undefined} */
|
|
328
|
+
let found;
|
|
274
329
|
if (command.includes("/") || command.includes("\\")) {
|
|
275
|
-
|
|
330
|
+
found = candidates(nodePath.resolve(command)).find(_runnable);
|
|
331
|
+
} else {
|
|
332
|
+
for (const dir of searchPath.split(nodePath.delimiter)) {
|
|
333
|
+
if (!dir) continue;
|
|
334
|
+
found = candidates(nodePath.join(dir.replace(/^"|"$/g, ""), command)).find(_runnable);
|
|
335
|
+
if (found) break;
|
|
336
|
+
}
|
|
276
337
|
}
|
|
277
|
-
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
|
|
338
|
+
if (found) {
|
|
339
|
+
if (_found.size >= 200) _found.clear();
|
|
340
|
+
_found.set(key, found);
|
|
341
|
+
} else {
|
|
342
|
+
_found.delete(key);
|
|
281
343
|
}
|
|
282
|
-
return
|
|
344
|
+
return found;
|
|
283
345
|
}
|
|
284
346
|
|
|
285
347
|
/**
|
|
@@ -296,8 +358,8 @@ function open(target) {
|
|
|
296
358
|
const value = /^[a-z][a-z\d+.-]*:/i.test(target) ? target : nodePath.resolve(target);
|
|
297
359
|
const child =
|
|
298
360
|
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" });
|
|
361
|
+
? _cp().spawn(process.env.comspec || "cmd.exe", ["/d", "/s", "/c", `start "" ${_escapeArg(value, false)}`], { detached: true, stdio: "ignore", windowsHide: true, windowsVerbatimArguments: true })
|
|
362
|
+
: _cp().spawn(process.platform === "darwin" ? "open" : "xdg-open", [value], { detached: true, stdio: "ignore" });
|
|
301
363
|
child.on("error", () => {});
|
|
302
364
|
child.unref();
|
|
303
365
|
return true;
|
|
@@ -306,9 +368,10 @@ function open(target) {
|
|
|
306
368
|
}
|
|
307
369
|
}
|
|
308
370
|
|
|
309
|
-
/** @type {Array<() => unknown>} */
|
|
371
|
+
/** @type {Array<(reason: ShutdownReason) => unknown>} */
|
|
310
372
|
const _shutdownHandlers = [];
|
|
311
|
-
|
|
373
|
+
/** @type {Set<string>} */
|
|
374
|
+
const _shutdownSignals = new Set();
|
|
312
375
|
let _shuttingDown = false;
|
|
313
376
|
/** @type {{ timeout: number, exitCode: number }} */
|
|
314
377
|
const _shutdownConfig = { timeout: 10_000, exitCode: 0 };
|
|
@@ -322,29 +385,26 @@ const _shutdownConfig = { timeout: 10_000, exitCode: 0 };
|
|
|
322
385
|
* @example
|
|
323
386
|
* const server = app.listen(3000);
|
|
324
387
|
* sys.onShutdown(() => new Promise((done) => server.close(done)));
|
|
325
|
-
* sys.onShutdown(() =>
|
|
388
|
+
* sys.onShutdown((reason) => log.info(`Stopping (${reason})`));
|
|
326
389
|
*
|
|
327
|
-
* @param {() => unknown} handler Can be async.
|
|
390
|
+
* @param {(reason: ShutdownReason) => unknown} handler Can be async. Gets the signal that stopped the process.
|
|
328
391
|
* @param {ShutdownOptions} [options]
|
|
329
392
|
* @returns {() => void} Removes the handler.
|
|
330
393
|
*/
|
|
331
394
|
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
|
-
}
|
|
395
|
+
if (options.timeout !== undefined) _shutdownConfig.timeout = toMs(options.timeout, "timeout");
|
|
335
396
|
if (options.exitCode !== undefined) _shutdownConfig.exitCode = options.exitCode;
|
|
336
397
|
_shutdownHandlers.push(handler);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
398
|
+
const signals = options.signals ?? /** @type {NodeJS.Signals[]} */ (["SIGINT", "SIGTERM", "SIGHUP", ...(process.platform === "win32" ? ["SIGBREAK"] : [])]);
|
|
399
|
+
for (const signal of signals) {
|
|
400
|
+
if (_shutdownSignals.has(signal)) continue;
|
|
401
|
+
_shutdownSignals.add(signal);
|
|
402
|
+
try {
|
|
403
|
+
process.on(signal, () => {
|
|
404
|
+
void _shutdown(signal, undefined);
|
|
405
|
+
});
|
|
406
|
+
} catch {
|
|
407
|
+
// unsupported signal on this platform
|
|
348
408
|
}
|
|
349
409
|
}
|
|
350
410
|
return () => {
|
|
@@ -354,9 +414,11 @@ function onShutdown(handler, options = {}) {
|
|
|
354
414
|
}
|
|
355
415
|
|
|
356
416
|
/**
|
|
357
|
-
* @param {
|
|
417
|
+
* @param {ShutdownReason} reason
|
|
418
|
+
* @param {number | undefined} exitCode
|
|
419
|
+
* @returns {Promise<void>}
|
|
358
420
|
*/
|
|
359
|
-
const _shutdown = async (reason) => {
|
|
421
|
+
const _shutdown = async (reason, exitCode) => {
|
|
360
422
|
if (_shuttingDown) {
|
|
361
423
|
process.exit(1);
|
|
362
424
|
return;
|
|
@@ -365,21 +427,40 @@ const _shutdown = async (reason) => {
|
|
|
365
427
|
const force = setTimeout(() => {
|
|
366
428
|
process.stderr.write(`[@ix-xs/node-comfort] Shutdown (${reason}) took longer than ${_shutdownConfig.timeout}ms, forcing exit\n`);
|
|
367
429
|
process.exit(1);
|
|
368
|
-
}, _shutdownConfig.timeout);
|
|
369
|
-
|
|
430
|
+
}, Math.min(_shutdownConfig.timeout, 2 ** 31 - 1));
|
|
431
|
+
// Not unref'd: a handler that never settles must not let the process end
|
|
432
|
+
// quietly with the wrong exit code once nothing else keeps it alive.
|
|
370
433
|
let failed = false;
|
|
371
434
|
for (const handler of [..._shutdownHandlers].reverse()) {
|
|
372
435
|
try {
|
|
373
|
-
await handler();
|
|
436
|
+
await handler(reason);
|
|
374
437
|
} catch (error) {
|
|
375
438
|
failed = true;
|
|
376
439
|
process.stderr.write(`[@ix-xs/node-comfort] Shutdown handler failed: ${error instanceof Error ? error.stack : String(error)}\n`);
|
|
377
440
|
}
|
|
378
441
|
}
|
|
379
442
|
clearTimeout(force);
|
|
380
|
-
process.exit(failed ? 1 : _shutdownConfig.exitCode);
|
|
443
|
+
process.exit(failed ? 1 : exitCode ?? _shutdownConfig.exitCode);
|
|
381
444
|
};
|
|
382
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Stops the process the clean way: runs the `onShutdown()` handlers, then
|
|
448
|
+
* exits. Use it instead of `process.exit()` when something goes wrong for
|
|
449
|
+
* good, so servers and databases still get closed.
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* process.on("uncaughtException", (error) => {
|
|
453
|
+
* log.fatal(error);
|
|
454
|
+
* sys.shutdown(1);
|
|
455
|
+
* });
|
|
456
|
+
*
|
|
457
|
+
* @param {number} [exitCode] Defaults to the `exitCode` given to `onShutdown()`, or `0`. A failing handler makes it `1`.
|
|
458
|
+
* @returns {Promise<void>} Never resolves: the process exits first.
|
|
459
|
+
*/
|
|
460
|
+
function shutdown(exitCode) {
|
|
461
|
+
return _shutdown("shutdown", exitCode);
|
|
462
|
+
}
|
|
463
|
+
|
|
383
464
|
/**
|
|
384
465
|
* Are we on Windows?
|
|
385
466
|
* @returns {boolean}
|
|
@@ -423,19 +504,23 @@ function isCI() {
|
|
|
423
504
|
);
|
|
424
505
|
}
|
|
425
506
|
|
|
507
|
+
/** @type {boolean | undefined} */
|
|
508
|
+
let _docker;
|
|
509
|
+
|
|
426
510
|
/**
|
|
427
511
|
* Are we running inside a container?
|
|
428
512
|
*
|
|
429
513
|
* @returns {boolean}
|
|
430
514
|
*/
|
|
431
515
|
function isDocker() {
|
|
432
|
-
if (
|
|
516
|
+
if (_docker !== undefined) return _docker;
|
|
517
|
+
if (process.platform !== "linux") return (_docker = false);
|
|
433
518
|
try {
|
|
434
|
-
|
|
435
|
-
return /docker|kubepods|containerd/.test(fs.readFileSync("/proc/self/cgroup", "utf8"));
|
|
519
|
+
_docker = fs.existsSync("/.dockerenv") || fs.existsSync("/run/.containerenv") || /docker|kubepods|containerd/.test(fs.readFileSync("/proc/self/cgroup", "utf8"));
|
|
436
520
|
} catch {
|
|
437
|
-
|
|
521
|
+
_docker = false;
|
|
438
522
|
}
|
|
523
|
+
return _docker;
|
|
439
524
|
}
|
|
440
525
|
|
|
441
526
|
/**
|
|
@@ -481,6 +566,61 @@ function info() {
|
|
|
481
566
|
};
|
|
482
567
|
}
|
|
483
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Finds a TCP port nobody is listening on. Give a preferred port to get it
|
|
571
|
+
* back when it's free, and another one when it's taken.
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
* const port = await sys.freePort(3000); // 3000, or a free one if 3000 is in use
|
|
575
|
+
* app.listen(port);
|
|
576
|
+
*
|
|
577
|
+
* @param {number} [preferred] Tried first.
|
|
578
|
+
* @param {string} [host] The address to check. Defaults to all of them.
|
|
579
|
+
* @returns {Promise<number>}
|
|
580
|
+
*/
|
|
581
|
+
function freePort(preferred, host) {
|
|
582
|
+
const net = require("node:net");
|
|
583
|
+
/**
|
|
584
|
+
* @param {number} port
|
|
585
|
+
* @returns {Promise<number>}
|
|
586
|
+
*/
|
|
587
|
+
const listen = (port) =>
|
|
588
|
+
new Promise((resolve, reject) => {
|
|
589
|
+
const server = net.createServer();
|
|
590
|
+
server.unref();
|
|
591
|
+
server.once("error", reject);
|
|
592
|
+
server.listen({ port, host, exclusive: true }, () => {
|
|
593
|
+
const address = server.address();
|
|
594
|
+
server.close(() => resolve(address !== null && typeof address === "object" ? address.port : port));
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
if (preferred === undefined) return listen(0);
|
|
598
|
+
if (!Number.isInteger(preferred) || preferred < 0 || preferred > 65535) return Promise.reject(new RangeError(`freePort(): expected a port from 0 to 65535, got ${preferred}`));
|
|
599
|
+
return listen(preferred).catch(() => listen(0));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* The address of this machine on the local network, like `"192.168.1.20"`.
|
|
604
|
+
* It's what you print next to `localhost` so a phone on the same Wi-Fi can
|
|
605
|
+
* reach a dev server.
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* console.log(`http://${sys.localIP() ?? "localhost"}:3000`);
|
|
609
|
+
*
|
|
610
|
+
* @param {4 | 6} [family=4] IPv4 or IPv6.
|
|
611
|
+
* @returns {string | undefined} `undefined` when the machine has no network.
|
|
612
|
+
*/
|
|
613
|
+
function localIP(family = 4) {
|
|
614
|
+
const wanted = family === 6 ? "IPv6" : "IPv4";
|
|
615
|
+
for (const addresses of Object.values(os.networkInterfaces())) {
|
|
616
|
+
for (const address of addresses ?? []) {
|
|
617
|
+
// fe80:: addresses only work with an interface name attached, so they're no use in a URL
|
|
618
|
+
if (address.family === wanted && !address.internal && !address.address.startsWith("fe80:")) return address.address;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
|
|
484
624
|
/**
|
|
485
625
|
* Memory used by this process, in bytes.
|
|
486
626
|
*
|
|
@@ -500,6 +640,9 @@ module.exports = {
|
|
|
500
640
|
which,
|
|
501
641
|
open,
|
|
502
642
|
onShutdown,
|
|
643
|
+
shutdown,
|
|
644
|
+
freePort,
|
|
645
|
+
localIP,
|
|
503
646
|
isWindows,
|
|
504
647
|
isMac,
|
|
505
648
|
isLinux,
|