@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
package/dist/src/utils/fs.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { access, chmod, copyFile as fsCopyFile, lstat, mkdir, open, readdir, readFile, rename, rm, stat, statfs, } from 'node:fs/promises';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
5
|
const RETRIABLE_REMOVE_ERRORS = new Set(['ENOTEMPTY', 'EBUSY', 'EPERM']);
|
|
6
|
+
const RETRIABLE_RENAME_ERRORS = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
6
7
|
function sleep(ms) {
|
|
7
8
|
return new Promise((resolve) => {
|
|
8
9
|
setTimeout(resolve, ms);
|
|
@@ -199,7 +200,7 @@ export async function writeFileAtomic(path, content) {
|
|
|
199
200
|
if (existingMode !== undefined) {
|
|
200
201
|
await chmod(tempPath, existingMode);
|
|
201
202
|
}
|
|
202
|
-
await
|
|
203
|
+
await renameWithRetries(tempPath, path);
|
|
203
204
|
await syncParentDir(path);
|
|
204
205
|
}
|
|
205
206
|
catch (error) {
|
|
@@ -207,6 +208,31 @@ export async function writeFileAtomic(path, content) {
|
|
|
207
208
|
throw error;
|
|
208
209
|
}
|
|
209
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Renames the temp file over its destination, retrying transient sharing
|
|
213
|
+
* violations. On Windows, a rename onto a target that another writer is
|
|
214
|
+
* concurrently replacing — or that an antivirus/indexer briefly holds open —
|
|
215
|
+
* fails with EPERM/EACCES/EBUSY even though the same rename succeeds a moment
|
|
216
|
+
* later; POSIX renames never hit this. Non-retriable codes and exhausted
|
|
217
|
+
* budgets rethrow, so a genuine permission problem still surfaces.
|
|
218
|
+
*/
|
|
219
|
+
async function renameWithRetries(from, to) {
|
|
220
|
+
for (let attempt = 0;; attempt++) {
|
|
221
|
+
try {
|
|
222
|
+
await rename(from, to);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
const code = error instanceof Error && 'code' in error && typeof error.code === 'string'
|
|
227
|
+
? error.code
|
|
228
|
+
: undefined;
|
|
229
|
+
if (!code || !RETRIABLE_RENAME_ERRORS.has(code) || attempt === 4) {
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
await sleep(25 * (attempt + 1));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
210
236
|
async function syncParentDir(path) {
|
|
211
237
|
let directoryHandle;
|
|
212
238
|
try {
|
|
@@ -270,7 +296,10 @@ export const FIREFORGE_TMP_PATH_PATTERN = /(^|\/)\.[^/]+\.fireforge-tmp-\d+-[0-9
|
|
|
270
296
|
*/
|
|
271
297
|
function createAtomicTempPath(path) {
|
|
272
298
|
const directory = dirname(path);
|
|
273
|
-
|
|
299
|
+
// basename, not slice(dirname.length + 1): when dirname ends with the
|
|
300
|
+
// separator (filesystem/drive roots, e.g. dirname('/foo') === '/'), the
|
|
301
|
+
// slice arithmetic chopped the first filename character ('/foo' → 'oo').
|
|
302
|
+
const filename = basename(path);
|
|
274
303
|
return join(directory, `.${filename}.fireforge-tmp-${process.pid}-${randomUUID()}`);
|
|
275
304
|
}
|
|
276
305
|
/**
|
|
@@ -287,7 +316,11 @@ function createAtomicTempPath(path) {
|
|
|
287
316
|
export async function checkDiskSpace(path, minBytes, onLowSpace) {
|
|
288
317
|
try {
|
|
289
318
|
const stats = await statfs(path);
|
|
290
|
-
|
|
319
|
+
// bavail = blocks available to UNPRIVILEGED users; bfree includes the
|
|
320
|
+
// root-reserved blocks (typically 5% on ext4), so it over-reported free
|
|
321
|
+
// space and silently suppressed the low-space warning exactly when it
|
|
322
|
+
// mattered.
|
|
323
|
+
const availableBytes = stats.bavail * stats.bsize;
|
|
291
324
|
if (availableBytes < minBytes) {
|
|
292
325
|
const availableGB = (availableBytes / (1024 * 1024 * 1024)).toFixed(1);
|
|
293
326
|
const requiredGB = (minBytes / (1024 * 1024 * 1024)).toFixed(1);
|
|
@@ -302,4 +335,3 @@ export async function checkDiskSpace(path, minBytes, onLowSpace) {
|
|
|
302
335
|
return undefined;
|
|
303
336
|
}
|
|
304
337
|
}
|
|
305
|
-
//# sourceMappingURL=fs.js.map
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
* @param enabled - Whether to enable verbose output
|
|
4
4
|
*/
|
|
5
5
|
export declare function setVerbose(enabled: boolean): void;
|
|
6
|
+
/**
|
|
7
|
+
* Switches the logger into (or out of) machine-output mode. Commands with
|
|
8
|
+
* `--json`/`--raw` flags call this before producing any output.
|
|
9
|
+
*/
|
|
10
|
+
export declare function setMachineOutputMode(enabled: boolean): void;
|
|
11
|
+
/** True when machine-output mode is active. */
|
|
12
|
+
export declare function isMachineOutputMode(): boolean;
|
|
6
13
|
/**
|
|
7
14
|
* Displays a verbose/debug message (only shown if verbose mode is enabled).
|
|
8
15
|
* @param message - Message to display
|
package/dist/src/utils/logger.js
CHANGED
|
@@ -3,6 +3,19 @@ import * as p from '@clack/prompts';
|
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
/** Whether verbose mode is enabled */
|
|
5
5
|
let verboseMode = false;
|
|
6
|
+
/**
|
|
7
|
+
* Whether machine-output mode is active (`--json` / `--raw`).
|
|
8
|
+
*
|
|
9
|
+
* In machine mode, stdout belongs EXCLUSIVELY to the machine-readable
|
|
10
|
+
* payload: all human-facing diagnostics (intro/outro banners, info, warn,
|
|
11
|
+
* error, steps, notes) are routed to stderr as plain unstyled lines.
|
|
12
|
+
* Before this mode existed, clack's log helpers wrote styled warnings to
|
|
13
|
+
* stdout *before* the JSON body — a truncated-directory warning during
|
|
14
|
+
* `status --json` broke every `JSON.parse(stdout)` consumer, and error
|
|
15
|
+
* objects on the machine contract were followed by a styled duplicate on
|
|
16
|
+
* the same stream.
|
|
17
|
+
*/
|
|
18
|
+
let machineOutputMode = false;
|
|
6
19
|
/**
|
|
7
20
|
* Enables or disables verbose mode.
|
|
8
21
|
* @param enabled - Whether to enable verbose output
|
|
@@ -10,6 +23,21 @@ let verboseMode = false;
|
|
|
10
23
|
export function setVerbose(enabled) {
|
|
11
24
|
verboseMode = enabled;
|
|
12
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Switches the logger into (or out of) machine-output mode. Commands with
|
|
28
|
+
* `--json`/`--raw` flags call this before producing any output.
|
|
29
|
+
*/
|
|
30
|
+
export function setMachineOutputMode(enabled) {
|
|
31
|
+
machineOutputMode = enabled;
|
|
32
|
+
}
|
|
33
|
+
/** True when machine-output mode is active. */
|
|
34
|
+
export function isMachineOutputMode() {
|
|
35
|
+
return machineOutputMode;
|
|
36
|
+
}
|
|
37
|
+
/** Writes a plain diagnostic line to stderr (machine-mode side channel). */
|
|
38
|
+
function writeDiagnostic(prefix, message) {
|
|
39
|
+
process.stderr.write(`${prefix}${message}\n`);
|
|
40
|
+
}
|
|
13
41
|
/**
|
|
14
42
|
* Checks if verbose mode is enabled.
|
|
15
43
|
* @returns True if verbose mode is enabled
|
|
@@ -22,43 +50,75 @@ function isVerbose() {
|
|
|
22
50
|
* @param message - Message to display
|
|
23
51
|
*/
|
|
24
52
|
export function verbose(message) {
|
|
25
|
-
if (isVerbose())
|
|
26
|
-
|
|
53
|
+
if (!isVerbose())
|
|
54
|
+
return;
|
|
55
|
+
if (machineOutputMode) {
|
|
56
|
+
writeDiagnostic('[debug] ', message);
|
|
57
|
+
return;
|
|
27
58
|
}
|
|
59
|
+
p.log.info(`[debug] ${message}`);
|
|
28
60
|
}
|
|
29
61
|
function supportsInteractiveSpinner() {
|
|
30
62
|
return process.stdout.isTTY && process.stderr.isTTY;
|
|
31
63
|
}
|
|
32
64
|
/** Displays the top-level intro banner for a command. */
|
|
33
65
|
export function intro(message) {
|
|
66
|
+
if (machineOutputMode)
|
|
67
|
+
return;
|
|
34
68
|
p.intro(message);
|
|
35
69
|
}
|
|
36
70
|
/** Displays the closing outro banner for a command. */
|
|
37
71
|
export function outro(message) {
|
|
72
|
+
if (machineOutputMode)
|
|
73
|
+
return;
|
|
38
74
|
p.outro(message);
|
|
39
75
|
}
|
|
40
76
|
/** Logs an informational message. */
|
|
41
77
|
export function info(message) {
|
|
78
|
+
if (machineOutputMode) {
|
|
79
|
+
writeDiagnostic('', message);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
42
82
|
p.log.info(message);
|
|
43
83
|
}
|
|
44
84
|
/** Logs a success message. */
|
|
45
85
|
export function success(message) {
|
|
86
|
+
if (machineOutputMode) {
|
|
87
|
+
writeDiagnostic('', message);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
46
90
|
p.log.success(message);
|
|
47
91
|
}
|
|
48
92
|
/** Logs a warning message. */
|
|
49
93
|
export function warn(message) {
|
|
94
|
+
if (machineOutputMode) {
|
|
95
|
+
writeDiagnostic('warning: ', message);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
50
98
|
p.log.warn(message);
|
|
51
99
|
}
|
|
52
100
|
/** Logs an error message. */
|
|
53
101
|
export function error(message) {
|
|
102
|
+
if (machineOutputMode) {
|
|
103
|
+
writeDiagnostic('error: ', message);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
54
106
|
p.log.error(message);
|
|
55
107
|
}
|
|
56
108
|
/** Logs an in-progress step message. */
|
|
57
109
|
export function step(message) {
|
|
110
|
+
if (machineOutputMode) {
|
|
111
|
+
writeDiagnostic('', message);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
58
114
|
p.log.step(message);
|
|
59
115
|
}
|
|
60
116
|
/** Logs a plain message without a status prefix. */
|
|
61
117
|
export function message(message) {
|
|
118
|
+
if (machineOutputMode) {
|
|
119
|
+
writeDiagnostic('', message);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
62
122
|
p.log.message(message);
|
|
63
123
|
}
|
|
64
124
|
/** Formats text using the success color without logging it. */
|
|
@@ -75,6 +135,21 @@ export function formatErrorText(message) {
|
|
|
75
135
|
* @returns Spinner handle with message(), stop(), and error() methods
|
|
76
136
|
*/
|
|
77
137
|
export function spinner(initialMessage) {
|
|
138
|
+
if (machineOutputMode) {
|
|
139
|
+
// Spinner progress is diagnostics; keep stdout clean for the payload.
|
|
140
|
+
return {
|
|
141
|
+
message: (msg) => {
|
|
142
|
+
writeDiagnostic('', msg);
|
|
143
|
+
},
|
|
144
|
+
stop: (msg) => {
|
|
145
|
+
if (msg)
|
|
146
|
+
writeDiagnostic('', msg);
|
|
147
|
+
},
|
|
148
|
+
error: (msg) => {
|
|
149
|
+
writeDiagnostic('error: ', msg ?? 'Failed');
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
78
153
|
if (!supportsInteractiveSpinner()) {
|
|
79
154
|
let latestMessage = initialMessage;
|
|
80
155
|
return {
|
|
@@ -107,6 +182,10 @@ export function spinner(initialMessage) {
|
|
|
107
182
|
}
|
|
108
183
|
/** Emits a cancellation message. */
|
|
109
184
|
export function cancel(message) {
|
|
185
|
+
if (machineOutputMode) {
|
|
186
|
+
writeDiagnostic('cancelled: ', message);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
110
189
|
p.cancel(message);
|
|
111
190
|
}
|
|
112
191
|
/** Checks whether a prompt result represents a user cancellation. */
|
|
@@ -115,6 +194,9 @@ export function isCancel(value) {
|
|
|
115
194
|
}
|
|
116
195
|
/** Displays a titled note block for follow-up details. */
|
|
117
196
|
export function note(message, title) {
|
|
197
|
+
if (machineOutputMode) {
|
|
198
|
+
writeDiagnostic('', title ? `${title}: ${message}` : message);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
118
201
|
p.note(message, title);
|
|
119
202
|
}
|
|
120
|
-
//# sourceMappingURL=logger.js.map
|
|
@@ -6,3 +6,18 @@
|
|
|
6
6
|
export declare function pickDefined<T extends Record<string, unknown>>(obj: T): {
|
|
7
7
|
[K in keyof T]+?: Exclude<T[K], undefined>;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Wraps an option-argument parser so its failures surface through
|
|
11
|
+
* commander's own invalid-argument channel.
|
|
12
|
+
*
|
|
13
|
+
* Commander only treats errors whose `code` is `'commander.invalidArgument'`
|
|
14
|
+
* as argument-validation failures; anything else re-throws out of
|
|
15
|
+
* `parseAsync`, BYPASSING `withErrorHandling` entirely and landing in the
|
|
16
|
+
* bin's `main().catch` as an unformatted `Fatal error: …` dump with exit 1.
|
|
17
|
+
* That was the observed behavior for `run --smoke-exit abc` (plain Error),
|
|
18
|
+
* `test --harness-retries 99` (GeneralError), and `export --order 0`
|
|
19
|
+
* (FireForge InvalidArgumentError, whose `code` is a numeric ExitCode).
|
|
20
|
+
* Every `.argParser()`/option-parser callback must be wrapped with this
|
|
21
|
+
* helper (or throw commander's InvalidArgumentError directly).
|
|
22
|
+
*/
|
|
23
|
+
export declare function commanderArgParser<T>(parse: (raw: string) => T): (raw: string) => T;
|
|
@@ -13,4 +13,28 @@ export function pickDefined(obj) {
|
|
|
13
13
|
}
|
|
14
14
|
return result;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
import { InvalidArgumentError as CommanderInvalidArgumentError } from 'commander';
|
|
17
|
+
/**
|
|
18
|
+
* Wraps an option-argument parser so its failures surface through
|
|
19
|
+
* commander's own invalid-argument channel.
|
|
20
|
+
*
|
|
21
|
+
* Commander only treats errors whose `code` is `'commander.invalidArgument'`
|
|
22
|
+
* as argument-validation failures; anything else re-throws out of
|
|
23
|
+
* `parseAsync`, BYPASSING `withErrorHandling` entirely and landing in the
|
|
24
|
+
* bin's `main().catch` as an unformatted `Fatal error: …` dump with exit 1.
|
|
25
|
+
* That was the observed behavior for `run --smoke-exit abc` (plain Error),
|
|
26
|
+
* `test --harness-retries 99` (GeneralError), and `export --order 0`
|
|
27
|
+
* (FireForge InvalidArgumentError, whose `code` is a numeric ExitCode).
|
|
28
|
+
* Every `.argParser()`/option-parser callback must be wrapped with this
|
|
29
|
+
* helper (or throw commander's InvalidArgumentError directly).
|
|
30
|
+
*/
|
|
31
|
+
export function commanderArgParser(parse) {
|
|
32
|
+
return (raw) => {
|
|
33
|
+
try {
|
|
34
|
+
return parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new CommanderInvalidArgumentError(error instanceof Error ? error.message : String(error));
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -34,8 +34,8 @@ export function getPackageRoot() {
|
|
|
34
34
|
}
|
|
35
35
|
let current = dirname(fileURLToPath(import.meta.url));
|
|
36
36
|
for (;;) {
|
|
37
|
+
const packagePath = join(current, 'package.json');
|
|
37
38
|
try {
|
|
38
|
-
const packagePath = join(current, 'package.json');
|
|
39
39
|
const pkg = readPackageMetadata(packagePath);
|
|
40
40
|
if (isFireForgePackageMetadata(pkg)) {
|
|
41
41
|
cachedPackageRoot = current;
|
|
@@ -43,8 +43,16 @@ export function getPackageRoot() {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
catch (error) {
|
|
46
|
-
|
|
47
|
-
//
|
|
46
|
+
// Absent package.json: keep walking. A package.json that EXISTS but
|
|
47
|
+
// fails to parse is a different animal — walking past it could bind
|
|
48
|
+
// to a wrong ancestor package (any parent exposing bin.fireforge) or
|
|
49
|
+
// end in the unhelpful generic "could not locate" error, hiding the
|
|
50
|
+
// actual syntax problem.
|
|
51
|
+
const code = error.code;
|
|
52
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
|
|
53
|
+
throw new Error(`Found ${packagePath} but could not parse it: ` +
|
|
54
|
+
(error instanceof Error ? error.message : String(error)), { cause: error });
|
|
55
|
+
}
|
|
48
56
|
}
|
|
49
57
|
const parent = dirname(current);
|
|
50
58
|
if (parent === current) {
|
|
@@ -66,4 +74,3 @@ export function getPackageVersion() {
|
|
|
66
74
|
const packageRoot = getPackageRoot();
|
|
67
75
|
return readPackageMetadata(join(packageRoot, 'package.json')).version;
|
|
68
76
|
}
|
|
69
|
-
//# sourceMappingURL=package-root.js.map
|
package/dist/src/utils/parse.js
CHANGED
package/dist/src/utils/paths.js
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-group kill and post-run sweep helpers for the exec layer
|
|
3
|
+
* (0.37.0 item 9a). Split out of `process.ts` to keep that file within the
|
|
4
|
+
* per-file line budget; deliberately spawn-based (not `exec`-based) so the
|
|
5
|
+
* two modules do not import each other cyclically.
|
|
6
|
+
*/
|
|
7
|
+
import { type ChildProcess } from 'node:child_process';
|
|
8
|
+
/**
|
|
9
|
+
* Sends `signal` to the child's whole tree: the process GROUP on POSIX
|
|
10
|
+
* (negative-PID kill — reaches mach → python → firefox → content-process
|
|
11
|
+
* chains), or a `taskkill /T /F` tree kill plus a direct `child.kill`
|
|
12
|
+
* fallback on Windows. No-ops once the direct child has exited (group
|
|
13
|
+
* survivors after exit are the post-run sweep's job, not this function's).
|
|
14
|
+
*/
|
|
15
|
+
export declare function killProcessTree(child: ChildProcess, signal: NodeJS.Signals, usesProcessGroup: boolean): void;
|
|
16
|
+
/** One process still alive in a swept group. */
|
|
17
|
+
export interface ProcessGroupSurvivor {
|
|
18
|
+
/** PID, or -1 when pgrep was unavailable and only a liveness probe ran. */
|
|
19
|
+
pid: number;
|
|
20
|
+
/** Command line (pgrep -lf output), best-effort. */
|
|
21
|
+
command: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Post-run reaper for a process group FireForge itself spawned (`pgid` is
|
|
25
|
+
* the PID of a child started with `detached: true`): lists survivors,
|
|
26
|
+
* SIGTERMs the group, waits a short grace, escalates to SIGKILL, and warns
|
|
27
|
+
* about anything that still refuses to die. POSIX only (no-op on win32).
|
|
28
|
+
* The only kill target is `-pgid` — never anything outside the group.
|
|
29
|
+
* A healthy run costs exactly one `pgrep`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function sweepProcessGroup(pgid: number, graceMs?: number): Promise<{
|
|
32
|
+
survivors: ProcessGroupSurvivor[];
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Process-group kill and post-run sweep helpers for the exec layer
|
|
4
|
+
* (0.37.0 item 9a). Split out of `process.ts` to keep that file within the
|
|
5
|
+
* per-file line budget; deliberately spawn-based (not `exec`-based) so the
|
|
6
|
+
* two modules do not import each other cyclically.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { verbose, warn } from './logger.js';
|
|
10
|
+
/**
|
|
11
|
+
* Sends `signal` to the child's whole tree: the process GROUP on POSIX
|
|
12
|
+
* (negative-PID kill — reaches mach → python → firefox → content-process
|
|
13
|
+
* chains), or a `taskkill /T /F` tree kill plus a direct `child.kill`
|
|
14
|
+
* fallback on Windows. No-ops once the direct child has exited (group
|
|
15
|
+
* survivors after exit are the post-run sweep's job, not this function's).
|
|
16
|
+
*/
|
|
17
|
+
export function killProcessTree(child, signal, usesProcessGroup) {
|
|
18
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
19
|
+
return;
|
|
20
|
+
const targetPid = child.pid;
|
|
21
|
+
if (targetPid === undefined)
|
|
22
|
+
return;
|
|
23
|
+
try {
|
|
24
|
+
if (usesProcessGroup) {
|
|
25
|
+
// Negative PID routes to the process group, killing the Python
|
|
26
|
+
// wrapper, the firefox it forked, and every content process
|
|
27
|
+
// that inherited the group.
|
|
28
|
+
process.kill(-targetPid, signal);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// No process group on Windows — taskkill /T walks the descendant
|
|
32
|
+
// tree instead. Always forced (/F): there is no SIGTERM analogue,
|
|
33
|
+
// so the grace window only exists on POSIX.
|
|
34
|
+
spawn('taskkill', ['/pid', String(targetPid), '/T', '/F'], {
|
|
35
|
+
stdio: 'ignore',
|
|
36
|
+
}).on('error', () => {
|
|
37
|
+
// taskkill unavailable — nothing more we can do beyond the
|
|
38
|
+
// direct-child kill below.
|
|
39
|
+
});
|
|
40
|
+
child.kill(signal);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Already gone. Nothing to do.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const SWEEP_GRACE_MS = 2000;
|
|
48
|
+
const MULTIPROCESSING_WORKER_PATTERN = /multiprocessing\.(?:spawn|forkserver)|resource_tracker/;
|
|
49
|
+
function sweepDelay(ms) {
|
|
50
|
+
// Deliberately ref'd (no unref()): this promise is AWAITED between the
|
|
51
|
+
// group SIGTERM and the post-grace re-list/SIGKILL escalation, from a
|
|
52
|
+
// child 'close' handler after the signal forwarder is disposed — with an
|
|
53
|
+
// unref'd timer nothing kept the event loop alive, so Node could exit
|
|
54
|
+
// mid-grace and skip the escalation entirely. Healthy runs never reach
|
|
55
|
+
// this function (sweepProcessGroup early-returns on zero survivors), so
|
|
56
|
+
// the ref never holds a clean exit open.
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
setTimeout(resolve, ms);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Minimal spawn-based pgrep runner. exitCode -1 means pgrep itself was
|
|
63
|
+
* unavailable or errored (distinct from exit 1 = "no matches").
|
|
64
|
+
*/
|
|
65
|
+
async function runPgrep(args) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const child = spawn('pgrep', args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
68
|
+
let stdout = '';
|
|
69
|
+
child.stdout.on('data', (data) => {
|
|
70
|
+
stdout += data.toString('utf8');
|
|
71
|
+
});
|
|
72
|
+
child.on('error', () => {
|
|
73
|
+
resolve({ exitCode: -1, stdout: '' });
|
|
74
|
+
});
|
|
75
|
+
child.on('close', (code) => {
|
|
76
|
+
resolve({ exitCode: code ?? -1, stdout });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Lists processes still in `pgid` via `pgrep -g -lf`, with a kill(0) fallback. */
|
|
81
|
+
async function listGroupSurvivors(pgid) {
|
|
82
|
+
const result = await runPgrep(['-g', String(pgid), '-lf']);
|
|
83
|
+
if (result.exitCode === 0) {
|
|
84
|
+
return result.stdout
|
|
85
|
+
.split('\n')
|
|
86
|
+
.map((line) => line.trim())
|
|
87
|
+
.filter((line) => line.length > 0)
|
|
88
|
+
.map((line) => {
|
|
89
|
+
const match = /^(\d+)\s+(.*)$/.exec(line);
|
|
90
|
+
return match
|
|
91
|
+
? { pid: Number(match[1]), command: match[2] ?? '' }
|
|
92
|
+
: { pid: -1, command: line };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (result.exitCode === 1)
|
|
96
|
+
return []; // no matches
|
|
97
|
+
// pgrep unavailable/broken: fall back to a group liveness probe.
|
|
98
|
+
try {
|
|
99
|
+
process.kill(-pgid, 0);
|
|
100
|
+
return [{ pid: -1, command: 'unknown (pgrep unavailable; group still has live members)' }];
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function describeSurvivors(list) {
|
|
107
|
+
return list
|
|
108
|
+
.map((s) => {
|
|
109
|
+
const tag = MULTIPROCESSING_WORKER_PATTERN.test(s.command)
|
|
110
|
+
? ' [multiprocessing worker — the known busy-spin orphan shape]'
|
|
111
|
+
: '';
|
|
112
|
+
return `PID ${String(s.pid)}: ${s.command}${tag}`;
|
|
113
|
+
})
|
|
114
|
+
.join('; ');
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Post-run reaper for a process group FireForge itself spawned (`pgid` is
|
|
118
|
+
* the PID of a child started with `detached: true`): lists survivors,
|
|
119
|
+
* SIGTERMs the group, waits a short grace, escalates to SIGKILL, and warns
|
|
120
|
+
* about anything that still refuses to die. POSIX only (no-op on win32).
|
|
121
|
+
* The only kill target is `-pgid` — never anything outside the group.
|
|
122
|
+
* A healthy run costs exactly one `pgrep`.
|
|
123
|
+
*/
|
|
124
|
+
export async function sweepProcessGroup(pgid, graceMs = SWEEP_GRACE_MS) {
|
|
125
|
+
if (process.platform === 'win32')
|
|
126
|
+
return { survivors: [] };
|
|
127
|
+
const survivors = await listGroupSurvivors(pgid);
|
|
128
|
+
if (survivors.length === 0)
|
|
129
|
+
return { survivors };
|
|
130
|
+
warn(`Harness process group ${String(pgid)} left ${String(survivors.length)} surviving ` +
|
|
131
|
+
`process(es) after exit — reaping the group. ${describeSurvivors(survivors)}`);
|
|
132
|
+
try {
|
|
133
|
+
process.kill(-pgid, 'SIGTERM');
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { survivors };
|
|
137
|
+
}
|
|
138
|
+
await sweepDelay(graceMs);
|
|
139
|
+
let remaining = await listGroupSurvivors(pgid);
|
|
140
|
+
if (remaining.length > 0) {
|
|
141
|
+
try {
|
|
142
|
+
process.kill(-pgid, 'SIGKILL');
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return { survivors };
|
|
146
|
+
}
|
|
147
|
+
await sweepDelay(Math.min(200, graceMs));
|
|
148
|
+
remaining = await listGroupSurvivors(pgid);
|
|
149
|
+
if (remaining.length > 0) {
|
|
150
|
+
warn(`Process group ${String(pgid)} still has survivors after SIGKILL: ${describeSurvivors(remaining)}. ` +
|
|
151
|
+
'Inspect manually (ps -axo pid,ppid,time,command) and kill by PID.');
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
verbose(`Process group ${String(pgid)} reaped after SIGKILL escalation.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
verbose(`Process group ${String(pgid)} reaped cleanly with SIGTERM.`);
|
|
159
|
+
}
|
|
160
|
+
return { survivors };
|
|
161
|
+
}
|
|
@@ -8,6 +8,16 @@ export interface ExecResult {
|
|
|
8
8
|
stderr: string;
|
|
9
9
|
/** Process exit code */
|
|
10
10
|
exitCode: number;
|
|
11
|
+
/**
|
|
12
|
+
* True when `stdout` was cut off at the 50 MB collector cap. Optional so
|
|
13
|
+
* existing mocks stay valid; absent means "not truncated". Callers that
|
|
14
|
+
* feed the output into safety decisions (e.g. archive-listing scans) must
|
|
15
|
+
* treat `true` as a hard failure — a truncated listing looks exactly like
|
|
16
|
+
* a complete one otherwise.
|
|
17
|
+
*/
|
|
18
|
+
stdoutTruncated?: boolean;
|
|
19
|
+
/** True when `stderr` was cut off at the 50 MB collector cap. See {@link ExecResult.stdoutTruncated}. */
|
|
20
|
+
stderrTruncated?: boolean;
|
|
11
21
|
}
|
|
12
22
|
/**
|
|
13
23
|
* Options for command execution.
|
|
@@ -19,6 +29,21 @@ export interface ExecOptions {
|
|
|
19
29
|
env?: Record<string, string>;
|
|
20
30
|
/** Timeout in milliseconds */
|
|
21
31
|
timeout?: number;
|
|
32
|
+
/**
|
|
33
|
+
* POSIX: spawn the child as a process-group leader and route every kill
|
|
34
|
+
* (parent-signal forwarding, abort, escalation) to the whole GROUP, then
|
|
35
|
+
* sweep the group for survivors after close — so a harness that dies at
|
|
36
|
+
* startup cannot strand spinning `multiprocessing` workers (field
|
|
37
|
+
* incident: an orphaned Python spawn worker reparented to launchd and
|
|
38
|
+
* busy-spun at 100% CPU for ~26 days). Win32: tree-kill via
|
|
39
|
+
* `taskkill /T /F` on abort/signals only, no post-run sweep
|
|
40
|
+
* (best-effort). Default false — non-mach consumers are unaffected.
|
|
41
|
+
*
|
|
42
|
+
* NOTE for callers: a detached group leader does NOT receive terminal
|
|
43
|
+
* Ctrl+C; the exec layer installs its own group-aware signal forwarder
|
|
44
|
+
* whenever this option is set.
|
|
45
|
+
*/
|
|
46
|
+
processGroup?: boolean;
|
|
22
47
|
}
|
|
23
48
|
/**
|
|
24
49
|
* Executes a command and returns its output.
|
|
@@ -49,6 +74,13 @@ export interface StreamOptions extends ExecOptions {
|
|
|
49
74
|
* @returns Exit code of the process
|
|
50
75
|
*/
|
|
51
76
|
export declare function execStream(command: string, args: string[], options?: StreamOptions): Promise<number>;
|
|
77
|
+
/**
|
|
78
|
+
* Waits (bounded) for every tracked child process to close. Called by the
|
|
79
|
+
* bin signal handler after forwarding SIGINT/SIGTERM, so the parent stays
|
|
80
|
+
* alive long enough for the grace-then-SIGKILL escalation to actually run.
|
|
81
|
+
* Resolves immediately when no children are active.
|
|
82
|
+
*/
|
|
83
|
+
export declare function waitForActiveChildShutdown(timeoutMs: number): Promise<void>;
|
|
52
84
|
/**
|
|
53
85
|
* Executes a command and inherits stdio (shows output directly).
|
|
54
86
|
*
|
|
@@ -81,8 +113,16 @@ export declare function execInheritCapture(command: string, args: string[], opti
|
|
|
81
113
|
}): Promise<ExecResult>;
|
|
82
114
|
/** Per-line callback for smoke-run stream dispatch. */
|
|
83
115
|
export type SmokeLineCallback = (line: string) => void;
|
|
84
|
-
/**
|
|
85
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Options for {@link execSmokeRun}.
|
|
118
|
+
*
|
|
119
|
+
* Deliberately omits `ExecOptions.timeout`: the smoke run's only deadline is
|
|
120
|
+
* {@link SmokeRunOptions.smokeTimeoutMs}, which signals the whole process
|
|
121
|
+
* group. Inheriting `timeout` used to be a leaky trap — it was accepted by
|
|
122
|
+
* the type but silently ignored, so a caller setting it got no deadline at
|
|
123
|
+
* all.
|
|
124
|
+
*/
|
|
125
|
+
export interface SmokeRunOptions extends Omit<ExecOptions, 'timeout'> {
|
|
86
126
|
/**
|
|
87
127
|
* Hard deadline in milliseconds. When it elapses the child process
|
|
88
128
|
* group is sent SIGTERM and, after `killGraceMs`, SIGKILL. The returned
|
|
@@ -138,8 +178,12 @@ export interface SmokeRunResult extends ExecResult {
|
|
|
138
178
|
* that inherited the group.
|
|
139
179
|
*
|
|
140
180
|
* Windows fallback: `detached: true` does not create an equivalent group
|
|
141
|
-
* there, so we
|
|
142
|
-
*
|
|
181
|
+
* there, so we kill the descendant tree with `taskkill /pid <pid> /T /F`
|
|
182
|
+
* instead. taskkill has no graceful-signal equivalent — both the deadline
|
|
183
|
+
* and the grace re-invocation are forced kills, so Windows children get
|
|
184
|
+
* no shutdown window (best-effort; Windows is untested, see README).
|
|
185
|
+
* `child.kill()` is still sent as a direct-child fallback in case
|
|
186
|
+
* taskkill itself is unavailable.
|
|
143
187
|
*/
|
|
144
188
|
export declare function execSmokeRun(command: string, args: string[], options: SmokeRunOptions): Promise<SmokeRunResult>;
|
|
145
189
|
/**
|