@hominis/fireforge 0.45.0 → 0.46.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/CHANGELOG.md +80 -0
- package/dist/bin/fireforge.js +6 -0
- package/dist/build-info.json +4 -4
- package/dist/src/cli-usage-error.d.ts +46 -0
- package/dist/src/cli-usage-error.js +81 -0
- package/dist/src/cli.d.ts +5 -1
- package/dist/src/cli.js +20 -21
- package/dist/src/commands/bootstrap.js +1 -2
- package/dist/src/commands/build.js +15 -6
- package/dist/src/commands/destructive-decision.d.ts +19 -0
- package/dist/src/commands/destructive-decision.js +31 -0
- package/dist/src/commands/doctor-check-core.d.ts +0 -6
- package/dist/src/commands/doctor-check-core.js +0 -8
- package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
- package/dist/src/commands/doctor-orphaned-harness.js +4 -12
- package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
- package/dist/src/commands/doctor.d.ts +2 -0
- package/dist/src/commands/doctor.js +7 -8
- package/dist/src/commands/download.js +35 -24
- package/dist/src/commands/export-all.js +21 -4
- package/dist/src/commands/export-flow.js +15 -45
- package/dist/src/commands/export-placement-conflicts.js +3 -3
- package/dist/src/commands/export-placement-gate.js +2 -8
- package/dist/src/commands/export-placement-policy.js +3 -5
- package/dist/src/commands/export-shared.d.ts +42 -23
- package/dist/src/commands/export-shared.js +4 -19
- package/dist/src/commands/export.js +10 -1
- package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
- package/dist/src/commands/furnace/authoring-config.js +19 -0
- package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
- package/dist/src/commands/furnace/create-browser-test.js +3 -8
- package/dist/src/commands/furnace/create-dry-run.js +2 -5
- package/dist/src/commands/furnace/create-templates.d.ts +17 -1
- package/dist/src/commands/furnace/create-templates.js +2 -1
- package/dist/src/commands/furnace/create.js +25 -19
- package/dist/src/commands/furnace/deploy.js +11 -2
- package/dist/src/commands/furnace/init.js +2 -1
- package/dist/src/commands/furnace/override.js +28 -19
- package/dist/src/commands/furnace/refresh.js +9 -1
- package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
- package/dist/src/commands/furnace/remove-mutations.js +0 -1
- package/dist/src/commands/furnace/remove.js +2 -6
- package/dist/src/commands/furnace/rename-browser-test.js +1 -7
- package/dist/src/commands/furnace/rename.js +29 -29
- package/dist/src/commands/furnace/status.js +12 -9
- package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
- package/dist/src/commands/furnace/test-file-name.js +39 -0
- package/dist/src/commands/furnace/validation-output.d.ts +21 -9
- package/dist/src/commands/furnace/validation-output.js +3 -8
- package/dist/src/commands/import.js +2 -9
- package/dist/src/commands/lint-per-patch.d.ts +2 -2
- package/dist/src/commands/lint-per-patch.js +19 -4
- package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
- package/dist/src/commands/lint.d.ts +2 -1
- package/dist/src/commands/lint.js +1 -1
- package/dist/src/commands/manifest.js +1 -1
- package/dist/src/commands/package.js +2 -4
- package/dist/src/commands/patch/compact.js +14 -15
- package/dist/src/commands/patch/delete.js +15 -25
- package/dist/src/commands/patch/entry-projection.d.ts +13 -0
- package/dist/src/commands/patch/entry-projection.js +24 -0
- package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
- package/dist/src/commands/patch/lint-ignore.js +19 -29
- package/dist/src/commands/patch/move-files-create.js +24 -10
- package/dist/src/commands/patch/move-files-into.js +38 -34
- package/dist/src/commands/patch/move-files.js +4 -13
- package/dist/src/commands/patch/rename.js +27 -41
- package/dist/src/commands/patch/reorder.js +25 -38
- package/dist/src/commands/patch/split-plan.d.ts +4 -5
- package/dist/src/commands/patch/split-plan.js +4 -8
- package/dist/src/commands/patch/split.js +45 -43
- package/dist/src/commands/patch/staged-dependency.js +16 -22
- package/dist/src/commands/patch/tier.js +15 -25
- package/dist/src/commands/patch-rename-map.d.ts +19 -0
- package/dist/src/commands/patch-rename-map.js +36 -0
- package/dist/src/commands/pending-resolution.d.ts +15 -0
- package/dist/src/commands/pending-resolution.js +25 -0
- package/dist/src/commands/re-export-adjacent.d.ts +24 -2
- package/dist/src/commands/re-export-adjacent.js +32 -16
- package/dist/src/commands/re-export-bulk-scan.js +6 -10
- package/dist/src/commands/re-export-drift.js +4 -4
- package/dist/src/commands/re-export-files.d.ts +2 -3
- package/dist/src/commands/re-export-files.js +38 -29
- package/dist/src/commands/re-export-lint.d.ts +3 -4
- package/dist/src/commands/re-export-lint.js +9 -2
- package/dist/src/commands/re-export-scan.js +5 -2
- package/dist/src/commands/re-export-single.d.ts +22 -3
- package/dist/src/commands/re-export-single.js +19 -11
- package/dist/src/commands/re-export.js +28 -11
- package/dist/src/commands/rebase/abort.js +3 -8
- package/dist/src/commands/rebase/continue.js +12 -12
- package/dist/src/commands/rebase/index.js +2 -2
- package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
- package/dist/src/commands/rebase/patch-loop.js +2 -7
- package/dist/src/commands/resolve.js +18 -10
- package/dist/src/commands/run.d.ts +0 -13
- package/dist/src/commands/run.js +12 -25
- package/dist/src/commands/setup-support.d.ts +16 -4
- package/dist/src/commands/setup-support.js +16 -16
- package/dist/src/commands/setup.js +3 -9
- package/dist/src/commands/source.js +8 -1
- package/dist/src/commands/status-check.js +2 -2
- package/dist/src/commands/status-output.js +2 -2
- package/dist/src/commands/status.js +3 -4
- package/dist/src/commands/test-build-phase.d.ts +2 -2
- package/dist/src/commands/test-build-phase.js +31 -7
- package/dist/src/commands/test-modes.d.ts +2 -2
- package/dist/src/commands/test-modes.js +0 -1
- package/dist/src/commands/test-register.js +5 -3
- package/dist/src/commands/test-run.js +25 -18
- package/dist/src/commands/test-stale-gate.js +4 -4
- package/dist/src/commands/test-verdict.d.ts +16 -2
- package/dist/src/commands/test-verdict.js +18 -3
- package/dist/src/commands/test.js +42 -5
- package/dist/src/commands/token-list.d.ts +26 -0
- package/dist/src/commands/token-list.js +153 -0
- package/dist/src/commands/token.js +17 -1
- package/dist/src/commands/tree.js +7 -2
- package/dist/src/commands/typecheck.js +9 -9
- package/dist/src/commands/wire.js +4 -3
- package/dist/src/core/branding.d.ts +0 -7
- package/dist/src/core/branding.js +3 -2
- package/dist/src/core/browser-wire.js +6 -3
- package/dist/src/core/build-audit-platform.d.ts +29 -0
- package/dist/src/core/build-audit-platform.js +140 -0
- package/dist/src/core/build-audit-registration.js +3 -2
- package/dist/src/core/build-audit-skip.d.ts +57 -0
- package/dist/src/core/build-audit-skip.js +112 -0
- package/dist/src/core/build-audit.d.ts +15 -2
- package/dist/src/core/build-audit.js +110 -10
- package/dist/src/core/build-baseline.d.ts +45 -28
- package/dist/src/core/build-baseline.js +6 -29
- package/dist/src/core/build-jar-escalation.js +3 -2
- package/dist/src/core/build-overwrite-guard.js +1 -1
- package/dist/src/core/build-prepare.d.ts +0 -8
- package/dist/src/core/build-prepare.js +7 -12
- package/dist/src/core/config-paths.d.ts +2 -6
- package/dist/src/core/config-paths.js +4 -2
- package/dist/src/core/config-state.d.ts +0 -6
- package/dist/src/core/config-state.js +1 -19
- package/dist/src/core/config-validate-build-audit.d.ts +8 -0
- package/dist/src/core/config-validate-build-audit.js +80 -0
- package/dist/src/core/config-validate-file-size.js +1 -1
- package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
- package/dist/src/core/config-validate-patch-policy.js +3 -3
- package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
- package/dist/src/core/config-validate-test-toolchains.js +4 -5
- package/dist/src/core/config-validate.js +14 -3
- package/dist/src/core/config.d.ts +2 -2
- package/dist/src/core/config.js +11 -15
- package/dist/src/core/coverage-extend.js +4 -4
- package/dist/src/core/discard-baseline.js +7 -7
- package/dist/src/core/engine-precondition.d.ts +1 -9
- package/dist/src/core/engine-precondition.js +3 -8
- package/dist/src/core/engine-session-lock.js +13 -17
- package/dist/src/core/export-moved-code-shape.js +2 -2
- package/dist/src/core/file-lock-owner.d.ts +103 -0
- package/dist/src/core/file-lock-owner.js +259 -0
- package/dist/src/core/file-lock.d.ts +2 -7
- package/dist/src/core/file-lock.js +40 -91
- package/dist/src/core/firefox-cache.d.ts +13 -1
- package/dist/src/core/firefox-cache.js +59 -34
- package/dist/src/core/firefox-download.d.ts +15 -0
- package/dist/src/core/firefox-download.js +37 -21
- package/dist/src/core/firefox-extract.d.ts +6 -5
- package/dist/src/core/firefox-extract.js +36 -18
- package/dist/src/core/firefox.d.ts +30 -32
- package/dist/src/core/firefox.js +5 -36
- package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
- package/dist/src/core/furnace-apply-dry-run.js +1 -7
- package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
- package/dist/src/core/furnace-apply-helpers.js +8 -19
- package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
- package/dist/src/core/furnace-apply.d.ts +1 -1
- package/dist/src/core/furnace-apply.js +30 -10
- package/dist/src/core/furnace-config.d.ts +1 -1
- package/dist/src/core/furnace-config.js +23 -36
- package/dist/src/core/furnace-constants.js +3 -2
- package/dist/src/core/furnace-dir-entry.d.ts +20 -0
- package/dist/src/core/furnace-dir-entry.js +21 -0
- package/dist/src/core/furnace-operation.d.ts +2 -0
- package/dist/src/core/furnace-operation.js +3 -1
- package/dist/src/core/furnace-refresh.d.ts +18 -6
- package/dist/src/core/furnace-refresh.js +3 -6
- package/dist/src/core/furnace-registration-ast.d.ts +0 -1
- package/dist/src/core/furnace-registration-ast.js +0 -3
- package/dist/src/core/furnace-registration.d.ts +2 -1
- package/dist/src/core/furnace-registration.js +22 -29
- package/dist/src/core/furnace-scanner.js +3 -6
- package/dist/src/core/furnace-state-persist.d.ts +2 -2
- package/dist/src/core/furnace-validate-registration.js +5 -7
- package/dist/src/core/git-base.d.ts +3 -1
- package/dist/src/core/git-base.js +25 -3
- package/dist/src/core/git-diff-new-file.d.ts +40 -0
- package/dist/src/core/git-diff-new-file.js +119 -0
- package/dist/src/core/git-diff.d.ts +9 -9
- package/dist/src/core/git-diff.js +88 -157
- package/dist/src/core/git-file-ops.d.ts +0 -7
- package/dist/src/core/git-file-ops.js +4 -28
- package/dist/src/core/git-readonly-index.d.ts +0 -2
- package/dist/src/core/git-readonly-index.js +0 -4
- package/dist/src/core/git-status.d.ts +7 -0
- package/dist/src/core/git-status.js +21 -9
- package/dist/src/core/git.d.ts +0 -6
- package/dist/src/core/git.js +14 -43
- package/dist/src/core/harness-orphans.js +8 -7
- package/dist/src/core/history-log.d.ts +19 -0
- package/dist/src/core/history-log.js +29 -0
- package/dist/src/core/mach-error-hints.d.ts +0 -5
- package/dist/src/core/mach-error-hints.js +1 -1
- package/dist/src/core/mach-known-noise-filter.d.ts +4 -3
- package/dist/src/core/mach-known-noise-filter.js +7 -9
- package/dist/src/core/mach-mozconfig.js +2 -1
- package/dist/src/core/mach-python.d.ts +2 -0
- package/dist/src/core/mach-python.js +12 -7
- package/dist/src/core/mach-resource-shim.d.ts +13 -1
- package/dist/src/core/mach-resource-shim.js +112 -9
- package/dist/src/core/mach.d.ts +39 -53
- package/dist/src/core/mach.js +13 -81
- package/dist/src/core/marionette-port.d.ts +0 -17
- package/dist/src/core/marionette-port.js +12 -58
- package/dist/src/core/marionette-preflight.js +9 -27
- package/dist/src/core/mochitest-server-port.js +3 -3
- package/dist/src/core/moz-manifest-rules.js +37 -33
- package/dist/src/core/patch-apply-fuzz.js +4 -4
- package/dist/src/core/patch-apply.d.ts +4 -5
- package/dist/src/core/patch-apply.js +6 -12
- package/dist/src/core/patch-export-coverage.d.ts +0 -9
- package/dist/src/core/patch-export-coverage.js +1 -30
- package/dist/src/core/patch-export-update.d.ts +18 -1
- package/dist/src/core/patch-export-update.js +4 -3
- package/dist/src/core/patch-export.d.ts +11 -21
- package/dist/src/core/patch-export.js +44 -66
- package/dist/src/core/patch-files.d.ts +0 -4
- package/dist/src/core/patch-files.js +0 -11
- package/dist/src/core/patch-lint-binary.d.ts +6 -12
- package/dist/src/core/patch-lint-cache.d.ts +17 -1
- package/dist/src/core/patch-lint-cache.js +3 -6
- package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
- package/dist/src/core/patch-lint-checkjs.js +37 -24
- package/dist/src/core/patch-lint-creators.d.ts +2 -6
- package/dist/src/core/patch-lint-cross.d.ts +16 -8
- package/dist/src/core/patch-lint-cross.js +49 -92
- package/dist/src/core/patch-lint-css.js +10 -6
- package/dist/src/core/patch-lint-file-size.js +3 -3
- package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
- package/dist/src/core/patch-lint-forward-registration.js +210 -0
- package/dist/src/core/patch-lint-jsdoc.js +36 -36
- package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
- package/dist/src/core/patch-lint-prettier.js +1 -1
- package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
- package/dist/src/core/patch-lint-queue-types.js +12 -0
- package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
- package/dist/src/core/patch-lint-staged-registration.js +90 -0
- package/dist/src/core/patch-lint.d.ts +14 -9
- package/dist/src/core/patch-lint.js +4 -3
- package/dist/src/core/patch-lock.js +3 -3
- package/dist/src/core/patch-manifest-io.js +8 -7
- package/dist/src/core/patch-parse.d.ts +19 -8
- package/dist/src/core/patch-parse.js +25 -0
- package/dist/src/core/patch-policy.js +15 -27
- package/dist/src/core/patch-source-metadata.d.ts +1 -3
- package/dist/src/core/patch-source-metadata.js +0 -4
- package/dist/src/core/patch-transform.d.ts +0 -8
- package/dist/src/core/patch-transform.js +0 -10
- package/dist/src/core/process-cpu-time.js +1 -1
- package/dist/src/core/rebase-session.d.ts +2 -8
- package/dist/src/core/rebase-session.js +5 -17
- package/dist/src/core/register-browser-content.js +3 -2
- package/dist/src/core/register-browser-test.js +2 -1
- package/dist/src/core/register-module.js +2 -1
- package/dist/src/core/register-scaffold.js +5 -4
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/run-log-redact.d.ts +38 -0
- package/dist/src/core/run-log-redact.js +69 -0
- package/dist/src/core/run-log.d.ts +13 -2
- package/dist/src/core/run-log.js +67 -18
- package/dist/src/core/signal-critical.js +2 -4
- package/dist/src/core/smoke-patterns.d.ts +0 -1
- package/dist/src/core/smoke-patterns.js +6 -11
- package/dist/src/core/state-file.d.ts +12 -1
- package/dist/src/core/state-file.js +14 -1
- package/dist/src/core/status-classify.js +2 -1
- package/dist/src/core/test-changed-prefs.js +26 -11
- package/dist/src/core/test-harness-crash.js +2 -2
- package/dist/src/core/test-stale-check.d.ts +0 -2
- package/dist/src/core/test-stale-check.js +2 -2
- package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
- package/dist/src/core/test-xpcshell-retry.js +14 -11
- package/dist/src/core/token-category.d.ts +33 -0
- package/dist/src/core/token-category.js +56 -48
- package/dist/src/core/token-dark-mode.js +2 -14
- package/dist/src/core/token-inventory.d.ts +56 -0
- package/dist/src/core/token-inventory.js +109 -0
- package/dist/src/core/token-manager.d.ts +1 -1
- package/dist/src/core/token-manager.js +1 -1
- package/dist/src/core/token-variant.js +21 -22
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/tree-cow.d.ts +0 -2
- package/dist/src/core/tree-cow.js +2 -2
- package/dist/src/core/tree-store.js +4 -5
- package/dist/src/core/typecheck.js +2 -2
- package/dist/src/core/wire-dom-fragment.js +6 -17
- package/dist/src/core/wire-utils.js +5 -17
- package/dist/src/core/xpcshell-appdir.js +2 -4
- package/dist/src/errors/base.d.ts +53 -1
- package/dist/src/errors/base.js +79 -11
- package/dist/src/errors/build.js +20 -28
- package/dist/src/errors/config.d.ts +0 -14
- package/dist/src/errors/config.js +13 -34
- package/dist/src/errors/download.d.ts +13 -0
- package/dist/src/errors/download.js +53 -24
- package/dist/src/errors/furnace.js +8 -9
- package/dist/src/errors/git.d.ts +8 -8
- package/dist/src/errors/git.js +20 -30
- package/dist/src/errors/patch.js +8 -10
- package/dist/src/errors/rebase.js +7 -7
- package/dist/src/types/commands/options.d.ts +77 -123
- package/dist/src/types/config.d.ts +34 -2
- package/dist/src/types/index.d.ts +0 -1
- package/dist/src/utils/assert.d.ts +3 -15
- package/dist/src/utils/assert.js +1 -18
- package/dist/src/utils/build-info.d.ts +1 -1
- package/dist/src/utils/child-env.d.ts +2 -1
- package/dist/src/utils/elapsed.js +10 -1
- package/dist/src/utils/fs.js +8 -13
- package/dist/src/utils/hash.d.ts +12 -0
- package/dist/src/utils/hash.js +16 -0
- package/dist/src/utils/line-dispatch.d.ts +15 -0
- package/dist/src/utils/line-dispatch.js +48 -0
- package/dist/src/utils/options.d.ts +1 -7
- package/dist/src/utils/options.js +6 -7
- package/dist/src/utils/package-root.js +3 -12
- package/dist/src/utils/platform.d.ts +0 -4
- package/dist/src/utils/process-group.d.ts +2 -1
- package/dist/src/utils/process-group.js +20 -19
- package/dist/src/utils/process.d.ts +40 -26
- package/dist/src/utils/process.js +115 -166
- package/dist/src/utils/ps-duration.js +3 -3
- package/dist/src/utils/sleep.d.ts +26 -0
- package/dist/src/utils/sleep.js +31 -0
- package/dist/src/utils/validation.d.ts +2 -38
- package/dist/src/utils/validation.js +7 -16
- package/docs/configuration.md +89 -1
- package/docs/exit-codes.md +31 -19
- package/docs/lifecycle-invariants.md +18 -12
- package/docs/machine-output.md +31 -8
- package/docs/patch-workflow.md +2 -1
- package/docs/run-logs.md +44 -4
- package/docs/testing.md +8 -1
- package/package.json +8 -7
- package/dist/src/commands/rebase.d.ts +0 -4
- package/dist/src/commands/rebase.js +0 -5
- package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
- package/dist/src/core/patch-artifact-normalize.js +0 -24
- package/dist/src/core/patch-lint-reexports.d.ts +0 -7
- package/dist/src/core/patch-lint-reexports.js +0 -8
- package/dist/src/core/wire-targets.d.ts +0 -7
- package/dist/src/core/wire-targets.js +0 -8
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filename of a lock directory's owner record, relative to the lock dir.
|
|
3
|
+
*
|
|
4
|
+
* Re-exported by `file-lock.ts` so external readers (`tree-store.ts`) share
|
|
5
|
+
* the constant instead of re-spelling `'pid'`; the file *format* is
|
|
6
|
+
* documented on {@link readLockOwner}.
|
|
7
|
+
*/
|
|
8
|
+
export declare const LOCK_PID_FILE = "pid";
|
|
9
|
+
/**
|
|
10
|
+
* Owner record read back from a lock directory's PID file.
|
|
11
|
+
*
|
|
12
|
+
* `token` is the per-acquisition UUID written on line 2 of the PID file,
|
|
13
|
+
* `acquiredAtMs` the wall-clock time the record was written (diagnostic),
|
|
14
|
+
* and `startTick` the writer's boot-relative start tick (liveness). Records
|
|
15
|
+
* from releases before a field was introduced parse with it `undefined`;
|
|
16
|
+
* readers must treat that as "unknown", never as a mismatch.
|
|
17
|
+
*/
|
|
18
|
+
export type LockOwner = {
|
|
19
|
+
present: false;
|
|
20
|
+
} | {
|
|
21
|
+
present: true;
|
|
22
|
+
pid: number;
|
|
23
|
+
token?: string;
|
|
24
|
+
acquiredAtMs?: number;
|
|
25
|
+
startTick?: number;
|
|
26
|
+
metadata: string[];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Reads the owner PID (line 1), acquisition token (line 2), the mechanical
|
|
30
|
+
* `acquired-at-ms=` / `start-tick=` lines (when present), and diagnostic
|
|
31
|
+
* metadata (the remaining lines 3+) from a lock directory's PID file.
|
|
32
|
+
* Returns `{ present: false }` when the PID file is missing, unreadable, or
|
|
33
|
+
* the PID does not parse as a finite integer (caller falls back to the
|
|
34
|
+
* age-only staleness heuristic).
|
|
35
|
+
*
|
|
36
|
+
* File format note: external readers do `parseInt(content.trim(), 10)`,
|
|
37
|
+
* which parses the leading digits of a multi-line file — so the token and
|
|
38
|
+
* mechanical lines stay compatible.
|
|
39
|
+
*
|
|
40
|
+
* @param lockPath - Lock directory whose owner record to read
|
|
41
|
+
* @returns The parsed owner record, or `{ present: false }`
|
|
42
|
+
*/
|
|
43
|
+
export declare function readLockOwner(lockPath: string): Promise<LockOwner>;
|
|
44
|
+
/**
|
|
45
|
+
* Stamps this process's ownership (PID, per-acquisition token, acquisition
|
|
46
|
+
* time, own start tick, diagnostic metadata) into a lock directory it has
|
|
47
|
+
* just created, and reads the record back to prove it landed.
|
|
48
|
+
*
|
|
49
|
+
* The write is FATAL. It used to be best-effort, on the theory that stale
|
|
50
|
+
* recovery would fall back to the age heuristic — but that heuristic reaps
|
|
51
|
+
* any owner-less lock after five minutes, and the build lock legitimately
|
|
52
|
+
* holds for hours. A holder whose record failed to write was therefore
|
|
53
|
+
* reaped mid-build and a second process entered the critical section. On
|
|
54
|
+
* failure the just-created lock directory is removed (nothing else can own
|
|
55
|
+
* it yet) and a {@link GeneralError} names the cause.
|
|
56
|
+
*
|
|
57
|
+
* @param lockPath - Lock directory this process has just `mkdir`'d
|
|
58
|
+
* @param token - Per-acquisition UUID, later checked by release
|
|
59
|
+
* @param ownerMetadata - Extra human-diagnostic lines (see `FileLockOptions.ownerMetadata`)
|
|
60
|
+
* @throws {GeneralError} When the record cannot be written or does not read back as written
|
|
61
|
+
*/
|
|
62
|
+
export declare function writeLockOwner(lockPath: string, token: string, ownerMetadata: readonly string[] | undefined): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Boot-relative start tick of a process, where the platform exposes it
|
|
65
|
+
* cheaply.
|
|
66
|
+
*
|
|
67
|
+
* Linux only: field 22 of `/proc/<pid>/stat`, in USER_HZ since boot.
|
|
68
|
+
* Elsewhere (macOS, Windows) obtaining a start time means spawning
|
|
69
|
+
* `ps`/`wmic` per probe, which is not worth paying on every poll of a
|
|
70
|
+
* contended lock — those platforms return `undefined` and liveness stays
|
|
71
|
+
* PID-only.
|
|
72
|
+
*
|
|
73
|
+
* Deliberately NOT converted to wall-clock time via `/proc/stat` `btime`:
|
|
74
|
+
* on current kernels `btime` is derived from the realtime clock and moves
|
|
75
|
+
* with every clock step, so a wall-clock comparison against a recorded
|
|
76
|
+
* acquisition time misjudges a live holder as a reused PID after NTP steps
|
|
77
|
+
* the clock forward — and reaps a lock that is in use.
|
|
78
|
+
*
|
|
79
|
+
* @param pid - Process to look up
|
|
80
|
+
* @param procRoot - procfs mount, overridable for tests
|
|
81
|
+
* @returns Start tick, or `undefined` when unobtainable
|
|
82
|
+
*/
|
|
83
|
+
export declare function readProcessStartTick(pid: number, procRoot?: string): number | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Whether the process named by a lock's owner record is the SAME process
|
|
86
|
+
* that acquired the lock, not merely a live process wearing its PID.
|
|
87
|
+
*
|
|
88
|
+
* `isProcessAlive` answers "does this PID exist"; after a holder crashes,
|
|
89
|
+
* the kernel may hand its PID to a long-lived daemon, and a PID-only check
|
|
90
|
+
* then honours the dead lock until the waiter's timeout. Where the record
|
|
91
|
+
* carries the writer's start tick and the candidate's own tick is cheaply
|
|
92
|
+
* obtainable (see {@link readProcessStartTick}), the two must be equal: a
|
|
93
|
+
* different tick is a different process. Records without a start tick, and
|
|
94
|
+
* platforms without one, keep the PID-only answer.
|
|
95
|
+
*
|
|
96
|
+
* @param owner - Owner record fields that identify the holder
|
|
97
|
+
* @param procRoot - procfs mount, overridable for tests
|
|
98
|
+
* @returns True when the original holder is still running (or cannot be proven gone)
|
|
99
|
+
*/
|
|
100
|
+
export declare function isLockOwnerAlive(owner: {
|
|
101
|
+
pid: number;
|
|
102
|
+
startTick?: number | undefined;
|
|
103
|
+
}, procRoot?: string): boolean;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Owner record of a `withFileLock` directory: the file that says WHICH
|
|
4
|
+
* process holds the lock, so stale recovery can tell a dead holder from a
|
|
5
|
+
* slow one and release can refuse to remove a lock it no longer owns.
|
|
6
|
+
*
|
|
7
|
+
* Split out of `file-lock.ts` so the record's format, its write-and-verify
|
|
8
|
+
* contract, and the PID-reuse guard live in one place with their own tests.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { readFile, rm, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { GeneralError } from '../errors/base.js';
|
|
14
|
+
import { isProcessAlive, toError } from '../utils/errors.js';
|
|
15
|
+
import { verbose } from '../utils/logger.js';
|
|
16
|
+
import { sleep } from '../utils/sleep.js';
|
|
17
|
+
/**
|
|
18
|
+
* Filename of a lock directory's owner record, relative to the lock dir.
|
|
19
|
+
*
|
|
20
|
+
* Re-exported by `file-lock.ts` so external readers (`tree-store.ts`) share
|
|
21
|
+
* the constant instead of re-spelling `'pid'`; the file *format* is
|
|
22
|
+
* documented on {@link readLockOwner}.
|
|
23
|
+
*/
|
|
24
|
+
export const LOCK_PID_FILE = 'pid';
|
|
25
|
+
/**
|
|
26
|
+
* Prefix of the mechanical owner-record line that carries the acquisition
|
|
27
|
+
* wall time. It lives among the metadata lines (line 3 onward) so older
|
|
28
|
+
* readers, which treat everything past line 2 as free-form diagnostics,
|
|
29
|
+
* still parse the record; the current reader lifts it out of `metadata`.
|
|
30
|
+
* Diagnostic only — see {@link START_TICK_PREFIX} for the liveness field.
|
|
31
|
+
*/
|
|
32
|
+
const ACQUIRED_AT_PREFIX = 'acquired-at-ms=';
|
|
33
|
+
/**
|
|
34
|
+
* Prefix of the owner-record line carrying the holder's own process start
|
|
35
|
+
* tick (`/proc/self/stat` field 22, in USER_HZ since boot). Written only
|
|
36
|
+
* where procfs exposes it (Linux). Boot-relative ticks are what the
|
|
37
|
+
* PID-reuse guard compares: unlike a wall-clock time they are immune to
|
|
38
|
+
* the clock being stepped by NTP/chrony/`date -s` between acquisition and
|
|
39
|
+
* probe, and a process's start tick never changes for its lifetime, so the
|
|
40
|
+
* comparison is exact rather than a tolerance window.
|
|
41
|
+
*/
|
|
42
|
+
const START_TICK_PREFIX = 'start-tick=';
|
|
43
|
+
/**
|
|
44
|
+
* How many times the post-write read-back is retried when the READ itself
|
|
45
|
+
* errors (not when the record is absent or differs). An on-access scanner
|
|
46
|
+
* on Windows briefly holding the just-written file (`EBUSY`), or a FUSE
|
|
47
|
+
* mount's momentary `EIO`, must not be indistinguishable from "the record
|
|
48
|
+
* never landed" — that verdict releases a lock this process has correctly
|
|
49
|
+
* acquired and aborts the run before it starts.
|
|
50
|
+
*/
|
|
51
|
+
const READ_BACK_ATTEMPTS = 5;
|
|
52
|
+
/** Pause between read-back attempts. */
|
|
53
|
+
const READ_BACK_RETRY_MS = 20;
|
|
54
|
+
/**
|
|
55
|
+
* Parses the on-disk record text. Returns `undefined` when the PID line
|
|
56
|
+
* does not parse as a finite integer.
|
|
57
|
+
*/
|
|
58
|
+
function parseLockOwnerRecord(pidContent) {
|
|
59
|
+
const [pidLine, tokenLine, ...trailingLines] = pidContent.split('\n');
|
|
60
|
+
const pid = parseInt((pidLine ?? '').trim(), 10);
|
|
61
|
+
if (!Number.isFinite(pid))
|
|
62
|
+
return undefined;
|
|
63
|
+
const metadata = [];
|
|
64
|
+
let acquiredAtMs;
|
|
65
|
+
let startTick;
|
|
66
|
+
for (const rawLine of trailingLines) {
|
|
67
|
+
const line = rawLine.trim();
|
|
68
|
+
if (line.length === 0)
|
|
69
|
+
continue;
|
|
70
|
+
if (line.startsWith(ACQUIRED_AT_PREFIX)) {
|
|
71
|
+
const parsed = Number.parseInt(line.slice(ACQUIRED_AT_PREFIX.length), 10);
|
|
72
|
+
if (Number.isFinite(parsed))
|
|
73
|
+
acquiredAtMs ??= parsed;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (line.startsWith(START_TICK_PREFIX)) {
|
|
77
|
+
const parsed = Number.parseInt(line.slice(START_TICK_PREFIX.length), 10);
|
|
78
|
+
if (Number.isFinite(parsed))
|
|
79
|
+
startTick ??= parsed;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
metadata.push(line);
|
|
83
|
+
}
|
|
84
|
+
const token = tokenLine?.trim();
|
|
85
|
+
const owner = { present: true, pid, metadata };
|
|
86
|
+
if (token !== undefined && token.length > 0)
|
|
87
|
+
owner.token = token;
|
|
88
|
+
if (acquiredAtMs !== undefined)
|
|
89
|
+
owner.acquiredAtMs = acquiredAtMs;
|
|
90
|
+
if (startTick !== undefined)
|
|
91
|
+
owner.startTick = startTick;
|
|
92
|
+
return owner;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Reads the owner PID (line 1), acquisition token (line 2), the mechanical
|
|
96
|
+
* `acquired-at-ms=` / `start-tick=` lines (when present), and diagnostic
|
|
97
|
+
* metadata (the remaining lines 3+) from a lock directory's PID file.
|
|
98
|
+
* Returns `{ present: false }` when the PID file is missing, unreadable, or
|
|
99
|
+
* the PID does not parse as a finite integer (caller falls back to the
|
|
100
|
+
* age-only staleness heuristic).
|
|
101
|
+
*
|
|
102
|
+
* File format note: external readers do `parseInt(content.trim(), 10)`,
|
|
103
|
+
* which parses the leading digits of a multi-line file — so the token and
|
|
104
|
+
* mechanical lines stay compatible.
|
|
105
|
+
*
|
|
106
|
+
* @param lockPath - Lock directory whose owner record to read
|
|
107
|
+
* @returns The parsed owner record, or `{ present: false }`
|
|
108
|
+
*/
|
|
109
|
+
export async function readLockOwner(lockPath) {
|
|
110
|
+
try {
|
|
111
|
+
const owner = parseLockOwnerRecord(await readFile(join(lockPath, LOCK_PID_FILE), 'utf-8'));
|
|
112
|
+
if (owner !== undefined)
|
|
113
|
+
return owner;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// PID file missing or unreadable — treat as absent.
|
|
117
|
+
}
|
|
118
|
+
return { present: false };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Reads the just-written record back, retrying a READ ERROR a few times
|
|
122
|
+
* before giving up. A missing or malformed record is returned as-is (the
|
|
123
|
+
* caller treats it as a failed write); only errno-level failures retry.
|
|
124
|
+
*/
|
|
125
|
+
async function readBackLockOwner(lockPath) {
|
|
126
|
+
let lastError;
|
|
127
|
+
for (let attempt = 0; attempt < READ_BACK_ATTEMPTS; attempt++) {
|
|
128
|
+
try {
|
|
129
|
+
return parseLockOwnerRecord(await readFile(join(lockPath, LOCK_PID_FILE), 'utf-8'));
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
lastError = error;
|
|
133
|
+
if (attempt < READ_BACK_ATTEMPTS - 1)
|
|
134
|
+
await sleep(READ_BACK_RETRY_MS);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
throw new Error(`owner record could not be read back: ${toError(lastError).message}`);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Stamps this process's ownership (PID, per-acquisition token, acquisition
|
|
141
|
+
* time, own start tick, diagnostic metadata) into a lock directory it has
|
|
142
|
+
* just created, and reads the record back to prove it landed.
|
|
143
|
+
*
|
|
144
|
+
* The write is FATAL. It used to be best-effort, on the theory that stale
|
|
145
|
+
* recovery would fall back to the age heuristic — but that heuristic reaps
|
|
146
|
+
* any owner-less lock after five minutes, and the build lock legitimately
|
|
147
|
+
* holds for hours. A holder whose record failed to write was therefore
|
|
148
|
+
* reaped mid-build and a second process entered the critical section. On
|
|
149
|
+
* failure the just-created lock directory is removed (nothing else can own
|
|
150
|
+
* it yet) and a {@link GeneralError} names the cause.
|
|
151
|
+
*
|
|
152
|
+
* @param lockPath - Lock directory this process has just `mkdir`'d
|
|
153
|
+
* @param token - Per-acquisition UUID, later checked by release
|
|
154
|
+
* @param ownerMetadata - Extra human-diagnostic lines (see `FileLockOptions.ownerMetadata`)
|
|
155
|
+
* @throws {GeneralError} When the record cannot be written or does not read back as written
|
|
156
|
+
*/
|
|
157
|
+
export async function writeLockOwner(lockPath, token, ownerMetadata) {
|
|
158
|
+
const metadata = ownerMetadata
|
|
159
|
+
?.map((line) => line.replace(/\r?\n/g, ' ').trim())
|
|
160
|
+
.filter((line) => line.length > 0 &&
|
|
161
|
+
!line.startsWith(ACQUIRED_AT_PREFIX) &&
|
|
162
|
+
!line.startsWith(START_TICK_PREFIX)) ?? [];
|
|
163
|
+
const ownStartTick = readProcessStartTick(process.pid);
|
|
164
|
+
const mechanicalLines = [
|
|
165
|
+
`${ACQUIRED_AT_PREFIX}${Date.now()}`,
|
|
166
|
+
...(ownStartTick === undefined ? [] : [`${START_TICK_PREFIX}${ownStartTick}`]),
|
|
167
|
+
];
|
|
168
|
+
const record = `${process.pid}\n${token}\n${[...mechanicalLines, ...metadata].map((line) => `${line}\n`).join('')}`;
|
|
169
|
+
let failure;
|
|
170
|
+
try {
|
|
171
|
+
await writeFile(join(lockPath, LOCK_PID_FILE), record, 'utf-8');
|
|
172
|
+
const readBack = await readBackLockOwner(lockPath);
|
|
173
|
+
if (readBack !== undefined && readBack.pid === process.pid && readBack.token === token) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
failure = new Error('owner record did not read back as written');
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
failure = error;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
catch (cleanupError) {
|
|
185
|
+
verbose(`Could not remove unowned lock ${lockPath}: ${toError(cleanupError).message}`);
|
|
186
|
+
}
|
|
187
|
+
throw new GeneralError(`Could not record ownership of lock ${lockPath}: ${toError(failure).message}. ` +
|
|
188
|
+
'The lock was released without running the operation — a lock with no readable owner ' +
|
|
189
|
+
'would be reaped as stale after five minutes and a second process could enter the ' +
|
|
190
|
+
'critical section. Check permissions and free space on the lock directory, then retry.', failure);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Boot-relative start tick of a process, where the platform exposes it
|
|
194
|
+
* cheaply.
|
|
195
|
+
*
|
|
196
|
+
* Linux only: field 22 of `/proc/<pid>/stat`, in USER_HZ since boot.
|
|
197
|
+
* Elsewhere (macOS, Windows) obtaining a start time means spawning
|
|
198
|
+
* `ps`/`wmic` per probe, which is not worth paying on every poll of a
|
|
199
|
+
* contended lock — those platforms return `undefined` and liveness stays
|
|
200
|
+
* PID-only.
|
|
201
|
+
*
|
|
202
|
+
* Deliberately NOT converted to wall-clock time via `/proc/stat` `btime`:
|
|
203
|
+
* on current kernels `btime` is derived from the realtime clock and moves
|
|
204
|
+
* with every clock step, so a wall-clock comparison against a recorded
|
|
205
|
+
* acquisition time misjudges a live holder as a reused PID after NTP steps
|
|
206
|
+
* the clock forward — and reaps a lock that is in use.
|
|
207
|
+
*
|
|
208
|
+
* @param pid - Process to look up
|
|
209
|
+
* @param procRoot - procfs mount, overridable for tests
|
|
210
|
+
* @returns Start tick, or `undefined` when unobtainable
|
|
211
|
+
*/
|
|
212
|
+
export function readProcessStartTick(pid, procRoot = '/proc') {
|
|
213
|
+
try {
|
|
214
|
+
const stat = readFileSync(join(procRoot, String(pid), 'stat'), 'utf-8');
|
|
215
|
+
// `comm` (field 2) is parenthesised and may itself contain spaces or
|
|
216
|
+
// parentheses, so split only what follows the LAST closing paren:
|
|
217
|
+
// field 3 (state) is then index 0 and field 22 (starttime) index 19.
|
|
218
|
+
const startTick = Number.parseInt(stat
|
|
219
|
+
.slice(stat.lastIndexOf(')') + 1)
|
|
220
|
+
.trim()
|
|
221
|
+
.split(/\s+/)[19] ?? '', 10);
|
|
222
|
+
return Number.isFinite(startTick) ? startTick : undefined;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Whether the process named by a lock's owner record is the SAME process
|
|
230
|
+
* that acquired the lock, not merely a live process wearing its PID.
|
|
231
|
+
*
|
|
232
|
+
* `isProcessAlive` answers "does this PID exist"; after a holder crashes,
|
|
233
|
+
* the kernel may hand its PID to a long-lived daemon, and a PID-only check
|
|
234
|
+
* then honours the dead lock until the waiter's timeout. Where the record
|
|
235
|
+
* carries the writer's start tick and the candidate's own tick is cheaply
|
|
236
|
+
* obtainable (see {@link readProcessStartTick}), the two must be equal: a
|
|
237
|
+
* different tick is a different process. Records without a start tick, and
|
|
238
|
+
* platforms without one, keep the PID-only answer.
|
|
239
|
+
*
|
|
240
|
+
* @param owner - Owner record fields that identify the holder
|
|
241
|
+
* @param procRoot - procfs mount, overridable for tests
|
|
242
|
+
* @returns True when the original holder is still running (or cannot be proven gone)
|
|
243
|
+
*/
|
|
244
|
+
export function isLockOwnerAlive(owner, procRoot) {
|
|
245
|
+
if (!isProcessAlive(owner.pid))
|
|
246
|
+
return false;
|
|
247
|
+
if (owner.startTick === undefined)
|
|
248
|
+
return true;
|
|
249
|
+
const currentStartTick = readProcessStartTick(owner.pid, procRoot);
|
|
250
|
+
if (currentStartTick === undefined)
|
|
251
|
+
return true;
|
|
252
|
+
if (currentStartTick !== owner.startTick) {
|
|
253
|
+
verbose(`Lock owner PID ${owner.pid} started at tick ${currentStartTick}, but the ` +
|
|
254
|
+
`record was written by a process started at tick ${owner.startTick} — the PID was ` +
|
|
255
|
+
'reused; treating the holder as dead');
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LOCK_PID_FILE } from './file-lock-owner.js';
|
|
2
|
+
export { LOCK_PID_FILE };
|
|
1
3
|
/**
|
|
2
4
|
* Snapshot of the current lock holder handed to {@link FileLockOptions.onWaitProgress}.
|
|
3
5
|
* `metadata` carries the pid file's human-diagnostic lines (line 3 onward,
|
|
@@ -95,13 +97,6 @@ export interface FileLockOptions {
|
|
|
95
97
|
}
|
|
96
98
|
/** Derives the sibling lock-directory path used to guard a file-based resource. */
|
|
97
99
|
export declare function createSiblingLockPath(filePath: string, suffix?: string): string;
|
|
98
|
-
/**
|
|
99
|
-
* Filename of a lock directory's owner record, relative to the lock dir.
|
|
100
|
-
*
|
|
101
|
-
* Exported so external readers (`tree-store.ts`) share the constant instead of
|
|
102
|
-
* re-spelling `'pid'`; the file *format* is documented on {@link readLockOwner}.
|
|
103
|
-
*/
|
|
104
|
-
export declare const LOCK_PID_FILE = "pid";
|
|
105
100
|
/**
|
|
106
101
|
* Reads the live waiter queue around `lockPath`, dropping entries whose
|
|
107
102
|
* process is gone (a killed waiter must not inflate the depth forever).
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { mkdir, readdir,
|
|
3
|
+
import { mkdir, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { LockContentionError } from '../errors/base.js';
|
|
6
6
|
import { getNodeErrorCode, isProcessAlive, toError } from '../utils/errors.js';
|
|
7
7
|
import { ensureDir } from '../utils/fs.js';
|
|
8
8
|
import { verbose, warn } from '../utils/logger.js';
|
|
9
|
+
import { sleep } from '../utils/sleep.js';
|
|
10
|
+
import { isLockOwnerAlive, LOCK_PID_FILE, readLockOwner, writeLockOwner, } from './file-lock-owner.js';
|
|
11
|
+
export { LOCK_PID_FILE };
|
|
9
12
|
const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
|
|
10
13
|
const DEFAULT_LOCK_POLL_MS = 50;
|
|
11
14
|
const DEFAULT_STALE_LOCK_MS = 5 * 60_000;
|
|
@@ -23,52 +26,10 @@ const STALE_REPROBE_INTERVAL_MS = 5_000;
|
|
|
23
26
|
* deadline extension but no progress line of its own.
|
|
24
27
|
*/
|
|
25
28
|
const WAIT_PROBE_INTERVAL_MS = 5_000;
|
|
26
|
-
function sleep(ms) {
|
|
27
|
-
return new Promise((resolve) => {
|
|
28
|
-
setTimeout(resolve, ms);
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
29
|
/** Derives the sibling lock-directory path used to guard a file-based resource. */
|
|
32
30
|
export function createSiblingLockPath(filePath, suffix = '.fireforge.lock') {
|
|
33
31
|
return `${filePath}${suffix}`;
|
|
34
32
|
}
|
|
35
|
-
/**
|
|
36
|
-
* Filename of a lock directory's owner record, relative to the lock dir.
|
|
37
|
-
*
|
|
38
|
-
* Exported so external readers (`tree-store.ts`) share the constant instead of
|
|
39
|
-
* re-spelling `'pid'`; the file *format* is documented on {@link readLockOwner}.
|
|
40
|
-
*/
|
|
41
|
-
export const LOCK_PID_FILE = 'pid';
|
|
42
|
-
/**
|
|
43
|
-
* Reads the owner PID (line 1), acquisition token (line 2), and diagnostic
|
|
44
|
-
* metadata (lines 3+, see {@link FileLockOptions.ownerMetadata}) from a lock
|
|
45
|
-
* directory's PID file. Returns `{ present: false }` when the PID file is
|
|
46
|
-
* missing or the PID does not parse as a finite integer (caller falls back
|
|
47
|
-
* to the age-only staleness heuristic).
|
|
48
|
-
*
|
|
49
|
-
* File format note: external readers do `parseInt(content.trim(), 10)`,
|
|
50
|
-
* which parses the leading digits of a multi-line file — so the token line
|
|
51
|
-
* stays compatible.
|
|
52
|
-
*/
|
|
53
|
-
async function readLockOwner(lockPath) {
|
|
54
|
-
try {
|
|
55
|
-
const pidContent = await readFile(join(lockPath, LOCK_PID_FILE), 'utf-8');
|
|
56
|
-
const [pidLine, tokenLine, ...metadataLines] = pidContent.split('\n');
|
|
57
|
-
const pid = parseInt((pidLine ?? '').trim(), 10);
|
|
58
|
-
if (Number.isFinite(pid)) {
|
|
59
|
-
const metadata = metadataLines.map((line) => line.trim()).filter((line) => line.length > 0);
|
|
60
|
-
const token = tokenLine?.trim();
|
|
61
|
-
if (token !== undefined && token.length > 0) {
|
|
62
|
-
return { present: true, pid, token, metadata };
|
|
63
|
-
}
|
|
64
|
-
return { present: true, pid, metadata };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
// PID file missing or unreadable — treat as absent.
|
|
69
|
-
}
|
|
70
|
-
return { present: false };
|
|
71
|
-
}
|
|
72
33
|
/** Sibling directory in which waiters advertise themselves. */
|
|
73
34
|
function lockWaitersDir(lockPath) {
|
|
74
35
|
return `${lockPath}.waiters`;
|
|
@@ -127,7 +88,7 @@ export async function readLockStatus(lockPath) {
|
|
|
127
88
|
}
|
|
128
89
|
const owner = await readLockOwner(lockPath);
|
|
129
90
|
const holder = owner.present
|
|
130
|
-
? { pid: owner.pid, alive:
|
|
91
|
+
? { pid: owner.pid, alive: isLockOwnerAlive(owner), metadata: owner.metadata }
|
|
131
92
|
: undefined;
|
|
132
93
|
return { held: true, holder, heldForMs, queueDepth: queue.depth };
|
|
133
94
|
}
|
|
@@ -138,10 +99,10 @@ export async function readLockStatus(lockPath) {
|
|
|
138
99
|
*/
|
|
139
100
|
async function registerWaiter(lockPath, startedAt) {
|
|
140
101
|
const dir = lockWaitersDir(lockPath);
|
|
141
|
-
const file = join(dir, `${
|
|
102
|
+
const file = join(dir, `${startedAt}-${process.pid}-${randomUUID()}`);
|
|
142
103
|
try {
|
|
143
104
|
await ensureDir(dir);
|
|
144
|
-
await writeFile(file, `${
|
|
105
|
+
await writeFile(file, `${process.pid}\n`, 'utf-8');
|
|
145
106
|
}
|
|
146
107
|
catch (error) {
|
|
147
108
|
// Advisory only — never fail a lock acquisition over the queue display.
|
|
@@ -164,7 +125,7 @@ async function registerWaiter(lockPath, startedAt) {
|
|
|
164
125
|
async function probeWaitState(lockPath, startedAt) {
|
|
165
126
|
const owner = await readLockOwner(lockPath);
|
|
166
127
|
const holder = owner.present
|
|
167
|
-
? { pid: owner.pid, alive:
|
|
128
|
+
? { pid: owner.pid, alive: isLockOwnerAlive(owner), metadata: owner.metadata }
|
|
168
129
|
: undefined;
|
|
169
130
|
return { holder, queue: await readLockQueue(lockPath, startedAt) };
|
|
170
131
|
}
|
|
@@ -189,7 +150,7 @@ async function probeWaitState(lockPath, startedAt) {
|
|
|
189
150
|
* legitimately held and the caller should keep waiting.
|
|
190
151
|
*/
|
|
191
152
|
async function reapStaleLock(lockPath, ageMs, onStaleLockMessage) {
|
|
192
|
-
const reapPath = `${lockPath}.reaping-${
|
|
153
|
+
const reapPath = `${lockPath}.reaping-${process.pid}-${randomUUID()}`;
|
|
193
154
|
try {
|
|
194
155
|
await rename(lockPath, reapPath);
|
|
195
156
|
}
|
|
@@ -205,16 +166,16 @@ async function reapStaleLock(lockPath, ageMs, onStaleLockMessage) {
|
|
|
205
166
|
// We exclusively own reapPath now. Re-verify: the owner may have changed
|
|
206
167
|
// between the staleness probe and the rename (release + fresh acquire).
|
|
207
168
|
const owner = await readLockOwner(reapPath);
|
|
208
|
-
if (owner.present &&
|
|
169
|
+
if (owner.present && isLockOwnerAlive(owner)) {
|
|
209
170
|
try {
|
|
210
171
|
await rename(reapPath, lockPath);
|
|
211
|
-
verbose(`Aborted stale-lock reap of ${lockPath}: owner PID ${
|
|
172
|
+
verbose(`Aborted stale-lock reap of ${lockPath}: owner PID ${owner.pid} is alive (lock was re-acquired mid-probe)`);
|
|
212
173
|
}
|
|
213
174
|
catch (restoreError) {
|
|
214
175
|
// Rename-back can only fail if a third process mkdir'd the lock path
|
|
215
176
|
// in this instant. The displaced live owner keeps running without its
|
|
216
177
|
// lock — surface it loudly instead of hiding the lost exclusion.
|
|
217
|
-
warn(`Could not restore live lock for PID ${
|
|
178
|
+
warn(`Could not restore live lock for PID ${owner.pid} at ${lockPath} ` +
|
|
218
179
|
`(${toError(restoreError).message}). Displaced lock kept at ${reapPath} for inspection.`);
|
|
219
180
|
}
|
|
220
181
|
return false;
|
|
@@ -246,13 +207,14 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
|
246
207
|
// PID-first check an explicitly-dead owner unblocks immediately.
|
|
247
208
|
const owner = await readLockOwner(lockPath);
|
|
248
209
|
if (owner.present) {
|
|
249
|
-
if (!
|
|
210
|
+
if (!isLockOwnerAlive(owner)) {
|
|
250
211
|
return await reapStaleLock(lockPath, ageMs, onStaleLockMessage);
|
|
251
212
|
}
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
213
|
+
// The owner is alive (PID exists AND, where `/proc` can tell, it is
|
|
214
|
+
// the process that acquired the lock, not a recycled PID) — respect
|
|
215
|
+
// it regardless of age. A slow `mach build` legitimately holds the
|
|
216
|
+
// lock past the stale threshold and we don't want to race-remove it.
|
|
217
|
+
verbose(`Lock at ${lockPath} is ${Math.round(ageMs / 1000)}s old but PID ${owner.pid} is still running — not removing`);
|
|
256
218
|
return false;
|
|
257
219
|
}
|
|
258
220
|
// No readable PID file. Fall back to the age-only heuristic so locks
|
|
@@ -281,19 +243,16 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
|
281
243
|
* double-acquisition. When the PID file no longer names us, warn and leave
|
|
282
244
|
* the directory alone.
|
|
283
245
|
*
|
|
284
|
-
*
|
|
285
|
-
* acquisition
|
|
286
|
-
* removal is unconditional, which is strictly better than leaking the lock.
|
|
246
|
+
* The owner record is always present here: `writeLockOwner` is fatal, so an
|
|
247
|
+
* acquisition whose record failed to write never reached the operation.
|
|
287
248
|
*/
|
|
288
|
-
async function releaseLock(lockPath, ownerToken
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
249
|
+
async function releaseLock(lockPath, ownerToken) {
|
|
250
|
+
const owner = await readLockOwner(lockPath);
|
|
251
|
+
if (owner.present &&
|
|
252
|
+
(owner.pid !== process.pid || (owner.token !== undefined && owner.token !== ownerToken))) {
|
|
253
|
+
warn(`Not removing lock ${lockPath}: it is now owned by PID ${owner.pid} — ` +
|
|
254
|
+
'this process no longer holds it.');
|
|
255
|
+
return;
|
|
297
256
|
}
|
|
298
257
|
await rm(lockPath, { recursive: true, force: true });
|
|
299
258
|
}
|
|
@@ -339,19 +298,17 @@ export async function releaseLockIfOwned(lockPath) {
|
|
|
339
298
|
*/
|
|
340
299
|
async function buildLockTimeoutError(lockPath, onTimeoutMessage, cause, queue) {
|
|
341
300
|
const owner = await readLockOwner(lockPath);
|
|
342
|
-
const liveHolder = owner.present &&
|
|
301
|
+
const liveHolder = owner.present && isLockOwnerAlive(owner) ? owner : undefined;
|
|
343
302
|
const details = liveHolder !== undefined && liveHolder.metadata.length > 0
|
|
344
303
|
? ` (${liveHolder.metadata.join(', ')})`
|
|
345
304
|
: '';
|
|
346
305
|
const position = formatQueuePositionReached(queue);
|
|
347
306
|
if (onTimeoutMessage !== undefined) {
|
|
348
|
-
const identifiedHolder = liveHolder !== undefined
|
|
349
|
-
? ` The lock is held by PID ${String(liveHolder.pid)}${details}.`
|
|
350
|
-
: '';
|
|
307
|
+
const identifiedHolder = liveHolder !== undefined ? ` The lock is held by PID ${liveHolder.pid}${details}.` : '';
|
|
351
308
|
return new LockContentionError(`${onTimeoutMessage}${identifiedHolder}${position}`, cause);
|
|
352
309
|
}
|
|
353
310
|
const holderHint = liveHolder !== undefined
|
|
354
|
-
? ` It is held by running process ${
|
|
311
|
+
? ` It is held by running process ${liveHolder.pid}${details} — wait for it to finish, or stop it and retry.`
|
|
355
312
|
: ' If no other FireForge process is running, remove the lock directory and retry.';
|
|
356
313
|
return new LockContentionError(`Timed out waiting for file lock ${lockPath}.${holderHint}${position}`, cause);
|
|
357
314
|
}
|
|
@@ -369,9 +326,9 @@ function formatQueuePositionReached(queue) {
|
|
|
369
326
|
if (queue === undefined || queue.depth === 0)
|
|
370
327
|
return '';
|
|
371
328
|
if (queue.ahead === 0) {
|
|
372
|
-
return ` You were next in a queue of ${
|
|
329
|
+
return ` You were next in a queue of ${queue.depth} when the wait expired — a larger --wait-lock would very likely have got a turn.`;
|
|
373
330
|
}
|
|
374
|
-
return ` You were still ${
|
|
331
|
+
return ` You were still ${queue.ahead} from the head of a queue of ${queue.depth} when the wait expired; re-run with a larger --wait-lock.`;
|
|
375
332
|
}
|
|
376
333
|
/**
|
|
377
334
|
* Runs one wait probe: reads holder and queue, and reports progress when the
|
|
@@ -515,28 +472,20 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
515
472
|
// acquired the lock or timed out.
|
|
516
473
|
await deregisterWaiter?.();
|
|
517
474
|
}
|
|
518
|
-
// Stamp ownership (PID + per-acquisition token
|
|
519
|
-
// stale-lock recovery can check liveness and release
|
|
520
|
-
// is still ours before removing it.
|
|
475
|
+
// Stamp ownership (PID + per-acquisition token + acquisition time) into
|
|
476
|
+
// the lock directory so stale-lock recovery can check liveness and release
|
|
477
|
+
// can verify the lock is still ours before removing it. Fatal on failure:
|
|
478
|
+
// an owner-less lock is reaped by the age heuristic after five minutes,
|
|
479
|
+
// which for a multi-hour build meant a second holder mid-critical-section.
|
|
480
|
+
// `writeLockOwner` removes the just-created directory before throwing.
|
|
521
481
|
const ownerToken = randomUUID();
|
|
522
|
-
|
|
523
|
-
try {
|
|
524
|
-
const metadata = options.ownerMetadata
|
|
525
|
-
?.map((line) => line.replace(/\r?\n/g, ' ').trim())
|
|
526
|
-
.filter((line) => line.length > 0) ?? [];
|
|
527
|
-
await writeFile(join(lockPath, LOCK_PID_FILE), `${String(process.pid)}\n${ownerToken}\n${metadata.length > 0 ? `${metadata.join('\n')}\n` : ''}`, 'utf-8');
|
|
528
|
-
ownerFileWritten = true;
|
|
529
|
-
}
|
|
530
|
-
catch {
|
|
531
|
-
// Non-fatal: stale recovery falls back to age-only heuristic and release
|
|
532
|
-
// falls back to unconditional removal.
|
|
533
|
-
}
|
|
482
|
+
await writeLockOwner(lockPath, ownerToken, options.ownerMetadata);
|
|
534
483
|
heldLocks.add(lockPath);
|
|
535
484
|
try {
|
|
536
485
|
return await operation();
|
|
537
486
|
}
|
|
538
487
|
finally {
|
|
539
488
|
heldLocks.delete(lockPath);
|
|
540
|
-
await releaseLock(lockPath, ownerToken
|
|
489
|
+
await releaseLock(lockPath, ownerToken);
|
|
541
490
|
}
|
|
542
491
|
}
|
|
@@ -3,13 +3,25 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ResolvedArchive } from './firefox-archive.js';
|
|
5
5
|
import type { ProgressCallback } from './firefox-download.js';
|
|
6
|
+
/** Options governing the default (unpinned) integrity check. */
|
|
7
|
+
export interface ArchiveIntegrityOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Accept the download with a warning when the published SHA256SUMS cannot
|
|
10
|
+
* be obtained (`firefox.allowUnverifiedDownload`). Default: fail closed.
|
|
11
|
+
*/
|
|
12
|
+
allowUnverifiedDownload?: boolean;
|
|
13
|
+
}
|
|
6
14
|
/**
|
|
7
15
|
* Ensures a valid cached archive exists, downloading it if needed.
|
|
8
16
|
* @param archive - Resolved archive descriptor
|
|
9
17
|
* @param cacheDir - Cache directory
|
|
10
18
|
* @param onProgress - Optional progress callback
|
|
19
|
+
* @param expectedSha256 - Operator-pinned digest; when set it replaces the
|
|
20
|
+
* published-SHA256SUMS check
|
|
21
|
+
* @param onCacheProgress - Phase progress messages
|
|
22
|
+
* @param integrity - Default integrity-check options (unpinned downloads)
|
|
11
23
|
*/
|
|
12
|
-
export declare function ensureCachedArchive(archive: ResolvedArchive, cacheDir: string, onProgress?: ProgressCallback, expectedSha256?: string, onCacheProgress?: (message: string) => void): Promise<void>;
|
|
24
|
+
export declare function ensureCachedArchive(archive: ResolvedArchive, cacheDir: string, onProgress?: ProgressCallback, expectedSha256?: string, onCacheProgress?: (message: string) => void, integrity?: ArchiveIntegrityOptions): Promise<void>;
|
|
13
25
|
/**
|
|
14
26
|
* Removes cached tarball, metadata, and partial download files for an archive.
|
|
15
27
|
* @param archive - Resolved archive descriptor
|