@oh-my-pi/pi-utils 18.0.10 → 18.0.11
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 +6 -0
- package/dist/types/runtime-install.d.ts +8 -1
- package/package.json +2 -2
- package/src/runtime-install.ts +77 -26
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.0.11] - 2026-08-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed runtime installation getting stuck for up to 60 seconds after an installer crash or forced termination, allowing subsequent installation attempts to proceed normally.
|
|
10
|
+
|
|
5
11
|
## [18.0.10] - 2026-08-28
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -73,6 +73,13 @@ export interface EnsureRuntimeInstalledOptions {
|
|
|
73
73
|
export declare function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void>;
|
|
74
74
|
/**
|
|
75
75
|
* Materialize a pinned dependency set into `runtimeDir` (idempotent,
|
|
76
|
-
* cross-process safe
|
|
76
|
+
* cross-process safe). Returns `runtimeDir`.
|
|
77
|
+
*
|
|
78
|
+
* Serialization uses the OS-backed {@link withFileLock} at
|
|
79
|
+
* `${runtimeDir}.install.lock`, which the kernel releases on process death, so
|
|
80
|
+
* a crashed installer cannot wedge later attempts (issue #10120). The path is
|
|
81
|
+
* deliberately distinct from the legacy `${runtimeDir}.lock` mkdir directory;
|
|
82
|
+
* {@link withLegacyInstallLock} atomically reserves that namespace during the
|
|
83
|
+
* new install so older processes cannot cross the migration boundary.
|
|
77
84
|
*/
|
|
78
85
|
export declare function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOptions): Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "18.0.
|
|
4
|
+
"version": "18.0.11",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "18.0.
|
|
34
|
+
"@oh-my-pi/pi-natives": "18.0.11"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/bun": "^1.3.14"
|
package/src/runtime-install.ts
CHANGED
|
@@ -2,6 +2,8 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as fsp from "node:fs/promises";
|
|
3
3
|
import * as Module from "node:module";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { withFileLock } from "./file-lock";
|
|
6
|
+
import { isEexist, isEnoent } from "./fs-error";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* On-demand runtime dependency support for native-heavy optional packages
|
|
@@ -303,25 +305,62 @@ export interface EnsureRuntimeInstalledOptions {
|
|
|
303
305
|
lockSleepMs?: number;
|
|
304
306
|
}
|
|
305
307
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
308
|
+
/** No runtime install plausibly runs this long, so older legacy lock directories are crash orphans. */
|
|
309
|
+
const STALE_LEGACY_LOCK_MS = 10 * 60_000;
|
|
309
310
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Run `fn` while reserving the pre-crash-safe `${runtimeDir}.lock` namespace.
|
|
313
|
+
*
|
|
314
|
+
* Versions through 18.0.10 serialized installs with a bare lock *directory*
|
|
315
|
+
* that only its creator removed; an installer killed outside that window
|
|
316
|
+
* (SIGKILL/OOM/Ctrl-C) left it unreleasable, wedging every later install for
|
|
317
|
+
* the full wait envelope (issue #10120). During an in-flight upgrade a legacy
|
|
318
|
+
* process may still legitimately own this directory, so poll until it is
|
|
319
|
+
* released and only force-reclaim once the directory is older than any
|
|
320
|
+
* plausible install ({@link STALE_LEGACY_LOCK_MS}) — never merely because a
|
|
321
|
+
* retry budget elapsed, which would delete a still-active legacy lock and let
|
|
322
|
+
* two installers race the same tree. Once the namespace is free, atomically
|
|
323
|
+
* create and retain a regular file through `fn`: an older process cannot
|
|
324
|
+
* acquire it between the handoff check and the new install. A file left by a
|
|
325
|
+
* crashed new installer can be reused immediately because the outer OS lock
|
|
326
|
+
* proves its owner is gone, unlike a legacy directory whose owner is unknown.
|
|
327
|
+
*/
|
|
328
|
+
async function withLegacyInstallLock<T>(runtimeDir: string, sleepMs: number, fn: () => Promise<T>): Promise<T> {
|
|
329
|
+
const legacy = `${runtimeDir}.lock`;
|
|
330
|
+
for (;;) {
|
|
314
331
|
try {
|
|
315
|
-
await fsp.
|
|
316
|
-
|
|
317
|
-
await fsp.rm(lockDir, { recursive: true, force: true });
|
|
318
|
-
};
|
|
332
|
+
const reservation = await fsp.open(legacy, "wx");
|
|
333
|
+
await reservation.close();
|
|
319
334
|
} catch (error) {
|
|
320
|
-
if (!
|
|
321
|
-
|
|
335
|
+
if (!isEexist(error)) throw error;
|
|
336
|
+
let stat: fs.Stats;
|
|
337
|
+
try {
|
|
338
|
+
stat = await fsp.stat(legacy);
|
|
339
|
+
} catch (statError) {
|
|
340
|
+
if (isEnoent(statError)) continue; // released between open and stat; retry
|
|
341
|
+
throw statError;
|
|
342
|
+
}
|
|
343
|
+
// A non-directory is a reservation left by a newer installer. The
|
|
344
|
+
// outer OS lock proves that installer is gone, so reuse it at once.
|
|
345
|
+
if (!stat.isDirectory()) break;
|
|
346
|
+
// A fresh directory may still belong to a live pre-18.x installer, so
|
|
347
|
+
// wait for it to finish; only a crash orphan (older than any plausible
|
|
348
|
+
// install) is force-reclaimed.
|
|
349
|
+
if (Date.now() - stat.mtimeMs > STALE_LEGACY_LOCK_MS) {
|
|
350
|
+
await fsp.rm(legacy, { recursive: true, force: true });
|
|
351
|
+
} else {
|
|
352
|
+
await Bun.sleep(sleepMs);
|
|
353
|
+
}
|
|
354
|
+
continue;
|
|
322
355
|
}
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
// Retain the regular-file reservation across the install.
|
|
359
|
+
try {
|
|
360
|
+
return await fn();
|
|
361
|
+
} finally {
|
|
362
|
+
await fsp.rm(legacy, { force: true });
|
|
323
363
|
}
|
|
324
|
-
throw new Error(`Timed out waiting for runtime install lock: ${lockDir}`);
|
|
325
364
|
}
|
|
326
365
|
|
|
327
366
|
export async function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void> {
|
|
@@ -363,7 +402,14 @@ async function runRuntimeInstall(runtimeDir: string): Promise<void> {
|
|
|
363
402
|
|
|
364
403
|
/**
|
|
365
404
|
* Materialize a pinned dependency set into `runtimeDir` (idempotent,
|
|
366
|
-
* cross-process safe
|
|
405
|
+
* cross-process safe). Returns `runtimeDir`.
|
|
406
|
+
*
|
|
407
|
+
* Serialization uses the OS-backed {@link withFileLock} at
|
|
408
|
+
* `${runtimeDir}.install.lock`, which the kernel releases on process death, so
|
|
409
|
+
* a crashed installer cannot wedge later attempts (issue #10120). The path is
|
|
410
|
+
* deliberately distinct from the legacy `${runtimeDir}.lock` mkdir directory;
|
|
411
|
+
* {@link withLegacyInstallLock} atomically reserves that namespace during the
|
|
412
|
+
* new install so older processes cannot cross the migration boundary.
|
|
367
413
|
*/
|
|
368
414
|
export async function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOptions): Promise<string> {
|
|
369
415
|
const { runtimeDir, install, onPhase, lockAttempts = 240, lockSleepMs = 250 } = options;
|
|
@@ -379,15 +425,20 @@ export async function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOpti
|
|
|
379
425
|
if (await probeManifest.exists()) return runtimeDir;
|
|
380
426
|
|
|
381
427
|
onPhase?.("initiate");
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
428
|
+
// withFileLock does not create parent directories; the runtime cache dir may
|
|
429
|
+
// not exist yet on the very first install.
|
|
430
|
+
await fsp.mkdir(path.dirname(runtimeDir), { recursive: true });
|
|
431
|
+
return withFileLock(
|
|
432
|
+
`${runtimeDir}.install`,
|
|
433
|
+
() =>
|
|
434
|
+
withLegacyInstallLock(runtimeDir, lockSleepMs, async () => {
|
|
435
|
+
if (await probeManifest.exists()) return runtimeDir;
|
|
436
|
+
await writeRuntimeManifest(runtimeDir, install);
|
|
437
|
+
onPhase?.("download");
|
|
438
|
+
await runRuntimeInstall(runtimeDir);
|
|
439
|
+
onPhase?.("done");
|
|
440
|
+
return runtimeDir;
|
|
441
|
+
}),
|
|
442
|
+
{ retries: lockAttempts, retryDelayMs: lockSleepMs },
|
|
443
|
+
);
|
|
393
444
|
}
|