@napi-rs/cli 3.9.1 → 3.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 +17 -4
- package/dist/cli.js +1490 -178
- package/dist/index.cjs +1490 -178
- package/dist/index.d.cts +68 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.js +1490 -178
- package/docs/wasi.md +318 -4
- package/package.json +3 -3
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +3963 -9
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/build-regressions.spec.ts +210 -3
- package/src/api/__tests__/build.spec.ts +2139 -3
- package/src/api/__tests__/create-npm-dirs.spec.ts +101 -0
- package/src/api/__tests__/pre-publish.spec.ts +153 -1
- package/src/api/__tests__/templates.spec.ts +1247 -0
- package/src/api/build.ts +637 -31
- package/src/api/create-npm-dirs.ts +21 -9
- package/src/api/pre-publish.ts +29 -1
- package/src/api/templates/binding-target.ts +176 -0
- package/src/api/templates/index.ts +1 -0
- package/src/api/templates/js-binding.ts +48 -3
- package/src/api/templates/load-wasi-template.ts +641 -41
- package/src/utils/__tests__/reconciliation.spec.ts +676 -0
- package/src/utils/config.ts +50 -0
- package/src/utils/misc.ts +347 -74
- package/src/utils/typegen.ts +367 -12
package/src/utils/config.ts
CHANGED
|
@@ -95,6 +95,27 @@ export interface UserNapiConfig {
|
|
|
95
95
|
* @default 4000 pages (250 MiB), or 1024 pages (64 MiB) for the deferred workerd loader
|
|
96
96
|
*/
|
|
97
97
|
initialMemory?: number
|
|
98
|
+
/**
|
|
99
|
+
* Initial linear memory, in 64 KiB pages, for the **threadless**
|
|
100
|
+
* (`wasm32-wasip1`) loaders only: the Node CJS loader, the browser loader
|
|
101
|
+
* and the deferred `./workerd` loader.
|
|
102
|
+
*
|
|
103
|
+
* The threaded loaders allocate one `shared: true` memory that every
|
|
104
|
+
* wasi-threads worker maps, so every worker stack and every thread's
|
|
105
|
+
* allocations come out of it and growing it is a cross-thread event; it is
|
|
106
|
+
* sized for the whole pool up front. The threadless loaders drive a single
|
|
107
|
+
* thread through a plain growable `ArrayBuffer`: they only have to clear
|
|
108
|
+
* the module's own link-time floor (`-zstack-size` plus static data) and
|
|
109
|
+
* grow on demand, which is what lets the same build fit a host with a hard
|
|
110
|
+
* isolate cap such as workerd's 128 MiB.
|
|
111
|
+
*
|
|
112
|
+
* Falls back to `initialMemory` when unset, so existing projects are
|
|
113
|
+
* unaffected.
|
|
114
|
+
*
|
|
115
|
+
* @default `initialMemory` (4000 pages), or 1024 pages for the deferred
|
|
116
|
+
* `./workerd` loader
|
|
117
|
+
*/
|
|
118
|
+
threadlessInitialMemory?: number
|
|
98
119
|
/**
|
|
99
120
|
* @default 65536 pages (4GiB)
|
|
100
121
|
*/
|
|
@@ -119,6 +140,35 @@ export interface UserNapiConfig {
|
|
|
119
140
|
*/
|
|
120
141
|
optionalDependency?: boolean
|
|
121
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Whether the generated WASI loaders bootstrap the `napi-async-runtime`
|
|
145
|
+
* CurrentThread JavaScript hosts.
|
|
146
|
+
*
|
|
147
|
+
* A binding built with the `napi-async-runtime` crate runs the
|
|
148
|
+
* `CurrentThread` flavor on every WebAssembly target (it is the only
|
|
149
|
+
* flavor there). Its futures make no progress until a task host is
|
|
150
|
+
* registered, and its timers never fire until a timer host is, so the
|
|
151
|
+
* loader must install both right after instantiation and unregister them
|
|
152
|
+
* before the environment is destroyed.
|
|
153
|
+
*
|
|
154
|
+
* Enabling this emits that bootstrap in the node, browser and deferred
|
|
155
|
+
* (workerd) loaders, and declares `@napi-rs/async-runtime` in the
|
|
156
|
+
* generated `<packageName>-wasm32-*` packages. The loader delegates the
|
|
157
|
+
* whole host contract — the seven exports, contract version 4, the
|
|
158
|
+
* reservation identity, the liveness probe and the rollback — to that
|
|
159
|
+
* package, so a binding that does not actually expose the contract fails
|
|
160
|
+
* loudly at load with `ERR_NAPI_ASYNC_RUNTIME_BINDING_MISMATCH`.
|
|
161
|
+
*
|
|
162
|
+
* The bootstrap runs after instantiation, so after `#[module_init]`: the
|
|
163
|
+
* host registration functions are exports of the binding itself. Code that
|
|
164
|
+
* runs during module registration must not create timers (`sleep_until`
|
|
165
|
+
* fails loud at creation without a timer host) and cannot expect task
|
|
166
|
+
* progress until the loader has returned.
|
|
167
|
+
*
|
|
168
|
+
* @default false
|
|
169
|
+
*/
|
|
170
|
+
asyncRuntime?: boolean
|
|
171
|
+
|
|
122
172
|
/**
|
|
123
173
|
* Browser wasm binding configuration
|
|
124
174
|
*/
|
package/src/utils/misc.ts
CHANGED
|
@@ -64,13 +64,25 @@ const reconciliationLockKind = 'napi-rs-filesystem-reconciliation-lock'
|
|
|
64
64
|
const reconciliationReclaimKind = 'napi-rs-filesystem-reconciliation-reclaim'
|
|
65
65
|
const reconciliationStateVersion = 1
|
|
66
66
|
const reconciliationLockAcquisitionTimeout = 120_000
|
|
67
|
+
// Bounds how long acquisition rides out fast probe failures before degrading
|
|
68
|
+
// to lock-free operation. Slow-but-working probes are unaffected: a read that
|
|
69
|
+
// returns a complete identity always wins, regardless of this budget.
|
|
70
|
+
const reconciliationIdentityWaitTimeout = 10_000
|
|
67
71
|
const reconciliationLockCleanupTimeout = 5_000
|
|
68
72
|
const reconciliationLockCleanupRetryInterval = 250
|
|
69
73
|
const reconciliationMetadataMaximumSize = 64 * 1024
|
|
70
74
|
const processIncarnationCommandTimeout = 2_000
|
|
75
|
+
// One-shot host identity probes run once per process and must outlive slow
|
|
76
|
+
// first-start tools (e.g. powershell cold start on fresh CI VMs); per-PID
|
|
77
|
+
// incarnation probes run in poll loops and keep the shorter timeout.
|
|
78
|
+
const processExecutionIdentityCommandTimeout = 15_000
|
|
71
79
|
const incompleteProcessExecutionIdentityCacheDuration =
|
|
72
80
|
processIncarnationCommandTimeout
|
|
73
81
|
const processIncarnationObservationCacheDuration = 1_000
|
|
82
|
+
// A failed current-process incarnation probe (e.g. powershell hitting its
|
|
83
|
+
// kill timeout on a loaded host) is retried at most this often, so a burst
|
|
84
|
+
// of sequential lock acquisitions pays one probe instead of one each.
|
|
85
|
+
const processIncarnationProbeRetryCooldown = 1_000
|
|
74
86
|
const fileSystemTransactionJournalName = '.napi-rs-filesystem-transaction.swp'
|
|
75
87
|
const fileSystemTransactionCandidateMarker = '.candidate.'
|
|
76
88
|
const fileSystemTransactionRetiredMarker = '.retired.'
|
|
@@ -149,13 +161,20 @@ interface ProcessIncarnationObservation {
|
|
|
149
161
|
incarnation: string | null
|
|
150
162
|
}
|
|
151
163
|
|
|
152
|
-
interface ProcessExecutionIdentity {
|
|
164
|
+
export interface ProcessExecutionIdentity {
|
|
153
165
|
boot: string | null
|
|
154
166
|
bootSession: string | null
|
|
155
167
|
machine: string | null
|
|
156
168
|
namespace: string | null
|
|
157
169
|
}
|
|
158
170
|
|
|
171
|
+
export interface FileSystemReconciliationOptions {
|
|
172
|
+
getProcessExecutionIdentity?: () => Promise<ProcessExecutionIdentity>
|
|
173
|
+
getProcessIncarnation?: () => Promise<string | null>
|
|
174
|
+
identityWaitTimeout?: number
|
|
175
|
+
lockAcquisitionTimeout?: number
|
|
176
|
+
}
|
|
177
|
+
|
|
159
178
|
interface ProcessOwnerState {
|
|
160
179
|
stale: boolean
|
|
161
180
|
unverifiableReason?: string
|
|
@@ -185,8 +204,6 @@ const processIncarnationObservations = new Map<
|
|
|
185
204
|
number,
|
|
186
205
|
ProcessIncarnationObservation
|
|
187
206
|
>()
|
|
188
|
-
let currentProcessIncarnation: string | undefined
|
|
189
|
-
let currentProcessIncarnationProbe: Promise<string | null> | undefined
|
|
190
207
|
let linuxBootId: string | undefined
|
|
191
208
|
|
|
192
209
|
interface TransactionParentIdentity {
|
|
@@ -347,6 +364,7 @@ export async function copyFileAtomic(
|
|
|
347
364
|
export async function withFileSystemReconciliation<T>(
|
|
348
365
|
path: string,
|
|
349
366
|
operation: () => Promise<T>,
|
|
367
|
+
options?: FileSystemReconciliationOptions,
|
|
350
368
|
): Promise<T> {
|
|
351
369
|
const localKey = resolve(path)
|
|
352
370
|
const previous = reconciliationTails.get(localKey) ?? Promise.resolve()
|
|
@@ -358,21 +376,54 @@ export async function withFileSystemReconciliation<T>(
|
|
|
358
376
|
reconciliationTails.set(localKey, tail)
|
|
359
377
|
|
|
360
378
|
const releaseCrossProcessLocks: Array<() => Promise<void>> = []
|
|
379
|
+
let lockAcquisitionDegraded = false
|
|
361
380
|
let operationFailed = false
|
|
362
381
|
let operationError: unknown
|
|
363
382
|
let result!: T
|
|
364
383
|
try {
|
|
365
384
|
await previous.catch(() => {})
|
|
366
385
|
const identities = await resolveReconciliationLockIdentities(path)
|
|
386
|
+
const injectedIdentityGetter = options?.getProcessExecutionIdentity
|
|
387
|
+
const identityWaitTimeout =
|
|
388
|
+
options?.identityWaitTimeout ?? reconciliationIdentityWaitTimeout
|
|
389
|
+
// Injected getters bypass the degraded-decision memo entirely; only the
|
|
390
|
+
// default singleton path memoizes so one build pays the wait once.
|
|
391
|
+
const resolveExecutionIdentity =
|
|
392
|
+
(): Promise<ProcessExecutionIdentityResolution> =>
|
|
393
|
+
injectedIdentityGetter === undefined
|
|
394
|
+
? resolveDefaultProcessExecutionIdentityForLocking(
|
|
395
|
+
identityWaitTimeout,
|
|
396
|
+
)
|
|
397
|
+
: resolveProcessExecutionIdentityForLocking(
|
|
398
|
+
injectedIdentityGetter,
|
|
399
|
+
createReconciliationLockDeadline(identityWaitTimeout),
|
|
400
|
+
)
|
|
401
|
+
const getProcessExecutionIdentity =
|
|
402
|
+
injectedIdentityGetter ?? getCurrentProcessExecutionIdentity
|
|
403
|
+
const getProcessIncarnation =
|
|
404
|
+
options?.getProcessIncarnation ?? getCurrentProcessIncarnation
|
|
367
405
|
const acquisitionDeadline = createReconciliationLockDeadline(
|
|
368
|
-
reconciliationLockAcquisitionTimeout,
|
|
406
|
+
options?.lockAcquisitionTimeout ?? reconciliationLockAcquisitionTimeout,
|
|
369
407
|
)
|
|
370
408
|
for (const identity of identities) {
|
|
371
|
-
|
|
372
|
-
|
|
409
|
+
const acquisition = await acquireReconciliationLock(
|
|
410
|
+
identity,
|
|
411
|
+
acquisitionDeadline,
|
|
412
|
+
resolveExecutionIdentity,
|
|
413
|
+
getProcessExecutionIdentity,
|
|
414
|
+
getProcessIncarnation,
|
|
373
415
|
)
|
|
416
|
+
releaseCrossProcessLocks.push(acquisition.release)
|
|
417
|
+
lockAcquisitionDegraded ||= acquisition.degraded
|
|
418
|
+
}
|
|
419
|
+
if (!lockAcquisitionDegraded) {
|
|
420
|
+
// Recovery assumes cross-process exclusivity: without a verifiable lock
|
|
421
|
+
// it could roll back a live transaction, so degraded (lock-free) hosts
|
|
422
|
+
// keep pre-3.8.0 behavior and skip it. A genuinely crashed journal then
|
|
423
|
+
// fails the next transaction publish loudly and is recovered by the
|
|
424
|
+
// next run whose identity probes succeed.
|
|
425
|
+
await recoverFileSystemTransaction(identities[0].anchorPath)
|
|
374
426
|
}
|
|
375
|
-
await recoverFileSystemTransaction(identities[0].anchorPath)
|
|
376
427
|
const currentCapability = fileSystemReconciliationCapability.getStore()
|
|
377
428
|
const roots = new Set(currentCapability?.roots)
|
|
378
429
|
roots.add(fileSystemReconciliationCapabilityRoot(identities[0].anchorPath))
|
|
@@ -563,13 +614,18 @@ export async function withPackageFileSystemReconciliation<T>(
|
|
|
563
614
|
'boundary' | 'cwd' | 'packageRoot'
|
|
564
615
|
>,
|
|
565
616
|
operation: () => Promise<T>,
|
|
617
|
+
options?: FileSystemReconciliationOptions,
|
|
566
618
|
): Promise<T> {
|
|
567
619
|
const roots = getPackageReconciliationRoots(paths)
|
|
568
620
|
const acquire = (index: number): Promise<T> => {
|
|
569
621
|
if (index === roots.length) {
|
|
570
622
|
return operation()
|
|
571
623
|
}
|
|
572
|
-
return withFileSystemReconciliation(
|
|
624
|
+
return withFileSystemReconciliation(
|
|
625
|
+
roots[index],
|
|
626
|
+
() => acquire(index + 1),
|
|
627
|
+
options,
|
|
628
|
+
)
|
|
573
629
|
}
|
|
574
630
|
|
|
575
631
|
// Build takes the package lock before widening to its transaction root.
|
|
@@ -1569,16 +1625,25 @@ async function resolveReconciliationLockIdentities(
|
|
|
1569
1625
|
if (!anchorStats.isDirectory()) {
|
|
1570
1626
|
throw reconciliationAnchorError(anchorPath, 'is not a directory', 'ENOTDIR')
|
|
1571
1627
|
}
|
|
1572
|
-
const guardKeys = new
|
|
1628
|
+
const guardKeys = new Map<string, string | undefined>()
|
|
1573
1629
|
if (await directoryIsWritable(dirname(anchorPath))) {
|
|
1574
|
-
guardKeys.
|
|
1630
|
+
guardKeys.set(anchorPath, undefined)
|
|
1575
1631
|
}
|
|
1576
1632
|
let currentPath = requestedPath
|
|
1577
1633
|
while (true) {
|
|
1578
1634
|
const currentStats = await lstatIfExists(currentPath)
|
|
1579
1635
|
if (currentStats?.isSymbolicLink()) {
|
|
1580
|
-
|
|
1581
|
-
|
|
1636
|
+
// Far-ancestor symlink guards scatter locks outside the project (up to
|
|
1637
|
+
// a writable filesystem root, #3444); topology swaps are still caught
|
|
1638
|
+
// fail-safe by the anchor assertions, so a guard is kept only where its
|
|
1639
|
+
// lock stays within the anchor's parent. The lock root is resolved once
|
|
1640
|
+
// here at walk time so the checked root is the root used.
|
|
1641
|
+
const lockRoot = await realpath(dirname(currentPath))
|
|
1642
|
+
if (
|
|
1643
|
+
managedPackagePathIsWithin(dirname(anchorPath), lockRoot) &&
|
|
1644
|
+
(await directoryIsWritable(dirname(currentPath)))
|
|
1645
|
+
) {
|
|
1646
|
+
guardKeys.set(currentPath, lockRoot)
|
|
1582
1647
|
}
|
|
1583
1648
|
}
|
|
1584
1649
|
const parent = dirname(currentPath)
|
|
@@ -1589,8 +1654,8 @@ async function resolveReconciliationLockIdentities(
|
|
|
1589
1654
|
}
|
|
1590
1655
|
|
|
1591
1656
|
const pathIdentities = await Promise.all(
|
|
1592
|
-
[...guardKeys].map(async (key) => {
|
|
1593
|
-
const lockRootPath = await realpath(dirname(key))
|
|
1657
|
+
[...guardKeys].map(async ([key, carriedLockRoot]) => {
|
|
1658
|
+
const lockRootPath = carriedLockRoot ?? (await realpath(dirname(key)))
|
|
1594
1659
|
const lockRootStats = await lstat(lockRootPath, { bigint: true })
|
|
1595
1660
|
if (!lockRootStats.isDirectory()) {
|
|
1596
1661
|
throw reconciliationPathCollisionError(
|
|
@@ -1786,11 +1851,14 @@ function reconciliationRetiredPath(path: string) {
|
|
|
1786
1851
|
async function acquireReconciliationLock(
|
|
1787
1852
|
identity: ReconciliationLockIdentity,
|
|
1788
1853
|
acquisitionDeadline: ReconciliationLockDeadline,
|
|
1854
|
+
resolveExecutionIdentity: () => Promise<ProcessExecutionIdentityResolution>,
|
|
1855
|
+
getProcessExecutionIdentity: () => Promise<ProcessExecutionIdentity>,
|
|
1856
|
+
getProcessIncarnation: () => Promise<string | null>,
|
|
1789
1857
|
) {
|
|
1790
1858
|
const { key } = identity
|
|
1791
|
-
const [incarnation,
|
|
1792
|
-
|
|
1793
|
-
|
|
1859
|
+
const [incarnation, identityResolution] = await Promise.all([
|
|
1860
|
+
getProcessIncarnation(),
|
|
1861
|
+
resolveExecutionIdentity(),
|
|
1794
1862
|
])
|
|
1795
1863
|
const lockPath = reconciliationLockPath(identity)
|
|
1796
1864
|
await assertReconciliationAnchorUnchanged(identity)
|
|
@@ -1807,6 +1875,53 @@ async function acquireReconciliationLock(
|
|
|
1807
1875
|
isReconciliationReclaimOwner,
|
|
1808
1876
|
)
|
|
1809
1877
|
|
|
1878
|
+
let executionIdentity = identityResolution.identity
|
|
1879
|
+
if (!identityResolution.complete) {
|
|
1880
|
+
// Degraded (lock-free) wait: on hosts that cannot provide a complete
|
|
1881
|
+
// execution identity (FreeBSD, containers without /etc/machine-id,
|
|
1882
|
+
// policy-blocked Windows tools), skipping the owner write preserves
|
|
1883
|
+
// pre-reconciliation (3.7.4) behavior for uncontended builds; an owner we
|
|
1884
|
+
// cannot write verifiably could never be reclaimed. An existing lock is
|
|
1885
|
+
// still respected fail-closed: never bypassed, and an error only if it
|
|
1886
|
+
// outlives the acquisition deadline.
|
|
1887
|
+
while (true) {
|
|
1888
|
+
await waitForReconciliationReclaim(identity, acquisitionDeadline)
|
|
1889
|
+
const state = await inspectReconciliationLock(identity, lockPath)
|
|
1890
|
+
if (state === undefined) {
|
|
1891
|
+
debug.warn(
|
|
1892
|
+
`Skipping cross-process filesystem reconciliation lock for ${key}: the system did not provide a complete machine, boot-session, and process-namespace identity (missing: ${missingProcessExecutionIdentityComponents(executionIdentity)})`,
|
|
1893
|
+
)
|
|
1894
|
+
return { release: async () => {}, degraded: true }
|
|
1895
|
+
}
|
|
1896
|
+
// Re-probe with the raw getter every iteration: a transient probe
|
|
1897
|
+
// failure heals here, and the existing lock can then be evaluated and
|
|
1898
|
+
// reclaimed through the normal path instead of failing at the deadline.
|
|
1899
|
+
const healedIdentity = await getProcessExecutionIdentity()
|
|
1900
|
+
if (isCompleteProcessExecutionIdentity(healedIdentity)) {
|
|
1901
|
+
degradedProcessExecutionIdentityMemo = undefined
|
|
1902
|
+
executionIdentity = healedIdentity
|
|
1903
|
+
break
|
|
1904
|
+
}
|
|
1905
|
+
if (performance.now() >= acquisitionDeadline.expiresAt) {
|
|
1906
|
+
throw reconciliationExecutionIdentityUnavailableError(
|
|
1907
|
+
key,
|
|
1908
|
+
healedIdentity,
|
|
1909
|
+
)
|
|
1910
|
+
}
|
|
1911
|
+
try {
|
|
1912
|
+
await delayReconciliationLockRetry(acquisitionDeadline, key)
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
if ((error as NodeJS.ErrnoException).code === 'ETIMEDOUT') {
|
|
1915
|
+
throw reconciliationExecutionIdentityUnavailableError(
|
|
1916
|
+
key,
|
|
1917
|
+
healedIdentity,
|
|
1918
|
+
)
|
|
1919
|
+
}
|
|
1920
|
+
throw error
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1810
1925
|
while (true) {
|
|
1811
1926
|
assertReconciliationLockAcquisitionTimeRemaining(acquisitionDeadline, key)
|
|
1812
1927
|
await assertReconciliationAnchorUnchanged(identity)
|
|
@@ -1856,7 +1971,14 @@ async function acquireReconciliationLock(
|
|
|
1856
1971
|
if (
|
|
1857
1972
|
state?.owner &&
|
|
1858
1973
|
state.stale &&
|
|
1859
|
-
(await tryReclaimStaleReconciliationLock(
|
|
1974
|
+
(await tryReclaimStaleReconciliationLock(
|
|
1975
|
+
identity,
|
|
1976
|
+
lockPath,
|
|
1977
|
+
state,
|
|
1978
|
+
getProcessExecutionIdentity,
|
|
1979
|
+
getProcessIncarnation,
|
|
1980
|
+
acquisitionDeadline,
|
|
1981
|
+
))
|
|
1860
1982
|
) {
|
|
1861
1983
|
continue
|
|
1862
1984
|
}
|
|
@@ -1878,7 +2000,15 @@ async function acquireReconciliationLock(
|
|
|
1878
2000
|
`Lost filesystem reconciliation lock ownership before initialization: ${key}`,
|
|
1879
2001
|
)
|
|
1880
2002
|
}
|
|
1881
|
-
return
|
|
2003
|
+
return {
|
|
2004
|
+
release: maintainReconciliationLock(
|
|
2005
|
+
identity,
|
|
2006
|
+
lockPath,
|
|
2007
|
+
lockStats,
|
|
2008
|
+
token,
|
|
2009
|
+
),
|
|
2010
|
+
degraded: false,
|
|
2011
|
+
}
|
|
1882
2012
|
} catch (error) {
|
|
1883
2013
|
await cleanupFailedReconciliationLock(
|
|
1884
2014
|
identity,
|
|
@@ -2387,13 +2517,26 @@ async function tryReclaimStaleReconciliationLock(
|
|
|
2387
2517
|
identity: ReconciliationLockIdentity,
|
|
2388
2518
|
lockPath: string,
|
|
2389
2519
|
expectedState: ReconciliationLockState,
|
|
2520
|
+
getProcessExecutionIdentity: () => Promise<ProcessExecutionIdentity>,
|
|
2521
|
+
getProcessIncarnation: () => Promise<string | null>,
|
|
2522
|
+
acquisitionDeadline: ReconciliationLockDeadline,
|
|
2390
2523
|
) {
|
|
2391
2524
|
const reclaimPath = reconciliationReclaimPath(identity)
|
|
2392
2525
|
const token = randomUUID()
|
|
2393
|
-
const [incarnation,
|
|
2394
|
-
|
|
2395
|
-
|
|
2526
|
+
const [incarnation, identityResolution] = await Promise.all([
|
|
2527
|
+
getProcessIncarnation(),
|
|
2528
|
+
resolveProcessExecutionIdentityForLocking(
|
|
2529
|
+
getProcessExecutionIdentity,
|
|
2530
|
+
acquisitionDeadline,
|
|
2531
|
+
),
|
|
2396
2532
|
])
|
|
2533
|
+
// Never publish a reclaim owner with incomplete identity; an unverifiable
|
|
2534
|
+
// reclaim owner could never be reclaimed itself. Reachable in practice only
|
|
2535
|
+
// when the current side already proved complete (state.stale requires it).
|
|
2536
|
+
if (!identityResolution.complete) {
|
|
2537
|
+
return false
|
|
2538
|
+
}
|
|
2539
|
+
const executionIdentity = identityResolution.identity
|
|
2397
2540
|
const reclaimOwner: ReconciliationReclaimOwner = {
|
|
2398
2541
|
candidate: reconciliationCandidateName(reclaimPath, token),
|
|
2399
2542
|
createdAt: Date.now(),
|
|
@@ -3133,6 +3276,32 @@ function reconciliationHardLinkRequiredError(path: string, cause: unknown) {
|
|
|
3133
3276
|
return error
|
|
3134
3277
|
}
|
|
3135
3278
|
|
|
3279
|
+
function missingProcessExecutionIdentityComponents(
|
|
3280
|
+
identity: ProcessExecutionIdentity,
|
|
3281
|
+
) {
|
|
3282
|
+
return (
|
|
3283
|
+
[
|
|
3284
|
+
['machine', identity.machine],
|
|
3285
|
+
['boot', identity.boot],
|
|
3286
|
+
['namespace', identity.namespace],
|
|
3287
|
+
] as const
|
|
3288
|
+
)
|
|
3289
|
+
.filter(([, value]) => value === null)
|
|
3290
|
+
.map(([name]) => name)
|
|
3291
|
+
.join(', ')
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
function reconciliationExecutionIdentityUnavailableError(
|
|
3295
|
+
key: string,
|
|
3296
|
+
identity: ProcessExecutionIdentity,
|
|
3297
|
+
) {
|
|
3298
|
+
const error = new Error(
|
|
3299
|
+
`Filesystem reconciliation lock was not created for ${key}: the system did not provide a complete machine, boot-session, and process-namespace identity (missing: ${missingProcessExecutionIdentityComponents(identity)}), and an existing reconciliation lock cannot be evaluated without it. On Windows this means reg.exe or powershell.exe was blocked or too slow; retry the build or unblock those tools`,
|
|
3300
|
+
) as NodeJS.ErrnoException
|
|
3301
|
+
error.code = 'EAGAIN'
|
|
3302
|
+
return error
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3136
3305
|
function reconciliationOwnerCannotBeVerifiedError(
|
|
3137
3306
|
path: string,
|
|
3138
3307
|
owner: ReconciliationMetadataOwner,
|
|
@@ -3337,34 +3506,59 @@ function processExists(pid: number) {
|
|
|
3337
3506
|
}
|
|
3338
3507
|
}
|
|
3339
3508
|
|
|
3340
|
-
function
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3509
|
+
export function createProcessIncarnationGetter(
|
|
3510
|
+
readIncarnation: () => Promise<string | null>,
|
|
3511
|
+
failureRetryCooldown: number,
|
|
3512
|
+
now: () => number = () => performance.now(),
|
|
3513
|
+
) {
|
|
3514
|
+
// The current process incarnation is fixed for the process lifetime; only
|
|
3515
|
+
// the probe reading it can fail. A success is cached forever and an
|
|
3516
|
+
// in-flight probe is shared. A failure is reused for the cooldown window
|
|
3517
|
+
// so a burst of sequential lock acquisitions on a probe-failing host (e.g.
|
|
3518
|
+
// powershell hitting its kill timeout under load) spawns one probe instead
|
|
3519
|
+
// of one per acquisition.
|
|
3520
|
+
let incarnation: string | undefined
|
|
3521
|
+
let activeProbe: Promise<string | null> | undefined
|
|
3522
|
+
let retryAt = 0
|
|
3523
|
+
return function getProcessIncarnation() {
|
|
3524
|
+
if (incarnation !== undefined) {
|
|
3525
|
+
return Promise.resolve(incarnation)
|
|
3526
|
+
}
|
|
3527
|
+
if (activeProbe !== undefined) {
|
|
3528
|
+
return activeProbe
|
|
3529
|
+
}
|
|
3530
|
+
if (now() < retryAt) {
|
|
3531
|
+
return Promise.resolve(null)
|
|
3532
|
+
}
|
|
3533
|
+
const probe = readIncarnation()
|
|
3534
|
+
activeProbe = probe
|
|
3535
|
+
void probe.then(
|
|
3536
|
+
(observed) => {
|
|
3537
|
+
if (observed !== null) {
|
|
3538
|
+
incarnation = observed
|
|
3539
|
+
} else {
|
|
3540
|
+
retryAt = now() + failureRetryCooldown
|
|
3541
|
+
}
|
|
3542
|
+
if (activeProbe === probe) {
|
|
3543
|
+
activeProbe = undefined
|
|
3544
|
+
}
|
|
3545
|
+
},
|
|
3546
|
+
() => {
|
|
3547
|
+
retryAt = now() + failureRetryCooldown
|
|
3548
|
+
if (activeProbe === probe) {
|
|
3549
|
+
activeProbe = undefined
|
|
3550
|
+
}
|
|
3551
|
+
},
|
|
3552
|
+
)
|
|
3553
|
+
return probe
|
|
3346
3554
|
}
|
|
3347
|
-
|
|
3348
|
-
const probe = readProcessIncarnation(process.pid)
|
|
3349
|
-
currentProcessIncarnationProbe = probe
|
|
3350
|
-
void probe.then(
|
|
3351
|
-
(incarnation) => {
|
|
3352
|
-
if (incarnation !== null) {
|
|
3353
|
-
currentProcessIncarnation = incarnation
|
|
3354
|
-
}
|
|
3355
|
-
if (currentProcessIncarnationProbe === probe) {
|
|
3356
|
-
currentProcessIncarnationProbe = undefined
|
|
3357
|
-
}
|
|
3358
|
-
},
|
|
3359
|
-
() => {
|
|
3360
|
-
if (currentProcessIncarnationProbe === probe) {
|
|
3361
|
-
currentProcessIncarnationProbe = undefined
|
|
3362
|
-
}
|
|
3363
|
-
},
|
|
3364
|
-
)
|
|
3365
|
-
return probe
|
|
3366
3555
|
}
|
|
3367
3556
|
|
|
3557
|
+
const getCurrentProcessIncarnation = createProcessIncarnationGetter(
|
|
3558
|
+
() => readProcessIncarnation(process.pid),
|
|
3559
|
+
processIncarnationProbeRetryCooldown,
|
|
3560
|
+
)
|
|
3561
|
+
|
|
3368
3562
|
export function createProcessExecutionIdentityGetter(
|
|
3369
3563
|
readIdentity: () => Promise<ProcessExecutionIdentity>,
|
|
3370
3564
|
incompleteCacheDuration: number,
|
|
@@ -3442,11 +3636,74 @@ function isCompleteProcessExecutionIdentity(
|
|
|
3442
3636
|
)
|
|
3443
3637
|
}
|
|
3444
3638
|
|
|
3639
|
+
// An owner written with an incomplete identity can never be verified or
|
|
3640
|
+
// reclaimed (processOwnerState fails closed), so publishing one would wedge
|
|
3641
|
+
// the lock until manual cleanup. Re-probe within the wait budget instead, then
|
|
3642
|
+
// report the outcome: callers degrade to lock-free operation rather than fail.
|
|
3643
|
+
// Slow-but-working probes win on the first read regardless of the budget; the
|
|
3644
|
+
// budget only bounds riding out fast probe failures.
|
|
3645
|
+
export async function resolveProcessExecutionIdentityForLocking(
|
|
3646
|
+
getIdentity: () => Promise<ProcessExecutionIdentity>,
|
|
3647
|
+
waitBudget: ReconciliationLockDeadline,
|
|
3648
|
+
): Promise<
|
|
3649
|
+
| { complete: true; identity: ProcessExecutionIdentity }
|
|
3650
|
+
| { complete: false; identity: ProcessExecutionIdentity }
|
|
3651
|
+
> {
|
|
3652
|
+
while (true) {
|
|
3653
|
+
const identity = await getIdentity()
|
|
3654
|
+
if (isCompleteProcessExecutionIdentity(identity)) {
|
|
3655
|
+
return { complete: true, identity }
|
|
3656
|
+
}
|
|
3657
|
+
if (performance.now() >= waitBudget.expiresAt) {
|
|
3658
|
+
return { complete: false, identity }
|
|
3659
|
+
}
|
|
3660
|
+
await delay(
|
|
3661
|
+
Math.max(0, Math.min(20, waitBudget.expiresAt - performance.now())),
|
|
3662
|
+
)
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
type ProcessExecutionIdentityResolution = Awaited<
|
|
3667
|
+
ReturnType<typeof resolveProcessExecutionIdentityForLocking>
|
|
3668
|
+
>
|
|
3669
|
+
|
|
3445
3670
|
const getCurrentProcessExecutionIdentity = createProcessExecutionIdentityGetter(
|
|
3446
3671
|
readProcessExecutionIdentity,
|
|
3447
3672
|
incompleteProcessExecutionIdentityCacheDuration,
|
|
3448
3673
|
)
|
|
3449
3674
|
|
|
3675
|
+
const degradedProcessExecutionIdentityMemoDuration = 30_000
|
|
3676
|
+
// One build pays the probe-failure wait once: a degraded outcome is memoized
|
|
3677
|
+
// briefly so later acquisitions in this process skip the wait loop. 30s
|
|
3678
|
+
// outlives a single build but not separate invocations.
|
|
3679
|
+
let degradedProcessExecutionIdentityMemo: { decidedAt: number } | undefined
|
|
3680
|
+
|
|
3681
|
+
async function resolveDefaultProcessExecutionIdentityForLocking(
|
|
3682
|
+
identityWaitTimeout: number,
|
|
3683
|
+
): Promise<ProcessExecutionIdentityResolution> {
|
|
3684
|
+
const memo = degradedProcessExecutionIdentityMemo
|
|
3685
|
+
if (
|
|
3686
|
+
memo !== undefined &&
|
|
3687
|
+
performance.now() - memo.decidedAt <
|
|
3688
|
+
degradedProcessExecutionIdentityMemoDuration
|
|
3689
|
+
) {
|
|
3690
|
+
const identity = await getCurrentProcessExecutionIdentity()
|
|
3691
|
+
if (isCompleteProcessExecutionIdentity(identity)) {
|
|
3692
|
+
degradedProcessExecutionIdentityMemo = undefined
|
|
3693
|
+
return { complete: true, identity }
|
|
3694
|
+
}
|
|
3695
|
+
return { complete: false, identity }
|
|
3696
|
+
}
|
|
3697
|
+
const resolution = await resolveProcessExecutionIdentityForLocking(
|
|
3698
|
+
getCurrentProcessExecutionIdentity,
|
|
3699
|
+
createReconciliationLockDeadline(identityWaitTimeout),
|
|
3700
|
+
)
|
|
3701
|
+
degradedProcessExecutionIdentityMemo = resolution.complete
|
|
3702
|
+
? undefined
|
|
3703
|
+
: { decidedAt: performance.now() }
|
|
3704
|
+
return resolution
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3450
3707
|
async function readProcessExecutionIdentity(): Promise<ProcessExecutionIdentity> {
|
|
3451
3708
|
if (process.platform === 'linux') {
|
|
3452
3709
|
const [bootId, machineId, productUuid, pidNamespace] = await Promise.all([
|
|
@@ -3483,19 +3740,29 @@ async function readProcessExecutionIdentity(): Promise<ProcessExecutionIdentity>
|
|
|
3483
3740
|
)
|
|
3484
3741
|
: 'powershell.exe'
|
|
3485
3742
|
const [machineOutput, boot] = await Promise.all([
|
|
3486
|
-
executeProcessIncarnationCommand(
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3743
|
+
executeProcessIncarnationCommand(
|
|
3744
|
+
reg,
|
|
3745
|
+
[
|
|
3746
|
+
'query',
|
|
3747
|
+
String.raw`HKLM\SOFTWARE\Microsoft\Cryptography`,
|
|
3748
|
+
'/v',
|
|
3749
|
+
'MachineGuid',
|
|
3750
|
+
],
|
|
3751
|
+
process.env,
|
|
3752
|
+
processExecutionIdentityCommandTimeout,
|
|
3753
|
+
),
|
|
3754
|
+
executeProcessIncarnationCommand(
|
|
3755
|
+
powershell,
|
|
3756
|
+
[
|
|
3757
|
+
'-NoLogo',
|
|
3758
|
+
'-NoProfile',
|
|
3759
|
+
'-NonInteractive',
|
|
3760
|
+
'-Command',
|
|
3761
|
+
'(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().Ticks',
|
|
3762
|
+
],
|
|
3763
|
+
process.env,
|
|
3764
|
+
processExecutionIdentityCommandTimeout,
|
|
3765
|
+
),
|
|
3499
3766
|
])
|
|
3500
3767
|
const machineGuid = machineOutput?.match(
|
|
3501
3768
|
/\bMachineGuid\s+REG_SZ\s+([0-9a-f-]+)\s*$/i,
|
|
@@ -3512,19 +3779,24 @@ async function readProcessExecutionIdentity(): Promise<ProcessExecutionIdentity>
|
|
|
3512
3779
|
if (process.platform === 'darwin') {
|
|
3513
3780
|
const [machineOutput, bootSessionOutput, bootTimeOutput] =
|
|
3514
3781
|
await Promise.all([
|
|
3515
|
-
executeProcessIncarnationCommand(
|
|
3516
|
-
'
|
|
3517
|
-
'-c',
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
'
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3782
|
+
executeProcessIncarnationCommand(
|
|
3783
|
+
'/usr/sbin/ioreg',
|
|
3784
|
+
['-rd1', '-c', 'IOPlatformExpertDevice'],
|
|
3785
|
+
process.env,
|
|
3786
|
+
processExecutionIdentityCommandTimeout,
|
|
3787
|
+
),
|
|
3788
|
+
executeProcessIncarnationCommand(
|
|
3789
|
+
'/usr/sbin/sysctl',
|
|
3790
|
+
['-n', 'kern.bootsessionuuid'],
|
|
3791
|
+
process.env,
|
|
3792
|
+
processExecutionIdentityCommandTimeout,
|
|
3793
|
+
),
|
|
3794
|
+
executeProcessIncarnationCommand(
|
|
3795
|
+
'/usr/sbin/sysctl',
|
|
3796
|
+
['-n', 'kern.boottime'],
|
|
3797
|
+
process.env,
|
|
3798
|
+
processExecutionIdentityCommandTimeout,
|
|
3799
|
+
),
|
|
3528
3800
|
])
|
|
3529
3801
|
const platformUuid = machineOutput?.match(
|
|
3530
3802
|
/"IOPlatformUUID"\s*=\s*"([0-9a-f-]+)"/i,
|
|
@@ -3687,6 +3959,7 @@ function executeProcessIncarnationCommand(
|
|
|
3687
3959
|
command: string,
|
|
3688
3960
|
args: string[],
|
|
3689
3961
|
env: NodeJS.ProcessEnv = process.env,
|
|
3962
|
+
timeout: number = processIncarnationCommandTimeout,
|
|
3690
3963
|
): Promise<string | null> {
|
|
3691
3964
|
return new Promise((resolveCommand) => {
|
|
3692
3965
|
execFile(
|
|
@@ -3695,7 +3968,7 @@ function executeProcessIncarnationCommand(
|
|
|
3695
3968
|
{
|
|
3696
3969
|
encoding: 'utf8',
|
|
3697
3970
|
env,
|
|
3698
|
-
timeout
|
|
3971
|
+
timeout,
|
|
3699
3972
|
windowsHide: true,
|
|
3700
3973
|
},
|
|
3701
3974
|
(error, stdout) => {
|