Forge
csharpdeeb25a2
1using System.Collections.Concurrent;
2using System.Text.Json;
3using DotNetBuildTest.Core;
4
5namespace CascadeIDE.Features.Agent.Environment;
6
7/// <summary>
8/// Build/test через long-lived <c>BuildVerifyWorker serve</c> (ADR 0148 opt-in daemon).
9/// </summary>
10public sealed class DaemonBuildVerifyWorkerBackend : IEnvironmentJobBackend, IAsyncDisposable
11{
12 private readonly BuildVerifyWorkerDaemonClient _client;
13 private readonly ConcurrentDictionary<string, DaemonJobRecord> _jobs = new();
14
15 public DaemonBuildVerifyWorkerBackend(string workerDllPath)
16 {
17 _client = new BuildVerifyWorkerDaemonClient(workerDllPath);
18 }
19
20 public string HostKind => BuildTestHostFactory.WorkerDaemonHostKind;
21
22 public BuildTestEnqueueResult TryEnqueue(
23 BuildTestJobKind kind,
24 string path,
25 bool includeRawOutput,
26 int timeoutSeconds,
27 DotnetExecutionOptions dotnetOptions)
28 {
29 try
30 {
31 var resp = _client
32 .SendAsync(
33 BuildEnqueuePayload(kind, path, includeRawOutput, timeoutSeconds, dotnetOptions),
34 CancellationToken.None)
35 .GetAwaiter()
36 .GetResult();
37
38 if (!resp.TryGetProperty("ok", out var ok) || ok.ValueKind != JsonValueKind.True)
39 return new BuildTestEnqueueResult(false, null, 5);
40
41 if (resp.TryGetProperty("accepted", out var accepted) && accepted.ValueKind == JsonValueKind.False)
42 {
43 var retry = resp.TryGetProperty("retry_after_seconds", out var r) && r.TryGetInt32(out var sec)
44 ? sec
45 : 5;
46 return new BuildTestEnqueueResult(false, null, retry);
47 }
48
49 var jobId = resp.TryGetProperty("job_id", out var j) ? j.GetString() : null;
50 if (string.IsNullOrWhiteSpace(jobId))
51 return new BuildTestEnqueueResult(false, null, 5);
52
53 _jobs[jobId] = new DaemonJobRecord();
54 return new BuildTestEnqueueResult(true, jobId, 0);
55 }
56 catch
57 {
58 return new BuildTestEnqueueResult(false, null, 5);
59 }
60 }
61
62 public async Task<string?> WaitForCompletionAsync(string jobId, CancellationToken cancellationToken)
63 {
64 if (!_jobs.ContainsKey(jobId))
65 return null;
66
67 var resp = await _client
68 .SendAsync(
69 new Dictionary<string, object?> { ["op"] = "wait", ["job_id"] = jobId },
70 cancellationToken)
71 .ConfigureAwait(false);
72
73 if (!resp.TryGetProperty("ok", out var ok) || ok.ValueKind != JsonValueKind.True)
74 return null;
75
76 var json = resp.TryGetProperty("result_json", out var r) ? r.GetString() : null;
77 if (_jobs.TryGetValue(jobId, out var record))
78 {
79 record.IsComplete = true;
80 record.ResultJson = json;
81 record.Success = TryReadSuccess(json);
82 }
83
84 _jobs.TryRemove(jobId, out _);
85 return json;
86 }
87
88 public object? GetJobStatus(string jobId)
89 {
90 if (!_jobs.ContainsKey(jobId))
91 return null;
92
93 try
94 {
95 var resp = _client
96 .SendAsync(
97 new Dictionary<string, object?> { ["op"] = "get_status", ["job_id"] = jobId },
98 CancellationToken.None)
99 .GetAwaiter()
100 .GetResult();
101
102 if (!resp.TryGetProperty("ok", out var ok) || ok.ValueKind != JsonValueKind.True)
103 return null;
104
105 if (!resp.TryGetProperty("status", out var status))
106 return null;
107
108 return JsonSerializer.Deserialize<object>(status.GetRawText());
109 }
110 catch
111 {
112 return null;
113 }
114 }
115
116 public object CancelJob(string jobId)
117 {
118 try
119 {
120 var resp = _client
121 .SendAsync(
122 new Dictionary<string, object?> { ["op"] = "cancel", ["job_id"] = jobId },
123 CancellationToken.None)
124 .GetAwaiter()
125 .GetResult();
126
127 var cancelled = resp.TryGetProperty("cancelled", out var c)
128 && c.ValueKind == JsonValueKind.True;
129
130 if (_jobs.TryGetValue(jobId, out var record))
131 {
132 record.IsComplete = true;
133 record.Success = false;
134 record.ResultJson = JsonSerializer.Serialize(new { success = false, message = "cancelled" });
135 }
136
137 _jobs.TryRemove(jobId, out _);
138 return new { cancelled };
139 }
140 catch
141 {
142 return new { cancelled = false };
143 }
144 }
145
146 public async ValueTask DisposeAsync() => await _client.DisposeAsync().ConfigureAwait(false);
147
148 private static Dictionary<string, object?> BuildEnqueuePayload(
149 BuildTestJobKind kind,
150 string path,
151 bool includeRawOutput,
152 int timeoutSeconds,
153 DotnetExecutionOptions dotnetOptions)
154 {
155 var payload = new Dictionary<string, object?>
156 {
157 ["op"] = "enqueue",
158 ["kind"] = kind == BuildTestJobKind.RunTests ? "test" : "build",
159 ["path"] = Path.GetFullPath(path),
160 ["include_raw_output"] = includeRawOutput,
161 ["timeout_seconds"] = timeoutSeconds,
162 };
163
164 if (!string.IsNullOrWhiteSpace(dotnetOptions.Filter))
165 payload["filter"] = dotnetOptions.Filter.Trim();
166
167 return payload;
168 }
169
170 private static bool TryReadSuccess(string? resultJson)
171 {
172 if (string.IsNullOrWhiteSpace(resultJson))
173 return false;
174
175 try
176 {
177 using var doc = JsonDocument.Parse(resultJson);
178 var root = doc.RootElement;
179 if (root.TryGetProperty("result", out var result)
180 && result.ValueKind == JsonValueKind.Object
181 && result.TryGetProperty("success", out var nested))
182 {
183 return nested.ValueKind == JsonValueKind.True;
184 }
185
186 if (root.TryGetProperty("success", out var direct))
187 return direct.ValueKind == JsonValueKind.True;
188 }
189 catch (JsonException)
190 {
191 //
192 }
193
194 return false;
195 }
196
197 private sealed class DaemonJobRecord
198 {
199 public bool IsComplete { get; set; }
200 public bool? Success { get; set; }
201 public string? ResultJson { get; set; }
202 }
203}
204
View only · write via MCP/CIDE