using System.Runtime.Versioning; using DodoSSH.Client.Session; using Microsoft.Win32; namespace DodoSSH.Client.App.Platform; /// /// Registers this executable as the handler for dodossh:// links, for this user, on Windows. /// /// /// /// Under HKCU\Software\Classes, because the install is per user. Velopack installs into the user's /// own profile with no elevation, and a handler registered machine-wide would need the elevation the /// installer deliberately never asks for. Windows merges this key into HKEY_CLASSES_ROOT for the /// user, which is all a browser consults. /// /// /// Written by Velopack's install and update hooks, removed by its uninstall hook — see /// Program.Main. An update rewrites it because the first update to a build that has this is how an /// existing installation gets it at all, and because it costs nothing to say again. /// /// /// Removed only when it is still ours. The release and the nightly are separate installations that /// both answer dodossh://, and the last one installed holds the key. Uninstalling the other must not /// take the handler away from the one that is still there. /// /// [SupportedOSPlatform("windows")] internal static class WindowsUrlScheme { private const string KeyPath = @"Software\Classes\" + SpelunkLink.Scheme; private const string CommandPath = @"shell\open\command"; /// Points dodossh:// at . internal static void Register(string executable) { using var scheme = Registry.CurrentUser.CreateSubKey(KeyPath); scheme.SetValue(string.Empty, "URL:DodoSSH"); // Empty, and its presence is the whole of it: this value is what makes the key a URL scheme rather // than a file type. scheme.SetValue("URL Protocol", string.Empty); using (var icon = scheme.CreateSubKey("DefaultIcon")) { icon.SetValue(string.Empty, $"\"{executable}\",0"); } using var command = scheme.CreateSubKey(CommandPath); command.SetValue(string.Empty, CommandFor(executable)); } /// Takes the handler away, if it is still 's. internal static void Unregister(string executable) { string? registered; using (var command = Registry.CurrentUser.OpenSubKey($@"{KeyPath}\{CommandPath}")) { registered = command?.GetValue(string.Empty) as string; } if (IsOurs(registered, executable)) { Registry.CurrentUser.DeleteSubKeyTree(KeyPath, throwOnMissingSubKey: false); } } /// The command Windows runs for a link: this executable, with the link as its one argument. /// /// Both halves quoted. The path has spaces in it on any profile whose user name does, and the link is /// handed over whole as %1 — which Windows quotes into one argument only because it is quoted /// here. Program.Main then reads it back and checks it before anything acts on it. /// internal static string CommandFor(string executable) => $"\"{executable}\" \"%1\""; /// Whether a registered command is the one writes for this executable. internal static bool IsOurs(string? registered, string executable) => string.Equals(registered, CommandFor(executable), StringComparison.OrdinalIgnoreCase); }