Forge
csharpdeeb25a2
1using System.Diagnostics;
2using System.Text;
3using CascadeIDE.Contracts;
4
5namespace CascadeIDE.Features.Terminal.DataAcquisition;
6
7[IoBoundary]
8internal sealed class RedirectedIntegratedShellSession(ShellLaunchConfiguration launch) : IIntegratedShellSession
9{
10 private Process? _process;
11 private CancellationTokenSource? _pumpCancellation;
12
13 public event Action<byte[]>? DataReceived;
14
15 public event Action<int>? Exited;
16
17 public void Start()
18 {
19 var startInfo = new ProcessStartInfo
20 {
21 FileName = launch.FileName,
22 RedirectStandardInput = true,
23 RedirectStandardOutput = true,
24 RedirectStandardError = true,
25 StandardInputEncoding = Encoding.UTF8,
26 StandardOutputEncoding = Encoding.UTF8,
27 StandardErrorEncoding = Encoding.UTF8,
28 UseShellExecute = false,
29 CreateNoWindow = true,
30 WorkingDirectory = launch.WorkingDirectory,
31 };
32
33 foreach (var argument in launch.Arguments)
34 startInfo.ArgumentList.Add(argument);
35
36 startInfo.Environment["TERM"] = "xterm-256color";
37 startInfo.Environment["COLORTERM"] = "truecolor";
38 IntegratedShellLaunch.ApplyUtf8ConsoleEnvironment(startInfo.Environment);
39
40 _process = new Process
41 {
42 StartInfo = startInfo,
43 EnableRaisingEvents = true,
44 };
45 _process.Exited += OnProcessExited;
46 _process.Start();
47
48 _pumpCancellation = new CancellationTokenSource();
49 _ = PumpOutputAsync(_process.StandardOutput.BaseStream, _pumpCancellation.Token);
50 _ = PumpOutputAsync(_process.StandardError.BaseStream, _pumpCancellation.Token);
51 }
52
53 public void Send(byte[] input)
54 {
55 try
56 {
57 var inputStream = _process?.StandardInput.BaseStream;
58 if (inputStream?.CanWrite != true)
59 return;
60
61 var normalizedInput = IntegratedShellLaunch.NormalizeStandardInput(input, launch.DisplayName);
62 inputStream.Write(normalizedInput, 0, normalizedInput.Length);
63 inputStream.Flush();
64 }
65 catch (IOException)
66 {
67 }
68 catch (ObjectDisposedException)
69 {
70 }
71 }
72
73 public void Resize(int cols, int rows)
74 {
75 }
76
77 public void Dispose()
78 {
79 if (_process is not null)
80 _process.Exited -= OnProcessExited;
81
82 _pumpCancellation?.Cancel();
83 _pumpCancellation?.Dispose();
84 _pumpCancellation = null;
85
86 try
87 {
88 _process?.StandardInput.Close();
89 }
90 catch (IOException)
91 {
92 }
93 catch (ObjectDisposedException)
94 {
95 }
96
97 if (_process is { HasExited: false })
98 _process.Kill(entireProcessTree: true);
99
100 _process?.Dispose();
101 _process = null;
102 }
103
104 private async Task PumpOutputAsync(Stream stream, CancellationToken cancellationToken)
105 {
106 var buffer = new byte[4096];
107 try
108 {
109 while (!cancellationToken.IsCancellationRequested)
110 {
111 var bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
112 if (bytesRead == 0)
113 break;
114
115 DataReceived?.Invoke(buffer.AsSpan(0, bytesRead).ToArray());
116 }
117 }
118 catch (OperationCanceledException)
119 {
120 }
121 catch (ObjectDisposedException)
122 {
123 }
124 catch (IOException)
125 {
126 }
127 }
128
129 private void OnProcessExited(object? sender, EventArgs e) =>
130 Exited?.Invoke(_process?.ExitCode ?? 0);
131}
132
View only · write via MCP/CIDE