-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathInlineProcessManager.cs
More file actions
185 lines (159 loc) · 5.54 KB
/
InlineProcessManager.cs
File metadata and controls
185 lines (159 loc) · 5.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
namespace ExcelConsole;
/// <summary>
/// Manages live subprocesses for inline command execution (i: cells pointing to r: cells).
/// On Windows, uses ConPTY (Pseudo Console) to capture output from interactive/TUI programs.
/// On Linux, falls back to stdout/stderr pipe redirection.
/// Thread-safe: output can be read from the UI thread while processes write from background threads.
/// </summary>
public class InlineProcessManager : IDisposable
{
private const int MaxOutputLines = 200;
private readonly ConcurrentDictionary<(int row, int col), ManagedProcess> _processes = new();
private class ManagedProcess : IDisposable
{
public string Command { get; set; } = "";
#if PLATFORM_WINDOWS
private ConPtyProcess? _pty;
public bool HasNewOutput => _pty?.HasNewOutput ?? false;
public bool HasExited => _pty?.HasExited ?? true;
public void StartConPty(string cmdText, int ptyCols = 120, int ptyRows = 30)
{
_pty = new ConPtyProcess();
if (!_pty.Start(cmdText, ptyCols, ptyRows))
{
_pty.Dispose();
_pty = null;
}
}
public string? GetOutput() => _pty?.GetOutput();
public void Dispose() => _pty?.Dispose();
#else
public Process? Process { get; set; }
public bool HasNewOutput { get; set; }
public bool HasExited => Process?.HasExited ?? true;
private readonly object _lock = new();
private readonly List<string> _outputLines = new();
public void AppendLine(string line)
{
lock (_lock)
{
_outputLines.Add(line);
while (_outputLines.Count > MaxOutputLines)
_outputLines.RemoveAt(0);
HasNewOutput = true;
}
}
public string? GetOutput()
{
lock (_lock)
{
HasNewOutput = false;
if (_outputLines.Count == 0) return null;
return string.Join("\n", _outputLines);
}
}
public void Dispose()
{
try
{
if (Process != null && !Process.HasExited)
Process.Kill(entireProcessTree: true);
}
catch { }
Process?.Dispose();
}
#endif
}
/// <summary>
/// Starts or restarts a process for the given pointer cell.
/// ptyCols/ptyRows set the pseudo console size to match the visual span.
/// </summary>
public void EnsureRunning(int pointerRow, int pointerCol, string command, int ptyCols = 120, int ptyRows = 30)
{
var key = (pointerRow, pointerCol);
if (_processes.TryGetValue(key, out var existing))
{
if (existing.Command == command)
return; // same command — keep it and its output
StopProcess(pointerRow, pointerCol);
}
var managed = new ManagedProcess { Command = command };
string cmdText = command;
if (CellPrefix.IsCommand(cmdText))
cmdText = cmdText[3..].Trim();
if (string.IsNullOrWhiteSpace(cmdText)) return;
#if PLATFORM_WINDOWS
managed.StartConPty(cmdText, ptyCols, ptyRows);
_processes[key] = managed;
#else
try
{
var psi = new ProcessStartInfo
{
FileName = "/bin/sh",
Arguments = $"-c \"{cmdText.Replace("\"", "\\\"")}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
};
var proc = new Process { StartInfo = psi, EnableRaisingEvents = true };
managed.Process = proc;
proc.OutputDataReceived += (_, e) =>
{
if (e.Data != null) managed.AppendLine(e.Data);
};
proc.ErrorDataReceived += (_, e) =>
{
if (e.Data != null) managed.AppendLine($"[err] {e.Data}");
};
proc.Start();
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
_processes[key] = managed;
}
catch (Exception ex)
{
managed.AppendLine($"[failed to start: {ex.Message}]");
_processes[key] = managed;
}
#endif
}
/// <summary>
/// Gets the current output buffer for a pointer cell. Returns null if no process.
/// </summary>
public string? GetOutput(int pointerRow, int pointerCol)
{
return _processes.TryGetValue((pointerRow, pointerCol), out var mp) ? mp.GetOutput() : null;
}
/// <summary>
/// Returns true if any managed process has new output or is still running.
/// </summary>
public bool HasAnyNewOutput()
{
foreach (var mp in _processes.Values)
if (mp.HasNewOutput || !mp.HasExited)
return true;
return false;
}
public void StopProcess(int pointerRow, int pointerCol)
{
if (_processes.TryRemove((pointerRow, pointerCol), out var mp))
mp.Dispose();
}
public void StopAll()
{
foreach (var key in _processes.Keys.ToList())
if (_processes.TryRemove(key, out var mp))
mp.Dispose();
}
public void Dispose()
{
StopAll();
}
}