Forge
csharp4405de34
1using System.Text.Json;
2using DotNetBuildTest.Core;
3
4namespace CascadeIDE.BuildVerifyWorker;
5
6/// <summary>Long-lived verify worker: JSON-lines на stdin/stdout (ADR 0148 daemon, opt-in).</summary>
7internal static class BuildVerifyWorkerServeLoop
8{
9 internal static async Task<int> RunAsync(CancellationToken cancellationToken = default)
10 {
11 var coordinator = new BuildTestJobCoordinator();
12 await using var stdin = Console.OpenStandardInput();
13 await using var stdout = Console.OpenStandardOutput();
14 using var reader = new StreamReader(stdin);
15 await using var writer = new StreamWriter(stdout) { AutoFlush = true, NewLine = "\n" };
16
17 await WriteResponseAsync(
18 writer,
19 new Dictionary<string, object?>
20 {
21 ["id"] = "0",
22 ["ok"] = true,
23 ["ready"] = true,
24 ["protocol"] = 1,
25 },
26 cancellationToken).ConfigureAwait(false);
27
28 string? line;
29 while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) is not null)
30 {
31 if (line.Length == 0)
32 continue;
33
34 JsonDocument doc;
35 try
36 {
37 doc = JsonDocument.Parse(line);
38 }
39 catch (JsonException ex)
40 {
41 await WriteResponseAsync(
42 writer,
43 new Dictionary<string, object?> { ["id"] = "", ["ok"] = false, ["error"] = $"invalid_json: {ex.Message}" },
44 cancellationToken).ConfigureAwait(false);
45 continue;
46 }
47
48 using (doc)
49 {
50 var root = doc.RootElement;
51 var id = root.TryGetProperty("id", out var idEl) ? idEl.GetString() ?? "" : "";
52 var op = root.TryGetProperty("op", out var opEl) ? opEl.GetString() ?? "" : "";
53
54 try
55 {
56 var response = await DispatchAsync(coordinator, root, op, cancellationToken).ConfigureAwait(false);
57 response["id"] = id;
58 await WriteResponseAsync(writer, response, cancellationToken).ConfigureAwait(false);
59
60 if (string.Equals(op, "shutdown", StringComparison.OrdinalIgnoreCase))
61 return 0;
62 }
63 catch (Exception ex)
64 {
65 await WriteResponseAsync(
66 writer,
67 new Dictionary<string, object?> { ["id"] = id, ["ok"] = false, ["error"] = ex.Message },
68 cancellationToken).ConfigureAwait(false);
69 }
70 }
71 }
72
73 return 0;
74 }
75
76 private static async Task<Dictionary<string, object?>> DispatchAsync(
77 BuildTestJobCoordinator coordinator,
78 JsonElement req,
79 string op,
80 CancellationToken cancellationToken)
81 {
82 switch (op.Trim().ToLowerInvariant())
83 {
84 case "ping":
85 return new Dictionary<string, object?> { ["ok"] = true, ["pong"] = true };
86
87 case "enqueue":
88 return Enqueue(coordinator, req);
89
90 case "get_status":
91 return GetStatus(coordinator, req);
92
93 case "wait":
94 return await WaitAsync(coordinator, req, cancellationToken).ConfigureAwait(false);
95
96 case "cancel":
97 return Cancel(coordinator, req);
98
99 case "shutdown":
100 return new Dictionary<string, object?> { ["ok"] = true, ["shutting_down"] = true };
101
102 default:
103 return new Dictionary<string, object?> { ["ok"] = false, ["error"] = $"unknown_op:{op}" };
104 }
105 }
106
107 private static Dictionary<string, object?> Enqueue(BuildTestJobCoordinator coordinator, JsonElement req)
108 {
109 var kindWire = req.TryGetProperty("kind", out var k) ? k.GetString() : null;
110 var path = req.TryGetProperty("path", out var p) ? p.GetString() : null;
111 if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
112 return new Dictionary<string, object?> { ["ok"] = false, ["error"] = "path_not_found" };
113
114 var kind = string.Equals(kindWire, "test", StringComparison.OrdinalIgnoreCase)
115 || string.Equals(kindWire, "run_tests", StringComparison.OrdinalIgnoreCase)
116 ? BuildTestJobKind.RunTests
117 : BuildTestJobKind.BuildStructured;
118
119 var includeRaw = !req.TryGetProperty("include_raw_output", out var raw) || raw.ValueKind != JsonValueKind.False;
120 var timeout = req.TryGetProperty("timeout_seconds", out var t) && t.TryGetInt32(out var sec)
121 ? sec
122 : kind == BuildTestJobKind.RunTests
123 ? BuildTestToolRequestParser.DefaultTestTimeoutSeconds
124 : BuildTestToolRequestParser.DefaultBuildTimeoutSeconds;
125
126 var filter = req.TryGetProperty("filter", out var f) ? f.GetString() : null;
127 var dotnetOptions = !string.IsNullOrWhiteSpace(filter)
128 ? DotnetExecutionOptions.Empty with { Filter = filter.Trim() }
129 : DotnetExecutionOptions.Empty;
130
131 var enqueued = coordinator.TryEnqueue(
132 kind,
133 Path.GetFullPath(path),
134 includeRaw,
135 timeout,
136 dotnetOptions);
137
138 if (!enqueued.Accepted)
139 {
140 return new Dictionary<string, object?>
141 {
142 ["ok"] = true,
143 ["accepted"] = false,
144 ["retry_after_seconds"] = enqueued.RetryAfterSeconds,
145 };
146 }
147
148 return new Dictionary<string, object?>
149 {
150 ["ok"] = true,
151 ["accepted"] = true,
152 ["job_id"] = enqueued.JobId,
153 };
154 }
155
156 private static Dictionary<string, object?> GetStatus(BuildTestJobCoordinator coordinator, JsonElement req)
157 {
158 var jobId = req.TryGetProperty("job_id", out var j) ? j.GetString() : null;
159 if (string.IsNullOrWhiteSpace(jobId))
160 return new Dictionary<string, object?> { ["ok"] = false, ["error"] = "job_id_required" };
161
162 var status = coordinator.GetJobStatus(jobId);
163 if (status is null)
164 return new Dictionary<string, object?> { ["ok"] = false, ["error"] = "job_not_found" };
165
166 return new Dictionary<string, object?> { ["ok"] = true, ["status"] = status };
167 }
168
169 private static async Task<Dictionary<string, object?>> WaitAsync(
170 BuildTestJobCoordinator coordinator,
171 JsonElement req,
172 CancellationToken cancellationToken)
173 {
174 var jobId = req.TryGetProperty("job_id", out var j) ? j.GetString() : null;
175 if (string.IsNullOrWhiteSpace(jobId))
176 return new Dictionary<string, object?> { ["ok"] = false, ["error"] = "job_id_required" };
177
178 var result = await coordinator.WaitForCompletionAsync(jobId, cancellationToken).ConfigureAwait(false);
179 return new Dictionary<string, object?>
180 {
181 ["ok"] = true,
182 ["result_json"] = result ?? BuildTestJson.Serialize(new { success = false, message = "cancelled_or_missing" }),
183 };
184 }
185
186 private static Dictionary<string, object?> Cancel(BuildTestJobCoordinator coordinator, JsonElement req)
187 {
188 var jobId = req.TryGetProperty("job_id", out var j) ? j.GetString() : null;
189 if (string.IsNullOrWhiteSpace(jobId))
190 return new Dictionary<string, object?> { ["ok"] = false, ["error"] = "job_id_required" };
191
192 var outcome = coordinator.CancelJob(jobId);
193 var json = JsonSerializer.Serialize(outcome);
194 var cancelled = json.Contains("\"cancelled\":true", StringComparison.OrdinalIgnoreCase);
195 return new Dictionary<string, object?> { ["ok"] = true, ["cancelled"] = cancelled, ["cancel"] = outcome };
196 }
197
198 private static async Task WriteResponseAsync(
199 StreamWriter writer,
200 Dictionary<string, object?> payload,
201 CancellationToken cancellationToken)
202 {
203 var line = JsonSerializer.Serialize(payload);
204 await writer.WriteLineAsync(line.AsMemory(), cancellationToken).ConfigureAwait(false);
205 }
206}
207
View only · write via MCP/CIDE