Files
DodoSSH/src/DodoSSH.Client.App/Program.cs
T
jaap-jan 3ead865f01 Merge branch 'main' into the desktop updater, and give way on two numbers
Main landed a realtime push feature while this branch was building the updater,
and the two collided in three places. Every one of them resolves the same way:
main got there first, so this branch moves.

**Two ADRs were both numbered 0012.** Main's is realtime push; this one is now
[ADR 0013](docs/adr/0013-desktop-distribution-and-updates.md). Git did not call
this a conflict — the filenames differ — so it would have merged quietly and left
the directory with two 0012s and every cross-reference ambiguous. Renumbered here
along with the nine places that point at it.

**Two manual-check phases were both numbered 15**, and that one git did catch.
Main's "Changes that arrive without a timer" keeps 15; installing and updating
the desktop client becomes Phase 16, with its checks and every reference to them
renumbered. The file's own rule is that a number is for life, which is exactly
why the one that had not been pushed is the one that gives way.

**The merge rewrote several files with CRLF**, and `.editorconfig` asks for LF on
everything except `*.ps1`. That is not cosmetic here: IDE0055 is an error and
`EnforceCodeStyleInBuild` is on, so it failed the build on three lines of
App.axaml.cs whose only change in this branch was an ADR number in a comment.
Forty-six files normalised back to LF; the release script keeps CRLF, which is
what `.gitattributes` and `.editorconfig` both already say for a PowerShell file.

Nothing else conflicted. The updater does not touch the sync loop or the event
stream, and the one file both sides edited heavily — MainWindowViewModel — merged
without a hunk in common.

Verified after merging: the solution restores locked and builds clean, and 304
shell, 100 layout, 54 session, 28 client-api and 25 contracts tests pass. The
first two counts are higher than before the merge because main's own tests came
with it and pass alongside these.
2026-08-04 17:52:57 +02:00

104 lines
5.0 KiB
C#

using Avalonia;
using Avalonia.Media;
using DodoSSH.Client.Session;
using Velopack;
namespace DodoSSH.Client.App;
internal static class Program
{
/// <summary>
/// Entry point.
/// </summary>
/// <remarks>
/// <c>STAThread</c> is required, not decorative: WebView2 checks the apartment state and refuses
/// to initialise on an MTA thread. Without it the terminal is simply blank on Windows. It applies to
/// everything below, which is why the Velopack call lives inside this method rather than in an entry
/// point of its own.
/// </remarks>
[STAThread]
public static void Main(string[] args)
{
// First, before Avalonia is even configured.
//
// The installer, the updater and the uninstaller all re-run this executable with arguments that
// mean "do the install bookkeeping and stop". Run() is what notices, does it, and exits — so on
// those runs nothing below happens at all, and that is the point rather than a side effect:
// DodoSshApp.Compose opens the SQLite cache and starts the terminal workspace's listening socket,
// and a silent installer run that reached either would be a background process holding the cache
// file open during the very file operations the installer is performing.
//
// There are deliberately no OnFirstRun or OnAfterUpdate hooks. A hook process has no passphrase,
// so the cache is bytes it cannot read, and the one thing that would want doing after an update —
// a schema migration — already runs on every ordinary launch from MainWindowViewModel.StartAsync,
// before unlock and touching no encrypted content.
VelopackApp.Build().Run();
KeepTheWebViewProfileOutOfTheInstallDirectory();
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
/// <summary>
/// Puts WebView2's user data folder beside the vault cache instead of beside the executable.
/// </summary>
/// <remarks>
/// <para>
/// WebView2 defaults this to a directory next to the host executable. Under a Velopack install that is
/// <c>%LOCALAPPDATA%\DodoSSH.Desktop\current\</c>, and <c>current\</c> is <em>replaced</em> by every
/// update — so the browser profile would be destroyed on each one, and the first connect afterwards
/// would pay a cold WebView2 start: a new user data directory and a fresh process tree, which is the
/// slow path <c>TerminalWorkspaceOptions.RendererTimeout</c>'s fifteen seconds was sized for. It would
/// land at the exact moment somebody is most ready to believe the update broke the terminal.
/// </para>
/// <para>
/// The profile directory is the right home because Velopack never touches it — the pack id is
/// deliberately not <c>DodoSSH</c>, so the install root and <c>ClientPaths.DataDirectory</c> are
/// siblings rather than the same folder. See docs/adr/0013-desktop-distribution-and-updates.md.
/// </para>
/// <para>
/// An environment variable rather than the control's own options, because it is read by the WebView2
/// loader before any of this application's UI exists, and because it needs no reference to whichever
/// WebView package the terminal happens to be hosted by.
/// </para>
/// </remarks>
private static void KeepTheWebViewProfileOutOfTheInstallDirectory()
{
if (!OperatingSystem.IsWindows())
{
return;
}
var folder = Path.Combine(ClientPaths.Default.DataDirectory, "WebView2");
try
{
Directory.CreateDirectory(folder);
Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", folder);
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
// Left unset, which puts the profile back beside the executable. That is a slow first connect
// after each update, not a broken terminal, and refusing to start an SSH client over it would
// be the wrong trade.
}
}
/// <summary>Used by the designer as well as by <see cref="Main"/>.</summary>
/// <remarks>
/// <c>WithInterFont</c> registers Inter; it does not make it the default, and until this line the
/// application shipped a font it then declined to use — falling back to Segoe UI on Windows and to
/// whatever fontconfig offered on Linux. Almost nothing visible moves, because App.axaml sets
/// <c>MonoFont</c> on essentially every control that draws text, but the fallback behind those is now
/// a font that travels with the build rather than one the machine is assumed to have. The layout
/// suite pins the same family, and has to: see HeadlessApp.
/// </remarks>
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<DodoSshApp>()
.UsePlatformDetect()
.WithInterFont()
.With(new FontManagerOptions { DefaultFamilyName = "avares://Avalonia.Fonts.Inter/Assets#Inter" })
.LogToTrace();
}