60 lines
2.0 KiB
C#
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());
|
|
}
|
|
}
|