@alpsckr/unitycli 0.4.2 → 0.4.4
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/README.md +5 -5
- package/contracts/command-contract.json +109 -217
- package/contracts/package-files.json +374 -294
- package/contracts/stable-session/direct-call-semantics.json +8 -2
- package/contracts/tool-completion-contract.json +148 -672
- package/dist/bridge/admission.d.ts +17 -1
- package/dist/bridge/admission.js +2 -0
- package/dist/bridge/admission.js.map +1 -1
- package/dist/bridge/domains/asset.js +3 -1
- package/dist/bridge/domains/asset.js.map +1 -1
- package/dist/bridge/domains/camera.js +39 -7
- package/dist/bridge/domains/camera.js.map +1 -1
- package/dist/bridge/domains/console.js +2 -1
- package/dist/bridge/domains/console.js.map +1 -1
- package/dist/bridge/domains/doctor.d.ts +3 -0
- package/dist/bridge/domains/doctor.js +8 -0
- package/dist/bridge/domains/doctor.js.map +1 -0
- package/dist/bridge/domains/editor.js +0 -4
- package/dist/bridge/domains/editor.js.map +1 -1
- package/dist/bridge/domains/game-object.js +4 -12
- package/dist/bridge/domains/game-object.js.map +1 -1
- package/dist/bridge/domains/graphics.js +1 -16
- package/dist/bridge/domains/graphics.js.map +1 -1
- package/dist/bridge/domains/object.js +3 -0
- package/dist/bridge/domains/object.js.map +1 -1
- package/dist/bridge/domains/physics.js +1 -2
- package/dist/bridge/domains/physics.js.map +1 -1
- package/dist/bridge/domains/profiler.js +18 -7
- package/dist/bridge/domains/profiler.js.map +1 -1
- package/dist/bridge/domains/project-tool.js +4 -0
- package/dist/bridge/domains/project-tool.js.map +1 -1
- package/dist/bridge/domains/reflection.js +10 -2
- package/dist/bridge/domains/reflection.js.map +1 -1
- package/dist/bridge/domains/runtime-tool.js +3 -0
- package/dist/bridge/domains/runtime-tool.js.map +1 -1
- package/dist/bridge/domains/scene.js +3 -8
- package/dist/bridge/domains/scene.js.map +1 -1
- package/dist/bridge/domains/screenshot.js +30 -6
- package/dist/bridge/domains/screenshot.js.map +1 -1
- package/dist/bridge/domains/ui.js +3 -1
- package/dist/bridge/domains/ui.js.map +1 -1
- package/dist/bridge/index.d.ts +5 -5
- package/dist/bridge/index.js +6 -9
- package/dist/bridge/index.js.map +1 -1
- package/dist/bridge/registry.js +3 -5
- package/dist/bridge/registry.js.map +1 -1
- package/dist/bridge/session-supervisor.d.ts +6 -3
- package/dist/bridge/session-supervisor.js +122 -34
- package/dist/bridge/session-supervisor.js.map +1 -1
- package/dist/bridge/types.d.ts +6 -5
- package/dist/bridge/types.js +16 -2
- package/dist/bridge/types.js.map +1 -1
- package/dist/catalog-protocol.d.ts +64 -0
- package/dist/catalog-protocol.js +460 -0
- package/dist/catalog-protocol.js.map +1 -0
- package/dist/cli-commands/command.js +84 -61
- package/dist/cli-commands/command.js.map +1 -1
- package/dist/cli-commands/extensions.js +70 -50
- package/dist/cli-commands/extensions.js.map +1 -1
- package/dist/cli-commands/instances.js +1 -1
- package/dist/cli-commands/instances.js.map +1 -1
- package/dist/cli-process-owners.d.ts +6 -0
- package/dist/cli-process-owners.js +27 -0
- package/dist/cli-process-owners.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +20 -5
- package/dist/cli.js.map +1 -1
- package/dist/contracts/command-contract.js +14 -4
- package/dist/contracts/command-contract.js.map +1 -1
- package/dist/doctor.d.ts +4 -6
- package/dist/doctor.js +429 -398
- package/dist/doctor.js.map +1 -1
- package/dist/editor-blocker-observer.d.ts +2 -1
- package/dist/editor-blocker-observer.js +40 -13
- package/dist/editor-blocker-observer.js.map +1 -1
- package/dist/extension-availability.d.ts +27 -9
- package/dist/extension-availability.js +96 -56
- package/dist/extension-availability.js.map +1 -1
- package/dist/extensions/descriptor-loader.d.ts +2 -2
- package/dist/extensions/descriptor-loader.js +3 -4
- package/dist/extensions/descriptor-loader.js.map +1 -1
- package/dist/extensions/index.d.ts +1 -1
- package/dist/extensions/index.js +1 -1
- package/dist/extensions/index.js.map +1 -1
- package/dist/extensions/installer.d.ts +24 -7
- package/dist/extensions/installer.js +142 -66
- package/dist/extensions/installer.js.map +1 -1
- package/dist/extensions/registry.js +3 -5
- package/dist/extensions/registry.js.map +1 -1
- package/dist/extensions/state-contract.d.ts +11 -0
- package/dist/extensions/state-contract.js +50 -0
- package/dist/extensions/state-contract.js.map +1 -0
- package/dist/instances.d.ts +13 -1
- package/dist/instances.js +26 -0
- package/dist/instances.js.map +1 -1
- package/dist/package-manager.d.ts +0 -24
- package/dist/package-manager.js +5 -58
- package/dist/package-manager.js.map +1 -1
- package/dist/project-tool-discovery.d.ts +5 -2
- package/dist/project-tool-discovery.js +22 -18
- package/dist/project-tool-discovery.js.map +1 -1
- package/dist/runtime-code.js +1 -1
- package/dist/runtime-code.js.map +1 -1
- package/dist/tool-name.d.ts +4 -0
- package/dist/tool-name.js +15 -0
- package/dist/tool-name.js.map +1 -0
- package/dist/tools/.generated-completion-registry.d.ts +2 -0
- package/dist/tools/.generated-completion-registry.js +763 -325
- package/dist/tools/.generated-completion-registry.js.map +1 -1
- package/dist/tools/.generated-registry.d.ts +6 -11
- package/dist/tools/.generated-registry.js +6 -11
- package/dist/tools/.generated-registry.js.map +1 -1
- package/dist/tools/.generated-schemas.js +11 -16
- package/dist/tools/.generated-schemas.js.map +1 -1
- package/dist/tools/asset/refresh-status.d.ts +3 -0
- package/dist/tools/asset/refresh-status.js +12 -0
- package/dist/tools/asset/refresh-status.js.map +1 -0
- package/dist/tools/asset/refresh.js +7 -3
- package/dist/tools/asset/refresh.js.map +1 -1
- package/dist/tools/build/player-status.d.ts +3 -0
- package/dist/tools/build/player-status.js +12 -0
- package/dist/tools/build/player-status.js.map +1 -0
- package/dist/tools/build/run.js +1 -1
- package/dist/tools/build/run.js.map +1 -1
- package/dist/tools/catalog-control.d.ts +22 -0
- package/dist/tools/catalog-control.js +335 -0
- package/dist/tools/catalog-control.js.map +1 -0
- package/dist/tools/catalog.d.ts +7 -9
- package/dist/tools/catalog.js +66 -45
- package/dist/tools/catalog.js.map +1 -1
- package/dist/tools/commands.d.ts +1 -1
- package/dist/tools/commands.js +44 -11
- package/dist/tools/commands.js.map +1 -1
- package/dist/tools/define.js +3 -2
- package/dist/tools/define.js.map +1 -1
- package/dist/tools/dispatcher.d.ts +53 -6
- package/dist/tools/dispatcher.js +277 -108
- package/dist/tools/dispatcher.js.map +1 -1
- package/dist/tools/editor/compile.js +1 -1
- package/dist/tools/editor/compile.js.map +1 -1
- package/dist/tools/editor/play-mode-status.d.ts +3 -0
- package/dist/tools/editor/play-mode-status.js +12 -0
- package/dist/tools/editor/play-mode-status.js.map +1 -0
- package/dist/tools/editor/readiness.d.ts +0 -1
- package/dist/tools/editor/readiness.js +0 -1
- package/dist/tools/editor/readiness.js.map +1 -1
- package/dist/tools/editor/refresh.d.ts +7 -3
- package/dist/tools/editor/refresh.js +20 -15
- package/dist/tools/editor/refresh.js.map +1 -1
- package/dist/tools/editor/state.d.ts +2 -2
- package/dist/tools/editor/state.js +13 -12
- package/dist/tools/editor/state.js.map +1 -1
- package/dist/tools/metadata/derived.js +1 -7
- package/dist/tools/metadata/derived.js.map +1 -1
- package/dist/tools/metadata/status.js +1 -12
- package/dist/tools/metadata/status.js.map +1 -1
- package/dist/tools/metadata/types.d.ts +0 -2
- package/dist/tools/navmesh/live.js +1 -1
- package/dist/tools/navmesh/live.js.map +1 -1
- package/dist/tools/occlusion/live.js +1 -1
- package/dist/tools/occlusion/live.js.map +1 -1
- package/dist/tools/package/deploy.js +2 -2
- package/dist/tools/package/deploy.js.map +1 -1
- package/dist/tools/package/embed.js +1 -1
- package/dist/tools/package/embed.js.map +1 -1
- package/dist/tools/package/install-status.d.ts +3 -0
- package/dist/tools/package/install-status.js +12 -0
- package/dist/tools/package/install-status.js.map +1 -0
- package/dist/tools/package/install.js +3 -3
- package/dist/tools/package/install.js.map +1 -1
- package/dist/tools/package/live-resolve.js +45 -12
- package/dist/tools/package/live-resolve.js.map +1 -1
- package/dist/tools/package/registry-clear.js.map +1 -1
- package/dist/tools/package/registry-set.js.map +1 -1
- package/dist/tools/package/remove-status.d.ts +3 -0
- package/dist/tools/package/remove-status.js +12 -0
- package/dist/tools/package/remove-status.js.map +1 -0
- package/dist/tools/package/remove.js +3 -3
- package/dist/tools/package/remove.js.map +1 -1
- package/dist/tools/package/resolve.js +3 -3
- package/dist/tools/package/resolve.js.map +1 -1
- package/dist/tools/package/restore.js +2 -2
- package/dist/tools/package/restore.js.map +1 -1
- package/dist/tools/package/scoped-registry-add.js.map +1 -1
- package/dist/tools/status/support.d.ts +13 -0
- package/dist/tools/status/support.js +28 -0
- package/dist/tools/status/support.js.map +1 -0
- package/dist/tools/test/cancel.js +2 -2
- package/dist/tools/test/cancel.js.map +1 -1
- package/dist/tools/test/run-status.d.ts +3 -0
- package/dist/tools/test/run-status.js +12 -0
- package/dist/tools/test/run-status.js.map +1 -0
- package/dist/tools/test/run.js +7 -4
- package/dist/tools/test/run.js.map +1 -1
- package/dist/tools/tools/group-summaries.js +10 -14
- package/dist/tools/tools/group-summaries.js.map +1 -1
- package/docs/architecture.md +4 -4
- package/docs/availability-differences.md +4 -4
- package/docs/command-execution.md +4 -3
- package/docs/error-codes.md +13 -5
- package/docs/extensions.md +1 -7
- package/docs/risk-and-side-effects.md +4 -3
- package/docs/safety-boundaries.md +3 -1
- package/docs/target-and-instance.md +1 -1
- package/docs/tool-discovery.md +17 -10
- package/docs/tool-execution.md +18 -7
- package/extensions/cinemachine/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
- package/extensions/hdrp/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
- package/extensions/probuilder/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
- package/extensions/urp/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
- package/extensions/vfx-graph/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
- package/package.json +2 -4
- package/skills/unitycli/SKILL.md +35 -42
- package/skills/unitycli/manifest.json +1 -1
- package/skills/unitycli/references/operation-protocol.md +3 -3
- package/unitypkg/Editor/AssemblyInfo.cs +5 -0
- package/unitypkg/Editor/Bridge/BridgeCommandRegistry.cs +49 -14
- package/unitypkg/Editor/Bridge/BridgeHttpServer.cs +9 -35
- package/unitypkg/Editor/Bridge/BridgeQueueState.cs +672 -0
- package/unitypkg/Editor/Bridge/BridgeWorkQueue.cs +970 -120
- package/unitypkg/Editor/BridgeStartupStateMachine.cs +59 -1
- package/unitypkg/Editor/Commands/ProjectToolCommands.cs +4 -0
- package/unitypkg/Editor/Commands/RuntimeToolCommands.cs +3 -0
- package/unitypkg/Editor/Generated/UnityCliToolCompletionRegistry.g.cs +379 -14
- package/unitypkg/Editor/Infrastructure/CodeExecSupport.cs +28 -13
- package/unitypkg/Editor/Infrastructure/RegistryOwnershipResolver.cs +132 -105
- package/unitypkg/Editor/Infrastructure/UnityCliActiveOperation.cs +138 -29
- package/unitypkg/Editor/Infrastructure/UnityCliAdmission.cs +1856 -237
- package/unitypkg/Editor/Infrastructure/UnityCliDoctorDiagnostics.cs +220 -0
- package/unitypkg/Editor/Infrastructure/UnityCliDurableStatusCoordinator.cs +118 -0
- package/unitypkg/Editor/Infrastructure/UnityCliEditorStateSnapshot.cs +2 -1
- package/unitypkg/Editor/Infrastructure/UnityCliHierarchyPager.cs +203 -0
- package/unitypkg/Editor/Infrastructure/UnityCliInstanceCleanup.cs +2 -1
- package/unitypkg/Editor/Infrastructure/UnityCliInstanceRegistry.cs +201 -63
- package/unitypkg/Editor/Infrastructure/UnityCliJson.cs +2 -3
- package/unitypkg/Editor/Infrastructure/UnityCliLiveBuildCoordinator.cs +289 -46
- package/unitypkg/Editor/Infrastructure/UnityCliLiveRefreshCoordinator.cs +87 -53
- package/unitypkg/Editor/Infrastructure/UnityCliNavMeshBakeCoordinator.cs +4 -4
- package/unitypkg/Editor/Infrastructure/UnityCliObjectRef.cs +128 -25
- package/unitypkg/Editor/Infrastructure/UnityCliObjectSerializer.cs +435 -327
- package/unitypkg/Editor/Infrastructure/UnityCliOcclusionBakeCoordinator.cs +4 -4
- package/unitypkg/Editor/Infrastructure/UnityCliOperationCoordinator.cs +351 -53
- package/unitypkg/Editor/Infrastructure/UnityCliPackageResolveCoordinator.cs +118 -18
- package/unitypkg/Editor/Infrastructure/UnityCliPackageResolveState.cs +20 -1
- package/unitypkg/Editor/Infrastructure/UnityCliReadGeneration.cs +97 -0
- package/unitypkg/Editor/Infrastructure/UnityCliReflection.cs +644 -133
- package/unitypkg/Editor/Infrastructure/UnityCliReloadOperation.cs +128 -41
- package/unitypkg/Editor/Infrastructure/UnityCliRuntimeExecutor.cs +14 -84
- package/unitypkg/Editor/Infrastructure/UnityCliSerializer.cs +220 -0
- package/unitypkg/Editor/Infrastructure/UnityCliSessionMutationLease.cs +69 -29
- package/unitypkg/Editor/Infrastructure/UnityCliTestRunCoordinator.cs +440 -54
- package/unitypkg/Editor/Infrastructure/UnityCliWaiter.cs +128 -35
- package/unitypkg/Editor/ProjectTools/IUnityCliTool.cs +9 -1
- package/unitypkg/Editor/ProjectTools/OfficialAttributeToolRegistry.cs +32 -31
- package/unitypkg/Editor/ProjectTools/ProjectToolRegistry.cs +607 -458
- package/unitypkg/Editor/ProjectTools/ProjectToolSchemaValidator.cs +35 -7
- package/unitypkg/Editor/ProjectTools/RegisteredTool.cs +501 -60
- package/unitypkg/Editor/ProjectTools/UnityCliProjectToolContext.cs +21 -0
- package/unitypkg/Editor/ProjectTools/UnityCliSchemaDeriver.cs +36 -7
- package/unitypkg/Editor/ProjectTools/UnityCliToolName.cs +30 -0
- package/unitypkg/Editor/Properties/AssemblyInfo.cs +3 -0
- package/unitypkg/Editor/Tools/asset/get-data.cs +16 -7
- package/unitypkg/Editor/Tools/build/BuildSceneStateTool.cs +23 -3
- package/unitypkg/Editor/Tools/build/BuildToolDtos.cs +2 -1
- package/unitypkg/Editor/Tools/build/scene/add.cs +6 -3
- package/unitypkg/Editor/Tools/build/scene/disable.cs +1 -1
- package/unitypkg/Editor/Tools/build/scene/enable.cs +1 -1
- package/unitypkg/Editor/Tools/build/scene/list.cs +1 -1
- package/unitypkg/Editor/Tools/build/scene/remove.cs +13 -3
- package/unitypkg/Editor/Tools/camera/CameraToolSupport.cs +38 -38
- package/unitypkg/Editor/Tools/camera/multiview.cs +13 -6
- package/unitypkg/Editor/Tools/camera/screenshot.cs +14 -7
- package/unitypkg/Editor/Tools/component/get.cs +16 -9
- package/unitypkg/Editor/Tools/component/modify.cs +6 -4
- package/unitypkg/Editor/Tools/console/clear.cs +7 -6
- package/unitypkg/Editor/Tools/console/get.cs +48 -16
- package/unitypkg/Editor/Tools/editor/EditorToolSupport.cs +143 -28
- package/unitypkg/Editor/Tools/editor/menu-items.cs +1 -1
- package/unitypkg/Editor/Tools/editor/play.cs +2 -11
- package/unitypkg/Editor/Tools/editor/selection.cs +1 -1
- package/unitypkg/Editor/Tools/editor/stop.cs +2 -11
- package/unitypkg/Editor/Tools/editor/windows.cs +1 -1
- package/unitypkg/Editor/Tools/go/GameObjectToolSupport.cs +54 -4
- package/unitypkg/Editor/Tools/go/delete.cs +1 -1
- package/unitypkg/Editor/Tools/go/find.cs +167 -70
- package/unitypkg/Editor/Tools/graphics/GraphicsToolSupport.cs +25 -0
- package/unitypkg/Editor/Tools/graphics/light-probe-group.cs +9 -3
- package/unitypkg/Editor/Tools/graphics/pipeline-info.cs +1 -1
- package/unitypkg/Editor/Tools/graphics/pipeline-settings.cs +1 -1
- package/unitypkg/Editor/Tools/graphics/quality.cs +2 -2
- package/unitypkg/Editor/Tools/graphics/reflection-probe.cs +3 -3
- package/unitypkg/Editor/Tools/graphics/stats.cs +1 -1
- package/unitypkg/Editor/Tools/object/ObjectToolSupport.cs +4 -4
- package/unitypkg/Editor/Tools/object/get-data.cs +9 -7
- package/unitypkg/Editor/Tools/object/modify.cs +7 -5
- package/unitypkg/Editor/Tools/physics/raycast.cs +24 -6
- package/unitypkg/Editor/Tools/prefab/stage.cs +1 -1
- package/unitypkg/Editor/Tools/profiler/ProfilerFrameDebuggerSupport.cs +94 -21
- package/unitypkg/Editor/Tools/profiler/ProfilerMemorySnapshotSupport.cs +397 -10
- package/unitypkg/Editor/Tools/profiler/ProfilerToolSupport.cs +18 -4
- package/unitypkg/Editor/Tools/profiler/frame-debugger/enable.cs +1 -1
- package/unitypkg/Editor/Tools/profiler/frame-debugger/events.cs +88 -87
- package/unitypkg/Editor/Tools/profiler/hierarchy.cs +278 -112
- package/unitypkg/Editor/Tools/profiler/memory-snapshot/compare.cs +9 -5
- package/unitypkg/Editor/Tools/profiler/memory-snapshot/list.cs +37 -35
- package/unitypkg/Editor/Tools/profiler/memory-snapshot/take.cs +99 -97
- package/unitypkg/Editor/Tools/project/info.cs +1 -1
- package/unitypkg/Editor/Tools/project/layers.cs +1 -1
- package/unitypkg/Editor/Tools/project/tags.cs +1 -1
- package/unitypkg/Editor/Tools/reflection/method/call.cs +9 -7
- package/unitypkg/Editor/Tools/reflection/method/find.cs +27 -37
- package/unitypkg/Editor/Tools/reflection/type/schema.cs +24 -28
- package/unitypkg/Editor/Tools/scene/gameobject/component.cs +8 -5
- package/unitypkg/Editor/Tools/scene/gameobject/components.cs +10 -7
- package/unitypkg/Editor/Tools/scene/gameobject-support.cs +104 -8
- package/unitypkg/Editor/Tools/scene/gameobject.cs +37 -16
- package/unitypkg/Editor/Tools/scene/hierarchy.cs +146 -142
- package/unitypkg/Editor/Tools/screenshot/EditorElementCaptureToolSupport.cs +77 -25
- package/unitypkg/Editor/Tools/screenshot/ScreenshotToolSupport.cs +315 -95
- package/unitypkg/Editor/Tools/screenshot/editor-element.cs +7 -5
- package/unitypkg/Editor/Tools/screenshot/game-view.cs +35 -21
- package/unitypkg/Editor/Tools/screenshot/isolated-object.cs +29 -23
- package/unitypkg/Editor/Tools/screenshot/orbit.cs +37 -25
- package/unitypkg/Editor/Tools/screenshot/scene-view.cs +25 -18
- package/unitypkg/Editor/Tools/screenshot/screen.cs +177 -17
- package/unitypkg/Editor/Tools/screenshot/surround.cs +83 -79
- package/unitypkg/Editor/Tools/ui/render-preview.cs +16 -11
- package/unitypkg/Editor/UnityCliBridge.cs +97 -34
- package/unitypkg/Runtime/Infrastructure/UnityCliRuntimeBridge.cs +753 -122
- package/unitypkg/Runtime/Properties/AssemblyInfo.cs +5 -0
- package/unitypkg/Runtime/UnityCli.Runtime.asmdef +1 -1
- package/unitypkg/Shared/AssemblyInfo.cs +4 -0
- package/unitypkg/Shared/UnityCliAdmissionPool.cs +265 -75
- package/unitypkg/Shared/UnityCliBridgeQueuePolicy.cs +410 -0
- package/unitypkg/Shared/UnityCliBridgeWorkItemState.cs +47 -0
- package/unitypkg/Shared/UnityCliExceptionProjection.cs +151 -0
- package/unitypkg/Shared/UnityCliQueuePublication.cs +30 -0
- package/unitypkg/Shared/UnityCliRequestBudget.cs +352 -0
- package/unitypkg/Shared/UnityCliRuntimeRequestPublication.cs +107 -0
- package/unitypkg/Tests/Editor/UnityCli.Editor.Tests.asmdef +2 -2
- package/unitypkg/Tests/Editor/UnityCliAdmissionCoordinatorIntegrationTests.cs +1329 -0
- package/unitypkg/Tests/Editor/UnityCliAdmissionPoolTests.cs +311 -114
- package/unitypkg/Tests/Editor/UnityCliAdmissionRepairStateTests.cs +94 -0
- package/unitypkg/Tests/Editor/UnityCliBridgeAdmissionIntegrationTests.cs +1229 -0
- package/unitypkg/Tests/Editor/UnityCliBridgeQueuePolicyTests.cs +412 -0
- package/unitypkg/Tests/Editor/UnityCliContinuationIntegrationTests.cs +292 -0
- package/unitypkg/Tests/Editor/UnityCliDiagnosticBoundaryTests.cs +120 -0
- package/unitypkg/Tests/Editor/UnityCliDurableStatusTests.cs +84 -0
- package/unitypkg/Tests/Editor/UnityCliExceptionProjectionTests.cs +72 -0
- package/unitypkg/Tests/Editor/UnityCliInstancePublicationRetryTests.cs +180 -97
- package/unitypkg/Tests/Editor/UnityCliMemoryReflectionIntegrationTests.cs +434 -0
- package/unitypkg/Tests/Editor/UnityCliRequestBudgetTests.cs +615 -0
- package/unitypkg/Tests/Editor/UnityCliRuntimeBridgeIntegrationTests.cs +520 -0
- package/unitypkg/Tests/Editor/UnityCliToolCatalogTests.cs +562 -0
- package/unitypkg/Tests/Runtime/UnityCli.Runtime.Tests.asmdef +23 -0
- package/unitypkg/Tests/Runtime/UnityCliRuntimeCatalogValidationTests.cs +29 -0
- package/unitypkg/package.json +1 -1
- package/contracts/static-capability-catalog.json +0 -48226
- package/dist/bridge/domains/diagnose.d.ts +0 -2
- package/dist/bridge/domains/diagnose.js +0 -11
- package/dist/bridge/domains/diagnose.js.map +0 -1
- package/dist/bridge/domains/extension.d.ts +0 -8
- package/dist/bridge/domains/extension.js +0 -20
- package/dist/bridge/domains/extension.js.map +0 -1
- package/dist/capabilities.d.ts +0 -5
- package/dist/capabilities.js +0 -84
- package/dist/capabilities.js.map +0 -1
- package/dist/static-capability-catalog.d.ts +0 -13
- package/dist/static-capability-catalog.js +0 -41
- package/dist/static-capability-catalog.js.map +0 -1
- package/dist/tools/diagnose/capabilities.d.ts +0 -13
- package/dist/tools/diagnose/capabilities.js +0 -34
- package/dist/tools/diagnose/capabilities.js.map +0 -1
- package/dist/tools/package/info.d.ts +0 -19
- package/dist/tools/package/info.js +0 -27
- package/dist/tools/package/info.js.map +0 -1
- package/dist/tools/package/list.d.ts +0 -27
- package/dist/tools/package/list.js +0 -21
- package/dist/tools/package/list.js.map +0 -1
- package/dist/tools/package/registry-get.d.ts +0 -10
- package/dist/tools/package/registry-get.js +0 -22
- package/dist/tools/package/registry-get.js.map +0 -1
- package/dist/tools/package/scoped-registry-list.d.ts +0 -14
- package/dist/tools/package/scoped-registry-list.js +0 -22
- package/dist/tools/package/scoped-registry-list.js.map +0 -1
- package/dist/tools/package/search.d.ts +0 -17
- package/dist/tools/package/search.js +0 -39
- package/dist/tools/package/search.js.map +0 -1
- package/dist/tools/package/status.d.ts +0 -21
- package/dist/tools/package/status.js +0 -37
- package/dist/tools/package/status.js.map +0 -1
- package/dist/tools/tools/describe.d.ts +0 -27
- package/dist/tools/tools/describe.js +0 -35
- package/dist/tools/tools/describe.js.map +0 -1
- package/dist/tools/tools/group-list.d.ts +0 -16
- package/dist/tools/tools/group-list.js +0 -38
- package/dist/tools/tools/group-list.js.map +0 -1
- package/dist/tools/tools/group.d.ts +0 -13
- package/dist/tools/tools/group.js +0 -34
- package/dist/tools/tools/group.js.map +0 -1
- package/dist/tools/tools/pagination.d.ts +0 -17
- package/dist/tools/tools/pagination.js +0 -28
- package/dist/tools/tools/pagination.js.map +0 -1
- package/dist/tools/tools/search.d.ts +0 -15
- package/dist/tools/tools/search.js +0 -42
- package/dist/tools/tools/search.js.map +0 -1
- package/unitypkg/Editor/Tools/diagnose/preflight.cs +0 -269
- package/unitypkg/Editor/Tools/editor/console.cs +0 -26
- package/unitypkg/Editor/Tools/extension/status.cs +0 -87
- package/unitypkg/Editor/Tools/go/inspect.cs +0 -49
- package/unitypkg/Editor/Tools/graphics/pipeline-quality.cs +0 -24
- package/unitypkg/Editor/Tools/graphics/probe-positions.cs +0 -58
- package/unitypkg/Editor/Tools/graphics/settings.cs +0 -104
- package/unitypkg/Editor/Tools/graphics/stats-counters.cs +0 -28
- package/unitypkg/Editor/Tools/graphics/stats-memory.cs +0 -21
- package/unitypkg/Editor/Tools/instances/list.cs +0 -71
- package/unitypkg/Editor/Tools/physics/raycast-all.cs +0 -67
- package/unitypkg/Editor/Tools/scene/build-settings.cs +0 -69
- package/unitypkg/Editor/Tools/scene/data.cs +0 -125
- package/unitypkg/Editor/Tools/tests/list.cs +0 -98
|
@@ -3,8 +3,10 @@ using System;
|
|
|
3
3
|
using System.Collections;
|
|
4
4
|
using System.Collections.Generic;
|
|
5
5
|
using System.Globalization;
|
|
6
|
+
using System.IO;
|
|
6
7
|
using System.Linq;
|
|
7
8
|
using System.Net;
|
|
9
|
+
using System.Text;
|
|
8
10
|
using UnityEditor;
|
|
9
11
|
|
|
10
12
|
namespace UnityCli
|
|
@@ -12,294 +14,1767 @@ namespace UnityCli
|
|
|
12
14
|
public static partial class UnityCliBridge
|
|
13
15
|
{
|
|
14
16
|
private const string AdmissionSessionStateKey = "UnityCli.AdmissionPool.State";
|
|
15
|
-
private
|
|
17
|
+
private const string AdmissionSettlementScope = "admission-settlement";
|
|
18
|
+
private const int AdmissionPublicationMutexTimeoutMs = 5000;
|
|
19
|
+
private const int MaxCancelRecoveryEnvelopeBytes = 1536 * 1024;
|
|
20
|
+
private static long admissionPoolPublicationRevision;
|
|
21
|
+
private static string admissionPoolPublicationObserved = string.Empty;
|
|
22
|
+
private static readonly string[] AdmissionDurableScopes = { "compile", "package-resolve", "refresh", "asset-refresh", "package-embed", "test-run", "test-cancel", "build", "navmesh-bake", "occlusion-bake", "direct-mutation" };
|
|
23
|
+
private static UnityCliAdmissionPool admissionPool
|
|
24
|
+
{
|
|
25
|
+
get { return ProductionQueueState.AdmissionPool; }
|
|
26
|
+
set { ProductionQueueState.AdmissionPool = value; }
|
|
27
|
+
}
|
|
16
28
|
|
|
17
29
|
internal sealed class AdmissionOwnerIdentity
|
|
18
30
|
{
|
|
19
|
-
internal string RequestId;
|
|
20
|
-
internal string
|
|
21
|
-
internal string
|
|
22
|
-
internal string
|
|
23
|
-
internal
|
|
31
|
+
internal string RequestId;
|
|
32
|
+
internal string RequestDigest;
|
|
33
|
+
internal string TopRequestDigest;
|
|
34
|
+
internal string RootFingerprint;
|
|
35
|
+
internal string Token;
|
|
36
|
+
internal string SessionId;
|
|
37
|
+
internal string ListenerId;
|
|
38
|
+
internal long ListenerGeneration;
|
|
39
|
+
internal long ExecutionDeadlineMs;
|
|
40
|
+
internal string EditorSessionId;
|
|
41
|
+
internal UnityCliAdmissionPool.AdmissionOwnerState OwnerState;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private enum AdmissionTransferResult
|
|
45
|
+
{
|
|
46
|
+
NotAccepted,
|
|
47
|
+
Transferred,
|
|
48
|
+
TransferPending
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// <summary>队列publication失败时只补偿本次新消费;attach不拥有撤销权。</summary>
|
|
52
|
+
internal sealed class AdmissionQueueReservation
|
|
53
|
+
{
|
|
54
|
+
internal string RequestId;
|
|
55
|
+
internal string RequestDigest;
|
|
56
|
+
internal string RootFingerprint;
|
|
57
|
+
internal string Token;
|
|
58
|
+
internal long ExecutionDeadlineMs;
|
|
59
|
+
internal bool HasAdmissionHeaders;
|
|
60
|
+
internal bool HasExactTerminal;
|
|
61
|
+
internal bool IsBypass;
|
|
62
|
+
internal AdmissionOwnerIdentity Owner;
|
|
63
|
+
internal bool NewlyConsumed;
|
|
64
|
+
internal bool QueuePublished;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// <summary>按当前Editor session与项目配置建立唯一准入池,并恢复同启动内的consumed roots。</summary>
|
|
68
|
+
private static void InitializeAdmissionPool()
|
|
69
|
+
{
|
|
70
|
+
if (admissionPool != null) return;
|
|
71
|
+
var settings = UnityCliProjectSettings.Current;
|
|
72
|
+
admissionPool = new UnityCliAdmissionPool(InstanceId(), EditorSessionId(), settings.TaskPoolMaxActive, settings.TaskPoolReopenAt);
|
|
73
|
+
ProductionQueueState.ListenerId = ListenerId;
|
|
74
|
+
ProductionQueueState.ListenerGeneration = Math.Max(1, CurrentListenerGeneration());
|
|
75
|
+
string serialized;
|
|
76
|
+
try { serialized = LoadProductionAdmissionPoolState(); }
|
|
77
|
+
catch (Exception exception)
|
|
78
|
+
{
|
|
79
|
+
admissionPool.BlockRecovery();
|
|
80
|
+
try { EraseProductionAdmissionPoolState(); } catch { }
|
|
81
|
+
WriteLog("admission_publication_load_failed " + exception.GetType().Name + ": " + exception.Message);
|
|
82
|
+
ReconcileOrphanAdmissionOwnerMarkers();
|
|
83
|
+
ReconcilePendingAdmissionSettlements();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (string.IsNullOrEmpty(serialized))
|
|
87
|
+
{
|
|
88
|
+
ReconcileOrphanAdmissionOwnerMarkers();
|
|
89
|
+
ReconcilePendingAdmissionSettlements();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try
|
|
93
|
+
{
|
|
94
|
+
var state = AsDictionary(UnityCliSerializer.Parse(serialized));
|
|
95
|
+
if (state == null || AdmissionText(state, "editorSessionId") != EditorSessionId())
|
|
96
|
+
{
|
|
97
|
+
EraseProductionAdmissionPoolState();
|
|
98
|
+
ReconcileOrphanAdmissionOwnerMarkers();
|
|
99
|
+
ReconcilePendingAdmissionSettlements();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
var records = new List<UnityCliAdmissionPool.AdmissionRecord>();
|
|
103
|
+
object rawRecords;
|
|
104
|
+
var values = state.TryGetValue("records", out rawRecords) ? rawRecords as IEnumerable : null;
|
|
105
|
+
if (values == null) throw new InvalidOperationException("Admission SessionState records are missing.");
|
|
106
|
+
foreach (var value in values)
|
|
107
|
+
{
|
|
108
|
+
var record = AsDictionary(value);
|
|
109
|
+
if (record == null) throw new InvalidOperationException("Admission SessionState record is invalid.");
|
|
110
|
+
records.Add(new UnityCliAdmissionPool.AdmissionRecord
|
|
111
|
+
{
|
|
112
|
+
InstanceId = AdmissionText(record, "instanceId"),
|
|
113
|
+
SessionId = AdmissionText(record, "sessionId"),
|
|
114
|
+
RequestId = AdmissionText(record, "requestId"),
|
|
115
|
+
RootFingerprint = AdmissionText(record, "rootFingerprint"),
|
|
116
|
+
RequestDigest = AdmissionText(record, "requestDigest"),
|
|
117
|
+
Token = AdmissionText(record, "token"),
|
|
118
|
+
ListenerId = AdmissionText(record, "listenerId"),
|
|
119
|
+
ListenerGeneration = AdmissionLong(record, "listenerGeneration"),
|
|
120
|
+
GrantExpiresAtMs = AdmissionLong(record, "grantExpiresAtMs"),
|
|
121
|
+
ExecutionDeadlineMs = AdmissionLong(record, "executionDeadlineMs"),
|
|
122
|
+
OwnerState = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(record, "ownerState"),
|
|
123
|
+
IntendedOwnerState = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(record, "intendedOwnerState"),
|
|
124
|
+
TransferCommitted = AdmissionBool(record, "transferCommitted")
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
var revision = AdmissionLong(state, "publicationRevision");
|
|
128
|
+
if (revision < 1L || revision != admissionPoolPublicationRevision)
|
|
129
|
+
throw new InvalidOperationException("Admission SessionState publication revision is invalid.");
|
|
130
|
+
admissionPool.Restore(records, checked((int)AdmissionLong(state, "activeCount")), AdmissionBool(state, "gateClosed"), RequiredText(state, "recoveryDigest", 64), checked((int)AdmissionLong(state, "configuredMaxActive")), checked((int)AdmissionLong(state, "configuredReopenAt")));
|
|
131
|
+
ReconcileOrphanAdmissionOwnerMarkers();
|
|
132
|
+
ReconcilePendingAdmissionSettlements();
|
|
133
|
+
}
|
|
134
|
+
catch (Exception exception)
|
|
135
|
+
{
|
|
136
|
+
admissionPool.BlockRecovery();
|
|
137
|
+
try { EraseProductionAdmissionPoolState(); } catch { }
|
|
138
|
+
WriteLog("admission_restore_failed " + exception.GetType().Name + ": " + exception.Message);
|
|
139
|
+
ReconcileOrphanAdmissionOwnerMarkers();
|
|
140
|
+
ReconcilePendingAdmissionSettlements();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// <summary>reload前撤销未消费grant,只持久化同Editor启动内的有界owner state。</summary>
|
|
145
|
+
private static void SaveAdmissionPoolBeforeReload()
|
|
146
|
+
{
|
|
147
|
+
if (admissionPool == null) return;
|
|
148
|
+
admissionPool.RevokeUnconsumed(ListenerId, CurrentListenerGeneration());
|
|
149
|
+
PersistAdmissionPoolState();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// <summary>单个SessionState键保存最多maxActive条;新Editor启动因session不匹配整体作废。</summary>
|
|
153
|
+
private static bool PersistAdmissionPoolState(BridgeQueueState state = null)
|
|
154
|
+
{
|
|
155
|
+
state = state ?? ProductionQueueState;
|
|
156
|
+
if (state == null || state.AdmissionPool == null) return true;
|
|
157
|
+
if (!object.ReferenceEquals(state, ProductionQueueState)) return state.PersistPool();
|
|
158
|
+
return PersistProductionAdmissionPoolState();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private static bool PersistProductionAdmissionPoolState()
|
|
162
|
+
{
|
|
163
|
+
try
|
|
164
|
+
{
|
|
165
|
+
return WithAdmissionPublicationLock(() =>
|
|
166
|
+
{
|
|
167
|
+
if (admissionPool == null) return true;
|
|
168
|
+
var current = SessionState.GetString(AdmissionSessionStateKey, string.Empty);
|
|
169
|
+
var currentRevision = AdmissionPoolPublicationRevision(current);
|
|
170
|
+
if (currentRevision != admissionPoolPublicationRevision
|
|
171
|
+
|| !string.Equals(current, admissionPoolPublicationObserved, StringComparison.Ordinal)) return false;
|
|
172
|
+
var snapshot = admissionPool.CaptureRecovery();
|
|
173
|
+
var records = snapshot.Records.Select(record => (object)new Dictionary<string, object>
|
|
174
|
+
{
|
|
175
|
+
{ "instanceId", record.InstanceId }, { "sessionId", record.SessionId },
|
|
176
|
+
{ "requestId", record.RequestId }, { "rootFingerprint", record.RootFingerprint }, { "requestDigest", record.RequestDigest }, { "token", record.Token },
|
|
177
|
+
{ "listenerId", record.ListenerId }, { "listenerGeneration", record.ListenerGeneration },
|
|
178
|
+
{ "grantExpiresAtMs", record.GrantExpiresAtMs }, { "executionDeadlineMs", record.ExecutionDeadlineMs },
|
|
179
|
+
{ "ownerState", (long)record.OwnerState }, { "intendedOwnerState", (long)record.IntendedOwnerState },
|
|
180
|
+
{ "transferCommitted", record.TransferCommitted }
|
|
181
|
+
}).ToList();
|
|
182
|
+
var nextRevision = checked(currentRevision + 1L);
|
|
183
|
+
var serialized = UnityCliSerializer.Serialize(new Dictionary<string, object>
|
|
184
|
+
{
|
|
185
|
+
{ "editorSessionId", EditorSessionId() }, { "publicationRevision", nextRevision },
|
|
186
|
+
{ "gateClosed", snapshot.GateClosed }, { "records", records },
|
|
187
|
+
{ "activeCount", (long)records.Count }, { "recoveryDigest", snapshot.RecoveryDigest },
|
|
188
|
+
{ "configuredMaxActive", (long)snapshot.ConfiguredMaxActive }, { "configuredReopenAt", (long)snapshot.ReopenAt }
|
|
189
|
+
});
|
|
190
|
+
SessionState.SetString(AdmissionSessionStateKey, serialized);
|
|
191
|
+
var confirmed = SessionState.GetString(AdmissionSessionStateKey, string.Empty);
|
|
192
|
+
if (!string.Equals(confirmed, serialized, StringComparison.Ordinal)) return false;
|
|
193
|
+
admissionPoolPublicationRevision = nextRevision;
|
|
194
|
+
admissionPoolPublicationObserved = serialized;
|
|
195
|
+
return true;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch { return false; }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// <summary>pool snapshot与completion journal共享同一current-user mutex;锁内不进入QueueLock。</summary>
|
|
202
|
+
private static T WithAdmissionPublicationLock<T>(Func<T> action)
|
|
203
|
+
{
|
|
204
|
+
if (action == null) throw new ArgumentNullException("action");
|
|
205
|
+
T result = default(T);
|
|
206
|
+
WithCurrentUserMutex("Local\\UnityCli.AdmissionPublication." + InstanceId(), AdmissionPublicationMutexTimeoutMs, () => { result = action(); });
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private static string LoadProductionAdmissionPoolState()
|
|
211
|
+
{
|
|
212
|
+
return WithAdmissionPublicationLock(() =>
|
|
213
|
+
{
|
|
214
|
+
var serialized = SessionState.GetString(AdmissionSessionStateKey, string.Empty);
|
|
215
|
+
admissionPoolPublicationRevision = AdmissionPoolPublicationRevision(serialized);
|
|
216
|
+
admissionPoolPublicationObserved = serialized;
|
|
217
|
+
return serialized;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private static void EraseProductionAdmissionPoolState()
|
|
222
|
+
{
|
|
223
|
+
WithAdmissionPublicationLock(() =>
|
|
224
|
+
{
|
|
225
|
+
SessionState.EraseString(AdmissionSessionStateKey);
|
|
226
|
+
admissionPoolPublicationRevision = 0L;
|
|
227
|
+
admissionPoolPublicationObserved = string.Empty;
|
|
228
|
+
return true;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private static long AdmissionPoolPublicationRevision(string serialized)
|
|
233
|
+
{
|
|
234
|
+
if (string.IsNullOrEmpty(serialized)) return 0L;
|
|
235
|
+
var value = AsDictionary(UnityCliSerializer.Parse(serialized));
|
|
236
|
+
if (value == null || !string.Equals(AdmissionText(value, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)) return 0L;
|
|
237
|
+
var revision = AdmissionLong(value, "publicationRevision");
|
|
238
|
+
if (revision < 1L) throw new InvalidDataException("Admission publication revision is missing or invalid.");
|
|
239
|
+
return revision;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// <summary>重启后把固定journal恢复进同session在线settlement;只完成准入,不重新执行业务effect。</summary>
|
|
243
|
+
private static void ReconcilePendingAdmissionSettlements()
|
|
244
|
+
{
|
|
245
|
+
if (admissionPool == null) return;
|
|
246
|
+
Dictionary<string, object> root;
|
|
247
|
+
try { root = LoadProductionAdmissionSettlement(); }
|
|
248
|
+
catch { admissionPool.BlockRecovery(); return; }
|
|
249
|
+
if (root == null) return;
|
|
250
|
+
var entries = root.ContainsKey("entries") ? root["entries"] as List<object> : null;
|
|
251
|
+
if (entries == null || entries.Count > 256)
|
|
252
|
+
{
|
|
253
|
+
admissionPool.BlockRecovery();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
foreach (var raw in entries.ToArray())
|
|
257
|
+
{
|
|
258
|
+
var owner = ReadCompletionIntentOwner(AsDictionary(raw));
|
|
259
|
+
if (owner == null || !HasAdmissionCompletionIntent(ProductionQueueState, owner))
|
|
260
|
+
{
|
|
261
|
+
admissionPool.BlockRecovery();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
ProductionQueueState.TrackCompletionIntent(owner);
|
|
265
|
+
}
|
|
266
|
+
RetryPendingAdmissionCompletionIntents(ProductionQueueState);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// <summary>同session每tick固定批次推进pool、marker与intent三者收敛;失败保持journal并固定退避。</summary>
|
|
270
|
+
internal static void RetryPendingAdmissionCompletionIntents(BridgeQueueState state)
|
|
271
|
+
{
|
|
272
|
+
if (state == null || state.AdmissionPool == null) return;
|
|
273
|
+
var now = state.NowMs();
|
|
274
|
+
var due = state.DueCompletionIntents(now, UnityCliBridgeQueuePolicy.MaxStartsPerTick);
|
|
275
|
+
foreach (var pending in due)
|
|
276
|
+
{
|
|
277
|
+
var owner = pending.Owner;
|
|
278
|
+
var pool = state.AdmissionPool;
|
|
279
|
+
if (pending.Compensation)
|
|
280
|
+
{
|
|
281
|
+
try
|
|
282
|
+
{
|
|
283
|
+
if ((!pending.IntentPersisted && !PersistAdmissionCompletionIntent(state, owner))
|
|
284
|
+
|| !pool.CompensateAuthorization(ToPoolIdentity(owner))
|
|
285
|
+
|| !state.PersistPool()
|
|
286
|
+
|| !ClearAdmissionCompletionIntent(state, owner))
|
|
287
|
+
throw new IOException("Admission compensation continuation is not durable yet.");
|
|
288
|
+
state.Sinks.WriteLog("admission_prequeue_compensation_replayed requestId=" + owner.RequestId);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
catch { }
|
|
292
|
+
}
|
|
293
|
+
else
|
|
294
|
+
{
|
|
295
|
+
UnityCliAdmissionPool.CompletionOutcome outcome;
|
|
296
|
+
var activeWorkOwnsAdmission = false;
|
|
297
|
+
lock (state.QueueLock)
|
|
298
|
+
{
|
|
299
|
+
// 同一root可先describe再attach业务WorkItem;旧intent必须等活动WorkItem完成后再收敛。
|
|
300
|
+
activeWorkOwnsAdmission = ActiveWorkOwnsAdmissionUnderQueueLock(state, owner);
|
|
301
|
+
if (activeWorkOwnsAdmission)
|
|
302
|
+
{
|
|
303
|
+
outcome = UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked;
|
|
304
|
+
}
|
|
305
|
+
else
|
|
306
|
+
{
|
|
307
|
+
var exact = pool.InspectExactIdentityFields(ToPoolIdentity(owner));
|
|
308
|
+
outcome = exact == UnityCliAdmissionPool.ExactIdentityOutcome.ExactCurrent
|
|
309
|
+
? pool.CompleteTrusted(ToPoolIdentity(owner))
|
|
310
|
+
: exact == UnityCliAdmissionPool.ExactIdentityOutcome.DefinitelyAbsent
|
|
311
|
+
? UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent
|
|
312
|
+
: UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (activeWorkOwnsAdmission)
|
|
316
|
+
{
|
|
317
|
+
state.DeferCompletionIntent(pending, now);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (outcome == UnityCliAdmissionPool.CompletionOutcome.ExactCurrentCompleted
|
|
321
|
+
|| outcome == UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent)
|
|
322
|
+
{
|
|
323
|
+
try
|
|
324
|
+
{
|
|
325
|
+
if (state.PersistPool() && state.ClearMarkers(owner) && ClearAdmissionCompletionIntent(state, owner))
|
|
326
|
+
{
|
|
327
|
+
state.Sinks.WriteLog("admission_settlement_replayed requestId=" + owner.RequestId + " outcome=" + outcome.ToString());
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch { }
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
state.DeferCompletionIntent(pending, now);
|
|
335
|
+
if (pending.FailureLogged) continue;
|
|
336
|
+
state.MarkCompletionIntentFailureLogged(pending);
|
|
337
|
+
state.Sinks.WriteLog("admission_settlement_retry_pending requestId=" + owner.RequestId);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private static AdmissionOwnerIdentity ReadCompletionIntentOwner(IDictionary<string, object> entry)
|
|
342
|
+
{
|
|
343
|
+
if (entry == null || !AdmissionDigestIsValid(AdmissionText(entry, "requestDigest"))) return null;
|
|
344
|
+
var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(entry, "admissionOwnerState");
|
|
345
|
+
if (!Enum.IsDefined(typeof(UnityCliAdmissionPool.AdmissionOwnerState), source) || source == UnityCliAdmissionPool.AdmissionOwnerState.Granted) return null;
|
|
346
|
+
var owner = new AdmissionOwnerIdentity
|
|
347
|
+
{
|
|
348
|
+
RequestId = AdmissionText(entry, "requestId"), RequestDigest = AdmissionText(entry, "admissionRequestDigest"), TopRequestDigest = AdmissionText(entry, "requestDigest"),
|
|
349
|
+
RootFingerprint = AdmissionText(entry, "admissionRootFingerprint"), Token = AdmissionText(entry, "admissionToken"), SessionId = AdmissionText(entry, "admissionSessionId"),
|
|
350
|
+
ListenerId = AdmissionText(entry, "admissionListenerId"), ListenerGeneration = AdmissionLong(entry, "admissionListenerGeneration"),
|
|
351
|
+
ExecutionDeadlineMs = AdmissionLong(entry, "admissionExecutionDeadlineMs"), EditorSessionId = AdmissionText(entry, "editorSessionId"), OwnerState = source
|
|
352
|
+
};
|
|
353
|
+
if (!string.Equals(owner.EditorSessionId, EditorSessionId(), StringComparison.Ordinal)) return null;
|
|
354
|
+
try { ToPoolIdentity(owner); }
|
|
355
|
+
catch { return null; }
|
|
356
|
+
return owner;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private static void ShutdownAdmissionPool()
|
|
360
|
+
{
|
|
361
|
+
ShutdownAdmissionPool(ProductionQueueState);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/// <summary>publication关闭后在同一QueueLock内撤销未消费grant并裁决pool是否可分离。</summary>
|
|
365
|
+
private static void ShutdownAdmissionPool(BridgeQueueState state)
|
|
366
|
+
{
|
|
367
|
+
if (state == null) return;
|
|
368
|
+
lock (state.QueueLock)
|
|
369
|
+
{
|
|
370
|
+
state.QueuePublicationOpen = false;
|
|
371
|
+
state.QueueSessionStopping = true;
|
|
372
|
+
var pool = state.AdmissionPool;
|
|
373
|
+
if (pool != null)
|
|
374
|
+
{
|
|
375
|
+
foreach (var grant in pool.Capture().Records.Where(record => record.OwnerState == UnityCliAdmissionPool.AdmissionOwnerState.Granted).ToArray())
|
|
376
|
+
if (!pool.Revoke(grant.RequestId, grant.RootFingerprint, grant.Token)) pool.BlockRecovery();
|
|
377
|
+
}
|
|
378
|
+
if (state.RunningWork.Count != 0 || state.PendingSettlementWork.Count != 0)
|
|
379
|
+
{
|
|
380
|
+
state.AdmissionPoolShutdownPending = true;
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
FinalizeAdmissionPoolShutdown(state);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
internal static void TryFinalizeAdmissionPoolShutdown(BridgeQueueState state)
|
|
388
|
+
{
|
|
389
|
+
if (state == null || !state.AdmissionPoolShutdownPending) return;
|
|
390
|
+
FinalizeAdmissionPoolShutdown(state);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private static void FinalizeAdmissionPoolShutdown(BridgeQueueState state)
|
|
394
|
+
{
|
|
395
|
+
var detached = false;
|
|
396
|
+
lock (state.QueueLock)
|
|
397
|
+
{
|
|
398
|
+
var pool = state.AdmissionPool;
|
|
399
|
+
if (state.RunningWork.Count != 0 || state.PendingSettlementWork.Count != 0
|
|
400
|
+
|| (pool != null && pool.Capture().Active != 0))
|
|
401
|
+
{
|
|
402
|
+
state.AdmissionPoolShutdownPending = true;
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (pool != null) pool.Reset();
|
|
406
|
+
state.AdmissionPool = null;
|
|
407
|
+
state.AdmissionPoolShutdownPending = false;
|
|
408
|
+
detached = true;
|
|
409
|
+
}
|
|
410
|
+
if (detached && object.ReferenceEquals(state, ProductionQueueState))
|
|
411
|
+
WithAdmissionPublicationLock(() => { SessionState.EraseString(AdmissionSessionStateKey); admissionPoolPublicationRevision = 0L; admissionPoolPublicationObserved = string.Empty; return true; });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/// <summary>HTTP后台线程原子发grant;不进入主线程或transaction log。</summary>
|
|
415
|
+
private static string ExecuteAdmissionAcquire(byte[] rawBody, out string token)
|
|
416
|
+
{
|
|
417
|
+
token = null;
|
|
418
|
+
var value = StrictAdmissionObject(rawBody, "acquire", "protocolVersion", "requestId", "instanceId", "target", "command", "rootFingerprint");
|
|
419
|
+
var requestId = RequiredText(value, "requestId", 200);
|
|
420
|
+
if (AdmissionLong(value, "protocolVersion") != 1L || AdmissionText(value, "instanceId") != InstanceId() || AdmissionText(value, "target") != "editor")
|
|
421
|
+
throw new BridgeCommandException("E_INSTANCE_IDENTITY_MISMATCH", "Admission acquire identity does not match this Editor session.");
|
|
422
|
+
RequiredText(value, "command", 256);
|
|
423
|
+
var fingerprint = RequiredText(value, "rootFingerprint", 64);
|
|
424
|
+
var result = AcquireAdmissionAtEndpoint(ProductionQueueState, requestId, fingerprint);
|
|
425
|
+
var granted = result.Outcome == UnityCliAdmissionPool.AcquireOutcome.Granted;
|
|
426
|
+
if (granted) token = result.Token;
|
|
427
|
+
if (granted) WriteLog((result.NewlyGranted ? "admission_granted" : "admission_attached") + " requestId=" + requestId + " active=" + AdmissionActiveCount(ProductionQueueState).ToString(CultureInfo.InvariantCulture));
|
|
428
|
+
return UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Success(requestId, new Dictionary<string, object>
|
|
429
|
+
{
|
|
430
|
+
{ "protocolVersion", 1L }, { "granted", granted },
|
|
431
|
+
{ "grantExpiresInMs", (long)(granted ? result.GrantExpiresInMs : 0) },
|
|
432
|
+
{ "retryAfterMs", (long)result.RetryAfterMs }
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private static string ExecuteAdmissionRevoke(byte[] rawBody, HttpListenerRequest httpRequest)
|
|
437
|
+
{
|
|
438
|
+
var value = StrictAdmissionObject(rawBody, "revoke", "protocolVersion", "requestId", "instanceId", "target", "rootFingerprint");
|
|
439
|
+
var requestId = RequiredText(value, "requestId", 200);
|
|
440
|
+
if (AdmissionLong(value, "protocolVersion") != 1L || AdmissionText(value, "instanceId") != InstanceId() || AdmissionText(value, "target") != "editor")
|
|
441
|
+
throw new BridgeCommandException("E_INSTANCE_IDENTITY_MISMATCH", "Admission revoke identity does not match this Editor session.");
|
|
442
|
+
var fingerprint = RequiredText(value, "rootFingerprint", 64);
|
|
443
|
+
var revoked = RevokeAdmissionAtEndpoint(ProductionQueueState, requestId, fingerprint, RequiredReleaseToken(httpRequest, requestId, fingerprint));
|
|
444
|
+
if (!revoked) throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission revoke identity does not match an unconsumed root.");
|
|
445
|
+
WriteLog("admission_revoked requestId=" + requestId + " active=" + AdmissionActiveCount(ProductionQueueState).ToString(CultureInfo.InvariantCulture));
|
|
446
|
+
return UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Success(requestId, new Dictionary<string, object> { { "revoked", true } }));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/// <summary>Editor admission端点与publication shutdown共享QueueLock,锁内只做pool内存裁决。</summary>
|
|
450
|
+
private static UnityCliAdmissionPool.AcquireResult AcquireAdmissionAtEndpoint(BridgeQueueState state, string requestId, string fingerprint)
|
|
451
|
+
{
|
|
452
|
+
if (state == null) throw new ArgumentNullException("state");
|
|
453
|
+
lock (state.QueueLock)
|
|
454
|
+
{
|
|
455
|
+
if (!state.QueuePublicationOpen || state.AdmissionPool == null)
|
|
456
|
+
throw ClosedAdmissionEndpoint(state);
|
|
457
|
+
state.TestHooks.Hit("before-admission-endpoint-acquire");
|
|
458
|
+
var result = state.AdmissionPool.Acquire(requestId, fingerprint, state.ListenerId ?? ListenerId, state.ListenerGeneration);
|
|
459
|
+
if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.Invalid)
|
|
460
|
+
throw new BridgeCommandException("E_SCHEMA_INVALID", "Admission acquire fields are invalid.");
|
|
461
|
+
if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.IdentityMismatch)
|
|
462
|
+
throw new BridgeCommandException("E_REQUEST_ID_REUSED", "Admission requestId is already bound to different business input.");
|
|
463
|
+
if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.AlreadyConsumed)
|
|
464
|
+
throw new BridgeCommandException("E_ADMISSION_ALREADY_CONSUMED", "Admission root is already consumed and must use exact attach or terminal recovery.");
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private static bool RevokeAdmissionAtEndpoint(BridgeQueueState state, string requestId, string fingerprint, string token)
|
|
470
|
+
{
|
|
471
|
+
if (state == null) throw new ArgumentNullException("state");
|
|
472
|
+
lock (state.QueueLock)
|
|
473
|
+
{
|
|
474
|
+
if (!state.QueuePublicationOpen || state.AdmissionPool == null)
|
|
475
|
+
throw ClosedAdmissionEndpoint(state);
|
|
476
|
+
return state.AdmissionPool.Revoke(requestId, fingerprint, token);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private static BridgeCommandException ClosedAdmissionEndpoint(BridgeQueueState state)
|
|
481
|
+
{
|
|
482
|
+
return state != null && state.QueueSessionStopping
|
|
483
|
+
? new BridgeCommandException("E_RUNTIME_SESSION_STALE", "The Unity session is stopping and no longer accepts admission work.", null, new[] { "Start a new Unity session and submit a new request." })
|
|
484
|
+
: new BridgeCommandException("E_UNITY_DOMAIN_RELOAD", "Unity admission is closed during lifecycle transition.", null, new[] { "Wait for Unity to finish reloading and retry." }, true, 1000);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private static int AdmissionActiveCount(BridgeQueueState state)
|
|
488
|
+
{
|
|
489
|
+
lock (state.QueueLock) return state.AdmissionPool == null ? 0 : state.AdmissionPool.Capture().Active;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
internal static UnityCliAdmissionPool.AcquireResult AcquireAdmissionForTests(BridgeQueueState state, string requestId, string fingerprint)
|
|
493
|
+
{
|
|
494
|
+
if (state == null || state.IsolatedExecution == null) throw new ArgumentException("Admission endpoint test state must be isolated.", "state");
|
|
495
|
+
return AcquireAdmissionAtEndpoint(state, requestId, fingerprint);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/// <summary>只解析HTTP immutable admission输入;调用方在QueueLock内调用AuthorizeAdmissionForQueue完成pool线性化。</summary>
|
|
499
|
+
internal static AdmissionQueueReservation PrepareAdmissionForQueue(HttpListenerRequest httpRequest, UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes)
|
|
500
|
+
{
|
|
501
|
+
var rootId = httpRequest.Headers["x-unity-cli-admission-root-id"];
|
|
502
|
+
var fingerprint = httpRequest.Headers["x-unity-cli-admission-fingerprint"];
|
|
503
|
+
var token = httpRequest.Headers["x-unity-cli-admission-token"];
|
|
504
|
+
var deadlineText = httpRequest.Headers["x-unity-cli-admission-deadline-utc"];
|
|
505
|
+
var reservation = new AdmissionQueueReservation
|
|
506
|
+
{
|
|
507
|
+
RequestId = request.RequestId,
|
|
508
|
+
RequestDigest = UnityCliRequestDigest.Compute(rawRequestBytes, request.InstanceId),
|
|
509
|
+
RootFingerprint = fingerprint,
|
|
510
|
+
Token = token,
|
|
511
|
+
HasAdmissionHeaders = !string.IsNullOrEmpty(rootId) || !string.IsNullOrEmpty(fingerprint) || !string.IsNullOrEmpty(token) || !string.IsNullOrEmpty(deadlineText),
|
|
512
|
+
IsBypass = AdmissionBypassCommand(request.Command),
|
|
513
|
+
HasExactTerminal = AdmissionHasExactTerminal(request, rawRequestBytes)
|
|
514
|
+
};
|
|
515
|
+
if (!reservation.HasAdmissionHeaders) return reservation;
|
|
516
|
+
if (string.IsNullOrEmpty(rootId) || string.IsNullOrEmpty(fingerprint) || string.IsNullOrEmpty(deadlineText))
|
|
517
|
+
throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission root headers are incomplete.");
|
|
518
|
+
if (!string.Equals(rootId, request.RequestId, StringComparison.Ordinal))
|
|
519
|
+
throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission root requestId does not match the Bridge request body.");
|
|
520
|
+
DateTimeOffset deadline;
|
|
521
|
+
if (!DateTimeOffset.TryParse(deadlineText, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out deadline))
|
|
522
|
+
throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission execution deadline is invalid.");
|
|
523
|
+
reservation.ExecutionDeadlineMs = deadline.ToUnixTimeMilliseconds();
|
|
524
|
+
if (reservation.ExecutionDeadlineMs <= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())
|
|
525
|
+
throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission execution deadline has elapsed.");
|
|
526
|
+
reservation.RootFingerprint = fingerprint;
|
|
527
|
+
reservation.Token = token;
|
|
528
|
+
return reservation;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/// <summary>
|
|
532
|
+
/// QueueLock是外层锁,admission pool锁是内层锁;本方法只操作内存pool,不读取文件或SessionState。
|
|
533
|
+
/// </summary>
|
|
534
|
+
internal static void AuthorizeAdmissionForQueue(AdmissionQueueReservation reservation, UnityCli.Protocol.BridgeRequest request)
|
|
535
|
+
{
|
|
536
|
+
AuthorizeAdmissionForQueue(ProductionQueueState, reservation, request);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
internal static void AuthorizeAdmissionForQueue(BridgeQueueState state, AdmissionQueueReservation reservation, UnityCli.Protocol.BridgeRequest request)
|
|
540
|
+
{
|
|
541
|
+
if (state == null || reservation == null || request == null) throw new ArgumentNullException("reservation");
|
|
542
|
+
if (!reservation.HasAdmissionHeaders)
|
|
543
|
+
{
|
|
544
|
+
if (reservation.IsBypass) return;
|
|
545
|
+
throw new BridgeCommandException("E_ADMISSION_REQUIRED", "A valid Unity task-pool admission is required before this business request can enter the queue.");
|
|
546
|
+
}
|
|
547
|
+
var pool = state.AdmissionPool;
|
|
548
|
+
if (pool == null) throw new BridgeCommandException("E_RUNTIME_SESSION_STALE", "The Unity session ended before admission completed.");
|
|
549
|
+
if (!string.IsNullOrEmpty(reservation.Token))
|
|
550
|
+
{
|
|
551
|
+
var result = pool.AuthorizeAndCapture(reservation.RequestId, reservation.RootFingerprint, reservation.RequestDigest, reservation.Token, state.ListenerId ?? ListenerId, state.ListenerGeneration, reservation.ExecutionDeadlineMs);
|
|
552
|
+
if (result.Outcome == UnityCliAdmissionPool.AuthorizeOutcome.Authorized
|
|
553
|
+
|| result.Outcome == UnityCliAdmissionPool.AuthorizeOutcome.AlreadyConsumed)
|
|
554
|
+
{
|
|
555
|
+
reservation.Owner = ToAdmissionOwner(result.Identity, reservation.RequestDigest);
|
|
556
|
+
reservation.NewlyConsumed = result.NewlyConsumed;
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if ((result.Outcome == UnityCliAdmissionPool.AuthorizeOutcome.Invalid
|
|
560
|
+
|| result.Outcome == UnityCliAdmissionPool.AuthorizeOutcome.RecoveryBlocked)
|
|
561
|
+
&& !pool.Revoke(reservation.RequestId, reservation.RootFingerprint, reservation.Token))
|
|
562
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission rejection could not prove exact unconsumed cleanup.");
|
|
563
|
+
throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission token is missing, expired, reused, or bound to another root.");
|
|
564
|
+
}
|
|
565
|
+
if (pool.AttachConsumed(reservation.RequestId, reservation.RootFingerprint, reservation.RequestDigest, state.ListenerId ?? ListenerId, state.ListenerGeneration, reservation.ExecutionDeadlineMs))
|
|
566
|
+
{
|
|
567
|
+
reservation.Owner = CaptureAdmissionOwner(state, reservation.RequestId, reservation.RequestDigest);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (AdmissionRecoveryCommand(request.Command) && pool.RecoverConsumed(reservation.RequestId, reservation.RootFingerprint, reservation.RequestDigest, state.ListenerId ?? ListenerId, state.ListenerGeneration, reservation.ExecutionDeadlineMs))
|
|
571
|
+
{
|
|
572
|
+
reservation.Owner = CaptureAdmissionOwner(state, reservation.RequestId, reservation.RequestDigest);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (reservation.HasExactTerminal) return;
|
|
576
|
+
throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Consumed admission root cannot attach to this listener generation.");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/// <summary>durable settlement已失败并等待恢复时,同字节duplicate只附着原WorkItem。</summary>
|
|
580
|
+
private static bool TryAuthorizeCompletionPendingAttach(BridgeQueueState state, AdmissionQueueReservation reservation, BridgeWorkItem existing)
|
|
581
|
+
{
|
|
582
|
+
var owner = existing == null ? null : existing.AdmissionOwner;
|
|
583
|
+
if (state == null || reservation == null || owner == null || !existing.CompletionIntentReady
|
|
584
|
+
|| !existing.SettlementFailureLogged || !reservation.HasAdmissionHeaders
|
|
585
|
+
|| !string.Equals(reservation.RequestId, owner.RequestId, StringComparison.Ordinal)
|
|
586
|
+
|| !string.Equals(reservation.RequestDigest, owner.RequestDigest, StringComparison.Ordinal)
|
|
587
|
+
|| !string.Equals(reservation.RootFingerprint, owner.RootFingerprint, StringComparison.Ordinal)
|
|
588
|
+
|| reservation.ExecutionDeadlineMs != owner.ExecutionDeadlineMs
|
|
589
|
+
|| (!string.IsNullOrEmpty(reservation.Token) && !string.Equals(reservation.Token, owner.Token, StringComparison.Ordinal))) return false;
|
|
590
|
+
reservation.Owner = owner;
|
|
591
|
+
reservation.NewlyConsumed = false;
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
internal static void MarkAdmissionQueuePublished(AdmissionQueueReservation reservation)
|
|
596
|
+
{
|
|
597
|
+
if (reservation != null) reservation.QueuePublished = true;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/// <summary>publication前补偿只修改pool内存;持久化与日志由QueueLock外调用方完成。</summary>
|
|
601
|
+
internal static bool CompensateAdmissionBeforeQueuePublication(AdmissionQueueReservation reservation)
|
|
602
|
+
{
|
|
603
|
+
return CompensateAdmissionBeforeQueuePublication(ProductionQueueState, reservation);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
internal static bool CompensateAdmissionBeforeQueuePublication(BridgeQueueState state, AdmissionQueueReservation reservation)
|
|
607
|
+
{
|
|
608
|
+
if (state == null || reservation == null || reservation.QueuePublished || !reservation.NewlyConsumed || reservation.Owner == null) return true;
|
|
609
|
+
var pool = state.AdmissionPool;
|
|
610
|
+
var compensated = pool != null && pool.CompensateAuthorization(ToPoolIdentity(reservation.Owner));
|
|
611
|
+
if (compensated) reservation.NewlyConsumed = false;
|
|
612
|
+
return compensated;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/// <summary>只按当前pool中的完整digest冻结exact owner;缺失与identity冲突保持不同结果。</summary>
|
|
616
|
+
internal static bool TryCaptureExactAdmissionOwner(string requestId, string requestDigest, out AdmissionOwnerIdentity identity)
|
|
617
|
+
{
|
|
618
|
+
return TryCaptureExactAdmissionOwner(ProductionQueueState, requestId, requestDigest, out identity);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
internal static bool TryCaptureExactAdmissionOwner(BridgeQueueState state, string requestId, string requestDigest, out AdmissionOwnerIdentity identity)
|
|
622
|
+
{
|
|
623
|
+
identity = null;
|
|
624
|
+
UnityCliAdmissionPool.ConsumedIdentity current;
|
|
625
|
+
var pool = state == null ? null : state.AdmissionPool;
|
|
626
|
+
if (pool == null || !pool.TryGetConsumedIdentity(requestId, out current)) return false;
|
|
627
|
+
identity = ToAdmissionOwner(current, requestDigest);
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
internal static AdmissionOwnerIdentity CaptureAdmissionOwner(string requestId, string requestDigest = null)
|
|
632
|
+
{
|
|
633
|
+
return CaptureAdmissionOwner(ProductionQueueState, requestId, requestDigest);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
internal static AdmissionOwnerIdentity CaptureAdmissionOwner(BridgeQueueState state, string requestId, string requestDigest = null)
|
|
637
|
+
{
|
|
638
|
+
UnityCliAdmissionPool.ConsumedIdentity identity;
|
|
639
|
+
var pool = state == null ? null : state.AdmissionPool;
|
|
640
|
+
if (pool == null || !pool.TryGetConsumedIdentity(requestId, out identity))
|
|
641
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner identity cannot be frozen after consumption.");
|
|
642
|
+
return ToAdmissionOwner(identity, requestDigest);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/// <summary>先落exact completion intent,再在state锁内完成pool memory与map裁决。</summary>
|
|
646
|
+
internal static bool CompleteAdmissionRoot(AdmissionOwnerIdentity identity)
|
|
647
|
+
{
|
|
648
|
+
return CompleteAdmissionRoot(ProductionQueueState, identity);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
internal static bool CompleteAdmissionRoot(BridgeQueueState state, AdmissionOwnerIdentity identity)
|
|
652
|
+
{
|
|
653
|
+
if (state == null || identity == null) return false;
|
|
654
|
+
if (!HasAdmissionCompletionIntent(state, identity) && !PersistAdmissionCompletionIntent(state, identity)) return false;
|
|
655
|
+
var pool = state.AdmissionPool;
|
|
656
|
+
if (pool == null) return false;
|
|
657
|
+
UnityCliAdmissionPool.CompletionOutcome outcome;
|
|
658
|
+
lock (state.QueueLock)
|
|
659
|
+
{
|
|
660
|
+
if (ActiveWorkOwnsAdmissionUnderQueueLock(state, identity)) return false;
|
|
661
|
+
outcome = pool.CompleteTrusted(ToPoolIdentity(identity));
|
|
662
|
+
}
|
|
663
|
+
if (outcome != UnityCliAdmissionPool.CompletionOutcome.ExactCurrentCompleted
|
|
664
|
+
&& outcome != UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent) return false;
|
|
665
|
+
try
|
|
666
|
+
{
|
|
667
|
+
if (!state.PersistPool()) return false;
|
|
668
|
+
if (!state.ClearMarkers(identity)) return false;
|
|
669
|
+
if (!ClearAdmissionCompletionIntent(state, identity)) return false;
|
|
670
|
+
}
|
|
671
|
+
catch { return false; }
|
|
672
|
+
state.Sinks.WriteLog("admission_terminal requestId=" + identity.RequestId + " outcome=" + outcome.ToString() + " active=" + pool.Capture().Active.ToString(CultureInfo.InvariantCulture));
|
|
673
|
+
TryFinalizeAdmissionPoolShutdown(state);
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// 调用方必须持有QueueLock,使活动WorkItem检查与owner完成形成同一原子裁决。
|
|
678
|
+
private static bool ActiveWorkOwnsAdmissionUnderQueueLock(BridgeQueueState state, AdmissionOwnerIdentity identity)
|
|
679
|
+
{
|
|
680
|
+
BridgeWorkItem existing;
|
|
681
|
+
return state.ActiveRequestWork.TryGetValue(identity.RequestId, out existing)
|
|
682
|
+
&& existing.AdmissionOwner != null
|
|
683
|
+
&& SameAdmissionOwner(existing.AdmissionOwner, identity);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/// <summary>仅为将随父root落盘的continuation写入完整intent;pool transfer由队列terminal owner提交。</summary>
|
|
687
|
+
internal static void AddAdmissionOwnerFields(IDictionary<string, object> state, string requestId, UnityCliAdmissionPool.AdmissionOwnerState intendedOwner = UnityCliAdmissionPool.AdmissionOwnerState.Continuation) { AddAdmissionOwnerFields(state, CaptureAdmissionOwner(requestId), intendedOwner); }
|
|
688
|
+
|
|
689
|
+
internal static void AddAdmissionOwnerFields(IDictionary<string, object> state, AdmissionOwnerIdentity identity, UnityCliAdmissionPool.AdmissionOwnerState intendedOwner = UnityCliAdmissionPool.AdmissionOwnerState.Continuation)
|
|
690
|
+
{
|
|
691
|
+
if (state == null || identity == null) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner marker identity is unavailable.");
|
|
692
|
+
WriteAdmissionOwnerFields(state, identity, intendedOwner, identity.OwnerState == intendedOwner);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
internal static AdmissionOwnerIdentity ReadAdmissionOwnerFields(IDictionary<string, object> state, string requestId)
|
|
696
|
+
{
|
|
697
|
+
if (state == null || string.IsNullOrEmpty(requestId)) return null;
|
|
698
|
+
var committed = AdmissionBool(state, "admissionTransferCommitted");
|
|
699
|
+
var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(state, "admissionOwnerState");
|
|
700
|
+
var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(state, "admissionIntendedOwnerState");
|
|
701
|
+
var topDigest = AdmissionText(state, "requestDigest");
|
|
702
|
+
if (!AdmissionDigestIsValid(topDigest)) topDigest = AdmissionText(state, "cancelRootRequestDigest");
|
|
703
|
+
if (!AdmissionDigestIsValid(topDigest)) topDigest = AdmissionText(state, "admissionRequestDigest");
|
|
704
|
+
return new AdmissionOwnerIdentity
|
|
705
|
+
{
|
|
706
|
+
RequestId = requestId,
|
|
707
|
+
RequestDigest = AdmissionText(state, "admissionRequestDigest"),
|
|
708
|
+
TopRequestDigest = topDigest,
|
|
709
|
+
RootFingerprint = AdmissionText(state, "admissionRootFingerprint"),
|
|
710
|
+
Token = AdmissionText(state, "admissionToken"),
|
|
711
|
+
SessionId = AdmissionText(state, "admissionSessionId"),
|
|
712
|
+
ListenerId = AdmissionText(state, "admissionListenerId"),
|
|
713
|
+
ListenerGeneration = AdmissionLong(state, "admissionListenerGeneration"),
|
|
714
|
+
ExecutionDeadlineMs = AdmissionLong(state, "admissionExecutionDeadlineMs"),
|
|
715
|
+
EditorSessionId = AdmissionText(state, "editorSessionId"),
|
|
716
|
+
OwnerState = committed ? intended : source
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/// <summary>三段式owner transfer:先落完整intent,再exact转pool,最后落committed marker。</summary>
|
|
721
|
+
internal static AdmissionOwnerIdentity PersistAdmissionOwnerTransfer(string markerScope, IDictionary<string, object> marker, AdmissionOwnerIdentity identity, UnityCliAdmissionPool.AdmissionOwnerState intendedOwner)
|
|
722
|
+
{
|
|
723
|
+
return PersistAdmissionOwnerTransfer(ProductionQueueState, markerScope, marker, identity, intendedOwner);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
internal static AdmissionOwnerIdentity PersistAdmissionOwnerTransfer(BridgeQueueState state, string markerScope, IDictionary<string, object> marker, AdmissionOwnerIdentity identity, UnityCliAdmissionPool.AdmissionOwnerState intendedOwner)
|
|
727
|
+
{
|
|
728
|
+
if (state == null || marker == null || identity == null || string.IsNullOrEmpty(markerScope)) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission transfer marker is unavailable.");
|
|
729
|
+
var pool = state.AdmissionPool;
|
|
730
|
+
if (pool == null) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission pool is unavailable during owner transfer.");
|
|
731
|
+
var existing = state.LoadOperation(markerScope);
|
|
732
|
+
if (existing != null && !string.Equals(AdmissionText(existing, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal))
|
|
733
|
+
{
|
|
734
|
+
state.ClearOperation(markerScope);
|
|
735
|
+
existing = null;
|
|
736
|
+
}
|
|
737
|
+
if (existing != null && !AdmissionMarkerMatchesIdentity(existing, identity) && !AdmissionMarkerCanAcquireOwner(existing, identity))
|
|
738
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission transfer marker is owned by another exact request.");
|
|
739
|
+
marker["requestId"] = identity.RequestId;
|
|
740
|
+
marker["requestDigest"] = AdmissionTopDigest(identity);
|
|
741
|
+
marker["editorSessionId"] = identity.EditorSessionId ?? EditorSessionId();
|
|
742
|
+
WriteAdmissionOwnerFields(marker, identity, intendedOwner, false);
|
|
743
|
+
state.SaveOperation(markerScope, new Dictionary<string, object>(marker));
|
|
744
|
+
if (!pool.PrepareOwnerTransfer(ToPoolIdentity(identity), intendedOwner))
|
|
745
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner transfer intent does not match the current exact owner.");
|
|
746
|
+
if (!PersistAdmissionPoolState(state)) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner transfer intent could not be durably persisted.");
|
|
747
|
+
UnityCliAdmissionPool.ConsumedIdentity successor;
|
|
748
|
+
if (!pool.CommitOwnerTransfer(ToPoolIdentity(identity), intendedOwner, out successor))
|
|
749
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner transfer could not be committed.");
|
|
750
|
+
var updated = ToAdmissionOwner(successor, identity.RequestDigest);
|
|
751
|
+
updated.TopRequestDigest = AdmissionText(marker, "requestDigest");
|
|
752
|
+
updated.EditorSessionId = AdmissionText(marker, "editorSessionId");
|
|
753
|
+
marker["admissionTransferCommitted"] = true;
|
|
754
|
+
state.SaveOperation(markerScope, new Dictionary<string, object>(marker));
|
|
755
|
+
if (!PersistAdmissionPoolState(state)) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner transfer commit could not be durably persisted.");
|
|
756
|
+
return updated;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/// <summary>重试修复transfer窗口;pool确定缺失时仍先持久化当前SessionState再交给fixed marker清理。</summary>
|
|
760
|
+
internal static AdmissionOwnerIdentity ReconcileAdmissionTransferForLiveMarker(string markerScope, Dictionary<string, object> marker, string requestId, string requestDigest)
|
|
761
|
+
{
|
|
762
|
+
return ReconcileAdmissionTransferForLiveMarker(ProductionQueueState, markerScope, marker, requestId, requestDigest);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
internal static AdmissionOwnerIdentity ReconcileAdmissionTransferForLiveMarker(BridgeQueueState state, string markerScope, Dictionary<string, object> marker, string requestId, string requestDigest)
|
|
766
|
+
{
|
|
767
|
+
if (string.Equals(markerScope, "test-cancel-admission-owner", StringComparison.Ordinal)
|
|
768
|
+
|| string.Equals(markerScope, "test-cancel-terminal-cleanup", StringComparison.Ordinal))
|
|
769
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Test cancellation admission markers require dedicated parent and cancel identity reconciliation.");
|
|
770
|
+
var markerOwner = ReadAdmissionOwnerFields(marker, requestId);
|
|
771
|
+
var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionOwnerState");
|
|
772
|
+
var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
|
|
773
|
+
var pool = state == null ? null : state.AdmissionPool;
|
|
774
|
+
if (!AdmissionMarkerHasImmutableIdentity(marker, markerOwner, AdmissionTopDigest(markerOwner)) || pool == null)
|
|
775
|
+
{
|
|
776
|
+
if (pool != null) pool.BlockRecovery();
|
|
777
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker identity is invalid.");
|
|
778
|
+
}
|
|
779
|
+
var exact = pool.InspectExactIdentityFields(ToPoolIdentity(markerOwner));
|
|
780
|
+
if (exact == UnityCliAdmissionPool.ExactIdentityOutcome.DefinitelyAbsent)
|
|
781
|
+
{
|
|
782
|
+
try
|
|
783
|
+
{
|
|
784
|
+
if (!RestoreAbsentAdmissionOwner(state, markerScope, marker, markerOwner)) return null;
|
|
785
|
+
exact = UnityCliAdmissionPool.ExactIdentityOutcome.ExactCurrent;
|
|
786
|
+
}
|
|
787
|
+
catch
|
|
788
|
+
{
|
|
789
|
+
pool.BlockRecovery();
|
|
790
|
+
throw;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (exact == UnityCliAdmissionPool.ExactIdentityOutcome.IdentityMismatchOrBlocked)
|
|
794
|
+
{
|
|
795
|
+
UnityCliAdmissionPool.ConsumedIdentity rebound;
|
|
796
|
+
if (!pool.TryGetConsumedIdentity(requestId, out rebound))
|
|
797
|
+
{
|
|
798
|
+
pool.BlockRecovery();
|
|
799
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker conflicts with the current requestId owner.");
|
|
800
|
+
}
|
|
801
|
+
var reboundOwner = ToAdmissionOwner(rebound, markerOwner.RequestDigest);
|
|
802
|
+
reboundOwner.TopRequestDigest = AdmissionText(marker, "requestDigest");
|
|
803
|
+
reboundOwner.EditorSessionId = AdmissionText(marker, "editorSessionId");
|
|
804
|
+
if (!SameAdmissionTransferIdentityExceptListener(reboundOwner, markerOwner)
|
|
805
|
+
|| (reboundOwner.OwnerState != source && reboundOwner.OwnerState != intended))
|
|
806
|
+
{
|
|
807
|
+
pool.BlockRecovery();
|
|
808
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker conflicts with the current requestId owner.");
|
|
809
|
+
}
|
|
810
|
+
markerOwner = reboundOwner;
|
|
811
|
+
}
|
|
812
|
+
else if (exact != UnityCliAdmissionPool.ExactIdentityOutcome.ExactCurrent)
|
|
813
|
+
{
|
|
814
|
+
pool.BlockRecovery();
|
|
815
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker conflicts with the current requestId owner.");
|
|
816
|
+
}
|
|
817
|
+
var current = CaptureAdmissionOwner(state, requestId, markerOwner.RequestDigest);
|
|
818
|
+
current.TopRequestDigest = AdmissionText(marker, "requestDigest");
|
|
819
|
+
current.EditorSessionId = AdmissionText(marker, "editorSessionId");
|
|
820
|
+
if (current.OwnerState != source && current.OwnerState != intended)
|
|
821
|
+
{
|
|
822
|
+
pool.BlockRecovery();
|
|
823
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker owner state is outside its exact transfer window.");
|
|
824
|
+
}
|
|
825
|
+
if (current.OwnerState != intended) return PersistAdmissionOwnerTransfer(state, markerScope, marker, current, intended);
|
|
826
|
+
WriteAdmissionOwnerFields(marker, current, intended, true);
|
|
827
|
+
state.SaveOperation(markerScope, marker);
|
|
828
|
+
if (!PersistAdmissionPoolState(state)) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission marker reconciliation persistence was not confirmed.");
|
|
829
|
+
return current;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/// <summary>启动时按marker、对应root与lease三者重建或清理owner;pool缺失不能单独证明终态。</summary>
|
|
833
|
+
private static void ReconcileOrphanAdmissionOwnerMarkers()
|
|
834
|
+
{
|
|
835
|
+
var pool = admissionPool;
|
|
836
|
+
if (pool == null) return;
|
|
837
|
+
foreach (var scope in AdmissionDurableScopes)
|
|
838
|
+
{
|
|
839
|
+
if (scope == "test-cancel") continue;
|
|
840
|
+
var markerScope = scope + "-admission-owner";
|
|
841
|
+
if (!UnityCliActiveOperation.Exists(markerScope)) continue;
|
|
842
|
+
Dictionary<string, object> marker;
|
|
843
|
+
try { marker = UnityCliActiveOperation.Load(markerScope); }
|
|
844
|
+
catch { pool.BlockRecovery(); return; }
|
|
845
|
+
if (marker == null) continue;
|
|
846
|
+
if (!string.Equals(AdmissionText(marker, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal))
|
|
847
|
+
{
|
|
848
|
+
try { UnityCliActiveOperation.Clear(markerScope); } catch { pool.BlockRecovery(); return; }
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
AdmissionOwnerIdentity markerOwner;
|
|
852
|
+
try { markerOwner = ReadAdmissionOwnerFields(marker, AdmissionText(marker, "requestId")); }
|
|
853
|
+
catch { pool.BlockRecovery(); return; }
|
|
854
|
+
if (!AdmissionMarkerHasImmutableIdentity(marker, markerOwner, AdmissionTopDigest(markerOwner))) { pool.BlockRecovery(); return; }
|
|
855
|
+
var exact = pool.InspectExactIdentityFields(ToPoolIdentity(markerOwner));
|
|
856
|
+
if (exact == UnityCliAdmissionPool.ExactIdentityOutcome.IdentityMismatchOrBlocked) { pool.BlockRecovery(); return; }
|
|
857
|
+
if (exact != UnityCliAdmissionPool.ExactIdentityOutcome.DefinitelyAbsent) continue;
|
|
858
|
+
try { RestoreAbsentAdmissionOwner(markerScope, marker, markerOwner); }
|
|
859
|
+
catch { pool.BlockRecovery(); return; }
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/// <summary>pool缺失时,cleanup marker可覆盖root/lease清理窗口;普通owner marker只接受完整root+lease重建。</summary>
|
|
864
|
+
private static bool RestoreAbsentAdmissionOwner(string markerScope, Dictionary<string, object> marker, AdmissionOwnerIdentity markerOwner)
|
|
865
|
+
{
|
|
866
|
+
return RestoreAbsentAdmissionOwner(ProductionQueueState, markerScope, marker, markerOwner);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
private static bool RestoreAbsentAdmissionOwner(BridgeQueueState state, string markerScope, Dictionary<string, object> marker, AdmissionOwnerIdentity markerOwner)
|
|
870
|
+
{
|
|
871
|
+
bool rootReadable;
|
|
872
|
+
var rootExists = HasExactAdmissionMarkerRoot(state, markerScope, markerOwner, out rootReadable);
|
|
873
|
+
if (!rootReadable) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker root cannot be verified.");
|
|
874
|
+
bool leaseOwned;
|
|
875
|
+
if (!state.TryIsLeaseOwned(markerOwner.RequestId, markerOwner.RequestDigest, out leaseOwned))
|
|
876
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker lease ownership cannot be verified.");
|
|
877
|
+
var cleanupMarker = markerScope.EndsWith("-terminal-cleanup", StringComparison.Ordinal)
|
|
878
|
+
|| markerScope.EndsWith("-pre-effect-cleanup", StringComparison.Ordinal);
|
|
879
|
+
if (rootExists || leaseOwned)
|
|
880
|
+
{
|
|
881
|
+
if (!cleanupMarker && (!rootExists || !leaseOwned))
|
|
882
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner marker has only a partial root and lease proof.");
|
|
883
|
+
var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
|
|
884
|
+
var pool = state == null ? null : state.AdmissionPool;
|
|
885
|
+
if (pool == null || !pool.RestoreConsumed(ToPoolIdentity(markerOwner), intended, AdmissionBool(marker, "admissionTransferCommitted")))
|
|
886
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker could not rebuild its exact consumed owner.");
|
|
887
|
+
if (!PersistAdmissionPoolState(state)) throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission cleanup marker restore was not durably persisted.");
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
var preEffectCleanupMarker = markerScope.EndsWith("-pre-effect-cleanup", StringComparison.Ordinal);
|
|
891
|
+
if (!preEffectCleanupMarker && !AdmissionOwnerTerminalExists(state, markerScope, markerOwner))
|
|
892
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission owner marker has no exact cleanup completion proof.");
|
|
893
|
+
state.ClearOperation(markerScope);
|
|
894
|
+
if (state.OperationExists(markerScope))
|
|
895
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Completed admission cleanup marker could not be cleared.");
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
private static bool AdmissionOwnerTerminalExists(BridgeQueueState state, string markerScope, AdmissionOwnerIdentity owner)
|
|
900
|
+
{
|
|
901
|
+
try
|
|
902
|
+
{
|
|
903
|
+
var terminalScope = markerScope.EndsWith("-admission-owner", StringComparison.Ordinal)
|
|
904
|
+
? markerScope.Substring(0, markerScope.Length - "-admission-owner".Length)
|
|
905
|
+
: markerScope;
|
|
906
|
+
if (terminalScope.EndsWith("-terminal-cleanup", StringComparison.Ordinal))
|
|
907
|
+
terminalScope = terminalScope.Substring(0, terminalScope.Length - "-terminal-cleanup".Length);
|
|
908
|
+
if (terminalScope.EndsWith("-pre-effect-cleanup", StringComparison.Ordinal))
|
|
909
|
+
terminalScope = terminalScope.Substring(0, terminalScope.Length - "-pre-effect-cleanup".Length);
|
|
910
|
+
object terminal;
|
|
911
|
+
return state.TryGetTerminal(terminalScope, owner.RequestId, AdmissionTopDigest(owner), out terminal);
|
|
912
|
+
}
|
|
913
|
+
catch { return false; }
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
private static bool HasExactAdmissionMarkerRoot(BridgeQueueState state, string markerScope, AdmissionOwnerIdentity owner, out bool readable)
|
|
917
|
+
{
|
|
918
|
+
readable = true;
|
|
919
|
+
try
|
|
920
|
+
{
|
|
921
|
+
var rootScope = markerScope.EndsWith("-admission-owner", StringComparison.Ordinal)
|
|
922
|
+
? markerScope.Substring(0, markerScope.Length - "-admission-owner".Length)
|
|
923
|
+
: markerScope;
|
|
924
|
+
if (rootScope.EndsWith("-terminal-cleanup", StringComparison.Ordinal)) rootScope = rootScope.Substring(0, rootScope.Length - "-terminal-cleanup".Length);
|
|
925
|
+
if (rootScope.EndsWith("-pre-effect-cleanup", StringComparison.Ordinal)) rootScope = rootScope.Substring(0, rootScope.Length - "-pre-effect-cleanup".Length);
|
|
926
|
+
var root = state.LoadOperation(rootScope);
|
|
927
|
+
if (root == null || !string.Equals(AdmissionText(root, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)) return false;
|
|
928
|
+
return string.Equals(AdmissionText(root, "requestId"), owner.RequestId, StringComparison.Ordinal)
|
|
929
|
+
&& string.Equals(AdmissionText(root, "requestDigest"), owner.RequestDigest, StringComparison.Ordinal);
|
|
930
|
+
}
|
|
931
|
+
catch { readable = false; return false; }
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/// <summary>reload只按完整marker identity修复intent/commit;requestId单字段从不构成owner证明。</summary>
|
|
935
|
+
private static void ReconcileRestoredAdmissionRoots()
|
|
936
|
+
{
|
|
937
|
+
if (admissionPool == null) return;
|
|
938
|
+
foreach (var captured in admissionPool.Capture().Records)
|
|
939
|
+
{
|
|
940
|
+
var record = captured;
|
|
941
|
+
var cancelOwnership = DedicatedCancelMarkerOwnershipForState(ProductionQueueState, record);
|
|
942
|
+
if (cancelOwnership == DedicatedCancelMarkerOwnership.ExactOwner) continue;
|
|
943
|
+
if (cancelOwnership == DedicatedCancelMarkerOwnership.Blocked)
|
|
944
|
+
{
|
|
945
|
+
admissionPool.BlockRecovery();
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
Dictionary<string, object> marker;
|
|
949
|
+
string markerScope;
|
|
950
|
+
try
|
|
951
|
+
{
|
|
952
|
+
if (TryFindExactAdmissionMarker(record, out markerScope, out marker))
|
|
953
|
+
{
|
|
954
|
+
ReconcileAdmissionMarker(record, markerScope, marker);
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
if (record.OwnerState != UnityCliAdmissionPool.AdmissionOwnerState.WorkItem
|
|
958
|
+
&& record.OwnerState != UnityCliAdmissionPool.AdmissionOwnerState.Recovery)
|
|
959
|
+
{
|
|
960
|
+
admissionPool.BlockRecovery();
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
var owner = RecordIdentity(record);
|
|
964
|
+
string durableScope;
|
|
965
|
+
UnityCliAdmissionPool.AdmissionOwnerState durableOwner;
|
|
966
|
+
if (TryFindExactDurableBusinessRoot(record, out durableScope, out durableOwner))
|
|
967
|
+
{
|
|
968
|
+
if (durableScope == "direct-mutation")
|
|
969
|
+
{
|
|
970
|
+
var direct = ReconcileAcceptedDirectMutationAfterReload(ProductionQueueState, record.RequestId, record.RequestDigest);
|
|
971
|
+
if (direct == DirectMutationReloadOutcome.SettledEffectBefore) continue;
|
|
972
|
+
if (direct == DirectMutationReloadOutcome.IdentityBlocked)
|
|
973
|
+
throw new InvalidOperationException("Direct mutation reload identity is blocked.");
|
|
974
|
+
}
|
|
975
|
+
var markerState = new Dictionary<string, object>
|
|
976
|
+
{
|
|
977
|
+
{ "requestId", record.RequestId }, { "requestDigest", record.RequestDigest }, { "editorSessionId", EditorSessionId() }
|
|
978
|
+
};
|
|
979
|
+
PersistAdmissionOwnerTransfer(durableScope + "-admission-owner", markerState, owner,
|
|
980
|
+
owner.OwnerState == UnityCliAdmissionPool.AdmissionOwnerState.WorkItem ? durableOwner : UnityCliAdmissionPool.AdmissionOwnerState.Recovery);
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
if (owner.OwnerState == UnityCliAdmissionPool.AdmissionOwnerState.WorkItem)
|
|
984
|
+
owner = TransitionWithoutMarker(owner, UnityCliAdmissionPool.AdmissionOwnerState.Recovery);
|
|
985
|
+
owner = TransitionWithoutMarker(owner, UnityCliAdmissionPool.AdmissionOwnerState.TerminalCleanup);
|
|
986
|
+
if (!CompleteAdmissionRoot(owner)) { admissionPool.BlockRecovery(); return; }
|
|
987
|
+
}
|
|
988
|
+
catch (Exception exception)
|
|
989
|
+
{
|
|
990
|
+
admissionPool.BlockRecovery();
|
|
991
|
+
WriteLog("admission_reconcile_failed requestId=" + record.RequestId + " " + exception.GetType().Name + ": " + exception.Message);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
private enum DedicatedCancelMarkerOwnership { NoMarkerOrDifferentOwner, ExactOwner, Blocked }
|
|
998
|
+
|
|
999
|
+
/// <summary>cancel owner只有marker、parent root与run identity全部精确一致时才从通用recovery分流。</summary>
|
|
1000
|
+
private static DedicatedCancelMarkerOwnership DedicatedCancelMarkerOwnershipForState(BridgeQueueState state, UnityCliAdmissionPool.AdmissionRecord record)
|
|
1001
|
+
{
|
|
1002
|
+
const string markerScope = "test-cancel-admission-owner";
|
|
1003
|
+
if (state == null || record == null) return DedicatedCancelMarkerOwnership.NoMarkerOrDifferentOwner;
|
|
1004
|
+
try
|
|
1005
|
+
{
|
|
1006
|
+
if (!state.OperationExists(markerScope)) return DedicatedCancelMarkerOwnership.NoMarkerOrDifferentOwner;
|
|
1007
|
+
var marker = state.LoadOperation(markerScope);
|
|
1008
|
+
if (marker == null) return DedicatedCancelMarkerOwnership.Blocked;
|
|
1009
|
+
if (!string.Equals(AdmissionText(marker, "requestId"), record.RequestId, StringComparison.Ordinal))
|
|
1010
|
+
return DedicatedCancelMarkerOwnership.NoMarkerOrDifferentOwner;
|
|
1011
|
+
var rootRequestId = AdmissionText(marker, "rootRequestId");
|
|
1012
|
+
var rootRequestDigest = AdmissionText(marker, "rootRequestDigest");
|
|
1013
|
+
var runId = AdmissionText(marker, "runId");
|
|
1014
|
+
string continuationState;
|
|
1015
|
+
string repairEffectState;
|
|
1016
|
+
object candidateEnvelope;
|
|
1017
|
+
if (!AdmissionMarkerMatchesRecord(marker, record)
|
|
1018
|
+
|| !IsSafeId(rootRequestId) || !AdmissionDigestIsValid(rootRequestDigest) || !Guid.TryParse(runId, out _)
|
|
1019
|
+
|| !string.Equals(AdmissionText(marker, "requestDigest"), rootRequestDigest, StringComparison.Ordinal)
|
|
1020
|
+
|| !TryReadCancelAdmissionMarkerState(marker, record.RequestId, record.RequestDigest, rootRequestDigest, runId,
|
|
1021
|
+
out continuationState, out repairEffectState, out _, out candidateEnvelope))
|
|
1022
|
+
return DedicatedCancelMarkerOwnership.Blocked;
|
|
1023
|
+
var parent = state.LoadOperation("test-run");
|
|
1024
|
+
if (parent == null)
|
|
1025
|
+
{
|
|
1026
|
+
object parentTerminal;
|
|
1027
|
+
object cancelTerminal;
|
|
1028
|
+
object selectedCancelTerminal;
|
|
1029
|
+
return state.TryGetTerminal("test-run", rootRequestId, rootRequestDigest, out parentTerminal)
|
|
1030
|
+
&& state.TryGetTerminal("test-cancel", record.RequestId, record.RequestDigest, out cancelTerminal)
|
|
1031
|
+
&& state.TryGetTerminalBySelector("test-cancel", record.RequestId, rootRequestDigest, out selectedCancelTerminal)
|
|
1032
|
+
&& string.Equals(UnityCliSerializer.Serialize(cancelTerminal), UnityCliSerializer.Serialize(selectedCancelTerminal), StringComparison.Ordinal)
|
|
1033
|
+
&& (!string.Equals(continuationState, "continuation_pending", StringComparison.Ordinal)
|
|
1034
|
+
|| string.Equals(UnityCliSerializer.Serialize(cancelTerminal), UnityCliSerializer.Serialize(candidateEnvelope), StringComparison.Ordinal))
|
|
1035
|
+
? DedicatedCancelMarkerOwnership.ExactOwner
|
|
1036
|
+
: DedicatedCancelMarkerOwnership.Blocked;
|
|
1037
|
+
}
|
|
1038
|
+
object rawOperation;
|
|
1039
|
+
var operation = parent.TryGetValue("testOperation", out rawOperation) ? AsDictionary(rawOperation) : null;
|
|
1040
|
+
if (operation == null
|
|
1041
|
+
|| !string.Equals(AdmissionText(parent, "requestId"), rootRequestId, StringComparison.Ordinal)
|
|
1042
|
+
|| !string.Equals(AdmissionText(parent, "requestDigest"), rootRequestDigest, StringComparison.Ordinal)
|
|
1043
|
+
|| !string.Equals(AdmissionText(parent, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)
|
|
1044
|
+
|| !string.Equals(AdmissionText(operation, "runId"), runId, StringComparison.Ordinal))
|
|
1045
|
+
return DedicatedCancelMarkerOwnership.Blocked;
|
|
1046
|
+
if (string.Equals(continuationState, "dispatched", StringComparison.Ordinal))
|
|
1047
|
+
{
|
|
1048
|
+
return AdmissionBool(operation, "cancelRequested") && AdmissionBool(operation, "cancelNeedsContinuation")
|
|
1049
|
+
&& string.Equals(AdmissionText(operation, "cancelEffectState"), "dispatched", StringComparison.Ordinal)
|
|
1050
|
+
&& string.Equals(AdmissionText(operation, "cancelRunId"), runId, StringComparison.Ordinal)
|
|
1051
|
+
&& string.Equals(AdmissionText(operation, "cancelRequestId"), record.RequestId, StringComparison.Ordinal)
|
|
1052
|
+
&& string.Equals(AdmissionText(operation, "cancelRequestDigest"), record.RequestDigest, StringComparison.Ordinal)
|
|
1053
|
+
&& string.Equals(AdmissionText(operation, "cancelRootRequestDigest"), rootRequestDigest, StringComparison.Ordinal)
|
|
1054
|
+
? DedicatedCancelMarkerOwnership.ExactOwner
|
|
1055
|
+
: DedicatedCancelMarkerOwnership.Blocked;
|
|
1056
|
+
}
|
|
1057
|
+
if (string.Equals(repairEffectState, "not_invoked", StringComparison.Ordinal))
|
|
1058
|
+
{
|
|
1059
|
+
object exact;
|
|
1060
|
+
object selected;
|
|
1061
|
+
object stored;
|
|
1062
|
+
return !AdmissionBool(operation, "cancelRequested")
|
|
1063
|
+
&& string.Equals(AdmissionText(operation, "cancelEffectState"), "not_invoked", StringComparison.Ordinal)
|
|
1064
|
+
&& string.IsNullOrEmpty(AdmissionText(operation, "cancelRequestId"))
|
|
1065
|
+
&& operation.TryGetValue("cancelTerminalEnvelope", out stored)
|
|
1066
|
+
&& state.TryGetTerminal("test-cancel", record.RequestId, record.RequestDigest, out exact)
|
|
1067
|
+
&& state.TryGetTerminalBySelector("test-cancel", record.RequestId, rootRequestDigest, out selected)
|
|
1068
|
+
&& string.Equals(UnityCliSerializer.Serialize(exact), UnityCliSerializer.Serialize(selected), StringComparison.Ordinal)
|
|
1069
|
+
&& string.Equals(UnityCliSerializer.Serialize(exact), UnityCliSerializer.Serialize(stored), StringComparison.Ordinal)
|
|
1070
|
+
? DedicatedCancelMarkerOwnership.ExactOwner
|
|
1071
|
+
: DedicatedCancelMarkerOwnership.Blocked;
|
|
1072
|
+
}
|
|
1073
|
+
if (!string.Equals(AdmissionText(operation, "cancelRunId"), runId, StringComparison.Ordinal)
|
|
1074
|
+
|| !string.Equals(AdmissionText(operation, "cancelRequestId"), record.RequestId, StringComparison.Ordinal)
|
|
1075
|
+
|| !string.Equals(AdmissionText(operation, "cancelRequestDigest"), record.RequestDigest, StringComparison.Ordinal)
|
|
1076
|
+
|| !string.Equals(AdmissionText(operation, "cancelRootRequestDigest"), rootRequestDigest, StringComparison.Ordinal))
|
|
1077
|
+
return DedicatedCancelMarkerOwnership.Blocked;
|
|
1078
|
+
return DedicatedCancelMarkerOwnership.ExactOwner;
|
|
1079
|
+
}
|
|
1080
|
+
catch { return DedicatedCancelMarkerOwnership.Blocked; }
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
internal static string DedicatedCancelMarkerOwnershipForTests(BridgeQueueState state, UnityCliAdmissionPool.AdmissionRecord record)
|
|
1084
|
+
{
|
|
1085
|
+
return DedicatedCancelMarkerOwnershipForState(state, record).ToString();
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
private static AdmissionOwnerIdentity TransitionWithoutMarker(AdmissionOwnerIdentity identity, UnityCliAdmissionPool.AdmissionOwnerState target)
|
|
1089
|
+
{
|
|
1090
|
+
if (!admissionPool.PrepareOwnerTransfer(ToPoolIdentity(identity), target)) throw new InvalidOperationException("Admission recovery transfer intent failed.");
|
|
1091
|
+
PersistAdmissionPoolState();
|
|
1092
|
+
UnityCliAdmissionPool.ConsumedIdentity successor;
|
|
1093
|
+
if (!admissionPool.CommitOwnerTransfer(ToPoolIdentity(identity), target, out successor)) throw new InvalidOperationException("Admission recovery transfer commit failed.");
|
|
1094
|
+
PersistAdmissionPoolState();
|
|
1095
|
+
return ToAdmissionOwner(successor, identity.RequestDigest);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
private static bool TryFindExactDurableBusinessRoot(UnityCliAdmissionPool.AdmissionRecord record, out string markerScope, out UnityCliAdmissionPool.AdmissionOwnerState intendedOwner)
|
|
1099
|
+
{
|
|
1100
|
+
markerScope = null;
|
|
1101
|
+
intendedOwner = UnityCliAdmissionPool.AdmissionOwnerState.Recovery;
|
|
1102
|
+
foreach (var scope in AdmissionDurableScopes)
|
|
1103
|
+
{
|
|
1104
|
+
if (scope == "test-cancel") continue;
|
|
1105
|
+
var state = UnityCliActiveOperation.Load(scope);
|
|
1106
|
+
if (state == null || !string.Equals(AdmissionText(state, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)) continue;
|
|
1107
|
+
if (string.Equals(AdmissionText(state, "requestId"), record.RequestId, StringComparison.Ordinal)
|
|
1108
|
+
&& string.Equals(AdmissionText(state, "requestDigest"), record.RequestDigest, StringComparison.Ordinal))
|
|
1109
|
+
{
|
|
1110
|
+
markerScope = scope;
|
|
1111
|
+
intendedOwner = scope == "direct-mutation" ? UnityCliAdmissionPool.AdmissionOwnerState.Recovery : UnityCliAdmissionPool.AdmissionOwnerState.Coordinator;
|
|
1112
|
+
return true;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
private static bool TryFindExactAdmissionMarker(UnityCliAdmissionPool.AdmissionRecord record, out string markerScope, out Dictionary<string, object> marker)
|
|
1119
|
+
{
|
|
1120
|
+
markerScope = null;
|
|
1121
|
+
marker = null;
|
|
1122
|
+
foreach (var scope in AdmissionDurableScopes)
|
|
1123
|
+
{
|
|
1124
|
+
if (scope == "test-cancel") continue;
|
|
1125
|
+
foreach (var candidate in new[] { scope + "-terminal-cleanup", scope + "-pre-effect-cleanup", scope + "-admission-owner" })
|
|
1126
|
+
{
|
|
1127
|
+
if (!UnityCliActiveOperation.Exists(candidate)) continue;
|
|
1128
|
+
var value = UnityCliActiveOperation.Load(candidate);
|
|
1129
|
+
if (!AdmissionMarkerMatchesRecord(value, record)) continue;
|
|
1130
|
+
markerScope = candidate;
|
|
1131
|
+
marker = value;
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
private static void ReconcileAdmissionMarker(UnityCliAdmissionPool.AdmissionRecord record, string markerScope, Dictionary<string, object> marker)
|
|
1139
|
+
{
|
|
1140
|
+
var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionOwnerState");
|
|
1141
|
+
var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
|
|
1142
|
+
var current = RecordIdentity(record);
|
|
1143
|
+
if (current.OwnerState != intended)
|
|
1144
|
+
{
|
|
1145
|
+
if (current.OwnerState != source || !admissionPool.PrepareOwnerTransfer(ToPoolIdentity(current), intended))
|
|
1146
|
+
throw new InvalidOperationException("Admission marker source does not match the restored owner.");
|
|
1147
|
+
PersistAdmissionPoolState();
|
|
1148
|
+
UnityCliAdmissionPool.ConsumedIdentity successor;
|
|
1149
|
+
if (!admissionPool.CommitOwnerTransfer(ToPoolIdentity(current), intended, out successor))
|
|
1150
|
+
throw new InvalidOperationException("Admission marker transfer cannot be repaired.");
|
|
1151
|
+
current = ToAdmissionOwner(successor, AdmissionText(marker, "admissionRequestDigest"));
|
|
1152
|
+
}
|
|
1153
|
+
current.TopRequestDigest = AdmissionText(marker, "requestDigest");
|
|
1154
|
+
current.EditorSessionId = AdmissionText(marker, "editorSessionId");
|
|
1155
|
+
marker["admissionTransferCommitted"] = true;
|
|
1156
|
+
UnityCliActiveOperation.Save(markerScope, marker);
|
|
1157
|
+
PersistAdmissionPoolState();
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
private static bool AdmissionMarkerCanAcquireOwner(IDictionary<string, object> marker, AdmissionOwnerIdentity identity)
|
|
1161
|
+
{
|
|
1162
|
+
return marker != null && identity != null && string.IsNullOrEmpty(AdmissionText(marker, "admissionRootFingerprint"))
|
|
1163
|
+
&& string.Equals(AdmissionText(marker, "requestId"), identity.RequestId, StringComparison.Ordinal)
|
|
1164
|
+
&& string.Equals(AdmissionText(marker, "requestDigest"), AdmissionTopDigest(identity), StringComparison.Ordinal)
|
|
1165
|
+
&& string.Equals(AdmissionText(marker, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
private static bool AdmissionMarkerMatchesIdentity(IDictionary<string, object> marker, AdmissionOwnerIdentity identity)
|
|
1169
|
+
{
|
|
1170
|
+
return AdmissionMarkerIdentityIsValid(marker, identity, AdmissionTopDigest(identity));
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/// <summary>marker与pool snapshot只按完整immutable identity及合法三阶段状态匹配。</summary>
|
|
1174
|
+
private static bool AdmissionMarkerMatchesRecord(IDictionary<string, object> marker, UnityCliAdmissionPool.AdmissionRecord record)
|
|
1175
|
+
{
|
|
1176
|
+
if (marker == null || record == null || !AdmissionDigestIsValid(AdmissionText(marker, "requestDigest"))) return false;
|
|
1177
|
+
var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionOwnerState");
|
|
1178
|
+
var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
|
|
1179
|
+
object committedValue;
|
|
1180
|
+
if (!marker.TryGetValue("admissionTransferCommitted", out committedValue) || !(committedValue is bool)
|
|
1181
|
+
|| source == UnityCliAdmissionPool.AdmissionOwnerState.Granted || intended == UnityCliAdmissionPool.AdmissionOwnerState.Granted
|
|
1182
|
+
|| !Enum.IsDefined(typeof(UnityCliAdmissionPool.AdmissionOwnerState), source)
|
|
1183
|
+
|| !Enum.IsDefined(typeof(UnityCliAdmissionPool.AdmissionOwnerState), intended)
|
|
1184
|
+
|| !UnityCliAdmissionPool.IsLegalOwnerTransfer(source, intended)) return false;
|
|
1185
|
+
if (!string.Equals(AdmissionText(marker, "requestId"), record.RequestId, StringComparison.Ordinal)
|
|
1186
|
+
|| !string.Equals(AdmissionText(marker, "admissionRequestDigest"), record.RequestDigest, StringComparison.Ordinal)
|
|
1187
|
+
|| !string.Equals(AdmissionText(marker, "admissionRootFingerprint"), record.RootFingerprint, StringComparison.Ordinal)
|
|
1188
|
+
|| !string.Equals(AdmissionText(marker, "admissionToken"), record.Token, StringComparison.Ordinal)
|
|
1189
|
+
|| !string.Equals(AdmissionText(marker, "admissionSessionId"), record.SessionId, StringComparison.Ordinal)
|
|
1190
|
+
|| !string.Equals(AdmissionText(marker, "admissionListenerId"), record.ListenerId, StringComparison.Ordinal)
|
|
1191
|
+
|| AdmissionLong(marker, "admissionListenerGeneration") != record.ListenerGeneration
|
|
1192
|
+
|| AdmissionLong(marker, "admissionExecutionDeadlineMs") != record.ExecutionDeadlineMs
|
|
1193
|
+
|| !string.Equals(AdmissionText(marker, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)) return false;
|
|
1194
|
+
var sourceCommitted = record.TransferCommitted && record.OwnerState == source && record.IntendedOwnerState == source;
|
|
1195
|
+
var sourcePending = !record.TransferCommitted && record.OwnerState == source && record.IntendedOwnerState == intended;
|
|
1196
|
+
var intendedCommitted = record.TransferCommitted && record.OwnerState == intended && record.IntendedOwnerState == intended;
|
|
1197
|
+
return sourceCommitted || sourcePending || intendedCommitted;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
private static bool AdmissionMarkerIdentityIsValid(IDictionary<string, object> marker, AdmissionOwnerIdentity identity, string topRequestDigest)
|
|
1201
|
+
{
|
|
1202
|
+
if (!AdmissionMarkerHasImmutableIdentity(marker, identity, topRequestDigest)) return false;
|
|
1203
|
+
var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionOwnerState");
|
|
1204
|
+
var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
|
|
1205
|
+
return identity.OwnerState == source || identity.OwnerState == intended;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
private static bool AdmissionMarkerHasImmutableIdentity(IDictionary<string, object> marker, AdmissionOwnerIdentity identity, string topRequestDigest)
|
|
1209
|
+
{
|
|
1210
|
+
if (marker == null || identity == null || !AdmissionDigestIsValid(topRequestDigest)
|
|
1211
|
+
|| !AdmissionDigestIsValid(identity.RequestDigest)
|
|
1212
|
+
|| !Enum.IsDefined(typeof(UnityCliAdmissionPool.AdmissionOwnerState), identity.OwnerState)) return false;
|
|
1213
|
+
var source = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionOwnerState");
|
|
1214
|
+
var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
|
|
1215
|
+
object committed;
|
|
1216
|
+
return marker.TryGetValue("admissionTransferCommitted", out committed) && committed is bool
|
|
1217
|
+
&& source != UnityCliAdmissionPool.AdmissionOwnerState.Granted
|
|
1218
|
+
&& intended != UnityCliAdmissionPool.AdmissionOwnerState.Granted
|
|
1219
|
+
&& Enum.IsDefined(typeof(UnityCliAdmissionPool.AdmissionOwnerState), source)
|
|
1220
|
+
&& Enum.IsDefined(typeof(UnityCliAdmissionPool.AdmissionOwnerState), intended)
|
|
1221
|
+
&& UnityCliAdmissionPool.IsLegalOwnerTransfer(source, intended)
|
|
1222
|
+
&& string.Equals(AdmissionText(marker, "requestId"), identity.RequestId, StringComparison.Ordinal)
|
|
1223
|
+
&& string.Equals(AdmissionText(marker, "requestDigest"), topRequestDigest, StringComparison.Ordinal)
|
|
1224
|
+
&& string.Equals(AdmissionText(marker, "admissionRequestDigest"), identity.RequestDigest, StringComparison.Ordinal)
|
|
1225
|
+
&& string.Equals(AdmissionText(marker, "admissionRootFingerprint"), identity.RootFingerprint, StringComparison.Ordinal)
|
|
1226
|
+
&& string.Equals(AdmissionText(marker, "admissionToken"), identity.Token, StringComparison.Ordinal)
|
|
1227
|
+
&& string.Equals(AdmissionText(marker, "admissionSessionId"), identity.SessionId, StringComparison.Ordinal)
|
|
1228
|
+
&& AdmissionLong(marker, "admissionExecutionDeadlineMs") == identity.ExecutionDeadlineMs
|
|
1229
|
+
&& string.Equals(AdmissionText(marker, "editorSessionId"), identity.EditorSessionId ?? EditorSessionId(), StringComparison.Ordinal);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/// <summary>pool删除前先落固定有界completion intent;该intent只记录identity,不重新执行业务effect。</summary>
|
|
1233
|
+
internal static bool PersistAdmissionCompletionIntent(BridgeQueueState state, AdmissionOwnerIdentity identity)
|
|
1234
|
+
{
|
|
1235
|
+
if (state == null || identity == null) return false;
|
|
1236
|
+
return state.PersistIntent(identity);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
internal static bool HasAdmissionCompletionIntent(BridgeQueueState state, AdmissionOwnerIdentity identity)
|
|
1240
|
+
{
|
|
1241
|
+
if (state == null || identity == null) return false;
|
|
1242
|
+
return state.HasIntent(identity);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
internal static bool ClearAdmissionCompletionIntent(BridgeQueueState state, AdmissionOwnerIdentity identity)
|
|
1246
|
+
{
|
|
1247
|
+
if (state == null || identity == null) return false;
|
|
1248
|
+
return state.ClearIntent(identity);
|
|
24
1249
|
}
|
|
25
1250
|
|
|
26
|
-
|
|
27
|
-
private static void InitializeAdmissionPool()
|
|
1251
|
+
private static bool PersistProductionAdmissionCompletionIntent(AdmissionOwnerIdentity identity, string kind)
|
|
28
1252
|
{
|
|
29
|
-
if (admissionPool != null) return;
|
|
30
|
-
var settings = UnityCliProjectSettings.Current;
|
|
31
|
-
admissionPool = new UnityCliAdmissionPool(InstanceId(), EditorSessionId(), settings.TaskPoolMaxActive, settings.TaskPoolReopenAt);
|
|
32
|
-
var serialized = SessionState.GetString(AdmissionSessionStateKey, string.Empty);
|
|
33
|
-
if (string.IsNullOrEmpty(serialized)) return;
|
|
34
1253
|
try
|
|
35
1254
|
{
|
|
36
|
-
|
|
37
|
-
if (state == null || AdmissionText(state, "editorSessionId") != EditorSessionId()) { SessionState.EraseString(AdmissionSessionStateKey); return; }
|
|
38
|
-
var records = new List<UnityCliAdmissionPool.AdmissionRecord>();
|
|
39
|
-
object rawRecords;
|
|
40
|
-
var values = state.TryGetValue("records", out rawRecords) ? rawRecords as IEnumerable : null;
|
|
41
|
-
if (values == null) throw new InvalidOperationException("Admission SessionState records are missing.");
|
|
42
|
-
foreach (var value in values)
|
|
1255
|
+
return WithAdmissionPublicationLock(() =>
|
|
43
1256
|
{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
GrantExpiresAtMs = AdmissionLong(record, "grantExpiresAtMs"),
|
|
56
|
-
ExecutionDeadlineMs = AdmissionLong(record, "executionDeadlineMs"),
|
|
57
|
-
State = AdmissionText(record, "state") == "consumed" ? UnityCliAdmissionPool.AdmissionState.Consumed : UnityCliAdmissionPool.AdmissionState.Granted,
|
|
58
|
-
OwnerKind = (UnityCliAdmissionPool.AdmissionOwnerKind)AdmissionLong(record, "ownerKind")
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
admissionPool.Restore(records, checked((int)AdmissionLong(state, "activeCount")), AdmissionBool(state, "gateClosed"), RequiredText(state, "recoveryDigest", 64), checked((int)AdmissionLong(state, "configuredMaxActive")), checked((int)AdmissionLong(state, "configuredReopenAt")));
|
|
1257
|
+
var root = LoadAdmissionSettlementLocked(true);
|
|
1258
|
+
var entries = root["entries"] as List<object>;
|
|
1259
|
+
if (entries == null || entries.Cast<object>().Select(AsDictionary).Any(value => value == null)) return false;
|
|
1260
|
+
if (entries.Cast<object>().Select(AsDictionary).Any(value => CompletionIntentMatches(value, identity))) return true;
|
|
1261
|
+
if (entries.Count >= 256) return false;
|
|
1262
|
+
entries.Add(CompletionIntent(identity, kind));
|
|
1263
|
+
root["publicationRevision"] = checked(AdmissionLong(root, "publicationRevision") + 1L);
|
|
1264
|
+
UnityCliActiveOperation.Save(AdmissionSettlementScope, root);
|
|
1265
|
+
var confirmed = LoadAdmissionSettlementLocked(false);
|
|
1266
|
+
return confirmed != null && CompletionIntentEntries(confirmed).Any(value => CompletionIntentMatches(value, identity));
|
|
1267
|
+
});
|
|
62
1268
|
}
|
|
63
|
-
catch
|
|
1269
|
+
catch { return false; }
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
private static bool HasProductionAdmissionCompletionIntent(AdmissionOwnerIdentity identity)
|
|
1273
|
+
{
|
|
1274
|
+
try
|
|
64
1275
|
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
1276
|
+
return WithAdmissionPublicationLock(() =>
|
|
1277
|
+
{
|
|
1278
|
+
var root = LoadAdmissionSettlementLocked(false);
|
|
1279
|
+
return root != null && CompletionIntentEntries(root).Any(value => CompletionIntentMatches(value, identity));
|
|
1280
|
+
});
|
|
68
1281
|
}
|
|
1282
|
+
catch { return false; }
|
|
69
1283
|
}
|
|
70
1284
|
|
|
71
|
-
|
|
72
|
-
private static void SaveAdmissionPoolBeforeReload()
|
|
1285
|
+
private static bool ClearProductionAdmissionCompletionIntent(AdmissionOwnerIdentity identity)
|
|
73
1286
|
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
1287
|
+
try
|
|
1288
|
+
{
|
|
1289
|
+
return WithAdmissionPublicationLock(() =>
|
|
1290
|
+
{
|
|
1291
|
+
var root = LoadAdmissionSettlementLocked(false);
|
|
1292
|
+
if (root == null) return true;
|
|
1293
|
+
var entries = root["entries"] as List<object>;
|
|
1294
|
+
if (entries == null || entries.Cast<object>().Select(AsDictionary).Any(value => value == null)) return false;
|
|
1295
|
+
entries.RemoveAll(value => CompletionIntentMatches(AsDictionary(value), identity));
|
|
1296
|
+
if (entries.Count == 0)
|
|
1297
|
+
{
|
|
1298
|
+
UnityCliActiveOperation.Clear(AdmissionSettlementScope);
|
|
1299
|
+
return UnityCliActiveOperation.Load(AdmissionSettlementScope) == null;
|
|
1300
|
+
}
|
|
1301
|
+
root["publicationRevision"] = checked(AdmissionLong(root, "publicationRevision") + 1L);
|
|
1302
|
+
UnityCliActiveOperation.Save(AdmissionSettlementScope, root);
|
|
1303
|
+
var confirmed = LoadAdmissionSettlementLocked(false);
|
|
1304
|
+
return confirmed != null && !CompletionIntentEntries(confirmed).Any(value => CompletionIntentMatches(value, identity));
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
catch { return false; }
|
|
90
1308
|
}
|
|
91
1309
|
|
|
92
|
-
private static
|
|
1310
|
+
private static Dictionary<string, object> LoadProductionAdmissionSettlement()
|
|
93
1311
|
{
|
|
94
|
-
|
|
95
|
-
admissionPool = null;
|
|
96
|
-
SessionState.EraseString(AdmissionSessionStateKey);
|
|
1312
|
+
return WithAdmissionPublicationLock(() => LoadAdmissionSettlementLocked(false));
|
|
97
1313
|
}
|
|
98
1314
|
|
|
99
|
-
|
|
100
|
-
private static string ExecuteAdmissionAcquire(byte[] rawBody, out string token)
|
|
1315
|
+
private static Dictionary<string, object> LoadAdmissionSettlementLocked(bool create)
|
|
101
1316
|
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
var requestId = RequiredText(value, "requestId", 200);
|
|
105
|
-
if (AdmissionLong(value, "protocolVersion") != 1L || AdmissionText(value, "instanceId") != InstanceId() || AdmissionText(value, "target") != "editor")
|
|
106
|
-
throw new BridgeCommandException("E_INSTANCE_IDENTITY_MISMATCH", "Admission acquire identity does not match this Editor session.");
|
|
107
|
-
RequiredText(value, "command", 256);
|
|
108
|
-
var fingerprint = RequiredText(value, "rootFingerprint", 64);
|
|
109
|
-
EnsureAdmissionPool();
|
|
110
|
-
var result = admissionPool.Acquire(requestId, fingerprint, ListenerId, CurrentListenerGeneration());
|
|
111
|
-
if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.Invalid)
|
|
112
|
-
throw new BridgeCommandException("E_SCHEMA_INVALID", "Admission acquire fields are invalid.");
|
|
113
|
-
if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.IdentityMismatch)
|
|
114
|
-
throw new BridgeCommandException("E_REQUEST_ID_REUSED", "Admission requestId is already bound to different business input.");
|
|
115
|
-
if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.AlreadyConsumed)
|
|
116
|
-
throw new BridgeCommandException("E_ADMISSION_ALREADY_CONSUMED", "Admission root is already consumed and must use exact attach or terminal recovery.");
|
|
117
|
-
var granted = result.Outcome == UnityCliAdmissionPool.AcquireOutcome.Granted;
|
|
118
|
-
if (granted) token = result.Token;
|
|
119
|
-
if (granted) WriteLog((result.NewlyGranted ? "admission_granted" : "admission_attached") + " requestId=" + requestId + " active=" + admissionPool.Capture().Active.ToString(CultureInfo.InvariantCulture));
|
|
120
|
-
return UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Success(requestId, new Dictionary<string, object>
|
|
1317
|
+
var root = UnityCliActiveOperation.Load(AdmissionSettlementScope);
|
|
1318
|
+
if (root == null)
|
|
121
1319
|
{
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
1320
|
+
return create
|
|
1321
|
+
? new Dictionary<string, object> { { "editorSessionId", EditorSessionId() }, { "publicationRevision", 0L }, { "entries", new List<object>() } }
|
|
1322
|
+
: null;
|
|
1323
|
+
}
|
|
1324
|
+
if (!string.Equals(AdmissionText(root, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal))
|
|
1325
|
+
{
|
|
1326
|
+
return create
|
|
1327
|
+
? new Dictionary<string, object> { { "editorSessionId", EditorSessionId() }, { "publicationRevision", 0L }, { "entries", new List<object>() } }
|
|
1328
|
+
: null;
|
|
1329
|
+
}
|
|
1330
|
+
if (AdmissionLong(root, "publicationRevision") < 1L || !(root.ContainsKey("entries") && root["entries"] is List<object>))
|
|
1331
|
+
throw new InvalidDataException("Admission completion journal publication is invalid.");
|
|
1332
|
+
return root;
|
|
126
1333
|
}
|
|
127
1334
|
|
|
128
|
-
private static string
|
|
1335
|
+
private static IEnumerable<IDictionary<string, object>> CompletionIntentEntries(Dictionary<string, object> root)
|
|
129
1336
|
{
|
|
130
|
-
var
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
WriteLog("admission_revoked requestId=" + requestId + " active=" + admissionPool.Capture().Active.ToString(CultureInfo.InvariantCulture));
|
|
139
|
-
return UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Success(requestId, new Dictionary<string, object> { { "revoked", true } }));
|
|
1337
|
+
var entries = root == null ? null : root["entries"] as List<object>;
|
|
1338
|
+
if (entries == null || entries.Count > 256) throw new InvalidDataException("Admission completion journal entries are invalid.");
|
|
1339
|
+
foreach (var raw in entries)
|
|
1340
|
+
{
|
|
1341
|
+
var entry = AsDictionary(raw);
|
|
1342
|
+
if (entry == null) throw new InvalidDataException("Admission completion journal entry is invalid.");
|
|
1343
|
+
yield return entry;
|
|
1344
|
+
}
|
|
140
1345
|
}
|
|
141
1346
|
|
|
142
|
-
|
|
143
|
-
private static AdmissionOwnerIdentity AuthorizeAdmission(HttpListenerRequest httpRequest, UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes)
|
|
1347
|
+
private static Dictionary<string, object> CompletionIntent(AdmissionOwnerIdentity identity, string kind)
|
|
144
1348
|
{
|
|
145
|
-
|
|
146
|
-
var fingerprint = httpRequest.Headers["x-unity-cli-admission-fingerprint"];
|
|
147
|
-
var token = httpRequest.Headers["x-unity-cli-admission-token"];
|
|
148
|
-
var deadlineText = httpRequest.Headers["x-unity-cli-admission-deadline-utc"];
|
|
149
|
-
var any = !string.IsNullOrEmpty(rootId) || !string.IsNullOrEmpty(fingerprint) || !string.IsNullOrEmpty(token) || !string.IsNullOrEmpty(deadlineText);
|
|
150
|
-
if (!any)
|
|
151
|
-
{
|
|
152
|
-
if (AdmissionBypassCommand(request.Command)) return null;
|
|
153
|
-
throw new BridgeCommandException("E_ADMISSION_REQUIRED", "A valid Unity task-pool admission is required before this business request can enter the queue.");
|
|
154
|
-
}
|
|
155
|
-
if (string.IsNullOrEmpty(rootId) || string.IsNullOrEmpty(fingerprint) || string.IsNullOrEmpty(deadlineText))
|
|
156
|
-
throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission root headers are incomplete.");
|
|
157
|
-
if (!string.Equals(rootId, request.RequestId, StringComparison.Ordinal))
|
|
158
|
-
throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission root requestId does not match the Bridge request body.");
|
|
159
|
-
DateTimeOffset deadline;
|
|
160
|
-
if (!DateTimeOffset.TryParse(deadlineText, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out deadline))
|
|
161
|
-
throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission execution deadline is invalid.");
|
|
162
|
-
EnsureAdmissionPool();
|
|
163
|
-
var deadlineMs = deadline.ToUnixTimeMilliseconds();
|
|
164
|
-
if (!string.IsNullOrEmpty(token))
|
|
1349
|
+
return new Dictionary<string, object>
|
|
165
1350
|
{
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Consumed admission root cannot attach to this listener generation.");
|
|
1351
|
+
{ "requestId", identity.RequestId }, { "requestDigest", AdmissionTopDigest(identity) },
|
|
1352
|
+
{ "kind", kind ?? "admission-completion" }, { "admissionRequestDigest", identity.RequestDigest },
|
|
1353
|
+
{ "admissionRootFingerprint", identity.RootFingerprint }, { "admissionToken", identity.Token },
|
|
1354
|
+
{ "admissionSessionId", identity.SessionId }, { "admissionListenerId", identity.ListenerId },
|
|
1355
|
+
{ "admissionListenerGeneration", identity.ListenerGeneration }, { "admissionExecutionDeadlineMs", identity.ExecutionDeadlineMs },
|
|
1356
|
+
{ "admissionOwnerState", (long)identity.OwnerState }, { "editorSessionId", identity.EditorSessionId ?? EditorSessionId() }
|
|
1357
|
+
};
|
|
174
1358
|
}
|
|
175
1359
|
|
|
176
|
-
private static
|
|
1360
|
+
private static bool CompletionIntentMatches(IDictionary<string, object> entry, AdmissionOwnerIdentity identity)
|
|
177
1361
|
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
1362
|
+
return entry != null && identity != null
|
|
1363
|
+
&& string.Equals(AdmissionText(entry, "requestId"), identity.RequestId, StringComparison.Ordinal)
|
|
1364
|
+
&& string.Equals(AdmissionText(entry, "requestDigest"), AdmissionTopDigest(identity), StringComparison.Ordinal)
|
|
1365
|
+
&& string.Equals(AdmissionText(entry, "admissionRequestDigest"), identity.RequestDigest, StringComparison.Ordinal)
|
|
1366
|
+
&& string.Equals(AdmissionText(entry, "admissionRootFingerprint"), identity.RootFingerprint, StringComparison.Ordinal)
|
|
1367
|
+
&& string.Equals(AdmissionText(entry, "admissionToken"), identity.Token, StringComparison.Ordinal)
|
|
1368
|
+
&& string.Equals(AdmissionText(entry, "admissionSessionId"), identity.SessionId, StringComparison.Ordinal)
|
|
1369
|
+
&& string.Equals(AdmissionText(entry, "admissionListenerId"), identity.ListenerId, StringComparison.Ordinal)
|
|
1370
|
+
&& AdmissionLong(entry, "admissionListenerGeneration") == identity.ListenerGeneration
|
|
1371
|
+
&& AdmissionLong(entry, "admissionExecutionDeadlineMs") == identity.ExecutionDeadlineMs
|
|
1372
|
+
&& AdmissionLong(entry, "admissionOwnerState") == (long)identity.OwnerState
|
|
1373
|
+
&& string.Equals(AdmissionText(entry, "editorSessionId"), identity.EditorSessionId ?? EditorSessionId(), StringComparison.Ordinal);
|
|
182
1374
|
}
|
|
183
1375
|
|
|
184
|
-
|
|
1376
|
+
/// <summary>accepted durable transfer失败时只使用WorkItem冻结的消费角色与cancel候选建立可执行marker。</summary>
|
|
1377
|
+
private static AdmissionTransferResult SettleAdmissionAfterWorkItem(BridgeQueueState state, BridgeWorkItem item)
|
|
185
1378
|
{
|
|
186
|
-
|
|
1379
|
+
var identity = item == null ? null : item.AdmissionOwner;
|
|
1380
|
+
var command = item == null || item.Request == null ? null : item.Request.Command;
|
|
1381
|
+
var response = item == null ? null : item.Response;
|
|
1382
|
+
if (identity == null || string.IsNullOrEmpty(identity.RequestId)) return AdmissionTransferResult.NotAccepted;
|
|
1383
|
+
var transfer = TransferAdmissionAfterAcceptedWork(state, item, identity, command, response);
|
|
1384
|
+
if (transfer == AdmissionTransferResult.TransferPending)
|
|
1385
|
+
transfer = PersistWorkItemRecoveryAfterTransferFailure(state, item, identity, command, response);
|
|
1386
|
+
if (transfer == AdmissionTransferResult.TransferPending) return transfer;
|
|
1387
|
+
var pool = state.AdmissionPool;
|
|
187
1388
|
UnityCliAdmissionPool.ConsumedIdentity current;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return
|
|
1389
|
+
if (pool == null || !pool.TryGetConsumedIdentity(identity.RequestId, out current)) return transfer;
|
|
1390
|
+
AdmissionOwnerIdentity frozen;
|
|
1391
|
+
try { frozen = ToAdmissionOwner(current, identity.RequestDigest); }
|
|
1392
|
+
catch (BridgeCommandException) { return AdmissionTransferResult.TransferPending; }
|
|
1393
|
+
if (!SameAdmissionImmutableIdentity(frozen, identity)) return AdmissionTransferResult.TransferPending;
|
|
1394
|
+
identity.OwnerState = frozen.OwnerState;
|
|
1395
|
+
return transfer;
|
|
192
1396
|
}
|
|
193
1397
|
|
|
194
|
-
|
|
1398
|
+
/// <summary>root观察不确定时用队列冻结身份建立可执行Recovery marker;候选缺失时继续保留WorkItem。</summary>
|
|
1399
|
+
private static AdmissionTransferResult PersistWorkItemRecoveryAfterTransferFailure(BridgeQueueState state, BridgeWorkItem item, AdmissionOwnerIdentity identity, string command, string response)
|
|
1400
|
+
{
|
|
1401
|
+
if (identity == null || (!AdmissionDurableRootCommand(command) && command != "project-tool.run" && command != "code.exec"))
|
|
1402
|
+
return AdmissionTransferResult.TransferPending;
|
|
1403
|
+
try
|
|
1404
|
+
{
|
|
1405
|
+
var current = CaptureAdmissionOwner(state, identity.RequestId, identity.RequestDigest);
|
|
1406
|
+
if (current.OwnerState != UnityCliAdmissionPool.AdmissionOwnerState.WorkItem)
|
|
1407
|
+
return TryConfirmDurableTransferredOwner(state, identity, command, current)
|
|
1408
|
+
? AdmissionTransferResult.Transferred
|
|
1409
|
+
: AdmissionTransferResult.TransferPending;
|
|
1410
|
+
string markerScope;
|
|
1411
|
+
string rootScope;
|
|
1412
|
+
AdmissionOwnerScopes(command, out markerScope, out rootScope);
|
|
1413
|
+
var cancelCandidate = command == "internal.test.run.cancel" ? item.CaptureCancelRecoveryCandidate() : null;
|
|
1414
|
+
current.TopRequestDigest = cancelCandidate != null && AdmissionDigestIsValid(cancelCandidate.RootRequestDigest)
|
|
1415
|
+
? cancelCandidate.RootRequestDigest
|
|
1416
|
+
: AdmissionAcceptedTopDigest(state, response, current, command);
|
|
1417
|
+
var marker = new Dictionary<string, object>
|
|
1418
|
+
{
|
|
1419
|
+
{ "requestId", current.RequestId }, { "requestDigest", AdmissionTopDigest(current) }, { "editorSessionId", EditorSessionId() }
|
|
1420
|
+
};
|
|
1421
|
+
var cancelOwner = UnityCliAdmissionPool.AdmissionOwnerState.Recovery;
|
|
1422
|
+
if (command == "internal.test.run.cancel"
|
|
1423
|
+
&& !TryAddCancelAdmissionRootIdentity(state, marker, current, cancelCandidate, out cancelOwner))
|
|
1424
|
+
return AdmissionTransferResult.TransferPending;
|
|
1425
|
+
var recovered = PersistAdmissionOwnerTransfer(state, markerScope + "-admission-owner", marker, current,
|
|
1426
|
+
command == "internal.test.run.cancel" ? cancelOwner : UnityCliAdmissionPool.AdmissionOwnerState.Recovery);
|
|
1427
|
+
identity.OwnerState = recovered.OwnerState;
|
|
1428
|
+
identity.TopRequestDigest = recovered.TopRequestDigest;
|
|
1429
|
+
return AdmissionTransferResult.Transferred;
|
|
1430
|
+
}
|
|
1431
|
+
catch (Exception exception)
|
|
1432
|
+
{
|
|
1433
|
+
state.Sinks.WriteLog("admission_workitem_recovery_pending requestId=" + identity.RequestId + " " + exception.GetType().Name + ": " + exception.Message);
|
|
1434
|
+
return AdmissionTransferResult.TransferPending;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
195
1437
|
|
|
196
|
-
|
|
1438
|
+
/// <summary>accepted coordinator/cancel及仍持有exact direct durable root的请求只通过固定marker移交owner。</summary>
|
|
1439
|
+
private static AdmissionTransferResult TransferAdmissionAfterAcceptedWork(BridgeQueueState state, BridgeWorkItem item, AdmissionOwnerIdentity identity, string command, string response)
|
|
197
1440
|
{
|
|
198
|
-
if (
|
|
199
|
-
|
|
1441
|
+
if (identity == null) return AdmissionTransferResult.NotAccepted;
|
|
1442
|
+
var directMutation = command == "project-tool.run" || command == "code.exec";
|
|
1443
|
+
if (!AdmissionDurableRootCommand(command) && !directMutation) return AdmissionTransferResult.NotAccepted;
|
|
1444
|
+
string markerScope;
|
|
1445
|
+
string rootScope;
|
|
1446
|
+
AdmissionOwnerScopes(command, out markerScope, out rootScope);
|
|
1447
|
+
try
|
|
1448
|
+
{
|
|
1449
|
+
var current = CaptureAdmissionOwner(state, identity.RequestId, identity.RequestDigest);
|
|
1450
|
+
if (current.OwnerState != UnityCliAdmissionPool.AdmissionOwnerState.WorkItem)
|
|
1451
|
+
return TryConfirmDurableTransferredOwner(state, identity, command, current)
|
|
1452
|
+
? AdmissionTransferResult.Transferred
|
|
1453
|
+
: AdmissionTransferResult.TransferPending;
|
|
1454
|
+
bool durable;
|
|
1455
|
+
var readable = TryHasExactDurableBusinessRoot(state, rootScope, command, identity, out durable);
|
|
1456
|
+
if (!readable) return AdmissionTransferResult.TransferPending;
|
|
1457
|
+
var accepted = AdmissionResponseIsCoordinatorAccepted(response) || durable;
|
|
1458
|
+
if (!accepted) return AdmissionTransferResult.NotAccepted;
|
|
1459
|
+
var cancelCandidate = command == "internal.test.run.cancel" ? item.CaptureCancelRecoveryCandidate() : null;
|
|
1460
|
+
identity.TopRequestDigest = cancelCandidate != null && AdmissionDigestIsValid(cancelCandidate.RootRequestDigest)
|
|
1461
|
+
? cancelCandidate.RootRequestDigest
|
|
1462
|
+
: AdmissionAcceptedTopDigest(state, response, identity, command);
|
|
1463
|
+
var intended = durable
|
|
1464
|
+
? UnityCliAdmissionPool.AdmissionOwnerState.Coordinator
|
|
1465
|
+
: UnityCliAdmissionPool.AdmissionOwnerState.Recovery;
|
|
1466
|
+
if (directMutation) intended = UnityCliAdmissionPool.AdmissionOwnerState.Recovery;
|
|
1467
|
+
var marker = new Dictionary<string, object>
|
|
1468
|
+
{
|
|
1469
|
+
{ "requestId", identity.RequestId }, { "requestDigest", AdmissionTopDigest(identity) }, { "editorSessionId", EditorSessionId() }
|
|
1470
|
+
};
|
|
1471
|
+
if (command == "internal.test.run.cancel"
|
|
1472
|
+
&& !TryAddCancelAdmissionRootIdentity(state, marker, identity, cancelCandidate, out intended))
|
|
1473
|
+
return AdmissionTransferResult.TransferPending;
|
|
1474
|
+
var updated = PersistAdmissionOwnerTransfer(state, markerScope + "-admission-owner", marker, identity, intended);
|
|
1475
|
+
identity.OwnerState = updated.OwnerState;
|
|
1476
|
+
identity.TopRequestDigest = updated.TopRequestDigest;
|
|
1477
|
+
return AdmissionTransferResult.Transferred;
|
|
1478
|
+
}
|
|
1479
|
+
catch (Exception exception)
|
|
200
1480
|
{
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
identity.OwnerKind = ownerKind;
|
|
1481
|
+
state.Sinks.WriteLog("admission_durable_transfer_pending requestId=" + identity.RequestId + " " + exception.GetType().Name + ": " + exception.Message);
|
|
1482
|
+
return AdmissionTransferResult.TransferPending;
|
|
204
1483
|
}
|
|
205
|
-
state["admissionRootFingerprint"] = identity.RootFingerprint;
|
|
206
|
-
state["admissionToken"] = identity.Token;
|
|
207
|
-
state["admissionSessionId"] = identity.SessionId;
|
|
208
|
-
state["admissionOwnerKind"] = (long)identity.OwnerKind;
|
|
209
1484
|
}
|
|
210
1485
|
|
|
211
|
-
|
|
1486
|
+
/// <summary>pool owner已变化时仍须由exact marker reconcile证明commit与pool persistence均已完成。</summary>
|
|
1487
|
+
private static bool TryConfirmDurableTransferredOwner(BridgeQueueState state, AdmissionOwnerIdentity identity, string command, AdmissionOwnerIdentity current)
|
|
212
1488
|
{
|
|
213
|
-
if (
|
|
214
|
-
|
|
1489
|
+
if (command == "internal.test.run.cancel")
|
|
1490
|
+
return TryConfirmCancelTransferredOwner(state, identity, current);
|
|
1491
|
+
string markerScope;
|
|
1492
|
+
string rootScope;
|
|
1493
|
+
AdmissionOwnerScopes(command, out markerScope, out rootScope);
|
|
1494
|
+
foreach (var candidate in new[]
|
|
215
1495
|
{
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
1496
|
+
markerScope + "-admission-owner",
|
|
1497
|
+
rootScope + "-terminal-cleanup",
|
|
1498
|
+
rootScope + "-pre-effect-cleanup"
|
|
1499
|
+
}.Distinct(StringComparer.Ordinal))
|
|
1500
|
+
{
|
|
1501
|
+
Dictionary<string, object> marker;
|
|
1502
|
+
try { marker = state.LoadOperation(candidate); }
|
|
1503
|
+
catch { return false; }
|
|
1504
|
+
if (marker == null) continue;
|
|
1505
|
+
current.TopRequestDigest = AdmissionText(marker, "requestDigest");
|
|
1506
|
+
current.EditorSessionId = AdmissionText(marker, "editorSessionId");
|
|
1507
|
+
if (!AdmissionMarkerMatchesIdentity(marker, current)) continue;
|
|
1508
|
+
AdmissionOwnerIdentity reconciled;
|
|
1509
|
+
try { reconciled = ReconcileAdmissionTransferForLiveMarker(state, candidate, marker, current.RequestId, current.RequestDigest); }
|
|
1510
|
+
catch { return false; }
|
|
1511
|
+
if (reconciled == null || reconciled.OwnerState != current.OwnerState) return false;
|
|
1512
|
+
identity.OwnerState = reconciled.OwnerState;
|
|
1513
|
+
identity.TopRequestDigest = reconciled.TopRequestDigest;
|
|
1514
|
+
return true;
|
|
1515
|
+
}
|
|
1516
|
+
return false;
|
|
222
1517
|
}
|
|
223
1518
|
|
|
224
|
-
/// <summary>
|
|
225
|
-
private static
|
|
1519
|
+
/// <summary>cancel queue settlement只确认cancel owner marker,不进入通用root/lease reconciliation。</summary>
|
|
1520
|
+
private static bool TryConfirmCancelTransferredOwner(BridgeQueueState state, AdmissionOwnerIdentity identity, AdmissionOwnerIdentity current)
|
|
226
1521
|
{
|
|
227
|
-
|
|
228
|
-
|
|
1522
|
+
const string markerScope = "test-cancel-admission-owner";
|
|
1523
|
+
Dictionary<string, object> marker;
|
|
1524
|
+
try { marker = state.LoadOperation(markerScope); }
|
|
1525
|
+
catch { return false; }
|
|
1526
|
+
var rootRequestId = AdmissionText(marker, "rootRequestId");
|
|
1527
|
+
var rootRequestDigest = AdmissionText(marker, "rootRequestDigest");
|
|
1528
|
+
var runId = AdmissionText(marker, "runId");
|
|
1529
|
+
if (!IsSafeId(rootRequestId) || !AdmissionDigestIsValid(rootRequestDigest) || !Guid.TryParse(runId, out _)
|
|
1530
|
+
|| !string.Equals(AdmissionText(marker, "requestId"), current.RequestId, StringComparison.Ordinal)
|
|
1531
|
+
|| !string.Equals(AdmissionText(marker, "requestDigest"), rootRequestDigest, StringComparison.Ordinal)
|
|
1532
|
+
|| !TryReadCancelAdmissionMarkerState(marker, current.RequestId, current.RequestDigest, rootRequestDigest, runId,
|
|
1533
|
+
out _, out _, out _, out _)) return false;
|
|
1534
|
+
current.TopRequestDigest = rootRequestDigest;
|
|
1535
|
+
current.EditorSessionId = AdmissionText(marker, "editorSessionId");
|
|
1536
|
+
if (!AdmissionMarkerMatchesIdentity(marker, current)) return false;
|
|
1537
|
+
var intended = (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(marker, "admissionIntendedOwnerState");
|
|
1538
|
+
try
|
|
229
1539
|
{
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
1540
|
+
var reconciled = current.OwnerState == intended
|
|
1541
|
+
? current
|
|
1542
|
+
: PersistAdmissionOwnerTransfer(state, markerScope, marker, current, intended);
|
|
1543
|
+
if (current.OwnerState == intended)
|
|
233
1544
|
{
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (!admissionPool.TransferOwner(identity.RequestId, identity.RootFingerprint, identity.Token, identity.SessionId, UnityCliAdmissionPool.AdmissionOwnerKind.Recovery)) { admissionPool.BlockRecovery(); return; }
|
|
238
|
-
identity.OwnerKind = UnityCliAdmissionPool.AdmissionOwnerKind.Recovery;
|
|
239
|
-
}
|
|
240
|
-
CompleteAdmissionRoot(identity);
|
|
1545
|
+
marker["admissionTransferCommitted"] = true;
|
|
1546
|
+
state.SaveOperation(markerScope, marker);
|
|
1547
|
+
if (!state.PersistPool()) return false;
|
|
241
1548
|
}
|
|
1549
|
+
identity.OwnerState = reconciled.OwnerState;
|
|
1550
|
+
identity.TopRequestDigest = rootRequestDigest;
|
|
1551
|
+
return true;
|
|
242
1552
|
}
|
|
1553
|
+
catch { return false; }
|
|
243
1554
|
}
|
|
244
1555
|
|
|
245
|
-
|
|
1556
|
+
/// <summary>cancel首次转移冻结完整parent、cancel、continuation与repair schema;不可执行的半marker不接管WorkItem。</summary>
|
|
1557
|
+
private static bool TryAddCancelAdmissionRootIdentity(
|
|
1558
|
+
BridgeQueueState state,
|
|
1559
|
+
IDictionary<string, object> marker,
|
|
1560
|
+
AdmissionOwnerIdentity identity,
|
|
1561
|
+
BridgeWorkItem.CancelRecoveryCandidate candidate,
|
|
1562
|
+
out UnityCliAdmissionPool.AdmissionOwnerState intendedOwner)
|
|
246
1563
|
{
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
string leaseDigest;
|
|
250
|
-
if (!UnityCliSessionMutationLease.TryGetCurrentOwner(out leaseRequestId, out leaseDigest)) return false;
|
|
251
|
-
if (string.Equals(leaseRequestId, requestId, StringComparison.Ordinal)) { durable = true; return true; }
|
|
252
|
-
|
|
253
|
-
foreach (var scope in new[] { "compile", "package-resolve", "refresh", "asset-refresh", "package-embed", "test-run", "build", "navmesh-bake", "occlusion-bake", "direct-mutation" })
|
|
1564
|
+
intendedOwner = UnityCliAdmissionPool.AdmissionOwnerState.Recovery;
|
|
1565
|
+
try
|
|
254
1566
|
{
|
|
255
|
-
|
|
1567
|
+
var root = state.LoadOperation("test-run");
|
|
1568
|
+
if (root == null)
|
|
1569
|
+
return TryAddCancelAdmissionIdentityFromTerminalProof(state, marker, identity, candidate);
|
|
1570
|
+
object rawOperation;
|
|
1571
|
+
var operation = root.TryGetValue("testOperation", out rawOperation) ? AsDictionary(rawOperation) : null;
|
|
1572
|
+
var rootRequestId = AdmissionText(root, "requestId");
|
|
1573
|
+
var rootRequestDigest = AdmissionText(root, "requestDigest");
|
|
1574
|
+
var runId = AdmissionText(operation, "cancelRunId");
|
|
1575
|
+
if (!IsSafeId(rootRequestId) || !AdmissionDigestIsValid(rootRequestDigest) || !Guid.TryParse(runId, out _)
|
|
1576
|
+
|| !string.Equals(AdmissionText(operation, "cancelRequestId"), identity.RequestId, StringComparison.Ordinal)
|
|
1577
|
+
|| !string.Equals(AdmissionText(operation, "cancelRequestDigest"), identity.RequestDigest, StringComparison.Ordinal)
|
|
1578
|
+
|| !string.Equals(AdmissionText(operation, "cancelRootRequestDigest"), rootRequestDigest, StringComparison.Ordinal)
|
|
1579
|
+
|| !string.Equals(AdmissionTopDigest(identity), rootRequestDigest, StringComparison.Ordinal)) return false;
|
|
1580
|
+
marker["rootRequestId"] = rootRequestId;
|
|
1581
|
+
marker["rootRequestDigest"] = rootRequestDigest;
|
|
1582
|
+
marker["runId"] = runId;
|
|
1583
|
+
marker["cancelRequestId"] = identity.RequestId;
|
|
1584
|
+
marker["cancelRequestDigest"] = identity.RequestDigest;
|
|
1585
|
+
if (candidate == null)
|
|
256
1586
|
{
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (AdmissionStateOwnsRequest(state, requestId)) { durable = true; return true; }
|
|
265
|
-
}
|
|
266
|
-
catch { return false; }
|
|
1587
|
+
if (!string.Equals(AdmissionText(operation, "cancelEffectState"), "dispatched", StringComparison.Ordinal)
|
|
1588
|
+
|| !AdmissionBool(operation, "cancelNeedsContinuation")) return false;
|
|
1589
|
+
marker["cancelContinuationState"] = "dispatched";
|
|
1590
|
+
marker["parentRepairEffectState"] = "dispatched";
|
|
1591
|
+
marker["parentRepairPending"] = false;
|
|
1592
|
+
intendedOwner = UnityCliAdmissionPool.AdmissionOwnerState.Continuation;
|
|
1593
|
+
return true;
|
|
267
1594
|
}
|
|
1595
|
+
if (!string.Equals(candidate.RootRequestId, rootRequestId, StringComparison.Ordinal)
|
|
1596
|
+
|| !string.Equals(candidate.RootRequestDigest, rootRequestDigest, StringComparison.Ordinal)
|
|
1597
|
+
|| !string.Equals(candidate.RunId, runId, StringComparison.Ordinal)
|
|
1598
|
+
|| !CancelTerminalEffectState(candidate.EffectState)
|
|
1599
|
+
|| !CancelTerminalCandidateIsValid(candidate.Envelope, identity.RequestId)) return false;
|
|
1600
|
+
AddCancelRecoveryCandidateFields(marker, identity, candidate);
|
|
1601
|
+
return true;
|
|
268
1602
|
}
|
|
1603
|
+
catch { return false; }
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/// <summary>parent root已清理时,只用WorkItem候选与exact双terminal重建同一恢复marker。</summary>
|
|
1607
|
+
private static bool TryAddCancelAdmissionIdentityFromTerminalProof(
|
|
1608
|
+
BridgeQueueState state,
|
|
1609
|
+
IDictionary<string, object> marker,
|
|
1610
|
+
AdmissionOwnerIdentity identity,
|
|
1611
|
+
BridgeWorkItem.CancelRecoveryCandidate candidate)
|
|
1612
|
+
{
|
|
1613
|
+
if (candidate == null || !IsSafeId(candidate.RootRequestId) || !AdmissionDigestIsValid(candidate.RootRequestDigest)
|
|
1614
|
+
|| !Guid.TryParse(candidate.RunId, out _) || !string.Equals(AdmissionTopDigest(identity), candidate.RootRequestDigest, StringComparison.Ordinal)
|
|
1615
|
+
|| (candidate.EffectState != "cancelled" && candidate.EffectState != "outcome_unknown")
|
|
1616
|
+
|| !CancelTerminalCandidateIsValid(candidate.Envelope, identity.RequestId)) return false;
|
|
1617
|
+
object childTerminal;
|
|
1618
|
+
object selectedChildTerminal;
|
|
1619
|
+
object parentTerminal;
|
|
1620
|
+
if (!state.TryGetTerminal("test-cancel", identity.RequestId, identity.RequestDigest, out childTerminal)
|
|
1621
|
+
|| !state.TryGetTerminalBySelector("test-cancel", identity.RequestId, candidate.RootRequestDigest, out selectedChildTerminal)
|
|
1622
|
+
|| !state.TryGetTerminal("test-run", candidate.RootRequestId, candidate.RootRequestDigest, out parentTerminal)
|
|
1623
|
+
|| !SerializedEqual(childTerminal, selectedChildTerminal)
|
|
1624
|
+
|| !SerializedEqual(childTerminal, candidate.Envelope)
|
|
1625
|
+
|| !CancelParentTerminalCandidateIsValid(parentTerminal, candidate.RootRequestId, candidate.EffectState)) return false;
|
|
1626
|
+
AddCancelRecoveryCandidateFields(marker, identity, candidate);
|
|
269
1627
|
return true;
|
|
270
1628
|
}
|
|
271
1629
|
|
|
272
|
-
private static
|
|
1630
|
+
private static void AddCancelRecoveryCandidateFields(
|
|
1631
|
+
IDictionary<string, object> marker,
|
|
1632
|
+
AdmissionOwnerIdentity identity,
|
|
1633
|
+
BridgeWorkItem.CancelRecoveryCandidate candidate)
|
|
273
1634
|
{
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
1635
|
+
marker["rootRequestId"] = candidate.RootRequestId;
|
|
1636
|
+
marker["rootRequestDigest"] = candidate.RootRequestDigest;
|
|
1637
|
+
marker["runId"] = candidate.RunId;
|
|
1638
|
+
marker["cancelRequestId"] = identity.RequestId;
|
|
1639
|
+
marker["cancelRequestDigest"] = identity.RequestDigest;
|
|
1640
|
+
marker["cancelContinuationState"] = "continuation_pending";
|
|
1641
|
+
marker["parentRepairEffectState"] = candidate.EffectState;
|
|
1642
|
+
marker["parentRepairPending"] = true;
|
|
1643
|
+
marker["cancelTerminalEnvelope"] = candidate.Envelope;
|
|
278
1644
|
}
|
|
279
1645
|
|
|
280
|
-
private static bool
|
|
1646
|
+
private static bool CancelParentTerminalCandidateIsValid(object envelope, string rootRequestId, string effectState)
|
|
281
1647
|
{
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
1648
|
+
try
|
|
1649
|
+
{
|
|
1650
|
+
var value = AsDictionary(UnityCliSerializer.Parse(UnityCliSerializer.Serialize(envelope)));
|
|
1651
|
+
object ok;
|
|
1652
|
+
object rawError;
|
|
1653
|
+
var error = value != null && value.TryGetValue("error", out rawError) ? AsDictionary(rawError) : null;
|
|
1654
|
+
var expectedCode = effectState == "cancelled" ? "E_TEST_RUN_CANCELLED" : "E_EXECUTION_TIMEOUT";
|
|
1655
|
+
return value != null && string.Equals(AdmissionText(value, "requestId"), rootRequestId, StringComparison.Ordinal)
|
|
1656
|
+
&& value.TryGetValue("ok", out ok) && ok is bool && !(bool)ok
|
|
1657
|
+
&& error != null && string.Equals(AdmissionText(error, "code"), expectedCode, StringComparison.Ordinal);
|
|
1658
|
+
}
|
|
1659
|
+
catch { return false; }
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
private static bool SerializedEqual(object left, object right)
|
|
1663
|
+
{
|
|
1664
|
+
try { return string.Equals(UnityCliSerializer.Serialize(left), UnityCliSerializer.Serialize(right), StringComparison.Ordinal); }
|
|
1665
|
+
catch { return false; }
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
private static bool CancelTerminalEffectState(string value)
|
|
1669
|
+
{
|
|
1670
|
+
return string.Equals(value, "not_invoked", StringComparison.Ordinal)
|
|
1671
|
+
|| string.Equals(value, "outcome_unknown", StringComparison.Ordinal)
|
|
1672
|
+
|| string.Equals(value, "cancelled", StringComparison.Ordinal);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
/// <summary>候选terminal必须是exact request envelope且受terminal单条上限约束。</summary>
|
|
1676
|
+
private static bool CancelTerminalCandidateIsValid(object envelope, string cancelRequestId)
|
|
1677
|
+
{
|
|
1678
|
+
try
|
|
1679
|
+
{
|
|
1680
|
+
var serialized = UnityCliSerializer.Serialize(envelope);
|
|
1681
|
+
if (Encoding.UTF8.GetByteCount(serialized) > MaxCancelRecoveryEnvelopeBytes) return false;
|
|
1682
|
+
var value = AsDictionary(UnityCliSerializer.Parse(serialized));
|
|
1683
|
+
object ok;
|
|
1684
|
+
return value != null && string.Equals(AdmissionText(value, "requestId"), cancelRequestId, StringComparison.Ordinal)
|
|
1685
|
+
&& value.TryGetValue("ok", out ok) && ok is bool;
|
|
1686
|
+
}
|
|
1687
|
+
catch { return false; }
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/// <summary>reload与在线retry共用的cancel marker状态闭集。</summary>
|
|
1691
|
+
private static bool TryReadCancelAdmissionMarkerState(
|
|
1692
|
+
IDictionary<string, object> marker,
|
|
1693
|
+
string cancelRequestId,
|
|
1694
|
+
string cancelRequestDigest,
|
|
1695
|
+
string rootRequestDigest,
|
|
1696
|
+
string runId,
|
|
1697
|
+
out string continuationState,
|
|
1698
|
+
out string repairEffectState,
|
|
1699
|
+
out bool repairPending,
|
|
1700
|
+
out object candidateEnvelope)
|
|
1701
|
+
{
|
|
1702
|
+
continuationState = AdmissionText(marker, "cancelContinuationState");
|
|
1703
|
+
repairEffectState = AdmissionText(marker, "parentRepairEffectState");
|
|
1704
|
+
repairPending = false;
|
|
1705
|
+
candidateEnvelope = null;
|
|
1706
|
+
object repairPendingValue;
|
|
1707
|
+
if (marker == null || !marker.TryGetValue("parentRepairPending", out repairPendingValue) || !(repairPendingValue is bool)
|
|
1708
|
+
|| !string.Equals(AdmissionText(marker, "cancelRequestId"), cancelRequestId, StringComparison.Ordinal)
|
|
1709
|
+
|| !string.Equals(AdmissionText(marker, "cancelRequestDigest"), cancelRequestDigest, StringComparison.Ordinal)
|
|
1710
|
+
|| !string.Equals(AdmissionText(marker, "rootRequestDigest"), rootRequestDigest, StringComparison.Ordinal)
|
|
1711
|
+
|| !string.Equals(AdmissionText(marker, "runId"), runId, StringComparison.Ordinal)) return false;
|
|
1712
|
+
repairPending = (bool)repairPendingValue;
|
|
1713
|
+
object rawCandidate;
|
|
1714
|
+
if (string.Equals(continuationState, "dispatched", StringComparison.Ordinal))
|
|
1715
|
+
return string.Equals(repairEffectState, "dispatched", StringComparison.Ordinal) && !repairPending
|
|
1716
|
+
&& !marker.TryGetValue("cancelTerminalEnvelope", out rawCandidate);
|
|
1717
|
+
if (!string.Equals(continuationState, "continuation_pending", StringComparison.Ordinal)
|
|
1718
|
+
|| !CancelTerminalEffectState(repairEffectState)
|
|
1719
|
+
|| !marker.TryGetValue("cancelTerminalEnvelope", out rawCandidate)
|
|
1720
|
+
|| !CancelTerminalCandidateIsValid(rawCandidate, cancelRequestId)) return false;
|
|
1721
|
+
candidateEnvelope = rawCandidate;
|
|
293
1722
|
return true;
|
|
294
1723
|
}
|
|
295
1724
|
|
|
1725
|
+
private static bool TryHasExactDurableBusinessRoot(BridgeQueueState queueState, string rootScope, string command, AdmissionOwnerIdentity identity, out bool durable)
|
|
1726
|
+
{
|
|
1727
|
+
durable = false;
|
|
1728
|
+
try
|
|
1729
|
+
{
|
|
1730
|
+
var operationRoot = queueState.LoadOperation(rootScope);
|
|
1731
|
+
if (operationRoot == null)
|
|
1732
|
+
{
|
|
1733
|
+
if (command == "internal.test.run.cancel") return false;
|
|
1734
|
+
bool ownsLease;
|
|
1735
|
+
if (!queueState.TryIsLeaseOwned(identity.RequestId, identity.RequestDigest, out ownsLease)) return false;
|
|
1736
|
+
if (command == "project-tool.run" || command == "code.exec")
|
|
1737
|
+
{
|
|
1738
|
+
durable = ownsLease;
|
|
1739
|
+
return true;
|
|
1740
|
+
}
|
|
1741
|
+
// durable coordinator无root但仍持有exact lease时,不能把错误响应归约为未接受。
|
|
1742
|
+
return !ownsLease;
|
|
1743
|
+
}
|
|
1744
|
+
if (!string.Equals(AdmissionText(operationRoot, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)) return true;
|
|
1745
|
+
if (command == "internal.test.run.cancel")
|
|
1746
|
+
{
|
|
1747
|
+
object rawOperation;
|
|
1748
|
+
var operation = operationRoot.TryGetValue("testOperation", out rawOperation) ? AsDictionary(rawOperation) : null;
|
|
1749
|
+
var exactCancel = operation != null && string.Equals(AdmissionText(operation, "cancelRequestId"), identity.RequestId, StringComparison.Ordinal)
|
|
1750
|
+
&& string.Equals(AdmissionText(operation, "cancelRequestDigest"), identity.RequestDigest, StringComparison.Ordinal);
|
|
1751
|
+
durable = exactCancel && (AdmissionBool(operation, "cancelNeedsContinuation")
|
|
1752
|
+
|| new[] { "dispatching", "dispatched", "outcome_unknown", "terminal_cleanup" }.Contains(AdmissionText(operation, "cancelEffectState")));
|
|
1753
|
+
return true;
|
|
1754
|
+
}
|
|
1755
|
+
durable = string.Equals(AdmissionText(operationRoot, "requestId"), identity.RequestId, StringComparison.Ordinal)
|
|
1756
|
+
&& string.Equals(AdmissionText(operationRoot, "requestDigest"), identity.RequestDigest, StringComparison.Ordinal);
|
|
1757
|
+
return true;
|
|
1758
|
+
}
|
|
1759
|
+
catch { return false; }
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
private static void AdmissionOwnerScopes(string command, out string markerScope, out string rootScope)
|
|
1763
|
+
{
|
|
1764
|
+
markerScope = AdmissionTerminalScope(command);
|
|
1765
|
+
rootScope = markerScope == "test-cancel" ? "test-run" : markerScope;
|
|
1766
|
+
if (string.IsNullOrEmpty(markerScope)) throw new InvalidOperationException("Admission durable command has no owner scope.");
|
|
1767
|
+
}
|
|
1768
|
+
|
|
296
1769
|
/// <summary>deadline之后仍允许同字节direct request读取已发布terminal,不入队也不重新占slot。</summary>
|
|
297
1770
|
private static string AdmissionRecoveredDirectTerminal(UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes)
|
|
298
1771
|
{
|
|
299
1772
|
if (request == null || rawRequestBytes == null || (request.Command != "code.exec" && request.Command != "project-tool.run")) return null;
|
|
300
1773
|
var digest = UnityCliRequestDigest.Compute(rawRequestBytes, request.InstanceId);
|
|
301
1774
|
object terminal;
|
|
302
|
-
|
|
1775
|
+
if (!UnityCliActiveOperation.TryGetTerminal("direct-mutation", request.RequestId, digest, out terminal)) return null;
|
|
1776
|
+
CleanupPublishedDirectMutation(request.RequestId, digest);
|
|
1777
|
+
return UnityCliSerializer.Serialize(terminal);
|
|
303
1778
|
}
|
|
304
1779
|
|
|
305
1780
|
/// <summary>active admission已收敛后,只允许同字节请求读取现有exact durable terminal。</summary>
|
|
@@ -351,49 +1826,64 @@ namespace UnityCli
|
|
|
351
1826
|
catch { return false; }
|
|
352
1827
|
}
|
|
353
1828
|
|
|
354
|
-
private static bool AdmissionResponseHasTrustedTerminal(string response)
|
|
355
|
-
{
|
|
356
|
-
try
|
|
357
|
-
{
|
|
358
|
-
var envelope = AsDictionary(UnityCliSerializer.Parse(response));
|
|
359
|
-
object ok;
|
|
360
|
-
if (envelope == null || !envelope.TryGetValue("ok", out ok) || !(ok is bool)) return false;
|
|
361
|
-
if ((bool)ok) return true;
|
|
362
|
-
object rawError;
|
|
363
|
-
var error = envelope.TryGetValue("error", out rawError) ? AsDictionary(rawError) : null;
|
|
364
|
-
var code = AdmissionText(error, "code");
|
|
365
|
-
return error != null && code != "E_EXECUTION_TIMEOUT" && code != "E_UNITY_DOMAIN_RELOAD"
|
|
366
|
-
&& code != "E_UNITY_COMPILING" && code != "E_UNITY_IMPORTING" && code != "E_UNITY_PLAY_MODE"
|
|
367
|
-
&& code != "E_QUEUE_START_TIMEOUT";
|
|
368
|
-
}
|
|
369
|
-
catch { return false; }
|
|
370
|
-
}
|
|
371
|
-
|
|
372
1829
|
private static bool AdmissionDurableRootCommand(string command)
|
|
373
1830
|
{
|
|
374
1831
|
return command == "internal.build.run.start" || command == "internal.editor.compile.start"
|
|
375
1832
|
|| command == "internal.editor.refresh.start" || command == "internal.asset.refresh.start"
|
|
376
1833
|
|| command == "internal.package.embed.start" || command == "internal.package.resolve.start"
|
|
377
1834
|
|| command == "internal.test.run.start" || command == "internal.test.run.cancel"
|
|
378
|
-
|| command == "internal.navmesh.bake.start" || command == "internal.occlusion.bake.start"
|
|
1835
|
+
|| command == "internal.navmesh.bake.start" || command == "internal.occlusion.bake.start"
|
|
1836
|
+
|| command == "project-tool.run" || command == "code.exec";
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
private static string AdmissionAcceptedTopDigest(BridgeQueueState state, string response, AdmissionOwnerIdentity identity, string command)
|
|
1840
|
+
{
|
|
1841
|
+
try
|
|
1842
|
+
{
|
|
1843
|
+
var envelope = AsDictionary(UnityCliSerializer.Parse(response));
|
|
1844
|
+
object rawData;
|
|
1845
|
+
var data = envelope != null && envelope.TryGetValue("data", out rawData) ? AsDictionary(rawData) : null;
|
|
1846
|
+
var digest = AdmissionText(data, "rootRequestDigest");
|
|
1847
|
+
if (AdmissionDigestIsValid(digest)) return digest;
|
|
1848
|
+
if (command == "internal.test.run.cancel")
|
|
1849
|
+
{
|
|
1850
|
+
var root = state.LoadOperation("test-run");
|
|
1851
|
+
object rawOperation;
|
|
1852
|
+
var operation = root != null && root.TryGetValue("testOperation", out rawOperation) ? AsDictionary(rawOperation) : null;
|
|
1853
|
+
digest = AdmissionText(operation, "cancelRootRequestDigest");
|
|
1854
|
+
if (AdmissionDigestIsValid(digest)) return digest;
|
|
1855
|
+
}
|
|
1856
|
+
return identity.RequestDigest;
|
|
1857
|
+
}
|
|
1858
|
+
catch { return identity.RequestDigest; }
|
|
379
1859
|
}
|
|
380
1860
|
|
|
381
1861
|
private static bool AdmissionRecoveryCommand(string command)
|
|
382
1862
|
{
|
|
383
|
-
return command
|
|
1863
|
+
return IsProjectToolCatalogReadCommand(command) || command == "internal.editor.state.snapshot"
|
|
1864
|
+
|| IsDoctorAdmissionBypassCommand(command) || UnityCliDurableStatusCoordinator.IsCommand(command)
|
|
384
1865
|
|| UnityCliCompileCoordinator.IsCommand(command) || UnityCliLiveBuildCoordinator.IsCommand(command)
|
|
385
1866
|
|| UnityCliLiveRefreshCoordinator.IsCommand(command) || UnityCliAssetRefreshCoordinator.IsCommand(command)
|
|
386
1867
|
|| UnityCliPackageEmbedCoordinator.IsCommand(command) || UnityCliPackageResolveCoordinator.IsCommand(command)
|
|
387
1868
|
|| UnityCliTestRunCoordinator.IsCommand(command) || UnityCliNavMeshBakeCoordinator.IsCommand(command)
|
|
388
|
-
|| UnityCliOcclusionBakeCoordinator.IsCommand(command)
|
|
1869
|
+
|| UnityCliOcclusionBakeCoordinator.IsCommand(command)
|
|
1870
|
+
|| command == "project-tool.run" || command == "code.exec";
|
|
389
1871
|
}
|
|
390
1872
|
|
|
391
1873
|
private static bool AdmissionBypassCommand(string command)
|
|
392
1874
|
{
|
|
1875
|
+
if (IsDoctorAdmissionBypassCommand(command)) return true;
|
|
393
1876
|
switch (command)
|
|
394
1877
|
{
|
|
1878
|
+
case "project-tool.health":
|
|
1879
|
+
case "project-tool.groups":
|
|
1880
|
+
case "project-tool.summary":
|
|
1881
|
+
case "project-tool.describe":
|
|
395
1882
|
case "project-tool.list":
|
|
396
|
-
case "
|
|
1883
|
+
case "runtime-tool.groups":
|
|
1884
|
+
case "runtime-tool.summary":
|
|
1885
|
+
case "runtime-tool.describe":
|
|
1886
|
+
case "runtime-tool.list":
|
|
397
1887
|
case "internal.editor.state.snapshot":
|
|
398
1888
|
case "internal.editor.compile.status":
|
|
399
1889
|
case "internal.editor.refresh.status":
|
|
@@ -406,7 +1896,6 @@ namespace UnityCli
|
|
|
406
1896
|
case "internal.package.files.fail":
|
|
407
1897
|
case "internal.test.run.status":
|
|
408
1898
|
case "internal.test.run.cancel.plan":
|
|
409
|
-
case "internal.test.run.cancel":
|
|
410
1899
|
case "internal.test.run.cancel.status":
|
|
411
1900
|
case "internal.navmesh.bake.status":
|
|
412
1901
|
case "internal.navmesh.bake.plan":
|
|
@@ -422,9 +1911,139 @@ namespace UnityCli
|
|
|
422
1911
|
}
|
|
423
1912
|
}
|
|
424
1913
|
|
|
425
|
-
|
|
1914
|
+
/// <summary>已完成transfer的marker更新业务字段时复制原source→intended证据,禁止伪造同态transfer。</summary>
|
|
1915
|
+
private static bool CopyCommittedAdmissionOwnerFields(IDictionary<string, object> source, IDictionary<string, object> target, AdmissionOwnerIdentity current, string topRequestDigest)
|
|
1916
|
+
{
|
|
1917
|
+
if (!AdmissionMarkerHasImmutableIdentity(source, current, topRequestDigest)
|
|
1918
|
+
|| (UnityCliAdmissionPool.AdmissionOwnerState)AdmissionLong(source, "admissionIntendedOwnerState") != current.OwnerState)
|
|
1919
|
+
return false;
|
|
1920
|
+
foreach (var key in new[]
|
|
1921
|
+
{
|
|
1922
|
+
"admissionRootFingerprint", "admissionToken", "admissionSessionId", "admissionListenerId",
|
|
1923
|
+
"admissionListenerGeneration", "admissionExecutionDeadlineMs", "admissionRequestDigest",
|
|
1924
|
+
"admissionOwnerState", "admissionIntendedOwnerState"
|
|
1925
|
+
})
|
|
1926
|
+
{
|
|
1927
|
+
object value;
|
|
1928
|
+
if (!source.TryGetValue(key, out value)) return false;
|
|
1929
|
+
target[key] = value;
|
|
1930
|
+
}
|
|
1931
|
+
target["admissionTransferCommitted"] = true;
|
|
1932
|
+
return true;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
private static void WriteAdmissionOwnerFields(IDictionary<string, object> state, AdmissionOwnerIdentity identity, UnityCliAdmissionPool.AdmissionOwnerState intendedOwner, bool committed)
|
|
1936
|
+
{
|
|
1937
|
+
state["admissionRootFingerprint"] = identity.RootFingerprint;
|
|
1938
|
+
state["admissionToken"] = identity.Token;
|
|
1939
|
+
state["admissionSessionId"] = identity.SessionId;
|
|
1940
|
+
state["admissionListenerId"] = identity.ListenerId;
|
|
1941
|
+
state["admissionListenerGeneration"] = identity.ListenerGeneration;
|
|
1942
|
+
state["admissionExecutionDeadlineMs"] = identity.ExecutionDeadlineMs;
|
|
1943
|
+
state["admissionRequestDigest"] = identity.RequestDigest;
|
|
1944
|
+
state["admissionOwnerState"] = (long)identity.OwnerState;
|
|
1945
|
+
state["admissionIntendedOwnerState"] = (long)intendedOwner;
|
|
1946
|
+
state["admissionTransferCommitted"] = committed;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
private static AdmissionOwnerIdentity ToAdmissionOwner(UnityCliAdmissionPool.ConsumedIdentity identity, string requestDigest)
|
|
1950
|
+
{
|
|
1951
|
+
if (identity == null) return null;
|
|
1952
|
+
if (!string.IsNullOrEmpty(requestDigest) && !string.Equals(identity.RequestDigest, requestDigest, StringComparison.Ordinal))
|
|
1953
|
+
throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "Admission owner request digest does not match the frozen WorkItem.");
|
|
1954
|
+
return new AdmissionOwnerIdentity { RequestId = identity.RequestId, RequestDigest = identity.RequestDigest, TopRequestDigest = identity.RequestDigest, RootFingerprint = identity.RootFingerprint, Token = identity.Token, SessionId = identity.SessionId, ListenerId = identity.ListenerId, ListenerGeneration = identity.ListenerGeneration, ExecutionDeadlineMs = identity.ExecutionDeadlineMs, EditorSessionId = EditorSessionId(), OwnerState = identity.OwnerState };
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
private static UnityCliAdmissionPool.ConsumedIdentity ToPoolIdentity(AdmissionOwnerIdentity identity)
|
|
1958
|
+
{
|
|
1959
|
+
return identity == null ? null : UnityCliAdmissionPool.CreateConsumedIdentity(identity.RequestId, identity.RootFingerprint, identity.RequestDigest, identity.Token, identity.SessionId, identity.OwnerState, identity.ListenerId, identity.ListenerGeneration, identity.ExecutionDeadlineMs);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
private static AdmissionOwnerIdentity RecordIdentity(UnityCliAdmissionPool.AdmissionRecord record)
|
|
1963
|
+
{
|
|
1964
|
+
return new AdmissionOwnerIdentity { RequestId = record.RequestId, RequestDigest = record.RequestDigest, TopRequestDigest = record.RequestDigest, RootFingerprint = record.RootFingerprint, Token = record.Token, SessionId = record.SessionId, ListenerId = record.ListenerId, ListenerGeneration = record.ListenerGeneration, ExecutionDeadlineMs = record.ExecutionDeadlineMs, EditorSessionId = EditorSessionId(), OwnerState = record.OwnerState };
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
private static bool SameAdmissionTransferIdentityExceptListener(AdmissionOwnerIdentity left, AdmissionOwnerIdentity right)
|
|
1968
|
+
{
|
|
1969
|
+
return left != null && right != null
|
|
1970
|
+
&& string.Equals(left.RequestId, right.RequestId, StringComparison.Ordinal)
|
|
1971
|
+
&& string.Equals(left.RequestDigest, right.RequestDigest, StringComparison.Ordinal)
|
|
1972
|
+
&& string.Equals(left.RootFingerprint, right.RootFingerprint, StringComparison.Ordinal)
|
|
1973
|
+
&& string.Equals(left.Token, right.Token, StringComparison.Ordinal)
|
|
1974
|
+
&& string.Equals(left.SessionId, right.SessionId, StringComparison.Ordinal)
|
|
1975
|
+
&& left.ExecutionDeadlineMs == right.ExecutionDeadlineMs
|
|
1976
|
+
&& string.Equals(left.EditorSessionId ?? EditorSessionId(), right.EditorSessionId ?? EditorSessionId(), StringComparison.Ordinal);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
private static bool SameAdmissionImmutableIdentity(AdmissionOwnerIdentity left, AdmissionOwnerIdentity right)
|
|
1980
|
+
{
|
|
1981
|
+
return left != null && right != null
|
|
1982
|
+
&& string.Equals(left.RequestId, right.RequestId, StringComparison.Ordinal)
|
|
1983
|
+
&& string.Equals(left.RequestDigest, right.RequestDigest, StringComparison.Ordinal)
|
|
1984
|
+
&& string.Equals(left.RootFingerprint, right.RootFingerprint, StringComparison.Ordinal)
|
|
1985
|
+
&& string.Equals(left.Token, right.Token, StringComparison.Ordinal)
|
|
1986
|
+
&& string.Equals(left.SessionId, right.SessionId, StringComparison.Ordinal)
|
|
1987
|
+
&& string.Equals(left.ListenerId, right.ListenerId, StringComparison.Ordinal)
|
|
1988
|
+
&& left.ListenerGeneration == right.ListenerGeneration
|
|
1989
|
+
&& left.ExecutionDeadlineMs == right.ExecutionDeadlineMs
|
|
1990
|
+
&& string.Equals(left.EditorSessionId ?? EditorSessionId(), right.EditorSessionId ?? EditorSessionId(), StringComparison.Ordinal);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
private static string AdmissionTopDigest(AdmissionOwnerIdentity identity)
|
|
1994
|
+
{
|
|
1995
|
+
return identity == null || !AdmissionDigestIsValid(identity.TopRequestDigest) ? identity == null ? string.Empty : identity.RequestDigest : identity.TopRequestDigest;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
private static bool AdmissionDigestIsValid(string value)
|
|
1999
|
+
{
|
|
2000
|
+
if (string.IsNullOrEmpty(value) || value.Length != 64) return false;
|
|
2001
|
+
foreach (var character in value) if (!((character >= '0' && character <= '9') || (character >= 'a' && character <= 'f'))) return false;
|
|
2002
|
+
return true;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
internal static bool ClearExactAdmissionOwnerMarkers(AdmissionOwnerIdentity identity)
|
|
2006
|
+
{
|
|
2007
|
+
return ClearExactAdmissionOwnerMarkers(ProductionQueueState, identity);
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
internal static bool ClearExactAdmissionOwnerMarkers(BridgeQueueState state, AdmissionOwnerIdentity identity)
|
|
2011
|
+
{
|
|
2012
|
+
return ClearAdmissionOwnerMarkersForState(state, identity);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
internal static bool ClearAdmissionOwnerMarkersForState(BridgeQueueState state, AdmissionOwnerIdentity identity)
|
|
2016
|
+
{
|
|
2017
|
+
if (state == null || identity == null) return false;
|
|
2018
|
+
return ClearAdmissionOwnerMarkers(state, identity);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
private static bool ClearProductionAdmissionOwnerMarkers(AdmissionOwnerIdentity identity)
|
|
2022
|
+
{
|
|
2023
|
+
return ClearAdmissionOwnerMarkers(ProductionQueueState, identity);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
private static bool ClearAdmissionOwnerMarkers(BridgeQueueState state, AdmissionOwnerIdentity identity)
|
|
426
2027
|
{
|
|
427
|
-
if (
|
|
2028
|
+
if (state == null || identity == null) return false;
|
|
2029
|
+
var cleared = true;
|
|
2030
|
+
foreach (var scope in AdmissionDurableScopes)
|
|
2031
|
+
{
|
|
2032
|
+
var markerScope = scope + "-admission-owner";
|
|
2033
|
+
try
|
|
2034
|
+
{
|
|
2035
|
+
var marker = state.LoadOperation(markerScope);
|
|
2036
|
+
if (marker == null || !AdmissionMarkerHasImmutableIdentity(marker, identity, AdmissionTopDigest(identity))) continue;
|
|
2037
|
+
state.ClearOperation(markerScope);
|
|
2038
|
+
if (state.OperationExists(markerScope)) cleared = false;
|
|
2039
|
+
}
|
|
2040
|
+
catch (Exception exception)
|
|
2041
|
+
{
|
|
2042
|
+
cleared = false;
|
|
2043
|
+
state.Sinks.WriteLog("admission_owner_marker_cleanup_pending requestId=" + identity.RequestId + " scope=" + markerScope + " " + exception.GetType().Name);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
return cleared;
|
|
428
2047
|
}
|
|
429
2048
|
|
|
430
2049
|
private static Dictionary<string, object> StrictAdmissionObject(byte[] rawBody, string operation, params string[] allowed)
|