@stim-cli/core 1.8.0 → 1.10.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/README.md +6 -0
- package/dist/core-CJWLJIr6.mjs +421 -0
- package/dist/index.d.mts +6 -1
- package/dist/index.mjs +3 -347
- package/dist/{ownership-claim-Cn3NHqBS.mjs → ownership-claim-O31Y5iIb.mjs} +41 -31
- package/dist/ownership-claim.mjs +1 -1
- package/dist/process-identity.d.mts +11 -1
- package/dist/process-identity.mjs +14 -1
- package/dist/state.d.mts +507 -0
- package/dist/state.mjs +1269 -0
- package/package.json +6 -1
package/dist/index.mjs
CHANGED
|
@@ -1,347 +1,3 @@
|
|
|
1
|
-
import { S as quotedPath
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import path, { basename, dirname, join } from "node:path";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
|
-
//#region dir-lock.ts
|
|
7
|
-
const DEFAULT_LOCK_WAIT_MS = 12e3;
|
|
8
|
-
const DEFAULT_LOCK_POLL_MS = 25;
|
|
9
|
-
const lockDepths = /* @__PURE__ */ new Map();
|
|
10
|
-
const OWNER_MARKER = /^\.stim-claim-[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/;
|
|
11
|
-
function sleepSync(ms) {
|
|
12
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
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
|
-
}
|
|
22
|
-
function createLockDirectory(lockPath) {
|
|
23
|
-
try {
|
|
24
|
-
mkdirSync(lockPath);
|
|
25
|
-
return true;
|
|
26
|
-
} catch (error) {
|
|
27
|
-
if (error?.code === "EEXIST" || removalInFlight(error)) return false;
|
|
28
|
-
throw error;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
function removeLockDirectory(lockPath, marker) {
|
|
32
|
-
try {
|
|
33
|
-
unlinkSync(marker);
|
|
34
|
-
} catch (error) {
|
|
35
|
-
if (error?.code === "ENOENT" || removalInFlight(error)) return false;
|
|
36
|
-
throw error;
|
|
37
|
-
}
|
|
38
|
-
try {
|
|
39
|
-
rmdirSync(lockPath);
|
|
40
|
-
return true;
|
|
41
|
-
} catch (error) {
|
|
42
|
-
if ([
|
|
43
|
-
"ENOENT",
|
|
44
|
-
"ENOTEMPTY",
|
|
45
|
-
"EEXIST"
|
|
46
|
-
].includes(error?.code ?? "")) return false;
|
|
47
|
-
if (removalInFlight(error)) return false;
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function takeLockDirectory(lockPath, claim) {
|
|
52
|
-
if (!createLockDirectory(lockPath)) {
|
|
53
|
-
let entries;
|
|
54
|
-
try {
|
|
55
|
-
entries = readdirSync(lockPath);
|
|
56
|
-
} catch (error) {
|
|
57
|
-
if (error?.code === "ENOENT" || removalInFlight(error)) return null;
|
|
58
|
-
throw error;
|
|
59
|
-
}
|
|
60
|
-
const name = entries.length === 1 ? entries[0] : void 0;
|
|
61
|
-
if (!name || !OWNER_MARKER.test(name)) return null;
|
|
62
|
-
const marker = join(lockPath, name);
|
|
63
|
-
let entry;
|
|
64
|
-
try {
|
|
65
|
-
entry = lstatSync(marker);
|
|
66
|
-
} catch (error) {
|
|
67
|
-
if (error?.code === "ENOENT" || removalInFlight(error)) return null;
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
if (!entry.isFile() || entry.size !== 0) throw new ClaimRefusedError({
|
|
71
|
-
root: lockPath,
|
|
72
|
-
claimPath: lockPath,
|
|
73
|
-
label: "directory lock",
|
|
74
|
-
reason: `its compatibility marker ${name} is not an empty file`
|
|
75
|
-
});
|
|
76
|
-
if (!removeLockDirectory(lockPath, marker) || !createLockDirectory(lockPath)) return null;
|
|
77
|
-
}
|
|
78
|
-
const marker = join(lockPath, `.stim-claim-${claim.claimId}`);
|
|
79
|
-
writeFileSync(marker, "", { flag: "wx" });
|
|
80
|
-
return marker;
|
|
81
|
-
}
|
|
82
|
-
function acquireDirLock(lockPath, { waitMs, pollMs, ensureParent }) {
|
|
83
|
-
ensureParent?.();
|
|
84
|
-
statSync(dirname(lockPath));
|
|
85
|
-
const deadline = Date.now() + waitMs;
|
|
86
|
-
let claim;
|
|
87
|
-
try {
|
|
88
|
-
for (;;) {
|
|
89
|
-
if (!claim) {
|
|
90
|
-
const attempt = tryAcquireClaim({
|
|
91
|
-
root: `${lockPath}.claims`,
|
|
92
|
-
mode: "exclusive",
|
|
93
|
-
label: "directory lock"
|
|
94
|
-
});
|
|
95
|
-
claim = attempt.acquired;
|
|
96
|
-
if (attempt.pending) releaseClaim(attempt.pending);
|
|
97
|
-
}
|
|
98
|
-
if (claim) {
|
|
99
|
-
const marker = takeLockDirectory(lockPath, claim);
|
|
100
|
-
if (marker) return {
|
|
101
|
-
claim,
|
|
102
|
-
marker
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
if (Date.now() >= deadline) {
|
|
106
|
-
const error = /* @__PURE__ */ new Error(`Timed out waiting for the lock at ${lockPath}. Another Stim process may be holding it; if none is running, remove that directory.`);
|
|
107
|
-
error.code = "STIM_LOCK_TIMEOUT";
|
|
108
|
-
error.lockPath = lockPath;
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
sleepSync(pollMs);
|
|
112
|
-
}
|
|
113
|
-
} catch (error) {
|
|
114
|
-
releaseClaim(claim);
|
|
115
|
-
throw error;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
function withDirLock(lockPath, fn, { waitMs = DEFAULT_LOCK_WAIT_MS, pollMs = DEFAULT_LOCK_POLL_MS, ensureParent } = {}) {
|
|
119
|
-
const depth = lockDepths.get(lockPath) || 0;
|
|
120
|
-
if (depth > 0) {
|
|
121
|
-
lockDepths.set(lockPath, depth + 1);
|
|
122
|
-
try {
|
|
123
|
-
return fn();
|
|
124
|
-
} finally {
|
|
125
|
-
lockDepths.set(lockPath, (lockDepths.get(lockPath) ?? 1) - 1);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
const { claim, marker } = acquireDirLock(lockPath, {
|
|
129
|
-
waitMs,
|
|
130
|
-
pollMs,
|
|
131
|
-
ensureParent
|
|
132
|
-
});
|
|
133
|
-
lockDepths.set(lockPath, 1);
|
|
134
|
-
try {
|
|
135
|
-
return fn();
|
|
136
|
-
} finally {
|
|
137
|
-
lockDepths.set(lockPath, 0);
|
|
138
|
-
try {
|
|
139
|
-
removeLockDirectory(lockPath, marker);
|
|
140
|
-
} catch {}
|
|
141
|
-
releaseClaim(claim);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
//#endregion
|
|
145
|
-
//#region artifact-store.ts
|
|
146
|
-
function artifactIn(dir) {
|
|
147
|
-
if (!existsSync(dir)) return null;
|
|
148
|
-
let found;
|
|
149
|
-
try {
|
|
150
|
-
found = readdirSync(dir).find((f) => f.endsWith(".app") || f.endsWith(".apk"));
|
|
151
|
-
} catch {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
return found ? join(dir, found) : null;
|
|
155
|
-
}
|
|
156
|
-
function resolveArtifact(dir) {
|
|
157
|
-
const hit = artifactIn(dir);
|
|
158
|
-
if (!hit) return null;
|
|
159
|
-
try {
|
|
160
|
-
utimesSync(dir, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
|
|
161
|
-
} catch {}
|
|
162
|
-
return hit;
|
|
163
|
-
}
|
|
164
|
-
function storeArtifact(dest, buildPath, { runFile, overwrite = false, writeMetadata, onRenameError }) {
|
|
165
|
-
const existing = artifactIn(dest);
|
|
166
|
-
if (existing && !overwrite) return existing;
|
|
167
|
-
const staging = `${dest}.staging-${process.pid}`;
|
|
168
|
-
rmSync(staging, {
|
|
169
|
-
recursive: true,
|
|
170
|
-
force: true
|
|
171
|
-
});
|
|
172
|
-
mkdirSync(staging, { recursive: true });
|
|
173
|
-
try {
|
|
174
|
-
runFile("cp", [
|
|
175
|
-
"-c",
|
|
176
|
-
"-R",
|
|
177
|
-
buildPath,
|
|
178
|
-
join(staging, basename(buildPath))
|
|
179
|
-
]);
|
|
180
|
-
} catch {
|
|
181
|
-
runFile("cp", [
|
|
182
|
-
"-R",
|
|
183
|
-
buildPath,
|
|
184
|
-
join(staging, basename(buildPath))
|
|
185
|
-
]);
|
|
186
|
-
}
|
|
187
|
-
writeMetadata?.(staging);
|
|
188
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
189
|
-
rmSync(dest, {
|
|
190
|
-
recursive: true,
|
|
191
|
-
force: true
|
|
192
|
-
});
|
|
193
|
-
try {
|
|
194
|
-
renameSync(staging, dest);
|
|
195
|
-
} catch (error) {
|
|
196
|
-
if (!onRenameError) throw error;
|
|
197
|
-
onRenameError(staging);
|
|
198
|
-
}
|
|
199
|
-
return artifactIn(dest);
|
|
200
|
-
}
|
|
201
|
-
//#endregion
|
|
202
|
-
//#region index.ts
|
|
203
|
-
function configDir() {
|
|
204
|
-
return process.env.STIM_HOME || path.join(os.homedir(), ".stim");
|
|
205
|
-
}
|
|
206
|
-
function workspaceSlug(projectRoot) {
|
|
207
|
-
return path.basename(path.resolve(projectRoot)).normalize("NFKD").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase().slice(0, 48) || "workspace";
|
|
208
|
-
}
|
|
209
|
-
function workspaceId(projectRoot) {
|
|
210
|
-
return createHash("sha256").update(path.resolve(projectRoot)).digest("hex").slice(0, 16);
|
|
211
|
-
}
|
|
212
|
-
function workspaceName(projectRoot) {
|
|
213
|
-
return `${workspaceSlug(projectRoot)}--${workspaceId(projectRoot)}`;
|
|
214
|
-
}
|
|
215
|
-
function workspaceStateDir(projectRoot) {
|
|
216
|
-
return path.join(configDir(), "workspaces", workspaceName(projectRoot));
|
|
217
|
-
}
|
|
218
|
-
function workspaceLogDir(projectRoot) {
|
|
219
|
-
return path.join(workspaceStateDir(projectRoot), "logs");
|
|
220
|
-
}
|
|
221
|
-
function cachePathSetting(key) {
|
|
222
|
-
try {
|
|
223
|
-
const value = JSON.parse(fs.readFileSync(path.join(configDir(), "config.json"), "utf-8"))?.caches?.[key];
|
|
224
|
-
return typeof value === "string" && path.isAbsolute(value) ? value : null;
|
|
225
|
-
} catch {
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
function cacheNameSegment(name) {
|
|
230
|
-
return String(name).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^\.+/, "") || "app";
|
|
231
|
-
}
|
|
232
|
-
function buildCacheRoot() {
|
|
233
|
-
return process.env.STIM_BUILD_CACHE || cachePathSetting("buildCache") || path.join(configDir(), "build-cache");
|
|
234
|
-
}
|
|
235
|
-
function metroCacheRoot(name) {
|
|
236
|
-
const root = process.env.STIM_METRO_CACHE || cachePathSetting("metroCache") || path.join(configDir(), "metro-cache");
|
|
237
|
-
return name === void 0 || name === null || name === "" ? root : path.join(root, cacheNameSegment(name));
|
|
238
|
-
}
|
|
239
|
-
const STORE_ROOT_TAG = "stimStoreRoot";
|
|
240
|
-
function tagSharedStore(store, root) {
|
|
241
|
-
try {
|
|
242
|
-
Object.defineProperty(store, STORE_ROOT_TAG, {
|
|
243
|
-
value: root,
|
|
244
|
-
enumerable: false,
|
|
245
|
-
configurable: true
|
|
246
|
-
});
|
|
247
|
-
} catch {}
|
|
248
|
-
return store;
|
|
249
|
-
}
|
|
250
|
-
function sharedStoreRoot(store) {
|
|
251
|
-
if (store === null || typeof store !== "object") return null;
|
|
252
|
-
const tagged = store[STORE_ROOT_TAG];
|
|
253
|
-
if (typeof tagged === "string") return tagged;
|
|
254
|
-
const legacy = store._root;
|
|
255
|
-
return typeof legacy === "string" ? legacy : null;
|
|
256
|
-
}
|
|
257
|
-
const SIMULATOR_UDID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
258
|
-
const EMULATOR_SERIAL = /^emulator-\d+$/;
|
|
259
|
-
function slug(value) {
|
|
260
|
-
return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
261
|
-
}
|
|
262
|
-
function buildVariant(platform, options) {
|
|
263
|
-
const raw = platform === "android" ? options.variant : options.configuration ?? options.buildConfiguration;
|
|
264
|
-
return (typeof raw === "string" ? slug(raw) : "") || "debug";
|
|
265
|
-
}
|
|
266
|
-
function buildTarget(options) {
|
|
267
|
-
if (typeof options.isSimulator === "boolean") return options.isSimulator ? "sim" : "device";
|
|
268
|
-
const device = options.device;
|
|
269
|
-
if (device === void 0 || device === null || device === false) return "sim";
|
|
270
|
-
if (typeof device !== "string") return "prompted";
|
|
271
|
-
const name = device.trim();
|
|
272
|
-
if (name === "" || name === "generic") return "sim";
|
|
273
|
-
if (SIMULATOR_UDID.test(name) || EMULATOR_SERIAL.test(name)) return "sim";
|
|
274
|
-
return `on-${slug(name)}`;
|
|
275
|
-
}
|
|
276
|
-
function buildCacheKey(platform, fingerprintHash, options = {}) {
|
|
277
|
-
const opts = options && typeof options === "object" ? options : {};
|
|
278
|
-
const abi = platform === "android" && typeof opts.abi === "string" ? slug(opts.abi) : "";
|
|
279
|
-
const compiler = typeof opts.compiler === "string" ? slug(opts.compiler) : "";
|
|
280
|
-
const profile = typeof opts.buildProfile === "string" ? slug(opts.buildProfile) : "";
|
|
281
|
-
const scheme = platform === "ios" && typeof opts.scheme === "string" && opts.scheme ? createHash("sha256").update(opts.scheme).digest("hex") : "";
|
|
282
|
-
return `${fingerprintHash}-${buildVariant(platform, opts)}-${buildTarget(opts)}${abi ? `-${abi}` : ""}${compiler ? `-${compiler}` : ""}${profile ? `-${profile}` : ""}${scheme ? `-scheme-${scheme}` : ""}`;
|
|
283
|
-
}
|
|
284
|
-
const METRO_NAMED_CACHE_LAYOUT = "metro-named-v1";
|
|
285
|
-
function cacheManifestLockPath(file) {
|
|
286
|
-
return `${file}.lock`;
|
|
287
|
-
}
|
|
288
|
-
function readCacheManifest(file) {
|
|
289
|
-
try {
|
|
290
|
-
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
291
|
-
return {
|
|
292
|
-
version: 1,
|
|
293
|
-
caches: Array.isArray(parsed?.caches) ? parsed.caches : []
|
|
294
|
-
};
|
|
295
|
-
} catch {
|
|
296
|
-
return {
|
|
297
|
-
version: 1,
|
|
298
|
-
caches: []
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
function updateCacheManifest(file, mutate) {
|
|
303
|
-
const parent = path.dirname(file);
|
|
304
|
-
return withDirLock(cacheManifestLockPath(file), () => {
|
|
305
|
-
const next = {
|
|
306
|
-
version: 1,
|
|
307
|
-
caches: mutate(readCacheManifest(file).caches)
|
|
308
|
-
};
|
|
309
|
-
const temporary = path.join(parent, `.${path.basename(file)}.${process.pid}.tmp`);
|
|
310
|
-
try {
|
|
311
|
-
fs.writeFileSync(temporary, JSON.stringify(next, null, 2));
|
|
312
|
-
fs.renameSync(temporary, file);
|
|
313
|
-
} catch (error) {
|
|
314
|
-
fs.rmSync(temporary, { force: true });
|
|
315
|
-
throw error;
|
|
316
|
-
}
|
|
317
|
-
return next;
|
|
318
|
-
}, { ensureParent: () => fs.mkdirSync(parent, { recursive: true }) });
|
|
319
|
-
}
|
|
320
|
-
function registerCache({ dir, name, prune, note, entriesDepth, layout, replaces = [] }) {
|
|
321
|
-
try {
|
|
322
|
-
updateCacheManifest(path.join(configDir(), "caches.json"), (caches) => {
|
|
323
|
-
const others = caches.filter((cache) => cache.dir !== dir && !replaces.some((match) => matchesCache(cache, match)));
|
|
324
|
-
const record = {
|
|
325
|
-
dir,
|
|
326
|
-
name,
|
|
327
|
-
prune,
|
|
328
|
-
note,
|
|
329
|
-
registeredBy: process.cwd()
|
|
330
|
-
};
|
|
331
|
-
if (entriesDepth) record.entriesDepth = entriesDepth;
|
|
332
|
-
if (layout) record.layout = layout;
|
|
333
|
-
others.push(record);
|
|
334
|
-
return others;
|
|
335
|
-
});
|
|
336
|
-
} catch {}
|
|
337
|
-
}
|
|
338
|
-
function matchesCache(cache, match) {
|
|
339
|
-
if (cache.dir !== match.dir) return false;
|
|
340
|
-
if (match.name !== void 0 && cache.name !== match.name) return false;
|
|
341
|
-
if (match.prune !== void 0 && cache.prune !== match.prune) return false;
|
|
342
|
-
if (match.entriesDepth !== void 0 && cache.entriesDepth !== match.entriesDepth) return false;
|
|
343
|
-
if (match.layout === null) return !Object.hasOwn(cache, "layout");
|
|
344
|
-
return match.layout === void 0 || cache.layout === match.layout;
|
|
345
|
-
}
|
|
346
|
-
//#endregion
|
|
347
|
-
export { METRO_NAMED_CACHE_LAYOUT, STORE_ROOT_TAG, artifactIn, buildCacheKey, buildCacheRoot, cacheManifestLockPath, cacheNameSegment, cachePathSetting, configDir, metroCacheRoot, quotedPath, readCacheManifest, registerCache, resolveArtifact, sharedStoreRoot, storeArtifact, tagSharedStore, updateCacheManifest, withDirLock, workspaceId, workspaceLogDir, workspaceName, workspaceSlug, workspaceStateDir };
|
|
1
|
+
import { S as quotedPath } from "./ownership-claim-O31Y5iIb.mjs";
|
|
2
|
+
import { C as artifactIn, E as withDirLock, S as workspaceStateDir, T as storeArtifact, _ as updateCacheManifest, a as buildCacheRoot, b as workspaceName, c as cachePathSetting, d as readCacheManifest, f as registerCache, g as tagSharedStore, h as sharedStoreRoot, i as buildCacheKey, l as configDir, m as rotatedLogPath, n as METRO_NAMED_CACHE_LAYOUT, o as cacheManifestLockPath, p as rotateLog, r as STORE_ROOT_TAG, s as cacheNameSegment, t as LOG_ROTATE_BYTES, u as metroCacheRoot, v as workspaceId, w as resolveArtifact, x as workspaceSlug, y as workspaceLogDir } from "./core-CJWLJIr6.mjs";
|
|
3
|
+
export { LOG_ROTATE_BYTES, METRO_NAMED_CACHE_LAYOUT, STORE_ROOT_TAG, artifactIn, buildCacheKey, buildCacheRoot, cacheManifestLockPath, cacheNameSegment, cachePathSetting, configDir, metroCacheRoot, quotedPath, readCacheManifest, registerCache, resolveArtifact, rotateLog, rotatedLogPath, sharedStoreRoot, storeArtifact, tagSharedStore, updateCacheManifest, withDirLock, workspaceId, workspaceLogDir, workspaceName, workspaceSlug, workspaceStateDir };
|
|
@@ -18,7 +18,7 @@ const CLAIM_SUFFIX = ".claim";
|
|
|
18
18
|
const CHILD_SUFFIX = ".child";
|
|
19
19
|
const STAGING_PREFIX = ".staging-";
|
|
20
20
|
const PUBLISH_ATTEMPTS = 64;
|
|
21
|
-
const WIN32_SETTLE_MS =
|
|
21
|
+
const WIN32_SETTLE_MS = 2e3;
|
|
22
22
|
const WIN32_SETTLE_STEP_MS = 5;
|
|
23
23
|
const debug = debuglog("stim:claim");
|
|
24
24
|
var ClaimRefusedError = class extends Error {
|
|
@@ -99,61 +99,71 @@ function sleepSync(ms) {
|
|
|
99
99
|
* Run a read, listing or removal and resolve the one answer Windows leaves ambiguous. NTFS answers
|
|
100
100
|
* ERROR_ACCESS_DENIED, which Node reports as EPERM, for a name whose removal by another process is still
|
|
101
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)
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
102
|
+
* with 12 processes on 4 cores (https://github.com/appandflow/stim/issues/883), and within 986 ms on an
|
|
103
|
+
* oversubscribed hosted windows-latest runner (https://github.com/appandflow/stim/issues/943). The call
|
|
104
|
+
* is reissued in small steps until a call issued WIN32_SETTLE_MS after the first EPERM still answers
|
|
105
|
+
* EPERM, so a call that was itself descheduled past the window never ends it: a result or ENOENT is the
|
|
106
|
+
* settled answer, and an EPERM that outlasts the window is a real denial, thrown exactly as a POSIX
|
|
107
|
+
* EACCES is. This bounds the resolution of an ambiguous OS answer; it never waits on a holder.
|
|
106
108
|
*/
|
|
107
109
|
function settledAnswer(path, call) {
|
|
108
110
|
if (process.platform !== "win32") return call();
|
|
109
|
-
|
|
111
|
+
let denied;
|
|
110
112
|
for (;;) {
|
|
113
|
+
const asked = Date.now();
|
|
111
114
|
try {
|
|
112
115
|
const result = call();
|
|
113
|
-
if (
|
|
116
|
+
if (denied !== void 0) debug("%s settled after %d ms", path, Date.now() - denied);
|
|
114
117
|
return result;
|
|
115
118
|
} catch (err) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
if (err?.code !== "EPERM") throw err;
|
|
120
|
+
denied ??= Date.now();
|
|
121
|
+
if (asked - denied >= WIN32_SETTLE_MS) {
|
|
122
|
+
debug("%s still answers EPERM after %d ms", path, Date.now() - denied);
|
|
119
123
|
throw err;
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
126
|
sleepSync(WIN32_SETTLE_STEP_MS);
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
var Unreadable = class {
|
|
130
|
+
reason;
|
|
131
|
+
constructor(reason) {
|
|
132
|
+
this.reason = reason;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const NOT_JSON = new Unreadable("its record is missing, truncated or not valid JSON");
|
|
136
|
+
function readRecord(path, what) {
|
|
127
137
|
try {
|
|
128
|
-
|
|
138
|
+
return settledAnswer(path, () => readFileSync(path, "utf-8"));
|
|
129
139
|
} catch (err) {
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
const code = err?.code;
|
|
141
|
+
if (code === "ENOENT") return null;
|
|
142
|
+
return new Unreadable(`its ${what} could not be read (${code || err?.message})`);
|
|
132
143
|
}
|
|
144
|
+
}
|
|
145
|
+
function readChild(claimPath) {
|
|
146
|
+
const text = readRecord(childPath(claimPath), "child record");
|
|
147
|
+
if (text === null || text instanceof Unreadable) return text;
|
|
133
148
|
try {
|
|
134
149
|
return { record: asOwner(JSON.parse(text)?.record) };
|
|
135
150
|
} catch {
|
|
136
|
-
return
|
|
151
|
+
return NOT_JSON;
|
|
137
152
|
}
|
|
138
153
|
}
|
|
139
154
|
function readClaim(path, mode) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
text = settledAnswer(path, () => readFileSync(path, "utf-8"));
|
|
143
|
-
} catch (err) {
|
|
144
|
-
if (err?.code === "ENOENT") return null;
|
|
145
|
-
return "unreadable";
|
|
146
|
-
}
|
|
155
|
+
const text = readRecord(path, "record");
|
|
156
|
+
if (text === null || text instanceof Unreadable) return text;
|
|
147
157
|
let parsed;
|
|
148
158
|
try {
|
|
149
159
|
parsed = JSON.parse(text);
|
|
150
160
|
} catch {
|
|
151
|
-
return
|
|
161
|
+
return NOT_JSON;
|
|
152
162
|
}
|
|
153
163
|
const owner = asOwner(parsed?.owner);
|
|
154
|
-
if (!owner || typeof parsed.claimId !== "string" || !parsed.claimId) return
|
|
164
|
+
if (!owner || typeof parsed.claimId !== "string" || !parsed.claimId) return NOT_JSON;
|
|
155
165
|
const child = readChild(path);
|
|
156
|
-
if (child
|
|
166
|
+
if (child instanceof Unreadable) return child;
|
|
157
167
|
return {
|
|
158
168
|
path,
|
|
159
169
|
claimId: parsed.claimId,
|
|
@@ -183,7 +193,7 @@ function processGroupAlive(pid) {
|
|
|
183
193
|
function childLiveness(claimPath) {
|
|
184
194
|
const child = readChild(claimPath);
|
|
185
195
|
if (child === null) return "none";
|
|
186
|
-
if (child
|
|
196
|
+
if (child instanceof Unreadable || !child.record) return "unknown";
|
|
187
197
|
const status = inspectProcessIdentity(child.record);
|
|
188
198
|
if (status === "same") return "live";
|
|
189
199
|
if (status === "gone") return processGroupAlive(child.record.pid) ? "live" : "dead";
|
|
@@ -252,10 +262,10 @@ function survey(dir, mode, into) {
|
|
|
252
262
|
const path = join(dir, name);
|
|
253
263
|
const holder = readClaim(path, mode);
|
|
254
264
|
if (holder === null) continue;
|
|
255
|
-
if (holder
|
|
265
|
+
if (holder instanceof Unreadable) {
|
|
256
266
|
into.unresolved.push({
|
|
257
267
|
path,
|
|
258
|
-
reason:
|
|
268
|
+
reason: holder.reason
|
|
259
269
|
});
|
|
260
270
|
continue;
|
|
261
271
|
}
|
|
@@ -296,7 +306,7 @@ function removeOrRefuse(path, root, claimPath, label) {
|
|
|
296
306
|
function reap(holder, root, label) {
|
|
297
307
|
const again = readClaim(holder.path, holder.mode);
|
|
298
308
|
if (again === null) return true;
|
|
299
|
-
if (again
|
|
309
|
+
if (again instanceof Unreadable || again.claimId !== holder.claimId) return false;
|
|
300
310
|
removeOrRefuse(childPath(holder.path), root, holder.path, label);
|
|
301
311
|
removeOrRefuse(holder.path, root, holder.path, label);
|
|
302
312
|
return true;
|
|
@@ -606,7 +616,7 @@ function releaseClaim(handle) {
|
|
|
606
616
|
if (!handle) return false;
|
|
607
617
|
const current = readClaim(handle.path, handle.mode);
|
|
608
618
|
if (current === null) return true;
|
|
609
|
-
if (current
|
|
619
|
+
if (current instanceof Unreadable || current.claimId !== handle.claimId) return false;
|
|
610
620
|
try {
|
|
611
621
|
rmSync(childPath(handle.path), { force: true });
|
|
612
622
|
rmSync(handle.path, { force: true });
|
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-O31Y5iIb.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 };
|
|
@@ -15,6 +15,16 @@ declare function captureProcessIdentity(pid: number): CapturedIdentity;
|
|
|
15
15
|
declare function captureProcessToken(pid: number): string | null;
|
|
16
16
|
type ProcessIdentityStatus = "same" | "different" | "gone" | "unknown";
|
|
17
17
|
declare function inspectProcessIdentity(record: ProcessRecord | null | undefined): ProcessIdentityStatus;
|
|
18
|
+
type ProcessStart = {
|
|
19
|
+
status: "running";
|
|
20
|
+
startedAtMs: number;
|
|
21
|
+
} | {
|
|
22
|
+
status: "gone";
|
|
23
|
+
} | {
|
|
24
|
+
status: "unknown";
|
|
25
|
+
};
|
|
26
|
+
/** Start time of a live process, read from the same OS identity that `captureProcessIdentity` records. */
|
|
27
|
+
declare function inspectProcessStart(pid: number): ProcessStart;
|
|
18
28
|
declare function waitForProcessExit(record: ProcessRecord, timeoutMs: number): Promise<boolean>;
|
|
19
29
|
//#endregion
|
|
20
|
-
export { CapturedIdentity, ProcessIdentityStatus, ProcessRecord, captureProcessIdentity, captureProcessToken, inspectProcessIdentity, sameProcessRecord, waitForProcessExit };
|
|
30
|
+
export { CapturedIdentity, ProcessIdentityStatus, ProcessRecord, ProcessStart, captureProcessIdentity, captureProcessToken, inspectProcessIdentity, inspectProcessStart, sameProcessRecord, waitForProcessExit };
|
|
@@ -26,6 +26,19 @@ function inspectProcessIdentity(record) {
|
|
|
26
26
|
const result = check(record.processToken);
|
|
27
27
|
return result.ok ? result.value : "unknown";
|
|
28
28
|
}
|
|
29
|
+
/** Start time of a live process, read from the same OS identity that `captureProcessIdentity` records. */
|
|
30
|
+
function inspectProcessStart(pid) {
|
|
31
|
+
const captured = capture(pid);
|
|
32
|
+
if (!captured.ok) return captured.error.code === "NOT_FOUND" ? { status: "gone" } : { status: "unknown" };
|
|
33
|
+
const decoded = decode(captured.value);
|
|
34
|
+
if (!decoded.ok || decoded.value.platform !== "darwin") return { status: "unknown" };
|
|
35
|
+
const match = /^(\d+):(\d+)$/.exec(decoded.value.startTime);
|
|
36
|
+
if (!match) return { status: "unknown" };
|
|
37
|
+
return {
|
|
38
|
+
status: "running",
|
|
39
|
+
startedAtMs: Number(match[1]) * 1e3 + Math.floor(Number(match[2]) / 1e3)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
29
42
|
async function waitForProcessExit(record, timeoutMs) {
|
|
30
43
|
const deadline = Date.now() + timeoutMs;
|
|
31
44
|
while (Date.now() <= deadline) {
|
|
@@ -37,4 +50,4 @@ async function waitForProcessExit(record, timeoutMs) {
|
|
|
37
50
|
return false;
|
|
38
51
|
}
|
|
39
52
|
//#endregion
|
|
40
|
-
export { captureProcessIdentity, captureProcessToken, inspectProcessIdentity, sameProcessRecord, waitForProcessExit };
|
|
53
|
+
export { captureProcessIdentity, captureProcessToken, inspectProcessIdentity, inspectProcessStart, sameProcessRecord, waitForProcessExit };
|