Single-instance launcher + tolerant build pipeline
Single-instance --------------- Two PSLauncher.exe instances ran in parallel during the user's last release build, locking the repo-root copy and breaking the publish. Two-layer protection: 1. App.xaml.cs uses a Global\ named Mutex (UUID-based key) created at OnStartup. If the mutex is already held, the second instance PostMessages a registered Windows message (PSLAUNCHER_BRING_TO_FRONT) to HWND_BROADCAST and Shutdown()s immediately. 2. MainWindow hooks WndProc via SourceInitialized + HwndSource.AddHook; when it sees the broadcast, it restores from minimized, calls ShowWindow(SW_RESTORE) + Activate() + SetForegroundWindow so the already-running instance pops to the user's foreground. Net result: clicking the launcher icon a second time pops the existing window instead of starting a duplicate process. Tolerant build pipeline ----------------------- - Both csproj post-publish copy targets now have ContinueOnError="WarnAndContinue". A locked PSLauncher.exe at the repo root no longer fails the entire publish — the binary still exists in bin\Release\...\publish\ and the user gets a warning instead of an error. - All four .bat scripts (build-launcher / build-updater / build-all / build-installer) now run `taskkill /F /IM PSLauncher.exe /T` and the same for PSLauncher.Updater.exe before the publish step. This defensively closes any stray instance left behind by previous tests so the build pipeline is clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,10 @@ REM Pour l'installeur Inno Setup, utiliser build-installer.bat.
|
|||||||
setlocal
|
setlocal
|
||||||
set "REPO=%~dp0"
|
set "REPO=%~dp0"
|
||||||
echo.
|
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 PSLauncher.App (Release single-file)
|
||||||
echo.
|
echo.
|
||||||
dotnet publish "%REPO%src\PSLauncher.App\PSLauncher.App.csproj" -c Release --nologo
|
dotnet publish "%REPO%src\PSLauncher.App\PSLauncher.App.csproj" -c Release --nologo
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ REM powershell -ExecutionPolicy Bypass -File installer\build-installer.ps1 -IS
|
|||||||
setlocal
|
setlocal
|
||||||
set "SCRIPT_DIR=%~dp0"
|
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" %*
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%installer\build-installer.ps1" %*
|
||||||
set "EXITCODE=%ERRORLEVEL%"
|
set "EXITCODE=%ERRORLEVEL%"
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ REM Sortie : C:\ASTERION\GIT\PS_Launcher\PSLauncher.exe (~78 Mo single-file self
|
|||||||
setlocal
|
setlocal
|
||||||
set "REPO=%~dp0"
|
set "REPO=%~dp0"
|
||||||
echo.
|
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 ==^> dotnet publish PSLauncher.App (Release single-file)
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ REM L'updater accompagne PSLauncher.exe — il doit etre dans le meme dossier.
|
|||||||
setlocal
|
setlocal
|
||||||
set "REPO=%~dp0"
|
set "REPO=%~dp0"
|
||||||
echo.
|
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 ==^> dotnet publish PSLauncher.Updater (Release single-file)
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
@@ -24,6 +25,20 @@ namespace PSLauncher.App;
|
|||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
private IHost? _host;
|
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;
|
public static string LogsDirectory { get; private set; } = string.Empty;
|
||||||
|
|
||||||
@@ -31,6 +46,17 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
base.OnStartup(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
|
// Logs avant tout — pour ne rien perdre des erreurs au boot
|
||||||
LogsDirectory = Path.Combine(
|
LogsDirectory = Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
|||||||
@@ -55,8 +55,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Après chaque `dotnet publish -c Release`, copie le single-file exe à la racine du repo
|
<!-- Après chaque `dotnet publish -c Release`, copie le single-file exe à la racine du repo
|
||||||
(C:\ASTERION\GIT\PS_Launcher\PSLauncher.exe) pour avoir un binaire prêt à tester
|
(C:\ASTERION\GIT\PS_Launcher\PSLauncher.exe). ContinueOnError pour ne pas casser le
|
||||||
sans aller chercher dans bin\Release\... -->
|
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\... -->
|
||||||
<Target Name="CopyPublishedToRepoRoot" AfterTargets="Publish"
|
<Target Name="CopyPublishedToRepoRoot" AfterTargets="Publish"
|
||||||
Condition=" '$(Configuration)' == 'Release' ">
|
Condition=" '$(Configuration)' == 'Release' ">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -65,7 +66,8 @@
|
|||||||
<Copy SourceFiles="$(PublishDir)$(AssemblyName).exe"
|
<Copy SourceFiles="$(PublishDir)$(AssemblyName).exe"
|
||||||
DestinationFolder="$(RepoRoot)"
|
DestinationFolder="$(RepoRoot)"
|
||||||
SkipUnchangedFiles="false"
|
SkipUnchangedFiles="false"
|
||||||
OverwriteReadOnlyFiles="true" />
|
OverwriteReadOnlyFiles="true"
|
||||||
|
ContinueOnError="WarnAndContinue" />
|
||||||
<Message Importance="high" Text="Copied $(AssemblyName).exe to $(RepoRoot)" />
|
<Message Importance="high" Text="Copied $(AssemblyName).exe to $(RepoRoot)" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,58 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Controls.Primitives;
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
|
||||||
namespace PSLauncher.App.Views;
|
namespace PSLauncher.App.Views;
|
||||||
|
|
||||||
public partial class MainWindow : Window
|
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()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
SourceInitialized += MainWindow_SourceInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hook le pump de messages Windows pour réagir au broadcast envoyé par une
|
||||||
|
/// seconde instance qui veut nous mettre au premier plan.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -32,7 +32,8 @@
|
|||||||
<Copy SourceFiles="$(PublishDir)$(AssemblyName).exe"
|
<Copy SourceFiles="$(PublishDir)$(AssemblyName).exe"
|
||||||
DestinationFolder="$(RepoRoot)"
|
DestinationFolder="$(RepoRoot)"
|
||||||
SkipUnchangedFiles="false"
|
SkipUnchangedFiles="false"
|
||||||
OverwriteReadOnlyFiles="true" />
|
OverwriteReadOnlyFiles="true"
|
||||||
|
ContinueOnError="WarnAndContinue" />
|
||||||
<Message Importance="high" Text="Copied $(AssemblyName).exe to $(RepoRoot)" />
|
<Message Importance="high" Text="Copied $(AssemblyName).exe to $(RepoRoot)" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user