using Avalonia; using Avalonia.Media; using DodoSSH.Client.App.Platform; using DodoSSH.Client.Session; using Velopack; namespace DodoSSH.Client.App; internal static class Program { /// /// Entry point. /// /// /// STAThread 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. /// [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); } /// /// Puts WebView2's user data folder beside the vault cache instead of beside the executable. /// /// /// /// WebView2 defaults this to a directory next to the host executable. Under a Velopack install that is /// %LOCALAPPDATA%\DodoSSH.Desktop\current\, and current\ is replaced 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 TerminalWorkspaceOptions.RendererTimeout's fifteen seconds was sized for. It would /// land at the exact moment somebody is most ready to believe the update broke the terminal. /// /// /// The profile directory is the right home because Velopack never touches it — the pack id is /// deliberately not DodoSSH, so the install root and ClientPaths.DataDirectory are /// siblings rather than the same folder. See docs/adr/0013-desktop-distribution-and-updates.md. /// /// /// 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. /// /// private static void KeepTheWebViewProfileOutOfTheInstallDirectory() { if (!OperatingSystem.IsWindows()) { return; } // The same profile directory the rest of the application resolves, channel and all — a nightly // pointing WebView2 at the release build's profile would put two browser profiles in one folder // and hand the pair of them whichever process opened first. var folder = Path.Combine( ClientPaths.ForChannel(DesktopChannel.Name).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. } } /// Used by the designer as well as by . /// /// WithInterFont 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 /// MonoFont 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. /// public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() .WithInterFont() .With(new FontManagerOptions { DefaultFamilyName = "avares://Avalonia.Fonts.Inter/Assets#Inter" }) .LogToTrace(); }