| 1 | using System.Text; |
| 2 | using System.Text.Json; |
| 3 | using DotnetDebug.Core; |
| 4 | |
| 5 | namespace DotnetDebugMcp; |
| 6 | |
| 7 | internal 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 | |