@stim-cli/core 1.6.0 → 1.7.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/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as quotedPath, _ as releaseClaim, r as ClaimRefusedError, x as tryAcquireClaim } from "./ownership-claim-
|
|
1
|
+
import { S as quotedPath, _ as releaseClaim, r as ClaimRefusedError, x as tryAcquireClaim } from "./ownership-claim-Cn3NHqBS.mjs";
|
|
2
2
|
import fs, { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path, { basename, dirname, join } from "node:path";
|
|
@@ -11,12 +11,20 @@ const OWNER_MARKER = /^\.stim-claim-[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-
|
|
|
11
11
|
function sleepSync(ms) {
|
|
12
12
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether Windows refused a call on the lock directory because another process is removing it at that
|
|
16
|
+
* moment. NTFS answers ERROR_ACCESS_DENIED, which Node reports as EPERM, until that removal completes,
|
|
17
|
+
* where POSIX answers EEXIST or ENOENT: https://github.com/appandflow/stim/issues/883.
|
|
18
|
+
*/
|
|
19
|
+
function removalInFlight(error) {
|
|
20
|
+
return process.platform === "win32" && error?.code === "EPERM";
|
|
21
|
+
}
|
|
14
22
|
function createLockDirectory(lockPath) {
|
|
15
23
|
try {
|
|
16
24
|
mkdirSync(lockPath);
|
|
17
25
|
return true;
|
|
18
26
|
} catch (error) {
|
|
19
|
-
if (error?.code === "EEXIST") return false;
|
|
27
|
+
if (error?.code === "EEXIST" || removalInFlight(error)) return false;
|
|
20
28
|
throw error;
|
|
21
29
|
}
|
|
22
30
|
}
|
|
@@ -24,7 +32,7 @@ function removeLockDirectory(lockPath, marker) {
|
|
|
24
32
|
try {
|
|
25
33
|
unlinkSync(marker);
|
|
26
34
|
} catch (error) {
|
|
27
|
-
if (error?.code === "ENOENT") return false;
|
|
35
|
+
if (error?.code === "ENOENT" || removalInFlight(error)) return false;
|
|
28
36
|
throw error;
|
|
29
37
|
}
|
|
30
38
|
try {
|
|
@@ -36,6 +44,7 @@ function removeLockDirectory(lockPath, marker) {
|
|
|
36
44
|
"ENOTEMPTY",
|
|
37
45
|
"EEXIST"
|
|
38
46
|
].includes(error?.code ?? "")) return false;
|
|
47
|
+
if (removalInFlight(error)) return false;
|
|
39
48
|
throw error;
|
|
40
49
|
}
|
|
41
50
|
}
|
|
@@ -45,7 +54,7 @@ function takeLockDirectory(lockPath, claim) {
|
|
|
45
54
|
try {
|
|
46
55
|
entries = readdirSync(lockPath);
|
|
47
56
|
} catch (error) {
|
|
48
|
-
if (error?.code === "ENOENT") return null;
|
|
57
|
+
if (error?.code === "ENOENT" || removalInFlight(error)) return null;
|
|
49
58
|
throw error;
|
|
50
59
|
}
|
|
51
60
|
const name = entries.length === 1 ? entries[0] : void 0;
|
|
@@ -55,7 +64,7 @@ function takeLockDirectory(lockPath, claim) {
|
|
|
55
64
|
try {
|
|
56
65
|
entry = lstatSync(marker);
|
|
57
66
|
} catch (error) {
|
|
58
|
-
if (error?.code === "ENOENT") return null;
|
|
67
|
+
if (error?.code === "ENOENT" || removalInFlight(error)) return null;
|
|
59
68
|
throw error;
|
|
60
69
|
}
|
|
61
70
|
if (!entry.isFile() || entry.size !== 0) throw new ClaimRefusedError({
|
|
@@ -212,7 +221,7 @@ function workspaceLogDir(projectRoot) {
|
|
|
212
221
|
function cachePathSetting(key) {
|
|
213
222
|
try {
|
|
214
223
|
const value = JSON.parse(fs.readFileSync(path.join(configDir(), "config.json"), "utf-8"))?.caches?.[key];
|
|
215
|
-
return typeof value === "string" &&
|
|
224
|
+
return typeof value === "string" && path.isAbsolute(value) ? value : null;
|
|
216
225
|
} catch {
|
|
217
226
|
return null;
|
|
218
227
|
}
|
|
@@ -2,6 +2,7 @@ import { captureProcessIdentity, inspectProcessIdentity } from "./process-identi
|
|
|
2
2
|
import { lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { debuglog } from "node:util";
|
|
5
6
|
//#region quoted-path.ts
|
|
6
7
|
/** Quote a path for a shell command Stim prints for a human to copy and run. */
|
|
7
8
|
function quotedPath(path) {
|
|
@@ -17,6 +18,9 @@ const CLAIM_SUFFIX = ".claim";
|
|
|
17
18
|
const CHILD_SUFFIX = ".child";
|
|
18
19
|
const STAGING_PREFIX = ".staging-";
|
|
19
20
|
const PUBLISH_ATTEMPTS = 64;
|
|
21
|
+
const WIN32_SETTLE_MS = 250;
|
|
22
|
+
const WIN32_SETTLE_STEP_MS = 5;
|
|
23
|
+
const debug = debuglog("stim:claim");
|
|
20
24
|
var ClaimRefusedError = class extends Error {
|
|
21
25
|
code = CLAIM_REFUSED;
|
|
22
26
|
claimPath;
|
|
@@ -37,7 +41,7 @@ var ClaimUnavailableError = class extends Error {
|
|
|
37
41
|
constructor(reason, { claimPath } = {}) {
|
|
38
42
|
super(claimPath ? `Stim could not write the ownership claim at ${claimPath}: ${reason}.` : `Stim could not record a process identity, so it cannot take an ownership claim: ${reason}.`);
|
|
39
43
|
this.reason = reason;
|
|
40
|
-
this.remedy = claimPath ? `Restore write access to the existing claim store at ${quotedPath(claimPath)} (check parent permissions, symlink targets, mount access and sandbox rules)` : "Reinstall Stim so the unique-pid native module for this platform is present";
|
|
44
|
+
this.remedy = claimPath ? `Restore write access to the existing claim store at ${quotedPath(claimPath)} (check parent permissions, an antivirus or endpoint agent holding its files, symlink targets, mount access and sandbox rules)` : "Reinstall Stim so the unique-pid native module for this platform is present";
|
|
41
45
|
}
|
|
42
46
|
};
|
|
43
47
|
/**
|
|
@@ -88,10 +92,40 @@ function asOwner(record) {
|
|
|
88
92
|
processToken: candidate.processToken
|
|
89
93
|
};
|
|
90
94
|
}
|
|
95
|
+
function sleepSync(ms) {
|
|
96
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Run a read, listing or removal and resolve the one answer Windows leaves ambiguous. NTFS answers
|
|
100
|
+
* ERROR_ACCESS_DENIED, which Node reports as EPERM, for a name whose removal by another process is still
|
|
101
|
+
* in flight, and for every name below it, where POSIX answers ENOENT; measured to settle within 134 ms
|
|
102
|
+
* with 12 processes on 4 cores (https://github.com/appandflow/stim/issues/883). The call is reissued in
|
|
103
|
+
* small steps for at most WIN32_SETTLE_MS: a result or ENOENT is the settled answer, and an EPERM that
|
|
104
|
+
* outlasts the window is a real denial, thrown exactly as a POSIX EACCES is. This bounds the resolution
|
|
105
|
+
* of an ambiguous OS answer; it never waits on a holder.
|
|
106
|
+
*/
|
|
107
|
+
function settledAnswer(path, call) {
|
|
108
|
+
if (process.platform !== "win32") return call();
|
|
109
|
+
const started = Date.now();
|
|
110
|
+
for (;;) {
|
|
111
|
+
try {
|
|
112
|
+
const result = call();
|
|
113
|
+
if (Date.now() > started) debug("%s settled after %d ms", path, Date.now() - started);
|
|
114
|
+
return result;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const code = err?.code;
|
|
117
|
+
if (code !== "EPERM" || Date.now() - started >= WIN32_SETTLE_MS) {
|
|
118
|
+
if (code === "EPERM") debug("%s still answers EPERM after %d ms", path, Date.now() - started);
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
sleepSync(WIN32_SETTLE_STEP_MS);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
91
125
|
function readChild(claimPath) {
|
|
92
126
|
let text;
|
|
93
127
|
try {
|
|
94
|
-
text = readFileSync(childPath(claimPath), "utf-8");
|
|
128
|
+
text = settledAnswer(childPath(claimPath), () => readFileSync(childPath(claimPath), "utf-8"));
|
|
95
129
|
} catch (err) {
|
|
96
130
|
if (err?.code === "ENOENT") return null;
|
|
97
131
|
return "unreadable";
|
|
@@ -105,7 +139,7 @@ function readChild(claimPath) {
|
|
|
105
139
|
function readClaim(path, mode) {
|
|
106
140
|
let text;
|
|
107
141
|
try {
|
|
108
|
-
text = readFileSync(path, "utf-8");
|
|
142
|
+
text = settledAnswer(path, () => readFileSync(path, "utf-8"));
|
|
109
143
|
} catch (err) {
|
|
110
144
|
if (err?.code === "ENOENT") return null;
|
|
111
145
|
return "unreadable";
|
|
@@ -195,7 +229,7 @@ function refuseNonDirectory(root, path, label) {
|
|
|
195
229
|
}
|
|
196
230
|
function names(dir) {
|
|
197
231
|
try {
|
|
198
|
-
return readdirSync(dir).toSorted();
|
|
232
|
+
return settledAnswer(dir, () => readdirSync(dir)).toSorted();
|
|
199
233
|
} catch (err) {
|
|
200
234
|
if (err?.code === "ENOENT") return [];
|
|
201
235
|
return "unreadable";
|
|
@@ -252,7 +286,7 @@ function readClaimSet(root) {
|
|
|
252
286
|
}
|
|
253
287
|
function removeOrRefuse(path, root, claimPath, label) {
|
|
254
288
|
try {
|
|
255
|
-
unlinkSync(path);
|
|
289
|
+
settledAnswer(path, () => unlinkSync(path));
|
|
256
290
|
} catch (err) {
|
|
257
291
|
const code = err?.code;
|
|
258
292
|
if (code === "ENOENT") return;
|
|
@@ -370,10 +404,12 @@ function selfOwner() {
|
|
|
370
404
|
processToken: captured.token
|
|
371
405
|
};
|
|
372
406
|
}
|
|
373
|
-
function
|
|
374
|
-
if (error instanceof MissingClaimStoreError) return
|
|
375
|
-
const code = error
|
|
376
|
-
|
|
407
|
+
function contention(error) {
|
|
408
|
+
if (error instanceof MissingClaimStoreError) return null;
|
|
409
|
+
const { code, syscall } = error ?? {};
|
|
410
|
+
if (code === "EPERM" && process.platform === "win32") return { denied: syscall !== "rename" };
|
|
411
|
+
if (code === "EEXIST" || code === "ENOTEMPTY" || code === "ENOENT" || code === "EINVAL") return { denied: false };
|
|
412
|
+
return null;
|
|
377
413
|
}
|
|
378
414
|
var MissingClaimStoreError = class extends Error {
|
|
379
415
|
code = "ENOENT";
|
|
@@ -409,6 +445,12 @@ function createClaimDir(path) {
|
|
|
409
445
|
if (error?.code !== "EEXIST") throw error;
|
|
410
446
|
}
|
|
411
447
|
}
|
|
448
|
+
function removeStaging(staging, { recursive }) {
|
|
449
|
+
settledAnswer(staging, () => rmSync(staging, {
|
|
450
|
+
recursive,
|
|
451
|
+
force: true
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
412
454
|
function publishExclusive(root, payload, claimId, label) {
|
|
413
455
|
const staging = join(root, `${STAGING_PREFIX}${claimId}`);
|
|
414
456
|
const target = exclusiveClaimDir(root);
|
|
@@ -418,12 +460,10 @@ function publishExclusive(root, payload, claimId, label) {
|
|
|
418
460
|
renameSync(staging, target);
|
|
419
461
|
return join(target, `${claimId}${CLAIM_SUFFIX}`);
|
|
420
462
|
} catch (err) {
|
|
421
|
-
|
|
422
|
-
recursive: true,
|
|
423
|
-
force: true
|
|
424
|
-
});
|
|
463
|
+
removeStaging(staging, { recursive: true });
|
|
425
464
|
const code = err?.code;
|
|
426
|
-
|
|
465
|
+
const why = contention(err);
|
|
466
|
+
if (why) return why;
|
|
427
467
|
if (code === "ENOTDIR") refuseNonDirectory(root, target, label);
|
|
428
468
|
throw err;
|
|
429
469
|
}
|
|
@@ -437,8 +477,9 @@ function publishShared(root, payload, claimId) {
|
|
|
437
477
|
renameSync(staging, target);
|
|
438
478
|
return target;
|
|
439
479
|
} catch (err) {
|
|
440
|
-
|
|
441
|
-
|
|
480
|
+
removeStaging(staging, { recursive: false });
|
|
481
|
+
const why = contention(err);
|
|
482
|
+
if (why) return why;
|
|
442
483
|
throw err;
|
|
443
484
|
}
|
|
444
485
|
}
|
|
@@ -450,17 +491,32 @@ function settleOrRelease(claim) {
|
|
|
450
491
|
throw err;
|
|
451
492
|
}
|
|
452
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* On Windows, removal in progress can answer EPERM or ENOENT while a releaser removes the store.
|
|
496
|
+
* A spinning contender can outpace 64 attempts; yielding lets that removal finish. A real denial
|
|
497
|
+
* still spends at most PUBLISH_ATTEMPTS of these before it is reported.
|
|
498
|
+
*/
|
|
499
|
+
function yieldToRemoval() {
|
|
500
|
+
sleepSync(WIN32_SETTLE_STEP_MS);
|
|
501
|
+
}
|
|
453
502
|
function tryAcquireClaim({ root, mode, details = {}, label = "ownership" }) {
|
|
454
503
|
const owner = selfOwner();
|
|
455
504
|
const reaped = [];
|
|
505
|
+
let denied = true;
|
|
506
|
+
const retry = (why) => {
|
|
507
|
+
denied &&= why.denied;
|
|
508
|
+
if (process.platform === "win32") yieldToRemoval();
|
|
509
|
+
};
|
|
456
510
|
for (let attempt = 0; attempt < PUBLISH_ATTEMPTS; attempt++) {
|
|
457
511
|
try {
|
|
458
512
|
createClaimDir(root);
|
|
459
513
|
} catch (err) {
|
|
460
514
|
const code = err?.code;
|
|
461
515
|
if (code === "EEXIST" || code === "ENOTDIR") refuseNonDirectory(root, root, label);
|
|
462
|
-
|
|
463
|
-
throw err;
|
|
516
|
+
const why = contention(err);
|
|
517
|
+
if (!why) throw err;
|
|
518
|
+
retry(why);
|
|
519
|
+
continue;
|
|
464
520
|
}
|
|
465
521
|
const state = inspectClaimSet(root, { label });
|
|
466
522
|
reaped.push(...state.reaped);
|
|
@@ -489,7 +545,10 @@ function tryAcquireClaim({ root, mode, details = {}, label = "ownership" }) {
|
|
|
489
545
|
});
|
|
490
546
|
if (mode === "shared") {
|
|
491
547
|
const published = publishShared(root, payload, claimId);
|
|
492
|
-
if (
|
|
548
|
+
if (typeof published !== "string") {
|
|
549
|
+
retry(published);
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
493
552
|
const claim = handle(published);
|
|
494
553
|
const settled = settleOrRelease(claim);
|
|
495
554
|
reaped.push(...settled.reaped);
|
|
@@ -504,7 +563,10 @@ function tryAcquireClaim({ root, mode, details = {}, label = "ownership" }) {
|
|
|
504
563
|
};
|
|
505
564
|
}
|
|
506
565
|
const path = publishExclusive(root, payload, claimId, label);
|
|
507
|
-
if (
|
|
566
|
+
if (typeof path !== "string") {
|
|
567
|
+
retry(path);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
508
570
|
const claim = handle(path);
|
|
509
571
|
const settled = settleOrRelease(claim);
|
|
510
572
|
reaped.push(...settled.reaped);
|
|
@@ -525,6 +587,7 @@ function tryAcquireClaim({ root, mode, details = {}, label = "ownership" }) {
|
|
|
525
587
|
held: holder,
|
|
526
588
|
reaped
|
|
527
589
|
};
|
|
590
|
+
if (denied) throw new ClaimUnavailableError(`access was denied on every one of ${PUBLISH_ATTEMPTS} attempts to publish it`, { claimPath: root });
|
|
528
591
|
refuse(root, root, label, `another process took and gave up the claim on every one of ${PUBLISH_ATTEMPTS} attempts to take it`);
|
|
529
592
|
}
|
|
530
593
|
function settleClaim(pending) {
|
package/dist/ownership-claim.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as releaseClaim, a as claimFailure, b as sharedClaimDir, c as clearClaimChild, d as inspectClaimSet, f as isClaimRefusal, g as readClaimSet, h as processGroupAlive, i as ClaimUnavailableError, l as clearFreeClaimSet, m as markClaimChildPending, n as CLAIM_UNAVAILABLE, o as claimLiveness, p as isClaimUnavailable, r as ClaimRefusedError, s as claimRemoveCommand, t as CLAIM_REFUSED, u as exclusiveClaimDir, v as setClaimChild, x as tryAcquireClaim, y as settleClaim } from "./ownership-claim-
|
|
1
|
+
import { _ as releaseClaim, a as claimFailure, b as sharedClaimDir, c as clearClaimChild, d as inspectClaimSet, f as isClaimRefusal, g as readClaimSet, h as processGroupAlive, i as ClaimUnavailableError, l as clearFreeClaimSet, m as markClaimChildPending, n as CLAIM_UNAVAILABLE, o as claimLiveness, p as isClaimUnavailable, r as ClaimRefusedError, s as claimRemoveCommand, t as CLAIM_REFUSED, u as exclusiveClaimDir, v as setClaimChild, x as tryAcquireClaim, y as settleClaim } from "./ownership-claim-Cn3NHqBS.mjs";
|
|
2
2
|
export { CLAIM_REFUSED, CLAIM_UNAVAILABLE, ClaimRefusedError, ClaimUnavailableError, claimFailure, claimLiveness, claimRemoveCommand, clearClaimChild, clearFreeClaimSet, exclusiveClaimDir, inspectClaimSet, isClaimRefusal, isClaimUnavailable, markClaimChildPending, processGroupAlive, readClaimSet, releaseClaim, setClaimChild, settleClaim, sharedClaimDir, tryAcquireClaim };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stim-cli/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Shared internal cache and runtime primitives for Stim packages.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"unique-pid": "0.1.1"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
|
-
"node": "
|
|
40
|
+
"node": ">=22.12.0"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsdown"
|