Forge
csharp5e452b8e
1using System.Text;
2using System.Text.Json;
3using DotnetDebug.Core;
4
5namespace DotnetDebugMcp;
6
7internal static class DebugControlToolHandlers
8{
9 internal static async Task<string> HandleDebugContinue(IReadOnlyDictionary<string, JsonElement> _)
10 {
11 var (client, threadId) = DapHelpers.GetSessionAndThreadId();
12 await client.ContinueAsync(threadId).ConfigureAwait(false);
13 return "# Continued execution.";
14 }
15
16 internal static async Task<string> HandleDebugStepOver(IReadOnlyDictionary<string, JsonElement> _)
17 {
18 try { await DebugSession.WaitForStoppedAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); }
19 catch (TimeoutException) { return "# Timeout (5s) waiting for execution to stop."; }
20 var (client, threadId) = DapHelpers.GetSessionAndThreadId();
21 try { await DapHelpers.WithRetryVoidAsync(() => client.NextAsync(threadId)).ConfigureAwait(false); }
22 catch (InvalidOperationException ex) { return "# " + ex.Message; }
23 return "# Step over sent; execution will stop at next line.";
24 }
25
26 internal static async Task<string> HandleDebugStepInto(IReadOnlyDictionary<string, JsonElement> _)
27 {
28 try { await DebugSession.WaitForStoppedAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); }
29 catch (TimeoutException) { return "# Timeout (5s) waiting for execution to stop."; }
30 var (client, threadId) = DapHelpers.GetSessionAndThreadId();
31 try { await DapHelpers.WithRetryVoidAsync(() => client.StepInAsync(threadId)).ConfigureAwait(false); }
32 catch (InvalidOperationException ex) { return "# " + ex.Message; }
33 return "# Step into sent; execution will stop inside the call.";
34 }
35
36 internal static async Task<string> HandleDebugStepOut(IReadOnlyDictionary<string, JsonElement> _)
37 {
38 try { await DebugSession.WaitForStoppedAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false); }
39 catch (TimeoutException) { return "# Timeout (5s) waiting for execution to stop."; }
40 var (client, threadId) = DapHelpers.GetSessionAndThreadId();
41 try { await DapHelpers.WithRetryVoidAsync(() => client.StepOutAsync(threadId)).ConfigureAwait(false); }
42 catch (InvalidOperationException ex) { return "# " + ex.Message; }
43 return "# Step out sent; execution will stop at caller.";
44 }
45
46 internal static async Task<string> HandleDebugStop(IReadOnlyDictionary<string, JsonElement> _)
47 {
48 var client = DebugSession.CurrentClient;
49 if (client == null)
50 return "# No active debug session; nothing to stop.";
51 var threadId = DebugSession.LastStoppedThreadId;
52 DebugSession.CurrentClient = null;
53 DebugSession.LastStoppedThreadId = 0;
54 if (threadId != 0)
55 try { await client.ContinueAsync(threadId).ConfigureAwait(false); } catch { /* целевой процесс продолжит выполнение перед отключением */ }
56 await client.DisposeAsync().ConfigureAwait(false);
57 return "# Debug session stopped; target resumed, client disposed.";
58 }
59
60 internal static async Task<string> HandleDebugStackTrace(IReadOnlyDictionary<string, JsonElement> _)
61 {
62 try
63 {
64 await DebugSession.WaitForStoppedAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
65 }
66 catch (TimeoutException)
67 {
68 return "# Timeout (5s) waiting for execution to stop. Run debug_continue and try again after the next break.";
69 }
70 var (client, threadId) = DapHelpers.GetSessionAndThreadId();
71 JsonElement? body;
72 try
73 {
74 body = await DapHelpers.WithRetryAsync(() => client.StackTraceAsync(threadId)).ConfigureAwait(false);
75 }
76 catch (InvalidOperationException ex)
77 {
78 return "# " + ex.Message;
79 }
80 if (body == null || !body.Value.TryGetProperty("stackFrames", out var frames))
81 return "# No stack frames.";
82 var sb = new StringBuilder();
83 sb.AppendLine("# Stack trace");
84 var i = 0;
85 foreach (var f in frames.EnumerateArray())
86 {
87 var name = f.TryGetProperty("name", out var n) ? n.GetString() : "?";
88 var line = f.TryGetProperty("line", out var ln) ? ln.GetInt32() : 0;
89 var path = "";
90 if (f.TryGetProperty("source", out var src) && src.TryGetProperty("path", out var p))
91 path = p.GetString() ?? "";
92 var id = f.TryGetProperty("id", out var idEl) ? idEl.GetInt32() : 0;
93 sb.AppendLine($" [{i}] {name} — {path}:{line} (id={id})");
94 i++;
95 }
96 return sb.ToString();
97 }
98
99 internal static async Task<string> HandleDebugVariables(IReadOnlyDictionary<string, JsonElement> args)
100 {
101 try
102 {
103 await DebugSession.WaitForStoppedAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
104 }
105 catch (TimeoutException)
106 {
107 return "# Timeout (5s) waiting for execution to stop. Run debug_continue and try again after the next break.";
108 }
109 var (client, threadId) = DapHelpers.GetSessionAndThreadId();
110 var frameIndex = 0;
111 if (args.TryGetValue("frame_index", out var fiEl) && fiEl.ValueKind == JsonValueKind.Number && fiEl.TryGetInt32(out var fi))
112 frameIndex = fi;
113 var fast = args.TryGetValue("fast", out var fastEl) && fastEl.ValueKind == JsonValueKind.True;
114 var maxDepthDefault = fast ? 0 : DapVariableExpansion.DefaultMaxDepth;
115 var maxChildrenDefault = fast ? 24 : DapVariableExpansion.DefaultMaxChildrenPerNode;
116 var maxDepth = McpArgumentHelpers.GetOptionalClampedInt32(
117 args,
118 "max_depth",
119 maxDepthDefault,
120 min: 0,
121 max: 32);
122 var maxChildren = McpArgumentHelpers.GetOptionalClampedInt32(
123 args,
124 "max_children_per_node",
125 maxChildrenDefault,
126 min: 1,
127 max: 256);
128 var timeBudgetMs = McpArgumentHelpers.GetOptionalClampedInt32(
129 args,
130 "time_budget_ms",
131 fast ? 700 : 1800,
132 min: 100,
133 max: 10000);
134 var formatJson = args.TryGetValue("format", out var fmtEl) &&
135 fmtEl.ValueKind == JsonValueKind.String &&
136 string.Equals(fmtEl.GetString(), "json", StringComparison.OrdinalIgnoreCase);
137 var jsonIndented = !args.TryGetValue("json_indented", out var jindEl) || jindEl.ValueKind != JsonValueKind.False;
138 using var budgetCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeBudgetMs));
139 var ct = budgetCts.Token;
140
141 JsonElement? stackBody;
142 try
143 {
144 stackBody = await DapHelpers.WithRetryAsync(() => client.StackTraceAsync(threadId)).ConfigureAwait(false);
145 }
146 catch (InvalidOperationException ex)
147 {
148 return "# " + ex.Message;
149 }
150 if (stackBody == null || !stackBody.Value.TryGetProperty("stackFrames", out var frames))
151 return "# No stack; run debug_stack_trace first or ensure stopped.";
152 var frameList = frames.EnumerateArray().ToList();
153 if (frameIndex < 0 || frameIndex >= frameList.Count)
154 return $"# frame_index {frameIndex} out of range (0..{frameList.Count - 1}).";
155 var frame = frameList[frameIndex];
156 if (!frame.TryGetProperty("id", out var idEl))
157 return "# Frame has no id.";
158 var frameId = idEl.GetInt32();
159 var scopeBlocks = new List<(string Name, JsonElement Variables)>();
160 var usedScopes = false;
161 try
162 {
163 var scopesBody = await DapHelpers.WithRetryAsync(() => client.ScopesAsync(frameId)).ConfigureAwait(false);
164 if (scopesBody != null && scopesBody.Value.TryGetProperty("scopes", out var scopesArr))
165 {
166 foreach (var scope in scopesArr.EnumerateArray())
167 {
168 if (!scope.TryGetProperty("variablesReference", out var vrefEl) || !vrefEl.TryGetInt32(out var vref) || vref == 0)
169 continue;
170 var scopeName = scope.TryGetProperty("name", out var sn) ? sn.GetString() : "?";
171 var varsBody = await DapHelpers.WithRetryAsync(() => client.VariablesAsync(vref)).ConfigureAwait(false);
172 if (varsBody == null || !varsBody.Value.TryGetProperty("variables", out var vars))
173 continue;
174 usedScopes = true;
175 scopeBlocks.Add((scopeName ?? "?", vars));
176 }
177 }
178 }
179 catch (InvalidOperationException)
180 {
181 // scopes не поддерживается — ниже direct variables
182 }
183
184 if (!usedScopes)
185 {
186 JsonElement? varsBody;
187 try
188 {
189 varsBody = await DapHelpers.WithRetryAsync(() => client.VariablesAsync(frameId)).ConfigureAwait(false);
190 }
191 catch (InvalidOperationException ex)
192 {
193 return "# " + ex.Message;
194 }
195 if (varsBody == null || !varsBody.Value.TryGetProperty("variables", out var vars))
196 return "# No variables for this frame (tried scopes and direct variables).";
197 scopeBlocks.Add(("variables", vars));
198 }
199
200 if (scopeBlocks.Count == 0)
201 return "# No variable scopes for this frame.";
202
203 var partial = false;
204 string? partialNote = null;
205
206 if (formatJson)
207 {
208 var built = new List<(string ScopeName, IReadOnlyList<DapVariableTreeNode> Roots)>(scopeBlocks.Count);
209 foreach (var (name, varEl) in scopeBlocks)
210 {
211 try
212 {
213 var tree = await DapVariableExpansion
214 .BuildExpandedTreeAsync(client, varEl, maxDepth, maxChildren, ct)
215 .ConfigureAwait(false);
216 built.Add((name, tree));
217 }
218 catch (OperationCanceledException)
219 {
220 partial = true;
221 partialNote = $"Stopped by time budget ({timeBudgetMs} ms). Use fast=true, lower max_depth/max_children_per_node, or inspect via debug_variable_children.";
222 break;
223 }
224 }
225
226 return DapVariableExpansion.SerializeFrameVariablesDocumentToJson(
227 frameIndex,
228 maxDepth,
229 maxChildren,
230 built,
231 partial,
232 partialNote,
233 jsonIndented);
234 }
235
236 var sb = new StringBuilder();
237 sb.AppendLine($"# Variables (frame {frameIndex})");
238 sb.AppendLine($"# (max_depth={maxDepth}, max_children_per_node={maxChildren}, time_budget_ms={timeBudgetMs}, fast={fast.ToString().ToLowerInvariant()})");
239 foreach (var (name, varEl) in scopeBlocks)
240 {
241 sb.AppendLine($"## {name}");
242 try
243 {
244 await DapVariableExpansion
245 .AppendExpandedVariablesAsync(
246 client,
247 sb,
248 varEl,
249 indent: " ",
250 depth: 0,
251 maxDepth,
252 maxChildren,
253 ct)
254 .ConfigureAwait(false);
255 }
256 catch (OperationCanceledException)
257 {
258 sb.AppendLine($"# Partial: stopped by time budget ({timeBudgetMs} ms).");
259 sb.AppendLine("# Tip: use fast=true, lower max_depth/max_children_per_node, and expand specific refs via debug_variable_children.");
260 break;
261 }
262 }
263
264 return sb.ToString();
265 }
266
267 /// <summary>Один уровень детей по <c>variablesReference</c> (без рекурсии); тяжёлый кадр — сначала <c>debug_variables</c> с format=json и малым max_depth, потом сюда.</summary>
268 internal static async Task<string> HandleDebugVariableChildren(IReadOnlyDictionary<string, JsonElement> args)
269 {
270 try
271 {
272 await DebugSession.WaitForStoppedAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
273 }
274 catch (TimeoutException)
275 {
276 return "# Timeout (5s) waiting for execution to stop. Run debug_continue and try again after the next break.";
277 }
278 if (!args.TryGetValue("variables_reference", out var vrefTop) || vrefTop.ValueKind != JsonValueKind.Number || !vrefTop.TryGetInt32(out var vref) || vref == 0)
279 return "# variables_reference is required and must be a non-zero integer (from debug_variables JSON or DAP).";
280
281 int? parentIndexed = null;
282 if (args.TryGetValue("indexed_variables", out var ivEl) && ivEl.ValueKind == JsonValueKind.Number && ivEl.TryGetInt32(out var iv) && iv >= 0)
283 parentIndexed = iv;
284 int? parentNamed = null;
285 if (args.TryGetValue("named_variables", out var nvEl) && nvEl.ValueKind == JsonValueKind.Number && nvEl.TryGetInt32(out var nv) && nv >= 0)
286 parentNamed = nv;
287 var maxChildren = McpArgumentHelpers.GetOptionalClampedInt32(
288 args,
289 "max_children",
290 DapVariableExpansion.DefaultMaxChildrenPerNode,
291 min: 1,
292 max: 256);
293 var jsonIndented = !args.TryGetValue("json_indented", out var jindEl) || jindEl.ValueKind != JsonValueKind.False;
294
295 var (client, _) = DapHelpers.GetSessionAndThreadId();
296 var body = await DapVariableExpansion
297 .FetchChildVariablesBodyAsync(
298 client,
299 vref,
300 parentNamed,
301 parentIndexed,
302 maxChildren,
303 CancellationToken.None)
304 .ConfigureAwait(false);
305 if (body == null || !body.Value.TryGetProperty("variables", out var vars))
306 return "# No children for this variables_reference (or DAP error).";
307 if (vars.GetArrayLength() == 0)
308 return DapVariableExpansion.SerializeVariableListToJson(Array.Empty<DapVariableTreeNode>(), jsonIndented);
309
310 var oneLevel = await DapVariableExpansion
311 .BuildExpandedTreeAsync(client, vars, maxDepth: 0, maxChildren, CancellationToken.None)
312 .ConfigureAwait(false);
313 return DapVariableExpansion.SerializeVariableListToJson(oneLevel, jsonIndented);
314 }
315}
316
View only · write via MCP/CIDE