feat(app): gclo opens one window per data folder, and a second start brings the open one to the front #217

Merged
james.maes merged 2 commits from feat/216-one-window-per-data-folder into main 2026-11-11 22:55:17 +00:00
Owner

What

gclo opens one window per data folder. Starting it again brings the open window to the front and ends.

  • WindowClaim in gclo.ViewModels: a file in the data folder (window.lock) held open and shared with nobody while the window is open, and a named event that asks the holder to come to the front.
  • Program.Main makes the claim after Velopack's hooks and before any window; a second start signals the first and returns.
  • MainWindow comes to the front when asked (restores from minimized), and gives the claim up first thing when it closes.
  • The command line makes no claim. Another data folder (dev channel, GCLO_DATA_DIR) is another claim.

Why

Closes #216. Two windows each kept their own copy of the settings and accounts; the one that saved last undid the other. The owner decided on one window per data folder instead of making every shared file safe for two writers.

A lock file instead of a named mutex: no thread ownership to get wrong, released by Windows however the process ends, and keyed by the folder itself.

How it was tested

  • WindowClaimTests: first claim is the only one, a second is not, another folder is another claim, a claim given up is free, a request reaches the holder once per request and no other folder's holder, a folder that cannot be written to stops no window, the key is the same for every spelling of a folder.
  • UI suite: StartingGcloAgain_OnTheSameDataFolder_EndsAtOnce_AndBringsTheOpenWindowBack minimizes the window, starts a second gclo on the same data folder, and checks that it ends by itself with exit code 0 and that the window comes back.
  • Gates from PowerShell: engine and view models 1,482, CLI 249, line coverage 100% on the three gated assemblies, format clean, UI suite 19 of 19.

Checklist

  • Commits are signed and signed off (git commit -S -s)
  • Zero build warnings; format and lint are clean
  • Tests added or updated; the 100% coverage gate still passes
  • Docs updated for user-visible changes; threat model updated if the attack surface changed (README, threat model review log, CLAUDE.md)
  • No secrets, credentials, or personal data
  • New dependencies are justified below, or none were added (none)
  • Substantial AI assistance, if any, is noted here: written with Claude Code.

🤖 Generated with Claude Code

## What gclo opens one window per data folder. Starting it again brings the open window to the front and ends. - `WindowClaim` in `gclo.ViewModels`: a file in the data folder (`window.lock`) held open and shared with nobody while the window is open, and a named event that asks the holder to come to the front. - `Program.Main` makes the claim after Velopack's hooks and before any window; a second start signals the first and returns. - `MainWindow` comes to the front when asked (restores from minimized), and gives the claim up first thing when it closes. - The command line makes no claim. Another data folder (dev channel, `GCLO_DATA_DIR`) is another claim. ## Why Closes #216. Two windows each kept their own copy of the settings and accounts; the one that saved last undid the other. The owner decided on one window per data folder instead of making every shared file safe for two writers. A lock file instead of a named mutex: no thread ownership to get wrong, released by Windows however the process ends, and keyed by the folder itself. ## How it was tested - `WindowClaimTests`: first claim is the only one, a second is not, another folder is another claim, a claim given up is free, a request reaches the holder once per request and no other folder's holder, a folder that cannot be written to stops no window, the key is the same for every spelling of a folder. - UI suite: `StartingGcloAgain_OnTheSameDataFolder_EndsAtOnce_AndBringsTheOpenWindowBack` minimizes the window, starts a second gclo on the same data folder, and checks that it ends by itself with exit code 0 and that the window comes back. - Gates from PowerShell: engine and view models 1,482, CLI 249, line coverage 100% on the three gated assemblies, format clean, UI suite 19 of 19. ## Checklist - [x] Commits are signed and signed off (`git commit -S -s`) - [x] Zero build warnings; format and lint are clean - [x] Tests added or updated; the 100% coverage gate still passes - [x] Docs updated for user-visible changes; threat model updated if the attack surface changed (README, threat model review log, CLAUDE.md) - [x] No secrets, credentials, or personal data - [x] New dependencies are justified below, or none were added (none) - [x] Substantial AI assistance, if any, is noted here: written with Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
james.maes added 2 commits 2026-11-11 22:54:55 +00:00
Two windows on one data folder each kept their own copy of the settings and
accounts, and the one that saved last undid what the other had saved. The
owner's decision (#216): one window per data folder.

- WindowClaim (gclo.ViewModels): a window holds window.lock in its data
  folder while it is open, shared with nobody. Windows gives it up when the
  process ends, however it ends. A folder that cannot be written to holds no
  claim and stops no window from opening.
- A second start finds the claim, asks the open window to come to the front
  through an event named after the folder, and ends before any window or
  file of its own exists. The open window comes out of the taskbar and
  before the other windows.
- The claim is given up first thing when the window closes, so a start right
  after closing opens a window of its own.
- Another data folder is another claim: the dev channel beside the stable
  one, and GCLO_DATA_DIR. The command line makes no claim.
- README, threat model (review log), CLAUDE.md.

Closes #216

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
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
15f263676e
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>
james.maes force-pushed feat/216-one-window-per-data-folder from 70bee9e21f to 15f263676e 2026-11-11 22:54:55 +00:00 Compare
Author
Owner

One independent review: merge after fixes. It ran the claim under load (eight processes started at once, 25 rounds: one winner every time; a killed holder frees the folder; a restart after update is not swallowed, by reading Velopack). Fixed in 15f2636: the event that asks the open window forward can be unusable (a gclo that runs elevated made it) and no longer ends the start; a window that could not be made gives the claim up; a request that arrives while the window closes is dropped. Gates after the fixes: engine and view models 1,483, CLI 249, line coverage 100%, format clean, UI suite 19 of 19.

Accepted, not fixed (rare and short-lived, per the review): another program holding window.lock open (a leftover file being scanned) turns a start away until it lets go; a file left pending delete lets a second window open once; the same folder reached through another session, another machine on a shared GCLO_DATA_DIR, or a junction spelling is not recognised as the same claim's event. Not checked by anyone: whether the window really takes the foreground when gclo is started from Explorer (the UI test checks that it leaves the taskbar).

One independent review: **merge after fixes**. It ran the claim under load (eight processes started at once, 25 rounds: one winner every time; a killed holder frees the folder; a restart after update is not swallowed, by reading Velopack). Fixed in 15f2636: the event that asks the open window forward can be unusable (a gclo that runs elevated made it) and no longer ends the start; a window that could not be made gives the claim up; a request that arrives while the window closes is dropped. Gates after the fixes: engine and view models 1,483, CLI 249, line coverage 100%, format clean, UI suite 19 of 19. Accepted, not fixed (rare and short-lived, per the review): another program holding window.lock open (a leftover file being scanned) turns a start away until it lets go; a file left pending delete lets a second window open once; the same folder reached through another session, another machine on a shared GCLO_DATA_DIR, or a junction spelling is not recognised as the same claim's event. Not checked by anyone: whether the window really takes the foreground when gclo is started from Explorer (the UI test checks that it leaves the taskbar).
james.maes merged commit e2e099ed08 into main 2026-11-11 22:55:17 +00:00
Sign in to join this conversation.