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" />