diff --git a/build-all.bat b/build-all.bat index 3d7dde1..8f988fc 100644 --- a/build-all.bat +++ b/build-all.bat @@ -5,6 +5,10 @@ REM Pour l'installeur Inno Setup, utiliser build-installer.bat. setlocal set "REPO=%~dp0" echo. +echo ==^> Termine les eventuelles instances qui verrouilleraient les .exe +taskkill /F /IM PSLauncher.exe /T >nul 2>&1 +taskkill /F /IM PSLauncher.Updater.exe /T >nul 2>&1 +echo. echo ==^> dotnet publish PSLauncher.App (Release single-file) echo. dotnet publish "%REPO%src\PSLauncher.App\PSLauncher.App.csproj" -c Release --nologo diff --git a/build-installer.bat b/build-installer.bat index 4f5d46f..84cd3bc 100644 --- a/build-installer.bat +++ b/build-installer.bat @@ -12,6 +12,10 @@ REM powershell -ExecutionPolicy Bypass -File installer\build-installer.ps1 -IS setlocal set "SCRIPT_DIR=%~dp0" +REM Termine les eventuelles instances en cours qui verrouilleraient les .exe +taskkill /F /IM PSLauncher.exe /T >nul 2>&1 +taskkill /F /IM PSLauncher.Updater.exe /T >nul 2>&1 + powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%installer\build-installer.ps1" %* set "EXITCODE=%ERRORLEVEL%" diff --git a/build-launcher.bat b/build-launcher.bat index 32f02ed..fb0c88d 100644 --- a/build-launcher.bat +++ b/build-launcher.bat @@ -5,6 +5,10 @@ REM Sortie : C:\ASTERION\GIT\PS_Launcher\PSLauncher.exe (~78 Mo single-file self setlocal set "REPO=%~dp0" echo. +echo ==^> Termine les eventuelles instances de PSLauncher qui verrouilleraient les .exe +taskkill /F /IM PSLauncher.exe /T >nul 2>&1 +taskkill /F /IM PSLauncher.Updater.exe /T >nul 2>&1 +echo. echo ==^> dotnet publish PSLauncher.App (Release single-file) echo. diff --git a/build-updater.bat b/build-updater.bat index a72b79a..3b78336 100644 --- a/build-updater.bat +++ b/build-updater.bat @@ -6,6 +6,10 @@ REM L'updater accompagne PSLauncher.exe — il doit etre dans le meme dossier. setlocal set "REPO=%~dp0" echo. +echo ==^> Termine les eventuelles instances qui verrouilleraient les .exe +taskkill /F /IM PSLauncher.exe /T >nul 2>&1 +taskkill /F /IM PSLauncher.Updater.exe /T >nul 2>&1 +echo. echo ==^> dotnet publish PSLauncher.Updater (Release single-file) echo. diff --git a/src/PSLauncher.App/App.xaml.cs b/src/PSLauncher.App/App.xaml.cs index 132e913..bffe8f8 100644 --- a/src/PSLauncher.App/App.xaml.cs +++ b/src/PSLauncher.App/App.xaml.cs @@ -1,6 +1,7 @@ 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; @@ -24,6 +25,20 @@ 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; @@ -31,6 +46,17 @@ public partial class App : Application { 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), diff --git a/src/PSLauncher.App/PSLauncher.App.csproj b/src/PSLauncher.App/PSLauncher.App.csproj index c263904..7d88862 100644 --- a/src/PSLauncher.App/PSLauncher.App.csproj +++ b/src/PSLauncher.App/PSLauncher.App.csproj @@ -55,8 +55,9 @@ + (C:\ASTERION\GIT\PS_Launcher\PSLauncher.exe). ContinueOnError pour ne pas casser le + publish si une instance du launcher tourne et verrouille la cible — l'opérateur peut + killer et réessayer, ou simplement utiliser le binaire depuis bin\Release\... --> @@ -65,7 +66,8 @@ + OverwriteReadOnlyFiles="true" + ContinueOnError="WarnAndContinue" /> diff --git a/src/PSLauncher.App/Views/MainWindow.xaml.cs b/src/PSLauncher.App/Views/MainWindow.xaml.cs index 8ae2ace..69420b0 100644 --- a/src/PSLauncher.App/Views/MainWindow.xaml.cs +++ b/src/PSLauncher.App/Views/MainWindow.xaml.cs @@ -1,14 +1,58 @@ +using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; +using System.Windows.Interop; namespace PSLauncher.App.Views; public partial class MainWindow : Window { + private static readonly int WM_PSLAUNCHER_BRING = RegisterWindowMessage("PSLAUNCHER_BRING_TO_FRONT"); + + [DllImport("user32.dll")] + private static extern int RegisterWindowMessage(string lpString); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + private const int SW_RESTORE = 9; + public MainWindow() { InitializeComponent(); + SourceInitialized += MainWindow_SourceInitialized; + } + + /// + /// Hook le pump de messages Windows pour réagir au broadcast envoyé par une + /// seconde instance qui veut nous mettre au premier plan. + /// + private void MainWindow_SourceInitialized(object? sender, EventArgs e) + { + var helper = new WindowInteropHelper(this); + var source = HwndSource.FromHwnd(helper.Handle); + source?.AddHook(WndProc); + } + + private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + if (msg == WM_PSLAUNCHER_BRING) + { + if (WindowState == WindowState.Minimized) + WindowState = WindowState.Normal; + else + ShowWindow(hwnd, SW_RESTORE); + Activate(); + SetForegroundWindow(hwnd); + handled = true; + } + return IntPtr.Zero; } /// diff --git a/src/PSLauncher.Updater/PSLauncher.Updater.csproj b/src/PSLauncher.Updater/PSLauncher.Updater.csproj index 8167776..0ecf975 100644 --- a/src/PSLauncher.Updater/PSLauncher.Updater.csproj +++ b/src/PSLauncher.Updater/PSLauncher.Updater.csproj @@ -32,7 +32,8 @@ + OverwriteReadOnlyFiles="true" + ContinueOnError="WarnAndContinue" />