@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.
Files changed (420) hide show
  1. package/README.md +5 -5
  2. package/contracts/command-contract.json +109 -217
  3. package/contracts/package-files.json +374 -294
  4. package/contracts/stable-session/direct-call-semantics.json +8 -2
  5. package/contracts/tool-completion-contract.json +148 -672
  6. package/dist/bridge/admission.d.ts +17 -1
  7. package/dist/bridge/admission.js +2 -0
  8. package/dist/bridge/admission.js.map +1 -1
  9. package/dist/bridge/domains/asset.js +3 -1
  10. package/dist/bridge/domains/asset.js.map +1 -1
  11. package/dist/bridge/domains/camera.js +39 -7
  12. package/dist/bridge/domains/camera.js.map +1 -1
  13. package/dist/bridge/domains/console.js +2 -1
  14. package/dist/bridge/domains/console.js.map +1 -1
  15. package/dist/bridge/domains/doctor.d.ts +3 -0
  16. package/dist/bridge/domains/doctor.js +8 -0
  17. package/dist/bridge/domains/doctor.js.map +1 -0
  18. package/dist/bridge/domains/editor.js +0 -4
  19. package/dist/bridge/domains/editor.js.map +1 -1
  20. package/dist/bridge/domains/game-object.js +4 -12
  21. package/dist/bridge/domains/game-object.js.map +1 -1
  22. package/dist/bridge/domains/graphics.js +1 -16
  23. package/dist/bridge/domains/graphics.js.map +1 -1
  24. package/dist/bridge/domains/object.js +3 -0
  25. package/dist/bridge/domains/object.js.map +1 -1
  26. package/dist/bridge/domains/physics.js +1 -2
  27. package/dist/bridge/domains/physics.js.map +1 -1
  28. package/dist/bridge/domains/profiler.js +18 -7
  29. package/dist/bridge/domains/profiler.js.map +1 -1
  30. package/dist/bridge/domains/project-tool.js +4 -0
  31. package/dist/bridge/domains/project-tool.js.map +1 -1
  32. package/dist/bridge/domains/reflection.js +10 -2
  33. package/dist/bridge/domains/reflection.js.map +1 -1
  34. package/dist/bridge/domains/runtime-tool.js +3 -0
  35. package/dist/bridge/domains/runtime-tool.js.map +1 -1
  36. package/dist/bridge/domains/scene.js +3 -8
  37. package/dist/bridge/domains/scene.js.map +1 -1
  38. package/dist/bridge/domains/screenshot.js +30 -6
  39. package/dist/bridge/domains/screenshot.js.map +1 -1
  40. package/dist/bridge/domains/ui.js +3 -1
  41. package/dist/bridge/domains/ui.js.map +1 -1
  42. package/dist/bridge/index.d.ts +5 -5
  43. package/dist/bridge/index.js +6 -9
  44. package/dist/bridge/index.js.map +1 -1
  45. package/dist/bridge/registry.js +3 -5
  46. package/dist/bridge/registry.js.map +1 -1
  47. package/dist/bridge/session-supervisor.d.ts +6 -3
  48. package/dist/bridge/session-supervisor.js +122 -34
  49. package/dist/bridge/session-supervisor.js.map +1 -1
  50. package/dist/bridge/types.d.ts +6 -5
  51. package/dist/bridge/types.js +16 -2
  52. package/dist/bridge/types.js.map +1 -1
  53. package/dist/catalog-protocol.d.ts +64 -0
  54. package/dist/catalog-protocol.js +460 -0
  55. package/dist/catalog-protocol.js.map +1 -0
  56. package/dist/cli-commands/command.js +84 -61
  57. package/dist/cli-commands/command.js.map +1 -1
  58. package/dist/cli-commands/extensions.js +70 -50
  59. package/dist/cli-commands/extensions.js.map +1 -1
  60. package/dist/cli-commands/instances.js +1 -1
  61. package/dist/cli-commands/instances.js.map +1 -1
  62. package/dist/cli-process-owners.d.ts +6 -0
  63. package/dist/cli-process-owners.js +27 -0
  64. package/dist/cli-process-owners.js.map +1 -0
  65. package/dist/cli.d.ts +1 -1
  66. package/dist/cli.js +20 -5
  67. package/dist/cli.js.map +1 -1
  68. package/dist/contracts/command-contract.js +14 -4
  69. package/dist/contracts/command-contract.js.map +1 -1
  70. package/dist/doctor.d.ts +4 -6
  71. package/dist/doctor.js +429 -398
  72. package/dist/doctor.js.map +1 -1
  73. package/dist/editor-blocker-observer.d.ts +2 -1
  74. package/dist/editor-blocker-observer.js +40 -13
  75. package/dist/editor-blocker-observer.js.map +1 -1
  76. package/dist/extension-availability.d.ts +27 -9
  77. package/dist/extension-availability.js +96 -56
  78. package/dist/extension-availability.js.map +1 -1
  79. package/dist/extensions/descriptor-loader.d.ts +2 -2
  80. package/dist/extensions/descriptor-loader.js +3 -4
  81. package/dist/extensions/descriptor-loader.js.map +1 -1
  82. package/dist/extensions/index.d.ts +1 -1
  83. package/dist/extensions/index.js +1 -1
  84. package/dist/extensions/index.js.map +1 -1
  85. package/dist/extensions/installer.d.ts +24 -7
  86. package/dist/extensions/installer.js +142 -66
  87. package/dist/extensions/installer.js.map +1 -1
  88. package/dist/extensions/registry.js +3 -5
  89. package/dist/extensions/registry.js.map +1 -1
  90. package/dist/extensions/state-contract.d.ts +11 -0
  91. package/dist/extensions/state-contract.js +50 -0
  92. package/dist/extensions/state-contract.js.map +1 -0
  93. package/dist/instances.d.ts +13 -1
  94. package/dist/instances.js +26 -0
  95. package/dist/instances.js.map +1 -1
  96. package/dist/package-manager.d.ts +0 -24
  97. package/dist/package-manager.js +5 -58
  98. package/dist/package-manager.js.map +1 -1
  99. package/dist/project-tool-discovery.d.ts +5 -2
  100. package/dist/project-tool-discovery.js +22 -18
  101. package/dist/project-tool-discovery.js.map +1 -1
  102. package/dist/runtime-code.js +1 -1
  103. package/dist/runtime-code.js.map +1 -1
  104. package/dist/tool-name.d.ts +4 -0
  105. package/dist/tool-name.js +15 -0
  106. package/dist/tool-name.js.map +1 -0
  107. package/dist/tools/.generated-completion-registry.d.ts +2 -0
  108. package/dist/tools/.generated-completion-registry.js +763 -325
  109. package/dist/tools/.generated-completion-registry.js.map +1 -1
  110. package/dist/tools/.generated-registry.d.ts +6 -11
  111. package/dist/tools/.generated-registry.js +6 -11
  112. package/dist/tools/.generated-registry.js.map +1 -1
  113. package/dist/tools/.generated-schemas.js +11 -16
  114. package/dist/tools/.generated-schemas.js.map +1 -1
  115. package/dist/tools/asset/refresh-status.d.ts +3 -0
  116. package/dist/tools/asset/refresh-status.js +12 -0
  117. package/dist/tools/asset/refresh-status.js.map +1 -0
  118. package/dist/tools/asset/refresh.js +7 -3
  119. package/dist/tools/asset/refresh.js.map +1 -1
  120. package/dist/tools/build/player-status.d.ts +3 -0
  121. package/dist/tools/build/player-status.js +12 -0
  122. package/dist/tools/build/player-status.js.map +1 -0
  123. package/dist/tools/build/run.js +1 -1
  124. package/dist/tools/build/run.js.map +1 -1
  125. package/dist/tools/catalog-control.d.ts +22 -0
  126. package/dist/tools/catalog-control.js +335 -0
  127. package/dist/tools/catalog-control.js.map +1 -0
  128. package/dist/tools/catalog.d.ts +7 -9
  129. package/dist/tools/catalog.js +66 -45
  130. package/dist/tools/catalog.js.map +1 -1
  131. package/dist/tools/commands.d.ts +1 -1
  132. package/dist/tools/commands.js +44 -11
  133. package/dist/tools/commands.js.map +1 -1
  134. package/dist/tools/define.js +3 -2
  135. package/dist/tools/define.js.map +1 -1
  136. package/dist/tools/dispatcher.d.ts +53 -6
  137. package/dist/tools/dispatcher.js +277 -108
  138. package/dist/tools/dispatcher.js.map +1 -1
  139. package/dist/tools/editor/compile.js +1 -1
  140. package/dist/tools/editor/compile.js.map +1 -1
  141. package/dist/tools/editor/play-mode-status.d.ts +3 -0
  142. package/dist/tools/editor/play-mode-status.js +12 -0
  143. package/dist/tools/editor/play-mode-status.js.map +1 -0
  144. package/dist/tools/editor/readiness.d.ts +0 -1
  145. package/dist/tools/editor/readiness.js +0 -1
  146. package/dist/tools/editor/readiness.js.map +1 -1
  147. package/dist/tools/editor/refresh.d.ts +7 -3
  148. package/dist/tools/editor/refresh.js +20 -15
  149. package/dist/tools/editor/refresh.js.map +1 -1
  150. package/dist/tools/editor/state.d.ts +2 -2
  151. package/dist/tools/editor/state.js +13 -12
  152. package/dist/tools/editor/state.js.map +1 -1
  153. package/dist/tools/metadata/derived.js +1 -7
  154. package/dist/tools/metadata/derived.js.map +1 -1
  155. package/dist/tools/metadata/status.js +1 -12
  156. package/dist/tools/metadata/status.js.map +1 -1
  157. package/dist/tools/metadata/types.d.ts +0 -2
  158. package/dist/tools/navmesh/live.js +1 -1
  159. package/dist/tools/navmesh/live.js.map +1 -1
  160. package/dist/tools/occlusion/live.js +1 -1
  161. package/dist/tools/occlusion/live.js.map +1 -1
  162. package/dist/tools/package/deploy.js +2 -2
  163. package/dist/tools/package/deploy.js.map +1 -1
  164. package/dist/tools/package/embed.js +1 -1
  165. package/dist/tools/package/embed.js.map +1 -1
  166. package/dist/tools/package/install-status.d.ts +3 -0
  167. package/dist/tools/package/install-status.js +12 -0
  168. package/dist/tools/package/install-status.js.map +1 -0
  169. package/dist/tools/package/install.js +3 -3
  170. package/dist/tools/package/install.js.map +1 -1
  171. package/dist/tools/package/live-resolve.js +45 -12
  172. package/dist/tools/package/live-resolve.js.map +1 -1
  173. package/dist/tools/package/registry-clear.js.map +1 -1
  174. package/dist/tools/package/registry-set.js.map +1 -1
  175. package/dist/tools/package/remove-status.d.ts +3 -0
  176. package/dist/tools/package/remove-status.js +12 -0
  177. package/dist/tools/package/remove-status.js.map +1 -0
  178. package/dist/tools/package/remove.js +3 -3
  179. package/dist/tools/package/remove.js.map +1 -1
  180. package/dist/tools/package/resolve.js +3 -3
  181. package/dist/tools/package/resolve.js.map +1 -1
  182. package/dist/tools/package/restore.js +2 -2
  183. package/dist/tools/package/restore.js.map +1 -1
  184. package/dist/tools/package/scoped-registry-add.js.map +1 -1
  185. package/dist/tools/status/support.d.ts +13 -0
  186. package/dist/tools/status/support.js +28 -0
  187. package/dist/tools/status/support.js.map +1 -0
  188. package/dist/tools/test/cancel.js +2 -2
  189. package/dist/tools/test/cancel.js.map +1 -1
  190. package/dist/tools/test/run-status.d.ts +3 -0
  191. package/dist/tools/test/run-status.js +12 -0
  192. package/dist/tools/test/run-status.js.map +1 -0
  193. package/dist/tools/test/run.js +7 -4
  194. package/dist/tools/test/run.js.map +1 -1
  195. package/dist/tools/tools/group-summaries.js +10 -14
  196. package/dist/tools/tools/group-summaries.js.map +1 -1
  197. package/docs/architecture.md +4 -4
  198. package/docs/availability-differences.md +4 -4
  199. package/docs/command-execution.md +4 -3
  200. package/docs/error-codes.md +13 -5
  201. package/docs/extensions.md +1 -7
  202. package/docs/risk-and-side-effects.md +4 -3
  203. package/docs/safety-boundaries.md +3 -1
  204. package/docs/target-and-instance.md +1 -1
  205. package/docs/tool-discovery.md +17 -10
  206. package/docs/tool-execution.md +18 -7
  207. package/extensions/cinemachine/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  208. package/extensions/hdrp/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  209. package/extensions/probuilder/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  210. package/extensions/urp/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  211. package/extensions/vfx-graph/unitypkg/Editor/UnityCliExtensionAssembly.cs +3 -0
  212. package/package.json +2 -4
  213. package/skills/unitycli/SKILL.md +11 -2
  214. package/skills/unitycli/manifest.json +1 -1
  215. package/skills/unitycli/references/operation-protocol.md +3 -3
  216. package/unitypkg/Editor/AssemblyInfo.cs +5 -0
  217. package/unitypkg/Editor/Bridge/BridgeCommandRegistry.cs +49 -14
  218. package/unitypkg/Editor/Bridge/BridgeHttpServer.cs +9 -35
  219. package/unitypkg/Editor/Bridge/BridgeQueueState.cs +672 -0
  220. package/unitypkg/Editor/Bridge/BridgeWorkQueue.cs +970 -120
  221. package/unitypkg/Editor/BridgeStartupStateMachine.cs +59 -1
  222. package/unitypkg/Editor/Commands/ProjectToolCommands.cs +4 -0
  223. package/unitypkg/Editor/Commands/RuntimeToolCommands.cs +3 -0
  224. package/unitypkg/Editor/Generated/UnityCliToolCompletionRegistry.g.cs +379 -14
  225. package/unitypkg/Editor/Infrastructure/CodeExecSupport.cs +28 -13
  226. package/unitypkg/Editor/Infrastructure/RegistryOwnershipResolver.cs +132 -105
  227. package/unitypkg/Editor/Infrastructure/UnityCliActiveOperation.cs +138 -29
  228. package/unitypkg/Editor/Infrastructure/UnityCliAdmission.cs +1856 -237
  229. package/unitypkg/Editor/Infrastructure/UnityCliDoctorDiagnostics.cs +220 -0
  230. package/unitypkg/Editor/Infrastructure/UnityCliDurableStatusCoordinator.cs +118 -0
  231. package/unitypkg/Editor/Infrastructure/UnityCliEditorStateSnapshot.cs +2 -1
  232. package/unitypkg/Editor/Infrastructure/UnityCliHierarchyPager.cs +203 -0
  233. package/unitypkg/Editor/Infrastructure/UnityCliInstanceCleanup.cs +2 -1
  234. package/unitypkg/Editor/Infrastructure/UnityCliInstanceRegistry.cs +201 -63
  235. package/unitypkg/Editor/Infrastructure/UnityCliJson.cs +2 -3
  236. package/unitypkg/Editor/Infrastructure/UnityCliLiveBuildCoordinator.cs +289 -46
  237. package/unitypkg/Editor/Infrastructure/UnityCliLiveRefreshCoordinator.cs +87 -53
  238. package/unitypkg/Editor/Infrastructure/UnityCliNavMeshBakeCoordinator.cs +4 -4
  239. package/unitypkg/Editor/Infrastructure/UnityCliObjectRef.cs +128 -25
  240. package/unitypkg/Editor/Infrastructure/UnityCliObjectSerializer.cs +435 -327
  241. package/unitypkg/Editor/Infrastructure/UnityCliOcclusionBakeCoordinator.cs +4 -4
  242. package/unitypkg/Editor/Infrastructure/UnityCliOperationCoordinator.cs +351 -53
  243. package/unitypkg/Editor/Infrastructure/UnityCliPackageResolveCoordinator.cs +118 -18
  244. package/unitypkg/Editor/Infrastructure/UnityCliPackageResolveState.cs +20 -1
  245. package/unitypkg/Editor/Infrastructure/UnityCliReadGeneration.cs +97 -0
  246. package/unitypkg/Editor/Infrastructure/UnityCliReflection.cs +644 -133
  247. package/unitypkg/Editor/Infrastructure/UnityCliReloadOperation.cs +128 -41
  248. package/unitypkg/Editor/Infrastructure/UnityCliRuntimeExecutor.cs +14 -84
  249. package/unitypkg/Editor/Infrastructure/UnityCliSerializer.cs +220 -0
  250. package/unitypkg/Editor/Infrastructure/UnityCliSessionMutationLease.cs +69 -29
  251. package/unitypkg/Editor/Infrastructure/UnityCliTestRunCoordinator.cs +440 -54
  252. package/unitypkg/Editor/Infrastructure/UnityCliWaiter.cs +128 -35
  253. package/unitypkg/Editor/ProjectTools/IUnityCliTool.cs +9 -1
  254. package/unitypkg/Editor/ProjectTools/OfficialAttributeToolRegistry.cs +32 -31
  255. package/unitypkg/Editor/ProjectTools/ProjectToolRegistry.cs +607 -458
  256. package/unitypkg/Editor/ProjectTools/ProjectToolSchemaValidator.cs +35 -7
  257. package/unitypkg/Editor/ProjectTools/RegisteredTool.cs +501 -60
  258. package/unitypkg/Editor/ProjectTools/UnityCliProjectToolContext.cs +21 -0
  259. package/unitypkg/Editor/ProjectTools/UnityCliSchemaDeriver.cs +36 -7
  260. package/unitypkg/Editor/ProjectTools/UnityCliToolName.cs +30 -0
  261. package/unitypkg/Editor/Properties/AssemblyInfo.cs +3 -0
  262. package/unitypkg/Editor/Tools/asset/get-data.cs +16 -7
  263. package/unitypkg/Editor/Tools/build/BuildSceneStateTool.cs +23 -3
  264. package/unitypkg/Editor/Tools/build/BuildToolDtos.cs +2 -1
  265. package/unitypkg/Editor/Tools/build/scene/add.cs +6 -3
  266. package/unitypkg/Editor/Tools/build/scene/disable.cs +1 -1
  267. package/unitypkg/Editor/Tools/build/scene/enable.cs +1 -1
  268. package/unitypkg/Editor/Tools/build/scene/list.cs +1 -1
  269. package/unitypkg/Editor/Tools/build/scene/remove.cs +13 -3
  270. package/unitypkg/Editor/Tools/camera/CameraToolSupport.cs +38 -38
  271. package/unitypkg/Editor/Tools/camera/multiview.cs +13 -6
  272. package/unitypkg/Editor/Tools/camera/screenshot.cs +14 -7
  273. package/unitypkg/Editor/Tools/component/get.cs +16 -9
  274. package/unitypkg/Editor/Tools/component/modify.cs +6 -4
  275. package/unitypkg/Editor/Tools/console/clear.cs +7 -6
  276. package/unitypkg/Editor/Tools/console/get.cs +48 -16
  277. package/unitypkg/Editor/Tools/editor/EditorToolSupport.cs +143 -28
  278. package/unitypkg/Editor/Tools/editor/menu-items.cs +1 -1
  279. package/unitypkg/Editor/Tools/editor/play.cs +2 -11
  280. package/unitypkg/Editor/Tools/editor/selection.cs +1 -1
  281. package/unitypkg/Editor/Tools/editor/stop.cs +2 -11
  282. package/unitypkg/Editor/Tools/editor/windows.cs +1 -1
  283. package/unitypkg/Editor/Tools/go/GameObjectToolSupport.cs +54 -4
  284. package/unitypkg/Editor/Tools/go/delete.cs +1 -1
  285. package/unitypkg/Editor/Tools/go/find.cs +167 -70
  286. package/unitypkg/Editor/Tools/graphics/GraphicsToolSupport.cs +25 -0
  287. package/unitypkg/Editor/Tools/graphics/light-probe-group.cs +9 -3
  288. package/unitypkg/Editor/Tools/graphics/pipeline-info.cs +1 -1
  289. package/unitypkg/Editor/Tools/graphics/pipeline-settings.cs +1 -1
  290. package/unitypkg/Editor/Tools/graphics/quality.cs +2 -2
  291. package/unitypkg/Editor/Tools/graphics/reflection-probe.cs +3 -3
  292. package/unitypkg/Editor/Tools/graphics/stats.cs +1 -1
  293. package/unitypkg/Editor/Tools/object/ObjectToolSupport.cs +4 -4
  294. package/unitypkg/Editor/Tools/object/get-data.cs +9 -7
  295. package/unitypkg/Editor/Tools/object/modify.cs +7 -5
  296. package/unitypkg/Editor/Tools/physics/raycast.cs +24 -6
  297. package/unitypkg/Editor/Tools/prefab/stage.cs +1 -1
  298. package/unitypkg/Editor/Tools/profiler/ProfilerFrameDebuggerSupport.cs +94 -21
  299. package/unitypkg/Editor/Tools/profiler/ProfilerMemorySnapshotSupport.cs +397 -10
  300. package/unitypkg/Editor/Tools/profiler/ProfilerToolSupport.cs +18 -4
  301. package/unitypkg/Editor/Tools/profiler/frame-debugger/enable.cs +1 -1
  302. package/unitypkg/Editor/Tools/profiler/frame-debugger/events.cs +88 -87
  303. package/unitypkg/Editor/Tools/profiler/hierarchy.cs +278 -112
  304. package/unitypkg/Editor/Tools/profiler/memory-snapshot/compare.cs +9 -5
  305. package/unitypkg/Editor/Tools/profiler/memory-snapshot/list.cs +37 -35
  306. package/unitypkg/Editor/Tools/profiler/memory-snapshot/take.cs +99 -97
  307. package/unitypkg/Editor/Tools/project/info.cs +1 -1
  308. package/unitypkg/Editor/Tools/project/layers.cs +1 -1
  309. package/unitypkg/Editor/Tools/project/tags.cs +1 -1
  310. package/unitypkg/Editor/Tools/reflection/method/call.cs +9 -7
  311. package/unitypkg/Editor/Tools/reflection/method/find.cs +27 -37
  312. package/unitypkg/Editor/Tools/reflection/type/schema.cs +24 -28
  313. package/unitypkg/Editor/Tools/scene/gameobject/component.cs +8 -5
  314. package/unitypkg/Editor/Tools/scene/gameobject/components.cs +10 -7
  315. package/unitypkg/Editor/Tools/scene/gameobject-support.cs +104 -8
  316. package/unitypkg/Editor/Tools/scene/gameobject.cs +37 -16
  317. package/unitypkg/Editor/Tools/scene/hierarchy.cs +146 -142
  318. package/unitypkg/Editor/Tools/screenshot/EditorElementCaptureToolSupport.cs +77 -25
  319. package/unitypkg/Editor/Tools/screenshot/ScreenshotToolSupport.cs +315 -95
  320. package/unitypkg/Editor/Tools/screenshot/editor-element.cs +7 -5
  321. package/unitypkg/Editor/Tools/screenshot/game-view.cs +35 -21
  322. package/unitypkg/Editor/Tools/screenshot/isolated-object.cs +29 -23
  323. package/unitypkg/Editor/Tools/screenshot/orbit.cs +37 -25
  324. package/unitypkg/Editor/Tools/screenshot/scene-view.cs +25 -18
  325. package/unitypkg/Editor/Tools/screenshot/screen.cs +177 -17
  326. package/unitypkg/Editor/Tools/screenshot/surround.cs +83 -79
  327. package/unitypkg/Editor/Tools/ui/render-preview.cs +16 -11
  328. package/unitypkg/Editor/UnityCliBridge.cs +97 -34
  329. package/unitypkg/Runtime/Infrastructure/UnityCliRuntimeBridge.cs +753 -122
  330. package/unitypkg/Runtime/Properties/AssemblyInfo.cs +5 -0
  331. package/unitypkg/Runtime/UnityCli.Runtime.asmdef +1 -1
  332. package/unitypkg/Shared/AssemblyInfo.cs +4 -0
  333. package/unitypkg/Shared/UnityCliAdmissionPool.cs +265 -75
  334. package/unitypkg/Shared/UnityCliBridgeQueuePolicy.cs +410 -0
  335. package/unitypkg/Shared/UnityCliBridgeWorkItemState.cs +47 -0
  336. package/unitypkg/Shared/UnityCliExceptionProjection.cs +151 -0
  337. package/unitypkg/Shared/UnityCliQueuePublication.cs +30 -0
  338. package/unitypkg/Shared/UnityCliRequestBudget.cs +352 -0
  339. package/unitypkg/Shared/UnityCliRuntimeRequestPublication.cs +107 -0
  340. package/unitypkg/Tests/Editor/UnityCli.Editor.Tests.asmdef +2 -2
  341. package/unitypkg/Tests/Editor/UnityCliAdmissionCoordinatorIntegrationTests.cs +1329 -0
  342. package/unitypkg/Tests/Editor/UnityCliAdmissionPoolTests.cs +311 -114
  343. package/unitypkg/Tests/Editor/UnityCliAdmissionRepairStateTests.cs +94 -0
  344. package/unitypkg/Tests/Editor/UnityCliBridgeAdmissionIntegrationTests.cs +1229 -0
  345. package/unitypkg/Tests/Editor/UnityCliBridgeQueuePolicyTests.cs +412 -0
  346. package/unitypkg/Tests/Editor/UnityCliContinuationIntegrationTests.cs +292 -0
  347. package/unitypkg/Tests/Editor/UnityCliDiagnosticBoundaryTests.cs +120 -0
  348. package/unitypkg/Tests/Editor/UnityCliDurableStatusTests.cs +84 -0
  349. package/unitypkg/Tests/Editor/UnityCliExceptionProjectionTests.cs +72 -0
  350. package/unitypkg/Tests/Editor/UnityCliInstancePublicationRetryTests.cs +180 -97
  351. package/unitypkg/Tests/Editor/UnityCliMemoryReflectionIntegrationTests.cs +434 -0
  352. package/unitypkg/Tests/Editor/UnityCliRequestBudgetTests.cs +615 -0
  353. package/unitypkg/Tests/Editor/UnityCliRuntimeBridgeIntegrationTests.cs +520 -0
  354. package/unitypkg/Tests/Editor/UnityCliToolCatalogTests.cs +562 -0
  355. package/unitypkg/Tests/Runtime/UnityCli.Runtime.Tests.asmdef +23 -0
  356. package/unitypkg/Tests/Runtime/UnityCliRuntimeCatalogValidationTests.cs +29 -0
  357. package/unitypkg/package.json +1 -1
  358. package/contracts/static-capability-catalog.json +0 -48226
  359. package/dist/bridge/domains/diagnose.d.ts +0 -2
  360. package/dist/bridge/domains/diagnose.js +0 -11
  361. package/dist/bridge/domains/diagnose.js.map +0 -1
  362. package/dist/bridge/domains/extension.d.ts +0 -8
  363. package/dist/bridge/domains/extension.js +0 -20
  364. package/dist/bridge/domains/extension.js.map +0 -1
  365. package/dist/capabilities.d.ts +0 -5
  366. package/dist/capabilities.js +0 -84
  367. package/dist/capabilities.js.map +0 -1
  368. package/dist/static-capability-catalog.d.ts +0 -13
  369. package/dist/static-capability-catalog.js +0 -41
  370. package/dist/static-capability-catalog.js.map +0 -1
  371. package/dist/tools/diagnose/capabilities.d.ts +0 -13
  372. package/dist/tools/diagnose/capabilities.js +0 -34
  373. package/dist/tools/diagnose/capabilities.js.map +0 -1
  374. package/dist/tools/package/info.d.ts +0 -19
  375. package/dist/tools/package/info.js +0 -27
  376. package/dist/tools/package/info.js.map +0 -1
  377. package/dist/tools/package/list.d.ts +0 -27
  378. package/dist/tools/package/list.js +0 -21
  379. package/dist/tools/package/list.js.map +0 -1
  380. package/dist/tools/package/registry-get.d.ts +0 -10
  381. package/dist/tools/package/registry-get.js +0 -22
  382. package/dist/tools/package/registry-get.js.map +0 -1
  383. package/dist/tools/package/scoped-registry-list.d.ts +0 -14
  384. package/dist/tools/package/scoped-registry-list.js +0 -22
  385. package/dist/tools/package/scoped-registry-list.js.map +0 -1
  386. package/dist/tools/package/search.d.ts +0 -17
  387. package/dist/tools/package/search.js +0 -39
  388. package/dist/tools/package/search.js.map +0 -1
  389. package/dist/tools/package/status.d.ts +0 -21
  390. package/dist/tools/package/status.js +0 -37
  391. package/dist/tools/package/status.js.map +0 -1
  392. package/dist/tools/tools/describe.d.ts +0 -27
  393. package/dist/tools/tools/describe.js +0 -35
  394. package/dist/tools/tools/describe.js.map +0 -1
  395. package/dist/tools/tools/group-list.d.ts +0 -16
  396. package/dist/tools/tools/group-list.js +0 -38
  397. package/dist/tools/tools/group-list.js.map +0 -1
  398. package/dist/tools/tools/group.d.ts +0 -13
  399. package/dist/tools/tools/group.js +0 -34
  400. package/dist/tools/tools/group.js.map +0 -1
  401. package/dist/tools/tools/pagination.d.ts +0 -17
  402. package/dist/tools/tools/pagination.js +0 -28
  403. package/dist/tools/tools/pagination.js.map +0 -1
  404. package/dist/tools/tools/search.d.ts +0 -15
  405. package/dist/tools/tools/search.js +0 -42
  406. package/dist/tools/tools/search.js.map +0 -1
  407. package/unitypkg/Editor/Tools/diagnose/preflight.cs +0 -269
  408. package/unitypkg/Editor/Tools/editor/console.cs +0 -26
  409. package/unitypkg/Editor/Tools/extension/status.cs +0 -87
  410. package/unitypkg/Editor/Tools/go/inspect.cs +0 -49
  411. package/unitypkg/Editor/Tools/graphics/pipeline-quality.cs +0 -24
  412. package/unitypkg/Editor/Tools/graphics/probe-positions.cs +0 -58
  413. package/unitypkg/Editor/Tools/graphics/settings.cs +0 -104
  414. package/unitypkg/Editor/Tools/graphics/stats-counters.cs +0 -28
  415. package/unitypkg/Editor/Tools/graphics/stats-memory.cs +0 -21
  416. package/unitypkg/Editor/Tools/instances/list.cs +0 -71
  417. package/unitypkg/Editor/Tools/physics/raycast-all.cs +0 -67
  418. package/unitypkg/Editor/Tools/scene/build-settings.cs +0 -69
  419. package/unitypkg/Editor/Tools/scene/data.cs +0 -125
  420. 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 Dictionary<string, PendingRequest> requestCache = new Dictionary<string, PendingRequest>(StringComparer.Ordinal);
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 && pending.TryDequeue(out request)) { ExecuteOnMainThread(request); processed++; }
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
- lock (requestCacheSync)
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
- PendingRequest work = null;
189
- string immediate = null;
190
- var enqueue = false;
191
- // 同字节replay可读取已验证请求的terminal;只有首次request才要求deadline仍在未来。
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
- var result = admissionPool.Acquire(requestId, acquireFingerprint, listenerId, 1L);
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=" + admissionPool.Capture().Active.ToString(CultureInfo.InvariantCulture));
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
- var revoked = admissionPool.Revoke(requestId, fingerprint, token);
299
- if (revoked) Debug.Log("UnityCli admission_revoked requestId=" + requestId + " active=" + admissionPool.Capture().Active.ToString(CultureInfo.InvariantCulture));
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(HttpListenerRequest httpRequest, Dictionary<string, object> request, out UnityCliAdmissionPool.ConsumedIdentity admissionOwner)
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
- var rootId = httpRequest.Headers["x-unity-cli-admission-root-id"];
312
- var fingerprint = httpRequest.Headers["x-unity-cli-admission-fingerprint"];
313
- var token = httpRequest.Headers["x-unity-cli-admission-token"];
314
- var deadlineText = httpRequest.Headers["x-unity-cli-admission-deadline-utc"];
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 (string.Equals(command, "runtime-tool.list", StringComparison.Ordinal)) { rootId = null; return null; }
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(rootId) || string.IsNullOrEmpty(fingerprint) || string.IsNullOrEmpty(token) || string.IsNullOrEmpty(deadlineText))
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(rootId, requestId, StringComparison.Ordinal))
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(deadlineText, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out deadline))
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
- var outcome = admissionPool.Authorize(rootId, fingerprint, token, listenerId, 1L, deadline.ToUnixTimeMilliseconds());
331
- if (outcome != UnityCliAdmissionPool.AuthorizeOutcome.Authorized)
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
- if (!admissionPool.TryGetConsumedIdentity(rootId, out admissionOwner))
334
- return Error(requestId, "E_EXECUTION_TIMEOUT", "Runtime admission owner identity could not be frozen after consumption.", new[] { "Inspect the Runtime session before submitting another root." });
409
+ }
410
+ admissionOwner = result.Identity;
411
+ newlyConsumed = result.NewlyConsumed;
335
412
  return null;
336
413
  }
337
414
 
338
- /// <summary>所有Unity API只在Update主线程处理;descriptor由TS catalog唯一持有,Player只返回空执行器列表。</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 (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() >= deadline) { pendingRequest.Completion.TrySetResult(Error(requestId, "E_EXECUTION_TIMEOUT", "Runtime request deadline elapsed before execution.", new[] { "Retry only after the Player is responsive." }, true, 100)); return; }
349
- var command = RequiredString(request, "command", 128);
350
- if (string.Equals(command, "runtime-tool.list", StringComparison.Ordinal))
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 wrapper = ReadObject(request, "input");
357
- if (wrapper == null || !HasOnlyKeys(wrapper, "name", "input", "artifactDir", "playModeSessionId")) throw new FormatException("runtime-tool.run wrapper is invalid.");
358
- var name = RequiredString(wrapper, "name", 128);
359
- var toolInput = ReadObject(wrapper, "input");
360
- if (toolInput == null) throw new FormatException("runtime-tool.run input must be an object.");
361
- var execution = ReadObject(request, "execution");
362
- object dryRunValue;
363
- var dryRun = execution != null && execution.TryGetValue("dryRun", out dryRunValue) && dryRunValue is bool && (bool)dryRunValue;
364
- var result = UnityCliRuntimeTools.Execute(name, toolInput, dryRun);
365
- pendingRequest.Completion.TrySetResult(Success(requestId, new Dictionary<string, object> { { "result", result } }));
366
- return;
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) { pendingRequest.Completion.TrySetResult(Error(requestId, exception.Code, exception.Message, new[] { "Run tools describe for the selected Runtime Tool and inspect the current Player session." })); }
371
- catch (FormatException exception) { pendingRequest.Completion.TrySetResult(Error(requestId, "E_SCHEMA_INVALID", exception.Message, new[] { "Use the Runtime bridge request schema." })); }
372
- catch (Exception exception) { pendingRequest.Completion.TrySetResult(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); }
373
- finally { if (pendingRequest.AdmissionOwner != null) CompleteRuntimeAdmissionRoot(pendingRequest.AdmissionOwner); }
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 CompleteRuntimeAdmissionRoot(UnityCliAdmissionPool.ConsumedIdentity identity)
687
+ private void RetainRuntimeSettlement(PendingRequest request)
377
688
  {
378
- if (admissionPool != null && identity != null
379
- && admissionPool.CompleteTrusted(identity.RequestId, identity.RootFingerprint, identity.Token, identity.SessionId, identity.OwnerKind))
380
- Debug.Log("UnityCli admission_terminal requestId=" + identity.RequestId + " active=" + admissionPool.Capture().Active.ToString(CultureInfo.InvariantCulture));
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,并把嵌套execution deadline冻结为Unix毫秒。</summary>
436
- private static long ValidateRequest(Dictionary<string, object> request)
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
- var deadlineMs = deadline.ToUnixTimeMilliseconds();
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
- private sealed class PendingRequest { internal Dictionary<string, object> Request; internal TaskCompletionSource<string> Completion; internal long AbsoluteDeadlineMs; internal string RequestDigest; internal UnityCliAdmissionPool.ConsumedIdentity AdmissionOwner; internal long Order; internal int ExecutionState; }
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
  }