launcher: refuse to start when another instance holds the install dir

Tetsu hit this running two Metin2Launcher.exe processes against the
same Wine install dir — the second instance blew up during blob
download with

    The process cannot access the file '.../\.updates/staging/Metin2.exe'
    because it is being used by another process.

because the first instance was already writing to the staging file.
There was no guard against multiple concurrent launchers, and the
symptoms (file-in-use IO exception during staging) are hard to
diagnose from the user's end.

Add a per-install-dir FileStream lock at `.updates/launcher.lock`
opened with FileShare.None + DeleteOnClose. If the lock is held, log a
clear error and exit with code 3. Released automatically when the
process exits. Works uniformly across Windows, Wine and native Linux;
a named Mutex would behave differently across Wine prefixes, so this
sticks to plain filesystem locking.

Also:
- launcher: switch main window to an image background + semi-
  transparent column brushes so the Morion2 crystal gate branding art
  shows through the existing dark-theme layout. First step toward the
  art pack Jan dropped tonight; follow-up commits will redo the
  layout per the full mockup.
- Assets/Branding/launcher-bg.png: initial background (downscaled
  from the Gemini-generated crystal gate hero image, 1800x1120, ~2.5 MB).
- csproj: include Assets/**/*.png as AvaloniaResource.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jan Nedbal
2026-04-15 14:51:28 +02:00
parent db59f4963c
commit 3db306fbc7
4 changed files with 47 additions and 2 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@@ -26,4 +26,8 @@
<EmbeddedResource Include="Localization\en.json" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**\*.png" />
</ItemGroup>
</Project>

View File

@@ -29,6 +29,30 @@ public static class Program
Log.SetLogFilePath(Path.Combine(stateDir, "launcher.log"));
Log.Info($"metin2 launcher starting in {clientRoot}");
// Single-instance guard: two launchers running in the same install
// dir race on the staging files and blow up with "The process cannot
// access the file ... because it is being used by another process".
// Use a per-install-dir file lock (not a named mutex — named mutexes
// behave differently across Wine prefixes and Windows sessions). The
// lock is released when the process exits.
FileStream? instanceLock = null;
try
{
var lockPath = Path.Combine(stateDir, "launcher.lock");
instanceLock = new FileStream(
lockPath,
FileMode.OpenOrCreate,
FileAccess.ReadWrite,
FileShare.None,
bufferSize: 1,
FileOptions.DeleteOnClose);
}
catch (IOException)
{
Log.Error($"another Metin2Launcher is already running in {clientRoot} — refusing to start a second instance");
return 3;
}
if (args.Contains("--nogui"))
{
return RunHeadlessAsync(clientRoot, args).GetAwaiter().GetResult();

View File

@@ -26,8 +26,12 @@
<GradientStop Offset="1.0" Color="#8B0000"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="LeftColumnBrush" Color="#17171B"/>
<SolidColorBrush x:Key="RightColumnBrush" Color="#0F0F12"/>
<!-- Column brushes are semi-transparent so the branding image bleeds -->
<!-- through while still keeping text legible. The left column sits over -->
<!-- the status widgets so it's a bit opaquer; the right column is the -->
<!-- news/changelog and stays darker to avoid image-through-text. -->
<SolidColorBrush x:Key="LeftColumnBrush" Color="#17171B" Opacity="0.78"/>
<SolidColorBrush x:Key="RightColumnBrush" Color="#0F0F12" Opacity="0.68"/>
<SolidColorBrush x:Key="AccentBrush" Color="#C41515"/>
<SolidColorBrush x:Key="MutedTextBrush" Color="#888892"/>
<SolidColorBrush x:Key="BodyTextBrush" Color="#E0E0E4"/>
@@ -87,6 +91,18 @@
</Style>
</Window.Styles>
<Grid>
<!-- Full-window branding image, hand-authored for the m2pack release. -->
<!-- Sits behind the banner + body grid; the dark column brushes above it -->
<!-- give the status and news columns their own legible background while -->
<!-- the banner fades the top into the crimson header. -->
<Image Source="avares://Metin2Launcher/Assets/Branding/launcher-bg.png"
Stretch="UniformToFill"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
<!-- Dim overlay so text on the image reads reliably. -->
<Rectangle Fill="#000000" Opacity="0.35"/>
<Grid RowDefinitions="110,*">
<!-- Banner -->
<Border Grid.Row="0" Background="{StaticResource BannerGradient}">
@@ -232,4 +248,5 @@
</Border>
</Grid>
</Grid>
</Grid>
</Window>