using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using DodoSSH.Client.App.Terminal;
using DodoSSH.Client.App.ViewModels;
using DodoSSH.Client.App.Views;
using DodoSSH.Client.Auth;
using DodoSSH.Client.Session;
using DodoSSH.Client.Ssh;
using DodoSSH.Client.Storage;
using DodoSSH.Client.Terminal;
namespace DodoSSH.Client.App;
///
/// The Avalonia application.
///
///
/// Named DodoSshApp rather than the conventional App only because the assembly's root
/// namespace already ends in App, and a type whose name matches its namespace forces every
/// ambiguous reference to be fully qualified.
///
internal sealed partial class DodoSshApp : Application
{
///
public override void Initialize() => AvaloniaXamlLoader.Load(this);
///
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
Compose(desktop);
}
base.OnFrameworkInitializationCompleted();
}
///
///
/// Composed by hand rather than through a container. The graph is a handful of objects deep and an
/// indirection to read through would buy nothing at this size.
///
///
/// Everything disposable is a local captured by the closures below rather than a field, because an
/// Avalonia Application has no disposal hook of its own and a type that owned them would have
/// nowhere honest to release them.
///
///
private static void Compose(IClassicDesktopStyleApplicationLifetime desktop)
{
var paths = ClientPaths.Default;
var caches = ClientCacheFactory.ForFile(paths.CacheFile);
// Known hosts live in the vault, so trust survives a restart and follows the user to every device.
// Composed here, once, because the connection factory below needs it now and outlives every unlock;
// the vault behind it is attached and detached as one is opened and locked. See VaultKnownHostStore
// for why the handshake is answered from a snapshot rather than by reading the vault per lookup.
var knownHosts = new VaultKnownHostStore();
// One factory for both kinds of connection. Shells and file transfers start with the same handshake
// and the same host key decision, and composing two would mean two snapshots of the pins.
var connections = new SshNetConnectionFactory(knownHosts);
var workspace = new TerminalWorkspace(
new AvaloniaTerminalAssetProvider(),
connections,
TimeProvider.System);
workspace.Start();
var browser = new SystemBrowserLauncher();
// Chosen once, here, because it is a property of the machine and not of any session. A computer with
// a usable TPM gets the store that keeps a device key behind a Windows consent prompt; anything else
// gets one that reports itself unavailable, so unlock keeps asking for the passphrase. See ADR 0007.
var deviceKeys = DeviceKeyStores.ForThisMachine(paths);
var viewModel = new MainWindowViewModel(
paths,
caches,
workspace,
knownHosts,
deviceKeys,
async (url, cancellationToken) => await ServerConnection
.SignInAsync(url, browser, TimeProvider.System, cancellationToken)
.ConfigureAwait(false),
TimeProvider.System,
connections,
passphraseProfile: null,
// The other half of signing in: a refresh grant, no browser, and nobody present. It is what
// makes a launch after the first one arrive online rather than merely enrolled.
resume: async (url, refreshToken, cancellationToken) => await ServerConnection
.ResumeAsync(url, refreshToken, TimeProvider.System, cancellationToken)
.ConfigureAwait(false));
desktop.MainWindow = new MainWindow { DataContext = viewModel };
// Started rather than awaited: the framework's initialisation must not block on a schema
// migration. The view model shows its own progress and handles its own failures, which is why
// discarding the task here is safe rather than merely convenient.
_ = viewModel.StartAsync(CancellationToken.None);
WireShutdown(desktop, viewModel, workspace, caches);
}
///
/// Shutdown is deferred rather than blocked on. Sessions hold SSH connections and a listening socket, and
/// blocking the UI thread on their disposal is how an application comes to take several seconds to close —
/// or deadlocks, if any of that disposal needs the UI thread.
///
private static void WireShutdown(
IClassicDesktopStyleApplicationLifetime desktop,
MainWindowViewModel viewModel,
TerminalWorkspace workspace,
ClientCacheFactory caches)
{
var shuttingDown = false;
desktop.ShutdownRequested += async (_, e) =>
{
if (shuttingDown)
{
return;
}
shuttingDown = true;
e.Cancel = true;
// The view model first: it holds the vault session, and disposing that is what zeroes the
// identity keys, the vault keys and the cache key.
await viewModel.DisposeAsync().ConfigureAwait(true);
await workspace.DisposeAsync().ConfigureAwait(true);
caches.Dispose();
desktop.Shutdown();
};
}
}