using NLog;
using Python.Runtime;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python.Interop;
namespace StabilityMatrix.Core.Python;
public record struct PyVersionInfo(int Major, int Minor, int Micro, string ReleaseLevel, int Serial);
public class PyRunner : IPyRunner
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
// Set by ISettingsManager.TryFindLibrary()
public static string HomeDir { get; set; } = string.Empty;
public static string PythonDir => Path.Combine(HomeDir, "Assets", "Python310");
public static string PythonDllPath => Path.Combine(PythonDir, "python310.dll");
public static string PythonExePath => Path.Combine(PythonDir, "python.exe");
public static string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc");
public static string PipExePath => Path.Combine(PythonDir, "Scripts", "pip.exe");
public static string VenvPath => Path.Combine(PythonDir, "Scripts", "virtualenv.exe");
public static bool PipInstalled => File.Exists(PipExePath);
public static bool VenvInstalled => File.Exists(VenvPath);
private static readonly SemaphoreSlim PyRunning = new(1, 1);
public PyIOStream? StdOutStream;
public PyIOStream? StdErrStream;
///
/// Initializes the Python runtime using the embedded dll.
/// Can be called with no effect after initialization.
///
/// Thrown if Python DLL not found.
public async Task Initialize()
{
if (PythonEngine.IsInitialized) return;
Logger.Info("Setting PYTHONHOME and PATH to {HomePath}", PythonDir);
Environment.SetEnvironmentVariable("PYTHONHOME", PythonDir, EnvironmentVariableTarget.Process);
// Get existing PATH
var currentEnvPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process);
// Append Python path to PATH
Environment.SetEnvironmentVariable("PATH", $"{PythonDir};{currentEnvPath}", EnvironmentVariableTarget.Process);
Logger.Info("Initializing Python runtime with DLL: {DllPath}", PythonDllPath);
// Check PythonDLL exists
if (!File.Exists(PythonDllPath))
{
Logger.Error("Python DLL not found");
throw new FileNotFoundException("Python DLL not found", PythonDllPath);
}
Runtime.PythonDLL = PythonDllPath;
PythonEngine.Initialize();
PythonEngine.BeginAllowThreads();
// Redirect stdout and stderr
StdOutStream = new PyIOStream();
StdErrStream = new PyIOStream();
await RunInThreadWithLock(() =>
{
dynamic sys = Py.Import("sys");
sys.stdout = StdOutStream;
sys.stderr = StdErrStream;
});
}
///
/// One-time setup for get-pip
///
public async Task SetupPip()
{
if (!File.Exists(GetPipPath))
{
throw new FileNotFoundException("get-pip not found", GetPipPath);
}
var p = ProcessRunner.StartProcess(PythonExePath, "-m get-pip");
await ProcessRunner.WaitForExitConditionAsync(p);
}
///
/// Install a Python package with pip
///
public async Task InstallPackage(string package)
{
if (!File.Exists(PipExePath))
{
throw new FileNotFoundException("pip not found", PipExePath);
}
var p = ProcessRunner.StartProcess(PipExePath, $"install {package}");
await ProcessRunner.WaitForExitConditionAsync(p);
}
///
/// Run a Function with PyRunning lock as a Task with GIL.
///
/// Function to run.
/// Time limit for waiting on PyRunning lock.
/// Cancellation token.
/// cancelToken was canceled, or waitTimeout expired.
public async Task RunInThreadWithLock(Func func, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default)
{
// Wait to acquire PyRunning lock
await PyRunning.WaitAsync(cancelToken).ConfigureAwait(false);
try
{
return await Task.Run(() =>
{
using (Py.GIL())
{
return func();
}
}, cancelToken);
}
finally
{
PyRunning.Release();
}
}
///
/// Run an Action with PyRunning lock as a Task with GIL.
///
/// Action to run.
/// Time limit for waiting on PyRunning lock.
/// Cancellation token.
/// cancelToken was canceled, or waitTimeout expired.
public async Task RunInThreadWithLock(Action action, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default)
{
// Wait to acquire PyRunning lock
await PyRunning.WaitAsync(cancelToken).ConfigureAwait(false);
try
{
await Task.Run(() =>
{
using (Py.GIL())
{
action();
}
}, cancelToken);
}
finally
{
PyRunning.Release();
}
}
///
/// Evaluate Python expression and return its value as a string
///
///
public async Task Eval(string expression)
{
return await Eval(expression);
}
///
/// Evaluate Python expression and return its value
///
///
public Task Eval(string expression)
{
return RunInThreadWithLock(() =>
{
using var scope = Py.CreateScope();
var result = scope.Eval(expression);
// For string, cast with __str__()
if (typeof(T) == typeof(string))
{
return result.GetAttr("__str__").Invoke().As();
}
return result.As();
});
}
///
/// Execute Python code without returning a value
///
///
public Task Exec(string code)
{
return RunInThreadWithLock(() =>
{
using var scope = Py.CreateScope();
scope.Exec(code);
});
}
///
/// Return the Python version as a PyVersionInfo struct
///
public async Task GetVersionInfo()
{
var version = await RunInThreadWithLock(() =>
{
dynamic info = PythonEngine.Eval("tuple(__import__('sys').version_info)");
return new PyVersionInfo(
info[0].As(),
info[1].As(),
info[2].As(),
info[3].As(),
info[4].As()
);
});
return version;
}
}