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();
serviceCollection.AddSingleton<IPageService, PageService>();
serviceCollection.AddSingleton<IContentDialogService, ContentDialogService>();
serviceCollection.AddSingleton<IPackageFactory, PackageFactory>();
serviceCollection.AddSingleton<IPyRunner, PyRunner>();
serviceCollection.AddTransient<MainWindow>();
serviceCollection.AddTransient<SettingsPage>();
serviceCollection.AddTransient<LaunchPage>();
serviceCollection.AddTransient<InstallPage>();
serviceCollection.AddTransient<TextToImagePage>();
serviceCollection.AddTransient<MainWindowViewModel>();
serviceCollection.AddSingleton<SettingsViewModel>();
serviceCollection.AddSingleton<LaunchViewModel>();
serviceCollection.AddSingleton<InstallerViewModel>();
serviceCollection.AddSingleton<TextToImageViewModel>();
serviceCollection.AddSingleton<LaunchOptionsDialogViewModel>();
serviceCollection.AddSingleton<IContentDialogService, ContentDialogService>();
serviceCollection.AddSingleton<IPackageFactory, PackageFactory>();
serviceCollection.AddSingleton<BasePackage, A3WebUI>();
serviceCollection.AddSingleton<BasePackage, DankDiffusion>();
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
/// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal class PyIOStream
public class PyIOStream
{
private readonly StringBuilder TextBuilder;
private readonly StringWriter TextWriter;

42
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<PyRunner> 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<PyRunner> logger)
{
this.logger = logger;
}
/// <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>
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
/// <summary>
/// One-time setup for get-pip
/// </summary>
public static async Task SetupPip()
public async Task SetupPip()
{
if (!File.Exists(GetPipPath))
{
@ -75,7 +83,7 @@ internal static class PyRunner
/// <summary>
/// Install a Python package with pip
/// </summary>
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
/// <param name="waitTimeout">Time limit for waiting on PyRunning lock.</param>
/// <param name="cancelToken">Cancellation token.</param>
/// <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
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="cancelToken">Cancellation token.</param>
/// <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
await PyRunning.WaitAsync(cancelToken).ConfigureAwait(false);
@ -143,7 +151,7 @@ internal static class PyRunner
/// Evaluate Python expression and return its value as a string
/// </summary>
/// <param name="expression"></param>
public static async Task<string> Eval(string expression)
public async Task<string> Eval(string expression)
{
return await Eval<string>(expression);
}
@ -152,7 +160,7 @@ internal static class PyRunner
/// Evaluate Python expression and return its value
/// </summary>
/// <param name="expression"></param>
public static Task<T> Eval<T>(string expression)
public Task<T> Eval<T>(string expression)
{
return RunInThreadWithLock(() =>
{
@ -165,7 +173,7 @@ internal static class PyRunner
/// Execute Python code without returning a value
/// </summary>
/// <param name="code"></param>
public static Task Exec(string code)
public Task Exec(string code)
{
return RunInThreadWithLock(() =>
{
@ -176,7 +184,7 @@ internal static class PyRunner
/// <summary>
/// Return the Python version as a PyVersionInfo struct
/// </summary>
public static async Task<PyVersionInfo> GetVersionInfo()
public async Task<PyVersionInfo> GetVersionInfo()
{
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 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<InstallerViewModel> 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);
}

34
StabilityMatrix/ViewModels/LaunchViewModel.cs

@ -22,6 +22,8 @@ public partial class LaunchViewModel : ObservableObject
private readonly IContentDialogService contentDialogService;
private readonly LaunchOptionsDialogViewModel launchOptionsDialogViewModel;
private readonly ILogger<LaunchViewModel> 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<LaunchViewModel> logger)
ILogger<LaunchViewModel> 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];
}
}
}

10
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<string> 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",

Loading…
Cancel
Save