Public Access
Put a window on screen while it starts, and refuse the second one
Two things a desktop application is expected to do and this one did not. ◆ THREE SECONDS OF NOTHING. Measured on a debug build against a cold profile: the main window arrives 3.1 seconds after the executable does. Nothing was on screen for any of it — the icon had been double-clicked and the desktop looked unchanged. The main window cannot come sooner. It needs a view model, and the view model needs composition to have opened the SQLite cache, bound the terminal's loopback socket, asked the platform about a TPM and read the install layout off disk. Its own IsStarting card already narrates what happens after it appears; this is the part before, which no arrangement of that window can cover. So SplashWindow, and it is measurably the point: 668 ms to the splash, 3.1 s to the window that replaces it. Two mechanics are load-bearing rather than incidental. The splash *is* desktop.MainWindow for a moment. The lifetime shows whatever that property holds once OnFrameworkInitializationCompleted returns; a window merely Show()n there would have no lifetime attached, and under ShutdownMode.OnLastWindowClose the first close of anything would end the process. Handing the property over is one assignment and Avalonia reads it lazily. And composition is posted at DispatcherPriority.Background rather than called. It is synchronous and slow — which is the entire argument for a splash — so running it inline would block the very dispatcher turn that has to paint the thing. The window is SizeToContent="Height", which is about the failure and not the splash: CouldNotStart carries an exception's own message, and inside a fixed height a long one would wrap and be clipped at exactly the moment that text is all the window is for. A composition that throws is caught rather than left to the dispatcher, for the same reason — before this there was nothing on screen to abandon, and now there is, so an escaping exception would leave a window saying "Starting…" over a process that had stopped. Escape closes it, and only then. ◆ AND ONE INSTANCE. Claimed in Main before Avalonia is configured, so a second launch starts nothing at all: no toolkit, no window, no splash flashing up before it thinks better of it. Exclusion is an exclusive handle on instance.lock inside the profile directory, and that location settles two questions at once. A nightly and a release build do not exclude each other — separate profiles, ADR 0013 — and two people signed in to one machine each get their own instance. A global mutex would have had to re-derive both from somewhere. A file handle rather than a named mutex because the kernel releases it however the process ends. There is no such thing as a stale lock here, which is a state a named mutex makes both platforms model differently and neither model simply. The second launch is not silent. Somebody who double-clicks an icon and sees nothing has been told the application is broken, so it rings a named pipe — named from a digest of the profile path, so it is scoped exactly as the lock is — and the running instance un-minimises and activates. Activate is a request and not a guarantee: Windows refuses the foreground to a process that did not receive the input, and the flashing taskbar button that results is still an answer. Three seconds of retrying when the lock is held by something that does not answer its pipe, and that window has one job. Velopack's apply ends this process and its updater starts the new build; the two are ordered, but a lock held a moment too long would make the successor refuse itself and vanish — an update that looked like an uninstall. The doorbell is disposed before Shutdown for the same reason, so the pipe name is free before the process is. ◆ VERIFIED BY RUNNING IT, WHICH IS THE ONLY WAY EITHER OF THESE COULD BE. Two launches of the built executable: the first stayed up with instance.lock in the profile, the second exited. Window sizes over time are where the two timings above come from — 420x160 then 1196x776. SingleInstanceTests covers what a single process can reach: the lock, its release by disposal, the doorbell answering more than once, and two profiles not excluding each other. SplashCardTests measures the one thing the splash can get wrong, which is a failure message long enough to need the height the window is willing to give it. Checks 1.12 and 16.11 are what needs a second executable and a real screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aff776cb42
commit
b8c94ecb73
12 files changed
+1044
-3
No files matched your search
@@ -252,6 +252,34 @@ registered in the capture phase, that no native menu is drawn once it is prevent
|
||||
`ClipboardWriter` and `ClipboardReader` in each `App.axaml.cs` — and the view model is deliberately silent
|
||||
when neither is there, because there is no status line on this screen to say it on.
|
||||
|
||||
### 1.12 Something is on screen while it starts
|
||||
|
||||
Close DodoSSH and start it again. Watch the first second.
|
||||
|
||||
**Pass:** a small panel — about 420 pixels wide, no titlebar — appears saying **DodoSSH** and *Starting…*
|
||||
with a bar moving under it, and is then replaced by the main window. Measured on a debug build against a cold
|
||||
profile: the panel at 668 ms, the window at 3.1 s. A release install is quicker and the shape is the same.
|
||||
What must never come back is the old behaviour, which was nothing at all on screen for the whole of it.
|
||||
|
||||
**Pass, and worth checking separately:** the panel is **gone** once the window is up, there is never a moment
|
||||
with two windows both claiming to be DodoSSH, and clicking another application while the panel is up puts
|
||||
that application in front of it. The last one is deliberate — a splash that stays on top is a splash sitting
|
||||
over whatever you gave up and went back to.
|
||||
|
||||
**And the failure state, which is the part with something to get wrong.** It needs provoking: put a
|
||||
*directory* where `cache.db` should be — `%LOCALAPPDATA%\DodoSSH.Nightly\cache.db` for the nightly — and
|
||||
start the application.
|
||||
|
||||
**Pass:** the panel stays, the bar stops moving, and it reads *DodoSSH could not start:* followed by the
|
||||
reason and *Press Escape to close.* Escape closes it. Remove the directory afterwards.
|
||||
|
||||
**Failure means:** a panel left saying *Starting…* is the main window having failed to show with nothing
|
||||
catching it, which is the one outcome worse than the crash it replaced — `DodoSshApp.ComposeBehind` exists to
|
||||
make it impossible. Two windows at once is the show-then-close order in that method having been swapped:
|
||||
closing the only open window ends the process, so the real window is shown while the panel is still up.
|
||||
Nothing appearing at all means composition is running in the same dispatcher turn that has to paint the
|
||||
panel, which is what posting it at `DispatcherPriority.Background` prevents.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Known Hosts as its own page
|
||||
@@ -2890,6 +2918,43 @@ offer a nightly, ever, however many have been published since. It reads a differ
|
||||
prereleases; if a nightly version is ever offered there, stop and treat it as a release-channel incident
|
||||
rather than as a bug in the nightly.
|
||||
|
||||
### 16.11 One instance, and what a second launch does instead
|
||||
|
||||
With DodoSSH running, start it again — Start menu, desktop shortcut, or the executable directly. Do it three
|
||||
times: with the window in front, with it minimised, and with it behind another application.
|
||||
|
||||
**Pass:** no second window appears, and the one already open comes forward, un-minimised if it was
|
||||
minimised. **A flashing taskbar button rather than a raised window is also a pass**: Windows refuses the
|
||||
foreground to a process that did not receive the input, which is its own rule about focus stealing and not
|
||||
something to defeat. Nothing happening at all is the failure.
|
||||
|
||||
**And the side-by-side case, which is the whole reason the lock lives where it does.** 16.10's arrangement,
|
||||
both builds installed, both started.
|
||||
|
||||
**Pass:** both run. They are separate applications with separate profiles, and the lock is a file inside the
|
||||
profile — so neither can see the other's. Two people signed in to one machine is the same property from the
|
||||
other direction and needs no check of its own.
|
||||
|
||||
**And the update relaunch · needs a release.** Take an update (16.7) and press RESTART NOW.
|
||||
|
||||
**Pass:** the application comes back. This is the race worth naming: the updater starts the new build as the
|
||||
old one is exiting, and a lock held a moment too long would make the new process refuse itself and disappear
|
||||
— an update that looked like an uninstall. `SingleInstance.Claim` keeps trying for three seconds while the
|
||||
lock is held by something that does not answer its pipe, which is what covers it.
|
||||
|
||||
**Failure means:** a second window means the lock is not being taken. A profile directory that cannot be
|
||||
written to answers "you are the only one" on purpose — refusing to start an SSH client over a permissions
|
||||
problem would be the wrong trade — so check that the profile exists, is writable, and has an
|
||||
`instance.lock` in it while the application runs. A second launch that exits without raising the first means
|
||||
the lock worked and the doorbell did not: the pipe name is derived from the profile path, so two processes
|
||||
disagreeing about it means they disagree about which profile they are in. Two windows *after an update* is
|
||||
the opposite failure — the outgoing process never released the lock, which means something is holding it
|
||||
open past `Shutdown`.
|
||||
|
||||
`SingleInstanceTests` covers everything reachable without a second process: the lock, its release, the
|
||||
doorbell answering more than once, and two profiles not excluding each other. What is left for a person is a
|
||||
second *executable* and a real window coming forward.
|
||||
|
||||
---
|
||||
|
||||
## Phase 17 — Installing and updating the phone
|
||||
|
||||
Reference in new issue
Block a user