Browse Source

Extract PyRunner as singleton for DI

pull/5/head
Ionite 1 year ago
parent
commit
54ac62048d
No known key found for this signature in database
  1. 8
      StabilityMatrix/App.xaml.cs
  2. 47
      StabilityMatrix/IPyRunner.cs
  3. 2
      StabilityMatrix/PyIOStream.cs
  4. 42
      StabilityMatrix/PyRunner.cs
  5. 10
      StabilityMatrix/ViewModels/InstallerViewModel.cs
  6. 34
      StabilityMatrix/ViewModels/LaunchViewModel.cs
  7. 10
      StabilityMatrix/ViewModels/SettingsViewModel.cs

8
StabilityMatrix/App.xaml.cs

@ -38,19 +38,23 @@ namespace StabilityMatrix
var serviceCollection = new ServiceCollection(); var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IPageService, PageService>(); serviceCollection.AddSingleton<IPageService, PageService>();
serviceCollection.AddSingleton<IContentDialogService, ContentDialogService>();
serviceCollection.AddSingleton<IPackageFactory, PackageFactory>();
serviceCollection.AddSingleton<IPyRunner, PyRunner>();
serviceCollection.AddTransient<MainWindow>(); serviceCollection.AddTransient<MainWindow>();
serviceCollection.AddTransient<SettingsPage>(); serviceCollection.AddTransient<SettingsPage>();
serviceCollection.AddTransient<LaunchPage>(); serviceCollection.AddTransient<LaunchPage>();
serviceCollection.AddTransient<InstallPage>(); serviceCollection.AddTransient<InstallPage>();
serviceCollection.AddTransient<TextToImagePage>(); serviceCollection.AddTransient<TextToImagePage>();
serviceCollection.AddTransient<MainWindowViewModel>(); serviceCollection.AddTransient<MainWindowViewModel>();
serviceCollection.AddSingleton<SettingsViewModel>(); serviceCollection.AddSingleton<SettingsViewModel>();
serviceCollection.AddSingleton<LaunchViewModel>(); serviceCollection.AddSingleton<LaunchViewModel>();
serviceCollection.AddSingleton<InstallerViewModel>(); serviceCollection.AddSingleton<InstallerViewModel>();
serviceCollection.AddSingleton<TextToImageViewModel>(); serviceCollection.AddSingleton<TextToImageViewModel>();
serviceCollection.AddSingleton<LaunchOptionsDialogViewModel>(); serviceCollection.AddSingleton<LaunchOptionsDialogViewModel>();
serviceCollection.AddSingleton<IContentDialogService, ContentDialogService>();
serviceCollection.AddSingleton<IPackageFactory, PackageFactory>();
serviceCollection.AddSingleton<BasePackage, A3WebUI>(); serviceCollection.AddSingleton<BasePackage, A3WebUI>();
serviceCollection.AddSingleton<BasePackage, DankDiffusion>(); serviceCollection.AddSingleton<BasePackage, DankDiffusion>();
serviceCollection.AddSingleton<ISnackbarService, SnackbarService>(); serviceCollection.AddSingleton<ISnackbarService, SnackbarService>();

47
StabilityMatrix/IPyRunner.cs

@ -0,0 +1,47 @@
using System.IO;
using System.Threading.Tasks;
namespace StabilityMatrix;
public interface IPyRunner
{
/// <summary>
/// Initializes the Python runtime using the embedded dll.
/// Can be called with no effect after initialization.
/// </summary>
/// <exception cref="FileNotFoundException">Thrown if Python DLL not found.</exception>
Task Initialize();
/// <summary>
/// One-time setup for get-pip
/// </summary>
Task SetupPip();
/// <summary>
/// Install a Python package with pip
/// </summary>
Task InstallPackage(string package);
/// <summary>
/// Evaluate Python expression and return its value as a string
/// </summary>
/// <param name="expression"></param>
Task<string> Eval(string expression);
/// <summary>
/// Evaluate Python expression and return its value
/// </summary>
/// <param name="expression"></param>
Task<T> Eval<T>(string expression);
/// <summary>
/// Execute Python code without returning a value
/// </summary>
/// <param name="code"></param>
Task Exec(string code);
/// <summary>
/// Return the Python version as a PyVersionInfo struct
/// </summary>
Task<PyVersionInfo> GetVersionInfo();
}

2
StabilityMatrix/PyIOStream.cs

@ -10,7 +10,7 @@ namespace StabilityMatrix;
/// Implement the interface of the sys.stdout redirection /// Implement the interface of the sys.stdout redirection
/// </summary> /// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
internal class PyIOStream public class PyIOStream
{ {
private readonly StringBuilder TextBuilder; private readonly StringBuilder TextBuilder;
private readonly StringWriter TextWriter; private readonly StringWriter TextWriter;

42
StabilityMatrix/PyRunner.cs

@ -2,17 +2,19 @@
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NLog; using NLog;
using Python.Runtime; using Python.Runtime;
using StabilityMatrix.Helper; using StabilityMatrix.Helper;
using ILogger = NLog.ILogger;
namespace StabilityMatrix; 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<PyRunner> logger;
private const string RelativeDllPath = @"Assets\Python310\python310.dll"; private const string RelativeDllPath = @"Assets\Python310\python310.dll";
private const string RelativeExePath = @"Assets\Python310\python.exe"; 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 ExePath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, RelativeExePath);
public static string PipExePath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, RelativePipExePath); public static string PipExePath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, RelativePipExePath);
public static string GetPipPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, RelativeGetPipPath); 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 static readonly SemaphoreSlim PyRunning = new(1, 1);
private PyIOStream? StdOutStream;
private PyIOStream? StdErrStream;
public PyRunner(ILogger<PyRunner> logger)
{
this.logger = logger;
}
/// <summary> /// <summary>
/// Initializes the Python runtime using the embedded dll. /// Initializes the Python runtime using the embedded dll.
/// Can be called with no effect after initialization. /// Can be called with no effect after initialization.
/// </summary> /// </summary>
/// <exception cref="FileNotFoundException">Thrown if Python DLL not found.</exception> /// <exception cref="FileNotFoundException">Thrown if Python DLL not found.</exception>
public static async Task Initialize() public async Task Initialize()
{ {
if (PythonEngine.IsInitialized) return; if (PythonEngine.IsInitialized) return;
Logger.Trace($"Initializing Python runtime with DLL '{DllPath}'"); logger.LogInformation("Initializing Python runtime with DLL: {DllPath}", DllPath);
// Check PythonDLL exists // Check PythonDLL exists
if (!File.Exists(DllPath)) if (!File.Exists(DllPath))
{ {
logger.LogError("Python DLL not found");
throw new FileNotFoundException("Python DLL not found", DllPath); throw new FileNotFoundException("Python DLL not found", DllPath);
} }
Runtime.PythonDLL = DllPath; Runtime.PythonDLL = DllPath;
@ -62,7 +70,7 @@ internal static class PyRunner
/// <summary> /// <summary>
/// One-time setup for get-pip /// One-time setup for get-pip
/// </summary> /// </summary>
public static async Task SetupPip() public async Task SetupPip()
{ {
if (!File.Exists(GetPipPath)) if (!File.Exists(GetPipPath))
{ {
@ -75,7 +83,7 @@ internal static class PyRunner
/// <summary> /// <summary>
/// Install a Python package with pip /// Install a Python package with pip
/// </summary> /// </summary>
public static async Task InstallPackage(string package) public async Task InstallPackage(string package)
{ {
if (!File.Exists(PipExePath)) if (!File.Exists(PipExePath))
{ {
@ -92,7 +100,7 @@ internal static class PyRunner
/// <param name="waitTimeout">Time limit for waiting on PyRunning lock.</param> /// <param name="waitTimeout">Time limit for waiting on PyRunning lock.</param>
/// <param name="cancelToken">Cancellation token.</param> /// <param name="cancelToken">Cancellation token.</param>
/// <exception cref="OperationCanceledException">cancelToken was canceled, or waitTimeout expired.</exception> /// <exception cref="OperationCanceledException">cancelToken was canceled, or waitTimeout expired.</exception>
private static async Task<T> RunInThreadWithLock<T>(Func<T> func, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default) private async Task<T> RunInThreadWithLock<T>(Func<T> func, TimeSpan? waitTimeout = null, CancellationToken cancelToken = default)
{ {
// Wait to acquire PyRunning lock // Wait to acquire PyRunning lock
await PyRunning.WaitAsync(cancelToken).ConfigureAwait(false); await PyRunning.WaitAsync(cancelToken).ConfigureAwait(false);
@ -119,7 +127,7 @@ internal static class PyRunner
/// <param name="waitTimeout">Time limit for waiting on PyRunning lock.</param> /// <param name="waitTimeout">Time limit for waiting on PyRunning lock.</param>
/// <param name="cancelToken">Cancellation token.</param> /// <param name="cancelToken">Cancellation token.</param>
/// <exception cref="OperationCanceledException">cancelToken was canceled, or waitTimeout expired.</exception> /// <exception cref="OperationCanceledException">cancelToken was canceled, or waitTimeout expired.</exception>
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 // Wait to acquire PyRunning lock
await PyRunning.WaitAsync(cancelToken).ConfigureAwait(false); await PyRunning.WaitAsync(cancelToken).ConfigureAwait(false);
@ -143,7 +151,7 @@ internal static class PyRunner
/// Evaluate Python expression and return its value as a string /// Evaluate Python expression and return its value as a string
/// </summary> /// </summary>
/// <param name="expression"></param> /// <param name="expression"></param>
public static async Task<string> Eval(string expression) public async Task<string> Eval(string expression)
{ {
return await Eval<string>(expression); return await Eval<string>(expression);
} }
@ -152,7 +160,7 @@ internal static class PyRunner
/// Evaluate Python expression and return its value /// Evaluate Python expression and return its value
/// </summary> /// </summary>
/// <param name="expression"></param> /// <param name="expression"></param>
public static Task<T> Eval<T>(string expression) public Task<T> Eval<T>(string expression)
{ {
return RunInThreadWithLock(() => return RunInThreadWithLock(() =>
{ {
@ -165,7 +173,7 @@ internal static class PyRunner
/// Execute Python code without returning a value /// Execute Python code without returning a value
/// </summary> /// </summary>
/// <param name="code"></param> /// <param name="code"></param>
public static Task Exec(string code) public Task Exec(string code)
{ {
return RunInThreadWithLock(() => return RunInThreadWithLock(() =>
{ {
@ -176,7 +184,7 @@ internal static class PyRunner
/// <summary> /// <summary>
/// Return the Python version as a PyVersionInfo struct /// Return the Python version as a PyVersionInfo struct
/// </summary> /// </summary>
public static async Task<PyVersionInfo> GetVersionInfo() public async Task<PyVersionInfo> GetVersionInfo()
{ {
var version = await RunInThreadWithLock(() => var version = await RunInThreadWithLock(() =>
{ {

10
StabilityMatrix/ViewModels/InstallerViewModel.cs

@ -19,6 +19,7 @@ public partial class InstallerViewModel : ObservableObject
{ {
private readonly ILogger<InstallerViewModel> logger; private readonly ILogger<InstallerViewModel> logger;
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly IPyRunner pyRunner;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(ProgressBarVisibility))] [NotifyPropertyChangedFor(nameof(ProgressBarVisibility))]
@ -47,10 +48,11 @@ public partial class InstallerViewModel : ObservableObject
private bool updateAvailable; private bool updateAvailable;
public InstallerViewModel(ILogger<InstallerViewModel> logger, ISettingsManager settingsManager, public InstallerViewModel(ILogger<InstallerViewModel> logger, ISettingsManager settingsManager,
IPackageFactory packageFactory) IPackageFactory packageFactory, IPyRunner pyRunner)
{ {
this.logger = logger; this.logger = logger;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.pyRunner = pyRunner;
ProgressText = "shrug"; ProgressText = "shrug";
InstallButtonText = "Install"; InstallButtonText = "Install";
@ -120,16 +122,16 @@ public partial class InstallerViewModel : ObservableObject
await InstallPackage(); await InstallPackage();
ProgressText = "Installing dependencies..."; ProgressText = "Installing dependencies...";
await PyRunner.Initialize(); await pyRunner.Initialize();
if (!settingsManager.Settings.HasInstalledPip) if (!settingsManager.Settings.HasInstalledPip)
{ {
await PyRunner.SetupPip(); await pyRunner.SetupPip();
settingsManager.SetHasInstalledPip(true); settingsManager.SetHasInstalledPip(true);
} }
if (!settingsManager.Settings.HasInstalledVenv) if (!settingsManager.Settings.HasInstalledVenv)
{ {
await PyRunner.InstallPackage("virtualenv"); await pyRunner.InstallPackage("virtualenv");
settingsManager.SetHasInstalledVenv(true); settingsManager.SetHasInstalledVenv(true);
} }

34
StabilityMatrix/ViewModels/LaunchViewModel.cs

@ -22,6 +22,8 @@ public partial class LaunchViewModel : ObservableObject
private readonly IContentDialogService contentDialogService; private readonly IContentDialogService contentDialogService;
private readonly LaunchOptionsDialogViewModel launchOptionsDialogViewModel; private readonly LaunchOptionsDialogViewModel launchOptionsDialogViewModel;
private readonly ILogger<LaunchViewModel> logger; private readonly ILogger<LaunchViewModel> logger;
private readonly IPyRunner pyRunner;
private BasePackage? runningPackage; private BasePackage? runningPackage;
private bool clearingPackages = false; private bool clearingPackages = false;
@ -61,8 +63,10 @@ public partial class LaunchViewModel : ObservableObject
IPackageFactory packageFactory, IPackageFactory packageFactory,
IContentDialogService contentDialogService, IContentDialogService contentDialogService,
LaunchOptionsDialogViewModel launchOptionsDialogViewModel, LaunchOptionsDialogViewModel launchOptionsDialogViewModel,
ILogger<LaunchViewModel> logger) ILogger<LaunchViewModel> logger,
IPyRunner pyRunner)
{ {
this.pyRunner = pyRunner;
this.contentDialogService = contentDialogService; this.contentDialogService = contentDialogService;
this.launchOptionsDialogViewModel = launchOptionsDialogViewModel; this.launchOptionsDialogViewModel = launchOptionsDialogViewModel;
this.logger = logger; this.logger = logger;
@ -92,7 +96,7 @@ public partial class LaunchViewModel : ObservableObject
return; return;
} }
await PyRunner.Initialize(); await pyRunner.Initialize();
// Get path from package // Get path from package
var packagePath = SelectedPackage.Path!; var packagePath = SelectedPackage.Path!;
@ -117,14 +121,14 @@ public partial class LaunchViewModel : ObservableObject
var name = SelectedPackage?.Name; var name = SelectedPackage?.Name;
if (name == null) if (name == null)
{ {
Debug.WriteLine($"Selected package is null"); logger.LogWarning($"Selected package is null");
return; return;
} }
var package = packageFactory.FindPackageByName(name); var package = packageFactory.FindPackageByName(name);
if (package == null) if (package == null)
{ {
Debug.WriteLine($"Package {name} not found"); logger.LogWarning("Package {Name} not found", name);
return; return;
} }
@ -152,16 +156,20 @@ public partial class LaunchViewModel : ObservableObject
public void OnLoaded() public void OnLoaded()
{ {
LoadPackages(); LoadPackages();
if (InstalledPackages.Any() && settingsManager.Settings.ActiveInstalledPackage != null) lock (InstalledPackages)
{ {
SelectedPackage = // Skip if no packages
InstalledPackages[ if (!InstalledPackages.Any())
InstalledPackages.IndexOf(InstalledPackages.FirstOrDefault(x => {
x.Id == settingsManager.Settings.ActiveInstalledPackage))]; logger.LogTrace($"No packages for {nameof(LaunchViewModel)}");
} return;
else if (InstalledPackages.Any()) }
{ var activePackageId = settingsManager.Settings.ActiveInstalledPackage;
SelectedPackage = InstalledPackages[0]; if (activePackageId != null)
{
SelectedPackage = InstalledPackages.FirstOrDefault(
x => x.Id == activePackageId) ?? InstalledPackages[0];
}
} }
} }

10
StabilityMatrix/ViewModels/SettingsViewModel.cs

@ -17,6 +17,7 @@ namespace StabilityMatrix.ViewModels;
public partial class SettingsViewModel : ObservableObject public partial class SettingsViewModel : ObservableObject
{ {
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly IPyRunner pyRunner;
public ObservableCollection<string> AvailableThemes => new() public ObservableCollection<string> AvailableThemes => new()
{ {
@ -27,11 +28,12 @@ public partial class SettingsViewModel : ObservableObject
private readonly IContentDialogService contentDialogService; private readonly IContentDialogService contentDialogService;
private readonly IA3WebApi a3WebApi; 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.settingsManager = settingsManager;
this.contentDialogService = contentDialogService; this.contentDialogService = contentDialogService;
this.a3WebApi = a3WebApi; this.a3WebApi = a3WebApi;
this.pyRunner = pyRunner;
SelectedTheme = settingsManager.Settings.Theme ?? "Dark"; SelectedTheme = settingsManager.Settings.Theme ?? "Dark";
} }
@ -66,8 +68,8 @@ public partial class SettingsViewModel : ObservableObject
public AsyncRelayCommand PythonVersionCommand => new(async () => public AsyncRelayCommand PythonVersionCommand => new(async () =>
{ {
// Get python version // Get python version
await PyRunner.Initialize(); await pyRunner.Initialize();
var result = await PyRunner.GetVersionInfo(); var result = await pyRunner.GetVersionInfo();
// Show dialog box // Show dialog box
var dialog = contentDialogService.CreateDialog(); var dialog = contentDialogService.CreateDialog();
dialog.Title = "Python version info"; dialog.Title = "Python version info";
@ -78,9 +80,7 @@ public partial class SettingsViewModel : ObservableObject
public RelayCommand AddInstallationCommand => new(() => public RelayCommand AddInstallationCommand => new(() =>
{ {
var initialDirectory = $"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}\\StabilityMatrix\\Packages";
// Show dialog box to choose a folder // Show dialog box to choose a folder
var dialog = new VistaFolderBrowserDialog var dialog = new VistaFolderBrowserDialog
{ {
Description = "Select a folder", Description = "Select a folder",

Loading…
Cancel
Save