journal-bot/tools/JournalBot.Shared/BotRunner.cs

60 lines
2.0 KiB
C#

using System.Diagnostics;
using System.Text;
namespace JournalBot.Shared;
public sealed record BotResult(int ExitCode, string Output);
public sealed class BotRunner
{
private readonly string _pythonExe;
private readonly string _workingDir;
public BotRunner(string pythonExe, string workingDir)
{
_pythonExe = pythonExe;
_workingDir = workingDir;
}
// Runs `python -m journal_bot <command>`.
public Task<BotResult> RunAsync(string command, CancellationToken ct = default)
=> RunRawAsync(new[] { "-m", "journal_bot", command }, ct);
// Runs python with arbitrary args (used by tests and RunAsync).
public async Task<BotResult> RunRawAsync(IReadOnlyList<string> args, CancellationToken ct = default)
{
var psi = new ProcessStartInfo
{
FileName = _pythonExe,
WorkingDirectory = _workingDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
};
foreach (var a in args) psi.ArgumentList.Add(a);
var sb = new StringBuilder();
var gate = new object();
using var proc = new Process { StartInfo = psi };
proc.OutputDataReceived += (_, e) => { if (e.Data is not null) { lock (gate) sb.AppendLine(e.Data); } };
proc.ErrorDataReceived += (_, e) => { if (e.Data is not null) { lock (gate) sb.AppendLine(e.Data); } };
if (!proc.Start())
throw new InvalidOperationException($"Failed to start process: {_pythonExe}");
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
try
{
await proc.WaitForExitAsync(ct);
}
catch (OperationCanceledException)
{
try { proc.Kill(entireProcessTree: true); } catch { /* already exited */ }
throw;
}
return new BotResult(proc.ExitCode, sb.ToString());
}
}