From 54ac62048d4c5b17219a9ba70acb587f71e3ea29 Mon Sep 17 00:00:00 2001 From: Ionite Date: Sun, 28 May 2023 18:10:44 -0400 Subject: [PATCH] Extract PyRunner as singleton for DI --- StabilityMatrix/App.xaml.cs | 8 +++- StabilityMatrix/IPyRunner.cs | 47 +++++++++++++++++++ StabilityMatrix/PyIOStream.cs | 2 +- StabilityMatrix/PyRunner.cs | 42 ++++++++++------- .../ViewModels/InstallerViewModel.cs | 10 ++-- StabilityMatrix/ViewModels/LaunchViewModel.cs | 34 +++++++++----- .../ViewModels/SettingsViewModel.cs | 10 ++-- 7 files changed, 111 insertions(+), 42 deletions(-) create mode 100644 StabilityMatrix/IPyRunner.cs diff --git a/StabilityMatrix/App.xaml.cs b/StabilityMatrix/App.xaml.cs index 8335b1be..4dcc6170 100644 --- a/StabilityMatrix/App.xaml.cs +++ b/StabilityMatrix/App.xaml.cs @@ -38,19 +38,23 @@ namespace StabilityMatrix var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/StabilityMatrix/IPyRunner.cs b/StabilityMatrix/IPyRunner.cs new file mode 100644 index 00000000..a6f164b9 --- /dev/null +++ b/StabilityMatrix/IPyRunner.cs @@ -0,0 +1,47 @@ +using System.IO; +using System.Threading.Tasks; + +namespace StabilityMatrix; + +public interface IPyRunner +{ + /// + /// Initializes the Python runtime using the embedded dll. + /// Can be called with no effect after initialization. + /// + /// Thrown if Python DLL not found. + Task Initialize(); + + /// + /// One-time setup for get-pip + /// + Task SetupPip(); + + /// + /// Install a Python package with pip + /// + Task InstallPackage(string package); + + /// + /// Evaluate Python expression and return its value as a string + /// + /// + Task Eval(string expression); + + /// + /// Evaluate Python expression and return its value + /// + /// + Task Eval(string expression); + + /// + /// Execute Python code without returning a value + /// + /// + Task Exec(string code); + + /// + /// Return the Python version as a PyVersionInfo struct + /// + Task GetVersionInfo(); +} \ No newline at end of file diff --git a/StabilityMatrix/PyIOStream.cs b/StabilityMatrix/PyIOStream.cs index 0455e8bb..a0b771dc 100644 --- a/StabilityMatrix/PyIOStream.cs +++ b/StabilityMatrix/PyIOStream.cs @@ -10,7 +10,7 @@ namespace StabilityMatrix; /// Implement the interface of the sys.stdout redirection /// [SuppressMessage("ReSharper", "InconsistentNaming")] -internal class PyIOStream +public class PyIOStream { private readonly StringBuilder TextBuilder; private readonly StringWriter TextWriter; diff --git a/StabilityMatrix/PyRunner.cs b/StabilityMatrix/PyRunner.cs index 76f75898..cd406cdb 100644 --- a/StabilityMatrix/PyRunner.cs +++ b/StabilityMatrix/PyRunner.cs @@ -2,17 +2,19 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using NLog; using Python.Runtime; using StabilityMatrix.Helper; +using ILogger = NLog.ILogger; namespace StabilityMatrix; -internal record struct PyVersionInfo(int Major, int Minor, int Micro, string ReleaseLevel, int Serial); +public record struct PyVersionInfo(int Major, int Minor, int Micro, string ReleaseLevel, int Serial); -internal static class PyRunner +public class PyRunner : IPyRunner { - private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private readonly ILogger logger; private const string RelativeDllPath = @"Assets\Python310\python310.dll"; private const string RelativeExePath = @"Assets\Python310\python.exe"; @@ -22,26 +24,32 @@ internal static class PyRunner public static string ExePath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, RelativeExePath); public static string PipExePath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, RelativePipExePath); public static string GetPipPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, RelativeGetPipPath); - - public static PyIOStream? StdOutStream; - public static PyIOStream? StdErrStream; - + private static readonly SemaphoreSlim PyRunning = new(1, 1); + + private PyIOStream? StdOutStream; + private PyIOStream? StdErrStream; + + public PyRunner(ILogger logger) + { + this.logger = logger; + } /// /// Initializes the Python runtime using the embedded dll. /// Can be called with no effect after initialization. /// /// Thrown if Python DLL not found. - public static async Task Initialize() + public async Task Initialize() { if (PythonEngine.IsInitialized) return; - Logger.Trace($"Initializing Python runtime with DLL '{DllPath}'"); + logger.LogInformation("Initializing Python runtime with DLL: {DllPath}", DllPath); // Check PythonDLL exists if (!File.Exists(DllPath)) { + logger.LogError("Python DLL not found"); throw new FileNotFoundException("Python DLL not found", DllPath); } Runtime.PythonDLL = DllPath; @@ -62,7 +70,7 @@ internal static class PyRunner /// /// One-time setup for get-pip /// - public static async Task SetupPip() + public async Task SetupPip() { if (!File.Exists(GetPipPath)) { @@ -75,7 +83,7 @@ internal static class PyRunner /// /// Install a Python package with pip /// - public static async Task InstallPackage(string package) + public async Task InstallPackage(string package) { if (!File.Exists(PipExePath)) { @@ -92,7 +100,7 @@ internal static class PyRunner /// Time limit for waiting on PyRunning lock. /// Cancellation token. /// cancelToken was canceled, or waitTimeout expired. - private static async Task RunInThreadWithLock(Func func, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default) + private async Task RunInThreadWithLock(Func func, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default) { // Wait to acquire PyRunning lock await PyRunning.WaitAsync(cancelToken).ConfigureAwait(false); @@ -119,7 +127,7 @@ internal static class PyRunner /// Time limit for waiting on PyRunning lock. /// Cancellation token. /// cancelToken was canceled, or waitTimeout expired. - private static async Task RunInThreadWithLock(Action action, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default) + private async Task RunInThreadWithLock(Action action, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default) { // Wait to acquire PyRunning lock await PyRunning.WaitAsync(cancelToken).ConfigureAwait(false); @@ -143,7 +151,7 @@ internal static class PyRunner /// Evaluate Python expression and return its value as a string /// /// - public static async Task Eval(string expression) + public async Task Eval(string expression) { return await Eval(expression); } @@ -152,7 +160,7 @@ internal static class PyRunner /// Evaluate Python expression and return its value /// /// - public static Task Eval(string expression) + public Task Eval(string expression) { return RunInThreadWithLock(() => { @@ -165,7 +173,7 @@ internal static class PyRunner /// Execute Python code without returning a value /// /// - public static Task Exec(string code) + public Task Exec(string code) { return RunInThreadWithLock(() => { @@ -176,7 +184,7 @@ internal static class PyRunner /// /// Return the Python version as a PyVersionInfo struct /// - public static async Task GetVersionInfo() + public async Task GetVersionInfo() { var version = await RunInThreadWithLock(() => { diff --git a/StabilityMatrix/ViewModels/InstallerViewModel.cs b/StabilityMatrix/ViewModels/InstallerViewModel.cs index e6fa972a..a60d0f89 100644 --- a/StabilityMatrix/ViewModels/InstallerViewModel.cs +++ b/StabilityMatrix/ViewModels/InstallerViewModel.cs @@ -19,6 +19,7 @@ public partial class InstallerViewModel : ObservableObject { private readonly ILogger logger; private readonly ISettingsManager settingsManager; + private readonly IPyRunner pyRunner; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ProgressBarVisibility))] @@ -47,10 +48,11 @@ public partial class InstallerViewModel : ObservableObject private bool updateAvailable; public InstallerViewModel(ILogger logger, ISettingsManager settingsManager, - IPackageFactory packageFactory) + IPackageFactory packageFactory, IPyRunner pyRunner) { this.logger = logger; this.settingsManager = settingsManager; + this.pyRunner = pyRunner; ProgressText = "shrug"; InstallButtonText = "Install"; @@ -120,16 +122,16 @@ public partial class InstallerViewModel : ObservableObject await InstallPackage(); ProgressText = "Installing dependencies..."; - await PyRunner.Initialize(); + await pyRunner.Initialize(); if (!settingsManager.Settings.HasInstalledPip) { - await PyRunner.SetupPip(); + await pyRunner.SetupPip(); settingsManager.SetHasInstalledPip(true); } if (!settingsManager.Settings.HasInstalledVenv) { - await PyRunner.InstallPackage("virtualenv"); + await pyRunner.InstallPackage("virtualenv"); settingsManager.SetHasInstalledVenv(true); } diff --git a/StabilityMatrix/ViewModels/LaunchViewModel.cs b/StabilityMatrix/ViewModels/LaunchViewModel.cs index c8772713..7f352bbc 100644 --- a/StabilityMatrix/ViewModels/LaunchViewModel.cs +++ b/StabilityMatrix/ViewModels/LaunchViewModel.cs @@ -22,6 +22,8 @@ public partial class LaunchViewModel : ObservableObject private readonly IContentDialogService contentDialogService; private readonly LaunchOptionsDialogViewModel launchOptionsDialogViewModel; private readonly ILogger logger; + private readonly IPyRunner pyRunner; + private BasePackage? runningPackage; private bool clearingPackages = false; @@ -61,8 +63,10 @@ public partial class LaunchViewModel : ObservableObject IPackageFactory packageFactory, IContentDialogService contentDialogService, LaunchOptionsDialogViewModel launchOptionsDialogViewModel, - ILogger logger) + ILogger logger, + IPyRunner pyRunner) { + this.pyRunner = pyRunner; this.contentDialogService = contentDialogService; this.launchOptionsDialogViewModel = launchOptionsDialogViewModel; this.logger = logger; @@ -92,7 +96,7 @@ public partial class LaunchViewModel : ObservableObject return; } - await PyRunner.Initialize(); + await pyRunner.Initialize(); // Get path from package var packagePath = SelectedPackage.Path!; @@ -117,14 +121,14 @@ public partial class LaunchViewModel : ObservableObject var name = SelectedPackage?.Name; if (name == null) { - Debug.WriteLine($"Selected package is null"); + logger.LogWarning($"Selected package is null"); return; } var package = packageFactory.FindPackageByName(name); if (package == null) { - Debug.WriteLine($"Package {name} not found"); + logger.LogWarning("Package {Name} not found", name); return; } @@ -152,16 +156,20 @@ public partial class LaunchViewModel : ObservableObject public void OnLoaded() { LoadPackages(); - if (InstalledPackages.Any() && settingsManager.Settings.ActiveInstalledPackage != null) + lock (InstalledPackages) { - SelectedPackage = - InstalledPackages[ - InstalledPackages.IndexOf(InstalledPackages.FirstOrDefault(x => - x.Id == settingsManager.Settings.ActiveInstalledPackage))]; - } - else if (InstalledPackages.Any()) - { - SelectedPackage = InstalledPackages[0]; + // Skip if no packages + if (!InstalledPackages.Any()) + { + logger.LogTrace($"No packages for {nameof(LaunchViewModel)}"); + return; + } + var activePackageId = settingsManager.Settings.ActiveInstalledPackage; + if (activePackageId != null) + { + SelectedPackage = InstalledPackages.FirstOrDefault( + x => x.Id == activePackageId) ?? InstalledPackages[0]; + } } } diff --git a/StabilityMatrix/ViewModels/SettingsViewModel.cs b/StabilityMatrix/ViewModels/SettingsViewModel.cs index f858cb50..403029e9 100644 --- a/StabilityMatrix/ViewModels/SettingsViewModel.cs +++ b/StabilityMatrix/ViewModels/SettingsViewModel.cs @@ -17,6 +17,7 @@ namespace StabilityMatrix.ViewModels; public partial class SettingsViewModel : ObservableObject { private readonly ISettingsManager settingsManager; + private readonly IPyRunner pyRunner; public ObservableCollection AvailableThemes => new() { @@ -27,11 +28,12 @@ public partial class SettingsViewModel : ObservableObject private readonly IContentDialogService contentDialogService; private readonly IA3WebApi a3WebApi; - public SettingsViewModel(ISettingsManager settingsManager, IContentDialogService contentDialogService, IA3WebApi a3WebApi) + public SettingsViewModel(ISettingsManager settingsManager, IContentDialogService contentDialogService, IA3WebApi a3WebApi, IPyRunner pyRunner) { this.settingsManager = settingsManager; this.contentDialogService = contentDialogService; this.a3WebApi = a3WebApi; + this.pyRunner = pyRunner; SelectedTheme = settingsManager.Settings.Theme ?? "Dark"; } @@ -66,8 +68,8 @@ public partial class SettingsViewModel : ObservableObject public AsyncRelayCommand PythonVersionCommand => new(async () => { // Get python version - await PyRunner.Initialize(); - var result = await PyRunner.GetVersionInfo(); + await pyRunner.Initialize(); + var result = await pyRunner.GetVersionInfo(); // Show dialog box var dialog = contentDialogService.CreateDialog(); dialog.Title = "Python version info"; @@ -78,9 +80,7 @@ public partial class SettingsViewModel : ObservableObject public RelayCommand AddInstallationCommand => new(() => { - var initialDirectory = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}\\StabilityMatrix\\Packages"; // Show dialog box to choose a folder - var dialog = new VistaFolderBrowserDialog { Description = "Select a folder",