using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.InteropServices; using System.Windows; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using PSLauncher.App.Services; using PSLauncher.App.ViewModels; using PSLauncher.App.Views; using PSLauncher.Core.Configuration; using PSLauncher.Core.Downloads; using PSLauncher.Core.Installations; using PSLauncher.Core.Integrity; using PSLauncher.Core.Licensing; using PSLauncher.Core.Localization; using PSLauncher.Core.Manifests; using PSLauncher.Core.Migrations; using PSLauncher.Core.Process; using PSLauncher.Core.Updates; using PSLauncher.Models; using Serilog; namespace PSLauncher.App; public partial class App : Application { private IHost? _host; private static System.Threading.Mutex? _singleInstanceMutex; private const string SingleInstanceMutexName = "Global\\PSLauncher-3F8E2C1A-9B47-4D5E-A0F8-2E9D1B6C7A3F"; private const string BringToFrontMessage = "PSLAUNCHER_BRING_TO_FRONT"; private static readonly int WM_PSLAUNCHER_BRING = RegisterWindowMessage(BringToFrontMessage); [DllImport("user32.dll")] private static extern int RegisterWindowMessage(string lpString); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool PostMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam); private const int HWND_BROADCAST = 0xFFFF; public static string LogsDirectory { get; private set; } = string.Empty; protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // Single-instance : si une autre instance tourne déjà, on lui demande de // se mettre au premier plan via un broadcast Windows et on quitte tout de suite. _singleInstanceMutex = new System.Threading.Mutex(initiallyOwned: true, name: SingleInstanceMutexName, out var createdNew); if (!createdNew) { PostMessage((IntPtr)HWND_BROADCAST, WM_PSLAUNCHER_BRING, IntPtr.Zero, IntPtr.Zero); Shutdown(); return; } // Logs avant tout — pour ne rien perdre des erreurs au boot LogsDirectory = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "PSLauncher", "logs"); Directory.CreateDirectory(LogsDirectory); // Bootstrap i18n : on lit d'abord la config (sans services DI) pour fixer la // langue avant que le moindre élément XAML soit instancié. try { var bootstrapStore = new ConfigStore( Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); var bootstrapCfg = bootstrapStore.Load(); Strings.Init(bootstrapCfg.Language); } catch { Strings.Init("auto"); } Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .Enrich.FromLogContext() .WriteTo.Debug() .WriteTo.File( path: Path.Combine(LogsDirectory, "app-.log"), rollingInterval: RollingInterval.Day, retainedFileCountLimit: 10, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}") .CreateLogger(); Log.Information("PSLauncher starting (logs in {Path})", LogsDirectory); AppDomain.CurrentDomain.UnhandledException += (_, args) => Log.Fatal((Exception)args.ExceptionObject, "Unhandled exception (AppDomain)"); DispatcherUnhandledException += (_, args) => { Log.Error(args.Exception, "Unhandled UI exception"); args.Handled = false; }; _host = Host.CreateDefaultBuilder() .UseSerilog() .ConfigureServices((_, services) => { services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService().Load()); services.AddSingleton(sp => { // Handler tuné pour les gros téléchargements parallèles : // - MaxConnectionsPerServer=16 pour autoriser le multi-segment (8 par défaut). // - AutomaticDecompression sur l'API JSON uniquement (None ici car les builds // sont des ZIPs déjà compressés ; éviter la CPU+RAM gaspillée par un éventuel // wrapping gzip côté serveur). // - PooledConnectionLifetime court pour éviter qu'OVH ferme nos sockets sous nous. var handler = new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(5), PooledConnectionIdleTimeout = TimeSpan.FromSeconds(60), MaxConnectionsPerServer = 16, EnableMultipleHttp2Connections = true, AutomaticDecompression = System.Net.DecompressionMethods.None, }; var http = new HttpClient(handler) { Timeout = Timeout.InfiniteTimeSpan, DefaultRequestVersion = System.Net.HttpVersion.Version20, DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower, }; http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("PSLauncher", "0.5")); return http; }); services.AddSingleton(sp => new InstallationRegistry( sp.GetRequiredService>(), () => sp.GetRequiredService().InstallRoot)); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => new ManifestService( sp.GetRequiredService(), () => sp.GetRequiredService().ServerBaseUrl, sp.GetRequiredService>())); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => new LicenseService( sp.GetRequiredService(), () => sp.GetRequiredService().ServerBaseUrl, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>())); services.AddSingleton(); // Migration DB MySQL : appliquée juste après extraction d'un build PROSERVE // si le ZIP contient un sous-répertoire _migrations/. La config DB et le // password (DPAPI) sont lus à la demande pour refléter les changements // utilisateur dans Settings sans recréer le service. services.AddSingleton(sp => new DatabaseMigrationService( configProvider: () => sp.GetRequiredService().Database, passwordProvider: () => DatabasePasswordProtector.Unprotect( sp.GetRequiredService().Database.EncryptedPassword), sp.GetRequiredService>())); services.AddTransient(); services.AddSingleton(); services.AddSingleton(); }) .Build(); var window = _host.Services.GetRequiredService(); window.DataContext = _host.Services.GetRequiredService(); // Mise en page droite-à-gauche pour les langues RTL (arabe). if (Strings.IsRightToLeft) window.FlowDirection = FlowDirection.RightToLeft; MainWindow = window; window.Show(); } protected override void OnExit(ExitEventArgs e) { Log.Information("PSLauncher shutting down"); Log.CloseAndFlush(); _host?.Dispose(); base.OnExit(e); } }