@logictan/dsh-config-manager 0.1.60
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/LICENSE +21 -0
- package/README.md +105 -0
- package/cordis.patch.yml +21 -0
- package/lib/adapters/agent-instructions.d.ts +13 -0
- package/lib/adapters/agent-instructions.js +46 -0
- package/lib/adapters/agent-presets.d.ts +13 -0
- package/lib/adapters/agent-presets.js +14 -0
- package/lib/adapters/credentials.d.ts +37 -0
- package/lib/adapters/credentials.js +122 -0
- package/lib/adapters/file-collection.d.ts +15 -0
- package/lib/adapters/file-collection.js +127 -0
- package/lib/adapters/index.d.ts +57 -0
- package/lib/adapters/index.js +55 -0
- package/lib/adapters/link-report.d.ts +16 -0
- package/lib/adapters/link-report.js +47 -0
- package/lib/adapters/mcp.d.ts +28 -0
- package/lib/adapters/mcp.js +178 -0
- package/lib/adapters/plugin-files.d.ts +18 -0
- package/lib/adapters/plugin-files.js +142 -0
- package/lib/adapters/plugins.d.ts +41 -0
- package/lib/adapters/plugins.js +603 -0
- package/lib/adapters/pnpm-workspace.d.ts +26 -0
- package/lib/adapters/pnpm-workspace.js +147 -0
- package/lib/adapters/prompts.d.ts +30 -0
- package/lib/adapters/prompts.js +231 -0
- package/lib/adapters/providers.d.ts +34 -0
- package/lib/adapters/providers.js +155 -0
- package/lib/adapters/self.d.ts +17 -0
- package/lib/adapters/self.js +71 -0
- package/lib/adapters/sessions.d.ts +14 -0
- package/lib/adapters/sessions.js +15 -0
- package/lib/adapters/settings.d.ts +28 -0
- package/lib/adapters/settings.js +165 -0
- package/lib/adapters/skills.d.ts +13 -0
- package/lib/adapters/skills.js +14 -0
- package/lib/adapters/test-helpers.d.ts +125 -0
- package/lib/adapters/test-helpers.js +236 -0
- package/lib/adapters/ui.d.ts +27 -0
- package/lib/adapters/ui.js +89 -0
- package/lib/adapters/workspaces.d.ts +13 -0
- package/lib/adapters/workspaces.js +106 -0
- package/lib/client.d.ts +1026 -0
- package/lib/client.js +10047 -0
- package/lib/core/analyzer.d.ts +99 -0
- package/lib/core/analyzer.js +934 -0
- package/lib/core/backup.d.ts +138 -0
- package/lib/core/backup.js +513 -0
- package/lib/core/boot-rescue.d.ts +146 -0
- package/lib/core/boot-rescue.js +618 -0
- package/lib/core/cache-cleaner.d.ts +31 -0
- package/lib/core/cache-cleaner.js +95 -0
- package/lib/core/config-lifecycle.d.ts +180 -0
- package/lib/core/config-lifecycle.js +481 -0
- package/lib/core/config-snapshot.d.ts +135 -0
- package/lib/core/config-snapshot.js +424 -0
- package/lib/core/config-state.d.ts +70 -0
- package/lib/core/config-state.js +177 -0
- package/lib/core/consult-source.d.ts +58 -0
- package/lib/core/consult-source.js +260 -0
- package/lib/core/crash-report.d.ts +84 -0
- package/lib/core/crash-report.js +317 -0
- package/lib/core/exporter.d.ts +68 -0
- package/lib/core/exporter.js +375 -0
- package/lib/core/ghost-sweep.d.ts +19 -0
- package/lib/core/ghost-sweep.js +52 -0
- package/lib/core/importer.d.ts +60 -0
- package/lib/core/importer.js +29 -0
- package/lib/core/index.d.ts +14 -0
- package/lib/core/index.js +14 -0
- package/lib/core/journal.d.ts +197 -0
- package/lib/core/journal.js +418 -0
- package/lib/core/local-plugin-host.d.ts +35 -0
- package/lib/core/local-plugin-host.js +120 -0
- package/lib/core/local-plugin-pack.d.ts +135 -0
- package/lib/core/local-plugin-pack.js +278 -0
- package/lib/core/messages.d.ts +310 -0
- package/lib/core/messages.js +621 -0
- package/lib/core/migration-consult.d.ts +117 -0
- package/lib/core/migration-consult.js +363 -0
- package/lib/core/migration-history.d.ts +153 -0
- package/lib/core/migration-history.js +411 -0
- package/lib/core/model-tools.d.ts +50 -0
- package/lib/core/model-tools.js +148 -0
- package/lib/core/msg-types.d.ts +8 -0
- package/lib/core/msg-types.js +6 -0
- package/lib/core/phase3-child-crash.d.ts +1 -0
- package/lib/core/phase3-child-crash.js +87 -0
- package/lib/core/phase3-host.d.ts +138 -0
- package/lib/core/phase3-host.js +359 -0
- package/lib/core/phase3-prod-child.d.ts +1 -0
- package/lib/core/phase3-prod-child.js +35 -0
- package/lib/core/phase4-crash-child.d.ts +1 -0
- package/lib/core/phase4-crash-child.js +103 -0
- package/lib/core/plugin-cli.d.ts +150 -0
- package/lib/core/plugin-cli.js +497 -0
- package/lib/core/reconcile.d.ts +120 -0
- package/lib/core/reconcile.js +389 -0
- package/lib/core/recovery-orchestrator.d.ts +94 -0
- package/lib/core/recovery-orchestrator.js +327 -0
- package/lib/core/restore.d.ts +162 -0
- package/lib/core/restore.js +703 -0
- package/lib/core/rollback.d.ts +12 -0
- package/lib/core/rollback.js +145 -0
- package/lib/core/run-registry.d.ts +85 -0
- package/lib/core/run-registry.js +157 -0
- package/lib/core/startup-barrier.d.ts +53 -0
- package/lib/core/startup-barrier.js +77 -0
- package/lib/core/types.d.ts +515 -0
- package/lib/core/types.js +27 -0
- package/lib/core/undo.d.ts +73 -0
- package/lib/core/undo.js +87 -0
- package/lib/core/validator.d.ts +18 -0
- package/lib/core/validator.js +77 -0
- package/lib/core/verify-recovery.d.ts +21 -0
- package/lib/core/verify-recovery.js +256 -0
- package/lib/core/watcher.d.ts +101 -0
- package/lib/core/watcher.js +219 -0
- package/lib/index.d.ts +238 -0
- package/lib/index.js +2764 -0
- package/lib/migrations/index.d.ts +29 -0
- package/lib/migrations/index.js +56 -0
- package/lib/migrations/v1-to-v2.d.ts +9 -0
- package/lib/migrations/v1-to-v2.js +10 -0
- package/lib/profiles/index.d.ts +4 -0
- package/lib/profiles/index.js +5 -0
- package/lib/profiles/profile-manager.d.ts +106 -0
- package/lib/profiles/profile-manager.js +543 -0
- package/lib/schema/config.d.ts +28 -0
- package/lib/schema/config.js +165 -0
- package/lib/schema/index.d.ts +24 -0
- package/lib/schema/index.js +26 -0
- package/lib/schema/manifest.d.ts +28 -0
- package/lib/schema/manifest.js +109 -0
- package/lib/schema/tombstones.d.ts +46 -0
- package/lib/schema/tombstones.js +64 -0
- package/lib/schema/types.d.ts +232 -0
- package/lib/schema/types.js +8 -0
- package/lib/schema/versions.d.ts +27 -0
- package/lib/schema/versions.js +50 -0
- package/lib/security/encryption.d.ts +85 -0
- package/lib/security/encryption.js +279 -0
- package/lib/security/index.d.ts +16 -0
- package/lib/security/index.js +17 -0
- package/lib/security/integrity.d.ts +30 -0
- package/lib/security/integrity.js +78 -0
- package/lib/security/redaction.d.ts +26 -0
- package/lib/security/redaction.js +95 -0
- package/lib/security/secret-scanner.d.ts +86 -0
- package/lib/security/secret-scanner.js +404 -0
- package/lib/security/vault.d.ts +59 -0
- package/lib/security/vault.js +151 -0
- package/lib/security/zip-security.d.ts +27 -0
- package/lib/security/zip-security.js +154 -0
- package/lib/sync/autosync-config.d.ts +39 -0
- package/lib/sync/autosync-config.js +151 -0
- package/lib/sync/autosync-scheduler.d.ts +139 -0
- package/lib/sync/autosync-scheduler.js +564 -0
- package/lib/sync/fs.d.ts +18 -0
- package/lib/sync/fs.js +49 -0
- package/lib/sync/git/git-transport.d.ts +100 -0
- package/lib/sync/git/git-transport.js +437 -0
- package/lib/sync/github-auth.d.ts +131 -0
- package/lib/sync/github-auth.js +264 -0
- package/lib/sync/layout.d.ts +43 -0
- package/lib/sync/layout.js +203 -0
- package/lib/sync/snapshot-json.d.ts +9 -0
- package/lib/sync/snapshot-json.js +107 -0
- package/lib/sync/sync-config.d.ts +81 -0
- package/lib/sync/sync-config.js +281 -0
- package/lib/sync/sync-engine.d.ts +269 -0
- package/lib/sync/sync-engine.js +591 -0
- package/lib/sync/sync-history.d.ts +42 -0
- package/lib/sync/sync-history.js +78 -0
- package/lib/sync/sync-selection.d.ts +29 -0
- package/lib/sync/sync-selection.js +127 -0
- package/lib/sync/sync-session.d.ts +40 -0
- package/lib/sync/sync-session.js +54 -0
- package/lib/sync/sync-state.d.ts +37 -0
- package/lib/sync/sync-state.js +108 -0
- package/lib/sync/transport.d.ts +75 -0
- package/lib/sync/transport.js +55 -0
- package/lib/sync/ui-prefs.d.ts +34 -0
- package/lib/sync/ui-prefs.js +109 -0
- package/lib/sync/webdav/webdav-transport.d.ts +77 -0
- package/lib/sync/webdav/webdav-transport.js +451 -0
- package/lib/ui/errors.d.ts +19 -0
- package/lib/ui/errors.js +81 -0
- package/lib/ui/export-flow.d.ts +82 -0
- package/lib/ui/export-flow.js +98 -0
- package/lib/ui/i18n.d.ts +289 -0
- package/lib/ui/i18n.js +581 -0
- package/lib/ui/migration-consult-view.d.ts +91 -0
- package/lib/ui/migration-consult-view.js +115 -0
- package/lib/ui/progress.d.ts +38 -0
- package/lib/ui/progress.js +87 -0
- package/lib/ui/report.d.ts +23 -0
- package/lib/ui/report.js +153 -0
- package/lib/ui/test-helpers.d.ts +69 -0
- package/lib/ui/test-helpers.js +154 -0
- package/lib/ui/types.d.ts +257 -0
- package/lib/ui/types.js +21 -0
- package/lib/utils/atomic-write.d.ts +88 -0
- package/lib/utils/atomic-write.js +399 -0
- package/lib/utils/env-lock.d.ts +393 -0
- package/lib/utils/env-lock.js +930 -0
- package/lib/utils/hashing.d.ts +17 -0
- package/lib/utils/hashing.js +41 -0
- package/lib/utils/json.d.ts +24 -0
- package/lib/utils/json.js +76 -0
- package/lib/utils/logger.d.ts +39 -0
- package/lib/utils/logger.js +82 -0
- package/lib/utils/paths.d.ts +42 -0
- package/lib/utils/paths.js +189 -0
- package/lib/utils/proxy.d.ts +69 -0
- package/lib/utils/proxy.js +455 -0
- package/lib/utils/recursive-walk.d.ts +24 -0
- package/lib/utils/recursive-walk.js +134 -0
- package/lib/utils/zip.d.ts +54 -0
- package/lib/utils/zip.js +285 -0
- package/package.json +188 -0
- package/src/adapters/agent-instructions.ts +48 -0
- package/src/adapters/agent-presets.ts +14 -0
- package/src/adapters/credentials.test.ts +90 -0
- package/src/adapters/credentials.ts +143 -0
- package/src/adapters/file-collection.ts +137 -0
- package/src/adapters/files.test.ts +353 -0
- package/src/adapters/index.ts +94 -0
- package/src/adapters/link-report.test.ts +65 -0
- package/src/adapters/link-report.ts +61 -0
- package/src/adapters/mcp.test.ts +96 -0
- package/src/adapters/mcp.ts +185 -0
- package/src/adapters/plugin-files.ts +146 -0
- package/src/adapters/plugins.test.ts +326 -0
- package/src/adapters/plugins.ts +615 -0
- package/src/adapters/pnpm-workspace.test.ts +87 -0
- package/src/adapters/pnpm-workspace.ts +150 -0
- package/src/adapters/prompts.test.ts +110 -0
- package/src/adapters/prompts.ts +230 -0
- package/src/adapters/providers.test.ts +126 -0
- package/src/adapters/providers.ts +173 -0
- package/src/adapters/roundtrip.test.ts +190 -0
- package/src/adapters/self.test.ts +92 -0
- package/src/adapters/self.ts +75 -0
- package/src/adapters/sessions.ts +15 -0
- package/src/adapters/settings.test.ts +100 -0
- package/src/adapters/settings.ts +187 -0
- package/src/adapters/skills.ts +14 -0
- package/src/adapters/test-helpers.ts +248 -0
- package/src/adapters/ui.test.ts +57 -0
- package/src/adapters/ui.ts +108 -0
- package/src/adapters/workspaces.test.ts +106 -0
- package/src/adapters/workspaces.ts +109 -0
- package/src/client/ConfigManagerSection.tsx +90 -0
- package/src/client/api.ts +66 -0
- package/src/client/client-types.ts +18 -0
- package/src/client/common/ErrorBanner.tsx +73 -0
- package/src/client/common/Icon.tsx +129 -0
- package/src/client/common/Modal.tsx +173 -0
- package/src/client/common/ToastViewport.tsx +73 -0
- package/src/client/common/toast-store.test.ts +192 -0
- package/src/client/common/toast-store.ts +181 -0
- package/src/client/common/toast-types.ts +7 -0
- package/src/client/common/ui.tsx +342 -0
- package/src/client/config-manager.module.css +2971 -0
- package/src/client/css-modules.d.ts +8 -0
- package/src/client/index.ts +65 -0
- package/src/client/locales.ts +56 -0
- package/src/client/lucide-icons.d.ts +38 -0
- package/src/client/run-store.test.ts +223 -0
- package/src/client/run-store.ts +372 -0
- package/src/client/sync/SyncConfirmView.tsx +379 -0
- package/src/client/sync/SyncHistoryView.test.ts +40 -0
- package/src/client/sync/SyncHistoryView.tsx +220 -0
- package/src/client/sync/SyncSettingsView.tsx +1535 -0
- package/src/client/sync/history-model.test.ts +194 -0
- package/src/client/sync/history-model.ts +195 -0
- package/src/client/sync/sync-api.test.ts +585 -0
- package/src/client/sync/sync-api.ts +543 -0
- package/src/client/sync/sync-locales.ts +416 -0
- package/src/client/sync/sync-push-preview.test.ts +52 -0
- package/src/client/sync/sync-view-v2.test.ts +213 -0
- package/src/client/sync/sync-view.test.ts +487 -0
- package/src/client/sync/sync-view.ts +759 -0
- package/src/core/analyzer.ts +1068 -0
- package/src/core/backup.ts +605 -0
- package/src/core/boot-rescue.test.ts +847 -0
- package/src/core/boot-rescue.ts +737 -0
- package/src/core/cache-cleaner.test.ts +169 -0
- package/src/core/cache-cleaner.ts +121 -0
- package/src/core/config-lifecycle.test.ts +727 -0
- package/src/core/config-lifecycle.ts +573 -0
- package/src/core/config-snapshot.test.ts +369 -0
- package/src/core/config-snapshot.ts +522 -0
- package/src/core/config-state.test.ts +381 -0
- package/src/core/config-state.ts +207 -0
- package/src/core/consult-source.test.ts +199 -0
- package/src/core/consult-source.ts +293 -0
- package/src/core/crash-report.test.ts +380 -0
- package/src/core/crash-report.ts +369 -0
- package/src/core/exporter.ts +417 -0
- package/src/core/ghost-sweep.test.ts +94 -0
- package/src/core/ghost-sweep.ts +67 -0
- package/src/core/importer.ts +83 -0
- package/src/core/index.ts +56 -0
- package/src/core/journal.test.ts +257 -0
- package/src/core/journal.ts +518 -0
- package/src/core/local-plugin-host.ts +152 -0
- package/src/core/local-plugin-pack.test.ts +359 -0
- package/src/core/local-plugin-pack.ts +363 -0
- package/src/core/messages.test.ts +54 -0
- package/src/core/messages.ts +664 -0
- package/src/core/migration-consult.test.ts +315 -0
- package/src/core/migration-consult.ts +494 -0
- package/src/core/migration-history.test.ts +319 -0
- package/src/core/migration-history.ts +484 -0
- package/src/core/model-tools.test.ts +190 -0
- package/src/core/model-tools.ts +208 -0
- package/src/core/msg-types.ts +10 -0
- package/src/core/phase3-child-crash.ts +95 -0
- package/src/core/phase3-host.ts +411 -0
- package/src/core/phase3-p1.test.ts +212 -0
- package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
- package/src/core/phase3-prod-child.ts +36 -0
- package/src/core/phase3-production-integration.test.ts +286 -0
- package/src/core/phase4-crash-child.ts +101 -0
- package/src/core/phase4-crash-injection.test.ts +166 -0
- package/src/core/plugin-cli.fs.test.ts +184 -0
- package/src/core/plugin-cli.test.ts +137 -0
- package/src/core/plugin-cli.ts +563 -0
- package/src/core/reconcile.test.ts +392 -0
- package/src/core/reconcile.ts +498 -0
- package/src/core/recovery-orchestrator.test.ts +145 -0
- package/src/core/recovery-orchestrator.ts +366 -0
- package/src/core/restore-manage.test.ts +94 -0
- package/src/core/restore.ts +849 -0
- package/src/core/rollback.ts +151 -0
- package/src/core/run-progress.test.ts +237 -0
- package/src/core/run-registry.test.ts +232 -0
- package/src/core/run-registry.ts +209 -0
- package/src/core/smoke.test.ts +842 -0
- package/src/core/startup-barrier.ts +99 -0
- package/src/core/types.ts +542 -0
- package/src/core/undo.ts +116 -0
- package/src/core/validator.ts +84 -0
- package/src/core/verify-recovery.ts +265 -0
- package/src/core/watcher.test.ts +326 -0
- package/src/core/watcher.ts +263 -0
- package/src/index.facade.test.ts +265 -0
- package/src/index.sync.test.ts +217 -0
- package/src/index.ts +2975 -0
- package/src/migrations/index.ts +71 -0
- package/src/migrations/v1-to-v2.ts +17 -0
- package/src/profiles/index.ts +8 -0
- package/src/profiles/profile-manager.test.ts +282 -0
- package/src/profiles/profile-manager.ts +630 -0
- package/src/schema/config.ts +165 -0
- package/src/schema/index.ts +71 -0
- package/src/schema/manifest-schema.test.ts +519 -0
- package/src/schema/manifest.ts +124 -0
- package/src/schema/tombstones.test.ts +131 -0
- package/src/schema/tombstones.ts +89 -0
- package/src/schema/types.ts +245 -0
- package/src/schema/versions.ts +55 -0
- package/src/security/encryption.ts +335 -0
- package/src/security/index.ts +16 -0
- package/src/security/integrity.ts +102 -0
- package/src/security/redaction.ts +101 -0
- package/src/security/secret-scanner.ts +445 -0
- package/src/security/security.test.ts +1215 -0
- package/src/security/vault.test.ts +164 -0
- package/src/security/vault.ts +198 -0
- package/src/security/zip-security.ts +180 -0
- package/src/sync/autosync-config.test.ts +147 -0
- package/src/sync/autosync-config.ts +179 -0
- package/src/sync/autosync-scheduler.test.ts +490 -0
- package/src/sync/autosync-scheduler.ts +646 -0
- package/src/sync/fs.ts +48 -0
- package/src/sync/git/git-transport.test.ts +673 -0
- package/src/sync/git/git-transport.ts +491 -0
- package/src/sync/github-auth.test.ts +280 -0
- package/src/sync/github-auth.ts +356 -0
- package/src/sync/layout.test.ts +270 -0
- package/src/sync/layout.ts +222 -0
- package/src/sync/snapshot-json.test.ts +119 -0
- package/src/sync/snapshot-json.ts +128 -0
- package/src/sync/sync-config.test.ts +310 -0
- package/src/sync/sync-config.ts +299 -0
- package/src/sync/sync-engine.test.ts +1074 -0
- package/src/sync/sync-engine.ts +754 -0
- package/src/sync/sync-history.test.ts +116 -0
- package/src/sync/sync-history.ts +120 -0
- package/src/sync/sync-selection.test.ts +186 -0
- package/src/sync/sync-selection.ts +143 -0
- package/src/sync/sync-session.test.ts +143 -0
- package/src/sync/sync-session.ts +77 -0
- package/src/sync/sync-state.test.ts +164 -0
- package/src/sync/sync-state.ts +131 -0
- package/src/sync/transport.test.ts +111 -0
- package/src/sync/transport.ts +118 -0
- package/src/sync/ui-prefs.test.ts +152 -0
- package/src/sync/ui-prefs.ts +135 -0
- package/src/sync/webdav/webdav-transport.test.ts +871 -0
- package/src/sync/webdav/webdav-transport.ts +535 -0
- package/src/ui/errors.test.ts +47 -0
- package/src/ui/errors.ts +100 -0
- package/src/ui/export-flow.test.ts +87 -0
- package/src/ui/export-flow.ts +156 -0
- package/src/ui/i18n.ts +590 -0
- package/src/ui/migration-consult-view.test.ts +151 -0
- package/src/ui/migration-consult-view.ts +178 -0
- package/src/ui/progress.test.ts +47 -0
- package/src/ui/progress.ts +102 -0
- package/src/ui/report.test.ts +101 -0
- package/src/ui/report.ts +146 -0
- package/src/ui/test-helpers.ts +184 -0
- package/src/ui/types.ts +325 -0
- package/src/utils/atomic-write.test.ts +312 -0
- package/src/utils/atomic-write.ts +422 -0
- package/src/utils/env-lock.test.ts +999 -0
- package/src/utils/env-lock.ts +1086 -0
- package/src/utils/hashing.ts +53 -0
- package/src/utils/json.ts +77 -0
- package/src/utils/logger.ts +114 -0
- package/src/utils/paths.ts +176 -0
- package/src/utils/proxy.test.ts +465 -0
- package/src/utils/proxy.ts +493 -0
- package/src/utils/recursive-walk.test.ts +148 -0
- package/src/utils/recursive-walk.ts +145 -0
- package/src/utils/zip.ts +332 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 原子写 / 原子拷贝 primitive(Phase 1:单文件一致性层)。
|
|
3
|
+
*
|
|
4
|
+
* 目标:任何关键配置写入,目标文件在任意时刻要么是「旧完整版」、要么是「新完整版」,
|
|
5
|
+
* 绝无「半个新文件」。实现:同目录唯一 tmp → 写 + fsync → rename(tmp, target)。
|
|
6
|
+
*
|
|
7
|
+
* 设计要点(见 ATOMIC_WRITE_DESIGN.md v3 + ATOMIC_WRITE_DESIGN_REVIEW.md):
|
|
8
|
+
* - 无 `.bak` 多步 fallback(Phase 1 `atomicity > forced success`):目标被占用时
|
|
9
|
+
* 有界重试后**不改写原 target、返回明确错误**;绝不暂时移走 target。
|
|
10
|
+
* - sensitive 调用先 `open('wx', 0o600)` 再写(禁 default→write→chmod)。
|
|
11
|
+
* - symlink 默认 follow(realpath 写真实目标,保留用户符号链接结构);sensitive 用 'reject'。
|
|
12
|
+
* - tmp 名清洗(sanitizeTmpBase)+ 强制同目录,防恶意 base 路径逃逸。
|
|
13
|
+
* - Atomicity / Durability 分离:本层保证 process-crash atomicity;POSIX 额外 fsync 父目录
|
|
14
|
+
* 提供 strong power-loss durability;Windows 目录 fsync 不可用(自动跳过)——不宣称绝对保证。
|
|
15
|
+
* - 通过可注入 `AtomicIo` 支持故障注入测试(不 monkey-patch 全局 fs)。
|
|
16
|
+
*
|
|
17
|
+
* 零 DSH 依赖(仅 node:fs / node:path / node:crypto),CLI 离线引擎可复用。
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'node:fs/promises';
|
|
20
|
+
import fssync from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import crypto from 'node:crypto';
|
|
23
|
+
|
|
24
|
+
// ---------- 常量 ----------
|
|
25
|
+
|
|
26
|
+
/** 目录 fsync 在 Windows 不被 Node 支持:打开目录会抛错 → 平台跳过 */
|
|
27
|
+
const WINDOWS = process.platform === 'win32';
|
|
28
|
+
|
|
29
|
+
/** 新文件默认 mode(open 时叠加进程 umask) */
|
|
30
|
+
const DEFAULT_FILE_MODE = 0o666;
|
|
31
|
+
|
|
32
|
+
/** tmp 名前缀(专属,用于 orphan 识别,不误删其他程序文件) */
|
|
33
|
+
const TMP_PREFIX = '.dshcm.';
|
|
34
|
+
|
|
35
|
+
// ---------- 类型 ----------
|
|
36
|
+
|
|
37
|
+
/** 文件句柄最小面(node fs/promises FileHandle 的子集) */
|
|
38
|
+
export interface AtomicHandle {
|
|
39
|
+
writeFile(data: Uint8Array): Promise<void>;
|
|
40
|
+
sync(): Promise<void>;
|
|
41
|
+
close(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 可注入异步文件系统门面(默认实现包 node:fs/promises;测试注入失败点) */
|
|
45
|
+
export interface AtomicIo {
|
|
46
|
+
mkdir(dir: string, opts: { recursive: boolean }): Promise<void>;
|
|
47
|
+
/** 目标不存在返回 null */
|
|
48
|
+
lstat(p: string): Promise<{ isSymbolicLink(): boolean } | null>;
|
|
49
|
+
/** 目标不存在返回 null */
|
|
50
|
+
stat(p: string): Promise<{ mode: number } | null>;
|
|
51
|
+
realpath(p: string): Promise<string>;
|
|
52
|
+
open(p: string, flags: string, mode?: number): Promise<AtomicHandle>;
|
|
53
|
+
rename(src: string, dst: string): Promise<void>;
|
|
54
|
+
unlink(p: string): Promise<void>;
|
|
55
|
+
copyFile(src: string, dst: string, mode?: number): Promise<void>;
|
|
56
|
+
chmod(p: string, mode: number): Promise<void>;
|
|
57
|
+
/** 父目录 fsync(POSIX strong durability);Windows/不支持时静默 no-op */
|
|
58
|
+
fsyncDir(p: string): Promise<void>;
|
|
59
|
+
/** 重试退避(可注入便于测试) */
|
|
60
|
+
sleep(ms: number): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 同步版注入面(atomicWriteFileSync 用;仅 plugin-cli 等同步路径) */
|
|
64
|
+
export interface AtomicIoSync {
|
|
65
|
+
mkdir(dir: string, opts: { recursive: boolean }): void;
|
|
66
|
+
lstat(p: string): { isSymbolicLink(): boolean; mode: number } | null;
|
|
67
|
+
realpath(p: string): string;
|
|
68
|
+
open(p: string, flags: string, mode?: number): number;
|
|
69
|
+
writeFd(fd: number, buf: Uint8Array): void;
|
|
70
|
+
fsyncFd(fd: number): void;
|
|
71
|
+
closeFd(fd: number): void;
|
|
72
|
+
rename(src: string, dst: string): void;
|
|
73
|
+
unlink(p: string): void;
|
|
74
|
+
/** 目录 fsync(POSIX);Windows/不支持时 no-op */
|
|
75
|
+
fsyncDir(p: string): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type SymlinkPolicy = 'follow' | 'reject';
|
|
79
|
+
|
|
80
|
+
export interface AtomicWriteOptions {
|
|
81
|
+
/** 新文件权限;sensitive 调用显式 0o600 */
|
|
82
|
+
mode?: number;
|
|
83
|
+
/** 默认 true:目标存在则继承其 mode */
|
|
84
|
+
preserveMode?: boolean;
|
|
85
|
+
/** Windows / 目标被占用瞬时错误重试次数(默认 3) */
|
|
86
|
+
retries?: number;
|
|
87
|
+
/** 重试退避基数 ms(默认 25,翻倍递增:25→50→100) */
|
|
88
|
+
retryDelayMs?: number;
|
|
89
|
+
/** POSIX 是否 fsync 父目录(默认 true;Windows 自动跳过) */
|
|
90
|
+
fsyncDir?: boolean;
|
|
91
|
+
/** symlink 策略:默认 'follow';sensitive 调用传 'reject' */
|
|
92
|
+
symlink?: SymlinkPolicy;
|
|
93
|
+
/** 测试注入的异步 io */
|
|
94
|
+
io?: AtomicIo;
|
|
95
|
+
/** tmp 清理失败回调(调用方接线日志,满足「清理失败留痕」要求) */
|
|
96
|
+
onCleanupFailure?: (tmp: string, err: unknown) => void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface AtomicWriteSyncOptions extends Omit<AtomicWriteOptions, 'io'> {
|
|
100
|
+
syncIo?: AtomicIoSync;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 异步 io 默认实现 */
|
|
104
|
+
const defaultIo: AtomicIo = {
|
|
105
|
+
async mkdir(d, o) { await fs.mkdir(d, o); },
|
|
106
|
+
async lstat(p) {
|
|
107
|
+
try { return await fs.lstat(p); } catch (e) { if (isENOENT(e)) return null; throw e; }
|
|
108
|
+
},
|
|
109
|
+
async stat(p) {
|
|
110
|
+
try { return await fs.stat(p); } catch (e) { if (isENOENT(e)) return null; throw e; }
|
|
111
|
+
},
|
|
112
|
+
async realpath(p) { return fs.realpath(p); },
|
|
113
|
+
async open(p, f, m) { return fs.open(p, f as 'wx', m); },
|
|
114
|
+
async rename(a, b) { return fs.rename(a, b); },
|
|
115
|
+
async unlink(p) { return fs.unlink(p); },
|
|
116
|
+
async copyFile(a, b) { return fs.copyFile(a, b); },
|
|
117
|
+
async chmod(p, mode) { return fs.chmod(p, mode); },
|
|
118
|
+
async fsyncDir(p) { await fsyncDirAsync(p); },
|
|
119
|
+
async sleep(ms) { await new Promise((r) => setTimeout(r, ms)); },
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** 同步 io 默认实现 */
|
|
123
|
+
const defaultSyncIo: AtomicIoSync = {
|
|
124
|
+
mkdir(d, o) { fssync.mkdirSync(d, o); },
|
|
125
|
+
lstat(p) {
|
|
126
|
+
try { return fssync.lstatSync(p); } catch (e) { if (isENOENT(e)) return null; throw e; }
|
|
127
|
+
},
|
|
128
|
+
realpath(p) { return fssync.realpathSync(p); },
|
|
129
|
+
open(p, f, m) { return fssync.openSync(p, f as 'wx', m); },
|
|
130
|
+
writeFd(fd, buf) { fssync.writeSync(fd, buf, 0, buf.byteLength, 0); },
|
|
131
|
+
fsyncFd(fd) { fssync.fsyncSync(fd); },
|
|
132
|
+
closeFd(fd) { fssync.closeSync(fd); },
|
|
133
|
+
rename(a, b) { fssync.renameSync(a, b); },
|
|
134
|
+
unlink(p) { fssync.unlinkSync(p); },
|
|
135
|
+
fsyncDir(p) { fsyncDirSyncSafe(p); },
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** POSIX:fs.open(dir,'r') + sync(Windows 不可用 → 静默跳过) */
|
|
139
|
+
async function fsyncDirAsync(p: string): Promise<void> {
|
|
140
|
+
if (WINDOWS) return;
|
|
141
|
+
let h: AtomicHandle | null = null;
|
|
142
|
+
try {
|
|
143
|
+
h = await fs.open(p, 'r');
|
|
144
|
+
await h.sync();
|
|
145
|
+
} catch {
|
|
146
|
+
// 目录 fsync 是 best-effort:某些平台/文件系统不支持,忽略(不抛致命)
|
|
147
|
+
} finally {
|
|
148
|
+
if (h) try { await h.close(); } catch { /* ignore */ }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function fsyncDirSyncSafe(p: string): void {
|
|
153
|
+
if (WINDOWS) return;
|
|
154
|
+
let fd: number | null = null;
|
|
155
|
+
try {
|
|
156
|
+
fd = fssync.openSync(p, 'r');
|
|
157
|
+
fssync.fsyncSync(fd);
|
|
158
|
+
} catch {
|
|
159
|
+
// best-effort
|
|
160
|
+
} finally {
|
|
161
|
+
if (fd !== null) try { fssync.closeSync(fd); } catch { /* ignore */ }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isENOENT(e: unknown): boolean {
|
|
166
|
+
return typeof e === 'object' && e !== null && (e as { code?: unknown }).code === 'ENOENT';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isRetryableLockCode(code: string | undefined): boolean {
|
|
170
|
+
return code === 'EPERM' || code === 'EBUSY' || code === 'EACCES' || code === 'EEXIST' || code === 'ENOTEMPTY';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function randomHex(nBytes: number): string {
|
|
174
|
+
return crypto.randomBytes(nBytes).toString('hex');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 错误上下文(透传原始 code,便于上层识别占用/权限等) */
|
|
178
|
+
function withContext(e: unknown, target: string, op: string): Error {
|
|
179
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
180
|
+
(err as Error & { atomicTarget?: string; atomicOp?: string }).atomicTarget = target;
|
|
181
|
+
(err as Error & { atomicTarget?: string; atomicOp?: string }).atomicOp = op;
|
|
182
|
+
return err;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* tmp 名中 base 片段清洗:拒绝含 路径分隔符 / .. / 反斜杠 / 控制字符 / Windows 非法字符 的
|
|
187
|
+
* 目标 basename,防止恶意 base 把 tmp 引出目标目录。返回安全 base。
|
|
188
|
+
*/
|
|
189
|
+
export function sanitizeTmpBase(base: string): string {
|
|
190
|
+
if (base === '' || base === '.' || base === '..') throw new Error(`原子写:非法目标文件名「${base}」`);
|
|
191
|
+
if (/[\\/:*?"<>|]/.test(base)) throw new Error(`原子写:目标文件名含非法字符「${base}」`);
|
|
192
|
+
for (let i = 0; i < base.length; i++) {
|
|
193
|
+
const c = base.charCodeAt(i);
|
|
194
|
+
if (c < 0x20 || c === 0x7f) throw new Error(`原子写:目标文件名含控制字符「${base}」`);
|
|
195
|
+
}
|
|
196
|
+
return base;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 解析最终写入目标:symlink 'follow' → realpath;'reject' → 拒绝 */
|
|
200
|
+
async function resolveTarget(i: AtomicIo, target: string, symlink: SymlinkPolicy): Promise<string> {
|
|
201
|
+
const l = await i.lstat(target);
|
|
202
|
+
if (l && l.isSymbolicLink()) {
|
|
203
|
+
if (symlink === 'reject') {
|
|
204
|
+
throw new Error(`原子写:目标为符号链接且策略为 reject,拒绝写入「${target}」`);
|
|
205
|
+
}
|
|
206
|
+
// follow:解析真实路径写真实文件(保留用户符号链接结构)
|
|
207
|
+
return i.realpath(target);
|
|
208
|
+
}
|
|
209
|
+
return target;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function resolveModeAsync(i: AtomicIo, existing: { mode: number } | null, opts: AtomicWriteOptions): Promise<number> {
|
|
213
|
+
if (opts.mode !== undefined) return opts.mode;
|
|
214
|
+
if (opts.preserveMode !== false && existing) return existing.mode;
|
|
215
|
+
return DEFAULT_FILE_MODE;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function tmpPath(dir: string, base: string): string {
|
|
219
|
+
return path.join(dir, `${TMP_PREFIX}${base}.${process.pid}.${randomHex(8)}.tmp`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function cleanupTmp(i: AtomicIo, tmp: string, opts: AtomicWriteOptions): Promise<void> {
|
|
223
|
+
try {
|
|
224
|
+
await i.unlink(tmp);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
opts.onCleanupFailure?.(tmp, e);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 有界重试 rename:瞬时占用(杀软/句柄)退避重试;仍失败不改写 target,尽力清理 tmp,抛错。
|
|
231
|
+
* 绝不 rm(target) 或移走 target。 */
|
|
232
|
+
async function renameWithRetry(
|
|
233
|
+
i: AtomicIo, tmp: string, target: string,
|
|
234
|
+
rc: { retries: number; retryDelayMs: number },
|
|
235
|
+
cleanupOnFail?: () => Promise<void>,
|
|
236
|
+
): Promise<void> {
|
|
237
|
+
let delay = rc.retryDelayMs;
|
|
238
|
+
for (let attempt = 0; ; attempt++) {
|
|
239
|
+
try {
|
|
240
|
+
await i.rename(tmp, target);
|
|
241
|
+
return;
|
|
242
|
+
} catch (e) {
|
|
243
|
+
const code = (e as { code?: unknown }).code as string | undefined;
|
|
244
|
+
if (isRetryableLockCode(code) && attempt < rc.retries) {
|
|
245
|
+
await i.sleep(delay);
|
|
246
|
+
delay *= 2;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (cleanupOnFail) await cleanupOnFail();
|
|
250
|
+
throw withContext(e, target, 'rename');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 原子写文件:同目录唯一 tmp → 写完整内容 → fsync(tmp) → rename(tmp, target)。
|
|
257
|
+
* 失败时不改写原 target(目标要么旧完整、要么新完整)。
|
|
258
|
+
*/
|
|
259
|
+
export async function atomicWriteFile(target: string, data: string | Uint8Array, rawOpts: AtomicWriteOptions = {}): Promise<void> {
|
|
260
|
+
const opts: Required<Pick<AtomicWriteOptions, 'retries' | 'retryDelayMs' | 'fsyncDir' | 'preserveMode' | 'symlink'>>
|
|
261
|
+
& Omit<AtomicWriteOptions, 'retries' | 'retryDelayMs' | 'fsyncDir' | 'preserveMode' | 'symlink'> = {
|
|
262
|
+
retries: rawOpts.retries ?? 3,
|
|
263
|
+
retryDelayMs: rawOpts.retryDelayMs ?? 25,
|
|
264
|
+
fsyncDir: rawOpts.fsyncDir ?? true,
|
|
265
|
+
preserveMode: rawOpts.preserveMode ?? true,
|
|
266
|
+
symlink: rawOpts.symlink ?? 'follow',
|
|
267
|
+
...rawOpts,
|
|
268
|
+
};
|
|
269
|
+
const bytes: Uint8Array = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
|
270
|
+
const i = opts.io ?? defaultIo;
|
|
271
|
+
const dst = await resolveTarget(i, target, opts.symlink);
|
|
272
|
+
const dir = path.dirname(dst);
|
|
273
|
+
const safeBase = sanitizeTmpBase(path.basename(dst));
|
|
274
|
+
await i.mkdir(dir, { recursive: true });
|
|
275
|
+
|
|
276
|
+
// —— 1. tmp 完整落盘 + fsync(失败点:mkdir/write/sync) —
|
|
277
|
+
const existing = await i.stat(dst); // 跟随 symlink 已 resolve;存在则继承 mode
|
|
278
|
+
const mode = await resolveModeAsync(i, existing, opts);
|
|
279
|
+
const tmp = tmpPath(dir, safeBase);
|
|
280
|
+
let handle: AtomicHandle | null = null;
|
|
281
|
+
try {
|
|
282
|
+
handle = await i.open(tmp, 'wx', mode); // 独占创建,先定权限再写(sensitive 传 0o600)
|
|
283
|
+
await handle.writeFile(bytes);
|
|
284
|
+
await handle.sync();
|
|
285
|
+
await handle.close();
|
|
286
|
+
handle = null;
|
|
287
|
+
} catch (e) {
|
|
288
|
+
if (handle) try { await handle.close(); } catch { /* ignore */ }
|
|
289
|
+
await cleanupTmp(i, tmp, opts);
|
|
290
|
+
throw withContext(e, dst, 'atomicWriteFile');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// —— 2. rename(失败点:rename;不改写原 target;失败尽力清理 tmp) —
|
|
294
|
+
await renameWithRetry(i, tmp, dst, { retries: opts.retries, retryDelayMs: opts.retryDelayMs }, () => cleanupTmp(i, tmp, opts));
|
|
295
|
+
|
|
296
|
+
// —— 3. 父目录 fsync(POSIX strong durability;Windows 跳过;失败不致命) —
|
|
297
|
+
if (opts.fsyncDir && !WINDOWS) {
|
|
298
|
+
try { await i.fsyncDir(dir); } catch { /* best-effort */ }
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 原子拷贝:copyFile(src→tmp) + fsync(tmp) + rename(tmp, dst)。
|
|
304
|
+
* 不整文件读内存(大 snapshot/session 避免双内存)。失败不改写 dst。
|
|
305
|
+
*/
|
|
306
|
+
export async function atomicCopyFile(src: string, dst: string, rawOpts: AtomicWriteOptions = {}): Promise<void> {
|
|
307
|
+
const opts = { retries: 3, retryDelayMs: 25, fsyncDir: true, preserveMode: true, symlink: 'follow' as SymlinkPolicy, ...rawOpts };
|
|
308
|
+
const i = opts.io ?? defaultIo;
|
|
309
|
+
const resolved = await resolveTarget(i, dst, opts.symlink);
|
|
310
|
+
const dir = path.dirname(resolved);
|
|
311
|
+
const safeBase = sanitizeTmpBase(path.basename(resolved));
|
|
312
|
+
await i.mkdir(dir, { recursive: true });
|
|
313
|
+
|
|
314
|
+
const srcStat = await i.stat(src);
|
|
315
|
+
const dstStat = await i.stat(resolved);
|
|
316
|
+
let mode: number | undefined = opts.mode;
|
|
317
|
+
if (mode === undefined) {
|
|
318
|
+
// 存在→继承 dst mode;否则继承 src mode;否则默认
|
|
319
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
320
|
+
mode = dstStat ? dstStat.mode : (srcStat ? srcStat.mode : DEFAULT_FILE_MODE);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const tmp = tmpPath(dir, safeBase);
|
|
324
|
+
try {
|
|
325
|
+
// node fs.copyFile 的第三参是 COPYFILE_* 标志位,不是权限位 → 先 copy 再 chmod 设置权限
|
|
326
|
+
await i.copyFile(src, tmp);
|
|
327
|
+
if (mode !== undefined) await i.chmod(tmp, mode);
|
|
328
|
+
// 用 'a'(append)打开后 fsync:'r' 只读句柄在 Windows 上 fsync 报 EPERM
|
|
329
|
+
const h = await i.open(tmp, 'a');
|
|
330
|
+
try { await h.sync(); } finally { await h.close(); }
|
|
331
|
+
} catch (e) {
|
|
332
|
+
await cleanupTmp(i, tmp, opts);
|
|
333
|
+
throw withContext(e, resolved, 'atomicCopyFile');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
await renameWithRetry(i, tmp, resolved, { retries: opts.retries, retryDelayMs: opts.retryDelayMs }, () => cleanupTmp(i, tmp, opts));
|
|
337
|
+
|
|
338
|
+
if (opts.fsyncDir && !WINDOWS) {
|
|
339
|
+
try { await i.fsyncDir(dir); } catch { /* best-effort */ }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** 同步版原子写(plugin-cli 等同步路径)。语义与 atomicWriteFile 一致。 */
|
|
344
|
+
export function atomicWriteFileSync(target: string, data: string | Uint8Array, rawOpts: AtomicWriteSyncOptions = {}): void {
|
|
345
|
+
const opts: Required<Pick<AtomicWriteSyncOptions, 'retries' | 'retryDelayMs' | 'fsyncDir' | 'preserveMode' | 'symlink'>>
|
|
346
|
+
& Omit<AtomicWriteSyncOptions, 'retries' | 'retryDelayMs' | 'fsyncDir' | 'preserveMode' | 'symlink'> = {
|
|
347
|
+
retries: rawOpts.retries ?? 3,
|
|
348
|
+
retryDelayMs: rawOpts.retryDelayMs ?? 25,
|
|
349
|
+
fsyncDir: rawOpts.fsyncDir ?? true,
|
|
350
|
+
preserveMode: rawOpts.preserveMode ?? true,
|
|
351
|
+
symlink: rawOpts.symlink ?? 'follow',
|
|
352
|
+
...rawOpts,
|
|
353
|
+
};
|
|
354
|
+
const i = opts.syncIo ?? defaultSyncIo;
|
|
355
|
+
|
|
356
|
+
const resolveTargetSync = (): string => {
|
|
357
|
+
const l = i.lstat(target);
|
|
358
|
+
if (l && l.isSymbolicLink()) {
|
|
359
|
+
if (opts.symlink === 'reject') throw new Error(`原子写(同步):目标为符号链接且策略为 reject,拒绝写入「${target}」`);
|
|
360
|
+
return i.realpath(target);
|
|
361
|
+
}
|
|
362
|
+
return target;
|
|
363
|
+
};
|
|
364
|
+
const dst = resolveTargetSync();
|
|
365
|
+
const dir = path.dirname(dst);
|
|
366
|
+
const safeBase = sanitizeTmpBase(path.basename(dst));
|
|
367
|
+
i.mkdir(dir, { recursive: true });
|
|
368
|
+
|
|
369
|
+
const existing = statSyncSafe(i.lstat(dst));
|
|
370
|
+
let mode = opts.mode;
|
|
371
|
+
if (mode === undefined) {
|
|
372
|
+
if (opts.preserveMode !== false && existing !== null) mode = existing;
|
|
373
|
+
else mode = DEFAULT_FILE_MODE;
|
|
374
|
+
}
|
|
375
|
+
const tmp = tmpPath(dir, safeBase);
|
|
376
|
+
let fd: number | null = null;
|
|
377
|
+
try {
|
|
378
|
+
fd = i.open(tmp, 'wx', mode);
|
|
379
|
+
i.writeFd(fd, typeof data === 'string' ? new TextEncoder().encode(data) : data);
|
|
380
|
+
i.fsyncFd(fd);
|
|
381
|
+
i.closeFd(fd);
|
|
382
|
+
fd = null;
|
|
383
|
+
} catch (e) {
|
|
384
|
+
if (fd !== null) try { i.closeFd(fd); } catch { /* ignore */ }
|
|
385
|
+
try { i.unlink(tmp); } catch { rawOpts.onCleanupFailure?.(tmp, e); }
|
|
386
|
+
throw withContext(e, dst, 'atomicWriteFileSync');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let delay = opts.retryDelayMs;
|
|
390
|
+
for (let attempt = 0; ; attempt++) {
|
|
391
|
+
try {
|
|
392
|
+
i.rename(tmp, dst);
|
|
393
|
+
break;
|
|
394
|
+
} catch (e) {
|
|
395
|
+
const code = (e as { code?: unknown }).code as string | undefined;
|
|
396
|
+
if (isRetryableLockCode(code) && attempt < opts.retries) {
|
|
397
|
+
// 同步版退避:用 Atomics.wait 避免占用事件循环太久的自旋;小退避足够
|
|
398
|
+
busyWait(delay);
|
|
399
|
+
delay *= 2;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
try { i.unlink(tmp); } catch { rawOpts.onCleanupFailure?.(tmp, e); }
|
|
403
|
+
throw withContext(e, dst, 'rename');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (opts.fsyncDir && !WINDOWS) i.fsyncDir(dir);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function statSyncSafe(l: { isSymbolicLink(): boolean; mode: number } | null): number | null {
|
|
410
|
+
return l ? l.mode : null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function busyWait(ms: number): void {
|
|
414
|
+
if (ms <= 0) return;
|
|
415
|
+
const sab = new SharedArrayBuffer(4);
|
|
416
|
+
const ia = new Int32Array(sab);
|
|
417
|
+
try {
|
|
418
|
+
Atomics.wait(ia, 0, 0, ms);
|
|
419
|
+
} catch {
|
|
420
|
+
/* SharedArrayBuffer 不可用时退化为无退避(少见) */
|
|
421
|
+
}
|
|
422
|
+
}
|