diff --git a/StabilityMatrix/PyVenvRunner.cs b/StabilityMatrix/PyVenvRunner.cs new file mode 100644 index 00000000..d30d8277 --- /dev/null +++ b/StabilityMatrix/PyVenvRunner.cs @@ -0,0 +1,92 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +namespace StabilityMatrix; + +/// +/// Python runner using a subprocess, mainly for venv support. +/// +public class PyVenvRunner: IDisposable +{ + public Process? Process { get; private set; } + private ProcessStartInfo StartInfo => new() + { + FileName = PythonPath, + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + public string RootPath { get; private set; } + + public event EventHandler OnStdoutUpdate; + + public PyVenvRunner(string path) + { + RootPath = path; + OnStdoutUpdate += (_, _) => { }; + } + + // Whether the activate script exists + public bool Exists() => System.IO.File.Exists(RootPath + "Scripts/activate"); + + /// + /// The path to the python executable. + /// + public string PythonPath => RootPath + @"\Scripts\python.exe"; + + /// + /// Configures the venv at path. + /// + public async Task Setup(bool existsOk = false) + { + if (!existsOk && Exists()) + { + throw new InvalidOperationException("Venv already exists"); + } + + // Create RootPath if it doesn't exist + if (!System.IO.Directory.Exists(RootPath)) + { + System.IO.Directory.CreateDirectory(RootPath); + } + + await PyRunner.Exec(@" +import venv +venv.EnvBuilder(with_pip=True).create(r""" + RootPath + @""") + "); + } + + public void RunDetached(string arguments) + { + this.Process = new Process(); + Process.StartInfo = StartInfo; + Process.StartInfo.Arguments = arguments; + + // Bind output data event + Process.OutputDataReceived += OnProcessOutputReceived; + + // Start the process + Process.Start(); + Process.BeginOutputReadLine(); + } + + /// + /// Called on process output data. + /// + private void OnProcessOutputReceived(object sender, DataReceivedEventArgs e) + { + var data = e.Data; + if (!string.IsNullOrEmpty(data)) + { + OnStdoutUpdate?.Invoke(this, data); + } + } + + public void Dispose() + { + Process?.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file