@hominis/fireforge 0.45.0 → 0.46.1
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 +409 -446
- package/dist/bin/fireforge.d.ts +1 -1
- package/dist/bin/fireforge.js +21 -15
- 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 +7 -2
- package/dist/src/cli.js +47 -47
- package/dist/src/commands/bootstrap-checks.js +2 -1
- package/dist/src/commands/bootstrap.js +3 -4
- package/dist/src/commands/build.js +27 -17
- package/dist/src/commands/config.js +9 -9
- package/dist/src/commands/destructive-decision.d.ts +19 -0
- package/dist/src/commands/destructive-decision.js +31 -0
- package/dist/src/commands/discard.js +5 -5
- package/dist/src/commands/doctor-check-core.d.ts +4 -10
- package/dist/src/commands/doctor-check-core.js +0 -8
- package/dist/src/commands/doctor-external-toolchains.js +2 -2
- package/dist/src/commands/doctor-furnace-config-sync.d.ts +2 -2
- package/dist/src/commands/doctor-furnace-config-sync.js +20 -20
- package/dist/src/commands/doctor-furnace-jar.d.ts +1 -1
- package/dist/src/commands/doctor-furnace-jar.js +1 -1
- package/dist/src/commands/doctor-furnace.js +22 -22
- package/dist/src/commands/doctor-orphaned-harness.d.ts +10 -17
- package/dist/src/commands/doctor-orphaned-harness.js +14 -23
- package/dist/src/commands/doctor-patch-manifest.d.ts +2 -1
- package/dist/src/commands/doctor-patch-manifest.js +7 -6
- package/dist/src/commands/doctor-post-rebase-audit.js +3 -3
- package/dist/src/commands/doctor-source-pin.d.ts +12 -12
- package/dist/src/commands/doctor-source-pin.js +14 -14
- package/dist/src/commands/doctor-working-tree.d.ts +3 -3
- package/dist/src/commands/doctor-working-tree.js +3 -3
- package/dist/src/commands/doctor.d.ts +3 -1
- package/dist/src/commands/doctor.js +15 -16
- package/dist/src/commands/download.js +50 -37
- package/dist/src/commands/export-all.js +29 -12
- package/dist/src/commands/export-flow.d.ts +5 -5
- package/dist/src/commands/export-flow.js +29 -59
- package/dist/src/commands/export-placement-conflicts.d.ts +3 -3
- package/dist/src/commands/export-placement-conflicts.js +12 -12
- package/dist/src/commands/export-placement-gate.d.ts +2 -2
- package/dist/src/commands/export-placement-gate.js +7 -13
- package/dist/src/commands/export-placement-policy.d.ts +1 -1
- package/dist/src/commands/export-placement-policy.js +4 -6
- package/dist/src/commands/export-shared.d.ts +51 -32
- package/dist/src/commands/export-shared.js +17 -32
- package/dist/src/commands/export.js +14 -5
- package/dist/src/commands/furnace/apply.js +16 -16
- 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.d.ts +1 -1
- package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +14 -14
- package/dist/src/commands/furnace/chrome-doc-templates.js +17 -17
- package/dist/src/commands/furnace/chrome-doc-tests.d.ts +2 -2
- package/dist/src/commands/furnace/chrome-doc-tests.js +4 -4
- package/dist/src/commands/furnace/chrome-doc.d.ts +8 -8
- package/dist/src/commands/furnace/chrome-doc.js +18 -18
- package/dist/src/commands/furnace/create-browser-test.d.ts +1 -1
- package/dist/src/commands/furnace/create-browser-test.js +7 -12
- package/dist/src/commands/furnace/create-dry-run.d.ts +4 -4
- package/dist/src/commands/furnace/create-dry-run.js +6 -9
- package/dist/src/commands/furnace/create-features.d.ts +2 -2
- package/dist/src/commands/furnace/create-features.js +2 -2
- package/dist/src/commands/furnace/create-mochikit.d.ts +4 -4
- package/dist/src/commands/furnace/create-mochikit.js +7 -7
- package/dist/src/commands/furnace/create-templates.d.ts +31 -15
- package/dist/src/commands/furnace/create-templates.js +20 -19
- package/dist/src/commands/furnace/create-xpcshell.d.ts +5 -5
- package/dist/src/commands/furnace/create-xpcshell.js +8 -8
- package/dist/src/commands/furnace/create.d.ts +1 -1
- package/dist/src/commands/furnace/create.js +35 -29
- package/dist/src/commands/furnace/deploy.js +15 -6
- package/dist/src/commands/furnace/diff.js +7 -7
- package/dist/src/commands/furnace/index.js +1 -1
- package/dist/src/commands/furnace/init.js +17 -16
- package/dist/src/commands/furnace/list.js +2 -2
- package/dist/src/commands/furnace/override.d.ts +1 -1
- package/dist/src/commands/furnace/override.js +43 -34
- package/dist/src/commands/furnace/preview.js +17 -17
- package/dist/src/commands/furnace/refresh.js +15 -7
- package/dist/src/commands/furnace/remove-mutations.d.ts +9 -10
- package/dist/src/commands/furnace/remove-mutations.js +19 -19
- package/dist/src/commands/furnace/remove-state.d.ts +1 -1
- package/dist/src/commands/furnace/remove-state.js +1 -1
- package/dist/src/commands/furnace/remove.js +4 -8
- package/dist/src/commands/furnace/rename-browser-test.js +10 -15
- package/dist/src/commands/furnace/rename-xpcshell.d.ts +2 -2
- package/dist/src/commands/furnace/rename-xpcshell.js +3 -3
- package/dist/src/commands/furnace/rename.js +36 -36
- package/dist/src/commands/furnace/scan.js +3 -3
- package/dist/src/commands/furnace/status.js +19 -16
- package/dist/src/commands/furnace/sync.js +6 -6
- 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/validate.js +11 -12
- 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 +22 -29
- package/dist/src/commands/lint-per-patch.d.ts +2 -2
- package/dist/src/commands/lint-per-patch.js +27 -12
- package/dist/src/commands/lint-per-run-checkjs.d.ts +14 -15
- package/dist/src/commands/lint-per-run-checkjs.js +14 -15
- package/dist/src/commands/lint-report.d.ts +3 -3
- package/dist/src/commands/lint-report.js +4 -4
- package/dist/src/commands/lint.d.ts +5 -4
- package/dist/src/commands/lint.js +36 -36
- package/dist/src/commands/manifest.d.ts +4 -4
- package/dist/src/commands/manifest.js +1 -1
- package/dist/src/commands/package.js +4 -6
- package/dist/src/commands/patch/compact.d.ts +2 -2
- package/dist/src/commands/patch/compact.js +19 -20
- package/dist/src/commands/patch/delete.d.ts +1 -1
- package/dist/src/commands/patch/delete.js +23 -33
- 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 +3 -12
- package/dist/src/commands/patch/lint-ignore.js +33 -44
- package/dist/src/commands/patch/move-files-create.d.ts +2 -2
- package/dist/src/commands/patch/move-files-create.js +29 -15
- package/dist/src/commands/patch/move-files-into.d.ts +2 -2
- package/dist/src/commands/patch/move-files-into.js +41 -37
- package/dist/src/commands/patch/move-files.js +5 -14
- package/dist/src/commands/patch/patch-context.d.ts +1 -1
- package/dist/src/commands/patch/rename.d.ts +4 -4
- package/dist/src/commands/patch/rename.js +35 -49
- package/dist/src/commands/patch/reorder.js +35 -48
- package/dist/src/commands/patch/split-plan.d.ts +7 -8
- package/dist/src/commands/patch/split-plan.js +10 -13
- package/dist/src/commands/patch/split.d.ts +2 -2
- package/dist/src/commands/patch/split.js +50 -48
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +1 -1
- package/dist/src/commands/patch/staged-dependency-validate.js +3 -3
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -1
- package/dist/src/commands/patch/staged-dependency.js +22 -28
- package/dist/src/commands/patch/tier.d.ts +2 -2
- package/dist/src/commands/patch/tier.js +20 -30
- 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 +25 -3
- package/dist/src/commands/re-export-adjacent.js +40 -24
- package/dist/src/commands/re-export-bulk-scan.d.ts +3 -3
- package/dist/src/commands/re-export-bulk-scan.js +23 -27
- package/dist/src/commands/re-export-drift.d.ts +10 -10
- package/dist/src/commands/re-export-drift.js +33 -33
- package/dist/src/commands/re-export-files.d.ts +2 -3
- package/dist/src/commands/re-export-files.js +52 -44
- package/dist/src/commands/re-export-lint.d.ts +29 -30
- package/dist/src/commands/re-export-lint.js +36 -29
- package/dist/src/commands/re-export-options.d.ts +1 -1
- package/dist/src/commands/re-export-options.js +1 -1
- package/dist/src/commands/re-export-register.d.ts +1 -1
- package/dist/src/commands/re-export-register.js +1 -1
- package/dist/src/commands/re-export-scan.d.ts +1 -1
- package/dist/src/commands/re-export-scan.js +12 -9
- package/dist/src/commands/re-export-single.d.ts +23 -4
- package/dist/src/commands/re-export-single.js +39 -31
- package/dist/src/commands/re-export.js +35 -17
- package/dist/src/commands/rebase/abort.js +9 -14
- package/dist/src/commands/rebase/continue.js +20 -19
- package/dist/src/commands/rebase/index.d.ts +1 -1
- package/dist/src/commands/rebase/index.js +5 -5
- package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
- package/dist/src/commands/rebase/patch-loop.js +20 -24
- package/dist/src/commands/register.js +5 -5
- package/dist/src/commands/reset.js +1 -1
- package/dist/src/commands/resolve.d.ts +2 -2
- package/dist/src/commands/resolve.js +21 -13
- package/dist/src/commands/run.d.ts +0 -13
- package/dist/src/commands/run.js +38 -51
- package/dist/src/commands/setup-support.d.ts +16 -4
- package/dist/src/commands/setup-support.js +20 -20
- package/dist/src/commands/setup.js +4 -10
- package/dist/src/commands/source.js +9 -2
- package/dist/src/commands/status-check.d.ts +3 -3
- package/dist/src/commands/status-check.js +6 -6
- package/dist/src/commands/status-json.d.ts +14 -13
- package/dist/src/commands/status-json.js +9 -8
- package/dist/src/commands/status-output.d.ts +3 -3
- package/dist/src/commands/status-output.js +5 -5
- package/dist/src/commands/status-ownership.d.ts +3 -3
- package/dist/src/commands/status-ownership.js +3 -3
- package/dist/src/commands/status.js +25 -26
- package/dist/src/commands/test-build-phase.d.ts +3 -3
- package/dist/src/commands/test-build-phase.js +48 -24
- package/dist/src/commands/test-diagnose.d.ts +1 -1
- package/dist/src/commands/test-diagnose.js +20 -20
- package/dist/src/commands/test-modes.d.ts +4 -4
- package/dist/src/commands/test-modes.js +4 -5
- package/dist/src/commands/test-register.js +5 -3
- package/dist/src/commands/test-run.d.ts +11 -11
- package/dist/src/commands/test-run.js +40 -33
- package/dist/src/commands/test-stale-gate.d.ts +12 -12
- package/dist/src/commands/test-stale-gate.js +17 -17
- package/dist/src/commands/test-verdict.d.ts +19 -5
- package/dist/src/commands/test-verdict.js +26 -11
- package/dist/src/commands/test.d.ts +3 -3
- package/dist/src/commands/test.js +73 -36
- package/dist/src/commands/token-coverage.js +3 -3
- 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 +26 -10
- package/dist/src/commands/tree.js +26 -21
- package/dist/src/commands/typecheck.d.ts +6 -6
- package/dist/src/commands/typecheck.js +16 -16
- package/dist/src/commands/verify.d.ts +3 -3
- package/dist/src/commands/verify.js +10 -10
- package/dist/src/commands/watch.js +11 -11
- package/dist/src/commands/wire.js +17 -16
- package/dist/src/core/ast-utils.d.ts +7 -7
- package/dist/src/core/ast-utils.js +8 -8
- package/dist/src/core/branding.d.ts +5 -12
- package/dist/src/core/branding.js +6 -5
- package/dist/src/core/browser-wire.d.ts +1 -1
- package/dist/src/core/browser-wire.js +10 -7
- package/dist/src/core/build-audit-platform.d.ts +32 -3
- package/dist/src/core/build-audit-platform.js +153 -13
- package/dist/src/core/build-audit-registration.d.ts +5 -5
- package/dist/src/core/build-audit-registration.js +10 -9
- package/dist/src/core/build-audit-resolve.d.ts +5 -5
- package/dist/src/core/build-audit-resolve.js +6 -6
- 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-transforms.d.ts +2 -2
- package/dist/src/core/build-audit-transforms.js +3 -3
- package/dist/src/core/build-audit.d.ts +21 -8
- package/dist/src/core/build-audit.js +142 -40
- package/dist/src/core/build-baseline-types.d.ts +19 -18
- package/dist/src/core/build-baseline-types.js +1 -1
- package/dist/src/core/build-baseline.d.ts +48 -31
- package/dist/src/core/build-baseline.js +20 -43
- package/dist/src/core/build-jar-escalation.d.ts +8 -8
- package/dist/src/core/build-jar-escalation.js +18 -17
- package/dist/src/core/build-overwrite-guard.d.ts +4 -4
- package/dist/src/core/build-overwrite-guard.js +14 -14
- package/dist/src/core/build-prepare.d.ts +4 -12
- package/dist/src/core/build-prepare.js +22 -26
- package/dist/src/core/config-mutate.js +4 -4
- 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 +3 -21
- 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.d.ts +1 -1
- package/dist/src/core/config-validate-file-size.js +3 -3
- 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 +22 -12
- package/dist/src/core/config.d.ts +12 -12
- package/dist/src/core/config.js +21 -25
- package/dist/src/core/coverage-extend.d.ts +16 -16
- package/dist/src/core/coverage-extend.js +20 -20
- package/dist/src/core/destructive.d.ts +14 -14
- package/dist/src/core/destructive.js +9 -9
- package/dist/src/core/diff-hunks.d.ts +2 -2
- package/dist/src/core/diff-hunks.js +7 -7
- package/dist/src/core/discard-baseline.d.ts +10 -10
- package/dist/src/core/discard-baseline.js +22 -23
- package/dist/src/core/display-state.d.ts +11 -11
- package/dist/src/core/display-state.js +12 -12
- package/dist/src/core/engine-changes.d.ts +2 -2
- package/dist/src/core/engine-changes.js +2 -2
- package/dist/src/core/engine-precondition.d.ts +5 -13
- package/dist/src/core/engine-precondition.js +5 -10
- package/dist/src/core/engine-session-lock.d.ts +18 -18
- package/dist/src/core/engine-session-lock.js +50 -54
- package/dist/src/core/export-moved-code-shape.d.ts +7 -7
- package/dist/src/core/export-moved-code-shape.js +10 -10
- 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 +14 -19
- package/dist/src/core/file-lock.js +90 -128
- package/dist/src/core/firefox-cache.d.ts +13 -1
- package/dist/src/core/firefox-cache.js +61 -36
- package/dist/src/core/firefox-download.d.ts +15 -0
- package/dist/src/core/firefox-download.js +38 -22
- package/dist/src/core/firefox-extract.d.ts +8 -7
- package/dist/src/core/firefox-extract.js +45 -27
- package/dist/src/core/firefox.d.ts +34 -36
- package/dist/src/core/firefox.js +13 -44
- package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
- package/dist/src/core/furnace-apply-dry-run.js +2 -8
- package/dist/src/core/furnace-apply-ftl.d.ts +5 -5
- package/dist/src/core/furnace-apply-ftl.js +8 -8
- package/dist/src/core/furnace-apply-helpers.d.ts +9 -20
- package/dist/src/core/furnace-apply-helpers.js +28 -39
- package/dist/src/core/furnace-apply-output.js +1 -1
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +9 -9
- package/dist/src/core/furnace-apply-overwrite-warn.js +13 -11
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +1 -1
- package/dist/src/core/furnace-apply-undeploy-actions.js +1 -1
- package/dist/src/core/furnace-apply.d.ts +4 -4
- package/dist/src/core/furnace-apply.js +50 -29
- package/dist/src/core/furnace-checksum-utils.d.ts +5 -5
- package/dist/src/core/furnace-checksum-utils.js +5 -5
- package/dist/src/core/furnace-config-custom.d.ts +1 -1
- package/dist/src/core/furnace-config-custom.js +2 -2
- package/dist/src/core/furnace-config-order.js +1 -1
- package/dist/src/core/furnace-config-tokens.d.ts +3 -3
- package/dist/src/core/furnace-config-tokens.js +3 -3
- package/dist/src/core/furnace-config-validate.js +2 -2
- package/dist/src/core/furnace-config.d.ts +5 -5
- package/dist/src/core/furnace-config.js +40 -53
- package/dist/src/core/furnace-constants.d.ts +6 -6
- package/dist/src/core/furnace-constants.js +9 -8
- package/dist/src/core/furnace-css-fragments.d.ts +6 -6
- package/dist/src/core/furnace-css-fragments.js +11 -11
- 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-jsconfig.d.ts +7 -7
- package/dist/src/core/furnace-jsconfig.js +22 -20
- package/dist/src/core/furnace-operation.d.ts +22 -19
- package/dist/src/core/furnace-operation.js +38 -34
- package/dist/src/core/furnace-precondition.d.ts +10 -10
- package/dist/src/core/furnace-precondition.js +8 -8
- package/dist/src/core/furnace-refresh.d.ts +18 -6
- package/dist/src/core/furnace-refresh.js +5 -8
- package/dist/src/core/furnace-registration-ast.d.ts +2 -3
- package/dist/src/core/furnace-registration-ast.js +9 -12
- package/dist/src/core/furnace-registration-remove.d.ts +4 -4
- package/dist/src/core/furnace-registration-remove.js +6 -6
- package/dist/src/core/furnace-registration-validate.d.ts +2 -2
- package/dist/src/core/furnace-registration-validate.js +2 -2
- package/dist/src/core/furnace-registration.d.ts +7 -6
- package/dist/src/core/furnace-registration.js +29 -36
- package/dist/src/core/furnace-rollback.d.ts +1 -1
- package/dist/src/core/furnace-rollback.js +5 -5
- package/dist/src/core/furnace-scanner.js +3 -6
- package/dist/src/core/furnace-stale-export.d.ts +17 -16
- package/dist/src/core/furnace-stale-export.js +16 -15
- package/dist/src/core/furnace-staleness.d.ts +2 -2
- package/dist/src/core/furnace-staleness.js +2 -2
- package/dist/src/core/furnace-state-persist.d.ts +12 -12
- package/dist/src/core/furnace-state-persist.js +10 -10
- package/dist/src/core/furnace-step-errors.d.ts +6 -6
- package/dist/src/core/furnace-step-errors.js +6 -6
- package/dist/src/core/furnace-validate-accessibility.js +14 -14
- package/dist/src/core/furnace-validate-compatibility.js +9 -9
- package/dist/src/core/furnace-validate-registration.d.ts +2 -2
- package/dist/src/core/furnace-validate-registration.js +18 -20
- package/dist/src/core/furnace-validate-structure.d.ts +1 -1
- package/dist/src/core/furnace-validate-structure.js +4 -4
- package/dist/src/core/furnace-validate.js +9 -9
- package/dist/src/core/furnace-version-drift.d.ts +5 -5
- package/dist/src/core/furnace-version-drift.js +5 -5
- package/dist/src/core/git-base.d.ts +6 -4
- package/dist/src/core/git-base.js +29 -7
- 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 +13 -13
- package/dist/src/core/git-diff.js +121 -189
- package/dist/src/core/git-file-ops.d.ts +6 -13
- package/dist/src/core/git-file-ops.js +11 -35
- package/dist/src/core/git-readonly-index.d.ts +12 -14
- package/dist/src/core/git-readonly-index.js +12 -16
- package/dist/src/core/git-status.d.ts +9 -2
- package/dist/src/core/git-status.js +23 -11
- package/dist/src/core/git.d.ts +3 -9
- package/dist/src/core/git.js +28 -57
- package/dist/src/core/harness-orphans.d.ts +17 -17
- package/dist/src/core/harness-orphans.js +28 -27
- package/dist/src/core/history-log.d.ts +19 -0
- package/dist/src/core/history-log.js +29 -0
- package/dist/src/core/license-headers.d.ts +19 -18
- package/dist/src/core/license-headers.js +29 -28
- package/dist/src/core/mach-build-artifacts.d.ts +21 -21
- package/dist/src/core/mach-build-artifacts.js +23 -23
- package/dist/src/core/mach-error-hints.d.ts +5 -10
- package/dist/src/core/mach-error-hints.js +14 -14
- package/dist/src/core/mach-known-noise-filter.d.ts +42 -38
- package/dist/src/core/mach-known-noise-filter.js +57 -56
- package/dist/src/core/mach-mozconfig.d.ts +6 -6
- package/dist/src/core/mach-mozconfig.js +17 -16
- package/dist/src/core/mach-python.d.ts +3 -0
- package/dist/src/core/mach-python.js +13 -7
- package/dist/src/core/mach-resource-shim.d.ts +24 -12
- package/dist/src/core/mach-resource-shim.js +124 -21
- package/dist/src/core/mach.d.ts +59 -73
- package/dist/src/core/mach.js +47 -115
- package/dist/src/core/marionette-port.d.ts +15 -30
- package/dist/src/core/marionette-port.js +46 -89
- package/dist/src/core/marionette-preflight.d.ts +6 -6
- package/dist/src/core/marionette-preflight.js +32 -50
- package/dist/src/core/markdown-table.d.ts +9 -9
- package/dist/src/core/markdown-table.js +14 -14
- package/dist/src/core/mochitest-server-port.d.ts +8 -8
- package/dist/src/core/mochitest-server-port.js +17 -17
- package/dist/src/core/module-resolution-preflight.d.ts +8 -8
- package/dist/src/core/module-resolution-preflight.js +11 -11
- package/dist/src/core/moz-manifest-helpers.d.ts +7 -7
- package/dist/src/core/moz-manifest-helpers.js +7 -7
- package/dist/src/core/moz-manifest-register.d.ts +1 -1
- package/dist/src/core/moz-manifest-register.js +2 -2
- package/dist/src/core/moz-manifest-rules.js +46 -42
- package/dist/src/core/moz-manifest-tokenizers.js +1 -1
- package/dist/src/core/ownership-table.d.ts +5 -5
- package/dist/src/core/ownership-table.js +7 -7
- package/dist/src/core/parser-fallback.d.ts +3 -3
- package/dist/src/core/parser-fallback.js +3 -3
- package/dist/src/core/patch-apply-fuzz.d.ts +5 -5
- package/dist/src/core/patch-apply-fuzz.js +13 -13
- package/dist/src/core/patch-apply.d.ts +13 -14
- package/dist/src/core/patch-apply.js +36 -42
- 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 +21 -4
- package/dist/src/core/patch-export-update.js +7 -6
- package/dist/src/core/patch-export.d.ts +20 -30
- package/dist/src/core/patch-export.js +55 -77
- package/dist/src/core/patch-files.d.ts +0 -4
- package/dist/src/core/patch-files.js +0 -11
- package/dist/src/core/patch-identifier-suggest.d.ts +1 -1
- package/dist/src/core/patch-identifier-suggest.js +4 -4
- package/dist/src/core/patch-lint-binary.d.ts +12 -18
- package/dist/src/core/patch-lint-cache.d.ts +18 -2
- package/dist/src/core/patch-lint-cache.js +16 -19
- package/dist/src/core/patch-lint-checkjs.d.ts +55 -38
- package/dist/src/core/patch-lint-checkjs.js +78 -64
- package/dist/src/core/patch-lint-chrome-jsdoc.d.ts +8 -9
- package/dist/src/core/patch-lint-chrome-jsdoc.js +10 -11
- package/dist/src/core/patch-lint-creators.d.ts +5 -9
- package/dist/src/core/patch-lint-creators.js +3 -3
- package/dist/src/core/patch-lint-cross.d.ts +27 -19
- package/dist/src/core/patch-lint-cross.js +67 -110
- package/dist/src/core/patch-lint-css.d.ts +1 -1
- package/dist/src/core/patch-lint-css.js +15 -11
- package/dist/src/core/patch-lint-diff-tag.d.ts +5 -5
- package/dist/src/core/patch-lint-diff-tag.js +5 -5
- package/dist/src/core/patch-lint-diff.d.ts +4 -4
- package/dist/src/core/patch-lint-diff.js +4 -4
- package/dist/src/core/patch-lint-file-size.d.ts +3 -3
- package/dist/src/core/patch-lint-file-size.js +6 -6
- 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.d.ts +2 -2
- package/dist/src/core/patch-lint-jsdoc.js +46 -46
- package/dist/src/core/patch-lint-module-registration.d.ts +3 -9
- package/dist/src/core/patch-lint-module-registration.js +1 -1
- package/dist/src/core/patch-lint-mozbuild.d.ts +1 -1
- package/dist/src/core/patch-lint-mozbuild.js +10 -10
- package/dist/src/core/patch-lint-observer.d.ts +3 -3
- package/dist/src/core/patch-lint-observer.js +7 -7
- package/dist/src/core/patch-lint-ownership.d.ts +6 -6
- package/dist/src/core/patch-lint-ownership.js +6 -6
- package/dist/src/core/patch-lint-prettier.d.ts +6 -6
- package/dist/src/core/patch-lint-prettier.js +7 -7
- 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-unmanaged-companion.d.ts +23 -23
- package/dist/src/core/patch-lint-unmanaged-companion.js +26 -26
- package/dist/src/core/patch-lint.d.ts +41 -35
- package/dist/src/core/patch-lint.js +52 -49
- package/dist/src/core/patch-lock.d.ts +1 -1
- package/dist/src/core/patch-lock.js +11 -11
- package/dist/src/core/patch-manifest-consistency.d.ts +7 -7
- package/dist/src/core/patch-manifest-consistency.js +16 -17
- package/dist/src/core/patch-manifest-files-affected.d.ts +1 -1
- package/dist/src/core/patch-manifest-files-affected.js +3 -3
- package/dist/src/core/patch-manifest-io.d.ts +14 -14
- package/dist/src/core/patch-manifest-io.js +35 -34
- package/dist/src/core/patch-manifest-query.d.ts +1 -1
- package/dist/src/core/patch-manifest-query.js +4 -4
- package/dist/src/core/patch-manifest-resolve.d.ts +5 -5
- package/dist/src/core/patch-manifest-resolve.js +7 -7
- package/dist/src/core/patch-manifest-validate.js +3 -3
- package/dist/src/core/patch-manifest.d.ts +2 -2
- package/dist/src/core/patch-manifest.js +2 -2
- package/dist/src/core/patch-parse.d.ts +32 -21
- package/dist/src/core/patch-parse.js +40 -15
- package/dist/src/core/patch-policy.js +15 -27
- package/dist/src/core/patch-registration-refs.d.ts +4 -4
- package/dist/src/core/patch-registration-refs.js +7 -7
- 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 +8 -16
- package/dist/src/core/patch-transform.js +13 -23
- package/dist/src/core/process-cpu-time.d.ts +1 -1
- package/dist/src/core/process-cpu-time.js +6 -6
- package/dist/src/core/rebase-session.d.ts +9 -15
- package/dist/src/core/rebase-session.js +13 -25
- 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-result.d.ts +1 -1
- package/dist/src/core/register-result.js +1 -1
- package/dist/src/core/register-scaffold.d.ts +2 -2
- package/dist/src/core/register-scaffold.js +8 -7
- package/dist/src/core/register-shared-css.d.ts +3 -3
- package/dist/src/core/register-shared-css.js +6 -5
- package/dist/src/core/register-xpcshell-test.d.ts +2 -2
- package/dist/src/core/register-xpcshell-test.js +2 -2
- 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 +21 -10
- package/dist/src/core/run-log.js +76 -27
- package/dist/src/core/shared-ftl.d.ts +2 -2
- package/dist/src/core/shared-ftl.js +6 -6
- package/dist/src/core/signal-critical.d.ts +2 -2
- package/dist/src/core/signal-critical.js +4 -6
- package/dist/src/core/smoke-patterns.d.ts +5 -6
- package/dist/src/core/smoke-patterns.js +15 -20
- package/dist/src/core/state-file.d.ts +15 -4
- package/dist/src/core/state-file.js +14 -1
- package/dist/src/core/status-binary.d.ts +8 -8
- package/dist/src/core/status-binary.js +12 -12
- package/dist/src/core/status-classify.d.ts +17 -17
- package/dist/src/core/status-classify.js +13 -12
- package/dist/src/core/test-changed-prefs.d.ts +12 -12
- package/dist/src/core/test-changed-prefs.js +40 -25
- package/dist/src/core/test-harness-crash.d.ts +39 -39
- package/dist/src/core/test-harness-crash.js +69 -69
- package/dist/src/core/test-harness-output.d.ts +7 -7
- package/dist/src/core/test-harness-output.js +7 -7
- package/dist/src/core/test-harness-verdict-notes.d.ts +13 -14
- package/dist/src/core/test-harness-verdict-notes.js +4 -4
- package/dist/src/core/test-path-scope.d.ts +8 -8
- package/dist/src/core/test-path-scope.js +11 -11
- package/dist/src/core/test-stale-check.d.ts +22 -23
- package/dist/src/core/test-stale-check.js +31 -30
- package/dist/src/core/test-stale-symlink.d.ts +1 -1
- package/dist/src/core/test-stale-symlink.js +1 -1
- package/dist/src/core/test-stall-triage.d.ts +9 -9
- package/dist/src/core/test-stall-triage.js +22 -22
- 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 +35 -2
- package/dist/src/core/token-category.js +67 -58
- package/dist/src/core/token-coverage.js +4 -5
- package/dist/src/core/token-dark-mode.d.ts +9 -9
- package/dist/src/core/token-dark-mode.js +13 -25
- package/dist/src/core/token-docs.d.ts +1 -1
- 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 +4 -4
- package/dist/src/core/token-manager.js +22 -22
- package/dist/src/core/token-scaffold.d.ts +4 -4
- package/dist/src/core/token-scaffold.js +4 -4
- package/dist/src/core/token-variant.d.ts +14 -14
- package/dist/src/core/token-variant.js +52 -53
- package/dist/src/core/toolchain-preflight.d.ts +15 -15
- package/dist/src/core/toolchain-preflight.js +23 -23
- package/dist/src/core/tree-cow.d.ts +3 -5
- package/dist/src/core/tree-cow.js +8 -8
- package/dist/src/core/tree-guard.d.ts +3 -3
- package/dist/src/core/tree-guard.js +23 -23
- package/dist/src/core/tree-store.d.ts +15 -15
- package/dist/src/core/tree-store.js +39 -38
- package/dist/src/core/typecheck-shim.d.ts +16 -15
- package/dist/src/core/typecheck-shim.js +26 -25
- package/dist/src/core/typecheck.d.ts +12 -12
- package/dist/src/core/typecheck.js +33 -33
- package/dist/src/core/wire-destroy.d.ts +2 -2
- package/dist/src/core/wire-destroy.js +5 -5
- package/dist/src/core/wire-dom-fragment.d.ts +3 -3
- package/dist/src/core/wire-dom-fragment.js +12 -23
- package/dist/src/core/wire-init.d.ts +2 -2
- package/dist/src/core/wire-init.js +7 -7
- package/dist/src/core/wire-subscript.d.ts +1 -1
- package/dist/src/core/wire-subscript.js +3 -3
- package/dist/src/core/wire-utils.d.ts +9 -9
- package/dist/src/core/wire-utils.js +16 -28
- package/dist/src/core/xpcshell-appdir.d.ts +20 -20
- package/dist/src/core/xpcshell-appdir.js +28 -30
- package/dist/src/core/xpcshell-profile-dir.d.ts +3 -3
- package/dist/src/core/xpcshell-profile-dir.js +7 -7
- package/dist/src/errors/base.d.ts +69 -16
- package/dist/src/errors/base.js +95 -26
- package/dist/src/errors/build.d.ts +6 -6
- package/dist/src/errors/build.js +26 -34
- package/dist/src/errors/codes.d.ts +10 -10
- package/dist/src/errors/codes.js +10 -10
- 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.d.ts +2 -2
- package/dist/src/errors/furnace.js +8 -9
- package/dist/src/errors/git.d.ts +9 -9
- package/dist/src/errors/git.js +21 -31
- package/dist/src/errors/patch.d.ts +3 -3
- package/dist/src/errors/patch.js +11 -13
- package/dist/src/errors/rebase.d.ts +1 -1
- package/dist/src/errors/rebase.js +8 -8
- package/dist/src/errors/run.d.ts +1 -1
- package/dist/src/errors/run.js +1 -1
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +4 -4
- package/dist/src/types/commands/options.d.ts +155 -200
- package/dist/src/types/commands/patches.d.ts +19 -19
- package/dist/src/types/commands/project.d.ts +3 -3
- package/dist/src/types/config.d.ts +52 -19
- package/dist/src/types/furnace.d.ts +17 -17
- package/dist/src/types/index.d.ts +0 -1
- package/dist/src/types/json.d.ts +3 -3
- package/dist/src/types/json.js +2 -2
- package/dist/src/types/typecheck.d.ts +3 -3
- package/dist/src/utils/assert.d.ts +4 -16
- package/dist/src/utils/assert.js +7 -24
- package/dist/src/utils/build-info.d.ts +6 -6
- package/dist/src/utils/build-info.js +6 -6
- package/dist/src/utils/child-env.d.ts +7 -6
- package/dist/src/utils/child-env.js +5 -5
- package/dist/src/utils/concurrency.d.ts +2 -2
- package/dist/src/utils/concurrency.js +2 -2
- package/dist/src/utils/elapsed.js +10 -1
- package/dist/src/utils/errors.d.ts +8 -8
- package/dist/src/utils/errors.js +8 -8
- package/dist/src/utils/fs.d.ts +1 -1
- package/dist/src/utils/fs.js +15 -20
- 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/logger.d.ts +4 -4
- package/dist/src/utils/logger.js +11 -11
- package/dist/src/utils/machine-output.js +2 -2
- package/dist/src/utils/options.d.ts +14 -20
- package/dist/src/utils/options.js +26 -27
- package/dist/src/utils/package-root.js +5 -14
- package/dist/src/utils/paths.d.ts +4 -4
- package/dist/src/utils/paths.js +4 -4
- package/dist/src/utils/platform.d.ts +0 -4
- package/dist/src/utils/process-group.d.ts +10 -9
- package/dist/src/utils/process-group.js +37 -36
- package/dist/src/utils/process.d.ts +70 -55
- package/dist/src/utils/process.js +129 -179
- package/dist/src/utils/ps-duration.d.ts +4 -4
- package/dist/src/utils/ps-duration.js +9 -9
- package/dist/src/utils/regex.d.ts +4 -4
- package/dist/src/utils/regex.js +7 -7
- package/dist/src/utils/sleep.d.ts +26 -0
- package/dist/src/utils/sleep.js +31 -0
- package/dist/src/utils/stdio-drain.d.ts +7 -7
- package/dist/src/utils/stdio-drain.js +8 -8
- package/dist/src/utils/validation.d.ts +12 -49
- package/dist/src/utils/validation.js +26 -36
- package/docs/README.md +2 -2
- package/docs/configuration.md +163 -69
- package/docs/exit-codes.md +66 -49
- package/docs/furnace.md +20 -19
- package/docs/lifecycle-invariants.md +414 -247
- package/docs/machine-output.md +58 -34
- package/docs/patch-workflow.md +48 -44
- package/docs/run-logs.md +72 -28
- package/docs/testing.md +105 -98
- package/docs/verification-trees.md +23 -22
- package/package.json +7 -6
- 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
|
@@ -1,18 +1,21 @@
|
|
|
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;
|
|
12
15
|
/**
|
|
13
16
|
* Minimum interval between stale-lock probes while a waiter polls. Probing
|
|
14
|
-
* exactly once per waiter means a holder that dies
|
|
15
|
-
* the waiter polling a permanently dead lock until `timeoutMs
|
|
17
|
+
* exactly once per waiter means a holder that dies after that probe leaves
|
|
18
|
+
* the waiter polling a permanently dead lock until `timeoutMs`, up to 24
|
|
16
19
|
* hours for the build lock (see `withBuildLock` in mach.ts). Re-probing on a
|
|
17
20
|
* small interval bounds that to seconds while keeping the per-poll cost
|
|
18
21
|
* negligible: one stat plus one small read every ~5 s.
|
|
@@ -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,13 +99,13 @@ 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
|
-
// Advisory only
|
|
108
|
+
// Advisory only. Never fail a lock acquisition over the queue display.
|
|
148
109
|
verbose(`Could not register lock waiter for ${lockPath}: ${toError(error).message}`);
|
|
149
110
|
return async () => {
|
|
150
111
|
/* nothing to remove */
|
|
@@ -158,13 +119,13 @@ async function registerWaiter(lockPath, startedAt) {
|
|
|
158
119
|
* Reads the contended lock's owner and queue in one go. One probe per cycle
|
|
159
120
|
* serves both the progress line and the deadline extension, so the two can
|
|
160
121
|
* never disagree about the position they saw. Failure to read the owner
|
|
161
|
-
* degrades to `holder: undefined
|
|
162
|
-
* loop.
|
|
122
|
+
* degrades to `holder: undefined`, since a diagnostic must never break the
|
|
123
|
+
* wait loop.
|
|
163
124
|
*/
|
|
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
|
}
|
|
@@ -174,13 +135,13 @@ async function probeWaitState(lockPath, startedAt) {
|
|
|
174
135
|
*
|
|
175
136
|
* A naive stat → read-PID → `rm` sequence has a TOCTOU hole: two waiters
|
|
176
137
|
* both observe the dead owner, one removes the stale dir and `mkdir`s its
|
|
177
|
-
* own lock, and the other's already-decided `rm` then deletes the
|
|
178
|
-
* lock
|
|
138
|
+
* own lock, and the other's already-decided `rm` then deletes the fresh
|
|
139
|
+
* lock, putting two processes inside the critical section at once.
|
|
179
140
|
*
|
|
180
141
|
* Rename-aside closes it: `rename()` is atomic, so exactly one reaper wins
|
|
181
142
|
* ownership of the directory (the loser gets ENOENT and re-polls), and
|
|
182
143
|
* nothing ever deletes the live path directly. After winning the rename the
|
|
183
|
-
* owner is re-read from the renamed dir
|
|
144
|
+
* owner is re-read from the renamed dir. If it is now a live process (the
|
|
184
145
|
* lock was released and re-acquired between the staleness read and the
|
|
185
146
|
* rename), it is renamed back instead of reaped.
|
|
186
147
|
*
|
|
@@ -189,13 +150,13 @@ 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
|
}
|
|
196
157
|
catch (error) {
|
|
197
158
|
if (getNodeErrorCode(error) === 'ENOENT') {
|
|
198
|
-
// Another reaper won, or the owner released
|
|
159
|
+
// Another reaper won, or the owner released. Either way, retry mkdir.
|
|
199
160
|
verbose(`Stale lock disappeared before reaping completed: ${lockPath}`);
|
|
200
161
|
return true;
|
|
201
162
|
}
|
|
@@ -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
|
-
// lock
|
|
217
|
-
warn(`Could not restore live lock for PID ${
|
|
177
|
+
// lock, so surface it loudly instead of hiding the lost exclusion.
|
|
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;
|
|
@@ -238,21 +199,22 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
|
238
199
|
try {
|
|
239
200
|
const lockStat = await stat(lockPath);
|
|
240
201
|
const ageMs = Date.now() - lockStat.mtimeMs;
|
|
241
|
-
// Check PID
|
|
202
|
+
// Check the PID first, independent of age. Age-gating everything means a
|
|
242
203
|
// lock younger than `staleMs` (default 5 minutes) is never removed even
|
|
243
|
-
// when its PID file points at a process that has already exited
|
|
244
|
-
// situation after SIGINT'ing `furnace preview`, where the signal handler
|
|
204
|
+
// when its PID file points at a process that has already exited. That is
|
|
205
|
+
// the situation after SIGINT'ing `furnace preview`, where the signal handler
|
|
245
206
|
// calls `process.exit` and `withFileLock`'s release never runs. With the
|
|
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), so 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
|
|
@@ -275,25 +237,22 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
|
275
237
|
}
|
|
276
238
|
/**
|
|
277
239
|
* Releases a lock previously acquired by this process, verifying ownership
|
|
278
|
-
* first. An unconditional `finally { rm }` would remove
|
|
279
|
-
* directory is present
|
|
240
|
+
* first. An unconditional `finally { rm }` would remove whichever lock
|
|
241
|
+
* directory is present. If a stale-lock reaper had (in a pathological race)
|
|
280
242
|
* replaced our lock, that deletes the new owner's lock and compounds the
|
|
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
|
}
|
|
@@ -301,11 +260,11 @@ async function releaseLock(lockPath, ownerToken, ownerFileWritten) {
|
|
|
301
260
|
* Removes a lock directory only if this process still owns it, for callers
|
|
302
261
|
* that hold no acquisition token.
|
|
303
262
|
*
|
|
304
|
-
* The ownership half of {@link releaseLock}, minus the token check
|
|
263
|
+
* The ownership half of {@link releaseLock}, minus the token check: a
|
|
305
264
|
* signal-time sweeper knows which lock paths its own operations opened, but
|
|
306
265
|
* not the per-acquisition UUID those acquisitions minted. The PID check is
|
|
307
266
|
* still the one that matters: it stops the sweeper deleting a lock a
|
|
308
|
-
*
|
|
267
|
+
* different process acquired in the window between our operation dying and
|
|
309
268
|
* the sweep running.
|
|
310
269
|
*
|
|
311
270
|
* Never throws: callers are shutdown paths where a slow or failing I/O must
|
|
@@ -334,44 +293,52 @@ export async function releaseLockIfOwned(lockPath) {
|
|
|
334
293
|
* Typed as {@link LockContentionError} (a `FireForgeError`) so the CLI
|
|
335
294
|
* boundary prints the refusal as one clean line instead of an "Unexpected
|
|
336
295
|
* error" stack. Callers supplying `onTimeoutMessage` still get the holder
|
|
337
|
-
* identification appended
|
|
296
|
+
* identification appended: the reason-first copy stays theirs, the "who
|
|
338
297
|
* holds it" is ours.
|
|
298
|
+
*
|
|
299
|
+
* `queue` is read at expiry by the caller, not carried over from the last
|
|
300
|
+
* progress probe: see {@link formatQueuePositionReached}.
|
|
339
301
|
*/
|
|
340
302
|
async function buildLockTimeoutError(lockPath, onTimeoutMessage, cause, queue) {
|
|
341
303
|
const owner = await readLockOwner(lockPath);
|
|
342
|
-
const liveHolder = owner.present &&
|
|
304
|
+
const liveHolder = owner.present && isLockOwnerAlive(owner) ? owner : undefined;
|
|
343
305
|
const details = liveHolder !== undefined && liveHolder.metadata.length > 0
|
|
344
306
|
? ` (${liveHolder.metadata.join(', ')})`
|
|
345
307
|
: '';
|
|
346
308
|
const position = formatQueuePositionReached(queue);
|
|
347
309
|
if (onTimeoutMessage !== undefined) {
|
|
348
|
-
const identifiedHolder = liveHolder !== undefined
|
|
349
|
-
? ` The lock is held by PID ${String(liveHolder.pid)}${details}.`
|
|
350
|
-
: '';
|
|
310
|
+
const identifiedHolder = liveHolder !== undefined ? ` The lock is held by PID ${liveHolder.pid}${details}.` : '';
|
|
351
311
|
return new LockContentionError(`${onTimeoutMessage}${identifiedHolder}${position}`, cause);
|
|
352
312
|
}
|
|
353
313
|
const holderHint = liveHolder !== undefined
|
|
354
|
-
? ` It is held by running process ${
|
|
314
|
+
? ` It is held by running process ${liveHolder.pid}${details} — wait for it to finish, or stop it and retry.`
|
|
355
315
|
: ' If no other FireForge process is running, remove the lock directory and retry.';
|
|
356
316
|
return new LockContentionError(`Timed out waiting for file lock ${lockPath}.${holderHint}${position}`, cause);
|
|
357
317
|
}
|
|
358
318
|
/**
|
|
359
319
|
* States the queue position the expired wait actually reached.
|
|
360
320
|
*
|
|
361
|
-
* Silence here
|
|
321
|
+
* Silence here made the worst outcome invisible: a wait that expires
|
|
362
322
|
* one position from the head has paid the whole budget for nothing, and the
|
|
363
323
|
* operator cannot tell that from a queue that never moved. Naming the
|
|
364
324
|
* position turns "raise the budget" into an informed decision instead of a
|
|
365
|
-
* guess. Omitted when no queue was observed
|
|
325
|
+
* guess. Omitted when no queue was observed, since an empty registry is not
|
|
366
326
|
* evidence of an empty queue.
|
|
327
|
+
*
|
|
328
|
+
* The queue is read at the moment the budget expires rather than taken from
|
|
329
|
+
* the last progress probe. A probe snapshot is up to one `waitProgressMs`
|
|
330
|
+
* stale, and, the reason this changed, it may not exist at all: a short
|
|
331
|
+
* budget can expire before the first probe lands (a slow runner's
|
|
332
|
+
* `registerWaiter` alone can outlast it), and a caller that configured no
|
|
333
|
+
* probe at all never had one. Both used to lose the position silently.
|
|
367
334
|
*/
|
|
368
335
|
function formatQueuePositionReached(queue) {
|
|
369
|
-
if (queue
|
|
336
|
+
if (queue.depth === 0)
|
|
370
337
|
return '';
|
|
371
338
|
if (queue.ahead === 0) {
|
|
372
|
-
return ` You were next in a queue of ${
|
|
339
|
+
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
340
|
}
|
|
374
|
-
return ` You were still ${
|
|
341
|
+
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
342
|
}
|
|
376
343
|
/**
|
|
377
344
|
* Runs one wait probe: reads holder and queue, and reports progress when the
|
|
@@ -395,13 +362,13 @@ async function runWaitProbe(lockPath, startedAt, timeoutMs, options) {
|
|
|
395
362
|
* SIGTERM'd command leaves its lock on disk. The next contender reclaims it
|
|
396
363
|
* immediately (the PID-first staleness check sees a dead owner), but in the
|
|
397
364
|
* window between, `fireforge status --lock` reports the lock held by a
|
|
398
|
-
* process that is not running
|
|
399
|
-
*
|
|
365
|
+
* process that is not running, which is the question an operator asks after
|
|
366
|
+
* a kill. The signal sweep closes that window.
|
|
400
367
|
*
|
|
401
368
|
* Paths only: the per-acquisition token lives in `withFileLock`'s frame and
|
|
402
369
|
* a sweeper cannot see it. {@link releaseLockIfOwned} checks the PID, which
|
|
403
|
-
* is the check that matters
|
|
404
|
-
*
|
|
370
|
+
* is the check that matters: it stops the sweep deleting a lock a
|
|
371
|
+
* different process acquired in the meantime.
|
|
405
372
|
*/
|
|
406
373
|
const heldLocks = new Set();
|
|
407
374
|
/**
|
|
@@ -435,14 +402,12 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
435
402
|
let lastStaleProbeAt;
|
|
436
403
|
let currentPollMs = pollMs;
|
|
437
404
|
let lastProbeAt = startedAt;
|
|
438
|
-
/** Best (lowest) queue position seen so far
|
|
405
|
+
/** Best (lowest) queue position seen so far. Used to detect an advance. */
|
|
439
406
|
let bestAhead;
|
|
440
|
-
/** Last observed queue state, so the timeout refusal can name the position reached. */
|
|
441
|
-
let lastQueue;
|
|
442
407
|
/** Set on first contention so an uncontended acquire costs nothing. */
|
|
443
408
|
let deregisterWaiter;
|
|
444
409
|
// One cadence drives both the progress line and the deadline extension.
|
|
445
|
-
// Either feature alone is enough to want the probe
|
|
410
|
+
// Either feature alone is enough to want the probe. Neither is worth a
|
|
446
411
|
// second timer.
|
|
447
412
|
const probeIntervalMs = options.waitProgressMs ?? WAIT_PROBE_INTERVAL_MS;
|
|
448
413
|
const probeWanted = (options.onWaitProgress !== undefined && options.waitProgressMs !== undefined) ||
|
|
@@ -469,26 +434,31 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
469
434
|
}
|
|
470
435
|
}
|
|
471
436
|
if (Date.now() >= deadline) {
|
|
472
|
-
|
|
437
|
+
// The queue is read here, not carried over from the last progress
|
|
438
|
+
// probe: the position the wait reached is the one number that says
|
|
439
|
+
// whether a larger budget would have helped, and it must not depend
|
|
440
|
+
// on a probe having happened to land inside the budget. We are still
|
|
441
|
+
// registered at this point. `deregisterWaiter` runs in the `finally`
|
|
442
|
+
// below, which is only entered once this refusal has been built.
|
|
443
|
+
throw await buildLockTimeoutError(lockPath, options.onTimeoutMessage, error, await readLockQueue(lockPath, startedAt));
|
|
473
444
|
}
|
|
474
445
|
if (probeWanted && Date.now() - lastProbeAt >= probeIntervalMs) {
|
|
475
446
|
lastProbeAt = Date.now();
|
|
476
|
-
// Report the budget that is
|
|
447
|
+
// Report the budget that is currently in force, not the one
|
|
477
448
|
// originally requested: once an extension has been granted, the
|
|
478
449
|
// requested figure is stale, and a progress line that keeps
|
|
479
450
|
// quoting it reads as a timeout that does not fire.
|
|
480
451
|
const probe = await runWaitProbe(lockPath, startedAt, deadline - startedAt, options);
|
|
481
|
-
|
|
482
|
-
// An advance — someone ahead of us took their turn and finished —
|
|
452
|
+
// An advance (someone ahead of us took their turn and finished)
|
|
483
453
|
// buys a fresh budget, because the wait is working. Only a queue
|
|
484
454
|
// that stops moving starves, which is the case the timeout is for.
|
|
485
455
|
//
|
|
486
|
-
// The
|
|
456
|
+
// The first observation is a seed, never an advance. `bestAhead`
|
|
487
457
|
// used to start `undefined`, so the opening probe always satisfied
|
|
488
458
|
// this test and handed out a free full budget: a `--wait-lock 300`
|
|
489
459
|
// run outlived its 300 s even against a queue that never moved
|
|
490
|
-
// once, which is
|
|
491
|
-
// seconds" a downstream fork reported.
|
|
460
|
+
// once, which is the "it does not reliably give up after N
|
|
461
|
+
// seconds" behaviour a downstream fork reported.
|
|
492
462
|
if (hardDeadline !== undefined) {
|
|
493
463
|
if (bestAhead === undefined) {
|
|
494
464
|
bestAhead = probe.queue.ahead;
|
|
@@ -511,32 +481,24 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
511
481
|
}
|
|
512
482
|
}
|
|
513
483
|
finally {
|
|
514
|
-
// Leaving the queue happens the moment we stop waiting
|
|
484
|
+
// Leaving the queue happens the moment we stop waiting, whether we
|
|
515
485
|
// acquired the lock or timed out.
|
|
516
486
|
await deregisterWaiter?.();
|
|
517
487
|
}
|
|
518
|
-
// Stamp ownership (PID + per-acquisition token
|
|
519
|
-
// stale-lock recovery can check liveness and release
|
|
520
|
-
// is still ours before removing it.
|
|
488
|
+
// Stamp ownership (PID + per-acquisition token + acquisition time) into
|
|
489
|
+
// the lock directory so stale-lock recovery can check liveness and release
|
|
490
|
+
// can verify the lock is still ours before removing it. Fatal on failure:
|
|
491
|
+
// an owner-less lock is reaped by the age heuristic after five minutes,
|
|
492
|
+
// which for a multi-hour build meant a second holder mid-critical-section.
|
|
493
|
+
// `writeLockOwner` removes the just-created directory before throwing.
|
|
521
494
|
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
|
-
}
|
|
495
|
+
await writeLockOwner(lockPath, ownerToken, options.ownerMetadata);
|
|
534
496
|
heldLocks.add(lockPath);
|
|
535
497
|
try {
|
|
536
498
|
return await operation();
|
|
537
499
|
}
|
|
538
500
|
finally {
|
|
539
501
|
heldLocks.delete(lockPath);
|
|
540
|
-
await releaseLock(lockPath, ownerToken
|
|
502
|
+
await releaseLock(lockPath, ownerToken);
|
|
541
503
|
}
|
|
542
504
|
}
|
|
@@ -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
|
|
@@ -7,13 +7,13 @@ import { createReadStream } from 'node:fs';
|
|
|
7
7
|
import { readdir, rename, stat } from 'node:fs/promises';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { pipeline } from 'node:stream/promises';
|
|
10
|
-
import { ChecksumMismatchError } from '../errors/download.js';
|
|
10
|
+
import { ChecksumMismatchError, ChecksumUnavailableError } from '../errors/download.js';
|
|
11
11
|
import { toError } from '../utils/errors.js';
|
|
12
12
|
import { pathExistsStrict, readJson, removeFile, writeJson } from '../utils/fs.js';
|
|
13
13
|
import { verbose, warn } from '../utils/logger.js';
|
|
14
14
|
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
15
15
|
import { validateArchiveMetadata } from './firefox-archive.js';
|
|
16
|
-
import { downloadFile } from './firefox-download.js';
|
|
16
|
+
import { downloadFile, fetchWithRetry } from './firefox-download.js';
|
|
17
17
|
/**
|
|
18
18
|
* Computes the SHA-256 hex digest of a file.
|
|
19
19
|
* @param filePath - Path to the file
|
|
@@ -26,10 +26,15 @@ async function sha256File(filePath) {
|
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* Fetches Mozilla's published SHA256SUMS for the release and extracts the
|
|
29
|
-
* digest for this archive.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* digest for this archive. Yields a reason instead of a digest when the
|
|
30
|
+
* checksum file cannot be fetched (non-2xx, network error, timeout) or does
|
|
31
|
+
* not list the archive (a captive portal's HTML page lands here too). The
|
|
32
|
+
* caller decides whether that fails closed. The request goes through the
|
|
33
|
+
* same `fetchWithRetry` as the archive download (same 60 s abort ceiling
|
|
34
|
+
* per attempt, same three-attempt back-off on 429/5xx and network errors),
|
|
35
|
+
* so an unresponsive checksum host cannot hang the command after the
|
|
36
|
+
* tarball has landed, and one transient blip on the small checksum request
|
|
37
|
+
* does not fail the verification closed and discard the archive.
|
|
33
38
|
*
|
|
34
39
|
* Line format (both variants appear in the wild):
|
|
35
40
|
* `<64-hex-digest> <release-relative-path>`
|
|
@@ -37,33 +42,67 @@ async function sha256File(filePath) {
|
|
|
37
42
|
*/
|
|
38
43
|
async function fetchPublishedSha256(archive) {
|
|
39
44
|
try {
|
|
40
|
-
const response = await
|
|
45
|
+
const response = await fetchWithRetry(archive.checksumsUrl);
|
|
41
46
|
if (!response.ok) {
|
|
42
|
-
|
|
43
|
-
return null;
|
|
47
|
+
return { sha256: null, reason: `HTTP ${response.status} fetching SHA256SUMS` };
|
|
44
48
|
}
|
|
45
49
|
const body = await response.text();
|
|
46
50
|
for (const line of body.split('\n')) {
|
|
47
51
|
const match = /^([0-9a-f]{64})\s+\*?(.+)$/.exec(line.trim());
|
|
48
52
|
if (match?.[1] && match[2] === archive.pathInChecksums) {
|
|
49
|
-
return match[1];
|
|
53
|
+
return { sha256: match[1] };
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
return {
|
|
57
|
+
sha256: null,
|
|
58
|
+
reason: `SHA256SUMS does not list ${archive.pathInChecksums} (wrong file, or a non-Mozilla response)`,
|
|
59
|
+
};
|
|
54
60
|
}
|
|
55
61
|
catch (error) {
|
|
56
|
-
|
|
57
|
-
return null;
|
|
62
|
+
return { sha256: null, reason: `SHA256SUMS fetch failed: ${toError(error).message}` };
|
|
58
63
|
}
|
|
59
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Default integrity check for a download with no operator pin: the archive
|
|
67
|
+
* digest must equal the one Mozilla publishes. TLS alone is thin trust for
|
|
68
|
+
* the artifact that becomes the git baseline every patch is built on: a
|
|
69
|
+
* compromised CDN response would otherwise be trusted with no signal.
|
|
70
|
+
*
|
|
71
|
+
* Fails closed both on a mismatch and when the published digest cannot be
|
|
72
|
+
* obtained: a 404, a captive-portal HTML page, or a dropped connection
|
|
73
|
+
* would otherwise quietly downgrade the check to "TLS looked fine", which
|
|
74
|
+
* is the condition an attacker positioned on the network can
|
|
75
|
+
* arrange. `allowUnverifiedDownload` (an explicit config opt-in for offline
|
|
76
|
+
* mirrors) degrades the unavailable case to a loud warning. A fetched but
|
|
77
|
+
* mismatching digest is never accepted.
|
|
78
|
+
*/
|
|
79
|
+
async function verifyAgainstPublishedSha256(archive, actualSha256, options) {
|
|
80
|
+
const published = await fetchPublishedSha256(archive);
|
|
81
|
+
if (published.sha256 !== null) {
|
|
82
|
+
if (actualSha256 !== published.sha256) {
|
|
83
|
+
throw new ChecksumMismatchError(archive.product, published.sha256, actualSha256, archive.url);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
verbose(`${published.reason} (${archive.checksumsUrl})`);
|
|
88
|
+
if (options.allowUnverifiedDownload !== true) {
|
|
89
|
+
throw new ChecksumUnavailableError(archive.product, archive.checksumsUrl, published.reason, archive.url);
|
|
90
|
+
}
|
|
91
|
+
warn(`Could not verify ${archive.filename} against Mozilla's published SHA256SUMS ` +
|
|
92
|
+
`(${published.reason}). firefox.allowUnverifiedDownload is set, so the download is ` +
|
|
93
|
+
'trusted on TLS alone — pin firefox.sha256 in fireforge.json to verify it.');
|
|
94
|
+
}
|
|
60
95
|
/**
|
|
61
96
|
* Ensures a valid cached archive exists, downloading it if needed.
|
|
62
97
|
* @param archive - Resolved archive descriptor
|
|
63
98
|
* @param cacheDir - Cache directory
|
|
64
99
|
* @param onProgress - Optional progress callback
|
|
100
|
+
* @param expectedSha256 - Operator-pinned digest. When set it replaces the
|
|
101
|
+
* published-SHA256SUMS check
|
|
102
|
+
* @param onCacheProgress - Phase progress messages
|
|
103
|
+
* @param integrity - Default integrity-check options (unpinned downloads)
|
|
65
104
|
*/
|
|
66
|
-
export async function ensureCachedArchive(archive, cacheDir, onProgress, expectedSha256, onCacheProgress) {
|
|
105
|
+
export async function ensureCachedArchive(archive, cacheDir, onProgress, expectedSha256, onCacheProgress, integrity = {}) {
|
|
67
106
|
const lockPath = createSiblingLockPath(join(cacheDir, archive.filename), '.fireforge-cache.lock');
|
|
68
107
|
await withFileLock(lockPath, async () => {
|
|
69
108
|
onCacheProgress?.(`Validating source archive cache metadata for ${archive.filename}...`);
|
|
@@ -78,7 +117,7 @@ export async function ensureCachedArchive(archive, cacheDir, onProgress, expecte
|
|
|
78
117
|
else {
|
|
79
118
|
await removeArchivePartFiles(archive, cacheDir);
|
|
80
119
|
}
|
|
81
|
-
await downloadToCache(archive, cacheDir, onProgress, expectedSha256, onCacheProgress);
|
|
120
|
+
await downloadToCache(archive, cacheDir, onProgress, expectedSha256, onCacheProgress, integrity);
|
|
82
121
|
});
|
|
83
122
|
}
|
|
84
123
|
async function cacheEntryExists(archive, cacheDir) {
|
|
@@ -94,7 +133,7 @@ async function cacheEntryExists(archive, cacheDir) {
|
|
|
94
133
|
async function validateCachedArchive(archive, cacheDir, expectedSha256) {
|
|
95
134
|
const tarballPath = join(cacheDir, archive.filename);
|
|
96
135
|
const metadataPath = join(cacheDir, archive.metadataFilename);
|
|
97
|
-
//
|
|
136
|
+
// Outside the try/catch below on purpose: a permission error probing the
|
|
98
137
|
// cache must propagate instead of reading as "invalid cache" and triggering
|
|
99
138
|
// a re-download into a directory we cannot write anyway.
|
|
100
139
|
if (!(await pathExistsStrict(tarballPath)) || !(await pathExistsStrict(metadataPath))) {
|
|
@@ -135,7 +174,7 @@ async function validateCachedArchive(archive, cacheDir, expectedSha256) {
|
|
|
135
174
|
* @param cacheDir - Cache directory
|
|
136
175
|
* @param onProgress - Optional progress callback
|
|
137
176
|
*/
|
|
138
|
-
async function downloadToCache(archive, cacheDir, onProgress, expectedSha256, onCacheProgress) {
|
|
177
|
+
async function downloadToCache(archive, cacheDir, onProgress, expectedSha256, onCacheProgress, integrity = {}) {
|
|
139
178
|
const tarballPath = join(cacheDir, archive.filename);
|
|
140
179
|
// Use a unique .part path so concurrent downloads for the same archive
|
|
141
180
|
// do not clobber each other's partial files.
|
|
@@ -157,24 +196,10 @@ async function downloadToCache(archive, cacheDir, onProgress, expectedSha256, on
|
|
|
157
196
|
}
|
|
158
197
|
}
|
|
159
198
|
else {
|
|
160
|
-
// Default integrity check
|
|
161
|
-
//
|
|
162
|
-
// the git baseline every patch is built on — a compromised CDN
|
|
163
|
-
// response would otherwise be trusted with no signal. Mismatch fails
|
|
164
|
-
// closed (the catch below deletes the artifact); an unfetchable
|
|
165
|
-
// SHA256SUMS degrades to a loud warning so offline/mirror workflows
|
|
166
|
-
// keep working (pin firefox.sha256 in fireforge.json to fail closed
|
|
167
|
-
// even then).
|
|
199
|
+
// Default integrity check (see verifyAgainstPublishedSha256): any
|
|
200
|
+
// failure throws, and the catch below deletes the promoted artifact.
|
|
168
201
|
onCacheProgress?.(`Verifying archive against published SHA256SUMS...`);
|
|
169
|
-
|
|
170
|
-
if (publishedSha256 === null) {
|
|
171
|
-
warn(`Could not verify ${archive.filename} against Mozilla's published SHA256SUMS ` +
|
|
172
|
-
`(${archive.checksumsUrl} unavailable). The download is trusted on TLS alone — ` +
|
|
173
|
-
'set firefox.sha256 in fireforge.json to require checksum verification.');
|
|
174
|
-
}
|
|
175
|
-
else if (sha256 !== publishedSha256) {
|
|
176
|
-
throw new ChecksumMismatchError(archive.product, publishedSha256, sha256, archive.url);
|
|
177
|
-
}
|
|
202
|
+
await verifyAgainstPublishedSha256(archive, sha256, integrity);
|
|
178
203
|
}
|
|
179
204
|
onCacheProgress?.(`Writing source archive cache metadata for ${archive.metadataFilename}...`);
|
|
180
205
|
await writeJson(metadataPath, {
|
|
@@ -219,6 +244,6 @@ async function removeArchivePartFiles(archive, cacheDir) {
|
|
|
219
244
|
}
|
|
220
245
|
catch (error) {
|
|
221
246
|
void error;
|
|
222
|
-
// Cache dir may not exist yet
|
|
247
|
+
// Cache dir may not exist yet. That's fine.
|
|
223
248
|
}
|
|
224
249
|
}
|