Each PROSERVE ZIP can now ship a _migrations/ subfolder with versioned SQL scripts. The launcher applies them right after extraction, in the same install session as the binary copy, so the user never lands on a PROSERVE build whose schema doesn't match its code. Service (PSLauncher.Core/Migrations) - IDatabaseMigrationService + DatabaseMigrationService (MySqlConnector, BSD-licensed). Each .sql runs in a transaction; on failure the DB state is rolled back and the install completes (files only) but the user is warned to fix the connection / replay later. - Tracking table _launcher_migrations (filename, applied_at, checksum, duration_ms) — same model as Flyway / Doctrine. Already-applied scripts are skipped on subsequent installs. Modified scripts trigger a warning log without blocking. - Custom SQL splitter that respects strings/comments/backticks so a single .sql file can contain multiple statements separated by `;`. - DatabasePasswordProtector: DPAPI CurrentUser scope for the MySQL password in config.json (same protection as the license key). Config (PSLauncher.Models/LocalConfig.cs) - New DatabaseConfig section: Host=localhost, Port=3306, User=root, empty password, Database=proserve, AutoApplyMigrations=true. Defaults match a fresh XAMPP install. Override via Settings (next commit). Install pipeline (MainViewModel) - After ZipInstaller.InstallAsync and before declaring the install complete, if AutoApplyMigrations and a _migrations/ folder exists, run ApplyMigrationsAsync with progress reporting (per-file %, filename in footer). Failure shows MsgMigrationFailed dialog explaining XAMPP must be running and pointing to Settings → Database for connection params. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
200 lines
8.8 KiB
C#
200 lines
8.8 KiB
C#
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<ConfigStore>.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<IConfigStore, ConfigStore>();
|
|
services.AddSingleton<LocalConfig>(sp => sp.GetRequiredService<IConfigStore>().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<IInstallationRegistry>(sp =>
|
|
new InstallationRegistry(
|
|
sp.GetRequiredService<ILogger<InstallationRegistry>>(),
|
|
() => sp.GetRequiredService<LocalConfig>().InstallRoot));
|
|
|
|
services.AddSingleton<IProcessLauncher, ProcessLauncher>();
|
|
services.AddSingleton<IIntegrityService, IntegrityService>();
|
|
services.AddSingleton<IZipInstaller, ZipInstaller>();
|
|
services.AddSingleton<IDownloadStateStore, DownloadStateStore>();
|
|
|
|
services.AddSingleton<IManifestService>(sp =>
|
|
new ManifestService(
|
|
sp.GetRequiredService<HttpClient>(),
|
|
() => sp.GetRequiredService<LocalConfig>().ServerBaseUrl,
|
|
sp.GetRequiredService<ILogger<ManifestService>>()));
|
|
|
|
services.AddSingleton<IDownloadManager, DownloadManager>();
|
|
services.AddSingleton<IUpdateChecker, UpdateChecker>();
|
|
services.AddSingleton<ILauncherSelfUpdater, LauncherSelfUpdater>();
|
|
|
|
services.AddSingleton<ILicenseService>(sp =>
|
|
new LicenseService(
|
|
sp.GetRequiredService<HttpClient>(),
|
|
() => sp.GetRequiredService<LocalConfig>().ServerBaseUrl,
|
|
sp.GetRequiredService<IConfigStore>(),
|
|
sp.GetRequiredService<LocalConfig>(),
|
|
sp.GetRequiredService<ILogger<LicenseService>>()));
|
|
|
|
services.AddSingleton<IToastService, ToastService>();
|
|
|
|
// 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<IDatabaseMigrationService>(sp =>
|
|
new DatabaseMigrationService(
|
|
configProvider: () => sp.GetRequiredService<LocalConfig>().Database,
|
|
passwordProvider: () => DatabasePasswordProtector.Unprotect(
|
|
sp.GetRequiredService<LocalConfig>().Database.EncryptedPassword),
|
|
sp.GetRequiredService<ILogger<DatabaseMigrationService>>()));
|
|
|
|
services.AddTransient<SettingsViewModel>();
|
|
services.AddSingleton<MainViewModel>();
|
|
services.AddSingleton<MainWindow>();
|
|
})
|
|
.Build();
|
|
|
|
var window = _host.Services.GetRequiredService<MainWindow>();
|
|
window.DataContext = _host.Services.GetRequiredService<MainViewModel>();
|
|
// 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);
|
|
}
|
|
}
|