fix(app): the claim on the data folder never stops a start, and is given up when no window came of it
windows / windows (push) Failing after 5s
build / windows (push) Failing after 5s

From the review of #217. This code runs at every start, before a window or
the log exists.

- The event that asks the open window to come to the front may be taken by
  something this process cannot use (a gclo that runs elevated made it) or
  that is no event. That ended the start with an unhandled exception. The
  claim now stands as the lock file says and only the request is not made;
  whatever else fails while the claim is made, the window opens without one.
- A window that could not be made gives the claim up: the process can live
  on without a window, and every later start would have ended at once with
  nothing to bring to the front.
- A request to come to the front that arrives while the window closes is
  dropped, and is queued on the dispatcher captured at start, not read from
  a window that may be gone.

Refs #216

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
This commit is contained in:
james.maesandClaude Fable 5.2 committed 2026-11-11 16:54:50 -06:00
1 parent 96cf2fcabc
commit 15f263676e
5 files changed
+77 -11

No files matched your search

+20
View File
@@ -123,6 +123,26 @@ public sealed class WindowClaimTests : IDisposable
[Fact]
public void WhenTheRequestToComeToTheFrontCannotBeMade_TheClaimStands_AndNothingFails()
{
// The name of the event is taken by something that is no event (it could as well be one this process may not use).
string folder = Path.Combine(_root, "data");
using var inTheWay = new Mutex(initiallyOwned: false, $@"Local\gclo-show-{WindowClaim.KeyFor(folder)}");
using var first = WindowClaim.For(folder);
using var second = WindowClaim.For(folder);
using var asked = new ManualResetEventSlim();
first.OnAskedToShow(asked.Set);
second.AskTheOtherToShowItself();
Assert.True(first.IsTheOnlyOne);
Assert.False(second.IsTheOnlyOne);
Assert.False(asked.Wait(TimeSpan.FromMilliseconds(300))); // nothing is asked, and nothing is heard
}
[Fact]
public void TheKeyOfAFolder_IsTheSameForEverySpellingOfIt_AndAnotherForAnotherFolder()
{
+23 -6
View File
@@ -36,13 +36,14 @@ public sealed class WindowClaim : IDisposable
private readonly FileStream? _held;
private readonly EventWaitHandle _show;
/// <summary>Null when the event could not be made or opened: nothing is asked then, and nothing heard.</summary>
private readonly EventWaitHandle? _show;
private RegisteredWaitHandle? _waiting;
private WindowClaim(FileStream? held, bool isTheOnlyOne, EventWaitHandle show)
private WindowClaim(FileStream? held, bool isTheOnlyOne, EventWaitHandle? show)
{
_held = held;
IsTheOnlyOne = isTheOnlyOne;
@@ -78,7 +79,21 @@ public sealed class WindowClaim : IDisposable
// No claim can be made here (a read-only profile, a path that is no folder): the window opens all the same.
}
var show = new EventWaitHandle(initialState: false, EventResetMode.AutoReset, $@"Local\gclo-show-{KeyFor(dataRoot)}");
EventWaitHandle? show = null;
try
{
show = new EventWaitHandle(initialState: false, EventResetMode.AutoReset, $@"Local\gclo-show-{KeyFor(dataRoot)}");
}
catch(Exception ex) when(ex is UnauthorizedAccessException or WaitHandleCannotBeOpenedException or IOException)
{
/////////////////////////////////////////////////////////////////////////////
// The name is taken by something this process may not use (a gclo that //
// runs elevated made it), or by something that is no event. The claim on //
// the folder stands as the file said; only the request to come to the //
// front is not made. This runs at every start: it must not end one. //
/////////////////////////////////////////////////////////////////////////////
}
return new WindowClaim(held, !anotherHoldsIt, show);
}
@@ -103,7 +118,7 @@ public sealed class WindowClaim : IDisposable
/// <summary>Asks the window that holds the data folder to come to the front.</summary>
public void AskTheOtherToShowItself() => _show.Set();
public void AskTheOtherToShowItself() => _show?.Set();
@@ -116,7 +131,9 @@ public sealed class WindowClaim : IDisposable
{
ArgumentNullException.ThrowIfNull(show);
_waiting?.Unregister(null);
_waiting = ThreadPool.RegisterWaitForSingleObject(_show, (_, _) => show(), state: null, Timeout.Infinite, executeOnlyOnce: false);
_waiting = _show is null
? null
: ThreadPool.RegisterWaitForSingleObject(_show, (_, _) => show(), state: null, Timeout.Infinite, executeOnlyOnce: false);
}
@@ -126,7 +143,7 @@ public sealed class WindowClaim : IDisposable
{
_waiting?.Unregister(null);
_waiting = null;
_show.Dispose();
_show?.Dispose();
_held?.Dispose();
}
}
+12 -2
View File
@@ -79,7 +79,17 @@ public partial class App : Application
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
_window = new MainWindow();
_window.Activate();
try
{
_window = new MainWindow();
_window.Activate();
}
catch
{
// No window came of it, and this process may live on (the handler above keeps it alive): it must not
// hold the data folder, or every later start would end at once with nothing to bring to the front (#216).
Program.Claim?.Dispose();
throw;
}
}
}
+11 -1
View File
@@ -52,6 +52,9 @@ public sealed partial class MainWindow : Window
/// <summary>Set once the root's theme-change subscription exists.</summary>
private bool _themeHooked;
/// <summary>Set when the window has closed: a request to come to the front that arrives after that is dropped (#216).</summary>
private bool _closed;
private readonly AppSettings _settings;
private readonly UpdateService _updateService;
@@ -204,11 +207,13 @@ public sealed partial class MainWindow : Window
}
// Another start of gclo on this data folder ends at once and asks this window to come to the front (#216).
Program.Claim?.OnAskedToShow(() => DispatcherQueue.TryEnqueue(ComeToTheFront));
Program.Claim?.OnAskedToShow(() => queue.TryEnqueue(ComeToTheFront));
Closed += (_, _) =>
{
// First: from here on a new start opens a window of its own, and does not ask one that is closing.
// A request that was already on its way finds the window closed and does nothing.
_closed = true;
Program.Claim?.Dispose();
// The workspace that is open now is where the next start opens: written here, whatever was written before.
@@ -1658,6 +1663,11 @@ public sealed partial class MainWindow : Window
/// </summary>
private void ComeToTheFront()
{
if(_closed)
{
return;
}
if(AppWindow.Presenter is OverlappedPresenter { State: OverlappedPresenterState.Minimized } presenter)
{
presenter.Restore();
+11 -2
View File
@@ -54,8 +54,17 @@ public static class Program
// was started by the user, so it may pass that right on), and this one //
// ends before any window or file of its own exists. //
/////////////////////////////////////////////////////////////////////////////
Claim = WindowClaim.For(GcloPaths.DataRoot);
if(!Claim.IsTheOnlyOne)
try
{
Claim = WindowClaim.For(GcloPaths.DataRoot);
}
catch(Exception)
{
// No log exists yet, and nothing here is worth not opening for: without a claim the window opens as it always did.
Claim = null;
}
if(Claim is { IsTheOnlyOne: false })
{
_ = AllowSetForegroundWindow(AnyProcess);
Claim.AskTheOtherToShowItself();