using System.Diagnostics.CodeAnalysis; using NLog; using Python.Runtime; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python.Interop; namespace StabilityMatrix.Core.Python; [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Global")] public record struct PyVersionInfo(int Major, int Minor, int Micro, string ReleaseLevel, int Serial); [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class PyRunner : IPyRunner { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); // Set by ISettingsManager.TryFindLibrary() public static DirectoryPath HomeDir { get; set; } = string.Empty; // This is same for all platforms public const string PythonDirName = "Python310"; public static string PythonDir => Path.Combine(GlobalConfig.LibraryDir, "Assets", PythonDirName); public static string PythonDllPath => Compat.Switch( (PlatformKind.Windows, Path.Combine(PythonDir, "python310.dll")), (PlatformKind.Linux, Path.Combine(PythonDir, "lib", "libpython3.10.so"))); public static string PythonExePath => Compat.Switch( (PlatformKind.Windows, Path.Combine(PythonDir, "python.exe")), (PlatformKind.Linux, Path.Combine(PythonDir, "bin", "python3"))); public static string PipExePath => Compat.Switch( (PlatformKind.Windows, Path.Combine(PythonDir, "Scripts", "pip.exe")), (PlatformKind.Linux, Path.Combine(PythonDir, "bin", "pip3"))); public static string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc"); public static string VenvPath => Path.Combine(PythonDir, "Scripts", "virtualenv" + Compat.ExeExtension); 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={PythonDir}", PythonDir.ToRepr()); // Append Python path to PATH var newEnvPath = Compat.GetEnvPathWithExtensions(PythonDir); Logger.Debug("Setting PATH={NewEnvPath}", newEnvPath.ToRepr()); Environment.SetEnvironmentVariable("PATH", newEnvPath, EnvironmentVariableTarget.Process); Logger.Info("Initializing Python runtime with DLL: {DllPath}", PythonDllPath); // Check PythonDLL exists if (!File.Exists(PythonDllPath)) { throw new FileNotFoundException("Python linked library not found", PythonDllPath); } Runtime.PythonDLL = PythonDllPath; PythonEngine.PythonHome = PythonDir; PythonEngine.Initialize(); PythonEngine.BeginAllowThreads(); // Redirect stdout and stderr StdOutStream = new PyIOStream(); StdErrStream = new PyIOStream(); await RunInThreadWithLock(() => { var sys = Py.Import("sys") as PyModule ?? throw new NullReferenceException("sys module not found"); sys.Set("stdout", StdOutStream); sys.Set("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.StartAnsiProcess(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.StartAnsiProcess(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 info = await Eval("tuple(__import__('sys').version_info)"); return new PyVersionInfo( info[0].As(), info[1].As(), info[2].As(), info[3].As(), info[4].As() ); } }