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>
208 lines
9.3 KiB
C#
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);
|
|
}
|
|
}
|