Forge
csharp8f5a77c3
1using System.Collections.Concurrent;
2using System.Diagnostics;
3using System.Text;
4using System.Text.Json;
5using System.Text.Json.Nodes;
6
7#nullable enable
8
9namespace CascadeIDE.Services.Lsp;
10
11/// <summary>
12/// Общий слой LSP по stdio: Content-Length framing, корреляция <c>id</c> запрос/ответ,
13/// диспетчеризация <c>method</c> без <c>id</c> (нотификации). Процесс и stdin/stdout — снаружи.
14/// </summary>
15public sealed class LspStdioSession : IDisposable
16{
17 private readonly ConcurrentDictionary<int, TaskCompletionSource<JsonDocument?>> _pending = new();
18 private readonly Action<string, JsonElement> _onNotification;
19 private Process? _process;
20 private Stream? _stdIn;
21 private CancellationTokenSource? _runCts;
22 private Task? _readLoop;
23 private int _nextRequestId = 1;
24 private volatile bool _disposed;
25
26 /// <param name="onNotification"><paramref name="method"/> LSP, <paramref name="params"/> — элемент params.</param>
27 public LspStdioSession(Process process, Stream stdIn, Action<string, JsonElement> onNotification)
28 {
29 _process = process;
30 _stdIn = stdIn;
31 _onNotification = onNotification;
32 }
33
34 public Process? Process => _process;
35
36 public Stream? StdIn => _stdIn;
37
38 /// <summary>Следующий положительный id для <c>jsonrpc</c> request (потокобезопасно).</summary>
39 public int AllocateRequestId() => Interlocked.Increment(ref _nextRequestId);
40
41 public void StartReadLoop(CancellationToken parentCt)
42 {
43 if (_process is null || _stdIn is null)
44 return;
45 _runCts = CancellationTokenSource.CreateLinkedTokenSource(parentCt);
46 var token = _runCts.Token;
47 _ = Task.Run(() => DrainStdErrAsync(_process.StandardError, token), token);
48 _readLoop = Task.Run(() => ReadLoopAsync(_process.StandardOutput.BaseStream, token), token);
49 }
50
51 public async Task<JsonDocument?> SendRequestAsync(JsonObject rpcRequest, int requestId, TimeSpan timeout, CancellationToken ct)
52 {
53 if (_stdIn is null || _disposed)
54 return null;
55 var tcs = new TaskCompletionSource<JsonDocument?>(TaskCreationOptions.RunContinuationsAsynchronously);
56 _pending[requestId] = tcs;
57 try
58 {
59 var bytes = Encoding.UTF8.GetBytes(rpcRequest.ToJsonString());
60 await LspStdioFraming.WriteMessageAsync(_stdIn, bytes, ct).ConfigureAwait(false);
61 return await tcs.Task.WaitAsync(timeout, ct).ConfigureAwait(false);
62 }
63 catch
64 {
65 _pending.TryRemove(requestId, out _);
66 return null;
67 }
68 }
69
70 public async Task SendEnvelopeAsync(JsonObject rpcMessage, CancellationToken ct)
71 {
72 if (_stdIn is null || _disposed)
73 return;
74 var bytes = Encoding.UTF8.GetBytes(rpcMessage.ToJsonString());
75 await LspStdioFraming.WriteMessageAsync(_stdIn, bytes, ct).ConfigureAwait(false);
76 }
77
78 public async Task SendRawUtf8Async(ReadOnlyMemory<byte> utf8Body, CancellationToken ct)
79 {
80 if (_stdIn is null || _disposed)
81 return;
82 await LspStdioFraming.WriteMessageAsync(_stdIn, utf8Body, ct).ConfigureAwait(false);
83 }
84
85 private async Task ReadLoopAsync(Stream stdout, CancellationToken ct)
86 {
87 try
88 {
89 while (!ct.IsCancellationRequested)
90 {
91 JsonDocument? doc;
92 try
93 {
94 doc = await LspStdioFraming.ReadMessageAsync(stdout, ct).ConfigureAwait(false);
95 }
96 catch
97 {
98 break;
99 }
100
101 if (doc is null)
102 break;
103
104 using (doc)
105 {
106 var root = doc.RootElement;
107 if (root.TryGetProperty("id", out var idEl) && idEl.ValueKind == JsonValueKind.Number)
108 {
109 var id = idEl.GetInt32();
110 if (root.TryGetProperty("result", out _))
111 {
112 if (_pending.TryRemove(id, out var tcs))
113 {
114 var copy = JsonDocument.Parse(root.GetRawText());
115 tcs.TrySetResult(copy);
116 }
117
118 continue;
119 }
120
121 if (root.TryGetProperty("error", out var errorEl))
122 {
123 if (_pending.TryRemove(id, out var tcs))
124 tcs.TrySetException(new InvalidOperationException(errorEl.GetRawText()));
125 continue;
126 }
127 }
128
129 if (root.TryGetProperty("method", out var methodEl))
130 {
131 var method = methodEl.GetString();
132 if (string.IsNullOrEmpty(method))
133 continue;
134 if (!root.TryGetProperty("params", out var p))
135 continue;
136 try
137 {
138 _onNotification(method, p);
139 }
140 catch
141 {
142 // не рвём read loop из-за обработчика
143 }
144 }
145 }
146 }
147 }
148 catch
149 {
150 // ignored
151 }
152 finally
153 {
154 foreach (var kv in _pending)
155 {
156 if (_pending.TryRemove(kv.Key, out var tcs))
157 tcs.TrySetCanceled(ct);
158 }
159 }
160 }
161
162 private static async Task DrainStdErrAsync(StreamReader err, CancellationToken ct)
163 {
164 try
165 {
166 while (!ct.IsCancellationRequested)
167 {
168 var line = await err.ReadLineAsync(ct).ConfigureAwait(false);
169 if (line is null)
170 break;
171 Debug.WriteLine("[lsp stderr] " + line);
172 }
173 }
174 catch
175 {
176 // ignored
177 }
178 }
179
180 public void Dispose()
181 {
182 if (_disposed)
183 return;
184 _disposed = true;
185
186 try
187 {
188 _runCts?.Cancel();
189 }
190 catch { }
191
192 try
193 {
194 _stdIn?.Dispose();
195 }
196 catch { }
197
198 _stdIn = null;
199
200 try
201 {
202 if (_process is { HasExited: false })
203 {
204 _process.Kill(entireProcessTree: true);
205 _process.WaitForExit(2000);
206 }
207 }
208 catch { }
209
210 try
211 {
212 _process?.Dispose();
213 }
214 catch { }
215
216 _process = null;
217 _runCts?.Dispose();
218 _runCts = null;
219 }
220}
221
View only · write via MCP/CIDE