@oh-my-pi/pi-utils 18.15.0 → 18.17.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 +27 -0
- package/dist/types/acp/schema.d.ts +1 -1
- package/dist/types/ar/codecs/xz.d.ts +4 -0
- package/dist/types/ar/lzh.d.ts +2 -0
- package/dist/types/bytes.d.ts +7 -0
- package/dist/types/dirs.d.ts +6 -0
- package/dist/types/held-file.d.ts +5 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/logger.d.ts +29 -0
- package/dist/types/path.d.ts +2 -0
- package/dist/types/pid.d.ts +8 -0
- package/dist/types/procmgr.d.ts +1 -1
- package/dist/types/sqlite.d.ts +12 -0
- package/package.json +4 -3
- package/src/acp/schema.ts +53 -107
- package/src/ar/arj.ts +13 -32
- package/src/ar/asar.ts +5 -8
- package/src/ar/codecs/xz.ts +7 -8
- package/src/ar/lzh.ts +21 -27
- package/src/ar/sevenzip/decode.ts +1 -56
- package/src/ar/tar.ts +1 -8
- package/src/bytes.ts +14 -0
- package/src/dirs.ts +9 -0
- package/src/held-file.ts +22 -0
- package/src/index.ts +2 -0
- package/src/logger.ts +143 -29
- package/src/path.ts +14 -0
- package/src/pid.ts +16 -0
- package/src/postmortem.ts +9 -6
- package/src/procmgr.ts +1 -1
- package/src/ptree.ts +82 -18
- package/src/runtime-install.ts +1 -4
- package/src/sqlite.ts +32 -2
- package/src/stderr-guard.ts +18 -10
package/src/postmortem.ts
CHANGED
|
@@ -10,6 +10,7 @@ import * as fs from "node:fs";
|
|
|
10
10
|
import { APP_NAME } from "./dirs";
|
|
11
11
|
import * as logger from "./logger";
|
|
12
12
|
import { restoreTerminalStderr } from "./stderr-guard";
|
|
13
|
+
import { toError } from "./type-guards";
|
|
13
14
|
|
|
14
15
|
// Cleanup reasons, in order of priority/meaning.
|
|
15
16
|
export enum Reason {
|
|
@@ -276,7 +277,7 @@ function runCleanup(reason: Reason, keepAlive = false): Promise<void> {
|
|
|
276
277
|
const cleanupSettled = Promise.allSettled(promises).then(async results => {
|
|
277
278
|
for (const result of results) {
|
|
278
279
|
if (result.status === "rejected") {
|
|
279
|
-
const err =
|
|
280
|
+
const err = toError(result.reason);
|
|
280
281
|
logger.error("Cleanup callback failed", { err, stack: err.stack });
|
|
281
282
|
}
|
|
282
283
|
}
|
|
@@ -708,10 +709,12 @@ function handleWorkerSendEpipe(err: Error): boolean {
|
|
|
708
709
|
* Reports a caught top-level failure after terminal owners restore their display, then exits.
|
|
709
710
|
*/
|
|
710
711
|
export async function fatal(error: unknown): Promise<never> {
|
|
711
|
-
const err =
|
|
712
|
+
const err = toError(error);
|
|
712
713
|
const report = `${Bun.inspect(error, { colors: process.stderr.isTTY === true })}\n`;
|
|
713
714
|
if (!Bun.isMainThread) {
|
|
714
715
|
process.stderr.write(`${report}${formatFatalRecoveryHints(fatalRecoveryHintProviders)}`);
|
|
716
|
+
// An explicit exit alone leaves the parent only `close` with code 1; this reaches its `error` listeners.
|
|
717
|
+
reportError(err);
|
|
715
718
|
process.exit(1);
|
|
716
719
|
}
|
|
717
720
|
return exitAfterFatal(report, "Fatal error", err, Reason.UNHANDLED_REJECTION);
|
|
@@ -753,7 +756,7 @@ export function installProcessHandlers(): void {
|
|
|
753
756
|
logger.warn("Ignoring expected cleanup exception", { err: thrown });
|
|
754
757
|
return;
|
|
755
758
|
}
|
|
756
|
-
const err =
|
|
759
|
+
const err = toError(thrown);
|
|
757
760
|
// A worker IPC `send()` race can surface through either global error event;
|
|
758
761
|
// contain it in both. Stdout write disconnects are attributed to stdout by
|
|
759
762
|
// registerStdioDisconnectHandling's `error` listener, not classified here.
|
|
@@ -786,7 +789,7 @@ export function installProcessHandlers(): void {
|
|
|
786
789
|
);
|
|
787
790
|
})
|
|
788
791
|
.on("unhandledRejection", async reason => {
|
|
789
|
-
const err =
|
|
792
|
+
const err = toError(reason);
|
|
790
793
|
if (handleWorkerSendEpipe(err)) return;
|
|
791
794
|
if (isExpectedCleanupError(reason)) {
|
|
792
795
|
logger.warn("Ignoring expected cleanup rejection", { err });
|
|
@@ -876,7 +879,7 @@ export function register(
|
|
|
876
879
|
const pending = invokeCleanup(registration, reason, keepAlive, cleanupPass);
|
|
877
880
|
if (!pending) return;
|
|
878
881
|
const tracked = pending.catch(error => {
|
|
879
|
-
const err =
|
|
882
|
+
const err = toError(error);
|
|
880
883
|
logger.error("Cleanup callback failed", { err, id, stack: err.stack });
|
|
881
884
|
});
|
|
882
885
|
// Join the active pass so cleanup()/signal exits await it; after a
|
|
@@ -886,7 +889,7 @@ export function register(
|
|
|
886
889
|
activePassDeadline?.extend(tracked, registration.deadlineMs, registration.waitMessage);
|
|
887
890
|
}
|
|
888
891
|
} catch (error) {
|
|
889
|
-
const err =
|
|
892
|
+
const err = toError(error);
|
|
890
893
|
logger.error("Cleanup callback failed", { err, id, stack: err.stack });
|
|
891
894
|
}
|
|
892
895
|
};
|
package/src/procmgr.ts
CHANGED
|
@@ -142,7 +142,7 @@ export function resolveBasicShell(): string | undefined {
|
|
|
142
142
|
* Resolve the external shell to advertise on Windows.
|
|
143
143
|
*
|
|
144
144
|
* A host bash is OPTIONAL: bash tool commands always execute in the embedded
|
|
145
|
-
*
|
|
145
|
+
* bash engine (pi-shell). The resolved binary only serves the spawn-a-shell paths
|
|
146
146
|
* (interactive PTY sessions, ACP client terminals, SHELL env), so this
|
|
147
147
|
* prefers a real Git Bash when one exists and otherwise falls back to
|
|
148
148
|
* cmd.exe — it never fails.
|
package/src/ptree.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Convenience helpers: captureText / execText, AbortSignal, timeouts.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import * as os from "node:os";
|
|
10
11
|
import { Process, type ProcessTerminateOptions } from "@oh-my-pi/pi-natives";
|
|
11
12
|
import type { Spawn, Subprocess } from "bun";
|
|
12
13
|
|
|
@@ -193,8 +194,9 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
193
194
|
#stderrStream?: ReadableStream<Uint8Array>;
|
|
194
195
|
// Termination in flight after kill(); aborted exits await it before reporting.
|
|
195
196
|
#terminating?: Promise<boolean | void>;
|
|
196
|
-
//
|
|
197
|
-
|
|
197
|
+
// The one stop sequence of a subreaper-supervised command; a later hard
|
|
198
|
+
// kill request ends its grace period, and any other later request is a no-op.
|
|
199
|
+
#subreaperStop?: { escalate: () => void };
|
|
198
200
|
#terminateGroup: boolean;
|
|
199
201
|
#hardKillTree: boolean;
|
|
200
202
|
// Windows has no process groups. Retaining the root's native handle pins
|
|
@@ -339,22 +341,31 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
339
341
|
// group leader; wait() still needs to report the later deadline.
|
|
340
342
|
if (this.proc.exitCode !== null) this.#exitReason = reason;
|
|
341
343
|
}
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
344
|
+
// A command under the Linux subreaper is stopped without ever signalling
|
|
345
|
+
// the subreaper politely: SIGTERM would end it, its orphaned descendants
|
|
346
|
+
// would reparent to init, and the SIGKILL that follows would not find a
|
|
347
|
+
// descendant that ignored SIGTERM. One stop sequence serves every kill
|
|
348
|
+
// request; a hard request during its grace period ends the grace early.
|
|
349
|
+
if (this.#subreaperStop) {
|
|
350
|
+
if (gracefulMs !== undefined && gracefulMs < 0) this.#subreaperStop.escalate();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (this.#hardKillTree && this.proc.exitCode === null && this.proc.signalCode === null) {
|
|
354
|
+
let root: Process | null = null;
|
|
355
|
+
try {
|
|
356
|
+
root = Process.fromPid(this.proc.pid);
|
|
357
|
+
} catch {
|
|
358
|
+
// No usable native handle: the direct-signal fallback below still ends the subreaper.
|
|
359
|
+
}
|
|
351
360
|
if (root) {
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
361
|
+
const { promise: escalated, resolve: escalate } = Promise.withResolvers<void>();
|
|
362
|
+
const graceMs = gracefulMs === undefined ? 1000 : gracefulMs;
|
|
363
|
+
if (graceMs < 0) escalate();
|
|
364
|
+
// A stop that fails part-way (a native handle that cannot be used)
|
|
365
|
+
// must not count as a finished termination: signal the subreaper directly.
|
|
366
|
+
const stop = this.#stopSubreaperTree(root, graceMs, escalated).catch(() => this.#signalChild(graceMs));
|
|
367
|
+
this.#subreaperStop = { escalate };
|
|
368
|
+
this.#terminating = stop;
|
|
358
369
|
return;
|
|
359
370
|
}
|
|
360
371
|
}
|
|
@@ -421,6 +432,57 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
421
432
|
if (this.proc.exitCode === null && this.proc.signalCode === null) signal("SIGKILL");
|
|
422
433
|
}
|
|
423
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Stop a subreaper-supervised command: SIGTERM to each of the subreaper's
|
|
437
|
+
* children and their trees (never the subreaper itself), then, after
|
|
438
|
+
* `gracefulMs`, the subreaper's own exit, or `escalated`, the hard sweep.
|
|
439
|
+
* A negative `gracefulMs` skips the SIGTERM; zero sends it and sweeps at once.
|
|
440
|
+
* The subreaper stays alive through the grace period, so a descendant
|
|
441
|
+
* orphaned in it is still the subreaper's child when the sweep runs.
|
|
442
|
+
*
|
|
443
|
+
* A subreaper with no child has not launched the command yet (or the
|
|
444
|
+
* command has ended). It is looked at again while frozen with SIGSTOP and,
|
|
445
|
+
* when still childless, killed, so a command cancelled before its launch
|
|
446
|
+
* never starts. Freeze, kill and resume all go through `root`, the native
|
|
447
|
+
* handle pinned to the subreaper's identity, never through the numeric pid:
|
|
448
|
+
* once the subreaper has been reaped and its pid reused, they signal
|
|
449
|
+
* nothing. The resume runs on every way out of the frozen section,
|
|
450
|
+
* including a throw; it is a no-op on a killed subreaper. The grace timer is
|
|
451
|
+
* cleared as soon as the sequence moves on, so a finished stop never keeps
|
|
452
|
+
* the caller's process alive.
|
|
453
|
+
*/
|
|
454
|
+
async #stopSubreaperTree(root: Process, gracefulMs: number, escalated: Promise<void>): Promise<void> {
|
|
455
|
+
let children = root.children();
|
|
456
|
+
if (children.length === 0) {
|
|
457
|
+
try {
|
|
458
|
+
root.killTree(os.constants.signals.SIGSTOP);
|
|
459
|
+
children = root.children();
|
|
460
|
+
if (children.length === 0) {
|
|
461
|
+
root.killTree(9);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
// It launched the command just before the freeze: the first wave
|
|
465
|
+
// goes to the snapshot taken while it cannot fork.
|
|
466
|
+
for (const child of children) child.killTree(gracefulMs < 0 ? 9 : 15);
|
|
467
|
+
} finally {
|
|
468
|
+
root.killTree(os.constants.signals.SIGCONT);
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
// The subreaper launches one command; once it has a child, no later launch can escape the snapshot.
|
|
472
|
+
for (const child of children) child.killTree(gracefulMs < 0 ? 9 : 15);
|
|
473
|
+
}
|
|
474
|
+
if (gracefulMs >= 0) {
|
|
475
|
+
const { promise: graceOver, resolve: endGrace } = Promise.withResolvers<void>();
|
|
476
|
+
const graceTimer = setTimeout(endGrace, gracefulMs);
|
|
477
|
+
try {
|
|
478
|
+
await Promise.race([this.proc.exited, graceOver, escalated]);
|
|
479
|
+
} finally {
|
|
480
|
+
clearTimeout(graceTimer);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
await this.#hardKillSubreaperTree(root);
|
|
484
|
+
}
|
|
485
|
+
|
|
424
486
|
async #hardKillSubreaperTree(root: Process): Promise<void> {
|
|
425
487
|
try {
|
|
426
488
|
const deadline = Date.now() + SUBREAPER_KILL_WINDOW_MS;
|
|
@@ -605,7 +667,9 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
605
667
|
}
|
|
606
668
|
|
|
607
669
|
attachTimeout(ms: number): void {
|
|
608
|
-
|
|
670
|
+
// Not skipped for a root Bun already reports as exited (`proc.killed`): it can
|
|
671
|
+
// exit before spawn() returns, while a descendant still holds its pipes.
|
|
672
|
+
if (ms <= 0) return;
|
|
609
673
|
this.#exited.catch(() => {});
|
|
610
674
|
// One unref'd deadline controls both termination and pipe collection.
|
|
611
675
|
// A clean command clears it in wait(), so fast invocations do not hold
|
package/src/runtime-install.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as Module from "node:module";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { withFileLock } from "./file-lock";
|
|
6
6
|
import { isEexist, isEnoent } from "./fs-error";
|
|
7
|
+
import { isRecord } from "./type-guards";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* On-demand runtime dependency support for native-heavy optional packages
|
|
@@ -32,10 +33,6 @@ const RUNTIME_CONDITIONS: Record<string, true> = { node: true, require: true, de
|
|
|
32
33
|
/** Extension probes appended to a `main`/`exports` target that lacks one. */
|
|
33
34
|
const RUNTIME_EXTENSIONS: readonly string[] = [".js", ".cjs", ".mjs", ".json", ".node"];
|
|
34
35
|
|
|
35
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
36
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
36
|
/**
|
|
40
37
|
* Walk a conditional `exports` target (string, array of fallbacks, or a
|
|
41
38
|
* condition object) and return the first relative path that matches a runtime
|
package/src/sqlite.ts
CHANGED
|
@@ -2,15 +2,43 @@
|
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { installSqliteLockShim } from "@oh-my-pi/pi-natives/sqlite-locks";
|
|
5
6
|
import { getDbBusyTimeoutMs } from "./env";
|
|
6
7
|
import { withFileLockSync } from "./file-lock";
|
|
7
8
|
import { isEnoent } from "./fs-error";
|
|
9
|
+
import { toError } from "./type-guards";
|
|
8
10
|
import * as logger from "./logger";
|
|
9
11
|
|
|
10
12
|
const BUSY_MAX_ATTEMPTS = 4;
|
|
11
13
|
const BUSY_BASE_DELAY_MS = 100;
|
|
12
14
|
const SQLITE_STORE_SUFFIXES = ["-wal", "-shm", "-journal", ""];
|
|
13
15
|
|
|
16
|
+
let sqliteLockShimInstalled: boolean | undefined;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Moves this process's SQLite locks onto open-file-description locks (Linux), once.
|
|
20
|
+
*
|
|
21
|
+
* SQLite's POSIX record locks belong to the process, so any close of another
|
|
22
|
+
* descriptor of a database or its `-shm` released them, including Bun's own file
|
|
23
|
+
* APIs and the bash tool's builtins; a peer omp then unlinked the WAL under the live
|
|
24
|
+
* connection (issue #15586). Call before the process opens its first database: a
|
|
25
|
+
* connection opened earlier keeps POSIX locks. Returns whether the shim is active; a
|
|
26
|
+
* Linux build where it cannot be installed (Bun's SQLite layout changed) logs a warning
|
|
27
|
+
* once, and callers with a UI surface it.
|
|
28
|
+
*/
|
|
29
|
+
export function ensureSqliteLockShim(): boolean {
|
|
30
|
+
if (sqliteLockShimInstalled === undefined) {
|
|
31
|
+
sqliteLockShimInstalled = installSqliteLockShim();
|
|
32
|
+
if (!sqliteLockShimInstalled && process.platform === "linux") {
|
|
33
|
+
logger.warn("SQLite OFD lock shim unavailable; in-process reads of a live store can release its locks", {
|
|
34
|
+
bun: Bun.version,
|
|
35
|
+
issue: "https://github.com/can1357/oh-my-pi/issues/15586",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return sqliteLockShimInstalled;
|
|
40
|
+
}
|
|
41
|
+
|
|
14
42
|
/**
|
|
15
43
|
* A store's identity as `dev:ino:birthtimeMs`, `null` when the path holds no
|
|
16
44
|
* file, or `undefined` when the stat itself failed and no conclusion is safe.
|
|
@@ -100,7 +128,7 @@ function closeFailedDatabase(db: Database | undefined, error: unknown, identity:
|
|
|
100
128
|
try {
|
|
101
129
|
db?.close();
|
|
102
130
|
} catch (closeError) {
|
|
103
|
-
const original =
|
|
131
|
+
const original = toError(error);
|
|
104
132
|
const detail = closeError instanceof Error ? closeError.message : String(closeError);
|
|
105
133
|
original.message += `; failed to close the SQLite handle: ${detail}`;
|
|
106
134
|
throw new SqliteAttemptFailure(original, identity, { canRecover: false });
|
|
@@ -152,6 +180,7 @@ async function openWithBusyRetries<T>(
|
|
|
152
180
|
let db: Database | undefined;
|
|
153
181
|
const identity = sqliteFileIdentity(dbPath);
|
|
154
182
|
try {
|
|
183
|
+
ensureSqliteLockShim();
|
|
155
184
|
db = new Database(dbPath);
|
|
156
185
|
// WAL recovery can bypass the busy handler; both it and retries are needed (#2421).
|
|
157
186
|
db.run(`PRAGMA busy_timeout = ${getDbBusyTimeoutMs()}`);
|
|
@@ -175,6 +204,7 @@ function openOnce<T>(dbPath: string, initialize: (db: Database) => T, options: S
|
|
|
175
204
|
let db: Database | undefined;
|
|
176
205
|
const identity = sqliteFileIdentity(dbPath);
|
|
177
206
|
try {
|
|
207
|
+
ensureSqliteLockShim();
|
|
178
208
|
db = new Database(dbPath);
|
|
179
209
|
db.run(`PRAGMA busy_timeout = ${getDbBusyTimeoutMs()}`);
|
|
180
210
|
return initialize(db);
|
|
@@ -333,7 +363,7 @@ export function openSqliteDatabaseSync<T>(
|
|
|
333
363
|
|
|
334
364
|
/** Adds the failing store's path to an error without losing SQLite result codes or its original stack. */
|
|
335
365
|
export function annotateSqliteError(error: unknown, dbPath: string): Error {
|
|
336
|
-
const annotated =
|
|
366
|
+
const annotated = toError(error);
|
|
337
367
|
annotated.message = `Database "${dbPath}": ${annotated.message}`;
|
|
338
368
|
return annotated;
|
|
339
369
|
}
|
package/src/stderr-guard.ts
CHANGED
|
@@ -14,9 +14,14 @@
|
|
|
14
14
|
* so diagnostics stay greppable and Bun native-crash reports (which abort
|
|
15
15
|
* before any JS cleanup can restore fd 2) are preserved.
|
|
16
16
|
*
|
|
17
|
-
* Only
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Only dup2 goes through bun:ffi. The saved copy of fd 2 comes from opening
|
|
18
|
+
* `/dev/fd/2` instead of `dup(2)`, because Bun opens files close-on-exec: a
|
|
19
|
+
* plain dup would leak the terminal into every child spawned without explicit
|
|
20
|
+
* fd handling (Rust `Command`, gitoxide filter drivers), which could then read
|
|
21
|
+
* the user's keystrokes and terminal replies. fcntl is deliberately avoided:
|
|
22
|
+
* it is variadic, and the arm64-darwin ABI passes variadic arguments on the
|
|
23
|
+
* stack, so a fixed-arity FFI signature would read garbage for the third
|
|
24
|
+
* argument.
|
|
20
25
|
*/
|
|
21
26
|
import { dlopen, FFIType } from "bun:ffi";
|
|
22
27
|
import * as fs from "node:fs";
|
|
@@ -27,7 +32,6 @@ const STDOUT_FILENO = 1;
|
|
|
27
32
|
const STDERR_FILENO = 2;
|
|
28
33
|
|
|
29
34
|
interface LibcFdOps {
|
|
30
|
-
dup(fd: number): number;
|
|
31
35
|
dup2(oldFd: number, newFd: number): number;
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -44,7 +48,6 @@ function libcFdOps(): LibcFdOps | null {
|
|
|
44
48
|
for (const candidate of candidates) {
|
|
45
49
|
try {
|
|
46
50
|
const libc = dlopen(candidate, {
|
|
47
|
-
dup: { args: [FFIType.i32], returns: FFIType.i32 },
|
|
48
51
|
dup2: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 },
|
|
49
52
|
});
|
|
50
53
|
libcFdOpsCache = libc.symbols;
|
|
@@ -102,6 +105,15 @@ export function suppressTerminalStderr(options?: SuppressTerminalStderrOptions):
|
|
|
102
105
|
const libc = libcFdOps();
|
|
103
106
|
if (!libc) return false;
|
|
104
107
|
|
|
108
|
+
// First, so a fd 2 that cannot be reopened (a socket: Linux refuses
|
|
109
|
+
// `/dev/fd/N` for those) leaves the guard inert with no redirect file made.
|
|
110
|
+
let saved: number;
|
|
111
|
+
try {
|
|
112
|
+
saved = fs.openSync(`/dev/fd/${STDERR_FILENO}`, fs.constants.O_WRONLY);
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
105
117
|
let redirectFd: number;
|
|
106
118
|
try {
|
|
107
119
|
const redirectPath = options?.redirectPath ?? redirectTarget ?? getLogPath();
|
|
@@ -115,15 +127,11 @@ export function suppressTerminalStderr(options?: SuppressTerminalStderrOptions):
|
|
|
115
127
|
try {
|
|
116
128
|
redirectFd = fs.openSync("/dev/null", "w");
|
|
117
129
|
} catch {
|
|
130
|
+
fs.closeSync(saved);
|
|
118
131
|
return false;
|
|
119
132
|
}
|
|
120
133
|
}
|
|
121
134
|
|
|
122
|
-
const saved = libc.dup(STDERR_FILENO);
|
|
123
|
-
if (saved === -1) {
|
|
124
|
-
fs.closeSync(redirectFd);
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
135
|
if (libc.dup2(redirectFd, STDERR_FILENO) === -1) {
|
|
128
136
|
fs.closeSync(redirectFd);
|
|
129
137
|
fs.closeSync(saved);
|