Public Access
Let the desktop client replace itself, and give the repository one version
Packaging for Windows, and the updater that only exists once something is
packaged. Velopack, win-x64, fed from the project's own forge — never from the
deployment a client signs in to, which is ADR 0011 rule 2 carried over
unchanged and is why the feed address is a constant in the code rather than a
setting. See docs/adr/0012-desktop-distribution-and-updates.md.
**Nothing is ever installed while somebody is using it.** A newer build is found
on a six-hourly pass, downloaded in the background, and then waits — for a
restart the user presses, or for the next launch they were going to do anyway.
That is a policy rather than caution: this application argues at length that
locking keeps shells running, because a lock that destroyed work would stop
being used, and a restart does not keep them. Having taught that, it owes the
user the choice at the one moment it stops being true, and the sentence saying
so counts the shells it would close.
**The version is now derived from the v* tag**, by MinVer, for everything. There
was no version before this — no property anywhere, so every assembly reported
the SDK's 1.0.0 and the API served that string as its serverVersion to every
client that asked. The tag was already the version of record for the container
image; this makes it the version of record full stop. MinVer's failure mode is
answering plausibly rather than failing, and here a wrong version is a client
that never updates, so it is guarded twice: fetch-depth 0 on every checkout, and
a step that fails a tag build when the tag and the computed version disagree.
**The pack id is DodoSSH.Desktop and not DodoSSH**, which is the one decision
here that would have destroyed data. Velopack installs to %LOCALAPPDATA%\<packId>
and removes that whole directory on uninstall, and %LOCALAPPDATA%\DodoSSH is
where ClientPaths keeps the encrypted cache, the outbox of changes not yet
pushed, and the device key. The obvious id would have had the uninstaller
silently delete work the server has never seen — the thing the application
refuses to do without a counted confirmation. Velopack's own advice to move user
data to roaming %APPDATA% is declined for the reason ClientPaths already gives.
**Releases are cut by a person, and CI gains no job that could.** The tempting
argument is that a forge write token is not a signing key. It does not survive
contact with what the token does: Velopack clients trust their feed and do not
verify a package signature when they apply one, so whoever can write a release
can ship an update every install runs. That is the capability ADR 0011 rule 1
puts on a machine which is not a runner, reached through a different door. The
mechanical objection — vpk needs Windows and the runners are Linux — is the
smaller of the two and is recorded beside it, because somebody will fix one and
believe they are done.
Unsigned for now, deliberately and with the cost stated where a user reads it:
SmartScreen warns once per person, on Setup.exe, because Mark-of-the-Web is
applied by the browser that downloaded it. In-app updates are fetched by the
application and applied from a local file, and never trip it.
The banner is a fourth row of the window rather than an overlay. Anything drawn
in the terminal's rectangle is sliced by the native child window that composites
above it — the defect this window has shipped once — and a sibling row is the
arrangement TitleBar and StatusBar already prove works.
----
Three defects surfaced on the way, none of them in the feature being built.
**A settings key absent from the file came back as the CLR default, not the
declared one.** The JSON source generator builds a record through a synthesised
parameterised constructor and assigns every property from its argument array, so
a property initializer runs and is then overwritten by a default for anything the
file did not contain. A settings.json of {} read back a font size of 0, clamped
up to the 8px floor rather than the 13px the renderer draws at. It could not bite
while there was one setting, because that setting was written on every save and
so was never absent; adding a second would have turned automatic update checks
off for every existing profile, silently, the opposite of the documented default.
Reflection-based deserialisation of the same JSON answers correctly, which is why
every way of checking it by hand agrees except the one that ships. The defaults
now live on the constructor parameters, which is the only place the generator
reads them from.
**Declaring a RuntimeIdentifier on the desktop head broke the server's image
build.** It is the obvious way to let a self-contained publish restore under
locked mode, and it writes a net10.0/win-x64 target into the lock file of every
project the head references transitively — including DodoSSH.Contracts and
DodoSSH.Crypto, which the API builds too. The Dockerfile restores those with no
RID and fails NU1004. Found by running docker build rather than by reading. The
RID stays out of the committed state; the two commands that need one ask for it
unlocked, and the release script puts the lock files back.
**A Docker ARG named VERSION silently sets MSBuild's Version.** An ARG is an
environment variable for the rest of the stage, MSBuild reads environment
variables as properties, and property names are case-insensitive. With the
workflow passing main-<short sha> on a main build the publish died with
NETSDK1018 pointing at DodoSSH.Contracts, a project nobody had touched. The build
stage's argument is ASSEMBLY_VERSION now, empty except on a tag build.
All three are in docs/platform-flags.md, which is where the next person will look.
----
Verified: the whole solution builds and restores locked; 289 shell, 93 layout and
54 session tests pass, including the regression test for the settings defect and
a measurement of the banner at the window's minimum width. vpk pack runs end to
end and reports "Verified VelopackApp.Run()" against Program.Main. The API image
builds correctly both as a main build and as a tag build, carrying 1.0.0 and
0.1.0 respectively.
Not verified, and it needs a published release to be: installing, updating and
uninstalling on a real machine. That is Phase 15 of docs/manual-checks.md, and
the pack id and the WebView2 profile fix are reasoned and commented but only
proved by walking it. Two things to watch at the first upload — the reverse
proxy's body-size limit for a 64 MB asset, and whether vpk upload gitea is happy
with Gitea 1.27.1.
This commit is contained in:
+86
-2
@@ -217,8 +217,33 @@ execution failed") and the terminal never appears. Hit while building the harnes
|
||||
that failed from a ~230-character directory ran first time from `%TEMP%\h`. The exact threshold was not
|
||||
established and the mechanism is unconfirmed — the user data folder is created beside the executable by
|
||||
default and the browser process is launched with paths derived from it, so `MAX_PATH` is the obvious
|
||||
suspect. Relevant to packaging: an installer that lands under a deep per-user path would break the
|
||||
terminal with an error that names nothing.
|
||||
suspect.
|
||||
|
||||
*Measured for the packaged layout, so this stops being a worry and becomes a number.* Velopack installs to
|
||||
`%LOCALAPPDATA%\DodoSSH.Desktop\current\`, and `…\AppData\Local\DodoSSH.Desktop\current\DodoSSH.exe` is
|
||||
**64 characters** against the ~230 that reproduced the failure — about 180 characters of headroom, and a
|
||||
40-character corporate username adds 35 of them back. The shipped installer is not at risk. Two things
|
||||
would reopen it and neither is in the plan: a self-extracting single-file publish, whose native libraries
|
||||
land under a hashed temp path, and `%LOCALAPPDATA%` folder-redirected to a deep UNC path in a domain.
|
||||
Manual check 15.2 measures it on the real machine rather than trusting this paragraph.
|
||||
|
||||
**WebView2's user data folder must be kept out of the install directory.** It defaults to a directory
|
||||
beside the host executable, which under Velopack is inside `current\` — and `current\` is *replaced* by
|
||||
every update. Left alone, the browser profile would be destroyed on each one, so the first connect after
|
||||
every update would pay a cold WebView2 start: a fresh user-data directory and a new process tree, which is
|
||||
the slow path `RendererTimeout`'s fifteen seconds was sized for, arriving at the exact moment somebody is
|
||||
most ready to believe the update broke the terminal. `Program.Main` sets `WEBVIEW2_USER_DATA_FOLDER` to
|
||||
`%LOCALAPPDATA%\DodoSSH\WebView2` — under the profile directory, which Velopack never touches. Check 15.8
|
||||
is what would notice it regressing, and it is worth having because the symptom is "slow but working", which
|
||||
gets dismissed as a fluke.
|
||||
|
||||
**The install root and the profile directory must not be the same folder.** Velopack removes
|
||||
`%LOCALAPPDATA%\<packId>` entirely on uninstall, and `ClientPaths` puts `cache.db` (plus `-wal` and `-shm`),
|
||||
`settings.json` and `device.key` in `%LOCALAPPDATA%\DodoSSH`. So a pack id of `DodoSSH` — the obvious
|
||||
choice — would have made the uninstaller delete the vault cache and the outbox of changes not yet pushed,
|
||||
silently, which is the thing the application will not do without a counted confirmation. The pack id is
|
||||
`DodoSSH.Desktop` for that reason and no other; `--packTitle` supplies the name people see, so nothing is
|
||||
lost. Do not "tidy" it. See [ADR 0012](adr/0012-desktop-distribution-and-updates.md) and check 15.9.
|
||||
|
||||
**The Windows app manifest must declare a `supportedOS` list.** Without it the process reports a
|
||||
downlevel Windows version and Avalonia's native control host fails outright — *"Unable to create child
|
||||
@@ -329,6 +354,15 @@ AppContainer where loopback connections are blocked without a `CheckNetIsolation
|
||||
terminal data plane *is* a loopback WebSocket, so MSIX would break the product outright. Velopack
|
||||
for Windows/macOS/AppImage; Flatpak and deb/rpm defer updates to the package manager.
|
||||
|
||||
*Checked rather than assumed, now that Velopack is actually wired up:* its Windows path does not
|
||||
reintroduce the thing MSIX was ruled out for. `Setup.exe` is an ordinary Win32 executable that unpacks a
|
||||
directory under `%LOCALAPPDATA%` and creates shortcuts — there is no `AppxManifest`, no package identity,
|
||||
no `runFullTrust`, no elevation and no execution alias, so the process stays an ordinary desktop process
|
||||
and WebView2 stays out of an AppContainer. That is reasoning, not measurement; manual check 15.4 is the
|
||||
measurement, because if a package identity ever did appear the symptom would be the terminal hanging and
|
||||
then reporting the WebView2 message after fifteen seconds, which reads like a broken runtime rather than
|
||||
like packaging.
|
||||
|
||||
**Linux ships AppImage and Flatpak first**, specifically so the WebKit runtime is bundled rather
|
||||
than assumed present on the user's machine.
|
||||
|
||||
@@ -486,6 +520,56 @@ so it cannot be discovered from Kestrel afterwards. The window for another proce
|
||||
few milliseconds; if the suite ever fails with an address-in-use, this is why, and a retry is the fix
|
||||
rather than a redesign.
|
||||
|
||||
**A RID must never reach the committed lock files, and the obvious fix for a RID-specific publish puts
|
||||
one there.** `dotnet publish -r win-x64` resolves a graph the committed `packages.lock.json` files do not
|
||||
describe — they carry a `net10.0` target and nothing else — so under locked mode it fails NU1004. The
|
||||
obvious answer is `<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>` on the desktop head plus a
|
||||
`--force-evaluate` to regenerate. **That is wrong here, and it was tried and reverted.**
|
||||
|
||||
A RID declared on one project flows to every project it references transitively while restoring, so the
|
||||
regenerated lock files for `DodoSSH.Contracts` and `DodoSSH.Crypto` grew a `net10.0/win-x64` target as
|
||||
well — and those two are built by the *server*. The API's Dockerfile restores them with no RID and
|
||||
`--locked-mode`, so it failed:
|
||||
|
||||
```
|
||||
error NU1004: The project's runtime identifiers have changed from.
|
||||
Project's runtime identifiers: , lock file's runtime identifiers win-x64.
|
||||
```
|
||||
|
||||
Packaging the desktop client had broken the server's image build, and nothing but the `image` job would
|
||||
have caught it. Found by running `docker build` locally rather than by reading the lock files.
|
||||
|
||||
So the RID stays out of the committed state, and the two commands that need one — the release script's
|
||||
publish and the `windows publish still resolves` step in `ci.yml` — pass `-p:RestoreLockedMode=false` for
|
||||
themselves alone. That restore rewrites the lock files as a side effect, which does not matter on a runner
|
||||
whose checkout is discarded and does matter on a developer's machine, so the release script runs
|
||||
`git checkout -- '*packages.lock.json'` afterwards. `-p:RestorePackagesWithLockFile=false` is not an
|
||||
alternative: it fails NU1005 whenever a lock file already exists.
|
||||
|
||||
**A Docker `ARG` named `VERSION` silently sets MSBuild's `Version`.** An `ARG` is an environment variable
|
||||
for the rest of the stage, MSBuild reads environment variables as global properties, and MSBuild property
|
||||
names are case-insensitive — so `ARG VERSION` in a build stage sets `Version` for every project built in
|
||||
it, with no line anywhere saying so. The workflow passes `main-<short sha>` on a main build, which is a
|
||||
fine docker tag and not a version, and the publish died with `NETSDK1018: Invalid NuGet version string`
|
||||
pointing at `DodoSSH.Contracts` — a project nobody had touched. The build stage's argument is therefore
|
||||
`ASSEMBLY_VERSION`, passed empty except on a tag build; the `VERSION` arg in the final stage is only ever
|
||||
an OCI label and never meets MSBuild. Renaming is the entire fix, and the reason it is written down is that
|
||||
the symptom names the wrong project and the cause is invisible.
|
||||
|
||||
**System.Text.Json's source generator does not honour property initializers on a record.** Defaults for a
|
||||
`ClientSettings`-style record must live on the **constructor parameters**, not on property initializers,
|
||||
and getting it wrong fails silently in the worst direction. The generator emits an
|
||||
`ObjectWithParameterizedConstructorCreator` — it treats the init-only properties as constructor arguments
|
||||
and builds `new ClientSettings() { A = (T)args[0], … }`, so the initializer runs and is then overwritten by
|
||||
`args`, which for a member absent from the JSON is the CLR default. Measured: a `settings.json` of `{}`
|
||||
read back `TerminalFontSize` 0 (clamped up to the 8px floor, not the 13px the renderer draws at) and, once
|
||||
it existed, `AutomaticUpdateChecks` false. **Reflection-based deserialisation of the same JSON answers 13
|
||||
and true**, which is what makes it so easy to miss — every way of checking it by hand is right except the
|
||||
one that ships. `JsonSourceGenerationMode.Metadata` does not help; it was tried. It stayed invisible while
|
||||
there was one setting, because that setting was written on every save and so was never absent; it went live
|
||||
the moment a second one was added, since every existing profile lacks the new key.
|
||||
`ASettingAbsentFromTheFile_ComesBackAsItsDeclaredDefault` fails without the fix.
|
||||
|
||||
**`[CallerFilePath]` is rewritten to `/_/...` under `ContinuousIntegrationBuild`.** Any test that
|
||||
locates a fixture by source path passes locally and fails in CI. Copy fixtures to the output
|
||||
directory and read them via `AppContext.BaseDirectory` instead; `GoldenVectorTests` shows the
|
||||
|
||||
Reference in New Issue
Block a user