oh-my-codex 0.21.0 → 0.21.2
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/Cargo.toml +1 -1
- package/README.md +4 -2
- package/dist/cli/__tests__/detached-launch-diagnostics-3578.test.d.ts +2 -0
- package/dist/cli/__tests__/detached-launch-diagnostics-3578.test.d.ts.map +1 -0
- package/dist/cli/__tests__/detached-launch-diagnostics-3578.test.js +592 -0
- package/dist/cli/__tests__/detached-launch-diagnostics-3578.test.js.map +1 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js +270 -33
- package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +133 -5
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +543 -2
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/issue-windows-cancel-identity.test.d.ts +2 -0
- package/dist/cli/__tests__/issue-windows-cancel-identity.test.d.ts.map +1 -0
- package/dist/cli/__tests__/issue-windows-cancel-identity.test.js +17 -0
- package/dist/cli/__tests__/issue-windows-cancel-identity.test.js.map +1 -0
- package/dist/cli/__tests__/launch-fallback.test.js +15 -3
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +20 -0
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/plugin-cache-stale-lock.test.d.ts +2 -0
- package/dist/cli/__tests__/plugin-cache-stale-lock.test.d.ts.map +1 -0
- package/dist/cli/__tests__/plugin-cache-stale-lock.test.js +76 -0
- package/dist/cli/__tests__/plugin-cache-stale-lock.test.js.map +1 -0
- package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.d.ts +2 -0
- package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.d.ts.map +1 -0
- package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.js +1662 -0
- package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.js.map +1 -0
- package/dist/cli/__tests__/plugin-launcher-provenance-3558.test.d.ts +2 -0
- package/dist/cli/__tests__/plugin-launcher-provenance-3558.test.d.ts.map +1 -0
- package/dist/cli/__tests__/plugin-launcher-provenance-3558.test.js +522 -0
- package/dist/cli/__tests__/plugin-launcher-provenance-3558.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan-advisory.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-advisory.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-advisory.test.js +301 -0
- package/dist/cli/__tests__/ralplan-advisory.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +39 -0
- package/dist/cli/__tests__/ralplan.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +81 -6
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +423 -4
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +60 -3
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +9 -0
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +4 -0
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +71 -0
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +30 -4
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +57 -8
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +113 -7
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +1850 -185
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/native-assets.d.ts +2 -1
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +7 -7
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +89 -4
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +2135 -118
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/ralplan.d.ts +2 -1
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +149 -9
- package/dist/cli/ralplan.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +7 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +9 -10
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/update-worker.js +9 -0
- package/dist/cli/update-worker.js.map +1 -1
- package/dist/cli/update.d.ts +1 -0
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +9 -0
- package/dist/cli/update.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +118 -0
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +74 -4
- package/dist/config/generator.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +74 -0
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/ralplan-advisory-keyword.test.d.ts +2 -0
- package/dist/hooks/__tests__/ralplan-advisory-keyword.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/ralplan-advisory-keyword.test.js +250 -0
- package/dist/hooks/__tests__/ralplan-advisory-keyword.test.js.map +1 -0
- package/dist/hooks/__tests__/session.test.js +10 -1
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +8 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +130 -9
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/session.d.ts +5 -0
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +17 -3
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +16 -1
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +134 -7
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/render.test.js +57 -0
- package/dist/hud/__tests__/render.test.js.map +1 -1
- package/dist/hud/__tests__/session-attached.test.d.ts +2 -0
- package/dist/hud/__tests__/session-attached.test.d.ts.map +1 -0
- package/dist/hud/__tests__/session-attached.test.js +82 -0
- package/dist/hud/__tests__/session-attached.test.js.map +1 -0
- package/dist/hud/__tests__/state.test.js +94 -2
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux-split-realtmux.test.js +169 -2
- package/dist/hud/__tests__/tmux-split-realtmux.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +104 -22
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/__tests__/types.test.js +9 -0
- package/dist/hud/__tests__/types.test.js.map +1 -1
- package/dist/hud/__tests__/watch-detached.test.d.ts +2 -0
- package/dist/hud/__tests__/watch-detached.test.d.ts.map +1 -0
- package/dist/hud/__tests__/watch-detached.test.js +383 -0
- package/dist/hud/__tests__/watch-detached.test.js.map +1 -0
- package/dist/hud/__tests__/watch-realtmux.test.d.ts +2 -0
- package/dist/hud/__tests__/watch-realtmux.test.d.ts.map +1 -0
- package/dist/hud/__tests__/watch-realtmux.test.js +322 -0
- package/dist/hud/__tests__/watch-realtmux.test.js.map +1 -0
- package/dist/hud/index.d.ts +3 -0
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +98 -20
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.d.ts.map +1 -1
- package/dist/hud/reconcile.js +68 -24
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.d.ts.map +1 -1
- package/dist/hud/render.js +20 -0
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/session-attached.d.ts +15 -0
- package/dist/hud/session-attached.d.ts.map +1 -0
- package/dist/hud/session-attached.js +47 -0
- package/dist/hud/session-attached.js.map +1 -0
- package/dist/hud/state.d.ts +9 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +140 -3
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.d.ts +4 -0
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +86 -23
- package/dist/hud/tmux.js.map +1 -1
- package/dist/hud/types.d.ts +20 -0
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/hud/types.js +3 -0
- package/dist/hud/types.js.map +1 -1
- package/dist/modes/base.d.ts +15 -2
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +105 -16
- package/dist/modes/base.js.map +1 -1
- package/dist/question/autopilot-wait.js +2 -2
- package/dist/question/autopilot-wait.js.map +1 -1
- package/dist/question/deep-interview.js +2 -2
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/ralplan/__tests__/advisory-activation.test.d.ts +2 -0
- package/dist/ralplan/__tests__/advisory-activation.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/advisory-activation.test.js +700 -0
- package/dist/ralplan/__tests__/advisory-activation.test.js.map +1 -0
- package/dist/ralplan/__tests__/advisory-detached-transition.test.d.ts +2 -0
- package/dist/ralplan/__tests__/advisory-detached-transition.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/advisory-detached-transition.test.js +88 -0
- package/dist/ralplan/__tests__/advisory-detached-transition.test.js.map +1 -0
- package/dist/ralplan/__tests__/advisory.test.d.ts +2 -0
- package/dist/ralplan/__tests__/advisory.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/advisory.test.js +1087 -0
- package/dist/ralplan/__tests__/advisory.test.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +416 -7
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/advisory-activation-verifier.d.ts +19 -0
- package/dist/ralplan/advisory-activation-verifier.d.ts.map +1 -0
- package/dist/ralplan/advisory-activation-verifier.js +77 -0
- package/dist/ralplan/advisory-activation-verifier.js.map +1 -0
- package/dist/ralplan/advisory-activation.d.ts +34 -0
- package/dist/ralplan/advisory-activation.d.ts.map +1 -0
- package/dist/ralplan/advisory-activation.js +205 -0
- package/dist/ralplan/advisory-activation.js.map +1 -0
- package/dist/ralplan/advisory-contract.d.ts +103 -0
- package/dist/ralplan/advisory-contract.d.ts.map +1 -0
- package/dist/ralplan/advisory-contract.js +83 -0
- package/dist/ralplan/advisory-contract.js.map +1 -0
- package/dist/ralplan/advisory-detached-transition.d.ts +11 -0
- package/dist/ralplan/advisory-detached-transition.d.ts.map +1 -0
- package/dist/ralplan/advisory-detached-transition.js +21 -0
- package/dist/ralplan/advisory-detached-transition.js.map +1 -0
- package/dist/ralplan/advisory-evidence.d.ts +91 -0
- package/dist/ralplan/advisory-evidence.d.ts.map +1 -0
- package/dist/ralplan/advisory-evidence.js +393 -0
- package/dist/ralplan/advisory-evidence.js.map +1 -0
- package/dist/ralplan/advisory-lifecycle-validation.d.ts +11 -0
- package/dist/ralplan/advisory-lifecycle-validation.d.ts.map +1 -0
- package/dist/ralplan/advisory-lifecycle-validation.js +72 -0
- package/dist/ralplan/advisory-lifecycle-validation.js.map +1 -0
- package/dist/ralplan/advisory-postconditions.d.ts +3 -0
- package/dist/ralplan/advisory-postconditions.d.ts.map +1 -0
- package/dist/ralplan/advisory-postconditions.js +33 -0
- package/dist/ralplan/advisory-postconditions.js.map +1 -0
- package/dist/ralplan/advisory-recovery-journal.d.ts +56 -0
- package/dist/ralplan/advisory-recovery-journal.d.ts.map +1 -0
- package/dist/ralplan/advisory-recovery-journal.js +136 -0
- package/dist/ralplan/advisory-recovery-journal.js.map +1 -0
- package/dist/ralplan/advisory-storage.d.ts +24 -0
- package/dist/ralplan/advisory-storage.d.ts.map +1 -0
- package/dist/ralplan/advisory-storage.js +106 -0
- package/dist/ralplan/advisory-storage.js.map +1 -0
- package/dist/ralplan/advisory.d.ts +99 -0
- package/dist/ralplan/advisory.d.ts.map +1 -0
- package/dist/ralplan/advisory.js +914 -0
- package/dist/ralplan/advisory.js.map +1 -0
- package/dist/ralplan/documented-leader-preflight.d.ts +8 -0
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -1
- package/dist/ralplan/documented-leader-preflight.js +60 -2
- package/dist/ralplan/documented-leader-preflight.js.map +1 -1
- package/dist/ralplan/runtime-advisory-lifecycle.d.ts +74 -0
- package/dist/ralplan/runtime-advisory-lifecycle.d.ts.map +1 -0
- package/dist/ralplan/runtime-advisory-lifecycle.js +352 -0
- package/dist/ralplan/runtime-advisory-lifecycle.js.map +1 -0
- package/dist/ralplan/runtime-contract.d.ts +126 -0
- package/dist/ralplan/runtime-contract.d.ts.map +1 -0
- package/dist/ralplan/runtime-contract.js +2 -0
- package/dist/ralplan/runtime-contract.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +6 -109
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +174 -56
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/scripts/__tests__/postinstall.test.js +26 -0
- package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
- package/dist/scripts/__tests__/ralplan-advisory-hook.test.d.ts +2 -0
- package/dist/scripts/__tests__/ralplan-advisory-hook.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/ralplan-advisory-hook.test.js +299 -0
- package/dist/scripts/__tests__/ralplan-advisory-hook.test.js.map +1 -0
- package/dist/scripts/__tests__/smoke-packed-install.test.js +102 -8
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts +20 -0
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +226 -15
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/postinstall.d.ts +7 -0
- package/dist/scripts/postinstall.d.ts.map +1 -1
- package/dist/scripts/postinstall.js +40 -0
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +21 -5
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +157 -57
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/state/__tests__/mode-binding-lease.test.d.ts +2 -0
- package/dist/state/__tests__/mode-binding-lease.test.d.ts.map +1 -0
- package/dist/state/__tests__/mode-binding-lease.test.js +498 -0
- package/dist/state/__tests__/mode-binding-lease.test.js.map +1 -0
- package/dist/state/__tests__/operations.test.js +1 -0
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/pinned-atomic-file.test.d.ts +2 -0
- package/dist/state/__tests__/pinned-atomic-file.test.d.ts.map +1 -0
- package/dist/state/__tests__/pinned-atomic-file.test.js +438 -0
- package/dist/state/__tests__/pinned-atomic-file.test.js.map +1 -0
- package/dist/state/__tests__/skill-active-exact-session-transaction.test.d.ts +2 -0
- package/dist/state/__tests__/skill-active-exact-session-transaction.test.d.ts.map +1 -0
- package/dist/state/__tests__/skill-active-exact-session-transaction.test.js +179 -0
- package/dist/state/__tests__/skill-active-exact-session-transaction.test.js.map +1 -0
- package/dist/state/__tests__/skill-active.test.js +29 -1
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/mode-binding-lease-helper.d.ts +2 -0
- package/dist/state/mode-binding-lease-helper.d.ts.map +1 -0
- package/dist/state/mode-binding-lease-helper.js +623 -0
- package/dist/state/mode-binding-lease-helper.js.map +1 -0
- package/dist/state/mode-binding-lease.d.ts +25 -0
- package/dist/state/mode-binding-lease.d.ts.map +1 -0
- package/dist/state/mode-binding-lease.js +380 -0
- package/dist/state/mode-binding-lease.js.map +1 -0
- package/dist/state/operations.d.ts +29 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +203 -122
- package/dist/state/operations.js.map +1 -1
- package/dist/state/pinned-atomic-file-darwin-client.d.ts +34 -0
- package/dist/state/pinned-atomic-file-darwin-client.d.ts.map +1 -0
- package/dist/state/pinned-atomic-file-darwin-client.js +174 -0
- package/dist/state/pinned-atomic-file-darwin-client.js.map +1 -0
- package/dist/state/pinned-atomic-file-darwin-helper.d.ts +2 -0
- package/dist/state/pinned-atomic-file-darwin-helper.d.ts.map +1 -0
- package/dist/state/pinned-atomic-file-darwin-helper.js +275 -0
- package/dist/state/pinned-atomic-file-darwin-helper.js.map +1 -0
- package/dist/state/pinned-atomic-file-darwin-janitor.d.ts +2 -0
- package/dist/state/pinned-atomic-file-darwin-janitor.d.ts.map +1 -0
- package/dist/state/pinned-atomic-file-darwin-janitor.js +272 -0
- package/dist/state/pinned-atomic-file-darwin-janitor.js.map +1 -0
- package/dist/state/pinned-atomic-file.d.ts +29 -0
- package/dist/state/pinned-atomic-file.d.ts.map +1 -0
- package/dist/state/pinned-atomic-file.js +536 -0
- package/dist/state/pinned-atomic-file.js.map +1 -0
- package/dist/state/process-identity.d.ts +30 -0
- package/dist/state/process-identity.d.ts.map +1 -0
- package/dist/state/process-identity.js +99 -0
- package/dist/state/process-identity.js.map +1 -0
- package/dist/state/skill-active.d.ts +18 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +82 -5
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +1 -1
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +109 -0
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +10 -1
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.js +20 -0
- package/dist/utils/__tests__/file-durability.test.js.map +1 -1
- package/dist/utils/file-durability.d.ts +1 -1
- package/dist/utils/file-durability.d.ts.map +1 -1
- package/dist/utils/file-durability.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +12 -4
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/ci-trusted-publish.test.d.ts +2 -0
- package/dist/verification/__tests__/ci-trusted-publish.test.d.ts.map +1 -0
- package/dist/verification/__tests__/ci-trusted-publish.test.js +205 -0
- package/dist/verification/__tests__/ci-trusted-publish.test.js.map +1 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +10 -5
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/publish-npm-manual-retired.test.d.ts +2 -0
- package/dist/verification/__tests__/publish-npm-manual-retired.test.d.ts.map +1 -0
- package/dist/verification/__tests__/publish-npm-manual-retired.test.js +22 -0
- package/dist/verification/__tests__/publish-npm-manual-retired.test.js.map +1 -0
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/skills/doctor/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +10 -1
- package/plugins/oh-my-codex/skills/team/SKILL.md +2 -0
- package/skills/doctor/SKILL.md +1 -1
- package/skills/omx-setup/SKILL.md +1 -1
- package/skills/ralplan/SKILL.md +10 -1
- package/skills/team/SKILL.md +2 -0
- package/src/scripts/__tests__/postinstall.test.ts +28 -0
- package/src/scripts/__tests__/ralplan-advisory-hook.test.ts +320 -0
- package/src/scripts/__tests__/smoke-packed-install.test.ts +120 -7
- package/src/scripts/codex-native-hook.ts +220 -10
- package/src/scripts/postinstall.ts +46 -0
- package/src/scripts/smoke-packed-install.ts +175 -70
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { constants as fsConstants } from 'node:fs';
|
|
3
|
+
import { lstat, mkdir, open, readdir, rename, stat, unlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { basename } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
import { formatProcessOwnerToken, hashHistoricalProcessStartIdentity, hashProcessStartIdentity, parseProcessOwnerToken, readHistoricalProcessStartIdentity, readProcessStartIdentity, } from './process-identity.js';
|
|
7
|
+
const INITIAL_RETRY_MS = 20;
|
|
8
|
+
const MAX_RETRY_MS = 500;
|
|
9
|
+
const PARTIAL_OWNER_STALE_MS = 5_000;
|
|
10
|
+
const BOOTSTRAP_OWNER_TOKEN = `99999999-1-${'0'.repeat(24)}`;
|
|
11
|
+
function respond(value) { process.stdout.write(`${JSON.stringify(value)}\n`); }
|
|
12
|
+
function sameIdentity(value, expected) {
|
|
13
|
+
return value.dev === expected.dev && value.ino === expected.ino;
|
|
14
|
+
}
|
|
15
|
+
function validOwnerToken(value) {
|
|
16
|
+
return parseProcessOwnerToken(value) !== null;
|
|
17
|
+
}
|
|
18
|
+
async function readPinnedRecord(name, token) {
|
|
19
|
+
if (!validOwnerToken(token) || basename(name) !== name)
|
|
20
|
+
return { kind: 'invalid' };
|
|
21
|
+
let handle = null;
|
|
22
|
+
try {
|
|
23
|
+
handle = await open(name, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
24
|
+
const before = await handle.stat();
|
|
25
|
+
const visible = await lstat(name);
|
|
26
|
+
if (!before.isFile() || before.nlink !== 1 || !visible.isFile() || visible.isSymbolicLink()
|
|
27
|
+
|| visible.nlink !== 1 || !sameIdentity(before, visible))
|
|
28
|
+
return { kind: 'changed' };
|
|
29
|
+
const value = await handle.readFile('utf8');
|
|
30
|
+
const after = await handle.stat();
|
|
31
|
+
if (!sameIdentity(before, after) || before.size !== after.size
|
|
32
|
+
|| before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
|
|
33
|
+
return { kind: 'changed' };
|
|
34
|
+
const owner = {
|
|
35
|
+
name,
|
|
36
|
+
token,
|
|
37
|
+
identity: {
|
|
38
|
+
dev: before.dev,
|
|
39
|
+
ino: before.ino,
|
|
40
|
+
size: before.size,
|
|
41
|
+
mtimeMs: before.mtimeMs,
|
|
42
|
+
ctimeMs: before.ctimeMs,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
if (value === token)
|
|
46
|
+
return { kind: 'valid', owner };
|
|
47
|
+
const publishedPrefix = value.length < token.length && token.startsWith(value);
|
|
48
|
+
return publishedPrefix ? { kind: 'partial', owner } : { kind: 'invalid' };
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === 'ENOENT')
|
|
52
|
+
return { kind: 'missing' };
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
await handle?.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function readPinnedOwner() {
|
|
60
|
+
const entries = await readdir('.');
|
|
61
|
+
if (entries.length === 0)
|
|
62
|
+
return { kind: 'ownerless' };
|
|
63
|
+
const ownerNames = entries.filter((entry) => entry.startsWith('owner-'));
|
|
64
|
+
const displacedNames = entries.filter((entry) => entry.startsWith('.owner-reclaim-'));
|
|
65
|
+
const candidateNames = entries.filter((entry) => entry.startsWith('.owner-publish-'));
|
|
66
|
+
const bootstrapName = `owner-${BOOTSTRAP_OWNER_TOKEN}`;
|
|
67
|
+
if (ownerNames.length === 2 && ownerNames.includes(bootstrapName)
|
|
68
|
+
&& displacedNames.length === 0 && candidateNames.length === 0)
|
|
69
|
+
return { kind: 'unstable' };
|
|
70
|
+
if (ownerNames.length > 1 || displacedNames.length > 1 || candidateNames.length > 1
|
|
71
|
+
|| ownerNames.length + displacedNames.length + candidateNames.length !== entries.length)
|
|
72
|
+
return { kind: 'ambiguous', reason: `entries:${entries.join(',')}` };
|
|
73
|
+
const owner = ownerNames[0]
|
|
74
|
+
? await readPinnedRecord(ownerNames[0], ownerNames[0].slice('owner-'.length))
|
|
75
|
+
: undefined;
|
|
76
|
+
const displacedName = displacedNames[0] ? parseDisplacedName(displacedNames[0]) : null;
|
|
77
|
+
const displacedRecord = displacedName
|
|
78
|
+
? await readPinnedRecord(displacedNames[0], displacedName.originalToken)
|
|
79
|
+
: undefined;
|
|
80
|
+
const displaced = displacedRecord && (displacedRecord.kind === 'valid' || displacedRecord.kind === 'partial')
|
|
81
|
+
? { ...displacedRecord, owner: { ...displacedRecord.owner, claimantToken: displacedName?.claimantToken } }
|
|
82
|
+
: displacedRecord;
|
|
83
|
+
const candidate = candidateNames[0]
|
|
84
|
+
? await readPinnedRecord(candidateNames[0], candidateNames[0].slice('.owner-publish-'.length))
|
|
85
|
+
: undefined;
|
|
86
|
+
if (owner?.kind === 'missing' || owner?.kind === 'changed'
|
|
87
|
+
|| displaced?.kind === 'missing' || displaced?.kind === 'changed'
|
|
88
|
+
|| candidate?.kind === 'missing' || candidate?.kind === 'changed')
|
|
89
|
+
return { kind: 'unstable' };
|
|
90
|
+
if (owner?.kind === 'invalid' || displaced?.kind === 'invalid' || candidate?.kind === 'invalid')
|
|
91
|
+
return { kind: 'ambiguous', reason: 'invalid-record' };
|
|
92
|
+
const stableOwner = presentRecord(owner) ? owner : undefined;
|
|
93
|
+
const stableDisplaced = presentRecord(displaced) ? displaced : undefined;
|
|
94
|
+
const stableCandidate = presentRecord(candidate) ? candidate : undefined;
|
|
95
|
+
if (stableCandidate && stableOwner)
|
|
96
|
+
return { kind: 'unstable' };
|
|
97
|
+
const isDeadRecord = async (record) => record.kind === 'valid' && await tokenIsStaleAndDead(record.owner.token)
|
|
98
|
+
|| record.kind === 'partial' && isOld(record.owner) && await tokenIsStaleAndDead(record.owner.token);
|
|
99
|
+
const isBoundOrDead = async (record) => record.owner.claimantToken !== undefined
|
|
100
|
+
|| await isDeadRecord(record);
|
|
101
|
+
if (stableCandidate && stableDisplaced && !stableOwner) {
|
|
102
|
+
if (!await isBoundOrDead(stableDisplaced))
|
|
103
|
+
return { kind: 'unstable' };
|
|
104
|
+
if (stableDisplaced.owner.claimantToken !== stableCandidate.owner.token)
|
|
105
|
+
return { kind: 'ambiguous', reason: 'candidate-claimant-mismatch' };
|
|
106
|
+
return {
|
|
107
|
+
kind: 'preparing', candidate: stableCandidate.owner, candidateComplete: stableCandidate.kind === 'valid', displaced: stableDisplaced.owner,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (stableOwner && stableDisplaced) {
|
|
111
|
+
if (!await isBoundOrDead(stableDisplaced))
|
|
112
|
+
return { kind: 'unstable' };
|
|
113
|
+
if (stableDisplaced.owner.claimantToken !== undefined && stableDisplaced.owner.claimantToken !== stableOwner.owner.token
|
|
114
|
+
|| stableOwner.owner.identity.dev === stableDisplaced.owner.identity.dev
|
|
115
|
+
&& stableOwner.owner.identity.ino === stableDisplaced.owner.identity.ino)
|
|
116
|
+
return { kind: 'ambiguous', reason: 'published-binding-mismatch' };
|
|
117
|
+
if (stableOwner.kind === 'valid')
|
|
118
|
+
return { kind: 'published', owner: stableOwner.owner, displaced: stableDisplaced.owner };
|
|
119
|
+
return isOld(stableOwner.owner) && await tokenIsStaleAndDead(stableOwner.owner.token)
|
|
120
|
+
? { kind: 'partial-published', owner: stableOwner.owner, displaced: stableDisplaced.owner }
|
|
121
|
+
: { kind: 'unstable' };
|
|
122
|
+
}
|
|
123
|
+
if (stableDisplaced)
|
|
124
|
+
return await isBoundOrDead(stableDisplaced)
|
|
125
|
+
? { kind: 'interrupted', displaced: stableDisplaced.owner }
|
|
126
|
+
: { kind: 'unstable' };
|
|
127
|
+
if (stableCandidate)
|
|
128
|
+
return { kind: 'unstable' };
|
|
129
|
+
if (stableOwner?.kind === 'valid')
|
|
130
|
+
return { kind: 'valid', owner: stableOwner.owner };
|
|
131
|
+
if (stableOwner?.kind === 'partial' && isOld(stableOwner.owner) && await tokenIsStaleAndDead(stableOwner.owner.token)) {
|
|
132
|
+
return { kind: 'recoverable', owner: stableOwner.owner };
|
|
133
|
+
}
|
|
134
|
+
return stableOwner ? { kind: 'unstable' } : { kind: 'ownerless' };
|
|
135
|
+
}
|
|
136
|
+
function presentRecord(record) {
|
|
137
|
+
return record?.kind === 'valid' || record?.kind === 'partial';
|
|
138
|
+
}
|
|
139
|
+
function parseDisplacedName(name) {
|
|
140
|
+
const encoded = name.slice('.owner-reclaim-'.length);
|
|
141
|
+
const separator = encoded.indexOf('~');
|
|
142
|
+
const originalToken = separator < 0 ? encoded : encoded.slice(0, separator);
|
|
143
|
+
const claimantToken = separator < 0 ? undefined : encoded.slice(separator + 1);
|
|
144
|
+
if (!validOwnerToken(originalToken) || claimantToken !== undefined && !validOwnerToken(claimantToken))
|
|
145
|
+
return null;
|
|
146
|
+
return { originalToken, ...(claimantToken ? { claimantToken } : {}) };
|
|
147
|
+
}
|
|
148
|
+
function isOld(owner) {
|
|
149
|
+
return Date.now() - owner.identity.mtimeMs >= PARTIAL_OWNER_STALE_MS;
|
|
150
|
+
}
|
|
151
|
+
function sameOwner(a, b) {
|
|
152
|
+
return a.name === b.name && a.token === b.token && a.claimantToken === b.claimantToken
|
|
153
|
+
&& a.identity.dev === b.identity.dev && a.identity.ino === b.identity.ino
|
|
154
|
+
&& a.identity.size === b.identity.size && a.identity.mtimeMs === b.identity.mtimeMs
|
|
155
|
+
&& a.identity.ctimeMs === b.identity.ctimeMs;
|
|
156
|
+
}
|
|
157
|
+
async function replaceObservedOwner(observed, replacementToken) {
|
|
158
|
+
const quarantine = `.owner-reclaim-${observed.token}~${replacementToken}`;
|
|
159
|
+
try {
|
|
160
|
+
await rename(observed.name, quarantine);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error.code === 'ENOENT')
|
|
164
|
+
return false;
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
await maybeCrashReclaimPhase('after-quarantine');
|
|
168
|
+
const moved = await lstat(quarantine);
|
|
169
|
+
if (!moved.isFile() || moved.isSymbolicLink() || moved.nlink !== 1
|
|
170
|
+
|| moved.dev !== observed.identity.dev || moved.ino !== observed.identity.ino
|
|
171
|
+
|| moved.size !== observed.identity.size || moved.mtimeMs !== observed.identity.mtimeMs) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
await publishSuccessor(replacementToken);
|
|
175
|
+
const published = await readPinnedOwner();
|
|
176
|
+
if (published.kind !== 'published' || published.owner.token !== replacementToken
|
|
177
|
+
|| published.displaced.name !== quarantine
|
|
178
|
+
|| published.displaced.identity.dev !== observed.identity.dev
|
|
179
|
+
|| published.displaced.identity.ino !== observed.identity.ino
|
|
180
|
+
|| published.displaced.identity.size !== observed.identity.size
|
|
181
|
+
|| published.displaced.identity.mtimeMs !== observed.identity.mtimeMs) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
await maybeCrashReclaimPhase('before-cleanup');
|
|
185
|
+
await unlink(quarantine);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
async function publishSuccessor(replacementToken) {
|
|
189
|
+
const candidate = `.owner-publish-${replacementToken}`;
|
|
190
|
+
await writeFile(candidate, replacementToken, { flag: 'wx', mode: 0o600 });
|
|
191
|
+
const staged = await readPinnedRecord(candidate, replacementToken);
|
|
192
|
+
if (staged?.kind !== 'valid')
|
|
193
|
+
throw new Error('canonical state lock successor staging changed');
|
|
194
|
+
await rename(candidate, `owner-${replacementToken}`);
|
|
195
|
+
await maybeCrashReclaimPhase('after-successor-publish');
|
|
196
|
+
}
|
|
197
|
+
async function restoreDisplacedOwner(displaced) {
|
|
198
|
+
try {
|
|
199
|
+
await rename(displaced.name, `owner-${displaced.token}`);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error.code === 'ENOENT')
|
|
204
|
+
return false;
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function maybeCrashReclaimPhase(phase) {
|
|
209
|
+
if (process.env.OMX_TEST_MODE_BINDING_RECLAIM_CRASH_PHASE !== phase)
|
|
210
|
+
return;
|
|
211
|
+
const sentinel = process.env.OMX_TEST_MODE_BINDING_RECLAIM_CRASH_SENTINEL;
|
|
212
|
+
if (!sentinel)
|
|
213
|
+
return;
|
|
214
|
+
let handle = null;
|
|
215
|
+
try {
|
|
216
|
+
handle = await open(sentinel, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW, 0o600);
|
|
217
|
+
await handle.writeFile(phase);
|
|
218
|
+
await handle.sync();
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (error.code === 'EEXIST')
|
|
222
|
+
return;
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
await handle?.close();
|
|
227
|
+
}
|
|
228
|
+
process.kill(process.pid, 'SIGKILL');
|
|
229
|
+
}
|
|
230
|
+
async function ownerIsDead(value) {
|
|
231
|
+
const parsed = parseProcessOwnerToken(value);
|
|
232
|
+
if (!parsed)
|
|
233
|
+
return false;
|
|
234
|
+
try {
|
|
235
|
+
process.kill(parsed.pid, 0);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
return error.code === 'ESRCH';
|
|
239
|
+
}
|
|
240
|
+
if (parsed.version === 'v2' && parsed.processStartHash === 'unknown'
|
|
241
|
+
|| parsed.version === 'v3' && parsed.processStartHash === 'unavailable')
|
|
242
|
+
return false;
|
|
243
|
+
if (parsed.version === 'v2' || parsed.version === 'v3') {
|
|
244
|
+
const reader = parsed.version === 'v2' ? readHistoricalProcessStartIdentity : readProcessStartIdentity;
|
|
245
|
+
const hash = parsed.version === 'v2' ? hashHistoricalProcessStartIdentity : hashProcessStartIdentity;
|
|
246
|
+
const current = await reader(parsed.pid);
|
|
247
|
+
if (current === null || hash(current) === parsed.processStartHash)
|
|
248
|
+
return false;
|
|
249
|
+
// A live PID identity mismatch is security-sensitive: re-sample before
|
|
250
|
+
// reclaim so a transient platform probe cannot evict the real owner.
|
|
251
|
+
await new Promise((resolve) => setTimeout(resolve, INITIAL_RETRY_MS));
|
|
252
|
+
const confirmed = await reader(parsed.pid);
|
|
253
|
+
return confirmed !== null
|
|
254
|
+
&& hash(confirmed) === hash(current)
|
|
255
|
+
&& hash(confirmed) !== parsed.processStartHash;
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
async function tokenIsStaleAndDead(value) {
|
|
260
|
+
const parsed = parseProcessOwnerToken(value);
|
|
261
|
+
return parsed !== null && Date.now() - parsed.issuedAtMs >= PARTIAL_OWNER_STALE_MS && await ownerIsDead(value);
|
|
262
|
+
}
|
|
263
|
+
async function runPinnedClaimMode(args) {
|
|
264
|
+
const [lockName = '', replacementToken = '', nsDev = '', nsIno = '', lockDev = '', lockIno = ''] = args;
|
|
265
|
+
if (!lockName || basename(lockName) !== lockName || !replacementToken || !nsDev || !nsIno || !lockDev || !lockIno) {
|
|
266
|
+
throw new Error('invalid pinned canonical state lock claim arguments');
|
|
267
|
+
}
|
|
268
|
+
const namespaceIdentity = { dev: Number(nsDev), ino: Number(nsIno) };
|
|
269
|
+
const lockIdentity = { dev: Number(lockDev), ino: Number(lockIno) };
|
|
270
|
+
const current = await stat('.');
|
|
271
|
+
const parent = await stat('..');
|
|
272
|
+
if (!current.isDirectory() || !parent.isDirectory()
|
|
273
|
+
|| !sameIdentity(current, lockIdentity) || !sameIdentity(parent, namespaceIdentity)) {
|
|
274
|
+
respond({ claimed: false });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const bootstrapName = `owner-${BOOTSTRAP_OWNER_TOKEN}`;
|
|
278
|
+
const initialEntries = await readdir('.');
|
|
279
|
+
if (initialEntries.includes(bootstrapName) && initialEntries.length > 1) {
|
|
280
|
+
const bootstrap = await readPinnedRecord(bootstrapName, BOOTSTRAP_OWNER_TOKEN);
|
|
281
|
+
if (bootstrap.kind === 'valid') {
|
|
282
|
+
const confirmedEntries = await readdir('.');
|
|
283
|
+
const confirmed = await readPinnedRecord(bootstrapName, BOOTSTRAP_OWNER_TOKEN);
|
|
284
|
+
if (confirmed.kind === 'valid' && sameOwner(bootstrap.owner, confirmed.owner)
|
|
285
|
+
&& confirmedEntries.includes(bootstrapName) && confirmedEntries.length > 1) {
|
|
286
|
+
await unlink(bootstrapName);
|
|
287
|
+
respond({ claimed: false });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const observed = await readPinnedOwner();
|
|
293
|
+
if (observed.kind === 'unstable') {
|
|
294
|
+
respond({ claimed: false });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (observed.kind === 'ambiguous') {
|
|
298
|
+
await new Promise((resolve) => setTimeout(resolve, INITIAL_RETRY_MS));
|
|
299
|
+
if ((await readPinnedOwner()).kind !== 'ambiguous') {
|
|
300
|
+
respond({ claimed: false });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
throw new Error(`canonical state lock owner ambiguous:${observed.reason}`);
|
|
304
|
+
}
|
|
305
|
+
if (observed.kind === 'valid') {
|
|
306
|
+
if (observed.owner.token === replacementToken) {
|
|
307
|
+
respond({ claimed: true });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!await tokenIsStaleAndDead(observed.owner.token)) {
|
|
311
|
+
respond({ claimed: false });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (observed.kind === 'published' && observed.owner.token !== replacementToken
|
|
316
|
+
&& !await tokenIsStaleAndDead(observed.owner.token)) {
|
|
317
|
+
respond({ claimed: false });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (observed.kind === 'preparing' && observed.candidate.token !== replacementToken
|
|
321
|
+
&& !await tokenIsStaleAndDead(observed.candidate.token)) {
|
|
322
|
+
respond({ claimed: false });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (observed.kind === 'interrupted' && observed.displaced.claimantToken
|
|
326
|
+
&& observed.displaced.claimantToken !== replacementToken
|
|
327
|
+
&& !await tokenIsStaleAndDead(observed.displaced.claimantToken)) {
|
|
328
|
+
respond({ claimed: false });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const confirmed = await readPinnedOwner();
|
|
332
|
+
const changed = !sameOwnerState(confirmed, observed);
|
|
333
|
+
if (changed || !sameIdentity(await stat('.'), lockIdentity) || !sameIdentity(await stat('..'), namespaceIdentity)) {
|
|
334
|
+
respond({ claimed: false });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (observed.kind === 'preparing') {
|
|
338
|
+
if (observed.candidate.token === replacementToken) {
|
|
339
|
+
if (!observed.candidateComplete)
|
|
340
|
+
await unlink(observed.candidate.name);
|
|
341
|
+
if (observed.candidateComplete) {
|
|
342
|
+
await rename(observed.candidate.name, `owner-${replacementToken}`);
|
|
343
|
+
await maybeCrashReclaimPhase('after-successor-publish');
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
await publishSuccessor(replacementToken);
|
|
347
|
+
}
|
|
348
|
+
await maybeCrashReclaimPhase('before-cleanup');
|
|
349
|
+
await unlink(observed.displaced.name);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
await unlink(observed.candidate.name);
|
|
353
|
+
await restoreDisplacedOwner(observed.displaced);
|
|
354
|
+
respond({ claimed: false });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else if (observed.kind === 'partial-published') {
|
|
359
|
+
await unlink(observed.owner.name);
|
|
360
|
+
await restoreDisplacedOwner(observed.displaced);
|
|
361
|
+
respond({ claimed: false });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
else if (observed.kind === 'published') {
|
|
365
|
+
if (observed.owner.token === replacementToken) {
|
|
366
|
+
await maybeCrashReclaimPhase('before-cleanup');
|
|
367
|
+
await unlink(observed.displaced.name);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
// Both recorded owners are dead. Collapse to the newer canonical owner;
|
|
371
|
+
// the next retry can reclaim that single inode through the normal path.
|
|
372
|
+
await unlink(observed.displaced.name);
|
|
373
|
+
respond({ claimed: false });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else if (observed.kind === 'interrupted') {
|
|
378
|
+
if (observed.displaced.claimantToken === replacementToken) {
|
|
379
|
+
await publishSuccessor(replacementToken);
|
|
380
|
+
await maybeCrashReclaimPhase('before-cleanup');
|
|
381
|
+
await unlink(observed.displaced.name);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
// A legacy quarantine or a dead claimant is restored to the canonical
|
|
385
|
+
// owner name. Exactly one contender can win this rename; the next
|
|
386
|
+
// rename to a claimant-bound quarantine elects the successor.
|
|
387
|
+
await restoreDisplacedOwner(observed.displaced);
|
|
388
|
+
respond({ claimed: false });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else if (observed.kind === 'valid' || observed.kind === 'recoverable') {
|
|
393
|
+
if (!await replaceObservedOwner(observed.owner, replacementToken)) {
|
|
394
|
+
respond({ claimed: false });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
// Legacy empty persistent directories converge on one canonical owner
|
|
400
|
+
// pathname. This is an owner record, not a second election protocol.
|
|
401
|
+
try {
|
|
402
|
+
const bootstrapName = `owner-${BOOTSTRAP_OWNER_TOKEN}`;
|
|
403
|
+
await writeFile(bootstrapName, BOOTSTRAP_OWNER_TOKEN, { flag: 'wx', mode: 0o600 });
|
|
404
|
+
const bootstrap = await readPinnedRecord(bootstrapName, BOOTSTRAP_OWNER_TOKEN);
|
|
405
|
+
const visible = await readdir('.');
|
|
406
|
+
if (bootstrap.kind === 'valid' && (visible.length !== 1 || visible[0] !== bootstrapName)) {
|
|
407
|
+
const current = await readPinnedRecord(bootstrapName, BOOTSTRAP_OWNER_TOKEN);
|
|
408
|
+
if (current.kind === 'valid' && sameOwner(current.owner, bootstrap.owner))
|
|
409
|
+
await unlink(bootstrapName);
|
|
410
|
+
respond({ claimed: false });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
if (error.code !== 'EEXIST')
|
|
416
|
+
throw error;
|
|
417
|
+
respond({ claimed: false });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const finalOwner = await readPinnedOwner();
|
|
422
|
+
respond({ claimed: finalOwner.kind === 'valid' && finalOwner.owner.token === replacementToken });
|
|
423
|
+
}
|
|
424
|
+
function sameOwnerState(a, b) {
|
|
425
|
+
if (a.kind !== b.kind)
|
|
426
|
+
return false;
|
|
427
|
+
if ((a.kind === 'valid' || a.kind === 'recoverable') && (b.kind === 'valid' || b.kind === 'recoverable')) {
|
|
428
|
+
return sameOwner(a.owner, b.owner);
|
|
429
|
+
}
|
|
430
|
+
if (a.kind === 'interrupted' && b.kind === 'interrupted')
|
|
431
|
+
return sameOwner(a.displaced, b.displaced);
|
|
432
|
+
if (a.kind === 'preparing' && b.kind === 'preparing') {
|
|
433
|
+
return a.candidateComplete === b.candidateComplete
|
|
434
|
+
&& sameOwner(a.candidate, b.candidate) && sameOwner(a.displaced, b.displaced);
|
|
435
|
+
}
|
|
436
|
+
if (a.kind === 'published' && b.kind === 'published') {
|
|
437
|
+
return sameOwner(a.owner, b.owner) && sameOwner(a.displaced, b.displaced);
|
|
438
|
+
}
|
|
439
|
+
if (a.kind === 'partial-published' && b.kind === 'partial-published') {
|
|
440
|
+
return sameOwner(a.owner, b.owner) && sameOwner(a.displaced, b.displaced);
|
|
441
|
+
}
|
|
442
|
+
return a.kind === 'ownerless' && b.kind === 'ownerless'
|
|
443
|
+
|| a.kind === 'unstable' && b.kind === 'unstable'
|
|
444
|
+
|| a.kind === 'ambiguous' && b.kind === 'ambiguous';
|
|
445
|
+
}
|
|
446
|
+
if (process.argv[2] === '--claim-existing') {
|
|
447
|
+
try {
|
|
448
|
+
await runPinnedClaimMode(process.argv.slice(3));
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
respond({ claimed: false, error: error instanceof Error ? error.message : String(error) });
|
|
452
|
+
}
|
|
453
|
+
process.exit(0);
|
|
454
|
+
}
|
|
455
|
+
const [key = '', nonce = '', expectedDev = '', expectedIno = ''] = process.argv.slice(2);
|
|
456
|
+
if (!key || basename(key) !== key || !nonce || !expectedDev || !expectedIno) {
|
|
457
|
+
throw new Error('invalid canonical state lock helper arguments');
|
|
458
|
+
}
|
|
459
|
+
const [issuedAt = '', randomNonce = ''] = nonce.split('-', 2);
|
|
460
|
+
const processStartIdentity = await readProcessStartIdentity(process.pid);
|
|
461
|
+
const token = formatProcessOwnerToken({
|
|
462
|
+
pid: process.pid,
|
|
463
|
+
issuedAtMs: Number(issuedAt),
|
|
464
|
+
nonce: randomNonce,
|
|
465
|
+
processStartIdentity,
|
|
466
|
+
});
|
|
467
|
+
const lockName = `${key}.lock`;
|
|
468
|
+
const namespaceIdentity = { dev: Number(expectedDev), ino: Number(expectedIno) };
|
|
469
|
+
let lockIdentity = null;
|
|
470
|
+
async function assertPinnedNamespace() {
|
|
471
|
+
const current = await stat(lockIdentity ? '..' : '.');
|
|
472
|
+
if (!current.isDirectory() || !sameIdentity(current, namespaceIdentity)) {
|
|
473
|
+
throw new Error('canonical state lock namespace changed');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function assertPinnedLock() {
|
|
477
|
+
if (!lockIdentity)
|
|
478
|
+
throw new Error('canonical state lock was not pinned');
|
|
479
|
+
await assertPinnedNamespace();
|
|
480
|
+
const pinned = await stat('.');
|
|
481
|
+
let visible;
|
|
482
|
+
try {
|
|
483
|
+
visible = await lstat(`../${lockName}`);
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
throw new Error('canonical state lock ownership lost');
|
|
487
|
+
}
|
|
488
|
+
if (!pinned.isDirectory() || !visible.isDirectory() || visible.isSymbolicLink()
|
|
489
|
+
|| !sameIdentity(pinned, lockIdentity) || !sameIdentity(visible, lockIdentity)) {
|
|
490
|
+
throw new Error('canonical state lock ownership lost');
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async function enterPinnedLock(expected) {
|
|
494
|
+
process.chdir(lockName);
|
|
495
|
+
const pinned = await stat('.');
|
|
496
|
+
const parent = await stat('..');
|
|
497
|
+
if (!pinned.isDirectory() || !sameIdentity(pinned, expected) || !sameIdentity(parent, namespaceIdentity)) {
|
|
498
|
+
throw new Error('canonical state lock changed while pinning');
|
|
499
|
+
}
|
|
500
|
+
lockIdentity = expected;
|
|
501
|
+
}
|
|
502
|
+
async function tryClaimExisting(expected) {
|
|
503
|
+
const child = spawn(process.execPath, [process.argv[1], '--claim-existing', lockName, token,
|
|
504
|
+
String(namespaceIdentity.dev), String(namespaceIdentity.ino), String(expected.dev), String(expected.ino)], {
|
|
505
|
+
cwd: lockName, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
506
|
+
});
|
|
507
|
+
let output = '';
|
|
508
|
+
child.stdout.setEncoding('utf8');
|
|
509
|
+
child.stdout.on('data', (chunk) => { output += chunk; });
|
|
510
|
+
const code = await new Promise((resolve) => child.once('close', resolve));
|
|
511
|
+
if (code !== 0)
|
|
512
|
+
return false;
|
|
513
|
+
const parsed = JSON.parse(output.trim());
|
|
514
|
+
if (typeof parsed.error === 'string')
|
|
515
|
+
throw new Error(parsed.error);
|
|
516
|
+
return parsed.claimed === true;
|
|
517
|
+
}
|
|
518
|
+
async function acquire() {
|
|
519
|
+
// Keep the helper below the caller's explicit 35s initialization ceiling,
|
|
520
|
+
// while leaving enough room for Darwin process-start revalidation under the
|
|
521
|
+
// documented 64-contender recovery stress.
|
|
522
|
+
const deadline = Date.now() + 34_000;
|
|
523
|
+
let retryMs = INITIAL_RETRY_MS;
|
|
524
|
+
for (;;) {
|
|
525
|
+
await assertPinnedNamespace();
|
|
526
|
+
try {
|
|
527
|
+
await mkdir(lockName);
|
|
528
|
+
const created = await lstat(lockName);
|
|
529
|
+
if (!created.isDirectory() || created.isSymbolicLink())
|
|
530
|
+
throw new Error('canonical state lock invalid');
|
|
531
|
+
const identity = { dev: created.dev, ino: created.ino };
|
|
532
|
+
await enterPinnedLock(identity);
|
|
533
|
+
await writeFile(`owner-${token}`, token, { flag: 'wx', mode: 0o600 });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
if (lockIdentity)
|
|
538
|
+
throw error;
|
|
539
|
+
let visible = null;
|
|
540
|
+
try {
|
|
541
|
+
visible = await lstat(lockName);
|
|
542
|
+
}
|
|
543
|
+
catch (readError) {
|
|
544
|
+
if (readError.code !== 'ENOENT')
|
|
545
|
+
throw readError;
|
|
546
|
+
}
|
|
547
|
+
if (visible?.isDirectory() && !visible.isSymbolicLink()) {
|
|
548
|
+
const identity = { dev: visible.dev, ino: visible.ino };
|
|
549
|
+
if (await tryClaimExisting(identity)) {
|
|
550
|
+
const confirmed = await lstat(lockName);
|
|
551
|
+
if (!confirmed.isDirectory() || confirmed.isSymbolicLink() || !sameIdentity(confirmed, identity)) {
|
|
552
|
+
throw new Error('canonical state lock changed after stale claim');
|
|
553
|
+
}
|
|
554
|
+
await enterPinnedLock(identity);
|
|
555
|
+
const claimed = await readPinnedOwner();
|
|
556
|
+
if (claimed.kind !== 'valid' || claimed.owner.token !== token)
|
|
557
|
+
throw new Error('canonical state lock ownership lost');
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
else if (visible) {
|
|
562
|
+
throw new Error('canonical state lock invalid');
|
|
563
|
+
}
|
|
564
|
+
if (error.code !== 'EEXIST')
|
|
565
|
+
throw error;
|
|
566
|
+
if (Date.now() >= deadline)
|
|
567
|
+
throw new Error(`timed out waiting for canonical state lock: ${lockName}`);
|
|
568
|
+
const jitter = Math.floor(Math.random() * retryMs);
|
|
569
|
+
const remaining = Math.max(0, deadline - Date.now());
|
|
570
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(remaining, retryMs + jitter)));
|
|
571
|
+
if (Date.now() >= deadline)
|
|
572
|
+
throw new Error(`timed out waiting for canonical state lock: ${lockName}`);
|
|
573
|
+
retryMs = Math.min(MAX_RETRY_MS, retryMs * 2);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async function release() {
|
|
578
|
+
await assertPinnedLock();
|
|
579
|
+
const observed = await readPinnedOwner();
|
|
580
|
+
if (observed.kind !== 'valid' || observed.owner.token !== token)
|
|
581
|
+
throw new Error('canonical state lock ownership lost');
|
|
582
|
+
await unlink(`owner-${token}`);
|
|
583
|
+
// The persistent directory remains pinned; a successor converges through
|
|
584
|
+
// the fixed bootstrap owner without depending on this process's liveness.
|
|
585
|
+
}
|
|
586
|
+
let acquired = false;
|
|
587
|
+
try {
|
|
588
|
+
await acquire();
|
|
589
|
+
acquired = true;
|
|
590
|
+
await new Promise((resolve) => process.stdout.write(`${JSON.stringify({ id: 0, ok: true, ready: true })}\n`, () => resolve()));
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
const response = { id: 0, ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
594
|
+
await new Promise((resolve) => process.stdout.write(`${JSON.stringify(response)}\n`, () => resolve()));
|
|
595
|
+
}
|
|
596
|
+
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
597
|
+
if (!acquired)
|
|
598
|
+
lines.close();
|
|
599
|
+
for await (const line of acquired ? lines : []) {
|
|
600
|
+
let id;
|
|
601
|
+
try {
|
|
602
|
+
const request = JSON.parse(line);
|
|
603
|
+
id = request.id;
|
|
604
|
+
if (request.op === 'assert') {
|
|
605
|
+
await assertPinnedLock();
|
|
606
|
+
const observed = await readPinnedOwner();
|
|
607
|
+
if (observed.kind !== 'valid' || observed.owner.token !== token)
|
|
608
|
+
throw new Error('canonical state lock ownership lost');
|
|
609
|
+
respond({ id, ok: true });
|
|
610
|
+
}
|
|
611
|
+
else if (request.op === 'close') {
|
|
612
|
+
await release();
|
|
613
|
+
respond({ id, ok: true });
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
else
|
|
617
|
+
throw new Error('unsupported canonical state lock operation');
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
respond({ id, ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
//# sourceMappingURL=mode-binding-lease-helper.js.map
|