using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Net.Http; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Platform.Storage; using Avalonia.Styling; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NLog; using NLog.Config; using NLog.Extensions.Logging; using NLog.Targets; using Octokit; using Polly; using Polly.Contrib.WaitAndRetry; using Polly.Extensions.Http; using Polly.Timeout; using Refit; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.DesignData; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Converters.Json; using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Updater; using Application = Avalonia.Application; using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace StabilityMatrix.Avalonia; public sealed class App : Application { [NotNull] public static IServiceProvider? Services { get; private set; } [NotNull] public static Visual? VisualRoot { get; private set; } [NotNull] public static IStorageProvider? StorageProvider { get; private set; } public override void Initialize() { AvaloniaXamlLoader.Load(this); // Set design theme if (Design.IsDesignMode) { RequestedThemeVariant = ThemeVariant.Dark; } } public override void OnFrameworkInitializationCompleted() { if (Design.IsDesignMode) { DesignData.DesignData.Initialize(); Services = DesignData.DesignData.Services; } else { ConfigureServiceProvider(); } var mainViewModel = Services.GetRequiredService(); var notificationService = Services.GetRequiredService(); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { var mainWindow = Services.GetRequiredService(); mainWindow.DataContext = mainViewModel; mainWindow.NotificationService = notificationService; VisualRoot = mainWindow; StorageProvider = mainWindow.StorageProvider; desktop.MainWindow = mainWindow; desktop.Exit += OnExit; } base.OnFrameworkInitializationCompleted(); } private static void ConfigureServiceProvider() { var services = ConfigureServices(); Services = services.BuildServiceProvider(); var settingsManager = Services.GetRequiredService(); settingsManager.TryFindLibrary(); Services.GetRequiredService().StartEventListener(); } internal static void ConfigurePageViewModels(IServiceCollection services) { services.AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(); services.AddSingleton(provider => new MainWindowViewModel(provider.GetRequiredService(), provider.GetRequiredService>()) { Pages = { provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), }, FooterPages = { provider.GetRequiredService() } }); // Register disposable view models for shutdown cleanup services.AddSingleton(p => p.GetRequiredService()); } internal static void ConfigureDialogViewModels(IServiceCollection services) { // Dialog view models (transient) services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddSingleton(); // Other transients (usually sub view models) services.AddTransient(); services.AddTransient(); // Global progress services.AddSingleton(); // Controls services.AddTransient(); // Dialog factory services.AddSingleton>(provider => new ServiceManager() .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService) .Register(provider.GetRequiredService)); } internal static void ConfigureViews(IServiceCollection services) { // Pages services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); // Dialogs services.AddTransient(); services.AddTransient(); services.AddTransient(); // Controls services.AddTransient(); // Window services.AddSingleton(); } internal static void ConfigurePackages(IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); } private static IServiceCollection ConfigureServices() { var services = new ServiceCollection(); services.AddMemoryCache(); ConfigurePageViewModels(services); ConfigureDialogViewModels(services); ConfigurePackages(services); // Other services services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); if (Compat.IsWindows) { services.AddSingleton(); } else if (Compat.IsLinux) { services.AddSingleton(); } ConfigureViews(services); if (Design.IsDesignMode) { services.AddSingleton(); } else { services.AddSingleton(); services.AddSingleton(p => p.GetRequiredService()); } services.AddTransient(_ => { var client = new GitHubClient(new ProductHeaderValue("StabilityMatrix")); // var githubApiKey = Config["GithubApiKey"]; // if (string.IsNullOrWhiteSpace(githubApiKey)) // return client; // // client.Credentials = new Credentials(githubApiKey); return client; }); // Configure Refit and Polly var jsonSerializerOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; jsonSerializerOptions.Converters.Add(new ObjectToInferredTypesConverter()); jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter()); jsonSerializerOptions.Converters.Add( new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); jsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; var defaultRefitSettings = new RefitSettings { ContentSerializer = new SystemTextJsonContentSerializer(jsonSerializerOptions) }; // HTTP Policies var retryStatusCodes = new[] { HttpStatusCode.RequestTimeout, // 408 HttpStatusCode.InternalServerError, // 500 HttpStatusCode.BadGateway, // 502 HttpStatusCode.ServiceUnavailable, // 503 HttpStatusCode.GatewayTimeout // 504 }; var delay = Backoff .DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromMilliseconds(80), retryCount: 5); var retryPolicy = HttpPolicyExtensions .HandleTransientHttpError() .Or() .OrResult(r => retryStatusCodes.Contains(r.StatusCode)) .WaitAndRetryAsync(delay); // Shorter timeout for local requests var localTimeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(3)); var localDelay = Backoff .DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromMilliseconds(50), retryCount: 3); var localRetryPolicy = HttpPolicyExtensions .HandleTransientHttpError() .Or() .OrResult(r => retryStatusCodes.Contains(r.StatusCode)) .WaitAndRetryAsync(localDelay, onRetryAsync: (_, _) => { Debug.WriteLine("Retrying local request..."); return Task.CompletedTask; }); // named client for update services.AddHttpClient("UpdateClient") .AddPolicyHandler(retryPolicy); // Add Refit clients services.AddRefitClient(defaultRefitSettings) .ConfigureHttpClient(c => { c.BaseAddress = new Uri("https://civitai.com"); c.Timeout = TimeSpan.FromSeconds(15); }) .AddPolicyHandler(retryPolicy); // Add Refit client managers services.AddHttpClient("A3Client") .AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy)); // Add logging services.AddLogging(builder => { builder.ClearProviders(); builder.AddFilter("Microsoft.Extensions.Http", LogLevel.Warning) .AddFilter("Microsoft.Extensions.Http.DefaultHttpClientFactory", LogLevel.Warning) .AddFilter("Microsoft", LogLevel.Warning) .AddFilter("System", LogLevel.Warning); builder.SetMinimumLevel(LogLevel.Debug); builder.AddNLog(ConfigureLogging()); }); return services; } public static void Shutdown() { if (Current is null) throw new InvalidOperationException("Current Application is not defined"); if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) { lifetime.Shutdown(); } } private void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs args) { Debug.WriteLine("Start OnExit"); // Services.GetRequiredService().OnShutdown(); var settingsManager = Services.GetRequiredService(); // Unless KeepFolderLinksOnShutdown is set, delete all package junctions if (settingsManager is { IsLibraryDirSet: true, Settings.KeepFolderLinksOnShutdown: false }) { var sharedFolders = Services.GetRequiredService(); sharedFolders.RemoveLinksForAllPackages(); } Debug.WriteLine("Start OnExit: Disposing services"); // Dispose all services foreach (var disposable in Services.GetServices()) { Debug.WriteLine($"Disposing {disposable.GetType().Name}"); disposable.Dispose(); } Debug.WriteLine("End OnExit"); } private static LoggingConfiguration ConfigureLogging() { var logConfig = new LoggingConfiguration(); // File target logConfig.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, new FileTarget("logfile") { Layout = "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}", ArchiveOldFileOnStartup = true, FileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log", ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log", ArchiveNumbering = ArchiveNumberingMode.Rolling, MaxArchiveFiles = 2 }); // Debugger Target logConfig.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, new DebuggerTarget("debugger") { Layout = "${message}" }); LogManager.Configuration = logConfig; // Add Sentry to NLog if enabled if (true) { logConfig.AddSentry(o => { o.InitializeSdk = false; o.Layout = "${message}"; o.ShutdownTimeoutSeconds = 5; o.IncludeEventDataOnBreadcrumbs = true; o.BreadcrumbLayout = "${logger}: ${message}"; // Debug and higher are stored as breadcrumbs (default is Info) o.MinimumBreadcrumbLevel = NLog.LogLevel.Debug; // Error and higher is sent as event (default is Error) o.MinimumEventLevel = NLog.LogLevel.Error; }); } return logConfig; } }