@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
|
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|
|
4
4
|
using Process = System.Diagnostics.Process;
|
|
5
5
|
using System.Globalization;
|
|
6
6
|
using System.IO;
|
|
7
|
+
using System.Linq;
|
|
7
8
|
using System.Net;
|
|
8
9
|
using System.Net.Sockets;
|
|
9
10
|
using System.Reflection;
|
|
@@ -21,13 +22,23 @@ namespace UnityCli
|
|
|
21
22
|
private const int ProtocolVersion = 2;
|
|
22
23
|
private const int RequestBodyLimitBytes = 1024 * 1024;
|
|
23
24
|
private const int MaxQueueItems = 128;
|
|
25
|
+
private const int MaxRequestCacheItems = 160;
|
|
24
26
|
private const int MaxRequestsPerFrame = 16;
|
|
25
27
|
private const int RegistryRefreshSeconds = 30;
|
|
28
|
+
private const int MaxSettlementRetriesPerTick = 16;
|
|
29
|
+
private const int SettlementRetryDelayMs = 100;
|
|
26
30
|
private static UnityCliRuntimeBridge current;
|
|
31
|
+
|
|
32
|
+
private sealed class RuntimeCatalogCursorException : FormatException
|
|
33
|
+
{
|
|
34
|
+
internal RuntimeCatalogCursorException() : base("The Runtime catalog continuation is altered, stale, or belongs to another query.") { }
|
|
35
|
+
}
|
|
27
36
|
private readonly ConcurrentQueue<PendingRequest> pending = new ConcurrentQueue<PendingRequest>();
|
|
28
37
|
private readonly object requestCacheSync = new object();
|
|
29
|
-
private readonly
|
|
38
|
+
private readonly UnityCliRuntimeRequestPublication<PendingRequest> requestCache = new UnityCliRuntimeRequestPublication<PendingRequest>(MaxRequestCacheItems);
|
|
39
|
+
private readonly Dictionary<string, PendingRequest> pendingSettlements = new Dictionary<string, PendingRequest>(StringComparer.Ordinal);
|
|
30
40
|
private readonly CancellationTokenSource stopping = new CancellationTokenSource();
|
|
41
|
+
private readonly byte[] catalogCursorSecret = CreateCatalogCursorSecret();
|
|
31
42
|
private long requestOrder;
|
|
32
43
|
private long mainThreadPulseSequence;
|
|
33
44
|
private long mainThreadPulseTimestamp;
|
|
@@ -36,6 +47,8 @@ namespace UnityCli
|
|
|
36
47
|
private UnityCliRuntimeRegistry registry;
|
|
37
48
|
private UnityCliRuntimeIdentity identity;
|
|
38
49
|
private UnityCliAdmissionPool admissionPool;
|
|
50
|
+
private Func<long> utcNowMsProvider = () => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
51
|
+
private Func<long> monotonicNowMsProvider = DefaultMonotonicMilliseconds;
|
|
39
52
|
private string sessionId;
|
|
40
53
|
private string listenerId;
|
|
41
54
|
private string buildId;
|
|
@@ -44,11 +57,27 @@ namespace UnityCli
|
|
|
44
57
|
private float nextRegistryRefreshAt;
|
|
45
58
|
private bool previousRunInBackground;
|
|
46
59
|
private bool runInBackgroundCaptured;
|
|
60
|
+
private readonly RuntimeOneShotTestHooks runtimeTestHooks = new RuntimeOneShotTestHooks();
|
|
61
|
+
|
|
62
|
+
private void HitRuntimeTestBoundary(string name)
|
|
63
|
+
{
|
|
64
|
+
runtimeTestHooks.Hit(name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
internal void ExpectRuntimeBoundaryForTests(string name, Action action)
|
|
68
|
+
{
|
|
69
|
+
runtimeTestHooks.ExpectOnce(name, action);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
internal void AssertRuntimeBoundariesDrainedForTests()
|
|
73
|
+
{
|
|
74
|
+
runtimeTestHooks.AssertDrained();
|
|
75
|
+
}
|
|
47
76
|
|
|
48
77
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
|
49
78
|
private static void StartBeforeSceneLoad()
|
|
50
79
|
{
|
|
51
|
-
#if UNITY_STANDALONE_WIN
|
|
80
|
+
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR
|
|
52
81
|
// Runtime assembly即使被Editor加载也不得启动listener或登记runtime claim。
|
|
53
82
|
if (!Debug.isDebugBuild || current != null) return;
|
|
54
83
|
RuntimeConfig config;
|
|
@@ -75,7 +104,7 @@ namespace UnityCli
|
|
|
75
104
|
var process = Process.GetCurrentProcess();
|
|
76
105
|
sessionId = "session-" + Sha256(config.InstanceId + "\0" + config.BuildId + "\0" + process.Id.ToString(CultureInfo.InvariantCulture) + "\0" + ProcessStartedAt(process));
|
|
77
106
|
listenerId = "listener-" + Guid.NewGuid().ToString("N");
|
|
78
|
-
admissionPool = new UnityCliAdmissionPool(config.InstanceId, sessionId, config.TaskPoolMaxActive, config.TaskPoolReopenAt);
|
|
107
|
+
admissionPool = new UnityCliAdmissionPool(config.InstanceId, sessionId, config.TaskPoolMaxActive, config.TaskPoolReopenAt, null, UtcNowMs);
|
|
79
108
|
int preferredPort;
|
|
80
109
|
listener = StartListener(config.InstanceId, out preferredPort, out port);
|
|
81
110
|
registry = new UnityCliRuntimeRegistry(identity, sessionId, listenerId, preferredPort, port, buildStamp, buildId);
|
|
@@ -98,8 +127,14 @@ namespace UnityCli
|
|
|
98
127
|
Interlocked.Increment(ref mainThreadPulseSequence);
|
|
99
128
|
PendingRequest request;
|
|
100
129
|
UnityCliRuntimeTools.TickFrame(Time.frameCount);
|
|
130
|
+
RetryPendingRuntimeSettlements();
|
|
101
131
|
var processed = 0;
|
|
102
|
-
while (processed < MaxRequestsPerFrame
|
|
132
|
+
while (processed < MaxRequestsPerFrame)
|
|
133
|
+
{
|
|
134
|
+
if (!TryTakeNextRuntimeRequest(out request)) break;
|
|
135
|
+
ExecuteOnMainThread(request);
|
|
136
|
+
processed++;
|
|
137
|
+
}
|
|
103
138
|
// Player 生命周期稳定时只低频刷新登记;启动和退出仍即时发布或移除。
|
|
104
139
|
if (registry != null && Time.realtimeSinceStartup >= nextRegistryRefreshAt)
|
|
105
140
|
{
|
|
@@ -114,15 +149,8 @@ namespace UnityCli
|
|
|
114
149
|
/// <summary>终止listener,完成active请求,清空session state并移除registry claim。</summary>
|
|
115
150
|
private void StopBridge()
|
|
116
151
|
{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// cancellation与request入队共享同一线性化点,锁释放后不能再产生本session work。
|
|
120
|
-
try { stopping.Cancel(); } catch { }
|
|
121
|
-
foreach (var entry in requestCache.Values)
|
|
122
|
-
if (!entry.Completion.Task.IsCompleted) entry.Completion.TrySetResult(Error(ReadString(entry.Request, "requestId") ?? string.Empty, "E_RUNTIME_SESSION_STALE", "The development Player session ended before the request completed.", new[] { "Launch a new development Player and submit a new request." }));
|
|
123
|
-
requestCache.Clear();
|
|
124
|
-
if (admissionPool != null) { admissionPool.Reset(); admissionPool = null; }
|
|
125
|
-
}
|
|
152
|
+
// cancellation与request入队共享同一线性化点,锁释放后不能再产生本session work。
|
|
153
|
+
StopRuntimeRequestsLocked();
|
|
126
154
|
try { if (listener != null) { listener.Stop(); listener.Close(); listener = null; } } catch { }
|
|
127
155
|
try { UnityCliRuntimeTools.Shutdown(); } catch { }
|
|
128
156
|
try { if (registry != null) { registry.Dispose(); registry = null; } } catch { }
|
|
@@ -184,65 +212,50 @@ namespace UnityCli
|
|
|
184
212
|
Write(context, Error(requestId, "E_INSTANCE_IDENTITY_MISMATCH", attestationError, new[] { "Resolve the runtime target again through --instance." }));
|
|
185
213
|
return;
|
|
186
214
|
}
|
|
215
|
+
var deadline = ValidateRequestStructure(envelope);
|
|
187
216
|
var digest = Sha256(body);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
lock (requestCacheSync)
|
|
193
|
-
{
|
|
194
|
-
if (stopping.IsCancellationRequested)
|
|
195
|
-
{
|
|
196
|
-
immediate = Error(requestId, "E_RUNTIME_SESSION_STALE", "The development Player session is stopping.", new[] { "Launch a new development Player and submit a new request." });
|
|
197
|
-
}
|
|
198
|
-
else if (requestCache.TryGetValue(requestId, out work))
|
|
199
|
-
{
|
|
200
|
-
if (!string.Equals(work.RequestDigest, digest, StringComparison.Ordinal))
|
|
201
|
-
immediate = Error(requestId, "E_REQUEST_REPLAY_MISMATCH", "requestId was reused with different immutable request bytes.", new[] { "Submit a new requestId for different Runtime Tool input." });
|
|
202
|
-
}
|
|
203
|
-
else
|
|
204
|
-
{
|
|
205
|
-
var deadline = ValidateRequest(envelope);
|
|
206
|
-
UnityCliAdmissionPool.ConsumedIdentity admissionOwner;
|
|
207
|
-
var admissionError = AuthorizeRuntimeAdmission(context.Request, envelope, out admissionOwner);
|
|
208
|
-
if (admissionError != null) immediate = admissionError;
|
|
209
|
-
else if (!ReserveRequestCacheSlot())
|
|
210
|
-
immediate = Error(requestId, "E_RUNTIME_QUEUE_FULL", "Runtime request cache is full of active work.", new[] { "Wait for active Player requests to complete and retry." }, true, 100);
|
|
211
|
-
else if (pending.Count >= MaxQueueItems)
|
|
212
|
-
immediate = Error(requestId, "E_RUNTIME_QUEUE_FULL", "Runtime bridge queue is full.", new[] { "Wait for the Player to process pending requests and retry." }, true, 100);
|
|
213
|
-
else
|
|
214
|
-
{
|
|
215
|
-
work = new PendingRequest { Request = envelope, Completion = new TaskCompletionSource<string>(), AbsoluteDeadlineMs = deadline, RequestDigest = digest, Order = ++requestOrder, AdmissionOwner = admissionOwner };
|
|
216
|
-
requestCache.Add(requestId, work);
|
|
217
|
-
enqueue = true;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (immediate == null && enqueue) pending.Enqueue(work);
|
|
221
|
-
}
|
|
222
|
-
if (immediate != null) { Write(context, immediate); return; }
|
|
223
|
-
while (!work.Completion.Task.IsCompleted)
|
|
224
|
-
{
|
|
225
|
-
var remaining = work.AbsoluteDeadlineMs - DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
226
|
-
if (remaining > 0L)
|
|
227
|
-
{
|
|
228
|
-
if (work.Completion.Task.Wait((int)Math.Min(remaining, int.MaxValue))) break;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
var timeout = Error(requestId, "E_EXECUTION_TIMEOUT", "Runtime request did not reach the effect barrier before its immutable deadline.", new[] { "Inspect runtime.state before submitting a new request." }, true, 100);
|
|
232
|
-
// deadline与main thread只允许一个owner;effect已开始时必须等待同一terminal,不能伪报timeout。
|
|
233
|
-
if (Interlocked.CompareExchange(ref work.ExecutionState, 2, 0) == 0)
|
|
234
|
-
{
|
|
235
|
-
work.Completion.TrySetResult(timeout);
|
|
236
|
-
if (work.AdmissionOwner != null) CompleteRuntimeAdmissionRoot(work.AdmissionOwner);
|
|
237
|
-
}
|
|
238
|
-
else work.Completion.Task.Wait();
|
|
239
|
-
}
|
|
240
|
-
Write(context, work.Completion.Task.Result);
|
|
217
|
+
var publication = PublishRuntimeRequest(envelope, deadline, digest, ReadRuntimeAdmission(context.Request));
|
|
218
|
+
var work = publication.Work;
|
|
219
|
+
if (publication.Error != null) { Write(context, publication.Error); return; }
|
|
220
|
+
Write(context, AwaitRuntimeResponse(work, requestId));
|
|
241
221
|
}
|
|
242
222
|
catch (FormatException exception) { Write(context, Error(requestId, "E_SCHEMA_INVALID", exception.Message, new[] { "Send protocolVersion 2 with a complete Runtime bridge request." })); }
|
|
243
223
|
catch (Exception exception) { Write(context, Error(requestId, "E_RUNTIME_BRIDGE_FAILED", "Runtime bridge could not process the request.", new[] { "Inspect Player logs and retry after the runtime is ready." })); Debug.LogWarning("UnityCli runtime request failed: " + exception.GetType().Name); }
|
|
244
224
|
}
|
|
245
225
|
|
|
226
|
+
/// <summary>HTTP只等待到immutable deadline或shutdown;running owner继续由主线程提交唯一terminal。</summary>
|
|
227
|
+
private string AwaitRuntimeResponse(PendingRequest work, string requestId)
|
|
228
|
+
{
|
|
229
|
+
return AwaitRuntimeResponse(work, requestId, new RuntimeWaitDependencies(
|
|
230
|
+
UtcNowMs,
|
|
231
|
+
(completion, timeoutMs) => Task.WaitAny(completion, Task.Delay(timeoutMs, stopping.Token)) == 0,
|
|
232
|
+
() => stopping.IsCancellationRequested));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
internal string AwaitRuntimeResponseForTests(PendingRequest work, RuntimeWaitDependencies dependencies)
|
|
236
|
+
{
|
|
237
|
+
if (work == null) throw new ArgumentNullException("work");
|
|
238
|
+
return AwaitRuntimeResponse(work, work.RequestId, dependencies);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private static string AwaitRuntimeResponse(PendingRequest work, string requestId, RuntimeWaitDependencies dependencies)
|
|
242
|
+
{
|
|
243
|
+
if (work == null || dependencies == null) throw new ArgumentNullException("Runtime wait dependencies must be explicit.");
|
|
244
|
+
while (!work.Completion.Task.IsCompleted)
|
|
245
|
+
{
|
|
246
|
+
var remaining = work.AbsoluteDeadlineMs - dependencies.UtcNowMs();
|
|
247
|
+
if (remaining <= 0L || dependencies.IsStopping())
|
|
248
|
+
{
|
|
249
|
+
var timeout = Error(requestId, "E_EXECUTION_TIMEOUT", "Runtime request did not reach a proven terminal before its immutable deadline or session shutdown.", new[] { "Inspect runtime.state before submitting a new request." }, true, 100);
|
|
250
|
+
if (work.Owner.FinalizeRuntimeRequest(work, timeout, 0)) return work.Completion.Task.Result;
|
|
251
|
+
return timeout;
|
|
252
|
+
}
|
|
253
|
+
var waitMs = checked((int)Math.Min(remaining, int.MaxValue));
|
|
254
|
+
dependencies.WaitForCompletion(work.Completion.Task, Math.Max(1, waitMs));
|
|
255
|
+
}
|
|
256
|
+
return work.Completion.Task.Result;
|
|
257
|
+
}
|
|
258
|
+
|
|
246
259
|
/// <summary>admission route只操作有界内存,不进入Player主线程队列。</summary>
|
|
247
260
|
private void HandleAdmission(HttpListenerContext context, string route)
|
|
248
261
|
{
|
|
@@ -258,7 +271,13 @@ namespace UnityCli
|
|
|
258
271
|
|| RequiredString(value, "target", 16) != "runtime") throw new FormatException("Runtime admission acquire identity is invalid.");
|
|
259
272
|
RequiredString(value, "command", 256);
|
|
260
273
|
var acquireFingerprint = RequiredString(value, "rootFingerprint", 64);
|
|
261
|
-
|
|
274
|
+
UnityCliAdmissionPool.AcquireResult result;
|
|
275
|
+
int active;
|
|
276
|
+
if (!TryAcquireRuntimeAdmission(requestId, acquireFingerprint, out result, out active))
|
|
277
|
+
{
|
|
278
|
+
Write(context, Error(requestId, "E_RUNTIME_SESSION_STALE", "The development Player session is stopping.", new[] { "Launch a new development Player and submit a new request." }));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
262
281
|
if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.Invalid) throw new FormatException("Runtime admission acquire fields are invalid.");
|
|
263
282
|
if (result.Outcome == UnityCliAdmissionPool.AcquireOutcome.IdentityMismatch)
|
|
264
283
|
{
|
|
@@ -274,7 +293,7 @@ namespace UnityCli
|
|
|
274
293
|
if (granted)
|
|
275
294
|
{
|
|
276
295
|
context.Response.Headers["x-unity-cli-admission-token"] = result.Token;
|
|
277
|
-
Debug.Log("UnityCli " + (result.NewlyGranted ? "admission_granted" : "admission_attached") + " requestId=" + requestId + " active=" +
|
|
296
|
+
Debug.Log("UnityCli " + (result.NewlyGranted ? "admission_granted" : "admission_attached") + " requestId=" + requestId + " active=" + active.ToString(CultureInfo.InvariantCulture));
|
|
278
297
|
}
|
|
279
298
|
Write(context, Success(requestId, new Dictionary<string, object>
|
|
280
299
|
{
|
|
@@ -295,8 +314,14 @@ namespace UnityCli
|
|
|
295
314
|
var token = context.Request.Headers["x-unity-cli-admission-token"];
|
|
296
315
|
if (string.IsNullOrEmpty(token) || token.Length > 256) throw new FormatException("Runtime admission token header is missing or invalid.");
|
|
297
316
|
if (!string.Equals(route, "/bridge/admission/revoke", StringComparison.Ordinal)) throw new FormatException("Runtime admission release route is invalid.");
|
|
298
|
-
|
|
299
|
-
|
|
317
|
+
bool revoked;
|
|
318
|
+
int remainingActive;
|
|
319
|
+
if (!TryRevokeRuntimeAdmission(requestId, fingerprint, token, out revoked, out remainingActive))
|
|
320
|
+
{
|
|
321
|
+
Write(context, Error(requestId, "E_RUNTIME_SESSION_STALE", "The development Player session is stopping.", new[] { "Launch a new development Player and submit a new request." }));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (revoked) Debug.Log("UnityCli admission_revoked requestId=" + requestId + " active=" + remainingActive.ToString(CultureInfo.InvariantCulture));
|
|
300
325
|
if (!revoked) { Write(context, Error(requestId, "E_ADMISSION_IDENTITY_MISMATCH", "Admission revoke identity does not match an unconsumed Runtime root.", new[] { "Inspect the selected Runtime session before retrying." })); return; }
|
|
301
326
|
Write(context, Success(requestId, new Dictionary<string, object> { { "revoked", true } }));
|
|
302
327
|
}
|
|
@@ -304,80 +329,405 @@ namespace UnityCli
|
|
|
304
329
|
catch (Exception exception) { Write(context, Error(requestId, "E_RUNTIME_BRIDGE_FAILED", "Runtime admission failed: " + exception.GetType().Name + ".", new[] { "Inspect the Player session before retrying." })); }
|
|
305
330
|
}
|
|
306
331
|
|
|
332
|
+
/// <summary>admission端点与execute publication、dequeue和stop共享requestCacheSync。</summary>
|
|
333
|
+
private bool TryAcquireRuntimeAdmission(string requestId, string fingerprint, out UnityCliAdmissionPool.AcquireResult result, out int active)
|
|
334
|
+
{
|
|
335
|
+
lock (requestCacheSync)
|
|
336
|
+
{
|
|
337
|
+
result = null;
|
|
338
|
+
active = 0;
|
|
339
|
+
if (stopping.IsCancellationRequested || admissionPool == null) return false;
|
|
340
|
+
HitRuntimeTestBoundary("before-runtime-admission-acquire");
|
|
341
|
+
result = admissionPool.Acquire(requestId, fingerprint, listenerId, 1L);
|
|
342
|
+
active = admissionPool.Capture().Active;
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private bool TryRevokeRuntimeAdmission(string requestId, string fingerprint, string token, out bool revoked, out int active)
|
|
348
|
+
{
|
|
349
|
+
lock (requestCacheSync)
|
|
350
|
+
{
|
|
351
|
+
revoked = false;
|
|
352
|
+
active = 0;
|
|
353
|
+
if (stopping.IsCancellationRequested || admissionPool == null) return false;
|
|
354
|
+
revoked = admissionPool.Revoke(requestId, fingerprint, token);
|
|
355
|
+
active = admissionPool.Capture().Active;
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
internal bool TryAcquireRuntimeAdmissionForTests(string requestId, string fingerprint, out UnityCliAdmissionPool.AcquireResult result)
|
|
361
|
+
{
|
|
362
|
+
int active;
|
|
363
|
+
return TryAcquireRuntimeAdmission(requestId, fingerprint, out result, out active);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private static RuntimeAdmissionInput ReadRuntimeAdmission(HttpListenerRequest request)
|
|
367
|
+
{
|
|
368
|
+
return new RuntimeAdmissionInput
|
|
369
|
+
{
|
|
370
|
+
RootId = request.Headers["x-unity-cli-admission-root-id"],
|
|
371
|
+
Fingerprint = request.Headers["x-unity-cli-admission-fingerprint"],
|
|
372
|
+
Token = request.Headers["x-unity-cli-admission-token"],
|
|
373
|
+
DeadlineUtc = request.Headers["x-unity-cli-admission-deadline-utc"]
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
307
377
|
/// <summary>新业务request必须消费同session grant;同字节requestCache replay不再次授权。</summary>
|
|
308
|
-
private string AuthorizeRuntimeAdmission(
|
|
378
|
+
private string AuthorizeRuntimeAdmission(UnityCliAdmissionPool pool, RuntimeAdmissionInput admission, Dictionary<string, object> request, string requestDigest, out UnityCliAdmissionPool.ConsumedIdentity admissionOwner, out bool newlyConsumed)
|
|
309
379
|
{
|
|
310
380
|
admissionOwner = null;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
var
|
|
314
|
-
|
|
315
|
-
var any = !string.IsNullOrEmpty(rootId) || !string.IsNullOrEmpty(fingerprint) || !string.IsNullOrEmpty(token) || !string.IsNullOrEmpty(deadlineText);
|
|
381
|
+
newlyConsumed = false;
|
|
382
|
+
admission = admission ?? new RuntimeAdmissionInput();
|
|
383
|
+
var any = !string.IsNullOrEmpty(admission.RootId) || !string.IsNullOrEmpty(admission.Fingerprint)
|
|
384
|
+
|| !string.IsNullOrEmpty(admission.Token) || !string.IsNullOrEmpty(admission.DeadlineUtc);
|
|
316
385
|
var requestId = ReadString(request, "requestId") ?? string.Empty;
|
|
317
386
|
var command = ReadString(request, "command");
|
|
318
387
|
if (!any)
|
|
319
388
|
{
|
|
320
|
-
if (
|
|
389
|
+
if (RuntimeCatalogReadCommand(command)) return null;
|
|
321
390
|
return Error(requestId, "E_ADMISSION_REQUIRED", "A valid Unity task-pool admission is required before this Runtime business request can enter the queue.", new[] { "Use the current unitycli to submit the Runtime Tool." });
|
|
322
391
|
}
|
|
323
|
-
if (string.IsNullOrEmpty(
|
|
392
|
+
if (string.IsNullOrEmpty(admission.RootId) || string.IsNullOrEmpty(admission.Fingerprint)
|
|
393
|
+
|| string.IsNullOrEmpty(admission.Token) || string.IsNullOrEmpty(admission.DeadlineUtc))
|
|
324
394
|
return Error(requestId, "E_ADMISSION_IDENTITY_MISMATCH", "Runtime admission headers are incomplete.", new[] { "Resolve the Runtime target again before retrying." });
|
|
325
|
-
if (!string.Equals(
|
|
395
|
+
if (!string.Equals(admission.RootId, requestId, StringComparison.Ordinal))
|
|
326
396
|
return Error(requestId, "E_ADMISSION_IDENTITY_MISMATCH", "Runtime admission root requestId does not match the Bridge request body.", new[] { "Submit the request through the current unitycli." });
|
|
327
397
|
DateTimeOffset deadline;
|
|
328
|
-
if (!DateTimeOffset.TryParse(
|
|
398
|
+
if (!DateTimeOffset.TryParse(admission.DeadlineUtc, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out deadline))
|
|
329
399
|
return Error(requestId, "E_ADMISSION_IDENTITY_MISMATCH", "Runtime admission deadline is invalid.", new[] { "Submit the request through the current unitycli." });
|
|
330
|
-
|
|
331
|
-
|
|
400
|
+
if (pool == null) return Error(requestId, "E_RUNTIME_SESSION_STALE", "The development Player session ended before admission completed.", new[] { "Launch a new development Player and submit a new request." });
|
|
401
|
+
var result = pool.AuthorizeAndCapture(admission.RootId, admission.Fingerprint, requestDigest, admission.Token, listenerId, 1L, deadline.ToUnixTimeMilliseconds());
|
|
402
|
+
if (result.Outcome != UnityCliAdmissionPool.AuthorizeOutcome.Authorized || result.Identity == null)
|
|
403
|
+
{
|
|
404
|
+
if ((result.Outcome == UnityCliAdmissionPool.AuthorizeOutcome.Invalid
|
|
405
|
+
|| result.Outcome == UnityCliAdmissionPool.AuthorizeOutcome.RecoveryBlocked)
|
|
406
|
+
&& !pool.Revoke(admission.RootId, admission.Fingerprint, admission.Token))
|
|
407
|
+
return Error(requestId, "E_EXECUTION_TIMEOUT", "Runtime admission rejection could not prove exact unconsumed cleanup.", new[] { "Launch a new development Player before submitting more work." });
|
|
332
408
|
return Error(requestId, "E_ADMISSION_IDENTITY_MISMATCH", "Runtime admission token is missing, expired, or bound to another root.", new[] { "Wait for a fresh Runtime admission before submitting business work." });
|
|
333
|
-
|
|
334
|
-
|
|
409
|
+
}
|
|
410
|
+
admissionOwner = result.Identity;
|
|
411
|
+
newlyConsumed = result.NewlyConsumed;
|
|
335
412
|
return null;
|
|
336
413
|
}
|
|
337
414
|
|
|
338
|
-
/// <summary
|
|
415
|
+
/// <summary>requestCacheSync内取得effect ownership后,所有Unity API只在Update主线程处理。</summary>
|
|
339
416
|
private void ExecuteOnMainThread(PendingRequest pendingRequest)
|
|
340
417
|
{
|
|
341
|
-
// 只有pending request可取得effect ownership;deadline或其他waiter已终结时不得执行。
|
|
342
|
-
if (Interlocked.CompareExchange(ref pendingRequest.ExecutionState, 1, 0) != 0) return;
|
|
343
418
|
var request = pendingRequest.Request;
|
|
344
419
|
var requestId = ReadString(request, "requestId") ?? string.Empty;
|
|
420
|
+
string terminal;
|
|
345
421
|
try
|
|
346
422
|
{
|
|
347
423
|
var deadline = pendingRequest.AbsoluteDeadlineMs;
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
{
|
|
352
|
-
pendingRequest.Completion.TrySetResult(Success(requestId, new Dictionary<string, object> { { "tools", new List<object>() }, { "target", "runtime" }, { "runtimeSessionId", sessionId } })); return;
|
|
353
|
-
}
|
|
354
|
-
if (string.Equals(command, "runtime-tool.run", StringComparison.Ordinal))
|
|
424
|
+
if (UtcNowMs() >= deadline)
|
|
425
|
+
terminal = Error(requestId, "E_EXECUTION_TIMEOUT", "Runtime request deadline elapsed before execution.", new[] { "Retry only after the Player is responsive." }, true, 100);
|
|
426
|
+
else
|
|
355
427
|
{
|
|
356
|
-
var
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
428
|
+
var command = RequiredString(request, "command", 128);
|
|
429
|
+
if (RuntimeCatalogReadCommand(command))
|
|
430
|
+
{
|
|
431
|
+
var input = ReadObject(request, "input") ?? new Dictionary<string, object>();
|
|
432
|
+
ValidateRuntimeCatalogInput(command, input);
|
|
433
|
+
// Catalog continuation只绑定当前Runtime session与唯一listener generation。
|
|
434
|
+
var generation = Sha256(sessionId + "\0" + "1");
|
|
435
|
+
var expectedGeneration = ReadString(input, "expectedCatalogGeneration");
|
|
436
|
+
if (!string.IsNullOrEmpty(expectedGeneration) && !string.Equals(expectedGeneration, generation, StringComparison.Ordinal))
|
|
437
|
+
terminal = Error(requestId, "E_TOOL_CATALOG_CHANGED", "The Tool catalog generation changed before this continuation page was read.", new[] { "Restart this discovery query from its first page against the current Runtime generation." });
|
|
438
|
+
else if (string.Equals(command, "runtime-tool.groups", StringComparison.Ordinal))
|
|
439
|
+
terminal = Success(requestId, new Dictionary<string, object> { { "catalogGeneration", generation }, { "groups", new List<object>() } });
|
|
440
|
+
else if (string.Equals(command, "runtime-tool.summary", StringComparison.Ordinal))
|
|
441
|
+
terminal = Success(requestId, RuntimeCatalogSummaryData(input, generation));
|
|
442
|
+
else if (string.Equals(command, "runtime-tool.describe", StringComparison.Ordinal))
|
|
443
|
+
terminal = Success(requestId, new Dictionary<string, object> { { "catalogGeneration", generation }, { "found", false } });
|
|
444
|
+
else
|
|
445
|
+
terminal = Success(requestId, new Dictionary<string, object> { { "tools", new List<object>() }, { "target", "runtime" }, { "runtimeSessionId", sessionId } });
|
|
446
|
+
}
|
|
447
|
+
else if (string.Equals(command, "runtime-tool.run", StringComparison.Ordinal))
|
|
448
|
+
{
|
|
449
|
+
var wrapper = ReadObject(request, "input");
|
|
450
|
+
if (wrapper == null || !HasOnlyKeys(wrapper, "name", "input", "artifactDir", "playModeSessionId")) throw new FormatException("runtime-tool.run wrapper is invalid.");
|
|
451
|
+
var name = RequiredString(wrapper, "name", 256);
|
|
452
|
+
var toolInput = ReadObject(wrapper, "input");
|
|
453
|
+
if (toolInput == null) throw new FormatException("runtime-tool.run input must be an object.");
|
|
454
|
+
var execution = ReadObject(request, "execution");
|
|
455
|
+
object dryRunValue;
|
|
456
|
+
var dryRun = execution != null && execution.TryGetValue("dryRun", out dryRunValue) && dryRunValue is bool && (bool)dryRunValue;
|
|
457
|
+
var result = UnityCliRuntimeTools.Execute(name, toolInput, dryRun);
|
|
458
|
+
terminal = Success(requestId, new Dictionary<string, object> { { "result", result } });
|
|
459
|
+
}
|
|
460
|
+
else
|
|
461
|
+
{
|
|
462
|
+
terminal = Error(requestId, "E_TOOL_NOT_FOUND", "Runtime bridge command is not approved.", new[] { "Use the public Tool catalog to select a Runtime Tool." });
|
|
463
|
+
}
|
|
367
464
|
}
|
|
368
|
-
pendingRequest.Completion.TrySetResult(Error(requestId, "E_TOOL_NOT_FOUND", "Runtime bridge command is not approved.", new[] { "Use the public Tool catalog to select a Runtime Tool." }));
|
|
369
465
|
}
|
|
370
|
-
catch (UnityCliRuntimeToolException exception) {
|
|
371
|
-
catch (
|
|
372
|
-
catch (
|
|
373
|
-
|
|
466
|
+
catch (UnityCliRuntimeToolException exception) { terminal = Error(requestId, exception.Code, exception.Message, new[] { "Run tools describe for the selected Runtime Tool and inspect the current Player session." }); }
|
|
467
|
+
catch (RuntimeCatalogCursorException exception) { terminal = Error(requestId, "E_DISCOVERY_CURSOR_INVALID", exception.Message, new[] { "Restart this exact discovery query from its first page." }); }
|
|
468
|
+
catch (FormatException exception) { terminal = Error(requestId, "E_SCHEMA_INVALID", exception.Message, new[] { "Use the Runtime bridge request schema." }); }
|
|
469
|
+
catch (Exception exception) { terminal = Error(requestId, "E_RUNTIME_BRIDGE_FAILED", "Runtime command failed before a terminal result was proven.", new[] { "Inspect Player logs and retry after the runtime is ready." }); Debug.LogWarning("UnityCli runtime main-thread command failed: " + exception.GetType().Name); }
|
|
470
|
+
FinalizeRuntimeRequest(pendingRequest, terminal, 1);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private static bool RuntimeCatalogReadCommand(string command)
|
|
474
|
+
{
|
|
475
|
+
return string.Equals(command, "runtime-tool.groups", StringComparison.Ordinal)
|
|
476
|
+
|| string.Equals(command, "runtime-tool.summary", StringComparison.Ordinal)
|
|
477
|
+
|| string.Equals(command, "runtime-tool.describe", StringComparison.Ordinal)
|
|
478
|
+
|| string.Equals(command, "runtime-tool.list", StringComparison.Ordinal);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/// <summary>Runtime catalog是私有闭集协议;空执行器也必须拒绝畸形或无界输入。</summary>
|
|
482
|
+
private static void ValidateRuntimeCatalogInput(string command, Dictionary<string, object> input)
|
|
483
|
+
{
|
|
484
|
+
var expected = ReadString(input, "expectedCatalogGeneration");
|
|
485
|
+
if (expected != null && Encoding.UTF8.GetByteCount(expected) > 128) throw new FormatException("Catalog generation exceeds its bounded string limit.");
|
|
486
|
+
object reservedValue;
|
|
487
|
+
if (input.TryGetValue("reservedNames", out reservedValue))
|
|
488
|
+
{
|
|
489
|
+
var reserved = reservedValue as List<object>;
|
|
490
|
+
if (reserved == null || reserved.Count > 4096 || reserved.Any(value => !(value is string) || string.IsNullOrWhiteSpace((string)value) || Encoding.UTF8.GetByteCount((string)value) > 256))
|
|
491
|
+
throw new FormatException("Catalog reserved names exceed their bounded item or string limits.");
|
|
492
|
+
}
|
|
493
|
+
if (string.Equals(command, "runtime-tool.groups", StringComparison.Ordinal))
|
|
494
|
+
{
|
|
495
|
+
if (!HasOnlyKeys(input, "expectedCatalogGeneration", "reservedNames")) throw new FormatException("Runtime catalog groups input contains an unknown field.");
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (string.Equals(command, "runtime-tool.summary", StringComparison.Ordinal))
|
|
499
|
+
{
|
|
500
|
+
if (!HasOnlyKeys(input, "group", "keyword", "limit", "expectedCatalogGeneration", "reservedNames", "localNames", "cursorContext", "cursorToken"))
|
|
501
|
+
throw new FormatException("Runtime catalog summary input contains an unknown field.");
|
|
502
|
+
if (!input.ContainsKey("reservedNames") || !input.ContainsKey("localNames") || !input.ContainsKey("cursorContext"))
|
|
503
|
+
throw new FormatException("Runtime catalog summary input is missing its cursor context fields.");
|
|
504
|
+
var limit = ReadLong(input, "limit");
|
|
505
|
+
if (limit < 1L || limit > 101L) throw new FormatException("Catalog summary limit must be an integer in [1,101].");
|
|
506
|
+
OptionalRuntimeCatalogString(input, "group", 256);
|
|
507
|
+
OptionalRuntimeCatalogString(input, "keyword", 2048);
|
|
508
|
+
OptionalRuntimeCatalogString(input, "cursorContext", 64);
|
|
509
|
+
OptionalRuntimeCatalogString(input, "cursorToken", 8192);
|
|
510
|
+
var keyword = ReadString(input, "keyword") ?? string.Empty;
|
|
511
|
+
var tokens = keyword.Split('|').Select(token => token.Trim()).Where(token => token.Length > 0).ToArray();
|
|
512
|
+
if (tokens.Length > 16 || tokens.Any(token => Encoding.UTF8.GetByteCount(token) > 128))
|
|
513
|
+
throw new FormatException("Catalog search accepts at most 16 keywords of at most 128 UTF-8 bytes each.");
|
|
514
|
+
RuntimeCatalogStringList(input, "localNames", true);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (string.Equals(command, "runtime-tool.describe", StringComparison.Ordinal))
|
|
518
|
+
{
|
|
519
|
+
if (!HasOnlyKeys(input, "name")) throw new FormatException("Runtime catalog describe input contains an unknown field.");
|
|
520
|
+
var name = RequiredString(input, "name", 256);
|
|
521
|
+
if (Encoding.UTF8.GetByteCount(name) > 256) throw new FormatException("Runtime catalog describe name exceeds 256 UTF-8 bytes.");
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (!HasOnlyKeys(input)) throw new FormatException("Runtime catalog list input contains an unknown field.");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/// <summary>Runtime空live catalog仍签发本listener generation的opaque continuation并证明local page边界。</summary>
|
|
528
|
+
private Dictionary<string, object> RuntimeCatalogSummaryData(Dictionary<string, object> input, string generation)
|
|
529
|
+
{
|
|
530
|
+
var localNames = RuntimeCatalogStringList(input, "localNames", true);
|
|
531
|
+
var reservedNames = RuntimeCatalogStringList(input, "reservedNames", false);
|
|
532
|
+
if (localNames.Any(name => !reservedNames.Contains(name))) throw new FormatException("Catalog local names must be a subset of reserved names.");
|
|
533
|
+
var context = ReadString(input, "cursorContext");
|
|
534
|
+
if (string.IsNullOrEmpty(context) || context.Length != 64 || context.Any(value => !Uri.IsHexDigit(value)))
|
|
535
|
+
throw new FormatException("Runtime catalog cursor context is invalid.");
|
|
536
|
+
var scope = Sha256(generation + "\n" + context + "\n" + (ReadString(input, "group") ?? string.Empty) + "\n"
|
|
537
|
+
+ (ReadString(input, "keyword") ?? string.Empty) + "\n" + string.Join("\n", localNames) + "\n--reserved--\n"
|
|
538
|
+
+ string.Join("\n", reservedNames));
|
|
539
|
+
int offset;
|
|
540
|
+
string boundary;
|
|
541
|
+
ReadRuntimeCatalogCursor(ReadString(input, "cursorToken"), scope, out offset, out boundary);
|
|
542
|
+
if (offset > localNames.Count || offset > 0 && !string.Equals(localNames[offset - 1], boundary, StringComparison.Ordinal))
|
|
543
|
+
throw new RuntimeCatalogCursorException();
|
|
544
|
+
var limit = (int)ReadLong(input, "limit");
|
|
545
|
+
var pageNames = localNames.Skip(offset).Take(limit).ToList();
|
|
546
|
+
var end = offset + pageNames.Count;
|
|
547
|
+
var data = new Dictionary<string, object>
|
|
548
|
+
{
|
|
549
|
+
{ "catalogGeneration", generation }, { "total", 0L }, { "hasMore", end < localNames.Count },
|
|
550
|
+
{ "tools", new List<object>() }, { "pageNames", pageNames.Cast<object>().ToList() },
|
|
551
|
+
{ "continuationToken", SignRuntimeCatalogCursor(scope, end, end > 0 ? localNames[end - 1] : string.Empty) }
|
|
552
|
+
};
|
|
553
|
+
if (Encoding.UTF8.GetByteCount(UnityCliRuntimeJson.Serialize(data)) > 256 * 1024)
|
|
554
|
+
throw new FormatException("Runtime catalog summary exceeds the bounded UTF-8 transport size.");
|
|
555
|
+
return data;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/// <summary>解析并按UTF-8预算校验Runtime catalog名称集合。</summary>
|
|
559
|
+
private static List<string> RuntimeCatalogStringList(Dictionary<string, object> input, string key, bool requireSorted)
|
|
560
|
+
{
|
|
561
|
+
object raw;
|
|
562
|
+
if (!input.TryGetValue(key, out raw)) return new List<string>();
|
|
563
|
+
var values = raw as List<object>;
|
|
564
|
+
if (values == null || values.Count > 4096 || values.Any(value => !(value is string)
|
|
565
|
+
|| string.IsNullOrWhiteSpace((string)value) || Encoding.UTF8.GetByteCount((string)value) > 256))
|
|
566
|
+
throw new FormatException("Runtime catalog field " + key + " is invalid.");
|
|
567
|
+
var result = values.Cast<string>().ToList();
|
|
568
|
+
if (requireSorted && !result.SequenceEqual(result.Distinct(StringComparer.Ordinal).OrderBy(value => value, StringComparer.Ordinal)))
|
|
569
|
+
throw new FormatException("Runtime catalog local names must be ordinally sorted and unique.");
|
|
570
|
+
return result;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/// <summary>验证当前Player listener签发的continuation并恢复边界。</summary>
|
|
574
|
+
private void ReadRuntimeCatalogCursor(string token, string scope, out int offset, out string boundary)
|
|
575
|
+
{
|
|
576
|
+
offset = 0;
|
|
577
|
+
boundary = string.Empty;
|
|
578
|
+
if (string.IsNullOrEmpty(token)) return;
|
|
579
|
+
try
|
|
580
|
+
{
|
|
581
|
+
var parts = token.Split('.');
|
|
582
|
+
if (parts.Length != 2) throw new RuntimeCatalogCursorException();
|
|
583
|
+
var signature = RuntimeBase64UrlDecode(parts[1]);
|
|
584
|
+
byte[] expected;
|
|
585
|
+
using (var hmac = new HMACSHA256(catalogCursorSecret)) expected = hmac.ComputeHash(Encoding.UTF8.GetBytes(parts[0]));
|
|
586
|
+
if (!RuntimeFixedTimeEqual(signature, expected)) throw new RuntimeCatalogCursorException();
|
|
587
|
+
var fields = Encoding.UTF8.GetString(RuntimeBase64UrlDecode(parts[0])).Split('\n');
|
|
588
|
+
int parsed;
|
|
589
|
+
if (fields.Length != 3 || !string.Equals(fields[0], scope, StringComparison.Ordinal)
|
|
590
|
+
|| !int.TryParse(fields[1], NumberStyles.None, CultureInfo.InvariantCulture, out parsed) || parsed < 0)
|
|
591
|
+
throw new RuntimeCatalogCursorException();
|
|
592
|
+
offset = parsed;
|
|
593
|
+
boundary = Encoding.UTF8.GetString(Convert.FromBase64String(fields[2]));
|
|
594
|
+
if (Encoding.UTF8.GetByteCount(boundary) > 256 || (offset == 0) != string.IsNullOrEmpty(boundary))
|
|
595
|
+
throw new RuntimeCatalogCursorException();
|
|
596
|
+
}
|
|
597
|
+
catch (RuntimeCatalogCursorException) { throw; }
|
|
598
|
+
catch { throw new RuntimeCatalogCursorException(); }
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/// <summary>使用当前Player listener secret签发opaque continuation。</summary>
|
|
602
|
+
private string SignRuntimeCatalogCursor(string scope, int offset, string boundary)
|
|
603
|
+
{
|
|
604
|
+
var payloadText = scope + "\n" + offset.ToString(CultureInfo.InvariantCulture) + "\n"
|
|
605
|
+
+ Convert.ToBase64String(Encoding.UTF8.GetBytes(boundary ?? string.Empty));
|
|
606
|
+
var payload = RuntimeBase64UrlEncode(Encoding.UTF8.GetBytes(payloadText));
|
|
607
|
+
byte[] signature;
|
|
608
|
+
using (var hmac = new HMACSHA256(catalogCursorSecret)) signature = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
|
|
609
|
+
return payload + "." + RuntimeBase64UrlEncode(signature);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/// <summary>secret不落盘,Player listener重启后旧token失效。</summary>
|
|
613
|
+
private static byte[] CreateCatalogCursorSecret()
|
|
614
|
+
{
|
|
615
|
+
var bytes = new byte[32];
|
|
616
|
+
using (var random = RandomNumberGenerator.Create()) random.GetBytes(bytes);
|
|
617
|
+
return bytes;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/// <summary>编码URL安全token片段。</summary>
|
|
621
|
+
private static string RuntimeBase64UrlEncode(byte[] value)
|
|
622
|
+
{
|
|
623
|
+
return Convert.ToBase64String(value).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/// <summary>解码URL安全token片段。</summary>
|
|
627
|
+
private static byte[] RuntimeBase64UrlDecode(string value)
|
|
628
|
+
{
|
|
629
|
+
var normalized = value.Replace('-', '+').Replace('_', '/');
|
|
630
|
+
normalized = normalized.PadRight(normalized.Length + (4 - normalized.Length % 4) % 4, '=');
|
|
631
|
+
return Convert.FromBase64String(normalized);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/// <summary>常量步数比较Runtime token签名。</summary>
|
|
635
|
+
private static bool RuntimeFixedTimeEqual(byte[] left, byte[] right)
|
|
636
|
+
{
|
|
637
|
+
if (left == null || right == null || left.Length != right.Length) return false;
|
|
638
|
+
var difference = 0;
|
|
639
|
+
for (var index = 0; index < left.Length; index++) difference |= left[index] ^ right[index];
|
|
640
|
+
return difference == 0;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private static void OptionalRuntimeCatalogString(Dictionary<string, object> input, string key, int maximum)
|
|
644
|
+
{
|
|
645
|
+
object value;
|
|
646
|
+
if (!input.TryGetValue(key, out value)) return;
|
|
647
|
+
var text = value as string;
|
|
648
|
+
if (text == null || Encoding.UTF8.GetByteCount(text) > maximum) throw new FormatException("Runtime catalog field " + key + " is invalid.");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/// <summary>先冻结terminal,再仅在captured pool证明exact completion或definite absence后公开完成。</summary>
|
|
652
|
+
private bool FinalizeRuntimeRequest(PendingRequest request, string terminal, int expectedState)
|
|
653
|
+
{
|
|
654
|
+
if (request == null || request.Owner != this) return false;
|
|
655
|
+
if (Interlocked.CompareExchange(ref request.ExecutionState, 2, expectedState) != expectedState) return false;
|
|
656
|
+
request.Terminal = terminal;
|
|
657
|
+
if (TrySettleRuntimeRequest(request)) return true;
|
|
658
|
+
RetainRuntimeSettlement(request);
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private bool TrySettleRuntimeRequest(PendingRequest request, bool shutdownOnly = false)
|
|
663
|
+
{
|
|
664
|
+
if (request == null || Interlocked.CompareExchange(ref request.ExecutionState, 4, 2) != 2)
|
|
665
|
+
return request != null && Volatile.Read(ref request.ExecutionState) == 3;
|
|
666
|
+
var pool = request.AdmissionPool;
|
|
667
|
+
var identity = request.AdmissionOwner;
|
|
668
|
+
var outcome = identity == null
|
|
669
|
+
? UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent
|
|
670
|
+
: pool == null
|
|
671
|
+
? UnityCliAdmissionPool.CompletionOutcome.IdentityMismatchOrBlocked
|
|
672
|
+
: shutdownOnly ? pool.CompleteWorkItemForShutdown(identity) : pool.CompleteTrusted(identity);
|
|
673
|
+
if (outcome != UnityCliAdmissionPool.CompletionOutcome.ExactCurrentCompleted
|
|
674
|
+
&& outcome != UnityCliAdmissionPool.CompletionOutcome.DefinitelyAbsent)
|
|
675
|
+
{
|
|
676
|
+
Interlocked.Exchange(ref request.ExecutionState, 2);
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
lock (requestCacheSync) pendingSettlements.Remove(request.RequestId);
|
|
680
|
+
Interlocked.Exchange(ref request.ExecutionState, 3);
|
|
681
|
+
request.Completion.TrySetResult(request.Terminal);
|
|
682
|
+
if (pool != null && identity != null && outcome == UnityCliAdmissionPool.CompletionOutcome.ExactCurrentCompleted)
|
|
683
|
+
Debug.Log("UnityCli admission_terminal requestId=" + identity.RequestId + " outcome=" + outcome.ToString() + " active=" + pool.Capture().Active.ToString(CultureInfo.InvariantCulture));
|
|
684
|
+
return true;
|
|
374
685
|
}
|
|
375
686
|
|
|
376
|
-
private void
|
|
687
|
+
private void RetainRuntimeSettlement(PendingRequest request)
|
|
377
688
|
{
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
689
|
+
request.NextSettlementRetryAtMs = checked(MonotonicNowMs() + SettlementRetryDelayMs);
|
|
690
|
+
lock (requestCacheSync)
|
|
691
|
+
{
|
|
692
|
+
PendingRequest existing;
|
|
693
|
+
if (pendingSettlements.TryGetValue(request.RequestId, out existing) && !object.ReferenceEquals(existing, request))
|
|
694
|
+
throw new InvalidOperationException("Runtime settlement requestId is already owned by another exact request.");
|
|
695
|
+
if (pendingSettlements.Count >= MaxRequestCacheItems && !pendingSettlements.ContainsKey(request.RequestId))
|
|
696
|
+
throw new InvalidOperationException("Runtime settlement journal reached its fixed bound.");
|
|
697
|
+
pendingSettlements[request.RequestId] = request;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/// <summary>每次只推进固定数量且使用固定单调退避;失败identity保留captured pool与完整owner。</summary>
|
|
702
|
+
private void RetryPendingRuntimeSettlements()
|
|
703
|
+
{
|
|
704
|
+
PendingRequest[] due;
|
|
705
|
+
var now = MonotonicNowMs();
|
|
706
|
+
lock (requestCacheSync)
|
|
707
|
+
{
|
|
708
|
+
due = new List<PendingRequest>(pendingSettlements.Values).FindAll(value => value.NextSettlementRetryAtMs <= now)
|
|
709
|
+
.ToArray();
|
|
710
|
+
Array.Sort(due, (left, right) => left.Order.CompareTo(right.Order));
|
|
711
|
+
if (due.Length > MaxSettlementRetriesPerTick) Array.Resize(ref due, MaxSettlementRetriesPerTick);
|
|
712
|
+
}
|
|
713
|
+
foreach (var request in due)
|
|
714
|
+
{
|
|
715
|
+
if (TrySettleRuntimeRequest(request)) continue;
|
|
716
|
+
request.NextSettlementRetryAtMs = checked(MonotonicNowMs() + SettlementRetryDelayMs);
|
|
717
|
+
if (request.SettlementFailureLogged) continue;
|
|
718
|
+
request.SettlementFailureLogged = true;
|
|
719
|
+
Debug.LogWarning("UnityCli runtime admission settlement deferred requestId=" + request.RequestId);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
internal void RetryPendingRuntimeSettlementsForTests()
|
|
724
|
+
{
|
|
725
|
+
RetryPendingRuntimeSettlements();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
internal int PendingRuntimeSettlementCountForTests()
|
|
729
|
+
{
|
|
730
|
+
lock (requestCacheSync) return pendingSettlements.Count;
|
|
381
731
|
}
|
|
382
732
|
|
|
383
733
|
private Dictionary<string, object> HealthData()
|
|
@@ -432,8 +782,8 @@ namespace UnityCli
|
|
|
432
782
|
finally { try { context.Response.Close(); } catch { } }
|
|
433
783
|
}
|
|
434
784
|
|
|
435
|
-
/// <summary>严格校验BridgeRequest v2
|
|
436
|
-
private static long
|
|
785
|
+
/// <summary>严格校验BridgeRequest v2并解析deadline;freshness只在cache miss的publication锁内裁决。</summary>
|
|
786
|
+
private static long ValidateRequestStructure(Dictionary<string, object> request)
|
|
437
787
|
{
|
|
438
788
|
if (!HasOnlyKeys(request, "protocolVersion", "instanceId", "command", "requestId", "input", "execution")) throw new FormatException("Runtime request contains an unknown field.");
|
|
439
789
|
if (ReadLong(request, "protocolVersion") != ProtocolVersion) throw new FormatException("Runtime request protocolVersion must be 2.");
|
|
@@ -449,9 +799,7 @@ namespace UnityCli
|
|
|
449
799
|
var deadlineText = RequiredString(execution, "absoluteDeadlineUtc", 128);
|
|
450
800
|
DateTimeOffset deadline;
|
|
451
801
|
if (!DateTimeOffset.TryParse(deadlineText, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out deadline)) throw new FormatException("Runtime request execution.absoluteDeadlineUtc is invalid.");
|
|
452
|
-
|
|
453
|
-
if (deadlineMs <= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()) throw new FormatException("Runtime request execution.absoluteDeadlineUtc must be in the future.");
|
|
454
|
-
return deadlineMs;
|
|
802
|
+
return deadline.ToUnixTimeMilliseconds();
|
|
455
803
|
}
|
|
456
804
|
|
|
457
805
|
/// <summary>health与execute都必须绑定同一target/session;execute额外冻结listener generation。</summary>
|
|
@@ -568,13 +916,296 @@ namespace UnityCli
|
|
|
568
916
|
private static Dictionary<string, object> ReadObject(Dictionary<string, object> value, string key) { object result; return value != null && value.TryGetValue(key, out result) ? result as Dictionary<string, object> : null; }
|
|
569
917
|
private static bool IsHex(string value, int length) { if (string.IsNullOrEmpty(value) || value.Length != length) return false; foreach (var item in value) if (!Uri.IsHexDigit(item)) return false; return true; }
|
|
570
918
|
private static bool HasOnlyKeys(Dictionary<string, object> value, params string[] allowed) { if (value == null) return false; var names = new HashSet<string>(allowed, StringComparer.Ordinal); foreach (var key in value.Keys) if (!names.Contains(key)) return false; return true; }
|
|
571
|
-
/// <summary>缓存固定为128项;只淘汰最旧terminal,进行中的mutation不会失去去重身份。</summary>
|
|
572
|
-
private bool ReserveRequestCacheSlot() { if (requestCache.Count < MaxQueueItems) return true; PendingRequest oldest = null; string oldestId = null; foreach (var pair in requestCache) if (pair.Value.Completion.Task.IsCompleted && (oldest == null || pair.Value.Order < oldest.Order)) { oldest = pair.Value; oldestId = pair.Key; } if (oldestId == null) return false; requestCache.Remove(oldestId); return true; }
|
|
573
919
|
private static string Sha256(string value) { using (var sha = SHA256.Create()) { var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(value)); var result = new StringBuilder(bytes.Length * 2); foreach (var item in bytes) result.Append(item.ToString("x2")); return result.ToString(); } }
|
|
574
920
|
private static string ProcessStartedAt(Process process) { return process.StartTime.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture); }
|
|
575
921
|
private static string RuntimeBuildGuid() { var property = typeof(Application).GetProperty("buildGUID", BindingFlags.Static | BindingFlags.Public); return property == null ? string.Empty : property.GetValue(null, null) as string ?? string.Empty; }
|
|
576
922
|
|
|
577
|
-
|
|
923
|
+
internal void InjectUtcNowProviderForTests(Func<long> provider)
|
|
924
|
+
{
|
|
925
|
+
if (provider == null) throw new ArgumentNullException("provider");
|
|
926
|
+
utcNowMsProvider = provider;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
internal void InjectMonotonicNowProviderForTests(Func<long> provider)
|
|
930
|
+
{
|
|
931
|
+
if (provider == null) throw new ArgumentNullException("provider");
|
|
932
|
+
monotonicNowMsProvider = provider;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
internal void ResetClockProvidersForTests()
|
|
936
|
+
{
|
|
937
|
+
utcNowMsProvider = () => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
938
|
+
monotonicNowMsProvider = DefaultMonotonicMilliseconds;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
private long UtcNowMs()
|
|
942
|
+
{
|
|
943
|
+
return utcNowMsProvider();
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
private long MonotonicNowMs()
|
|
947
|
+
{
|
|
948
|
+
return monotonicNowMsProvider();
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private static long DefaultMonotonicMilliseconds()
|
|
952
|
+
{
|
|
953
|
+
return checked((long)(System.Diagnostics.Stopwatch.GetTimestamp() * (1000d / System.Diagnostics.Stopwatch.Frequency)));
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
internal enum RuntimePublicationOutcome { Published, Existing, Stopping, CacheFull, QueueFull, Rejected }
|
|
957
|
+
|
|
958
|
+
private sealed class RuntimeOneShotTestHooks
|
|
959
|
+
{
|
|
960
|
+
private readonly object sync = new object();
|
|
961
|
+
private readonly Dictionary<string, Action> expected = new Dictionary<string, Action>(StringComparer.Ordinal);
|
|
962
|
+
|
|
963
|
+
internal void ExpectOnce(string name, Action action)
|
|
964
|
+
{
|
|
965
|
+
if (string.IsNullOrWhiteSpace(name) || action == null) throw new ArgumentException("Runtime one-shot test hook is invalid.");
|
|
966
|
+
lock (sync)
|
|
967
|
+
{
|
|
968
|
+
if (expected.ContainsKey(name)) throw new InvalidOperationException("Runtime one-shot test hook is already registered: " + name);
|
|
969
|
+
expected.Add(name, action);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
internal void Hit(string name)
|
|
974
|
+
{
|
|
975
|
+
Action action;
|
|
976
|
+
lock (sync)
|
|
977
|
+
{
|
|
978
|
+
if (!expected.TryGetValue(name, out action)) return;
|
|
979
|
+
expected.Remove(name);
|
|
980
|
+
}
|
|
981
|
+
action();
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
internal void AssertDrained()
|
|
985
|
+
{
|
|
986
|
+
lock (sync)
|
|
987
|
+
{
|
|
988
|
+
if (expected.Count != 0)
|
|
989
|
+
throw new InvalidOperationException("Runtime one-shot test hooks were not consumed: " + string.Join(",", expected.Keys));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
internal sealed class RuntimeWaitDependencies
|
|
995
|
+
{
|
|
996
|
+
private readonly Func<long> utcNowMs;
|
|
997
|
+
private readonly Func<Task<string>, int, bool> waitForCompletion;
|
|
998
|
+
private readonly Func<bool> isStopping;
|
|
999
|
+
|
|
1000
|
+
internal RuntimeWaitDependencies(Func<long> utcNowMs, Func<Task<string>, int, bool> waitForCompletion, Func<bool> isStopping)
|
|
1001
|
+
{
|
|
1002
|
+
if (utcNowMs == null || waitForCompletion == null || isStopping == null)
|
|
1003
|
+
throw new ArgumentNullException("Runtime wait dependencies must be explicit.");
|
|
1004
|
+
this.utcNowMs = utcNowMs;
|
|
1005
|
+
this.waitForCompletion = waitForCompletion;
|
|
1006
|
+
this.isStopping = isStopping;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
internal long UtcNowMs() { return utcNowMs(); }
|
|
1010
|
+
internal bool WaitForCompletion(Task<string> completion, int timeoutMs) { return waitForCompletion(completion, timeoutMs); }
|
|
1011
|
+
internal bool IsStopping() { return isStopping(); }
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
internal sealed class RuntimeAdmissionInput
|
|
1015
|
+
{
|
|
1016
|
+
internal string RootId;
|
|
1017
|
+
internal string Fingerprint;
|
|
1018
|
+
internal string Token;
|
|
1019
|
+
internal string DeadlineUtc;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
internal sealed class RuntimePublicationResult
|
|
1023
|
+
{
|
|
1024
|
+
internal RuntimePublicationOutcome Outcome;
|
|
1025
|
+
internal PendingRequest Work;
|
|
1026
|
+
internal string Error;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/// <summary>exact attach、admission consume、cache reservation与pending publication共享唯一生产锁边界。</summary>
|
|
1030
|
+
internal RuntimePublicationResult PublishRuntimeRequest(Dictionary<string, object> request, long absoluteDeadlineMs, string requestDigest, RuntimeAdmissionInput admission)
|
|
1031
|
+
{
|
|
1032
|
+
if (request == null) throw new ArgumentNullException("request");
|
|
1033
|
+
var requestId = ReadString(request, "requestId") ?? string.Empty;
|
|
1034
|
+
lock (requestCacheSync)
|
|
1035
|
+
{
|
|
1036
|
+
if (stopping.IsCancellationRequested)
|
|
1037
|
+
return Publication(RuntimePublicationOutcome.Stopping, null, Error(requestId, "E_RUNTIME_SESSION_STALE", "The development Player session is stopping.", new[] { "Launch a new development Player and submit a new request." }));
|
|
1038
|
+
PendingRequest existing;
|
|
1039
|
+
if (pendingSettlements.TryGetValue(requestId, out existing))
|
|
1040
|
+
{
|
|
1041
|
+
if (!string.Equals(existing.RequestDigest, requestDigest, StringComparison.Ordinal))
|
|
1042
|
+
return Publication(RuntimePublicationOutcome.Rejected, null, Error(requestId, "E_REQUEST_REPLAY_MISMATCH", "requestId was reused with different immutable request bytes.", new[] { "Submit a new requestId for different Runtime Tool input." }));
|
|
1043
|
+
return Publication(RuntimePublicationOutcome.Existing, existing, null);
|
|
1044
|
+
}
|
|
1045
|
+
if (requestCache.TryGet(requestId, out existing))
|
|
1046
|
+
{
|
|
1047
|
+
if (!string.Equals(existing.RequestDigest, requestDigest, StringComparison.Ordinal))
|
|
1048
|
+
return Publication(RuntimePublicationOutcome.Rejected, null, Error(requestId, "E_REQUEST_REPLAY_MISMATCH", "requestId was reused with different immutable request bytes.", new[] { "Submit a new requestId for different Runtime Tool input." }));
|
|
1049
|
+
return Publication(RuntimePublicationOutcome.Existing, existing, null);
|
|
1050
|
+
}
|
|
1051
|
+
if (absoluteDeadlineMs <= UtcNowMs())
|
|
1052
|
+
return Publication(RuntimePublicationOutcome.Rejected, null, Error(requestId, "E_SCHEMA_INVALID", "Runtime request execution.absoluteDeadlineUtc must be in the future for a cache miss.", new[] { "Submit a new request with a future immutable deadline." }));
|
|
1053
|
+
if (pendingSettlements.Count >= MaxRequestCacheItems)
|
|
1054
|
+
return Publication(RuntimePublicationOutcome.QueueFull, null, Error(requestId, "E_RUNTIME_QUEUE_FULL", "Runtime admission settlement capacity is full.", new[] { "Wait for exact Runtime admission cleanup before retrying." }, true, SettlementRetryDelayMs));
|
|
1055
|
+
var pool = admissionPool;
|
|
1056
|
+
UnityCliAdmissionPool.ConsumedIdentity admissionOwner;
|
|
1057
|
+
bool newlyConsumed;
|
|
1058
|
+
var admissionError = AuthorizeRuntimeAdmission(pool, admission, request, requestDigest, out admissionOwner, out newlyConsumed);
|
|
1059
|
+
if (admissionError != null) return Publication(RuntimePublicationOutcome.Rejected, null, admissionError);
|
|
1060
|
+
PendingRequest work = null;
|
|
1061
|
+
try
|
|
1062
|
+
{
|
|
1063
|
+
work = new PendingRequest(this, request, absoluteDeadlineMs, requestDigest, ++requestOrder, pool, admissionOwner);
|
|
1064
|
+
var outcome = PublishPreparedRequestLocked(work);
|
|
1065
|
+
if (outcome == RuntimePublicationOutcome.Published) return Publication(outcome, work, null);
|
|
1066
|
+
var error = outcome == RuntimePublicationOutcome.Stopping
|
|
1067
|
+
? Error(requestId, "E_RUNTIME_SESSION_STALE", "The development Player session is stopping.", new[] { "Launch a new development Player and submit a new request." })
|
|
1068
|
+
: Error(requestId, "E_RUNTIME_QUEUE_FULL", "Runtime bridge capacity is full.", new[] { "Wait for the Player to process active requests and retry." }, true, 100);
|
|
1069
|
+
if (newlyConsumed && admissionOwner != null && pool != null && !TryCompensateRuntimeAdmission(pool, admissionOwner))
|
|
1070
|
+
{
|
|
1071
|
+
FinalizeRuntimeRequest(work, error, 0);
|
|
1072
|
+
return Publication(outcome, work, null);
|
|
1073
|
+
}
|
|
1074
|
+
return Publication(outcome, null, error);
|
|
1075
|
+
}
|
|
1076
|
+
catch
|
|
1077
|
+
{
|
|
1078
|
+
if (newlyConsumed && admissionOwner != null && pool != null && !TryCompensateRuntimeAdmission(pool, admissionOwner))
|
|
1079
|
+
{
|
|
1080
|
+
var terminal = Error(requestId, "E_RUNTIME_BRIDGE_FAILED", "Runtime queue publication failed and exact admission cleanup is pending.", new[] { "Inspect runtime.state before submitting a new request." });
|
|
1081
|
+
FinalizeRuntimeRequest(work, terminal, 0);
|
|
1082
|
+
return Publication(RuntimePublicationOutcome.Rejected, work, null);
|
|
1083
|
+
}
|
|
1084
|
+
throw;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
private bool TryCompensateRuntimeAdmission(UnityCliAdmissionPool pool, UnityCliAdmissionPool.ConsumedIdentity identity)
|
|
1090
|
+
{
|
|
1091
|
+
try
|
|
1092
|
+
{
|
|
1093
|
+
HitRuntimeTestBoundary("before-runtime-compensation");
|
|
1094
|
+
return pool.CompensateAuthorization(identity);
|
|
1095
|
+
}
|
|
1096
|
+
catch { return false; }
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
private RuntimePublicationOutcome PublishPreparedRequestLocked(PendingRequest work)
|
|
1100
|
+
{
|
|
1101
|
+
if (stopping.IsCancellationRequested) return RuntimePublicationOutcome.Stopping;
|
|
1102
|
+
var reservation = requestCache.Plan(work.RequestId, work.RequestDigest, entry => entry.Completion.Task.IsCompleted, entry => entry.Order);
|
|
1103
|
+
if (reservation == null) return RuntimePublicationOutcome.CacheFull;
|
|
1104
|
+
if (pending.Count >= MaxQueueItems) return RuntimePublicationOutcome.QueueFull;
|
|
1105
|
+
HitRuntimeTestBoundary("before-runtime-publish");
|
|
1106
|
+
return requestCache.Publish(reservation, work, value => { pending.Enqueue(value); return true; })
|
|
1107
|
+
? RuntimePublicationOutcome.Published
|
|
1108
|
+
: RuntimePublicationOutcome.QueueFull;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
private static RuntimePublicationResult Publication(RuntimePublicationOutcome outcome, PendingRequest work, string error)
|
|
1112
|
+
{
|
|
1113
|
+
return new RuntimePublicationResult { Outcome = outcome, Work = work, Error = error };
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/// <summary>dequeue与pending→running ownership共享shutdown使用的requestCacheSync。</summary>
|
|
1117
|
+
private bool TryTakeNextRuntimeRequest(out PendingRequest request)
|
|
1118
|
+
{
|
|
1119
|
+
lock (requestCacheSync)
|
|
1120
|
+
{
|
|
1121
|
+
request = null;
|
|
1122
|
+
if (stopping.IsCancellationRequested) return false;
|
|
1123
|
+
while (pending.TryDequeue(out request))
|
|
1124
|
+
if (Interlocked.CompareExchange(ref request.ExecutionState, 1, 0) == 0) return true;
|
|
1125
|
+
request = null;
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
internal void StopRuntimeRequestsLocked()
|
|
1131
|
+
{
|
|
1132
|
+
lock (requestCacheSync)
|
|
1133
|
+
{
|
|
1134
|
+
try { HitRuntimeTestBoundary("before-runtime-stop"); } catch { }
|
|
1135
|
+
try { stopping.Cancel(); } catch { }
|
|
1136
|
+
var entries = new Dictionary<string, PendingRequest>(StringComparer.Ordinal);
|
|
1137
|
+
foreach (var entry in requestCache.Values) entries[entry.RequestId] = entry;
|
|
1138
|
+
foreach (var entry in pendingSettlements.Values) entries[entry.RequestId] = entry;
|
|
1139
|
+
foreach (var entry in entries.Values)
|
|
1140
|
+
{
|
|
1141
|
+
var running = Volatile.Read(ref entry.ExecutionState) == 1;
|
|
1142
|
+
var terminal = running
|
|
1143
|
+
? Error(entry.RequestId, "E_EXECUTION_TIMEOUT", "The development Player session ended after Runtime effect ownership began; outcome is unknown.", new[] { "Inspect the affected runtime state before submitting a new request." })
|
|
1144
|
+
: Error(entry.RequestId, "E_RUNTIME_SESSION_STALE", "The development Player session ended before the request completed.", new[] { "Launch a new development Player and submit a new request." });
|
|
1145
|
+
FinalizeRuntimeRequestForShutdown(entry, terminal);
|
|
1146
|
+
}
|
|
1147
|
+
PendingRequest ignored;
|
|
1148
|
+
while (pending.TryDequeue(out ignored)) { }
|
|
1149
|
+
requestCache.Clear();
|
|
1150
|
+
var unresolved = new Dictionary<string, PendingRequest>(StringComparer.Ordinal);
|
|
1151
|
+
foreach (var entry in entries.Values)
|
|
1152
|
+
if (Volatile.Read(ref entry.ExecutionState) != 3) unresolved[entry.RequestId] = entry;
|
|
1153
|
+
pendingSettlements.Clear();
|
|
1154
|
+
foreach (var entry in unresolved.Values) pendingSettlements[entry.RequestId] = entry;
|
|
1155
|
+
if (unresolved.Count == 0)
|
|
1156
|
+
{
|
|
1157
|
+
var pool = admissionPool;
|
|
1158
|
+
admissionPool = null;
|
|
1159
|
+
if (pool != null) pool.Reset();
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
private bool FinalizeRuntimeRequestForShutdown(PendingRequest request, string terminal)
|
|
1165
|
+
{
|
|
1166
|
+
if (request == null || request.Owner != this) return false;
|
|
1167
|
+
while (true)
|
|
1168
|
+
{
|
|
1169
|
+
var state = Volatile.Read(ref request.ExecutionState);
|
|
1170
|
+
if (state == 3) return true;
|
|
1171
|
+
if (state == 4) return false;
|
|
1172
|
+
if (state == 0 || state == 1)
|
|
1173
|
+
{
|
|
1174
|
+
if (Interlocked.CompareExchange(ref request.ExecutionState, 2, state) != state) continue;
|
|
1175
|
+
request.Terminal = terminal;
|
|
1176
|
+
}
|
|
1177
|
+
else if (state != 2) return false;
|
|
1178
|
+
return TrySettleRuntimeRequest(request, true);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
internal sealed class PendingRequest
|
|
1183
|
+
{
|
|
1184
|
+
internal PendingRequest(UnityCliRuntimeBridge owner, Dictionary<string, object> request, long absoluteDeadlineMs, string requestDigest, long order, UnityCliAdmissionPool admissionPool, UnityCliAdmissionPool.ConsumedIdentity admissionOwner)
|
|
1185
|
+
{
|
|
1186
|
+
Owner = owner;
|
|
1187
|
+
Request = request;
|
|
1188
|
+
Completion = new TaskCompletionSource<string>();
|
|
1189
|
+
AbsoluteDeadlineMs = absoluteDeadlineMs;
|
|
1190
|
+
RequestDigest = requestDigest;
|
|
1191
|
+
Order = order;
|
|
1192
|
+
AdmissionPool = admissionPool;
|
|
1193
|
+
AdmissionOwner = admissionOwner;
|
|
1194
|
+
}
|
|
1195
|
+
internal UnityCliRuntimeBridge Owner;
|
|
1196
|
+
internal Dictionary<string, object> Request;
|
|
1197
|
+
internal string RequestId { get { return ReadString(Request, "requestId") ?? string.Empty; } }
|
|
1198
|
+
internal TaskCompletionSource<string> Completion;
|
|
1199
|
+
internal long AbsoluteDeadlineMs;
|
|
1200
|
+
internal string RequestDigest;
|
|
1201
|
+
internal UnityCliAdmissionPool AdmissionPool;
|
|
1202
|
+
internal UnityCliAdmissionPool.ConsumedIdentity AdmissionOwner;
|
|
1203
|
+
internal long Order;
|
|
1204
|
+
internal int ExecutionState;
|
|
1205
|
+
internal string Terminal;
|
|
1206
|
+
internal long NextSettlementRetryAtMs;
|
|
1207
|
+
internal bool SettlementFailureLogged;
|
|
1208
|
+
}
|
|
578
1209
|
private sealed class RuntimeConfig { internal string InstanceId; internal string CanonicalProjectPath; internal string DisplayProjectPath; internal string BuildId; internal string BuildStamp; internal int TaskPoolMaxActive; internal int TaskPoolReopenAt; }
|
|
579
1210
|
}
|
|
580
1211
|
}
|