Files
PS_Launcher/src/PSLauncher.App/App.xaml.cs
j.foucher 67f422539d v0.12.0 — Report tool auto-deploy from PROSERVE ZIP
Each PROSERVE ZIP can ship a _report/ subfolder alongside _migrations/.
After install + DB migration, the launcher copies _report/ to the local
XAMPP htdocs (default C:\xampp\htdocs\ProserveReport\) so the Reports
tab in the sidebar always serves the matching version.

Service (PSLauncher.Core/ReportTool)
- IReportToolDeployer + ReportToolDeployer.
- Atomic deploy: copy to {target}.new/ → rename current to .old/ →
  rename .new → final → cleanup .old. Apache never sees a half-state.
- Skip silencieux si _report/ absent du ZIP (PROSERVE release qui ne
  touche pas au Report).
- DeployStatus enum (SkippedNoSourceFolder, XamppNotFound, Failed,
  Deployed) renvoyé pour gestion UI claire.

Config (LocalConfig)
- ReportToolConfig (HtdocsRoot, FolderName, AutoDeploy). Defaults
  matchent l'install standard ASTERION (C:\xampp\htdocs\ProserveReport).

Install pipeline (MainViewModel)
- Étape 6 après extraction + migrations. Progress reporté via le footer
  Library : « 📂 Déploiement Report : 47/120 — assets/main.js ».
  XAMPP introuvable → dialog avec lien vers Settings → Avancés.

Settings UI (SettingsDialog → Avancés)
- Nouveau bloc « OUTIL REPORT (XAMPP htdocs) » : 2 champs path + checkbox
  AutoDeploy + bouton « 📂 Re-déployer maintenant » qui rejoue le deploy
  sur la dernière version installée. Utile après changement de chemin
  htdocs ou si l'auto-deploy avait foiré (XAMPP éteint).

Versions bumped to 0.12.0 (App + Updater + installer .iss).

Côté release : tu ajoutes _report/ dans le source de PROSERVE, tu zippes,
tu uploades. Les clients récupèrent ZIP + migrations + Report en une
seule install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:05:40 +02:00

208 lines
9.3 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.ReportTool;
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>>()));
// Déploiement de l'outil Report (page web XAMPP) à l'install d'une
// nouvelle version : copie atomique de _report/ → C:\xampp\htdocs\ProserveReport\
services.AddSingleton<IReportToolDeployer>(sp =>
new ReportToolDeployer(
configProvider: () => sp.GetRequiredService<LocalConfig>().ReportTool,
sp.GetRequiredService<ILogger<ReportToolDeployer>>()));
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);
}
}