@alpsckr/unitycli 0.4.3 → 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 +11 -2
- 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
|
@@ -28,20 +28,77 @@ namespace UnityCli
|
|
|
28
28
|
{
|
|
29
29
|
public static partial class UnityCliBridge
|
|
30
30
|
{
|
|
31
|
-
private static readonly
|
|
32
|
-
private static readonly HashSet<BridgeWorkItem> RunningWork = new HashSet<BridgeWorkItem>();
|
|
33
|
-
private static readonly Dictionary<string, BridgeWorkItem> ActiveRequestWork = new Dictionary<string, BridgeWorkItem>(StringComparer.Ordinal);
|
|
34
|
-
private const int MaxWorkStartsPerTick = 16;
|
|
31
|
+
private static readonly BridgeQueueState ProductionQueueState = CreateProductionQueueState();
|
|
35
32
|
|
|
36
|
-
private static
|
|
33
|
+
private static BridgeQueueState CreateProductionQueueState()
|
|
37
34
|
{
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
return new BridgeQueueState(
|
|
36
|
+
null,
|
|
37
|
+
null,
|
|
38
|
+
new BridgeQueueSinks(WriteLog, UnityCliTransactionLog.Append, true),
|
|
39
|
+
new BridgeQueueTestHooks(false),
|
|
40
|
+
PersistProductionAdmissionPoolState,
|
|
41
|
+
identity => PersistProductionAdmissionCompletionIntent(identity, "admission-completion"),
|
|
42
|
+
HasProductionAdmissionCompletionIntent,
|
|
43
|
+
ClearProductionAdmissionCompletionIntent,
|
|
44
|
+
null);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
internal static BridgeQueueState CreateQueueStateForTests(UnityCliAdmissionPool pool, Func<long> monotonicNowMs = null)
|
|
48
|
+
{
|
|
49
|
+
if (pool == null) throw new ArgumentNullException("pool");
|
|
50
|
+
var intentSync = new object();
|
|
51
|
+
var intents = new HashSet<string>(StringComparer.Ordinal);
|
|
52
|
+
var state = new BridgeQueueState(
|
|
53
|
+
pool,
|
|
54
|
+
monotonicNowMs,
|
|
55
|
+
new BridgeQueueSinks(_ => { }, (_, __, ___, ____) => { }, false),
|
|
56
|
+
new BridgeQueueTestHooks(true),
|
|
57
|
+
() => true,
|
|
58
|
+
identity => { lock (intentSync) return intents.Add(CompletionIntentKey(identity)); },
|
|
59
|
+
identity => { lock (intentSync) return intents.Contains(CompletionIntentKey(identity)); },
|
|
60
|
+
identity => { lock (intentSync) return intents.Remove(CompletionIntentKey(identity)); },
|
|
61
|
+
new BridgeIsolatedExecutionState())
|
|
62
|
+
{
|
|
63
|
+
QueuePublicationOpen = true,
|
|
64
|
+
QueueSessionStopping = false,
|
|
65
|
+
ListenerId = "test-listener-" + Guid.NewGuid().ToString("N"),
|
|
66
|
+
ListenerGeneration = 1
|
|
67
|
+
};
|
|
68
|
+
return state;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// <summary>关闭publication与摘除全部未启动WorkItem共享同一状态锁线性化点。</summary>
|
|
72
|
+
private static List<BridgeWorkItem> CloseQueuePublicationAndTakeQueuedWork(bool sessionStopping = false)
|
|
73
|
+
{
|
|
74
|
+
return CloseQueuePublicationAndTakeQueuedWork(ProductionQueueState, sessionStopping);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static List<BridgeWorkItem> CloseQueuePublicationAndTakeQueuedWork(BridgeQueueState state, bool sessionStopping)
|
|
78
|
+
{
|
|
79
|
+
lock (state.QueueLock)
|
|
40
80
|
{
|
|
41
|
-
|
|
42
|
-
|
|
81
|
+
if (sessionStopping) state.TestHooks.Hit("before-queue-shutdown-close");
|
|
82
|
+
state.QueuePublicationOpen = false;
|
|
83
|
+
if (sessionStopping) state.QueueSessionStopping = true;
|
|
84
|
+
var pending = new List<BridgeWorkItem>(state.WorkQueue);
|
|
85
|
+
foreach (var item in pending)
|
|
86
|
+
{
|
|
87
|
+
item.QueueNode = null;
|
|
88
|
+
item.SafeStatusNode = null;
|
|
89
|
+
item.ControlNode = null;
|
|
90
|
+
}
|
|
91
|
+
state.WorkQueue.Clear();
|
|
92
|
+
state.SafeStatusQueue.Clear();
|
|
93
|
+
state.ControlQueue.Clear();
|
|
94
|
+
state.ControlScanCursor = null;
|
|
95
|
+
return pending;
|
|
43
96
|
}
|
|
44
|
-
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private static void CancelQueuedWork(IEnumerable<BridgeWorkItem> pending)
|
|
100
|
+
{
|
|
101
|
+
foreach (var item in pending ?? Array.Empty<BridgeWorkItem>())
|
|
45
102
|
{
|
|
46
103
|
item.CancelBeforeStart();
|
|
47
104
|
item.Response = UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
|
|
@@ -55,8 +112,65 @@ namespace UnityCli
|
|
|
55
112
|
}
|
|
56
113
|
}
|
|
57
114
|
|
|
115
|
+
/// <summary>Editor退出时不再依赖后续update;只按captured pool完整身份隔离完成未启动WorkItem。</summary>
|
|
116
|
+
private static void CancelQueuedWorkForShutdown(IEnumerable<BridgeWorkItem> pending)
|
|
117
|
+
{
|
|
118
|
+
foreach (var item in pending ?? Array.Empty<BridgeWorkItem>())
|
|
119
|
+
{
|
|
120
|
+
if (!item.CancelBeforeStart()) continue;
|
|
121
|
+
item.Response = UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
|
|
122
|
+
item.Request.RequestId,
|
|
123
|
+
"E_UNITY_QUITTING",
|
|
124
|
+
"Unity quit before the queued request began any effect.",
|
|
125
|
+
nextActions: new[] { "Restart Unity and submit a new request." }));
|
|
126
|
+
CompleteQueuedWorkForShutdown(item);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private static bool CompleteQueuedWorkForShutdown(BridgeWorkItem item)
|
|
131
|
+
{
|
|
132
|
+
if (item == null || !item.TryBeginCompletion()) return false;
|
|
133
|
+
var outcome = item.AdmissionOwner == null
|
|
134
|
+
? UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent
|
|
135
|
+
: item.CapturedAdmissionPool == null
|
|
136
|
+
? UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked
|
|
137
|
+
: item.CapturedAdmissionPool.CompleteWorkItemForShutdown(ToPoolIdentity(item.AdmissionOwner));
|
|
138
|
+
if (outcome != UnityCliAdmissionPool.CompletionOutcome.ExactCurrentCompleted
|
|
139
|
+
&& outcome != UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent)
|
|
140
|
+
{
|
|
141
|
+
item.AllowCompletionRetry();
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
var state = item.QueueState;
|
|
145
|
+
lock (state.QueueLock)
|
|
146
|
+
{
|
|
147
|
+
state.RunningWork.Remove(item);
|
|
148
|
+
state.PendingSettlementWork.Remove(item);
|
|
149
|
+
RemoveActiveWorkItem(state, item);
|
|
150
|
+
}
|
|
151
|
+
CompleteAndLogWorkItem(item);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
internal static void SweepQueuedWorkForTests(BridgeQueueState state)
|
|
156
|
+
{
|
|
157
|
+
if (state == null) throw new ArgumentNullException("state");
|
|
158
|
+
CancelQueuedWork(CloseQueuePublicationAndTakeQueuedWork(state, false));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
internal static void ShutdownQueuedWorkForTests(BridgeQueueState state)
|
|
162
|
+
{
|
|
163
|
+
if (state == null) throw new ArgumentNullException("state");
|
|
164
|
+
CancelQueuedWorkForShutdown(CloseQueuePublicationAndTakeQueuedWork(state, true));
|
|
165
|
+
ShutdownAdmissionPool(state);
|
|
166
|
+
}
|
|
167
|
+
|
|
58
168
|
private static void ProcessQueuedWork()
|
|
59
169
|
{
|
|
170
|
+
var state = ProductionQueueState;
|
|
171
|
+
state.ListenerId = ListenerId;
|
|
172
|
+
state.ListenerGeneration = Math.Max(1, CurrentListenerGeneration());
|
|
173
|
+
state.AdmissionPool = admissionPool;
|
|
60
174
|
// pulse只证明EditorApplication.update最近推进;具体请求仍由TryStart证明开始。
|
|
61
175
|
Volatile.Write(ref mainThreadPulseTimestamp, System.Diagnostics.Stopwatch.GetTimestamp());
|
|
62
176
|
Interlocked.Increment(ref mainThreadPulseSequence);
|
|
@@ -78,58 +192,306 @@ namespace UnityCli
|
|
|
78
192
|
AdvanceBridgeStartup();
|
|
79
193
|
UnityCliSessionMutationLease.TryFinalizeOwnedPlayModeRoot();
|
|
80
194
|
UnityCliWaitScheduler.Tick();
|
|
195
|
+
RetryDirectMutationTerminalCleanup();
|
|
196
|
+
RetryPendingWorkItemSettlements();
|
|
81
197
|
|
|
82
198
|
// 新listener先保留至少一个主线程update的transitioning状态;后续生命周期变化即时发布,30秒刷新只修复漏事件。
|
|
83
|
-
if (listenerWasStarted && started
|
|
199
|
+
if (listenerWasStarted && started && InitialPublicationGate.Published
|
|
84
200
|
&& (readyPublicationPending || lifecycleChanged || EditorApplication.timeSinceStartup >= nextRegistryRefreshAt))
|
|
85
201
|
{
|
|
86
202
|
readyPublicationPending = false;
|
|
87
203
|
RegisterInstance("ready");
|
|
88
204
|
nextRegistryRefreshAt = EditorApplication.timeSinceStartup + RegistryRefreshSeconds;
|
|
89
205
|
}
|
|
90
|
-
TickInstancePublicationRetry();
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
BridgeWorkItem item;
|
|
95
|
-
var queueDepth = 0;
|
|
96
|
-
lock (QueueLock)
|
|
97
|
-
{
|
|
98
|
-
if (WorkQueue.Count == 0) return;
|
|
99
|
-
item = WorkQueue.Peek();
|
|
100
|
-
var mutation = CoordinatedRequestIsMutation(item.Request);
|
|
101
|
-
if (mutation && RunningWork.Any(running => running.IsMutation)) return;
|
|
102
|
-
item = WorkQueue.Dequeue();
|
|
103
|
-
item.IsMutation = mutation;
|
|
104
|
-
queueDepth = WorkQueue.Count;
|
|
105
|
-
}
|
|
106
|
-
WriteLog("bridge_request_dequeued requestId=" + item.Request.RequestId + " command=" + item.Request.Command + " queueDepth=" + queueDepth.ToString(CultureInfo.InvariantCulture));
|
|
206
|
+
HandleInitialPublicationOutcome(TickInstancePublicationRetry());
|
|
207
|
+
int queueDepth;
|
|
208
|
+
List<ControlQueueRejection> rejected;
|
|
209
|
+
var selected = SelectQueuedStarts(state, CurrentDurableMutationBarrier(state), true, out queueDepth, out rejected);
|
|
107
210
|
|
|
211
|
+
foreach (var rejection in rejected) CompleteRejectedControlWork(rejection);
|
|
212
|
+
foreach (var item in selected)
|
|
213
|
+
{
|
|
214
|
+
state.Sinks.WriteLog("bridge_request_dequeued requestId=" + item.Request.RequestId + " command=" + item.Request.Command + " queueDepth=" + queueDepth.ToString(CultureInfo.InvariantCulture));
|
|
108
215
|
if (!item.TryStart())
|
|
109
216
|
{
|
|
110
|
-
WriteLog("bridge_request_skipped_expired requestId=" + item.Request.RequestId + " command=" + item.Request.Command);
|
|
217
|
+
state.Sinks.WriteLog("bridge_request_skipped_expired requestId=" + item.Request.RequestId + " command=" + item.Request.Command);
|
|
111
218
|
item.Response = item.Response ?? UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
|
|
112
219
|
item.Request.RequestId,
|
|
113
|
-
"
|
|
114
|
-
"Bridge request
|
|
220
|
+
"E_EXECUTION_TIMEOUT",
|
|
221
|
+
"Bridge request deadline elapsed before Unity started executing it.",
|
|
115
222
|
nextActions: new[] { "Retry after Unity is no longer busy." }));
|
|
116
223
|
CompleteWorkItem(item);
|
|
117
224
|
continue;
|
|
118
225
|
}
|
|
119
|
-
|
|
120
|
-
lock (QueueLock) RunningWork.Add(item);
|
|
226
|
+
lock (state.QueueLock) state.RunningWork.Add(item);
|
|
121
227
|
_ = ExecuteWorkItemAsync(item);
|
|
122
|
-
startedThisTick += 1;
|
|
123
228
|
}
|
|
124
229
|
}
|
|
125
230
|
|
|
231
|
+
/// <summary>control proof在锁外冻结;每个linked-list节点在lane访问当下完成唯一policy检查。</summary>
|
|
232
|
+
private static List<BridgeWorkItem> SelectQueuedStarts(BridgeQueueState state, DurableMutationBarrierProof barrier, bool remove, out int queueDepth, out List<ControlQueueRejection> rejections)
|
|
233
|
+
{
|
|
234
|
+
bool hasControlWork;
|
|
235
|
+
lock (state.QueueLock) hasControlWork = state.ControlQueue.Count > 0;
|
|
236
|
+
var controlContext = LoadControlDecisionContext(state, barrier, hasControlWork);
|
|
237
|
+
var barrierState = barrier == null ? UnityCliBridgeQueuePolicy.DurableMutationBarrier.Unreadable : barrier.State;
|
|
238
|
+
var snapshotItems = new List<BridgeWorkItem>(UnityCliBridgeQueuePolicy.MaxNodesExaminedPerTick);
|
|
239
|
+
var selected = new List<BridgeWorkItem>(UnityCliBridgeQueuePolicy.MaxStartsPerTick);
|
|
240
|
+
rejections = new List<ControlQueueRejection>();
|
|
241
|
+
var safeStatusVisits = 0;
|
|
242
|
+
var controlVisits = 0;
|
|
243
|
+
var fifoVisits = 0;
|
|
244
|
+
lock (state.QueueLock)
|
|
245
|
+
{
|
|
246
|
+
var runningOrdinaryReads = state.RunningWork.Count(item => item.WorkClass == UnityCliBridgeQueuePolicy.WorkClass.OrdinaryRead);
|
|
247
|
+
var mutationRunning = state.RunningWork.Any(item => item.WorkClass == UnityCliBridgeQueuePolicy.WorkClass.Mutation
|
|
248
|
+
|| item.WorkClass == UnityCliBridgeQueuePolicy.WorkClass.Control);
|
|
249
|
+
var accumulator = UnityCliBridgeQueuePolicy.BeginSelection(runningOrdinaryReads, mutationRunning, barrierState);
|
|
250
|
+
var seen = new HashSet<BridgeWorkItem>();
|
|
251
|
+
|
|
252
|
+
var fifoCursor = state.WorkQueue.First;
|
|
253
|
+
while (fifoCursor != null && fifoVisits < UnityCliBridgeQueuePolicy.MaxFifoNodesExaminedPerTick)
|
|
254
|
+
{
|
|
255
|
+
ExamineQueueCandidate(fifoCursor.Value, controlContext, seen, snapshotItems, accumulator);
|
|
256
|
+
fifoVisits += 1;
|
|
257
|
+
fifoCursor = fifoCursor.Next;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
var controlLimit = Math.Min(state.ControlQueue.Count, UnityCliBridgeQueuePolicy.MaxControlNodesExaminedPerTick);
|
|
261
|
+
var controlCursor = state.ControlScanCursor;
|
|
262
|
+
if (controlCursor == null || controlCursor.List != state.ControlQueue) controlCursor = state.ControlQueue.First;
|
|
263
|
+
while (controlCursor != null && controlVisits < controlLimit)
|
|
264
|
+
{
|
|
265
|
+
ExamineQueueCandidate(controlCursor.Value, controlContext, seen, snapshotItems, accumulator);
|
|
266
|
+
controlVisits += 1;
|
|
267
|
+
controlCursor = controlCursor.Next ?? state.ControlQueue.First;
|
|
268
|
+
}
|
|
269
|
+
state.ControlScanCursor = state.ControlQueue.Count == 0 ? null : controlCursor;
|
|
270
|
+
|
|
271
|
+
var safeCursor = state.SafeStatusQueue.First;
|
|
272
|
+
while (safeCursor != null && safeStatusVisits < UnityCliBridgeQueuePolicy.MaxSafeStatusNodesExaminedPerTick)
|
|
273
|
+
{
|
|
274
|
+
ExamineQueueCandidate(safeCursor.Value, controlContext, seen, snapshotItems, accumulator);
|
|
275
|
+
safeStatusVisits += 1;
|
|
276
|
+
safeCursor = safeCursor.Next;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
state.RecordQueueNodesExamined(safeStatusVisits, controlVisits, fifoVisits);
|
|
280
|
+
var selection = accumulator.Complete();
|
|
281
|
+
foreach (var decision in selection.Rejects)
|
|
282
|
+
{
|
|
283
|
+
var item = snapshotItems[decision.QueueIndex];
|
|
284
|
+
if (item.QueueNode == null || item.QueueNode.List != state.WorkQueue) continue;
|
|
285
|
+
rejections.Add(new ControlQueueRejection(item, decision.Disposition));
|
|
286
|
+
if (remove) RemoveQueuedItemUnderLock(state, item);
|
|
287
|
+
}
|
|
288
|
+
foreach (var decision in selection.Starts)
|
|
289
|
+
{
|
|
290
|
+
var item = snapshotItems[decision.QueueIndex];
|
|
291
|
+
if (item.QueueNode == null || item.QueueNode.List != state.WorkQueue) continue;
|
|
292
|
+
selected.Add(item);
|
|
293
|
+
if (remove) RemoveQueuedItemUnderLock(state, item);
|
|
294
|
+
}
|
|
295
|
+
queueDepth = state.WorkQueue.Count;
|
|
296
|
+
}
|
|
297
|
+
return selected;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/// <summary>同一WorkItem跨lane只做一次policy检查,lane节点访问仍按各自预算计数。</summary>
|
|
301
|
+
private static void ExamineQueueCandidate(
|
|
302
|
+
BridgeWorkItem item,
|
|
303
|
+
ControlDecisionContext controlContext,
|
|
304
|
+
ISet<BridgeWorkItem> seen,
|
|
305
|
+
ICollection<BridgeWorkItem> snapshotItems,
|
|
306
|
+
UnityCliBridgeQueuePolicy.TickSelectionAccumulator accumulator)
|
|
307
|
+
{
|
|
308
|
+
if (!seen.Add(item)) return;
|
|
309
|
+
var node = item.WorkClass == UnityCliBridgeQueuePolicy.WorkClass.Control
|
|
310
|
+
? item.PolicyNode.WithControlDisposition(ControlDisposition(item, controlContext))
|
|
311
|
+
: item.PolicyNode;
|
|
312
|
+
accumulator.Examine(node);
|
|
313
|
+
snapshotItems.Add(item);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
internal static string[] PreviewQueuedStartRequestIdsForTests(BridgeQueueState state)
|
|
317
|
+
{
|
|
318
|
+
if (state == null || state.IsolatedExecution == null) throw new ArgumentException("Queue preview test state must be isolated.", "state");
|
|
319
|
+
int queueDepth;
|
|
320
|
+
List<ControlQueueRejection> ignored;
|
|
321
|
+
return SelectQueuedStarts(state, CurrentDurableMutationBarrier(state), false, out queueDepth, out ignored)
|
|
322
|
+
.Select(item => item.Request.RequestId).ToArray();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// <summary>测试以显式barrier调用production移除算法;不读取production lease或固定状态。</summary>
|
|
326
|
+
internal static BridgeWorkItem[] TakeQueuedStartsForTests(BridgeQueueState state, UnityCliBridgeQueuePolicy.DurableMutationBarrier barrier)
|
|
327
|
+
{
|
|
328
|
+
if (state == null || state.IsolatedExecution == null) throw new ArgumentException("Queue take test state must be isolated.", "state");
|
|
329
|
+
int queueDepth;
|
|
330
|
+
List<ControlQueueRejection> ignored;
|
|
331
|
+
return SelectQueuedStarts(state, new DurableMutationBarrierProof { State = barrier }, true, out queueDepth, out ignored).ToArray();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
internal static string[] RejectTerminalControlWorkForTests(BridgeQueueState state)
|
|
335
|
+
{
|
|
336
|
+
if (state == null || state.IsolatedExecution == null) throw new ArgumentException("Control rejection test state must be isolated.", "state");
|
|
337
|
+
int queueDepth;
|
|
338
|
+
List<ControlQueueRejection> rejections;
|
|
339
|
+
SelectQueuedStarts(state, CurrentDurableMutationBarrier(state), false, out queueDepth, out rejections);
|
|
340
|
+
var removed = new List<ControlQueueRejection>();
|
|
341
|
+
lock (state.QueueLock)
|
|
342
|
+
{
|
|
343
|
+
foreach (var rejection in rejections)
|
|
344
|
+
{
|
|
345
|
+
var item = rejection.Item;
|
|
346
|
+
if (item.QueueNode == null || item.QueueNode.List != state.WorkQueue) continue;
|
|
347
|
+
RemoveQueuedItemUnderLock(state, item);
|
|
348
|
+
removed.Add(rejection);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
foreach (var rejection in removed) CompleteRejectedControlWork(rejection);
|
|
352
|
+
return removed.Select(value => value.Item.Request.RequestId).ToArray();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private static readonly string[] DurableControlRootScopes =
|
|
356
|
+
{
|
|
357
|
+
"test-run", "package-resolve", "navmesh-bake", "occlusion-bake"
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/// <summary>固定scope的durable root与lease proof在QueueLock外一次冻结,lane访问只做纯裁决。</summary>
|
|
361
|
+
private static ControlDecisionContext LoadControlDecisionContext(BridgeQueueState state, DurableMutationBarrierProof barrier, bool hasControlWork)
|
|
362
|
+
{
|
|
363
|
+
var roots = new Dictionary<string, ControlRootObservation>(StringComparer.Ordinal);
|
|
364
|
+
if (!hasControlWork || barrier == null || barrier.State == UnityCliBridgeQueuePolicy.DurableMutationBarrier.Unreadable)
|
|
365
|
+
return new ControlDecisionContext(barrier, roots, false, false);
|
|
366
|
+
foreach (var scope in DurableControlRootScopes)
|
|
367
|
+
{
|
|
368
|
+
try { roots[scope] = new ControlRootObservation(true, state.LoadOperation(scope)); }
|
|
369
|
+
catch { roots[scope] = new ControlRootObservation(false, null); }
|
|
370
|
+
}
|
|
371
|
+
var leaseReadable = false;
|
|
372
|
+
var leaseOwned = false;
|
|
373
|
+
if (barrier.State == UnityCliBridgeQueuePolicy.DurableMutationBarrier.Held)
|
|
374
|
+
leaseReadable = state.TryIsLeaseOwned(barrier.RequestId, barrier.RequestDigest, out leaseOwned);
|
|
375
|
+
return new ControlDecisionContext(barrier, roots, leaseReadable, leaseOwned);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private static UnityCliBridgeQueuePolicy.ControlDisposition ControlDisposition(BridgeWorkItem item, ControlDecisionContext context)
|
|
379
|
+
{
|
|
380
|
+
int remainingMs;
|
|
381
|
+
if (item == null || item.Request == null || !item.Request.TryDeadlineRemainingMs(out remainingMs) || remainingMs <= 0)
|
|
382
|
+
return UnityCliBridgeQueuePolicy.ControlDisposition.ExpireDeadline;
|
|
383
|
+
var barrier = context == null ? null : context.Barrier;
|
|
384
|
+
if (barrier == null || barrier.State == UnityCliBridgeQueuePolicy.DurableMutationBarrier.Unreadable)
|
|
385
|
+
return UnityCliBridgeQueuePolicy.ControlDisposition.Pending;
|
|
386
|
+
var scope = ControlRootScope(item.Request.Command);
|
|
387
|
+
ControlRootObservation observation;
|
|
388
|
+
if (scope == null || !context.Roots.TryGetValue(scope, out observation) || !observation.Readable)
|
|
389
|
+
return UnityCliBridgeQueuePolicy.ControlDisposition.Pending;
|
|
390
|
+
if (barrier.State == UnityCliBridgeQueuePolicy.DurableMutationBarrier.Clear)
|
|
391
|
+
return UnityCliBridgeQueuePolicy.ControlDisposition.RejectParentUnavailable;
|
|
392
|
+
if (!context.LeaseReadable || !context.LeaseOwned)
|
|
393
|
+
return UnityCliBridgeQueuePolicy.ControlDisposition.Pending;
|
|
394
|
+
var root = observation.Root;
|
|
395
|
+
if (root == null
|
|
396
|
+
|| !string.Equals(AdmissionText(root, "requestId"), barrier.RequestId, StringComparison.Ordinal)
|
|
397
|
+
|| !string.Equals(AdmissionText(root, "requestDigest"), barrier.RequestDigest, StringComparison.Ordinal)
|
|
398
|
+
|| !string.Equals(AdmissionText(root, "editorSessionId"), EditorSessionId(), StringComparison.Ordinal)
|
|
399
|
+
|| !ControlSelectorMatchesParent(item.Request, root, barrier))
|
|
400
|
+
return UnityCliBridgeQueuePolicy.ControlDisposition.RejectIdentityMismatch;
|
|
401
|
+
return UnityCliBridgeQueuePolicy.ControlDisposition.Authorized;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private static bool ControlSelectorMatchesParent(UnityCli.Protocol.BridgeRequest request, IDictionary<string, object> root, DurableMutationBarrierProof barrier)
|
|
405
|
+
{
|
|
406
|
+
var scope = ControlRootScope(request.Command);
|
|
407
|
+
var input = request.Input as IDictionary<string, object>;
|
|
408
|
+
if (scope == "package-resolve")
|
|
409
|
+
return string.Equals(ControlInputText(input, "rootRequestId"), barrier.RequestId, StringComparison.Ordinal);
|
|
410
|
+
if (scope == "test-run")
|
|
411
|
+
{
|
|
412
|
+
object rawOperation;
|
|
413
|
+
var operation = root.TryGetValue("testOperation", out rawOperation) ? rawOperation as IDictionary<string, object> : null;
|
|
414
|
+
return operation != null && Guid.TryParse(AdmissionText(operation, "runId"), out _)
|
|
415
|
+
&& string.Equals(ControlInputText(input, "expectedEditorSessionId"), AdmissionText(root, "editorSessionId"), StringComparison.Ordinal);
|
|
416
|
+
}
|
|
417
|
+
return string.Equals(ControlInputText(input, "operationId"), AdmissionText(root, "operationId"), StringComparison.Ordinal);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private static void CompleteRejectedControlWork(ControlQueueRejection rejection)
|
|
421
|
+
{
|
|
422
|
+
if (rejection == null || rejection.Item == null || !rejection.Item.CancelBeforeStart()) return;
|
|
423
|
+
var item = rejection.Item;
|
|
424
|
+
var identityMismatch = rejection.Disposition == UnityCliBridgeQueuePolicy.ControlDisposition.RejectIdentityMismatch;
|
|
425
|
+
var deadlineElapsed = rejection.Disposition == UnityCliBridgeQueuePolicy.ControlDisposition.ExpireDeadline;
|
|
426
|
+
item.Response = UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
|
|
427
|
+
item.Request.RequestId,
|
|
428
|
+
identityMismatch ? "E_ADMISSION_IDENTITY_MISMATCH" : "E_EXECUTION_TIMEOUT",
|
|
429
|
+
identityMismatch ? "Control request identity does not match the exact active parent."
|
|
430
|
+
: deadlineElapsed ? "Control request deadline elapsed before exact parent authorization."
|
|
431
|
+
: "Control request parent is no longer active.",
|
|
432
|
+
nextActions: new[] { "Inspect the exact parent status before submitting another control request." }));
|
|
433
|
+
item.MarkNoEffectRejected();
|
|
434
|
+
CompleteWorkItem(item);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/// <summary>一次tick在QueueLock外冻结的control durable proof。</summary>
|
|
438
|
+
private sealed class ControlDecisionContext
|
|
439
|
+
{
|
|
440
|
+
internal ControlDecisionContext(
|
|
441
|
+
DurableMutationBarrierProof barrier,
|
|
442
|
+
IDictionary<string, ControlRootObservation> roots,
|
|
443
|
+
bool leaseReadable,
|
|
444
|
+
bool leaseOwned)
|
|
445
|
+
{
|
|
446
|
+
Barrier = barrier;
|
|
447
|
+
Roots = roots;
|
|
448
|
+
LeaseReadable = leaseReadable;
|
|
449
|
+
LeaseOwned = leaseOwned;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
internal DurableMutationBarrierProof Barrier;
|
|
453
|
+
internal IDictionary<string, ControlRootObservation> Roots;
|
|
454
|
+
internal bool LeaseReadable;
|
|
455
|
+
internal bool LeaseOwned;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private sealed class ControlRootObservation
|
|
459
|
+
{
|
|
460
|
+
internal ControlRootObservation(bool readable, Dictionary<string, object> root) { Readable = readable; Root = root; }
|
|
461
|
+
internal bool Readable;
|
|
462
|
+
internal Dictionary<string, object> Root;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private sealed class ControlQueueRejection
|
|
466
|
+
{
|
|
467
|
+
internal ControlQueueRejection(BridgeWorkItem item, UnityCliBridgeQueuePolicy.ControlDisposition disposition) { Item = item; Disposition = disposition; }
|
|
468
|
+
internal BridgeWorkItem Item;
|
|
469
|
+
internal UnityCliBridgeQueuePolicy.ControlDisposition Disposition;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private static string ControlRootScope(string command)
|
|
473
|
+
{
|
|
474
|
+
if (command == "internal.test.run.cancel.plan" || command == "internal.test.run.cancel") return "test-run";
|
|
475
|
+
if (command == "internal.package.files.claim" || command == "internal.package.files.commit"
|
|
476
|
+
|| command == "internal.package.files.prove" || command == "internal.package.files.fail") return "package-resolve";
|
|
477
|
+
if (command == "internal.navmesh.bake.cancel") return "navmesh-bake";
|
|
478
|
+
if (command == "internal.occlusion.bake.cancel") return "occlusion-bake";
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private static string ControlInputText(IDictionary<string, object> input, string key)
|
|
483
|
+
{
|
|
484
|
+
object value;
|
|
485
|
+
return input != null && input.TryGetValue(key, out value) ? value as string : null;
|
|
486
|
+
}
|
|
487
|
+
|
|
126
488
|
private static async Task ExecuteWorkItemAsync(BridgeWorkItem item)
|
|
127
489
|
{
|
|
128
490
|
try
|
|
129
491
|
{
|
|
130
|
-
WriteLog("bridge_request_started requestId=" + item.Request.RequestId + " command=" + item.Request.Command);
|
|
492
|
+
item.QueueState.Sinks.WriteLog("bridge_request_started requestId=" + item.Request.RequestId + " command=" + item.Request.Command);
|
|
131
493
|
item.Response = await ExecuteCoordinatedRequest(item.Request, item.RawRequestBytes, item.TryAccept, item.Token);
|
|
132
|
-
WriteLog("bridge_request_finished requestId=" + item.Request.RequestId + " command=" + item.Request.Command + " ok=" + Bool(item.Response != null && item.Response.Contains("\"ok\":true")));
|
|
494
|
+
item.QueueState.Sinks.WriteLog("bridge_request_finished requestId=" + item.Request.RequestId + " command=" + item.Request.Command + " ok=" + Bool(item.Response != null && item.Response.Contains("\"ok\":true")));
|
|
133
495
|
}
|
|
134
496
|
catch (BridgeCommandException exception)
|
|
135
497
|
{
|
|
@@ -157,88 +519,497 @@ namespace UnityCli
|
|
|
157
519
|
}
|
|
158
520
|
}
|
|
159
521
|
|
|
160
|
-
/// <summary
|
|
161
|
-
private static BridgeWorkItem EnqueueOrAttach(UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes, string admissionListenerId, long admissionGeneration,
|
|
522
|
+
/// <summary>immutable输入在锁外准备;existing attach与首次consume/publication共用QueueLock→pool线性化边界。</summary>
|
|
523
|
+
private static BridgeWorkItem EnqueueOrAttach(HttpListenerRequest httpRequest, UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes, string admissionListenerId, long admissionGeneration, out bool created, out int queueDepth)
|
|
524
|
+
{
|
|
525
|
+
var reservation = PrepareAdmissionForQueue(httpRequest, request, rawRequestBytes);
|
|
526
|
+
return EnqueuePrepared(ProductionQueueState, request, rawRequestBytes, admissionListenerId, admissionGeneration, reservation, out created, out queueDepth);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
internal static BridgeWorkItem EnqueuePreparedForTests(UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes, AdmissionQueueReservation reservation, BridgeQueueState state, out bool created, out int queueDepth)
|
|
530
|
+
{
|
|
531
|
+
if (state == null) throw new ArgumentNullException("state");
|
|
532
|
+
return EnqueuePrepared(state, request, rawRequestBytes, state.ListenerId ?? ListenerId, state.ListenerGeneration, reservation, out created, out queueDepth);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private static BridgeWorkItem EnqueuePrepared(BridgeQueueState state, UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes, string admissionListenerId, long admissionGeneration, AdmissionQueueReservation reservation, out bool created, out int queueDepth)
|
|
162
536
|
{
|
|
163
537
|
var digest = UnityCliRequestDigest.Compute(rawRequestBytes, request.InstanceId);
|
|
164
|
-
|
|
538
|
+
var workClass = QueueRequestWorkClass(request);
|
|
539
|
+
if (state == null) throw new ArgumentNullException("state");
|
|
540
|
+
if (reservation == null) throw new ArgumentNullException("reservation");
|
|
541
|
+
BridgeWorkItem item = null;
|
|
542
|
+
try
|
|
165
543
|
{
|
|
166
|
-
|
|
167
|
-
|
|
544
|
+
HitQueueTestBoundary(state, "before-queue-lock");
|
|
545
|
+
lock (state.QueueLock)
|
|
546
|
+
{
|
|
547
|
+
try
|
|
548
|
+
{
|
|
549
|
+
if (!state.QueuePublicationOpen)
|
|
550
|
+
{
|
|
551
|
+
if (state.QueueSessionStopping)
|
|
552
|
+
throw new BridgeCommandException("E_RUNTIME_SESSION_STALE", "The Unity session is stopping and no longer accepts admission or business work.", null, new[] { "Start a new Unity session and submit a new request." });
|
|
553
|
+
throw new BridgeCommandException("E_UNITY_DOMAIN_RELOAD", "Bridge queue admission is closed for the current Unity lifecycle.", null, new[] { "Wait for Unity to finish reloading and retry." }, true, 1000);
|
|
554
|
+
}
|
|
555
|
+
HitQueueTestBoundary(state, "before-existing-lookup");
|
|
556
|
+
BridgeWorkItem existing;
|
|
557
|
+
if (state.ActiveRequestWork.TryGetValue(request.RequestId, out existing))
|
|
558
|
+
{
|
|
559
|
+
if (!string.Equals(existing.RequestDigest, digest, StringComparison.Ordinal))
|
|
560
|
+
throw new BridgeCommandException("E_REQUEST_ID_REUSED", "requestId is already bound to different request bytes.", "Generate a new requestId before retrying this command.");
|
|
561
|
+
if (state.HasCompensationTombstone(request.RequestId, digest))
|
|
562
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Exact pre-queue admission compensation is still pending; duplicate attachment is forbidden.");
|
|
563
|
+
if (!TryAuthorizeCompletionPendingAttach(state, reservation, existing))
|
|
564
|
+
AuthorizeAdmissionForQueue(state, reservation, request);
|
|
565
|
+
if (!SameAdmissionOwner(existing.AdmissionOwner, reservation.Owner))
|
|
566
|
+
throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "requestId is already attached to another admission root.");
|
|
567
|
+
MarkAdmissionQueuePublished(reservation);
|
|
568
|
+
created = false;
|
|
569
|
+
queueDepth = state.WorkQueue.Count;
|
|
570
|
+
return existing;
|
|
571
|
+
}
|
|
572
|
+
AuthorizeAdmissionForQueue(state, reservation, request);
|
|
573
|
+
if (state.HasCompensationTombstone(request.RequestId, digest))
|
|
574
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Exact pre-queue admission compensation is still pending; duplicate publication is forbidden.");
|
|
575
|
+
var bypass = reservation.IsBypass && reservation.Owner == null;
|
|
576
|
+
UnityCliBridgeQueuePolicy.BypassAdmissionDecision bypassDecision = null;
|
|
577
|
+
if (bypass)
|
|
578
|
+
{
|
|
579
|
+
var identities = state.ActiveRequestWork.Values.Where(active => active.AdmissionBypass)
|
|
580
|
+
.Select(active => new UnityCliBridgeQueuePolicy.BypassIdentity(active.Request.RequestId, active.RequestDigest)).ToArray();
|
|
581
|
+
bypassDecision = UnityCliBridgeQueuePolicy.DecideBypassAdmission(identities, new UnityCliBridgeQueuePolicy.BypassIdentity(request.RequestId, digest));
|
|
582
|
+
if (bypassDecision.Outcome == UnityCliBridgeQueuePolicy.BypassAdmissionOutcome.RejectCapacity)
|
|
583
|
+
throw new BridgeCommandException("E_UNITY_BUSY", "Admission-bypass Bridge capacity is full.", "Wait for an active status or package-control request to complete before retrying.");
|
|
584
|
+
if (bypassDecision.Outcome == UnityCliBridgeQueuePolicy.BypassAdmissionOutcome.RejectIdentityMismatch)
|
|
585
|
+
throw new BridgeCommandException("E_REQUEST_ID_REUSED", "Admission-bypass requestId identity is inconsistent.");
|
|
586
|
+
if (bypassDecision.Outcome == UnityCliBridgeQueuePolicy.BypassAdmissionOutcome.Attach)
|
|
587
|
+
throw new BridgeCommandException("E_REQUEST_ID_REUSED", "Admission-bypass attach target disappeared before publication.");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
item = new BridgeWorkItem(state, request, rawRequestBytes, digest, admissionListenerId, admissionGeneration,
|
|
591
|
+
reservation.Owner, workClass, bypass, reservation.NewlyConsumed);
|
|
592
|
+
HitQueueTestBoundary(state, "before-queue-publication");
|
|
593
|
+
UnityCliQueuePublication.Publish(
|
|
594
|
+
() => state.ActiveRequestWork.Add(request.RequestId, item),
|
|
595
|
+
() =>
|
|
596
|
+
{
|
|
597
|
+
item.QueueNode = state.WorkQueue.AddLast(item);
|
|
598
|
+
try
|
|
599
|
+
{
|
|
600
|
+
if (item.WorkClass == UnityCliBridgeQueuePolicy.WorkClass.SafeStatus)
|
|
601
|
+
item.SafeStatusNode = state.SafeStatusQueue.AddLast(item);
|
|
602
|
+
if (item.WorkClass == UnityCliBridgeQueuePolicy.WorkClass.Control)
|
|
603
|
+
{
|
|
604
|
+
item.ControlNode = state.ControlQueue.AddLast(item);
|
|
605
|
+
if (state.ControlScanCursor == null) state.ControlScanCursor = item.ControlNode;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
catch
|
|
609
|
+
{
|
|
610
|
+
RemoveQueuedItemUnderLock(state, item);
|
|
611
|
+
throw;
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
() => { BridgeWorkItem current; if (state.ActiveRequestWork.TryGetValue(request.RequestId, out current) && object.ReferenceEquals(current, item)) state.ActiveRequestWork.Remove(request.RequestId); },
|
|
615
|
+
() => RemoveQueuedItemUnderLock(state, item),
|
|
616
|
+
() => MarkAdmissionQueuePublished(reservation));
|
|
617
|
+
created = true;
|
|
618
|
+
queueDepth = state.WorkQueue.Count;
|
|
619
|
+
return item;
|
|
620
|
+
}
|
|
621
|
+
catch
|
|
622
|
+
{
|
|
623
|
+
if (reservation.NewlyConsumed && reservation.Owner != null && !reservation.QueuePublished)
|
|
624
|
+
{
|
|
625
|
+
try { state.TrackCompensationContinuation(reservation.Owner); }
|
|
626
|
+
catch
|
|
627
|
+
{
|
|
628
|
+
state.QueuePublicationOpen = false;
|
|
629
|
+
throw;
|
|
630
|
+
}
|
|
631
|
+
HitQueueTestBoundary(state, "after-publication-compensation-tombstone");
|
|
632
|
+
}
|
|
633
|
+
throw;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
catch
|
|
638
|
+
{
|
|
639
|
+
if (item != null && !reservation.QueuePublished) item.DisposeUnpublished();
|
|
640
|
+
var compensationRequired = reservation.NewlyConsumed && reservation.Owner != null && !reservation.QueuePublished;
|
|
641
|
+
try
|
|
642
|
+
{
|
|
643
|
+
if (compensationRequired && !PersistAdmissionCompletionIntent(state, reservation.Owner))
|
|
644
|
+
throw new IOException("Admission compensation intent could not be durably recorded before pre-queue cleanup.");
|
|
645
|
+
var compensated = CompensateAdmissionBeforeQueuePublication(state, reservation);
|
|
646
|
+
if (!compensated)
|
|
647
|
+
throw new IOException("Admission compensation could not prove exact pre-queue cleanup.");
|
|
648
|
+
if (compensationRequired)
|
|
649
|
+
{
|
|
650
|
+
HitQueueTestBoundary(state, "before-admission-compensation-persist");
|
|
651
|
+
if (!PersistAdmissionPoolState(state)) throw new IOException("Admission pool compensation persistence was not confirmed.");
|
|
652
|
+
if (!ClearAdmissionCompletionIntent(state, reservation.Owner)) throw new IOException("Admission pool compensation intent cleanup was not confirmed.");
|
|
653
|
+
HitQueueTestBoundary(state, "before-admission-compensation-log");
|
|
654
|
+
state.Sinks.WriteLog("admission_prequeue_compensated requestId=" + reservation.Owner.RequestId + " active=" + state.AdmissionPool.Capture().Active.ToString(CultureInfo.InvariantCulture));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
catch (Exception exception)
|
|
168
658
|
{
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
throw new BridgeCommandException("E_ADMISSION_IDENTITY_MISMATCH", "requestId is already attached to another admission root.");
|
|
173
|
-
created = false;
|
|
174
|
-
queueDepth = WorkQueue.Count;
|
|
175
|
-
return existing;
|
|
659
|
+
if (compensationRequired && state.DeferTrackedCompletionIntent(reservation.Owner, state.NowMs()))
|
|
660
|
+
state.Sinks.WriteLog("admission_prequeue_compensation_failed requestId=" + reservation.Owner.RequestId + " " + exception.GetType().Name);
|
|
661
|
+
throw new BridgeCommandException("E_EXECUTION_TIMEOUT", "Admission compensation could not be durably recorded after queue publication failed.");
|
|
176
662
|
}
|
|
177
|
-
|
|
178
|
-
ActiveRequestWork.Add(request.RequestId, item);
|
|
179
|
-
WorkQueue.Enqueue(item);
|
|
180
|
-
created = true;
|
|
181
|
-
queueDepth = WorkQueue.Count;
|
|
182
|
-
return item;
|
|
663
|
+
throw;
|
|
183
664
|
}
|
|
184
665
|
}
|
|
185
666
|
|
|
186
667
|
private static bool SameAdmissionOwner(AdmissionOwnerIdentity left, AdmissionOwnerIdentity right)
|
|
187
668
|
{
|
|
188
669
|
if (left == null || right == null) return left == null && right == null;
|
|
189
|
-
return
|
|
190
|
-
&& string.Equals(left.RootFingerprint, right.RootFingerprint, StringComparison.Ordinal)
|
|
191
|
-
&& string.Equals(left.Token, right.Token, StringComparison.Ordinal)
|
|
192
|
-
&& string.Equals(left.SessionId, right.SessionId, StringComparison.Ordinal);
|
|
670
|
+
return SameAdmissionImmutableIdentity(left, right);
|
|
193
671
|
}
|
|
194
672
|
|
|
195
|
-
/// <summary
|
|
196
|
-
|
|
673
|
+
/// <summary>completion只作为production算法入口;WorkItem携带其所属state,测试不替换production state。</summary>
|
|
674
|
+
internal static bool CompleteWorkItemForTests(BridgeWorkItem item, string response)
|
|
197
675
|
{
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
676
|
+
if (item == null || item.QueueState == null || item.QueueState.IsolatedExecution == null)
|
|
677
|
+
throw new ArgumentException("WorkItem completion test state must be isolated.", "item");
|
|
678
|
+
if (item.Completed.IsSet) return false;
|
|
679
|
+
lock (item.QueueState.QueueLock)
|
|
680
|
+
{
|
|
681
|
+
if (item.QueueNode != null && item.QueueNode.List == item.QueueState.WorkQueue)
|
|
682
|
+
RemoveQueuedItemUnderLock(item.QueueState, item);
|
|
683
|
+
item.QueueState.RunningWork.Add(item);
|
|
684
|
+
}
|
|
685
|
+
item.SetResponseForTests(response);
|
|
686
|
+
return CompleteWorkItem(item);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/// <summary>按单调时钟和固定退避重试settlement;一个tick不超过准入容量。</summary>
|
|
690
|
+
private static void RetryPendingWorkItemSettlements()
|
|
691
|
+
{
|
|
692
|
+
RetryPendingWorkItemSettlements(ProductionQueueState);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
internal static void RetryPendingWorkItemSettlementsForTests(BridgeQueueState state)
|
|
696
|
+
{
|
|
697
|
+
RetryPendingWorkItemSettlements(state);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private static void RetryPendingWorkItemSettlements(BridgeQueueState state)
|
|
701
|
+
{
|
|
702
|
+
if (state == null) return;
|
|
703
|
+
var now = state.NowMs();
|
|
704
|
+
var limit = UnityCliBridgeQueuePolicy.MaxStartsPerTick;
|
|
705
|
+
if (state.AdmissionPool != null)
|
|
706
|
+
{
|
|
707
|
+
var snapshot = state.AdmissionPool.Capture();
|
|
708
|
+
limit = Math.Max(1, Math.Min(limit, snapshot.ConfiguredMaxActive));
|
|
709
|
+
}
|
|
710
|
+
BridgeWorkItem[] pending;
|
|
711
|
+
lock (state.QueueLock)
|
|
712
|
+
{
|
|
713
|
+
pending = state.PendingSettlementWork.Where(item => item.NextSettlementRetryAtMs <= now)
|
|
714
|
+
.OrderBy(item => item.NextSettlementRetryAtMs).ThenBy(item => item.Request.RequestId, StringComparer.Ordinal)
|
|
715
|
+
.Take(limit).ToArray();
|
|
716
|
+
}
|
|
717
|
+
foreach (var item in pending) CompleteWorkItem(item);
|
|
718
|
+
RetryPendingAdmissionCompletionIntents(state);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
internal static int ActiveRequestWorkCountForTests(BridgeQueueState state)
|
|
722
|
+
{
|
|
723
|
+
if (state == null) throw new ArgumentNullException("state");
|
|
724
|
+
lock (state.QueueLock) return state.ActiveRequestWork.Count;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
internal static bool QueueLockHeldForTests(BridgeQueueState state)
|
|
728
|
+
{
|
|
729
|
+
return state != null && Monitor.IsEntered(state.QueueLock);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private static bool CompleteWorkItem(BridgeWorkItem item)
|
|
733
|
+
{
|
|
734
|
+
if (item == null) return false;
|
|
735
|
+
var state = item.QueueState;
|
|
736
|
+
if (!item.TryBeginCompletion()) return false;
|
|
737
|
+
if (item.NoEffectRejected && !item.NoEffectRejectionOwnsAdmission)
|
|
738
|
+
{
|
|
739
|
+
lock (state.QueueLock)
|
|
740
|
+
{
|
|
741
|
+
state.RunningWork.Remove(item);
|
|
742
|
+
state.PendingSettlementWork.Remove(item);
|
|
743
|
+
RemoveActiveWorkItem(state, item);
|
|
744
|
+
}
|
|
745
|
+
CompleteAndLogWorkItem(item);
|
|
746
|
+
TryFinalizeAdmissionPoolShutdown(state);
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
var transfer = AdmissionTransferResult.TransferPending;
|
|
751
|
+
Exception settlementException = null;
|
|
752
|
+
Exception boundaryException = null;
|
|
753
|
+
try
|
|
754
|
+
{
|
|
755
|
+
// selection 已证明该 WorkItem 未启动时,禁止按命令名推断 durable coordinator 接管。
|
|
756
|
+
if (item.CompletionIntentReady || item.NoEffectRejected)
|
|
757
|
+
transfer = AdmissionTransferResult.NotAccepted;
|
|
758
|
+
else
|
|
759
|
+
transfer = SettleAdmissionAfterWorkItem(state, item);
|
|
760
|
+
}
|
|
761
|
+
catch (Exception exception) { settlementException = exception; }
|
|
762
|
+
|
|
763
|
+
if (transfer == AdmissionTransferResult.TransferPending)
|
|
764
|
+
{
|
|
765
|
+
HoldPendingSettlement(state, item, settlementException == null ? "recovery-marker-pending" : settlementException.GetType().Name);
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// 已被Coordinator/Continuation/Recovery/TerminalCleanup durable接管的owner不再由WorkItem完成。
|
|
770
|
+
if (item.AdmissionOwner == null || transfer == AdmissionTransferResult.Transferred)
|
|
771
|
+
{
|
|
772
|
+
lock (state.QueueLock)
|
|
773
|
+
{
|
|
774
|
+
state.RunningWork.Remove(item);
|
|
775
|
+
state.PendingSettlementWork.Remove(item);
|
|
776
|
+
RemoveActiveWorkItem(state, item);
|
|
777
|
+
}
|
|
778
|
+
CompleteAndLogWorkItem(item);
|
|
779
|
+
TryFinalizeAdmissionPoolShutdown(state);
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// 仍由WorkItem负责时,先在QueueLock外落completion intent,再锁内删除pool内存。
|
|
784
|
+
UnityCliAdmissionPool.ConsumedIdentity current;
|
|
785
|
+
AdmissionOwnerIdentity frozen;
|
|
786
|
+
try
|
|
787
|
+
{
|
|
788
|
+
if (item.CompletionIntentReady) frozen = item.AdmissionOwner;
|
|
789
|
+
else
|
|
790
|
+
{
|
|
791
|
+
frozen = CaptureAdmissionOwner(state, item.AdmissionOwner.RequestId, item.AdmissionOwner.RequestDigest);
|
|
792
|
+
if (!SameAdmissionOwner(frozen, item.AdmissionOwner)
|
|
793
|
+
|| frozen.OwnerState != UnityCliAdmissionPool.AdmissionOwnerState.WorkItem)
|
|
794
|
+
{
|
|
795
|
+
HoldPendingSettlement(state, item, "completion-owner-changed");
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
if (!HasAdmissionCompletionIntent(state, frozen)
|
|
799
|
+
&& !PersistAdmissionCompletionIntent(state, frozen))
|
|
800
|
+
{
|
|
801
|
+
HoldPendingSettlement(state, item, "completion-intent-pending");
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
item.CompletionIntentReady = true;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
catch (Exception exception)
|
|
808
|
+
{
|
|
809
|
+
HoldPendingSettlement(state, item, exception.GetType().Name);
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
UnityCliAdmissionPool.CompletionOutcome completionOutcome;
|
|
814
|
+
try
|
|
815
|
+
{
|
|
816
|
+
lock (state.QueueLock)
|
|
817
|
+
{
|
|
818
|
+
try { HitQueueTestBoundary(state, "before-completion-memory"); }
|
|
819
|
+
catch (Exception exception) { boundaryException = exception; }
|
|
820
|
+
if (boundaryException != null)
|
|
821
|
+
{
|
|
822
|
+
completionOutcome = UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked;
|
|
823
|
+
}
|
|
824
|
+
else
|
|
825
|
+
{
|
|
826
|
+
var pool = state.AdmissionPool;
|
|
827
|
+
if (pool == null)
|
|
828
|
+
completionOutcome = UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked;
|
|
829
|
+
else if (!pool.TryGetConsumedIdentity(item.AdmissionOwner.RequestId, out current))
|
|
830
|
+
completionOutcome = item.CompletionIntentReady
|
|
831
|
+
? UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent
|
|
832
|
+
: UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked;
|
|
833
|
+
else
|
|
834
|
+
{
|
|
835
|
+
frozen = ToAdmissionOwner(current, item.AdmissionOwner.RequestDigest);
|
|
836
|
+
completionOutcome = SameAdmissionOwner(frozen, item.AdmissionOwner)
|
|
837
|
+
? pool.CompleteTrusted(ToPoolIdentity(frozen))
|
|
838
|
+
: UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
state.RunningWork.Remove(item);
|
|
842
|
+
state.PendingSettlementWork.Add(item);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
catch (Exception exception)
|
|
846
|
+
{
|
|
847
|
+
if (state.AdmissionPool != null) state.AdmissionPool.BlockRecovery();
|
|
848
|
+
HoldPendingSettlement(state, item, exception.GetType().Name);
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (boundaryException != null)
|
|
853
|
+
{
|
|
854
|
+
if (!item.SettlementFailureLogged)
|
|
855
|
+
{
|
|
856
|
+
try { HitQueueTestBoundary(state, "before-queue-boundary-fault-log"); } catch { }
|
|
857
|
+
state.Sinks.WriteLog("queue_test_boundary_failed " + boundaryException.GetType().Name);
|
|
858
|
+
}
|
|
859
|
+
HoldPendingSettlement(state, item, boundaryException.GetType().Name);
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
if (completionOutcome == UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked)
|
|
863
|
+
{
|
|
864
|
+
HoldPendingSettlement(state, item, "completion-identity-blocked");
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
try
|
|
869
|
+
{
|
|
870
|
+
HitQueueTestBoundary(state, "before-admission-persist");
|
|
871
|
+
if (!state.PersistPool()) throw new IOException("Admission pool persistence was not confirmed.");
|
|
872
|
+
if (!state.ClearMarkers(item.AdmissionOwner)) throw new IOException("Admission owner marker cleanup was not confirmed.");
|
|
873
|
+
if (!ClearAdmissionCompletionIntent(state, item.AdmissionOwner)) throw new IOException("Admission completion intent cleanup was not confirmed.");
|
|
874
|
+
}
|
|
875
|
+
catch (Exception exception)
|
|
876
|
+
{
|
|
877
|
+
HoldPendingSettlement(state, item, exception.GetType().Name);
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
lock (state.QueueLock)
|
|
882
|
+
{
|
|
883
|
+
state.PendingSettlementWork.Remove(item);
|
|
884
|
+
RemoveActiveWorkItem(state, item);
|
|
885
|
+
}
|
|
886
|
+
CompleteAndLogWorkItem(item);
|
|
887
|
+
TryFinalizeAdmissionPoolShutdown(state);
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private static void HoldPendingSettlement(BridgeQueueState state, BridgeWorkItem item, string reason)
|
|
892
|
+
{
|
|
893
|
+
lock (state.QueueLock)
|
|
894
|
+
{
|
|
895
|
+
state.RunningWork.Remove(item);
|
|
896
|
+
state.PendingSettlementWork.Add(item);
|
|
897
|
+
}
|
|
898
|
+
item.ScheduleSettlementRetry(state.NowMs());
|
|
899
|
+
item.AllowCompletionRetry();
|
|
900
|
+
if (item.SettlementFailureLogged) return;
|
|
901
|
+
item.SettlementFailureLogged = true;
|
|
902
|
+
state.Sinks.WriteLog("admission_settlement_deferred requestId=" + item.Request.RequestId + " reason=" + reason);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/// <summary>QueueLock内同时摘除主FIFO与两个独立索引,任何入口都不得留下失效节点。</summary>
|
|
906
|
+
private static void RemoveQueuedItemUnderLock(BridgeQueueState state, BridgeWorkItem item)
|
|
907
|
+
{
|
|
908
|
+
if (item.QueueNode != null && item.QueueNode.List == state.WorkQueue)
|
|
909
|
+
state.WorkQueue.Remove(item.QueueNode);
|
|
910
|
+
if (item.SafeStatusNode != null && item.SafeStatusNode.List == state.SafeStatusQueue)
|
|
911
|
+
state.SafeStatusQueue.Remove(item.SafeStatusNode);
|
|
912
|
+
if (item.ControlNode != null && item.ControlNode.List == state.ControlQueue)
|
|
913
|
+
{
|
|
914
|
+
if (object.ReferenceEquals(state.ControlScanCursor, item.ControlNode))
|
|
915
|
+
state.ControlScanCursor = item.ControlNode.Next ?? state.ControlQueue.First;
|
|
916
|
+
state.ControlQueue.Remove(item.ControlNode);
|
|
917
|
+
if (state.ControlQueue.Count == 0) state.ControlScanCursor = null;
|
|
918
|
+
}
|
|
919
|
+
item.QueueNode = null;
|
|
920
|
+
item.SafeStatusNode = null;
|
|
921
|
+
item.ControlNode = null;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private static void RemoveActiveWorkItem(BridgeQueueState state, BridgeWorkItem item)
|
|
925
|
+
{
|
|
926
|
+
BridgeWorkItem current;
|
|
927
|
+
if (state.ActiveRequestWork.TryGetValue(item.Request.RequestId, out current) && object.ReferenceEquals(current, item))
|
|
928
|
+
state.ActiveRequestWork.Remove(item.Request.RequestId);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private static string CompletionIntentKey(AdmissionOwnerIdentity identity)
|
|
932
|
+
{
|
|
933
|
+
return identity == null ? string.Empty : identity.RequestId + "\0" + identity.RequestDigest + "\0" + (identity.TopRequestDigest ?? string.Empty)
|
|
934
|
+
+ "\0" + identity.RootFingerprint + "\0" + identity.Token + "\0" + identity.SessionId
|
|
935
|
+
+ "\0" + identity.ListenerId + "\0" + identity.ListenerGeneration.ToString(CultureInfo.InvariantCulture)
|
|
936
|
+
+ "\0" + identity.ExecutionDeadlineMs.ToString(CultureInfo.InvariantCulture) + "\0" + ((int)identity.OwnerState).ToString(CultureInfo.InvariantCulture);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
private static void CompleteAndLogWorkItem(BridgeWorkItem item)
|
|
940
|
+
{
|
|
941
|
+
item.Completed.Set();
|
|
942
|
+
try
|
|
207
943
|
{
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (ActiveRequestWork.TryGetValue(item.Request.RequestId, out current) && object.ReferenceEquals(current, item))
|
|
211
|
-
ActiveRequestWork.Remove(item.Request.RequestId);
|
|
212
|
-
item.Completed.Set();
|
|
944
|
+
item.QueueState.Sinks.AppendTransaction(item.Request.RequestId, item.Request.Command, Encoding.UTF8.GetString(item.RawRequestBytes), item.Response ?? UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
|
|
945
|
+
item.Request.RequestId, "E_BRIDGE_COMMAND_FAILED", "Bridge request completed without a response.")));
|
|
213
946
|
}
|
|
947
|
+
catch (Exception exception) { item.QueueState.Sinks.WriteLog("transaction_log_append_failed requestId=" + item.Request.RequestId + " " + exception.GetType().Name); }
|
|
214
948
|
}
|
|
215
949
|
|
|
216
|
-
|
|
950
|
+
internal sealed class BridgeWorkItem
|
|
217
951
|
{
|
|
218
952
|
private readonly object StateLock = new object();
|
|
219
953
|
private bool cancelled;
|
|
220
954
|
private bool started;
|
|
221
|
-
private bool
|
|
222
|
-
private
|
|
955
|
+
private bool completionStarted;
|
|
956
|
+
private CancelRecoveryCandidate cancelRecoveryCandidate;
|
|
957
|
+
private readonly UnityCliBridgeWorkItemState lifecycle = new UnityCliBridgeWorkItemState();
|
|
223
958
|
private readonly CancellationTokenSource cancellation;
|
|
224
959
|
|
|
225
|
-
public BridgeWorkItem(UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes, string requestDigest, string admissionListenerId, long admissionGeneration, AdmissionOwnerIdentity admissionOwner)
|
|
960
|
+
public BridgeWorkItem(BridgeQueueState queueState, UnityCli.Protocol.BridgeRequest request, byte[] rawRequestBytes, string requestDigest, string admissionListenerId, long admissionGeneration, AdmissionOwnerIdentity admissionOwner, UnityCliBridgeQueuePolicy.WorkClass workClass, bool admissionBypass, bool admissionNewlyConsumed)
|
|
226
961
|
{
|
|
962
|
+
QueueState = queueState;
|
|
227
963
|
Request = request;
|
|
228
964
|
RawRequestBytes = rawRequestBytes;
|
|
229
965
|
RequestDigest = requestDigest;
|
|
230
966
|
AdmissionListenerId = admissionListenerId;
|
|
231
967
|
AdmissionGeneration = admissionGeneration;
|
|
232
968
|
AdmissionOwner = admissionOwner;
|
|
969
|
+
CapturedAdmissionPool = queueState == null ? null : queueState.AdmissionPool;
|
|
970
|
+
WorkClass = workClass;
|
|
971
|
+
AdmissionBypass = admissionBypass;
|
|
972
|
+
AdmissionNewlyConsumed = admissionNewlyConsumed;
|
|
973
|
+
AdmissionOwnerStateAtPublication = admissionOwner == null
|
|
974
|
+
? UnityCliAdmissionPool.AdmissionOwnerState.Granted
|
|
975
|
+
: admissionOwner.OwnerState;
|
|
976
|
+
PolicyNode = new UnityCliBridgeQueuePolicy.QueueNode(request.RequestId, request.Command, workClass);
|
|
233
977
|
cancellation = CreateExecutionLifetime(request);
|
|
234
978
|
}
|
|
235
979
|
|
|
980
|
+
public BridgeQueueState QueueState { get; private set; }
|
|
236
981
|
public UnityCli.Protocol.BridgeRequest Request { get; private set; }
|
|
237
982
|
public byte[] RawRequestBytes { get; private set; }
|
|
238
983
|
public string RequestDigest { get; private set; }
|
|
239
984
|
public string AdmissionListenerId { get; private set; }
|
|
240
985
|
public long AdmissionGeneration { get; private set; }
|
|
241
986
|
public AdmissionOwnerIdentity AdmissionOwner { get; private set; }
|
|
987
|
+
internal UnityCliAdmissionPool CapturedAdmissionPool { get; private set; }
|
|
988
|
+
public UnityCliBridgeQueuePolicy.WorkClass WorkClass { get; private set; }
|
|
989
|
+
public UnityCliBridgeQueuePolicy.QueueNode PolicyNode { get; private set; }
|
|
990
|
+
public bool AdmissionBypass { get; private set; }
|
|
991
|
+
internal bool AdmissionNewlyConsumed { get; private set; }
|
|
992
|
+
internal UnityCliAdmissionPool.AdmissionOwnerState AdmissionOwnerStateAtPublication { get; private set; }
|
|
993
|
+
internal bool NoEffectRejectionOwnsAdmission
|
|
994
|
+
{
|
|
995
|
+
get
|
|
996
|
+
{
|
|
997
|
+
return AdmissionOwner != null && AdmissionNewlyConsumed
|
|
998
|
+
&& AdmissionOwnerStateAtPublication == UnityCliAdmissionPool.AdmissionOwnerState.WorkItem;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
internal bool AttachedToExistingDurableOwner
|
|
1002
|
+
{
|
|
1003
|
+
get
|
|
1004
|
+
{
|
|
1005
|
+
return AdmissionOwner != null && !AdmissionNewlyConsumed
|
|
1006
|
+
&& AdmissionOwnerStateAtPublication != UnityCliAdmissionPool.AdmissionOwnerState.WorkItem
|
|
1007
|
+
&& AdmissionOwnerStateAtPublication != UnityCliAdmissionPool.AdmissionOwnerState.Granted;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
public LinkedListNode<BridgeWorkItem> QueueNode { get; set; }
|
|
1011
|
+
internal LinkedListNode<BridgeWorkItem> SafeStatusNode { get; set; }
|
|
1012
|
+
internal LinkedListNode<BridgeWorkItem> ControlNode { get; set; }
|
|
242
1013
|
public bool HasStarted { get { lock (StateLock) return started; } }
|
|
243
1014
|
public CancellationToken Token { get { return cancellation.Token; } }
|
|
244
1015
|
public ManualResetEventSlim Started { get; private set; } = new ManualResetEventSlim(false);
|
|
@@ -249,38 +1020,129 @@ namespace UnityCli
|
|
|
249
1020
|
get { lock (StateLock) return response; }
|
|
250
1021
|
set { lock (StateLock) response = value; }
|
|
251
1022
|
}
|
|
252
|
-
|
|
253
|
-
|
|
1023
|
+
internal long NextSettlementRetryAtMs { get; private set; }
|
|
1024
|
+
internal bool SettlementFailureLogged { get; set; }
|
|
1025
|
+
internal bool CompletionIntentReady { get; set; }
|
|
1026
|
+
internal bool NoEffectRejected { get; private set; }
|
|
254
1027
|
|
|
255
|
-
|
|
1028
|
+
internal string RequestDigestValue { get { return RequestDigest; } }
|
|
1029
|
+
internal void SetResponseForTests(string response) { Response = response; }
|
|
1030
|
+
|
|
1031
|
+
/// <summary>cancel terminal首次持久化双故障时,把有界候选冻结在仍存活的WorkItem供settlement重建marker。</summary>
|
|
1032
|
+
internal void FreezeCancelRecoveryCandidate(string rootRequestId, string rootRequestDigest, string runId, string effectState, object envelope)
|
|
256
1033
|
{
|
|
1034
|
+
var serialized = UnityCliSerializer.Serialize(envelope);
|
|
1035
|
+
if (string.IsNullOrEmpty(rootRequestId) || string.IsNullOrEmpty(rootRequestDigest) || string.IsNullOrEmpty(runId)
|
|
1036
|
+
|| string.IsNullOrEmpty(effectState) || envelope == null || Encoding.UTF8.GetByteCount(serialized) > MaxCancelRecoveryEnvelopeBytes)
|
|
1037
|
+
throw new InvalidOperationException("Cancel recovery candidate is incomplete or exceeds its fixed bound.");
|
|
257
1038
|
lock (StateLock)
|
|
258
1039
|
{
|
|
259
|
-
if (
|
|
1040
|
+
if (cancelRecoveryCandidate != null)
|
|
260
1041
|
{
|
|
261
|
-
|
|
1042
|
+
if (!string.Equals(cancelRecoveryCandidate.RootRequestId, rootRequestId, StringComparison.Ordinal)
|
|
1043
|
+
|| !string.Equals(cancelRecoveryCandidate.RootRequestDigest, rootRequestDigest, StringComparison.Ordinal)
|
|
1044
|
+
|| !string.Equals(cancelRecoveryCandidate.RunId, runId, StringComparison.Ordinal)
|
|
1045
|
+
|| !string.Equals(cancelRecoveryCandidate.EffectState, effectState, StringComparison.Ordinal)
|
|
1046
|
+
|| !string.Equals(UnityCliSerializer.Serialize(cancelRecoveryCandidate.Envelope), serialized, StringComparison.Ordinal))
|
|
1047
|
+
throw new InvalidOperationException("Cancel recovery candidate identity changed after it was frozen.");
|
|
1048
|
+
return;
|
|
262
1049
|
}
|
|
1050
|
+
cancelRecoveryCandidate = new CancelRecoveryCandidate
|
|
1051
|
+
{
|
|
1052
|
+
RootRequestId = rootRequestId,
|
|
1053
|
+
RootRequestDigest = rootRequestDigest,
|
|
1054
|
+
RunId = runId,
|
|
1055
|
+
EffectState = effectState,
|
|
1056
|
+
Envelope = UnityCliSerializer.Parse(serialized)
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
internal CancelRecoveryCandidate CaptureCancelRecoveryCandidate()
|
|
1062
|
+
{
|
|
1063
|
+
lock (StateLock)
|
|
1064
|
+
{
|
|
1065
|
+
if (cancelRecoveryCandidate == null) return null;
|
|
1066
|
+
return new CancelRecoveryCandidate
|
|
1067
|
+
{
|
|
1068
|
+
RootRequestId = cancelRecoveryCandidate.RootRequestId,
|
|
1069
|
+
RootRequestDigest = cancelRecoveryCandidate.RootRequestDigest,
|
|
1070
|
+
RunId = cancelRecoveryCandidate.RunId,
|
|
1071
|
+
EffectState = cancelRecoveryCandidate.EffectState,
|
|
1072
|
+
Envelope = UnityCliSerializer.Parse(UnityCliSerializer.Serialize(cancelRecoveryCandidate.Envelope))
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
internal sealed class CancelRecoveryCandidate
|
|
1078
|
+
{
|
|
1079
|
+
internal string RootRequestId;
|
|
1080
|
+
internal string RootRequestDigest;
|
|
1081
|
+
internal string RunId;
|
|
1082
|
+
internal string EffectState;
|
|
1083
|
+
internal object Envelope;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/// <summary>只允许已取消且从未启动的 selection reject 冻结 no-effect settlement。</summary>
|
|
1087
|
+
internal void MarkNoEffectRejected()
|
|
1088
|
+
{
|
|
1089
|
+
lock (StateLock)
|
|
1090
|
+
{
|
|
1091
|
+
if (started || !cancelled) throw new InvalidOperationException("No-effect rejection requires a cancelled, unstarted WorkItem.");
|
|
1092
|
+
NoEffectRejected = true;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
internal void ScheduleSettlementRetry(long nowMs)
|
|
1097
|
+
{
|
|
1098
|
+
NextSettlementRetryAtMs = checked(nowMs + 100L);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
internal bool TryBeginCompletion()
|
|
1102
|
+
{
|
|
1103
|
+
lock (StateLock)
|
|
1104
|
+
{
|
|
1105
|
+
if (completionStarted) return false;
|
|
1106
|
+
completionStarted = true;
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
internal void AllowCompletionRetry()
|
|
1112
|
+
{
|
|
1113
|
+
lock (StateLock) completionStarted = false;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
public bool TryStart()
|
|
1117
|
+
{
|
|
1118
|
+
lock (StateLock)
|
|
1119
|
+
{
|
|
263
1120
|
// admission与主线程开始必须属于同一listener epoch;reload后的旧队列项不得执行。
|
|
264
|
-
|
|
265
|
-
|
|
1121
|
+
int remainingMs = 0;
|
|
1122
|
+
if (Request != null) Request.TryDeadlineRemainingMs(out remainingMs);
|
|
1123
|
+
var outcome = lifecycle.TryStart(
|
|
1124
|
+
QueueState != null && string.Equals(AdmissionListenerId, QueueState.ListenerId, StringComparison.Ordinal) && AdmissionGeneration == QueueState.ListenerGeneration,
|
|
1125
|
+
remainingMs);
|
|
1126
|
+
if (outcome != UnityCliBridgeWorkItemState.StartOutcome.Started)
|
|
266
1127
|
{
|
|
267
|
-
cancelled = true;
|
|
1128
|
+
if (outcome == UnityCliBridgeWorkItemState.StartOutcome.DeadlineElapsed) cancelled = true;
|
|
1129
|
+
if (outcome == UnityCliBridgeWorkItemState.StartOutcome.Cancelled) cancelled = true;
|
|
1130
|
+
if (outcome == UnityCliBridgeWorkItemState.StartOutcome.ListenerChanged)
|
|
1131
|
+
{
|
|
1132
|
+
cancelled = true;
|
|
1133
|
+
response = UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
|
|
1134
|
+
Request.RequestId,
|
|
1135
|
+
"E_UNITY_DOMAIN_RELOAD",
|
|
1136
|
+
"Unity listener generation changed before the queued request started.",
|
|
1137
|
+
nextActions: new[] { "Keep the same root requestId and immutable request bytes while the session supervisor rebinds." },
|
|
1138
|
+
transient: true,
|
|
1139
|
+
retryAfterMs: 1000));
|
|
1140
|
+
}
|
|
268
1141
|
cancellation.Cancel();
|
|
269
|
-
response = UnityCliSerializer.Serialize(UnityCli.Protocol.ProtocolEnvelopeFactory.Error(
|
|
270
|
-
Request.RequestId,
|
|
271
|
-
"E_UNITY_DOMAIN_RELOAD",
|
|
272
|
-
"Unity listener generation changed before the queued request started.",
|
|
273
|
-
nextActions: new[] { "Keep the same root requestId and immutable request bytes while the session supervisor rebinds." },
|
|
274
|
-
transient: true,
|
|
275
|
-
retryAfterMs: 1000));
|
|
276
1142
|
return false;
|
|
277
1143
|
}
|
|
1144
|
+
cancellation.CancelAfter(TimeSpan.FromMilliseconds(remainingMs));
|
|
278
1145
|
started = true;
|
|
279
|
-
int remainingMs;
|
|
280
|
-
if (Request != null && Request.TryDeadlineRemainingMs(out remainingMs))
|
|
281
|
-
cancellation.CancelAfter(TimeSpan.FromMilliseconds(remainingMs));
|
|
282
|
-
else
|
|
283
|
-
cancellation.Cancel();
|
|
284
1146
|
Started.Set();
|
|
285
1147
|
return true;
|
|
286
1148
|
}
|
|
@@ -290,7 +1152,7 @@ namespace UnityCli
|
|
|
290
1152
|
{
|
|
291
1153
|
lock (StateLock)
|
|
292
1154
|
{
|
|
293
|
-
if (started)
|
|
1155
|
+
if (started || !lifecycle.CancelBeforeStart())
|
|
294
1156
|
{
|
|
295
1157
|
return false;
|
|
296
1158
|
}
|
|
@@ -300,35 +1162,23 @@ namespace UnityCli
|
|
|
300
1162
|
}
|
|
301
1163
|
}
|
|
302
1164
|
|
|
303
|
-
/// <summary>StateLock
|
|
1165
|
+
/// <summary>StateLock使HTTP接受与主线程启动使用同一内存裁决顺序。</summary>
|
|
304
1166
|
public bool TryAccept()
|
|
305
1167
|
{
|
|
306
|
-
lock (StateLock)
|
|
307
|
-
{
|
|
308
|
-
if (acceptanceCancelled) return false;
|
|
309
|
-
accepted = true;
|
|
310
|
-
return true;
|
|
311
|
-
}
|
|
1168
|
+
lock (StateLock) return lifecycle.TryAccept();
|
|
312
1169
|
}
|
|
313
1170
|
|
|
314
|
-
|
|
315
|
-
public bool CancelBeforeAcceptance()
|
|
1171
|
+
public void DisposeUnpublished()
|
|
316
1172
|
{
|
|
317
1173
|
lock (StateLock)
|
|
318
1174
|
{
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
public void Cancel()
|
|
327
|
-
{
|
|
328
|
-
lock (StateLock)
|
|
329
|
-
{
|
|
330
|
-
TimedOut = true;
|
|
1175
|
+
cancelled = true;
|
|
1176
|
+
try { cancellation.Cancel(); } catch { }
|
|
1177
|
+
Completed.Set();
|
|
331
1178
|
}
|
|
1179
|
+
cancellation.Dispose();
|
|
1180
|
+
Started.Dispose();
|
|
1181
|
+
Completed.Dispose();
|
|
332
1182
|
}
|
|
333
1183
|
|
|
334
1184
|
}
|