Forge
csharpdeeb25a2
1using System.Diagnostics;
2using System.Threading.Channels;
3using CascadeIDE.Contracts;
4
5namespace CascadeIDE.Features.Build.DataAcquisition;
6
7/// <summary>
8/// DAL: запуск внешнего <c>dotnet</c> CLI в рабочем каталоге (процесс, чтение stdout/stderr, опционально потоковая передача).
9/// </summary>
10/// <inheritdoc cref="IDotnetCommandRunner" />
11[IoBoundary]
12public sealed class DotnetCommandRunner : IDotnetCommandRunner
13{
14 /// <summary>
15 /// Safety cap for accumulated process output. Keeps last N chars.
16 /// </summary>
17 public const int MaxOutputChars = 250_000;
18
19 public async Task<(bool Success, int ExitCode, string Output)> RunAsync(
20 IReadOnlyList<string> args,
21 string workingDirectory,
22 CancellationToken cancellationToken = default)
23 {
24 if (string.IsNullOrWhiteSpace(workingDirectory) || !Directory.Exists(workingDirectory))
25 return (false, -1, "Working directory is not available.");
26
27 try
28 {
29 var psi = new ProcessStartInfo("dotnet")
30 {
31 RedirectStandardOutput = true,
32 RedirectStandardError = true,
33 UseShellExecute = false,
34 CreateNoWindow = true,
35 WorkingDirectory = workingDirectory
36 };
37 DotnetProcessIoEncoding.ApplyUtf8(psi);
38 foreach (var arg in args)
39 psi.ArgumentList.Add(arg);
40
41 using var process = Process.Start(psi);
42 if (process is null)
43 return (false, -1, "Failed to start dotnet process.");
44
45 var acc = new OutputAccumulator(MaxOutputChars);
46
47 async Task PumpAsync(StreamReader reader)
48 {
49 var buffer = new char[4096];
50 while (true)
51 {
52 var read = await reader.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
53 if (read <= 0)
54 break;
55 acc.Append(buffer.AsSpan(0, read));
56 }
57 }
58
59 var pumpOut = PumpAsync(process.StandardOutput);
60 var pumpErr = PumpAsync(process.StandardError);
61
62 await Task.WhenAll(pumpOut, pumpErr).ConfigureAwait(false);
63 await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
64
65 var output = acc.ToStringAndTrim();
66 return (process.ExitCode == 0, process.ExitCode, output);
67 }
68 catch (Exception ex)
69 {
70 return (false, -1, ex.Message);
71 }
72 }
73
74 /// <inheritdoc />
75 public async Task<(bool Success, int ExitCode)> RunWithChunkWriterAsync(
76 IReadOnlyList<string> args,
77 string workingDirectory,
78 ChannelWriter<string> chunkWriter,
79 CancellationToken cancellationToken = default)
80 {
81 ArgumentNullException.ThrowIfNull(chunkWriter);
82
83 if (string.IsNullOrWhiteSpace(workingDirectory) || !Directory.Exists(workingDirectory))
84 {
85 chunkWriter.TryComplete();
86 return (false, -1);
87 }
88
89 try
90 {
91 var psi = new ProcessStartInfo("dotnet")
92 {
93 RedirectStandardOutput = true,
94 RedirectStandardError = true,
95 UseShellExecute = false,
96 CreateNoWindow = true,
97 WorkingDirectory = workingDirectory
98 };
99 DotnetProcessIoEncoding.ApplyUtf8(psi);
100 foreach (var arg in args)
101 psi.ArgumentList.Add(arg);
102
103 using var process = Process.Start(psi);
104 if (process is null)
105 {
106 chunkWriter.TryComplete();
107 return (false, -1);
108 }
109
110 static void TryKillEntireProcessTree(Process? p)
111 {
112 if (p is null) return;
113 try
114 {
115 if (!p.HasExited)
116 p.Kill(true);
117 }
118 catch
119 {
120 // ignore
121 }
122 }
123
124 using (cancellationToken.Register(() => TryKillEntireProcessTree(process), useSynchronizationContext: false))
125 {
126 async Task PumpAsync(StreamReader reader)
127 {
128 var buffer = new char[4096];
129 while (true)
130 {
131 var read = await reader.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
132 if (read <= 0)
133 break;
134 var chunk = new string(buffer, 0, read);
135 await chunkWriter.WriteAsync(chunk, cancellationToken).ConfigureAwait(false);
136 }
137 }
138
139 var pumpOut = PumpAsync(process.StandardOutput);
140 var pumpErr = PumpAsync(process.StandardError);
141 try
142 {
143 await Task.WhenAll(pumpOut, pumpErr).ConfigureAwait(false);
144 }
145 catch (Exception ex)
146 {
147 TryKillEntireProcessTree(process);
148 try
149 {
150 chunkWriter.TryComplete(ex);
151 }
152 catch
153 {
154 // channel may be closed
155 }
156 return (false, -1);
157 }
158
159 await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
160 return (process.ExitCode == 0, process.ExitCode);
161 }
162 }
163 catch (Exception ex)
164 {
165 try
166 {
167 chunkWriter.TryComplete(ex);
168 }
169 catch
170 {
171 // channel may be closed
172 }
173 return (false, -1);
174 }
175 finally
176 {
177 try
178 {
179 _ = chunkWriter.TryComplete();
180 }
181 catch
182 {
183 // ignore
184 }
185 }
186 }
187}
188
View only · write via MCP/CIDE