@gajae-code/utils 0.11.0 → 0.11.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/dist/types/dirs.d.ts +2 -0
- package/package.json +2 -2
- package/src/dirs.ts +6 -1
- package/src/postmortem.ts +69 -9
package/dist/types/dirs.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export declare function formatBunRuntimeError(opts: {
|
|
|
36
36
|
}): string;
|
|
37
37
|
export declare function resolveEquivalentPath(inputPath: string): string;
|
|
38
38
|
export declare function normalizePathForComparison(inputPath: string, platform?: NodeJS.Platform): string;
|
|
39
|
+
/** Return whether a relative path crosses above its root or is unexpectedly absolute. */
|
|
40
|
+
export declare function relativePathEscapesRoot(relative: string): boolean;
|
|
39
41
|
export declare function pathIsWithin(root: string, candidate: string): boolean;
|
|
40
42
|
export declare function relativePathWithinRoot(root: string, candidate: string): string | null;
|
|
41
43
|
/** Get the project directory. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/utils",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.2",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@gajae-code/natives": "0.11.
|
|
34
|
+
"@gajae-code/natives": "0.11.2",
|
|
35
35
|
"beautiful-mermaid": "^1.1.3",
|
|
36
36
|
"handlebars": "^4.7.9",
|
|
37
37
|
"winston": "^3.19.0",
|
package/src/dirs.ts
CHANGED
|
@@ -114,11 +114,16 @@ export function normalizePathForComparison(inputPath: string, platform: NodeJS.P
|
|
|
114
114
|
return platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/** Return whether a relative path crosses above its root or is unexpectedly absolute. */
|
|
118
|
+
export function relativePathEscapesRoot(relative: string): boolean {
|
|
119
|
+
return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
|
|
120
|
+
}
|
|
121
|
+
|
|
117
122
|
export function pathIsWithin(root: string, candidate: string): boolean {
|
|
118
123
|
const normalizedRoot = normalizePathForComparison(root);
|
|
119
124
|
const normalizedCandidate = normalizePathForComparison(candidate);
|
|
120
125
|
const relative = path.relative(normalizedRoot, normalizedCandidate);
|
|
121
|
-
return
|
|
126
|
+
return !relativePathEscapesRoot(relative);
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
export function relativePathWithinRoot(root: string, candidate: string): string | null {
|
package/src/postmortem.ts
CHANGED
|
@@ -55,7 +55,11 @@ function runCleanup(reason: Reason, options: CleanupOptions = {}): Promise<void>
|
|
|
55
55
|
cleanupStage = "running";
|
|
56
56
|
break;
|
|
57
57
|
case "running":
|
|
58
|
-
|
|
58
|
+
// Exit-bound waiters (signals, fatals, quit) legitimately join the
|
|
59
|
+
// in-flight cleanup via `cleanupPromise`; only a genuine manual
|
|
60
|
+
// recursion (a cleanup callback calling cleanup()) is a bug worth a
|
|
61
|
+
// diagnostic.
|
|
62
|
+
if (reason === Reason.MANUAL && !shouldSuppressCleanupLogging(quiet)) {
|
|
59
63
|
logger.error("Cleanup invoked recursively", { stack: new Error().stack });
|
|
60
64
|
}
|
|
61
65
|
return Promise.resolve();
|
|
@@ -90,9 +94,65 @@ function runCleanup(reason: Reason, options: CleanupOptions = {}): Promise<void>
|
|
|
90
94
|
return promise;
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Finite cleanup-liveness contract for every exit-bound wait.
|
|
99
|
+
*
|
|
100
|
+
* Governed waits: signal handlers (SIGINT/SIGTERM/SIGHUP), fatal handlers
|
|
101
|
+
* (uncaught exception / unhandled rejection), the quiet stdout-EPIPE exit, and
|
|
102
|
+
* `quit()`. Each waits at most `resolveCleanupDeadlineMs()` for the shared
|
|
103
|
+
* in-flight cleanup before exiting with its own unchanged exit code (130/143/
|
|
104
|
+
* 129, 1 for fatals, BROKEN_PIPE_EXIT_CODE, or quit's `code`).
|
|
105
|
+
*
|
|
106
|
+
* Ungoverned: `cleanup()` (Reason.MANUAL without exit) is caller-owned and
|
|
107
|
+
* unbounded, and Reason.EXIT stays fire-and-forget (exit is imminent).
|
|
108
|
+
*
|
|
109
|
+
* On expiry the stage is forced to "complete" so late re-entries no-op, a
|
|
110
|
+
* single diagnostic goes to stderr and the error log (suppressed during quiet
|
|
111
|
+
* broken-pipe shutdown), and late callback settlement is ignored — rejections
|
|
112
|
+
* were already routed through Promise.allSettled, so none can become unhandled.
|
|
113
|
+
*
|
|
114
|
+
* The deadline defaults to 5000 ms and can be overridden with
|
|
115
|
+
* `GJC_CLEANUP_DEADLINE_MS` (finite values >= 0; anything else falls back to
|
|
116
|
+
* the default).
|
|
117
|
+
*/
|
|
118
|
+
const DEFAULT_CLEANUP_DEADLINE_MS = 5_000;
|
|
119
|
+
|
|
120
|
+
function resolveCleanupDeadlineMs(): number {
|
|
121
|
+
const raw = process.env.GJC_CLEANUP_DEADLINE_MS;
|
|
122
|
+
if (raw === undefined || raw.trim() === "") return DEFAULT_CLEANUP_DEADLINE_MS;
|
|
123
|
+
const parsed = Number(raw);
|
|
124
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_CLEANUP_DEADLINE_MS;
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function awaitCleanupWithDeadline(reason: Reason, options: CleanupOptions = {}): Promise<void> {
|
|
129
|
+
const pending = cleanupPromise;
|
|
130
|
+
if (!pending || cleanupStage === "complete") return;
|
|
131
|
+
const deadlineMs = resolveCleanupDeadlineMs();
|
|
132
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
133
|
+
const timedOut = await Promise.race([
|
|
134
|
+
pending.then(() => false),
|
|
135
|
+
new Promise<boolean>(resolve => {
|
|
136
|
+
// Deliberately referenced: the timer is also the liveness floor that
|
|
137
|
+
// keeps the process alive until the bounded wait settles, so an
|
|
138
|
+
// otherwise-empty event loop cannot exit 0 underneath a governed wait.
|
|
139
|
+
timer = setTimeout(() => resolve(true), deadlineMs);
|
|
140
|
+
}),
|
|
141
|
+
]);
|
|
142
|
+
if (timer) clearTimeout(timer);
|
|
143
|
+
if (!timedOut) return;
|
|
144
|
+
// Force the terminal stage so late settlement and re-entries are no-ops.
|
|
145
|
+
cleanupStage = "complete";
|
|
146
|
+
if (!shouldSuppressCleanupLogging(options.quiet === true)) {
|
|
147
|
+
const diagnostic = `[postmortem] cleanup deadline (${deadlineMs}ms) expired for ${reason}; exiting without waiting for remaining callbacks.\n`;
|
|
148
|
+
safeStderrWrite(diagnostic);
|
|
149
|
+
logger.error("Cleanup deadline expired", { reason, deadlineMs });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function runCleanupBounded(reason: Reason, options: CleanupOptions = {}): Promise<void> {
|
|
94
154
|
void runCleanup(reason, options);
|
|
95
|
-
await (
|
|
155
|
+
await awaitCleanupWithDeadline(reason, options);
|
|
96
156
|
}
|
|
97
157
|
|
|
98
158
|
function installProcessStdoutWriteClassifier(): void {
|
|
@@ -150,7 +210,7 @@ async function exitQuietlyForAttributableStdoutEpipe(reason: Reason): Promise<vo
|
|
|
150
210
|
quietShutdownStarted = true;
|
|
151
211
|
// Set the observable status before cleanup can await or trigger another error.
|
|
152
212
|
process.exitCode = BROKEN_PIPE_EXIT_CODE;
|
|
153
|
-
await
|
|
213
|
+
await runCleanupBounded(reason, { quiet: true });
|
|
154
214
|
// An ordinary fatal that arrived during quiet cleanup takes precedence.
|
|
155
215
|
if (process.exitCode === BROKEN_PIPE_EXIT_CODE) process.exit(BROKEN_PIPE_EXIT_CODE);
|
|
156
216
|
}
|
|
@@ -173,7 +233,7 @@ async function handleFatalError(label: string, reason: unknown, cleanupReason: R
|
|
|
173
233
|
stack: err.stack,
|
|
174
234
|
});
|
|
175
235
|
}
|
|
176
|
-
await
|
|
236
|
+
await runCleanupBounded(cleanupReason);
|
|
177
237
|
process.exit(1);
|
|
178
238
|
}
|
|
179
239
|
|
|
@@ -181,7 +241,7 @@ if (isMainThread) {
|
|
|
181
241
|
installProcessStdoutWriteClassifier();
|
|
182
242
|
process
|
|
183
243
|
.on("SIGINT", async () => {
|
|
184
|
-
await
|
|
244
|
+
await runCleanupBounded(Reason.SIGINT);
|
|
185
245
|
process.exit(130); // 128 + SIGINT (2)
|
|
186
246
|
})
|
|
187
247
|
.on("SIGUSR1", () => {
|
|
@@ -201,11 +261,11 @@ if (isMainThread) {
|
|
|
201
261
|
void runCleanup(Reason.EXIT); // fire and forget (exit imminent)
|
|
202
262
|
})
|
|
203
263
|
.on("SIGTERM", async () => {
|
|
204
|
-
await
|
|
264
|
+
await runCleanupBounded(Reason.SIGTERM);
|
|
205
265
|
process.exit(143); // 128 + SIGTERM (15)
|
|
206
266
|
})
|
|
207
267
|
.on("SIGHUP", async () => {
|
|
208
|
-
await
|
|
268
|
+
await runCleanupBounded(Reason.SIGHUP);
|
|
209
269
|
process.exit(129); // 128 + SIGHUP (1)
|
|
210
270
|
});
|
|
211
271
|
} else {
|
|
@@ -296,7 +356,7 @@ export async function quit(code: number = 0): Promise<void> {
|
|
|
296
356
|
}
|
|
297
357
|
|
|
298
358
|
const exitAfterCleanup = async (): Promise<void> => {
|
|
299
|
-
await
|
|
359
|
+
await awaitCleanupWithDeadline(Reason.MANUAL);
|
|
300
360
|
if (process.stdout.writableLength > 0) {
|
|
301
361
|
const { promise, resolve } = Promise.withResolvers<void>();
|
|
302
362
|
process.stdout.once("drain", resolve);
|