Public Access
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.
104 lines
5.0 KiB
C#
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();
|
|
}
|