@hominis/fireforge 0.35.0 → 0.37.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 +42 -0
- package/README.md +34 -9
- package/dist/bin/fireforge.js +21 -6
- package/dist/src/cli.js +22 -5
- package/dist/src/commands/bootstrap-checks.js +0 -1
- package/dist/src/commands/bootstrap.d.ts +2 -1
- package/dist/src/commands/bootstrap.js +16 -7
- package/dist/src/commands/build.js +5 -4
- package/dist/src/commands/config.js +30 -10
- package/dist/src/commands/discard.js +18 -3
- package/dist/src/commands/doctor/post-rebase-audit.js +0 -1
- package/dist/src/commands/doctor-check-core.js +0 -1
- package/dist/src/commands/doctor-external-toolchains.d.ts +3 -0
- package/dist/src/commands/doctor-external-toolchains.js +54 -0
- package/dist/src/commands/doctor-furnace-jar.js +0 -1
- package/dist/src/commands/doctor-furnace-manifest-sync.js +0 -1
- package/dist/src/commands/doctor-furnace.js +2 -2
- package/dist/src/commands/doctor-orphaned-harness.d.ts +52 -0
- package/dist/src/commands/doctor-orphaned-harness.js +132 -0
- package/dist/src/commands/doctor-working-tree.js +0 -1
- package/dist/src/commands/doctor.js +6 -1
- package/dist/src/commands/download.js +28 -13
- package/dist/src/commands/export-all.js +5 -3
- package/dist/src/commands/export-flow.js +12 -7
- package/dist/src/commands/export-placement-gate.js +0 -1
- package/dist/src/commands/export-placement-policy.js +0 -1
- package/dist/src/commands/export-shared.d.ts +5 -1
- package/dist/src/commands/export-shared.js +18 -6
- package/dist/src/commands/export.js +22 -10
- package/dist/src/commands/furnace/apply.js +22 -6
- package/dist/src/commands/furnace/chrome-doc-remove.js +0 -1
- package/dist/src/commands/furnace/chrome-doc-templates.js +0 -1
- package/dist/src/commands/furnace/chrome-doc-tests.js +0 -1
- package/dist/src/commands/furnace/chrome-doc.js +0 -1
- package/dist/src/commands/furnace/create-browser-test.js +0 -1
- package/dist/src/commands/furnace/create-dry-run.js +0 -1
- package/dist/src/commands/furnace/create-features.js +0 -1
- package/dist/src/commands/furnace/create-mochikit.js +0 -1
- package/dist/src/commands/furnace/create-readback.js +0 -1
- package/dist/src/commands/furnace/create-templates.js +0 -1
- package/dist/src/commands/furnace/create-validation.js +0 -1
- package/dist/src/commands/furnace/create-xpcshell.js +0 -1
- package/dist/src/commands/furnace/create.js +0 -1
- package/dist/src/commands/furnace/deploy.js +7 -83
- package/dist/src/commands/furnace/diff.js +0 -1
- package/dist/src/commands/furnace/index.js +59 -25
- package/dist/src/commands/furnace/init.js +0 -1
- package/dist/src/commands/furnace/list.js +0 -1
- package/dist/src/commands/furnace/override.js +0 -1
- package/dist/src/commands/furnace/preview.js +3 -3
- package/dist/src/commands/furnace/refresh.js +0 -1
- package/dist/src/commands/furnace/remove-state.d.ts +15 -0
- package/dist/src/commands/furnace/remove-state.js +47 -1
- package/dist/src/commands/furnace/remove.js +3 -9
- package/dist/src/commands/furnace/rename-browser-test.js +1 -5
- package/dist/src/commands/furnace/rename-helpers.d.ts +6 -0
- package/dist/src/commands/furnace/rename-helpers.js +30 -1
- package/dist/src/commands/furnace/rename-xpcshell.js +7 -6
- package/dist/src/commands/furnace/rename.js +15 -36
- package/dist/src/commands/furnace/scan.js +0 -1
- package/dist/src/commands/furnace/status.js +0 -1
- package/dist/src/commands/furnace/sync.js +76 -4
- package/dist/src/commands/furnace/validate.js +0 -1
- package/dist/src/commands/furnace/validation-output.js +0 -1
- package/dist/src/commands/import.js +35 -9
- package/dist/src/commands/lint-per-patch.js +19 -7
- package/dist/src/commands/lint.js +13 -3
- package/dist/src/commands/manifest.js +1 -2
- package/dist/src/commands/package.js +2 -1
- package/dist/src/commands/patch/compact.js +1 -1
- package/dist/src/commands/patch/delete.js +1 -1
- package/dist/src/commands/patch/index.js +1 -1
- package/dist/src/commands/patch/lint-ignore.js +21 -6
- package/dist/src/commands/patch/move-files.js +1 -1
- package/dist/src/commands/patch/patch-context.js +0 -1
- package/dist/src/commands/patch/rename.js +4 -4
- package/dist/src/commands/patch/reorder.js +3 -4
- package/dist/src/commands/patch/split-plan.js +2 -1
- package/dist/src/commands/patch/split.js +4 -4
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -1
- package/dist/src/commands/patch/staged-dependency.js +114 -52
- package/dist/src/commands/patch/tier.js +18 -5
- package/dist/src/commands/re-export-bulk-scan.js +0 -1
- package/dist/src/commands/re-export-files.js +5 -1
- package/dist/src/commands/re-export-options.js +0 -1
- package/dist/src/commands/re-export-scan.js +0 -1
- package/dist/src/commands/re-export.js +14 -4
- package/dist/src/commands/rebase/abort.js +2 -9
- package/dist/src/commands/rebase/confirm.js +0 -1
- package/dist/src/commands/rebase/conflict-summary.js +0 -1
- package/dist/src/commands/rebase/continue.js +0 -1
- package/dist/src/commands/rebase/index.d.ts +1 -1
- package/dist/src/commands/rebase/index.js +10 -15
- package/dist/src/commands/rebase/patch-loop.js +38 -7
- package/dist/src/commands/rebase/summary.js +2 -3
- package/dist/src/commands/rebase.js +0 -1
- package/dist/src/commands/register.js +1 -1
- package/dist/src/commands/reset.js +16 -9
- package/dist/src/commands/resolve.js +2 -2
- package/dist/src/commands/run.js +4 -4
- package/dist/src/commands/setup-support.js +0 -1
- package/dist/src/commands/setup.js +1 -2
- package/dist/src/commands/source.js +1 -2
- package/dist/src/commands/status-output.js +1 -12
- package/dist/src/commands/status.js +147 -133
- package/dist/src/commands/test-appdir.js +0 -1
- package/dist/src/commands/test-diagnose.js +38 -3
- package/dist/src/commands/test-modes.d.ts +15 -0
- package/dist/src/commands/test-modes.js +61 -0
- package/dist/src/commands/test-register.d.ts +4 -0
- package/dist/src/commands/test-register.js +64 -0
- package/dist/src/commands/test-run.d.ts +23 -7
- package/dist/src/commands/test-run.js +15 -14
- package/dist/src/commands/test.d.ts +0 -4
- package/dist/src/commands/test.js +147 -96
- package/dist/src/commands/token-coverage.js +0 -1
- package/dist/src/commands/token.js +1 -2
- package/dist/src/commands/typecheck.js +1 -1
- package/dist/src/commands/verify.js +3 -9
- package/dist/src/commands/watch.js +5 -1
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.js +0 -1
- package/dist/src/core/brand-validation.js +0 -1
- package/dist/src/core/branding.js +0 -1
- package/dist/src/core/browser-wire.js +0 -1
- package/dist/src/core/build-audit-platform.js +0 -1
- package/dist/src/core/build-audit-registration.js +0 -1
- package/dist/src/core/build-audit-resolve.js +0 -1
- package/dist/src/core/build-audit-transforms.js +0 -1
- package/dist/src/core/build-audit.js +0 -1
- package/dist/src/core/build-baseline-types.d.ts +24 -0
- package/dist/src/core/build-baseline-types.js +0 -1
- package/dist/src/core/build-baseline.d.ts +5 -2
- package/dist/src/core/build-baseline.js +5 -2
- package/dist/src/core/build-prepare.js +2 -2
- package/dist/src/core/config-mutate.js +0 -1
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +6 -1
- package/dist/src/core/config-state.js +0 -1
- package/dist/src/core/config-validate-patch-policy.js +0 -1
- package/dist/src/core/config-validate-test-toolchains.d.ts +8 -0
- package/dist/src/core/config-validate-test-toolchains.js +105 -0
- package/dist/src/core/config-validate.js +3 -1
- package/dist/src/core/config.js +0 -1
- package/dist/src/core/destructive.js +0 -1
- package/dist/src/core/diff-hunks.js +13 -7
- package/dist/src/core/engine-changes.js +0 -1
- package/dist/src/core/engine-session-lock.d.ts +12 -0
- package/dist/src/core/engine-session-lock.js +45 -0
- package/dist/src/core/file-lock.d.ts +8 -0
- package/dist/src/core/file-lock.js +157 -48
- package/dist/src/core/firefox-archive.d.ts +4 -0
- package/dist/src/core/firefox-archive.js +6 -1
- package/dist/src/core/firefox-cache.js +60 -4
- package/dist/src/core/firefox-download.js +0 -1
- package/dist/src/core/firefox-extract.js +75 -16
- package/dist/src/core/firefox-ignorefile.js +0 -1
- package/dist/src/core/firefox.d.ts +12 -0
- package/dist/src/core/firefox.js +64 -2
- package/dist/src/core/furnace-apply-dry-run.js +0 -1
- package/dist/src/core/furnace-apply-ftl.d.ts +7 -3
- package/dist/src/core/furnace-apply-ftl.js +10 -5
- package/dist/src/core/furnace-apply-helpers.js +0 -1
- package/dist/src/core/furnace-apply-output.js +2 -2
- package/dist/src/core/furnace-apply.d.ts +7 -1
- package/dist/src/core/furnace-apply.js +47 -13
- package/dist/src/core/furnace-checksum-utils.js +0 -1
- package/dist/src/core/furnace-config-array-utils.js +0 -1
- package/dist/src/core/furnace-config-custom.js +10 -1
- package/dist/src/core/furnace-config-order.js +0 -1
- package/dist/src/core/furnace-config-tokens.js +0 -1
- package/dist/src/core/furnace-config-validate.js +0 -1
- package/dist/src/core/furnace-config.d.ts +11 -0
- package/dist/src/core/furnace-config.js +37 -1
- package/dist/src/core/furnace-constants.js +0 -1
- package/dist/src/core/furnace-css-fragments.js +0 -1
- package/dist/src/core/furnace-graph-utils.js +0 -1
- package/dist/src/core/furnace-jsconfig.d.ts +2 -3
- package/dist/src/core/furnace-jsconfig.js +25 -18
- package/dist/src/core/furnace-marker.js +0 -1
- package/dist/src/core/furnace-operation.js +0 -1
- package/dist/src/core/furnace-refresh.js +0 -1
- package/dist/src/core/furnace-registration-ast.d.ts +14 -0
- package/dist/src/core/furnace-registration-ast.js +1 -2
- package/dist/src/core/furnace-registration-remove.js +0 -1
- package/dist/src/core/furnace-registration-validate.js +1 -5
- package/dist/src/core/furnace-registration.js +0 -1
- package/dist/src/core/furnace-rollback.js +0 -1
- package/dist/src/core/furnace-scanner.js +0 -1
- package/dist/src/core/furnace-stale-export.d.ts +59 -0
- package/dist/src/core/furnace-stale-export.js +123 -0
- package/dist/src/core/furnace-staleness.js +0 -1
- package/dist/src/core/furnace-state-persist.d.ts +72 -0
- package/dist/src/core/furnace-state-persist.js +118 -0
- package/dist/src/core/furnace-step-errors.d.ts +23 -0
- package/dist/src/core/furnace-step-errors.js +25 -0
- package/dist/src/core/furnace-stories.js +0 -1
- package/dist/src/core/furnace-validate-accessibility.js +0 -1
- package/dist/src/core/furnace-validate-checks.js +0 -1
- package/dist/src/core/furnace-validate-compatibility.js +9 -3
- package/dist/src/core/furnace-validate-helpers.js +0 -1
- package/dist/src/core/furnace-validate-registration.js +7 -7
- package/dist/src/core/furnace-validate-structure.d.ts +8 -0
- package/dist/src/core/furnace-validate-structure.js +14 -4
- package/dist/src/core/furnace-validate.js +0 -1
- package/dist/src/core/furnace-version-drift.d.ts +6 -3
- package/dist/src/core/furnace-version-drift.js +6 -4
- package/dist/src/core/git-base.js +0 -1
- package/dist/src/core/git-diff.js +0 -1
- package/dist/src/core/git-file-ops.js +0 -1
- package/dist/src/core/git-status.d.ts +13 -0
- package/dist/src/core/git-status.js +32 -2
- package/dist/src/core/git.d.ts +10 -1
- package/dist/src/core/git.js +63 -6
- package/dist/src/core/license-headers.js +0 -1
- package/dist/src/core/lint-cache.js +0 -1
- package/dist/src/core/lint-projection.js +0 -1
- package/dist/src/core/mach-build-artifacts.js +0 -1
- package/dist/src/core/mach-error-hints.js +0 -1
- package/dist/src/core/mach-known-noise-filter.d.ts +60 -0
- package/dist/src/core/mach-known-noise-filter.js +193 -0
- package/dist/src/core/mach-mozconfig.js +0 -1
- package/dist/src/core/mach-python.js +0 -1
- package/dist/src/core/mach-resource-shim.js +0 -1
- package/dist/src/core/mach.d.ts +8 -0
- package/dist/src/core/mach.js +36 -4
- package/dist/src/core/manifest-helpers.js +0 -1
- package/dist/src/core/manifest-register.js +0 -1
- package/dist/src/core/manifest-rules.js +0 -1
- package/dist/src/core/manifest-tokenizers.js +0 -1
- package/dist/src/core/marionette-port.d.ts +8 -0
- package/dist/src/core/marionette-port.js +34 -3
- package/dist/src/core/marionette-preflight.js +0 -1
- package/dist/src/core/markdown-table.js +0 -1
- package/dist/src/core/ownership-table.js +2 -9
- package/dist/src/core/parser-fallback.js +0 -1
- package/dist/src/core/patch-apply-fuzz.d.ts +27 -9
- package/dist/src/core/patch-apply-fuzz.js +71 -34
- package/dist/src/core/patch-apply.d.ts +30 -0
- package/dist/src/core/patch-apply.js +101 -18
- package/dist/src/core/patch-artifact-normalize.d.ts +18 -5
- package/dist/src/core/patch-artifact-normalize.js +19 -15
- package/dist/src/core/patch-export-coverage.js +0 -1
- package/dist/src/core/patch-export-metadata.js +0 -1
- package/dist/src/core/patch-export-update.js +2 -3
- package/dist/src/core/patch-export.d.ts +2 -6
- package/dist/src/core/patch-export.js +24 -48
- package/dist/src/core/patch-files.js +0 -1
- package/dist/src/core/patch-identifier-suggest.js +0 -1
- package/dist/src/core/patch-lint-checkjs.js +0 -1
- package/dist/src/core/patch-lint-chrome-jsdoc.js +0 -1
- package/dist/src/core/patch-lint-cross.js +80 -3
- package/dist/src/core/patch-lint-css.js +0 -1
- package/dist/src/core/patch-lint-diff-tag.js +0 -1
- package/dist/src/core/patch-lint-diff.d.ts +2 -1
- package/dist/src/core/patch-lint-diff.js +17 -32
- package/dist/src/core/patch-lint-imports.js +0 -1
- package/dist/src/core/patch-lint-jsdoc.js +0 -1
- package/dist/src/core/patch-lint-observer.js +0 -1
- package/dist/src/core/patch-lint-ownership.js +0 -1
- package/dist/src/core/patch-lint-reexports.js +0 -1
- package/dist/src/core/patch-lint.d.ts +37 -2
- package/dist/src/core/patch-lint.js +47 -34
- package/dist/src/core/patch-lock.js +0 -1
- package/dist/src/core/patch-manifest-consistency.js +9 -1
- package/dist/src/core/patch-manifest-io.d.ts +19 -2
- package/dist/src/core/patch-manifest-io.js +57 -21
- package/dist/src/core/patch-manifest-query.js +18 -10
- package/dist/src/core/patch-manifest-resolve.js +0 -1
- package/dist/src/core/patch-manifest-validate.js +24 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -2
- package/dist/src/core/patch-parse.d.ts +59 -0
- package/dist/src/core/patch-parse.js +209 -92
- package/dist/src/core/patch-policy.js +0 -1
- package/dist/src/core/patch-registration-refs.js +16 -26
- package/dist/src/core/patch-source-metadata.js +0 -1
- package/dist/src/core/patch-transform.d.ts +4 -0
- package/dist/src/core/patch-transform.js +30 -38
- package/dist/src/core/rebase-session.js +0 -1
- package/dist/src/core/register-browser-content.js +0 -1
- package/dist/src/core/register-module.js +0 -1
- package/dist/src/core/register-result.js +0 -1
- package/dist/src/core/register-scaffold.js +0 -1
- package/dist/src/core/register-shared-css.js +0 -1
- package/dist/src/core/register-test-manifest.js +0 -1
- package/dist/src/core/register-xpcshell-test.js +0 -1
- package/dist/src/core/shared-ftl.js +0 -1
- package/dist/src/core/signal-critical.js +0 -1
- package/dist/src/core/smoke-patterns.js +0 -1
- package/dist/src/core/state-file.js +0 -1
- package/dist/src/core/status-classify.d.ts +16 -0
- package/dist/src/core/status-classify.js +24 -16
- package/dist/src/core/test-harness-crash.d.ts +80 -7
- package/dist/src/core/test-harness-crash.js +251 -15
- package/dist/src/core/test-harness-output.js +0 -1
- package/dist/src/core/test-path-scope.d.ts +21 -13
- package/dist/src/core/test-path-scope.js +43 -28
- package/dist/src/core/test-stale-check.d.ts +29 -1
- package/dist/src/core/test-stale-check.js +59 -1
- package/dist/src/core/test-stale-symlink.js +0 -1
- package/dist/src/core/test-xpcshell-retry.js +0 -1
- package/dist/src/core/token-coverage.js +0 -1
- package/dist/src/core/token-dark-mode.js +0 -1
- package/dist/src/core/token-docs.js +0 -1
- package/dist/src/core/token-manager.js +0 -1
- package/dist/src/core/token-scaffold.js +0 -1
- package/dist/src/core/token-variant.js +0 -1
- package/dist/src/core/toolchain-preflight.d.ts +31 -10
- package/dist/src/core/toolchain-preflight.js +97 -36
- package/dist/src/core/typecheck-shim.js +13 -1
- package/dist/src/core/typecheck.js +0 -1
- package/dist/src/core/wire-destroy.js +0 -1
- package/dist/src/core/wire-dom-fragment.js +0 -1
- package/dist/src/core/wire-init.js +0 -1
- package/dist/src/core/wire-subscript.js +0 -1
- package/dist/src/core/wire-targets.js +0 -1
- package/dist/src/core/wire-utils.js +0 -1
- package/dist/src/core/xpcshell-appdir.js +1 -4
- package/dist/src/errors/base.d.ts +29 -0
- package/dist/src/errors/base.js +35 -1
- package/dist/src/errors/build.js +0 -1
- package/dist/src/errors/codes.d.ts +6 -0
- package/dist/src/errors/codes.js +6 -1
- package/dist/src/errors/config.js +0 -1
- package/dist/src/errors/download.js +0 -1
- package/dist/src/errors/furnace.d.ts +11 -1
- package/dist/src/errors/furnace.js +6 -5
- package/dist/src/errors/git.js +0 -1
- package/dist/src/errors/patch.d.ts +17 -0
- package/dist/src/errors/patch.js +29 -1
- package/dist/src/errors/rebase.js +0 -1
- package/dist/src/errors/run.js +1 -1
- package/dist/src/index.js +0 -1
- package/dist/src/types/cli.js +0 -1
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/index.js +0 -1
- package/dist/src/types/commands/options.d.ts +40 -9
- package/dist/src/types/commands/options.js +0 -1
- package/dist/src/types/commands/patches.d.ts +37 -0
- package/dist/src/types/commands/patches.js +0 -1
- package/dist/src/types/commands/project.d.ts +11 -2
- package/dist/src/types/commands/project.js +0 -1
- package/dist/src/types/config.d.ts +32 -0
- package/dist/src/types/config.js +0 -1
- package/dist/src/types/furnace.d.ts +23 -0
- package/dist/src/types/furnace.js +0 -1
- package/dist/src/types/index.js +0 -1
- package/dist/src/types/typecheck.js +0 -1
- package/dist/src/utils/elapsed.js +0 -1
- package/dist/src/utils/errors.js +0 -1
- package/dist/src/utils/fs.js +37 -5
- package/dist/src/utils/logger.d.ts +7 -0
- package/dist/src/utils/logger.js +85 -3
- package/dist/src/utils/options.d.ts +15 -0
- package/dist/src/utils/options.js +25 -1
- package/dist/src/utils/package-root.js +11 -4
- package/dist/src/utils/parse.js +0 -1
- package/dist/src/utils/paths.js +0 -1
- package/dist/src/utils/platform.js +0 -1
- package/dist/src/utils/process-group.d.ts +33 -0
- package/dist/src/utils/process-group.js +161 -0
- package/dist/src/utils/process.d.ts +48 -4
- package/dist/src/utils/process.js +258 -59
- package/dist/src/utils/regex.d.ts +13 -3
- package/dist/src/utils/regex.js +53 -4
- package/dist/src/utils/validation.d.ts +0 -12
- package/dist/src/utils/validation.js +13 -26
- package/package.json +9 -8
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { dirname, join } from 'node:path';
|
|
4
5
|
import { toError } from '../utils/errors.js';
|
|
5
6
|
import { ensureDir } from '../utils/fs.js';
|
|
@@ -7,6 +8,15 @@ import { verbose, warn } from '../utils/logger.js';
|
|
|
7
8
|
const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
|
|
8
9
|
const DEFAULT_LOCK_POLL_MS = 50;
|
|
9
10
|
const DEFAULT_STALE_LOCK_MS = 5 * 60_000;
|
|
11
|
+
/**
|
|
12
|
+
* Minimum interval between stale-lock probes while a waiter polls. The probe
|
|
13
|
+
* used to run exactly once per waiter: if the holder died *after* that single
|
|
14
|
+
* probe, the waiter polled a permanently dead lock until `timeoutMs` — for
|
|
15
|
+
* the build lock that meant up to 24 hours (see `withBuildLock` in mach.ts).
|
|
16
|
+
* Re-probing on a small interval bounds that hang to seconds while keeping
|
|
17
|
+
* the per-poll cost negligible (one stat + one small read every ~5 s).
|
|
18
|
+
*/
|
|
19
|
+
const STALE_REPROBE_INTERVAL_MS = 5_000;
|
|
10
20
|
function sleep(ms) {
|
|
11
21
|
return new Promise((resolve) => {
|
|
12
22
|
setTimeout(resolve, ms);
|
|
@@ -39,6 +49,100 @@ function isProcessAlive(pid) {
|
|
|
39
49
|
return getNodeErrorCode(error) !== 'ESRCH';
|
|
40
50
|
}
|
|
41
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Reads the owner PID (line 1) and acquisition token (line 2) from a lock
|
|
54
|
+
* directory's PID file. Returns `{ present: false }` when the PID file is
|
|
55
|
+
* missing or the PID does not parse as a finite integer (caller falls back
|
|
56
|
+
* to the age-only staleness heuristic).
|
|
57
|
+
*
|
|
58
|
+
* File format note: external readers (`doctor-furnace.ts`, older FireForge
|
|
59
|
+
* releases) do `parseInt(content.trim(), 10)`, which parses the leading
|
|
60
|
+
* digits of a multi-line file — so adding the token line stays compatible.
|
|
61
|
+
*/
|
|
62
|
+
async function readLockOwner(lockPath) {
|
|
63
|
+
try {
|
|
64
|
+
const pidContent = await readFile(join(lockPath, LOCK_PID_FILE), 'utf-8');
|
|
65
|
+
const [pidLine, tokenLine] = pidContent.split('\n');
|
|
66
|
+
const pid = parseInt((pidLine ?? '').trim(), 10);
|
|
67
|
+
if (Number.isFinite(pid)) {
|
|
68
|
+
const token = tokenLine?.trim();
|
|
69
|
+
if (token !== undefined && token.length > 0) {
|
|
70
|
+
return { present: true, pid, token };
|
|
71
|
+
}
|
|
72
|
+
return { present: true, pid };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// PID file missing or unreadable — treat as absent.
|
|
77
|
+
}
|
|
78
|
+
return { present: false };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Reaps a lock directory believed to be stale, safely against concurrent
|
|
82
|
+
* reapers and a concurrent legitimate re-acquisition.
|
|
83
|
+
*
|
|
84
|
+
* The naive stat → read-PID → `rm` sequence had a TOCTOU hole: two waiters
|
|
85
|
+
* could both observe the dead owner, one would remove the stale dir and
|
|
86
|
+
* `mkdir` its own lock, and the other's already-decided `rm` then deleted
|
|
87
|
+
* the *fresh* lock — two processes inside the critical section at once.
|
|
88
|
+
*
|
|
89
|
+
* Rename-aside closes it: `rename()` is atomic, so exactly one reaper wins
|
|
90
|
+
* ownership of the directory (the loser gets ENOENT and simply re-polls),
|
|
91
|
+
* and nothing ever deletes the live path directly. After winning the rename
|
|
92
|
+
* we re-read the owner from the renamed dir — if it is now a *live* process
|
|
93
|
+
* (the lock was released and re-acquired between our staleness read and the
|
|
94
|
+
* rename), we rename it back instead of reaping.
|
|
95
|
+
*
|
|
96
|
+
* @returns true when the caller should immediately retry `mkdir` (the lock
|
|
97
|
+
* path is now free or was freed by someone else), false when the lock is
|
|
98
|
+
* legitimately held and the caller should keep waiting.
|
|
99
|
+
*/
|
|
100
|
+
async function reapStaleLock(lockPath, ageMs, onStaleLockMessage) {
|
|
101
|
+
const reapPath = `${lockPath}.reaping-${String(process.pid)}-${randomUUID()}`;
|
|
102
|
+
try {
|
|
103
|
+
await rename(lockPath, reapPath);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (getNodeErrorCode(error) === 'ENOENT') {
|
|
107
|
+
// Another reaper won, or the owner released — either way, retry mkdir.
|
|
108
|
+
verbose(`Stale lock disappeared before reaping completed: ${lockPath}`);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
verbose(`Could not rename stale lock aside (${lockPath}): ${toError(error).message}`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
// We exclusively own reapPath now. Re-verify: the owner may have changed
|
|
115
|
+
// between the staleness probe and the rename (release + fresh acquire).
|
|
116
|
+
const owner = await readLockOwner(reapPath);
|
|
117
|
+
if (owner.present && isProcessAlive(owner.pid)) {
|
|
118
|
+
try {
|
|
119
|
+
await rename(reapPath, lockPath);
|
|
120
|
+
verbose(`Aborted stale-lock reap of ${lockPath}: owner PID ${String(owner.pid)} is alive (lock was re-acquired mid-probe)`);
|
|
121
|
+
}
|
|
122
|
+
catch (restoreError) {
|
|
123
|
+
// Rename-back can only fail if a third process mkdir'd the lock path
|
|
124
|
+
// in this instant. The displaced live owner keeps running without its
|
|
125
|
+
// lock — surface it loudly instead of hiding the lost exclusion.
|
|
126
|
+
warn(`Could not restore live lock for PID ${String(owner.pid)} at ${lockPath} ` +
|
|
127
|
+
`(${toError(restoreError).message}). Displaced lock kept at ${reapPath} for inspection.`);
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
const staleMessage = onStaleLockMessage?.(ageMs);
|
|
132
|
+
if (staleMessage) {
|
|
133
|
+
warn(staleMessage);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
verbose(`Removing stale lock ${lockPath} (age: ${Math.round(ageMs / 1000)}s)`);
|
|
137
|
+
}
|
|
138
|
+
await rm(reapPath, { recursive: true, force: true });
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Probes a contended lock for staleness and reaps it when the owner is gone.
|
|
143
|
+
*
|
|
144
|
+
* @returns true when the caller should immediately retry `mkdir`.
|
|
145
|
+
*/
|
|
42
146
|
async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
43
147
|
try {
|
|
44
148
|
const lockStat = await stat(lockPath);
|
|
@@ -48,27 +152,19 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
|
48
152
|
// 5 minutes) was never removed even when its PID file pointed at a
|
|
49
153
|
// process that had already exited. That's the exact situation an
|
|
50
154
|
// operator lands in after SIGINT'ing `furnace preview` — the signal
|
|
51
|
-
// handler calls `process.exit`, `withFileLock`'s
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
if (!isProcessAlive(
|
|
58
|
-
|
|
59
|
-
if (staleMessage) {
|
|
60
|
-
warn(staleMessage);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
verbose(`Lock at ${lockPath} owner PID ${pidCheck.pid} is no longer running — removing (age: ${Math.round(ageMs / 1000)}s)`);
|
|
64
|
-
}
|
|
65
|
-
await rm(lockPath, { recursive: true, force: true });
|
|
66
|
-
return true;
|
|
155
|
+
// handler calls `process.exit`, `withFileLock`'s release never runs,
|
|
156
|
+
// and the next command has to either wait 5 minutes for the staleness
|
|
157
|
+
// gate or manually remove the lock directory. With the PID-first check,
|
|
158
|
+
// an explicitly-dead owner unblocks immediately.
|
|
159
|
+
const owner = await readLockOwner(lockPath);
|
|
160
|
+
if (owner.present) {
|
|
161
|
+
if (!isProcessAlive(owner.pid)) {
|
|
162
|
+
return await reapStaleLock(lockPath, ageMs, onStaleLockMessage);
|
|
67
163
|
}
|
|
68
164
|
// PID is alive — respect it regardless of age. A slow `mach build`
|
|
69
165
|
// legitimately holds the lock past the stale threshold and we don't
|
|
70
166
|
// want to race-remove it.
|
|
71
|
-
verbose(`Lock at ${lockPath} is ${Math.round(ageMs / 1000)}s old but PID ${
|
|
167
|
+
verbose(`Lock at ${lockPath} is ${Math.round(ageMs / 1000)}s old but PID ${String(owner.pid)} is still running — not removing`);
|
|
72
168
|
return false;
|
|
73
169
|
}
|
|
74
170
|
// No readable PID file. Fall back to the age-only heuristic so locks
|
|
@@ -77,12 +173,7 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
|
77
173
|
if (ageMs <= staleMs) {
|
|
78
174
|
return false;
|
|
79
175
|
}
|
|
80
|
-
|
|
81
|
-
if (staleMessage) {
|
|
82
|
-
warn(staleMessage);
|
|
83
|
-
}
|
|
84
|
-
await rm(lockPath, { recursive: true, force: true });
|
|
85
|
-
return true;
|
|
176
|
+
return await reapStaleLock(lockPath, ageMs, onStaleLockMessage);
|
|
86
177
|
}
|
|
87
178
|
catch (error) {
|
|
88
179
|
const code = getNodeErrorCode(error);
|
|
@@ -95,30 +186,38 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
|
95
186
|
}
|
|
96
187
|
}
|
|
97
188
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
189
|
+
* Releases a lock previously acquired by this process, verifying ownership
|
|
190
|
+
* first. The unconditional `finally { rm }` this replaces would remove
|
|
191
|
+
* *whichever* lock directory was present — if a stale-lock reaper had (in a
|
|
192
|
+
* pathological race) replaced our lock, we deleted the new owner's lock and
|
|
193
|
+
* compounded the double-acquisition. When the PID file no longer names us,
|
|
194
|
+
* warn and leave the directory alone.
|
|
195
|
+
*
|
|
196
|
+
* `ownerFileWritten` is false when our own owner-file write failed at
|
|
197
|
+
* acquisition (non-fatal there); ownership then cannot be verified and we
|
|
198
|
+
* fall back to unconditional removal — identical to the historical behavior
|
|
199
|
+
* and strictly better than leaking the lock.
|
|
101
200
|
*/
|
|
102
|
-
async function
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
201
|
+
async function releaseLock(lockPath, ownerToken, ownerFileWritten) {
|
|
202
|
+
if (ownerFileWritten) {
|
|
203
|
+
const owner = await readLockOwner(lockPath);
|
|
204
|
+
if (owner.present &&
|
|
205
|
+
(owner.pid !== process.pid || (owner.token !== undefined && owner.token !== ownerToken))) {
|
|
206
|
+
warn(`Not removing lock ${lockPath}: it is now owned by PID ${String(owner.pid)} — ` +
|
|
207
|
+
'this process no longer holds it.');
|
|
208
|
+
return;
|
|
108
209
|
}
|
|
109
210
|
}
|
|
110
|
-
|
|
111
|
-
// PID file missing or unreadable — treat as absent.
|
|
112
|
-
}
|
|
113
|
-
return { present: false };
|
|
211
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
114
212
|
}
|
|
115
213
|
/** Runs an async operation while holding a directory lock, with stale-lock recovery. */
|
|
116
214
|
export async function withFileLock(lockPath, operation, options = {}) {
|
|
117
215
|
const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
118
216
|
const pollMs = options.pollMs ?? DEFAULT_LOCK_POLL_MS;
|
|
119
217
|
const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
|
|
218
|
+
const staleReprobeMs = options.staleReprobeMs ?? STALE_REPROBE_INTERVAL_MS;
|
|
120
219
|
const deadline = Date.now() + timeoutMs;
|
|
121
|
-
let
|
|
220
|
+
let lastStaleProbeAt;
|
|
122
221
|
await ensureDir(dirname(lockPath));
|
|
123
222
|
for (;;) {
|
|
124
223
|
try {
|
|
@@ -134,32 +233,42 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
134
233
|
if (!isAlreadyLocked) {
|
|
135
234
|
throw error;
|
|
136
235
|
}
|
|
137
|
-
if (
|
|
138
|
-
|
|
236
|
+
if (lastStaleProbeAt === undefined || Date.now() - lastStaleProbeAt >= staleReprobeMs) {
|
|
237
|
+
lastStaleProbeAt = Date.now();
|
|
139
238
|
if (await removeIfStaleLock(lockPath, staleMs, options.onStaleLockMessage)) {
|
|
140
239
|
continue;
|
|
141
240
|
}
|
|
142
241
|
}
|
|
143
242
|
if (Date.now() >= deadline) {
|
|
144
|
-
|
|
145
|
-
|
|
243
|
+
const owner = await readLockOwner(lockPath);
|
|
244
|
+
const holderHint = owner.present && isProcessAlive(owner.pid)
|
|
245
|
+
? ` It is held by running process ${String(owner.pid)} — wait for it to finish, or stop it and retry.`
|
|
246
|
+
: ' If no other FireForge process is running, remove the lock directory and retry.';
|
|
247
|
+
throw new Error(options.onTimeoutMessage ?? `Timed out waiting for file lock ${lockPath}.${holderHint}`, { cause: error });
|
|
146
248
|
}
|
|
147
249
|
await sleep(pollMs);
|
|
148
250
|
}
|
|
149
251
|
}
|
|
150
|
-
//
|
|
151
|
-
//
|
|
252
|
+
// Stamp ownership (PID + per-acquisition token) into the lock directory so
|
|
253
|
+
// stale-lock recovery can check liveness and release can verify the lock
|
|
254
|
+
// is still ours before removing it.
|
|
255
|
+
const ownerToken = randomUUID();
|
|
256
|
+
let ownerFileWritten = false;
|
|
152
257
|
try {
|
|
153
|
-
|
|
258
|
+
const metadata = options.ownerMetadata
|
|
259
|
+
?.map((line) => line.replace(/\r?\n/g, ' ').trim())
|
|
260
|
+
.filter((line) => line.length > 0) ?? [];
|
|
261
|
+
await writeFile(join(lockPath, LOCK_PID_FILE), `${String(process.pid)}\n${ownerToken}\n${metadata.length > 0 ? `${metadata.join('\n')}\n` : ''}`, 'utf-8');
|
|
262
|
+
ownerFileWritten = true;
|
|
154
263
|
}
|
|
155
264
|
catch {
|
|
156
|
-
// Non-fatal: stale recovery falls back to age-only heuristic
|
|
265
|
+
// Non-fatal: stale recovery falls back to age-only heuristic and release
|
|
266
|
+
// falls back to unconditional removal.
|
|
157
267
|
}
|
|
158
268
|
try {
|
|
159
269
|
return await operation();
|
|
160
270
|
}
|
|
161
271
|
finally {
|
|
162
|
-
await
|
|
272
|
+
await releaseLock(lockPath, ownerToken, ownerFileWritten);
|
|
163
273
|
}
|
|
164
274
|
}
|
|
165
|
-
//# sourceMappingURL=file-lock.js.map
|
|
@@ -12,6 +12,10 @@ export interface ResolvedArchive {
|
|
|
12
12
|
url: string;
|
|
13
13
|
filename: string;
|
|
14
14
|
metadataFilename: string;
|
|
15
|
+
/** URL of Mozilla's published SHA256SUMS file for the release. */
|
|
16
|
+
checksumsUrl: string;
|
|
17
|
+
/** The archive's path as listed inside SHA256SUMS (relative to the release dir). */
|
|
18
|
+
pathInChecksums: string;
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
21
|
* Sidecar metadata stored alongside a cached archive.
|
|
@@ -62,6 +62,11 @@ export function resolveArchive(version, product = 'firefox') {
|
|
|
62
62
|
url: `${getArchiveBaseUrl(product)}/${archiveVersion}/source/firefox-${archiveVersion}.source.tar.xz`,
|
|
63
63
|
filename: `firefox-${safeProduct}-${archiveVersion}.source.tar.xz`,
|
|
64
64
|
metadataFilename: `firefox-${safeProduct}-${archiveVersion}.source.tar.xz.json`,
|
|
65
|
+
// Mozilla publishes one SHA256SUMS per release directory listing every
|
|
66
|
+
// artifact by its release-relative path. Downloads verify against it by
|
|
67
|
+
// default (fail closed on mismatch), so a compromised CDN response
|
|
68
|
+
// cannot silently become the trusted git baseline.
|
|
69
|
+
checksumsUrl: `${getArchiveBaseUrl(product)}/${archiveVersion}/SHA256SUMS`,
|
|
70
|
+
pathInChecksums: `source/firefox-${archiveVersion}.source.tar.xz`,
|
|
65
71
|
};
|
|
66
72
|
}
|
|
67
|
-
//# sourceMappingURL=firefox-archive.js.map
|
|
@@ -10,7 +10,7 @@ import { pipeline } from 'node:stream/promises';
|
|
|
10
10
|
import { ChecksumMismatchError } from '../errors/download.js';
|
|
11
11
|
import { toError } from '../utils/errors.js';
|
|
12
12
|
import { pathExistsStrict, readJson, removeFile, writeJson } from '../utils/fs.js';
|
|
13
|
-
import { verbose } from '../utils/logger.js';
|
|
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
16
|
import { downloadFile } from './firefox-download.js';
|
|
@@ -24,6 +24,39 @@ async function sha256File(filePath) {
|
|
|
24
24
|
await pipeline(stream, hash);
|
|
25
25
|
return hash.digest('hex');
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Fetches Mozilla's published SHA256SUMS for the release and extracts the
|
|
29
|
+
* digest for this archive. Returns null when the checksum file cannot be
|
|
30
|
+
* fetched or does not list the archive — the caller warns loudly and
|
|
31
|
+
* continues (offline mirrors and staging hosts legitimately lack it), while
|
|
32
|
+
* a FETCHED-but-mismatching digest always fails closed.
|
|
33
|
+
*
|
|
34
|
+
* Line format (both variants appear in the wild):
|
|
35
|
+
* `<64-hex-digest> <release-relative-path>`
|
|
36
|
+
* `<64-hex-digest> *<release-relative-path>`
|
|
37
|
+
*/
|
|
38
|
+
async function fetchPublishedSha256(archive) {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(archive.checksumsUrl);
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
verbose(`SHA256SUMS fetch returned HTTP ${String(response.status)} for ${archive.checksumsUrl}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const body = await response.text();
|
|
46
|
+
for (const line of body.split('\n')) {
|
|
47
|
+
const match = /^([0-9a-f]{64})\s+\*?(.+)$/.exec(line.trim());
|
|
48
|
+
if (match?.[1] && match[2] === archive.pathInChecksums) {
|
|
49
|
+
return match[1];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
verbose(`SHA256SUMS at ${archive.checksumsUrl} does not list ${archive.pathInChecksums}`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
verbose(`SHA256SUMS fetch failed for ${archive.checksumsUrl}: ${toError(error).message}`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
27
60
|
/**
|
|
28
61
|
* Ensures a valid cached archive exists, downloading it if needed.
|
|
29
62
|
* @param archive - Resolved archive descriptor
|
|
@@ -117,8 +150,32 @@ async function downloadToCache(archive, cacheDir, onProgress, expectedSha256, on
|
|
|
117
150
|
promotedTarball = true;
|
|
118
151
|
onCacheProgress?.(`Calculating source archive SHA-256 for ${archive.filename}...`);
|
|
119
152
|
const sha256 = await sha256File(tarballPath);
|
|
120
|
-
if (expectedSha256
|
|
121
|
-
|
|
153
|
+
if (expectedSha256) {
|
|
154
|
+
// Operator-pinned digest (firefox.sha256 config): takes precedence
|
|
155
|
+
// and always fails closed.
|
|
156
|
+
if (sha256 !== expectedSha256) {
|
|
157
|
+
throw new ChecksumMismatchError(archive.product, expectedSha256, sha256, archive.url);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Default integrity check: verify against Mozilla's published
|
|
162
|
+
// SHA256SUMS. TLS alone is thin trust for the artifact that becomes
|
|
163
|
+
// the git baseline every patch is built on — a compromised CDN
|
|
164
|
+
// response would otherwise be trusted with no signal. Mismatch fails
|
|
165
|
+
// closed (the catch below deletes the artifact); an unfetchable
|
|
166
|
+
// SHA256SUMS degrades to a loud warning so offline/mirror workflows
|
|
167
|
+
// keep working (pin firefox.sha256 in fireforge.json to fail closed
|
|
168
|
+
// even then).
|
|
169
|
+
onCacheProgress?.(`Verifying archive against published SHA256SUMS...`);
|
|
170
|
+
const publishedSha256 = await fetchPublishedSha256(archive);
|
|
171
|
+
if (publishedSha256 === null) {
|
|
172
|
+
warn(`Could not verify ${archive.filename} against Mozilla's published SHA256SUMS ` +
|
|
173
|
+
`(${archive.checksumsUrl} unavailable). The download is trusted on TLS alone — ` +
|
|
174
|
+
'set firefox.sha256 in fireforge.json to require checksum verification.');
|
|
175
|
+
}
|
|
176
|
+
else if (sha256 !== publishedSha256) {
|
|
177
|
+
throw new ChecksumMismatchError(archive.product, publishedSha256, sha256, archive.url);
|
|
178
|
+
}
|
|
122
179
|
}
|
|
123
180
|
onCacheProgress?.(`Writing source archive cache metadata for ${archive.metadataFilename}...`);
|
|
124
181
|
await writeJson(metadataPath, {
|
|
@@ -167,4 +224,3 @@ async function removeArchivePartFiles(archive, cacheDir) {
|
|
|
167
224
|
// Cache dir may not exist yet — that's fine.
|
|
168
225
|
}
|
|
169
226
|
}
|
|
170
|
-
//# sourceMappingURL=firefox-cache.js.map
|
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
import { ExtractionError } from '../errors/download.js';
|
|
7
7
|
import { elapsedSince } from '../utils/elapsed.js';
|
|
8
8
|
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
9
|
-
import { exec, executableExists } from '../utils/process.js';
|
|
9
|
+
import { exec, execStream, executableExists } from '../utils/process.js';
|
|
10
10
|
/**
|
|
11
11
|
* Returns true when an archive member path could land outside the
|
|
12
12
|
* extraction root: absolute (POSIX or Windows drive/backslash rooted) or
|
|
@@ -88,26 +88,86 @@ export function findUnsafeArchiveLink(verboseLines) {
|
|
|
88
88
|
* (~38%) to this one-time extraction step, versus ~44 s if run
|
|
89
89
|
* sequentially.
|
|
90
90
|
*/
|
|
91
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Incremental line splitter for streamed listings. Keeps only the current
|
|
93
|
+
* partial line in memory, so a 350k-entry Firefox listing costs O(longest
|
|
94
|
+
* line), not O(listing).
|
|
95
|
+
*/
|
|
96
|
+
function createLineScanner(onLine) {
|
|
97
|
+
let buffer = '';
|
|
98
|
+
return {
|
|
99
|
+
push(chunk) {
|
|
100
|
+
buffer += chunk;
|
|
101
|
+
let idx;
|
|
102
|
+
while ((idx = buffer.indexOf('\n')) !== -1) {
|
|
103
|
+
onLine(buffer.slice(0, idx));
|
|
104
|
+
buffer = buffer.slice(idx + 1);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
flush() {
|
|
108
|
+
if (buffer.length > 0) {
|
|
109
|
+
onLine(buffer);
|
|
110
|
+
buffer = '';
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Runs one tar listing pass, streaming lines through `checkLine` and
|
|
117
|
+
* returning the first unsafe finding (or null) plus the exit code.
|
|
118
|
+
*
|
|
119
|
+
* Streaming matters for safety, not just memory: the buffered `exec`
|
|
120
|
+
* collector silently truncates at 50 MB, and a full Firefox `-tvf` listing
|
|
121
|
+
* already sits in the 40–50 MB range — so the buffered implementation could
|
|
122
|
+
* scan only the head of the listing while reporting the archive safe. A
|
|
123
|
+
* crafted archive could exploit exactly that by padding benign entries
|
|
124
|
+
* first and hiding a traversal name or absolute link target past the cap.
|
|
125
|
+
* With per-line streaming EVERY entry is scanned, with bounded memory.
|
|
126
|
+
*/
|
|
127
|
+
async function runListingScan(archivePath, tarFlag, checkLine) {
|
|
92
128
|
// LC_ALL=C keeps the -tvf column format stable for the link parse.
|
|
93
129
|
const listEnv = { LC_ALL: 'C', LANG: 'C' };
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
130
|
+
let unsafe = null;
|
|
131
|
+
let stderrTail = '';
|
|
132
|
+
const scanner = createLineScanner((line) => {
|
|
133
|
+
if (unsafe === null) {
|
|
134
|
+
unsafe = checkLine(line);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
const exitCode = await execStream('tar', [tarFlag, archivePath], {
|
|
138
|
+
env: listEnv,
|
|
139
|
+
onStdout: (chunk) => {
|
|
140
|
+
scanner.push(chunk);
|
|
141
|
+
},
|
|
142
|
+
onStderr: (chunk) => {
|
|
143
|
+
// Keep a small diagnostic tail; the listing itself is the big stream.
|
|
144
|
+
stderrTail = (stderrTail + chunk).slice(-4096);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
scanner.flush();
|
|
148
|
+
return { unsafe, exitCode, stderrTail };
|
|
149
|
+
}
|
|
150
|
+
async function preflightArchiveEntries(archivePath) {
|
|
151
|
+
const [nameScan, linkScan] = await Promise.all([
|
|
152
|
+
runListingScan(archivePath, '-tf', (line) => {
|
|
153
|
+
const name = line.trim();
|
|
154
|
+
if (name.length === 0)
|
|
155
|
+
return null;
|
|
156
|
+
return isUnsafeArchivePath(name) ? name : null;
|
|
157
|
+
}),
|
|
158
|
+
runListingScan(archivePath, '-tvf', (line) => findUnsafeArchiveLink([line])),
|
|
97
159
|
]);
|
|
98
|
-
if (
|
|
99
|
-
throw new ExtractionError(archivePath, new Error(`tar -tf preflight exited with code ${
|
|
160
|
+
if (nameScan.exitCode !== 0) {
|
|
161
|
+
throw new ExtractionError(archivePath, new Error(`tar -tf preflight exited with code ${nameScan.exitCode}:\n${nameScan.stderrTail}`));
|
|
100
162
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
throw new ExtractionError(archivePath, new Error(`Archive rejected: member name could escape the extraction root: ${unsafeName}`));
|
|
163
|
+
if (nameScan.unsafe !== null) {
|
|
164
|
+
throw new ExtractionError(archivePath, new Error(`Archive rejected: member name could escape the extraction root: ${nameScan.unsafe}`));
|
|
104
165
|
}
|
|
105
|
-
if (
|
|
106
|
-
throw new ExtractionError(archivePath, new Error(`tar -tvf preflight exited with code ${
|
|
166
|
+
if (linkScan.exitCode !== 0) {
|
|
167
|
+
throw new ExtractionError(archivePath, new Error(`tar -tvf preflight exited with code ${linkScan.exitCode}:\n${linkScan.stderrTail}`));
|
|
107
168
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
throw new ExtractionError(archivePath, new Error(`Archive rejected: link could escape the extraction root (${unsafeLink})`));
|
|
169
|
+
if (linkScan.unsafe !== null) {
|
|
170
|
+
throw new ExtractionError(archivePath, new Error(`Archive rejected: link could escape the extraction root (${linkScan.unsafe})`));
|
|
111
171
|
}
|
|
112
172
|
}
|
|
113
173
|
/**
|
|
@@ -172,4 +232,3 @@ export function formatBytes(bytes) {
|
|
|
172
232
|
}
|
|
173
233
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
174
234
|
}
|
|
175
|
-
//# sourceMappingURL=firefox-extract.js.map
|
|
@@ -35,6 +35,18 @@ export type FirefoxSourcePhase = 'download' | 'extract';
|
|
|
35
35
|
/** Callback fired at phase transitions during {@link downloadFirefoxSource}. */
|
|
36
36
|
export type FirefoxSourcePhaseCallback = (phase: FirefoxSourcePhase) => void;
|
|
37
37
|
export type FirefoxSourceProgressCallback = (message: string) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Sweeps orphaned working directories left behind by interrupted downloads
|
|
40
|
+
* (`<engine>.tmp-*` from extraction, `<engine>.replacement-*` from --force
|
|
41
|
+
* replacement). Both hold multi-GB partial trees, are worthless after the
|
|
42
|
+
* owning process died, and nothing else ever reclaimed them. Deliberately
|
|
43
|
+
* does NOT touch `<engine>.backup-*`: a backup may hold the operator's
|
|
44
|
+
* previous engine after a failed forced replacement and is their recovery
|
|
45
|
+
* copy — callers should surface its existence instead.
|
|
46
|
+
*
|
|
47
|
+
* @returns names of the directories that were removed
|
|
48
|
+
*/
|
|
49
|
+
export declare function sweepOrphanedEngineWorkDirs(destDir: string): Promise<string[]>;
|
|
38
50
|
/**
|
|
39
51
|
* Downloads and extracts Firefox source.
|
|
40
52
|
* @param version - Firefox version to download
|
package/dist/src/core/firefox.js
CHANGED
|
@@ -33,6 +33,63 @@ export function getDownloadUrl(version, product = 'firefox') {
|
|
|
33
33
|
export function getTarballFilename(version, product = 'firefox') {
|
|
34
34
|
return resolveArchive(version, product).filename;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Classifies an extraction failure as archive corruption (invalidate the
|
|
38
|
+
* cache and re-download) versus an environmental problem (keep the cached
|
|
39
|
+
* archive; the retry can reuse it). Environmental signatures are the
|
|
40
|
+
* conservative list — anything unrecognized is treated as corruption so a
|
|
41
|
+
* genuinely bad archive can never survive in the cache.
|
|
42
|
+
*/
|
|
43
|
+
function isLikelyArchiveCorruptionError(error) {
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
const code = error instanceof Error ? error.code : undefined;
|
|
46
|
+
if (code === 'ENOSPC' || code === 'EACCES' || code === 'EPERM' || code === 'EDQUOT') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (/No space left on device|Permission denied|Disk quota exceeded/i.test(message)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
// Missing tar binary / preflight refusing an unsafe archive both carry
|
|
53
|
+
// ExtractionError text that names the condition; a missing tar is
|
|
54
|
+
// environmental, an unsafe-entry rejection indicts the archive.
|
|
55
|
+
if (/"tar" command was not found/i.test(message)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Sweeps orphaned working directories left behind by interrupted downloads
|
|
62
|
+
* (`<engine>.tmp-*` from extraction, `<engine>.replacement-*` from --force
|
|
63
|
+
* replacement). Both hold multi-GB partial trees, are worthless after the
|
|
64
|
+
* owning process died, and nothing else ever reclaimed them. Deliberately
|
|
65
|
+
* does NOT touch `<engine>.backup-*`: a backup may hold the operator's
|
|
66
|
+
* previous engine after a failed forced replacement and is their recovery
|
|
67
|
+
* copy — callers should surface its existence instead.
|
|
68
|
+
*
|
|
69
|
+
* @returns names of the directories that were removed
|
|
70
|
+
*/
|
|
71
|
+
export async function sweepOrphanedEngineWorkDirs(destDir) {
|
|
72
|
+
const { readdir } = await import('node:fs/promises');
|
|
73
|
+
const { basename, dirname } = await import('node:path');
|
|
74
|
+
const parent = dirname(destDir);
|
|
75
|
+
const enginePrefixTmp = `${basename(destDir)}.tmp-`;
|
|
76
|
+
const enginePrefixReplacement = `${basename(destDir)}.replacement-`;
|
|
77
|
+
let entries;
|
|
78
|
+
try {
|
|
79
|
+
entries = await readdir(parent);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
const removed = [];
|
|
85
|
+
for (const name of entries) {
|
|
86
|
+
if (name.startsWith(enginePrefixTmp) || name.startsWith(enginePrefixReplacement)) {
|
|
87
|
+
await removeDir(join(parent, name));
|
|
88
|
+
removed.push(name);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return removed;
|
|
92
|
+
}
|
|
36
93
|
/**
|
|
37
94
|
* Downloads and extracts Firefox source.
|
|
38
95
|
* @param version - Firefox version to download
|
|
@@ -60,7 +117,13 @@ export async function downloadFirefoxSource(version, product, destDir, cacheDir,
|
|
|
60
117
|
}
|
|
61
118
|
catch (error) {
|
|
62
119
|
await removeDir(tempDir);
|
|
63
|
-
|
|
120
|
+
// Only throw away the ~600 MB cached archive when the failure indicts
|
|
121
|
+
// the ARCHIVE, not the environment. ENOSPC, a permission problem in
|
|
122
|
+
// destDir, or tar disappearing mid-run used to invalidate a perfectly
|
|
123
|
+
// good download and force a full re-fetch on retry.
|
|
124
|
+
if (isLikelyArchiveCorruptionError(error)) {
|
|
125
|
+
await invalidateArchiveCache(archive, cacheDir);
|
|
126
|
+
}
|
|
64
127
|
throw error;
|
|
65
128
|
}
|
|
66
129
|
// Firefox source extracts to a subdirectory (e.g., firefox-140.0/)
|
|
@@ -80,4 +143,3 @@ export async function downloadFirefoxSource(version, product, destDir, cacheDir,
|
|
|
80
143
|
await rename(tempDir, destDir);
|
|
81
144
|
}
|
|
82
145
|
}
|
|
83
|
-
//# sourceMappingURL=firefox.js.map
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* per-file LOC budget.
|
|
5
5
|
*
|
|
6
6
|
* Every helper here degrades gracefully: if the locale jar.mn is missing or
|
|
7
|
-
* the FTL tree is non-standard, apply logs
|
|
8
|
-
* aborting the whole command.
|
|
9
|
-
*
|
|
7
|
+
* the FTL tree is non-standard, apply logs an ADVISORY `stepError`
|
|
8
|
+
* (`advisory: true`) rather than aborting the whole command. Advisory step
|
|
9
|
+
* errors are reported as warnings and never trigger rollback — missing
|
|
10
|
+
* jar.mn on a fork without a locale package should not block a working
|
|
11
|
+
* `.mjs`/`.css` from shipping (the blocking/advisory split exists because
|
|
12
|
+
* this contract used to be contradicted: any FTL step error rolled back
|
|
13
|
+
* the whole apply).
|
|
10
14
|
*/
|
|
11
15
|
import type { CustomComponentConfig, DryRunAction, StepError } from '../types/furnace.js';
|
|
12
16
|
import { type RollbackJournal } from './furnace-rollback.js';
|