@alpsckr/unitycli 0.4.0 → 0.4.1
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 +109 -300
- package/contracts/init-contract.json +57 -0
- package/contracts/instances-contract.json +49 -0
- package/contracts/package-assembly-closure.json +33 -0
- package/contracts/package-files.json +1953 -0
- package/dist/bridge/session-supervisor.d.ts +2 -0
- package/dist/bridge/session-supervisor.js +52 -19
- package/dist/bridge/session-supervisor.js.map +1 -1
- package/dist/cli-commands/command.js +33 -15
- package/dist/cli-commands/command.js.map +1 -1
- package/dist/cli-commands/init-discovery.d.ts +4 -0
- package/dist/cli-commands/init-discovery.js +175 -0
- package/dist/cli-commands/init-discovery.js.map +1 -0
- package/dist/cli-commands/init-help.d.ts +3 -0
- package/dist/cli-commands/init-help.js +37 -0
- package/dist/cli-commands/init-help.js.map +1 -0
- package/dist/cli-commands/init-skill.d.ts +10 -0
- package/dist/cli-commands/init-skill.js +105 -0
- package/dist/cli-commands/init-skill.js.map +1 -0
- package/dist/cli-commands/instances.d.ts +4 -0
- package/dist/cli-commands/instances.js +90 -0
- package/dist/cli-commands/instances.js.map +1 -0
- package/dist/cli-commands/package.d.ts +2 -2
- package/dist/cli-commands/package.js +57 -137
- package/dist/cli-commands/package.js.map +1 -1
- package/dist/cli-commands/skill-installer.d.ts +14 -6
- package/dist/cli-commands/skill-installer.js +37 -38
- package/dist/cli-commands/skill-installer.js.map +1 -1
- package/dist/cli-commands/types.d.ts +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +44 -37
- package/dist/cli.js.map +1 -1
- package/dist/contracts/command-contract.js +1 -0
- package/dist/contracts/command-contract.js.map +1 -1
- package/dist/doctor.js +1 -1
- package/dist/doctor.js.map +1 -1
- package/dist/instances.d.ts +1 -1
- package/dist/instances.js +63 -38
- package/dist/instances.js.map +1 -1
- package/dist/output.d.ts +4 -4
- package/dist/output.js +3 -4
- package/dist/output.js.map +1 -1
- package/dist/package-manager.d.ts +43 -2
- package/dist/package-manager.js +255 -16
- package/dist/package-manager.js.map +1 -1
- package/dist/tools/.generated-schemas.js +2 -2
- package/dist/tools/.generated-schemas.js.map +1 -1
- package/dist/tools/commands.js +11 -4
- package/dist/tools/commands.js.map +1 -1
- package/dist/tools/dispatcher.js +1 -1
- package/dist/tools/dispatcher.js.map +1 -1
- package/dist/tools/tools/group-list.d.ts +4 -8
- package/dist/tools/tools/group-list.js +12 -13
- package/dist/tools/tools/group-list.js.map +1 -1
- package/dist/tools/tools/pagination.d.ts +17 -0
- package/dist/tools/tools/pagination.js +28 -0
- package/dist/tools/tools/pagination.js.map +1 -0
- package/dist/tools/tools/search.d.ts +3 -5
- package/dist/tools/tools/search.js +12 -12
- package/dist/tools/tools/search.js.map +1 -1
- package/docs/README.md +7 -4
- package/docs/architecture.md +103 -0
- package/docs/availability-differences.md +1 -1
- package/docs/command-execution.md +6 -1
- package/docs/development.md +91 -0
- package/docs/error-codes.md +5 -5
- package/docs/extensions.md +11 -13
- package/docs/target-and-instance.md +6 -4
- package/docs/tool-discovery.md +12 -10
- package/docs/tool-execution.md +3 -2
- package/package.json +10 -4
- package/schemas/init.schema.json +124 -0
- package/schemas/instances.schema.json +104 -0
- package/skills/unitycli/SKILL.md +18 -6
- package/skills/unitycli/manifest.json +2 -2
- package/skills/unitycli/references/custom-tools.md +3 -3
- package/skills/unitycli/references/operation-protocol.md +6 -4
- package/skills/unitycli/reports/output_quality_scorecard.md +1 -1
- package/skills/unitycli/reports/trust-report.md +1 -1
- package/unitypkg/Editor/Bridge/BridgeCommandRegistry.cs +1 -0
- package/unitypkg/Editor/Bridge/BridgeWorkQueue.cs +18 -6
- package/unitypkg/Editor/Infrastructure/CodeExecSupport.cs +8 -77
- package/unitypkg/Editor/Infrastructure/UnityCliInstanceCleanup.cs +295 -0
- package/unitypkg/Editor/Infrastructure/UnityCliInstanceRegistry.cs +166 -10
- package/unitypkg/Editor/Infrastructure/UnityCliOperationCoordinator.cs +1 -0
- package/unitypkg/Editor/UnityCliBridge.cs +5 -1
- package/unitypkg/Tests/Editor/UnityCliInstanceCleanupTests.cs +232 -0
- package/unitypkg/Tests/Editor/UnityCliInstancePublicationRetryTests.cs +143 -0
- package/unitypkg/UnityCli.Editor.asmdef +1 -5
- package/unitypkg/package.json +1 -1
- package/unitypkg/Editor/Roslyn/LICENSE.txt +0 -23
- package/unitypkg/Editor/Roslyn/Microsoft.CodeAnalysis.CSharp.dll +0 -0
- package/unitypkg/Editor/Roslyn/Microsoft.CodeAnalysis.dll +0 -0
- package/unitypkg/Editor/Roslyn/System.Collections.Immutable.dll +0 -0
- package/unitypkg/Editor/Roslyn/System.Reflection.Metadata.dll +0 -0
|
@@ -96,16 +96,16 @@ AI 使用当前实例的统一公开目录自发现和执行:
|
|
|
96
96
|
|
|
97
97
|
```powershell
|
|
98
98
|
unitycli tools group --instance <id|projectPath|alias>
|
|
99
|
-
unitycli tools group list <group> --instance <id|projectPath|alias>
|
|
99
|
+
unitycli tools group list <group> --page-size 20 --instance <id|projectPath|alias>
|
|
100
100
|
unitycli tools describe <custom-tool> --instance <id|projectPath|alias>
|
|
101
101
|
unitycli tools run <custom-tool> --input '<json>' --instance <id|projectPath|alias>
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
`<group>` 必须来自当前实例 `tools group` 的真实输出;不要假设项目 Tool 位于 `custom`
|
|
104
|
+
`<group>` 必须来自当前实例 `tools group` 的真实输出;不要假设项目 Tool 位于 `custom` 组。存在`pageInfo.nextCursor`时必须继续读取所需页面。`source` 只作内部描述/审计,不改变发现或执行路径。
|
|
105
105
|
|
|
106
106
|
## 最小验证
|
|
107
107
|
|
|
108
108
|
1. Unity 编译无错误。
|
|
109
|
-
2. `tools group` 与 `tools group list <group>`
|
|
109
|
+
2. `tools group` 与 `tools group list <group>` 的分页结果能从同一实例完整目录发现 Tool。
|
|
110
110
|
3. `tools describe <tool>` 的 schema、风险、target、Play Mode 语义足以让陌生 AI 一次调用成功。
|
|
111
111
|
4. `tools run <tool> --input '<json>' --instance <id|projectPath|alias>` 覆盖默认正向、代表性可选参数和错误路径。
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
`--instance <full-id|canonical-project-path|alias>`是通用显式实例选择器;`command --project-path <path>`是同一canonical identity选择语法。显式空值必须失败;业务`input.instance`不是selector。未传selector时只从当前Unity项目CWD解析;无法确定实例即稳定失败,`--project`和Tool input都不能选实例。
|
|
8
8
|
|
|
9
|
-
`target alias` 只维护 canonical project identity alias。`source` 只供说明和审计,不影响显示、describe、owner
|
|
9
|
+
`target alias` 只维护 canonical project identity alias。`source` 只供说明和审计,不影响显示、describe、owner 路由或执行,也不进入搜索结果。连接实例的 Tool 目录是权威目录;离线静态 catalog 不能替代它。PowerShell中执行多token搜索时使用`tools search '<k1|k2|...>'`;单引号必须包住完整关键词,避免`|`被解释为shell管道。
|
|
10
10
|
|
|
11
|
-
`
|
|
11
|
+
`tools group list`、`tools search`和省略name的`command`列表按名称稳定分页,默认20项、最大100项。存在`pageInfo.nextCursor`时,将其原值传回同一查询的`--cursor`;不要重新解释或递增cursor。stdout默认是单行紧凑JSON,`--pretty`只增加缩进。
|
|
12
|
+
|
|
13
|
+
`instances list`逐个验证当前正式实例记录。旧格式、损坏或不可读文件不参与路由,也不阻断其他合法实例;存在时结果包含`invalidRecordCount`。当前项目由Unity在ready时自动重建。需要清理历史记录时调用`instances prune --instance <id|projectPath|alias>`并检查`removed`、`remaining`、`deferred`、`scanned`和`truncated`;不得读取、兼容解析、转换、移动或手工删除内部文件。
|
|
12
14
|
|
|
13
15
|
## Descriptor
|
|
14
16
|
|
|
@@ -46,7 +48,7 @@ Tool descriptor 常用字段:
|
|
|
46
48
|
|
|
47
49
|
每个live Editor或Development Player session拥有独立任务池,默认达到5个active root时关闭准入,降到4仍关闭,降到3才重开。CLI在业务请求前自动等待;AI不手工轮询、不读取active count,也不管理内部token。统一catalog在Editor和Runtime同时在线时,会分别经过两个session的准入后才返回完整结果。
|
|
48
50
|
|
|
49
|
-
`--timeout-ms T`先提供一份最多`T
|
|
51
|
+
`--timeout-ms T`先提供一份最多`T`的准入预算。CLI在其中等待首次ready记录、当前编译/导入/重载和任务池;证明Unity未运行时立即返回。准入deadline的`E_UNITY_BUSY`明确证明业务Bridge请求未发送、effect未开始,可以稍后提交新root。获得grant后再创建一份完整`T`的execution预算;token重取、status、rebind、reload和terminal recovery都只能消费该预算,不能刷新第三份预算。
|
|
50
52
|
|
|
51
53
|
`E_EXECUTION_TIMEOUT`、放行后的transport丢失、进程中断或没有可信terminal envelope时,只能确定本次调用没有返回可信终态,不能据此判断mutation是否发生。先读取对象、场景、Package、build/test状态或artifact manifest;只有形成新的业务决策后才提交新请求,不把outcome unknown自动重放。
|
|
52
54
|
|
|
@@ -80,4 +82,4 @@ unitycli extensions remove <id> --project <path>
|
|
|
80
82
|
unitycli extensions doctor --project <path>
|
|
81
83
|
```
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
安装协议先使用`unitycli init help`学习,再由`init discover`只读发现现场。Bridge只用`init package --project <UnityProject>`安装,并用同命令加`--check`校验;Skill只用`init skill --agent <.agents|claude> --scope <workspace|user>`,workspace scope还必须传`--workspace <path>`,同命令加`--check`校验。不得使用裸`init`或混装Bridge与Skill。Unity project文件投影完成后,如任务要求Editor效果,仍需在已连接实例中完成import/compile和业务readback;安装协议本身不得管理Unity进程。
|
|
@@ -24,7 +24,7 @@ The feedback output contract requires category, concise issue summary, optional
|
|
|
24
24
|
| Feedback privacy negatives | PASS | Secret/endpoint/path negative matrix | Novel secret formats can still exist |
|
|
25
25
|
| Concurrent JSONL validity | PASS | 16 parallel Windows PowerShell writers | Cross-session Windows desktop contention not measured |
|
|
26
26
|
| Rolling bound | PASS | Main + 3 archives, each <= 64 KiB | Longitudinal retention utility not measured |
|
|
27
|
-
| Installed bundle reachability | PASS | Public
|
|
27
|
+
| Installed bundle reachability | PASS | Public workspace/user install, drifted-update, check, and recorder smoke for `.agents` and Claude | Verified with isolated filesystem targets |
|
|
28
28
|
| Clean Agent interpretation | PASS | Black-box runtime discovery transcript and main-agent audit | Human adjudication unavailable |
|
|
29
29
|
|
|
30
30
|
## Rollback Boundary
|
|
@@ -36,7 +36,7 @@ The Skill does not add confirmation, intent review, or a predefined operation ra
|
|
|
36
36
|
|
|
37
37
|
- Full repository tests: 876/876 PASS; targeted Skill/installer/docs regression: 61/61 PASS.
|
|
38
38
|
- Recorder concurrency, rotation, privacy negatives, PowerShell 5.1 BOM, and non-ASCII stdout: PASS.
|
|
39
|
-
- Public runtime install, drifted update, check, marker preservation, and installed-recorder smoke across `.
|
|
39
|
+
- Public runtime install, drifted update, check, marker preservation, and installed-recorder smoke across `.claude` and `.agents`: PASS.
|
|
40
40
|
- Resource boundary: 963 estimated initial-load tokens (691 Skill body tokens), limit 1300.
|
|
41
41
|
- Trigger fixture gate: 29/29 classifications.
|
|
42
42
|
- Output assertion gate: 5/5 with-skill cases, 100% pass rate.
|
|
@@ -86,6 +86,7 @@ namespace UnityCli
|
|
|
86
86
|
RegisterInstalledExtensionCommands(registry);
|
|
87
87
|
RegisterCodeExecCommand(registry);
|
|
88
88
|
RegisterEditorStateSnapshotCommand(registry);
|
|
89
|
+
RegisterInstanceCleanupCommand(registry);
|
|
89
90
|
UnityCliCompileCoordinator.Register(registry);
|
|
90
91
|
UnityCliLiveBuildCoordinator.Register(registry);
|
|
91
92
|
UnityCliLiveRefreshCoordinator.Register(registry);
|
|
@@ -60,22 +60,34 @@ namespace UnityCli
|
|
|
60
60
|
// pulse只证明EditorApplication.update最近推进;具体请求仍由TryStart证明开始。
|
|
61
61
|
Volatile.Write(ref mainThreadPulseTimestamp, System.Diagnostics.Stopwatch.GetTimestamp());
|
|
62
62
|
Interlocked.Increment(ref mainThreadPulseSequence);
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
var listenerWasStarted = started;
|
|
64
|
+
var isCompiling = EditorApplication.isCompiling;
|
|
65
|
+
var isUpdating = EditorApplication.isUpdating;
|
|
66
|
+
var isBuilding = BuildPipeline.isBuildingPlayer;
|
|
67
|
+
var isPlayModeChanging = IsPlayModeChanging();
|
|
68
|
+
var lifecycleChanged = cachedIsCompiling != isCompiling
|
|
69
|
+
|| cachedIsUpdating != isUpdating
|
|
70
|
+
|| cachedIsBuilding != isBuilding
|
|
71
|
+
|| cachedIsPlayModeChanging != isPlayModeChanging;
|
|
72
|
+
cachedIsCompiling = isCompiling;
|
|
73
|
+
cachedIsUpdating = isUpdating;
|
|
65
74
|
cachedIsPlayingOrWillChangePlaymode = EditorApplication.isPlayingOrWillChangePlaymode;
|
|
66
75
|
cachedIsPlaying = EditorApplication.isPlaying;
|
|
67
|
-
cachedIsBuilding =
|
|
68
|
-
cachedIsPlayModeChanging =
|
|
76
|
+
cachedIsBuilding = isBuilding;
|
|
77
|
+
cachedIsPlayModeChanging = isPlayModeChanging;
|
|
69
78
|
AdvanceBridgeStartup();
|
|
70
79
|
UnityCliSessionMutationLease.TryFinalizeOwnedPlayModeRoot();
|
|
71
80
|
UnityCliWaitScheduler.Tick();
|
|
72
81
|
|
|
73
|
-
//
|
|
74
|
-
if (
|
|
82
|
+
// 新listener先保留至少一个主线程update的transitioning状态;后续生命周期变化即时发布,30秒刷新只修复漏事件。
|
|
83
|
+
if (listenerWasStarted && started
|
|
84
|
+
&& (readyPublicationPending || lifecycleChanged || EditorApplication.timeSinceStartup >= nextRegistryRefreshAt))
|
|
75
85
|
{
|
|
86
|
+
readyPublicationPending = false;
|
|
76
87
|
RegisterInstance("ready");
|
|
77
88
|
nextRegistryRefreshAt = EditorApplication.timeSinceStartup + RegistryRefreshSeconds;
|
|
78
89
|
}
|
|
90
|
+
TickInstancePublicationRetry();
|
|
79
91
|
var startedThisTick = 0;
|
|
80
92
|
while (startedThisTick < MaxWorkStartsPerTick)
|
|
81
93
|
{
|
|
@@ -11,8 +11,6 @@ using System.Text;
|
|
|
11
11
|
using System.Text.RegularExpressions;
|
|
12
12
|
using System.Threading;
|
|
13
13
|
using System.Threading.Tasks;
|
|
14
|
-
using Microsoft.CodeAnalysis;
|
|
15
|
-
using Microsoft.CodeAnalysis.CSharp;
|
|
16
14
|
using UnityEditor;
|
|
17
15
|
using UnityEditor.SceneManagement;
|
|
18
16
|
using UnityEngine;
|
|
@@ -34,9 +32,6 @@ namespace UnityCli
|
|
|
34
32
|
private const int MaxRoslynOutputBytes = 512 * 1024;
|
|
35
33
|
private const int MaxRoslynDiagnostics = 100;
|
|
36
34
|
|
|
37
|
-
// code.exec 编译核心开关:默认 in-process Roslyn(官方方案);置 false 回退到 csc.exe 子进程。
|
|
38
|
-
private const bool CodeExecUseInProcessRoslyn = true;
|
|
39
|
-
|
|
40
35
|
// 注册运行时代码执行命令。
|
|
41
36
|
private static void RegisterCodeExecCommand(BridgeCommandRegistry registry)
|
|
42
37
|
{
|
|
@@ -90,9 +85,7 @@ namespace UnityCli
|
|
|
90
85
|
byte[] assemblyBytes;
|
|
91
86
|
try
|
|
92
87
|
{
|
|
93
|
-
var compilation =
|
|
94
|
-
? CompileInProcess(source, references, request.StringArg("defines"), request, cancellationToken)
|
|
95
|
-
: CompileWithCscFallback(source, references, request.StringArg("defines"), request, cancellationToken);
|
|
88
|
+
var compilation = CompileWithUnityCompiler(source, references, request.StringArg("defines"), request, cancellationToken);
|
|
96
89
|
if (compilation.TimedOut)
|
|
97
90
|
return CodeExecutionResult.Fail("E_EXECUTION_TIMEOUT", "Runtime Code compilation exceeded the immutable deadline.", new Dictionary<string, object>(), "Inspect the Editor state before submitting a new request.");
|
|
98
91
|
if (!compilation.Success)
|
|
@@ -215,8 +208,8 @@ namespace UnityCli
|
|
|
215
208
|
}
|
|
216
209
|
|
|
217
210
|
/// <summary>
|
|
218
|
-
/// 汇总
|
|
219
|
-
/// 不排除 mscorlib/System*/netstandard
|
|
211
|
+
/// 汇总 Unity Editor 编译器所需引用;全包含 AppDomain 已加载程序集,
|
|
212
|
+
/// 不排除 mscorlib/System*/netstandard,避免项目程序集缺少核心库引用。
|
|
220
213
|
/// </summary>
|
|
221
214
|
private static IEnumerable<string> DefaultCodeReferences(string refsCsv, UnityCli.Protocol.BridgeRequest request, CancellationToken cancellationToken)
|
|
222
215
|
{
|
|
@@ -245,7 +238,7 @@ namespace UnityCli
|
|
|
245
238
|
if (!references.Contains(reference)) references.Add(reference);
|
|
246
239
|
}
|
|
247
240
|
|
|
248
|
-
//
|
|
241
|
+
// Unity Editor 编译器结果;成功时携带程序集字节,失败时携带结构化诊断。
|
|
249
242
|
private sealed class CodeCompilationResult
|
|
250
243
|
{
|
|
251
244
|
public bool Success;
|
|
@@ -255,71 +248,9 @@ namespace UnityCli
|
|
|
255
248
|
}
|
|
256
249
|
|
|
257
250
|
/// <summary>
|
|
258
|
-
///
|
|
259
|
-
/// 引用集全包含 AppDomain 已加载程序集,与官方 Editor 模式一致;
|
|
260
|
-
/// 编译无法中途终止,只能在编译前后重验 immutable deadline。
|
|
261
|
-
/// </summary>
|
|
262
|
-
private static CodeCompilationResult CompileInProcess(string source, IEnumerable<string> references, string definesCsv, UnityCli.Protocol.BridgeRequest request, CancellationToken cancellationToken)
|
|
263
|
-
{
|
|
264
|
-
var result = new CodeCompilationResult();
|
|
265
|
-
try
|
|
266
|
-
{
|
|
267
|
-
EnsureCodePreEffectDeadline(request, cancellationToken);
|
|
268
|
-
var parseOptions = new CSharpParseOptions(preprocessorSymbols: SplitCsv(definesCsv));
|
|
269
|
-
var tree = CSharpSyntaxTree.ParseText(WrapCodeSource(source), parseOptions);
|
|
270
|
-
var metadataReferences = new List<MetadataReference>();
|
|
271
|
-
foreach (var reference in references)
|
|
272
|
-
{
|
|
273
|
-
if (string.IsNullOrEmpty(reference)) continue;
|
|
274
|
-
try { metadataReferences.Add(MetadataReference.CreateFromFile(reference)); }
|
|
275
|
-
catch { /* 跳过不可读引用,避免个别文件缺失中断整次编译 */ }
|
|
276
|
-
}
|
|
277
|
-
var compilation = CSharpCompilation.Create(
|
|
278
|
-
"UnityCli.RuntimeCode",
|
|
279
|
-
new[] { tree },
|
|
280
|
-
metadataReferences,
|
|
281
|
-
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
|
282
|
-
using (var ms = new MemoryStream())
|
|
283
|
-
{
|
|
284
|
-
var emitResult = compilation.Emit(ms);
|
|
285
|
-
foreach (var diagnostic in emitResult.Diagnostics)
|
|
286
|
-
{
|
|
287
|
-
if (diagnostic.Severity != DiagnosticSeverity.Error) continue;
|
|
288
|
-
if (result.Diagnostics.Count >= MaxRoslynDiagnostics) continue;
|
|
289
|
-
var lineSpan = diagnostic.Location.GetLineSpan();
|
|
290
|
-
result.Diagnostics.Add(new Dictionary<string, object>
|
|
291
|
-
{
|
|
292
|
-
{ "severity", "error" }, { "id", BoundedText(diagnostic.Id, 64) },
|
|
293
|
-
{ "message", BoundedText(diagnostic.GetMessage(), 4096) },
|
|
294
|
-
{ "file", BoundedText(lineSpan.Path ?? string.Empty, 2048) },
|
|
295
|
-
{ "line", (long)(lineSpan.StartLinePosition.Line + 1) },
|
|
296
|
-
{ "column", (long)(lineSpan.StartLinePosition.Character + 1) },
|
|
297
|
-
{ "span", null }
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
result.Success = emitResult.Success;
|
|
301
|
-
if (emitResult.Success) result.AssemblyBytes = ms.ToArray();
|
|
302
|
-
}
|
|
303
|
-
// in-process 无法中途终止;编译完成后重验 deadline,拒绝超时结果。
|
|
304
|
-
int remainingMs;
|
|
305
|
-
if (cancellationToken.IsCancellationRequested || request == null || !request.TryDeadlineRemainingMs(out remainingMs))
|
|
306
|
-
{
|
|
307
|
-
result.TimedOut = true;
|
|
308
|
-
result.Success = false;
|
|
309
|
-
result.AssemblyBytes = null;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
catch (OperationCanceledException)
|
|
313
|
-
{
|
|
314
|
-
result.TimedOut = true;
|
|
315
|
-
}
|
|
316
|
-
return result;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/// <summary>
|
|
320
|
-
/// csc.exe 子进程编译回退;保留原临时文件与子进程超时终止逻辑,仅在 in-process 编译不可用时切换回来。
|
|
251
|
+
/// 使用当前 Unity Editor 自带的 csc.exe 编译源码;子进程受请求 deadline 约束并可终止。
|
|
321
252
|
/// </summary>
|
|
322
|
-
private static CodeCompilationResult
|
|
253
|
+
private static CodeCompilationResult CompileWithUnityCompiler(string source, IEnumerable<string> references, string definesCsv, UnityCli.Protocol.BridgeRequest request, CancellationToken cancellationToken)
|
|
323
254
|
{
|
|
324
255
|
var result = new CodeCompilationResult();
|
|
325
256
|
var editorDataPath = EditorDataPath();
|
|
@@ -339,7 +270,7 @@ namespace UnityCli
|
|
|
339
270
|
{
|
|
340
271
|
File.WriteAllText(sourceFilePath, WrapCodeSource(source), new UTF8Encoding(true));
|
|
341
272
|
WriteReferenceResponseFile(responseFilePath, references);
|
|
342
|
-
var compilation =
|
|
273
|
+
var compilation = RunUnityCompiler(editorDataPath, sourceFilePath, responseFilePath, assemblyFilePath, definesCsv, request, cancellationToken);
|
|
343
274
|
if (compilation.TimedOut)
|
|
344
275
|
{
|
|
345
276
|
result.TimedOut = true;
|
|
@@ -394,7 +325,7 @@ namespace UnityCli
|
|
|
394
325
|
/// <summary>
|
|
395
326
|
/// 使用 Unity 内置 Roslyn 编译临时源码;进程最多运行 60 秒,输出仅用于诊断,成功程序集由调用方读取到内存。
|
|
396
327
|
/// </summary>
|
|
397
|
-
private static RoslynCompilation
|
|
328
|
+
private static RoslynCompilation RunUnityCompiler(string editorDataPath, string sourceFilePath, string responseFilePath, string assemblyFilePath, string definesCsv, UnityCli.Protocol.BridgeRequest request, CancellationToken cancellationToken)
|
|
398
329
|
{
|
|
399
330
|
var monoPath = Path.Combine(editorDataPath, "MonoBleedingEdge", "bin", "mono.exe");
|
|
400
331
|
var cscPath = Path.Combine(editorDataPath, "MonoBleedingEdge", "lib", "mono", "4.5", "csc.exe");
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#if UNITY_EDITOR
|
|
2
|
+
using System;
|
|
3
|
+
using System.Collections.Generic;
|
|
4
|
+
using System.Globalization;
|
|
5
|
+
using System.IO;
|
|
6
|
+
using System.Linq;
|
|
7
|
+
using System.Text;
|
|
8
|
+
|
|
9
|
+
namespace UnityCli
|
|
10
|
+
{
|
|
11
|
+
public static partial class UnityCliBridge
|
|
12
|
+
{
|
|
13
|
+
private const int InstanceCleanupMaxScanned = 2000;
|
|
14
|
+
private const int InstanceCleanupMaxDeleted = 20;
|
|
15
|
+
private const int InstanceCleanupRetainedOffline = 100;
|
|
16
|
+
private const int InstanceCleanupRetainedDamaged = 20;
|
|
17
|
+
private const int InstanceCleanupRetentionDays = 30;
|
|
18
|
+
private static bool instanceCleanupAttempted;
|
|
19
|
+
private static readonly UnityCliToolCompletionContract InstanceCleanupCompletion = new UnityCliToolCompletionContract(
|
|
20
|
+
"internal.instances.prune", "control-plane", "direct-terminal", "atomic", "bounded-cleanup-summary",
|
|
21
|
+
"writer-owned-registry-reinspection", "caller-responsibility", "E_EXECUTION_TIMEOUT", "caller-responsibility",
|
|
22
|
+
"atomic-typed-result-v1", "required", true);
|
|
23
|
+
|
|
24
|
+
private sealed class InstanceCleanupCandidate
|
|
25
|
+
{
|
|
26
|
+
public string Path;
|
|
27
|
+
public string InstanceId;
|
|
28
|
+
public DateTimeOffset LastActivity;
|
|
29
|
+
public bool Damaged;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private sealed class InstanceCleanupSummary
|
|
33
|
+
{
|
|
34
|
+
public int Scanned;
|
|
35
|
+
public int Offline;
|
|
36
|
+
public int Damaged;
|
|
37
|
+
public int Preserved;
|
|
38
|
+
public int Deleted;
|
|
39
|
+
public int Deferred;
|
|
40
|
+
public int Remaining;
|
|
41
|
+
public bool Truncated;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// <summary>每个domain只尝试一次自动清理;其他Unity持有全局锁时直接让其负责。</summary>
|
|
45
|
+
private static void TryRunAutomaticInstanceCleanup()
|
|
46
|
+
{
|
|
47
|
+
if (instanceCleanupAttempted) return;
|
|
48
|
+
instanceCleanupAttempted = true;
|
|
49
|
+
try
|
|
50
|
+
{
|
|
51
|
+
InstanceCleanupSummary summary;
|
|
52
|
+
var acquired = TryExecuteInstanceCleanup(null, out summary);
|
|
53
|
+
if (!acquired)
|
|
54
|
+
{
|
|
55
|
+
WriteLog("instance_cleanup_deferred reason=global-lock");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
WriteLog("instance_cleanup_completed scanned=" + summary.Scanned.ToString(CultureInfo.InvariantCulture)
|
|
59
|
+
+ " offline=" + summary.Offline.ToString(CultureInfo.InvariantCulture)
|
|
60
|
+
+ " damaged=" + summary.Damaged.ToString(CultureInfo.InvariantCulture)
|
|
61
|
+
+ " preserved=" + summary.Preserved.ToString(CultureInfo.InvariantCulture)
|
|
62
|
+
+ " deleted=" + summary.Deleted.ToString(CultureInfo.InvariantCulture)
|
|
63
|
+
+ " deferred=" + summary.Deferred.ToString(CultureInfo.InvariantCulture)
|
|
64
|
+
+ " truncated=" + (summary.Truncated ? "true" : "false"));
|
|
65
|
+
}
|
|
66
|
+
catch (Exception exception)
|
|
67
|
+
{
|
|
68
|
+
WriteLog("instance_cleanup_failed kind=" + exception.GetType().Name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// <summary>跨Unity只允许一个清理owner;零等待失败由调用方延期或返回busy。</summary>
|
|
73
|
+
private static bool TryExecuteInstanceCleanup(Action beginEffect, out InstanceCleanupSummary summary)
|
|
74
|
+
{
|
|
75
|
+
InstanceCleanupSummary value = null;
|
|
76
|
+
var acquired = TryWithCurrentUserMutex("Local\\UnityCli.Registry.Cleanup", 0, delegate
|
|
77
|
+
{
|
|
78
|
+
if (beginEffect != null) beginEffect();
|
|
79
|
+
value = RunInstanceCleanup();
|
|
80
|
+
});
|
|
81
|
+
summary = value;
|
|
82
|
+
return acquired;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// <summary>扫描和删除均有硬上限;活跃、不可确认、链接或锁竞争记录只保留。</summary>
|
|
86
|
+
private static InstanceCleanupSummary RunInstanceCleanup()
|
|
87
|
+
{
|
|
88
|
+
var summary = new InstanceCleanupSummary();
|
|
89
|
+
var directory = InstancesDir();
|
|
90
|
+
if (!Directory.Exists(directory)) return summary;
|
|
91
|
+
var root = Path.GetFullPath(directory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
92
|
+
var candidates = new List<InstanceCleanupCandidate>();
|
|
93
|
+
using (var enumerator = Directory.EnumerateFiles(root, "*.json", SearchOption.TopDirectoryOnly).GetEnumerator())
|
|
94
|
+
{
|
|
95
|
+
while (summary.Scanned < InstanceCleanupMaxScanned && enumerator.MoveNext())
|
|
96
|
+
{
|
|
97
|
+
var fullPath = Path.GetFullPath(enumerator.Current);
|
|
98
|
+
var parent = Path.GetDirectoryName(fullPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
99
|
+
var fileName = Path.GetFileName(fullPath);
|
|
100
|
+
string candidateInstanceId;
|
|
101
|
+
if (!string.Equals(parent, root, StringComparison.OrdinalIgnoreCase)
|
|
102
|
+
|| !TryInstanceIdFromFileName(fileName, out candidateInstanceId)) continue;
|
|
103
|
+
summary.Scanned += 1;
|
|
104
|
+
if (string.Equals(candidateInstanceId, InstanceId(), StringComparison.Ordinal))
|
|
105
|
+
{
|
|
106
|
+
summary.Preserved += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
InstanceCleanupCandidate candidate;
|
|
110
|
+
if (!TryInspectCleanupCandidate(fullPath, candidateInstanceId, out candidate))
|
|
111
|
+
{
|
|
112
|
+
summary.Preserved += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (candidate.Damaged) summary.Damaged += 1;
|
|
116
|
+
else summary.Offline += 1;
|
|
117
|
+
candidates.Add(candidate);
|
|
118
|
+
}
|
|
119
|
+
if (enumerator.MoveNext()) summary.Truncated = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
var cutoff = DateTimeOffset.UtcNow.AddDays(-InstanceCleanupRetentionDays);
|
|
123
|
+
var offline = candidates.Where(value => !value.Damaged).OrderBy(value => value.LastActivity).ThenBy(value => value.InstanceId, StringComparer.Ordinal).ToList();
|
|
124
|
+
var damaged = candidates.Where(value => value.Damaged).OrderBy(value => value.LastActivity).ThenBy(value => value.InstanceId, StringComparer.Ordinal).ToList();
|
|
125
|
+
var planned = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
126
|
+
PlanCleanup(offline, InstanceCleanupRetainedOffline, cutoff, planned);
|
|
127
|
+
PlanCleanup(damaged, InstanceCleanupRetainedDamaged, cutoff, planned);
|
|
128
|
+
summary.Deferred += Math.Max(0, planned.Count - InstanceCleanupMaxDeleted);
|
|
129
|
+
|
|
130
|
+
foreach (var candidate in candidates.Where(value => planned.Contains(value.Path)).OrderBy(value => value.LastActivity).ThenBy(value => value.InstanceId, StringComparer.Ordinal).Take(InstanceCleanupMaxDeleted))
|
|
131
|
+
{
|
|
132
|
+
var deleted = false;
|
|
133
|
+
var locked = false;
|
|
134
|
+
try
|
|
135
|
+
{
|
|
136
|
+
locked = TryWithCurrentUserMutex("Local\\UnityCli.Registry." + candidate.InstanceId, 0, delegate
|
|
137
|
+
{
|
|
138
|
+
InstanceCleanupCandidate current;
|
|
139
|
+
if (!TryInspectCleanupCandidate(candidate.Path, candidate.InstanceId, out current)) return;
|
|
140
|
+
if (current.Damaged != candidate.Damaged || current.LastActivity > candidate.LastActivity) return;
|
|
141
|
+
File.Delete(candidate.Path);
|
|
142
|
+
deleted = true;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch
|
|
146
|
+
{
|
|
147
|
+
deleted = false;
|
|
148
|
+
}
|
|
149
|
+
if (deleted) summary.Deleted += 1;
|
|
150
|
+
else if (!locked || File.Exists(candidate.Path)) summary.Deferred += 1;
|
|
151
|
+
}
|
|
152
|
+
summary.Remaining = Math.Max(0, summary.Scanned - summary.Deleted);
|
|
153
|
+
return summary;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private static void PlanCleanup(List<InstanceCleanupCandidate> ordered, int retained, DateTimeOffset cutoff, HashSet<string> planned)
|
|
157
|
+
{
|
|
158
|
+
var excess = Math.Max(0, ordered.Count - retained);
|
|
159
|
+
for (var index = 0; index < ordered.Count; index++)
|
|
160
|
+
{
|
|
161
|
+
if (index < excess || ordered[index].LastActivity <= cutoff) planned.Add(ordered[index].Path);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// <summary>返回false表示活跃、不可确认、链接、占用或不安全路径,调用方必须保留。</summary>
|
|
166
|
+
private static bool TryInspectCleanupCandidate(string path, string expectedInstanceId, out InstanceCleanupCandidate candidate)
|
|
167
|
+
{
|
|
168
|
+
candidate = null;
|
|
169
|
+
try
|
|
170
|
+
{
|
|
171
|
+
var info = new FileInfo(path);
|
|
172
|
+
if (!info.Exists || (info.Attributes & FileAttributes.ReparsePoint) != 0) return false;
|
|
173
|
+
var fallbackActivity = new DateTimeOffset(info.LastWriteTimeUtc, TimeSpan.Zero);
|
|
174
|
+
if (info.Length > RegistryMaxBytes)
|
|
175
|
+
{
|
|
176
|
+
candidate = CleanupCandidate(path, expectedInstanceId, fallbackActivity, true);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
Dictionary<string, object> record;
|
|
181
|
+
try
|
|
182
|
+
{
|
|
183
|
+
record = UnityCliSerializer.Parse(File.ReadAllText(path, Encoding.UTF8)) as Dictionary<string, object>;
|
|
184
|
+
}
|
|
185
|
+
catch (FormatException)
|
|
186
|
+
{
|
|
187
|
+
candidate = CleanupCandidate(path, expectedInstanceId, fallbackActivity, true);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
if (record == null || ReadLong(record, "version") != 3L || ReadLong(record, "canonicalizationVersion") != 1L)
|
|
191
|
+
{
|
|
192
|
+
candidate = CleanupCandidate(path, expectedInstanceId, fallbackActivity, true);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
var identity = ReadDictionary(record, "identity");
|
|
196
|
+
var canonicalPath = ReadString(identity, "canonicalProjectPath");
|
|
197
|
+
var actualInstanceId = ReadString(identity, "instanceId");
|
|
198
|
+
if (string.IsNullOrEmpty(canonicalPath)
|
|
199
|
+
|| !string.Equals(actualInstanceId, expectedInstanceId, StringComparison.Ordinal)
|
|
200
|
+
|| !string.Equals("unity-" + Sha256Utf8(canonicalPath), expectedInstanceId, StringComparison.Ordinal))
|
|
201
|
+
{
|
|
202
|
+
candidate = CleanupCandidate(path, expectedInstanceId, fallbackActivity, true);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
object rawSessions;
|
|
206
|
+
var sessionValues = record.TryGetValue("sessions", out rawSessions) ? rawSessions as List<object> : null;
|
|
207
|
+
if (sessionValues == null)
|
|
208
|
+
{
|
|
209
|
+
candidate = CleanupCandidate(path, expectedInstanceId, fallbackActivity, true);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
var sessions = sessionValues.OfType<Dictionary<string, object>>().ToList();
|
|
213
|
+
if (sessions.Count != sessionValues.Count || sessions.Any(IsProcessClaimAliveOrUnverifiable)) return false;
|
|
214
|
+
DateTimeOffset publishedAt;
|
|
215
|
+
if (!DateTimeOffset.TryParse(ReadString(record, "publishedAt"), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out publishedAt))
|
|
216
|
+
{
|
|
217
|
+
candidate = CleanupCandidate(path, expectedInstanceId, fallbackActivity, true);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
candidate = CleanupCandidate(path, expectedInstanceId, publishedAt, false);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
catch (IOException) { return false; }
|
|
224
|
+
catch (UnauthorizedAccessException) { return false; }
|
|
225
|
+
catch { return false; }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private static InstanceCleanupCandidate CleanupCandidate(string path, string instanceId, DateTimeOffset lastActivity, bool damaged)
|
|
229
|
+
{
|
|
230
|
+
return new InstanceCleanupCandidate { Path = path, InstanceId = instanceId, LastActivity = lastActivity, Damaged = damaged };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// <summary>私有Bridge命令让公开instances prune只触发Unity writer,不把删除权交给CLI。</summary>
|
|
234
|
+
private static void RegisterInstanceCleanupCommand(BridgeCommandRegistry registry)
|
|
235
|
+
{
|
|
236
|
+
registry.Register<UnityCli.Protocol.BridgeRequest>(
|
|
237
|
+
"internal.instances.prune",
|
|
238
|
+
"internal",
|
|
239
|
+
true,
|
|
240
|
+
30000,
|
|
241
|
+
(request, cancellationToken) =>
|
|
242
|
+
{
|
|
243
|
+
cancellationToken.ThrowIfCancellationRequested();
|
|
244
|
+
InstanceCleanupSummary summary;
|
|
245
|
+
if (!TryExecuteInstanceCleanup(delegate { BeginDirectEffect(request.RequestId); }, out summary))
|
|
246
|
+
throw new BridgeCommandException("E_UNITY_BUSY", "Another Unity instance is cleaning historical instance records.", null, new[] { "Retry unitycli instances prune after the current cleanup finishes." }, true, 100);
|
|
247
|
+
var result = new Dictionary<string, object>
|
|
248
|
+
{
|
|
249
|
+
{ "removed", (long)summary.Deleted },
|
|
250
|
+
{ "remaining", (long)summary.Remaining },
|
|
251
|
+
{ "deferred", (long)summary.Deferred },
|
|
252
|
+
{ "scanned", (long)summary.Scanned },
|
|
253
|
+
{ "truncated", summary.Truncated },
|
|
254
|
+
{ "retention", new Dictionary<string, object>
|
|
255
|
+
{
|
|
256
|
+
{ "offlineMax", (long)InstanceCleanupRetainedOffline },
|
|
257
|
+
{ "damagedMax", (long)InstanceCleanupRetainedDamaged },
|
|
258
|
+
{ "maxAgeDays", (long)InstanceCleanupRetentionDays },
|
|
259
|
+
{ "scanLimit", (long)InstanceCleanupMaxScanned },
|
|
260
|
+
{ "deleteLimit", (long)InstanceCleanupMaxDeleted }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
RecordDirectCompletionEvidence(request.RequestId, "internal.instances.prune", InstanceCleanupCompletion, result);
|
|
265
|
+
return new System.Threading.Tasks.ValueTask<object>(result);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private static bool IsInstanceCleanupCommand(string command)
|
|
270
|
+
{
|
|
271
|
+
return string.Equals(command, "internal.instances.prune", StringComparison.Ordinal);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private static UnityCliToolCompletionContract InstanceCleanupCompletionContract()
|
|
275
|
+
{
|
|
276
|
+
return InstanceCleanupCompletion;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private static bool TryInstanceIdFromFileName(string fileName, out string instanceId)
|
|
280
|
+
{
|
|
281
|
+
instanceId = null;
|
|
282
|
+
if (string.IsNullOrEmpty(fileName) || !fileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) return false;
|
|
283
|
+
var value = fileName.Substring(0, fileName.Length - 5);
|
|
284
|
+
if (!value.StartsWith("unity-", StringComparison.Ordinal) || value.Length != 70) return false;
|
|
285
|
+
for (var index = 6; index < value.Length; index++)
|
|
286
|
+
{
|
|
287
|
+
var ch = value[index];
|
|
288
|
+
if (!((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f'))) return false;
|
|
289
|
+
}
|
|
290
|
+
instanceId = value;
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
#endif
|