diff --git a/InstallerLib.Tests/InstallerLib.Tests.csproj b/InstallerLib.Tests/InstallerLib.Tests.csproj index d4311fa..8d2fdff 100644 --- a/InstallerLib.Tests/InstallerLib.Tests.csproj +++ b/InstallerLib.Tests/InstallerLib.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1;net5 + net5 false diff --git a/InstallerLib/Info/InstallationInfo.cs b/InstallerLib/Info/InstallationInfo.cs index 7ee35f2..e02952b 100644 --- a/InstallerLib/Info/InstallationInfo.cs +++ b/InstallerLib/Info/InstallationInfo.cs @@ -8,7 +8,7 @@ public class InstallationInfo { public static InstallationInfo Current { private set; get; } = new InstallationInfo(); - private DirectoryInfo appdataDirectory, installationDirectory, configurationDirectory; + private DirectoryInfo appdataDirectory, installationDirectory, updaterDirectory, configurationDirectory; public DirectoryInfo AppdataDirectory { @@ -22,6 +22,12 @@ public DirectoryInfo InstallationDirectory get => this.installationDirectory ?? new DirectoryInfo(this.defaultInstallationDirectory); } + public DirectoryInfo UpdaterDirectory + { + set => this.updaterDirectory = value; + get => this.updaterDirectory ?? new DirectoryInfo(this.defaultUpdaterDirectory); + } + public DirectoryInfo ConfigurationDirectory { set => this.configurationDirectory = value; @@ -30,6 +36,8 @@ public DirectoryInfo ConfigurationDirectory private string defaultInstallationDirectory => Path.Join(AppdataDirectory.FullName, "bin"); + private string defaultUpdaterDirectory => Path.Join(AppdataDirectory.FullName, "updater"); + private string defaultConfigurationDirectory => Path.Join(InstallationDirectory.FullName, "Configurations"); private string defaultAppdataDirectory => SystemInfo.CurrentPlatform switch diff --git a/InstallerLib/Installer.cs b/InstallerLib/Installer.cs index 9f28c19..989ecdc 100644 --- a/InstallerLib/Installer.cs +++ b/InstallerLib/Installer.cs @@ -1,24 +1,26 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; using System.Threading.Tasks; using ICSharpCode.SharpZipLib.Tar; using InstallerLib.Info; +using InstallerLib.Platform.Windows; namespace InstallerLib { public class Installer : Progress { - public Installer() - { - } - public FileInfo VersionInfoFile => new FileInfo(Path.Join(InstallationInfo.Current.InstallationDirectory.FullName, "versionInfo.json")); private DirectoryInfo InstallationDirectory => InstallationInfo.Current.InstallationDirectory; private const int BufferSize = 81920; + private const string UNINSTALL_REG_KEY = @"HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\OpenTabletDriver"; public bool IsInstalled => VersionInfoFile.Exists; @@ -38,7 +40,7 @@ public async Task Install() var release = await Downloader.GetLatestRelease(repo); var asset = await Downloader.GetCurrentPlatformAsset(repo, release); var extension = asset.Name.Split('.').Last(); - + using (var httpStream = await Downloader.GetAssetStream(asset)) { if (extension == "zip") @@ -60,7 +62,7 @@ public async Task Install() { await CopyStreamWithProgress(asset.Size, httpStream, memoryStream); using (var decompressionStream = new GZipStream(memoryStream, CompressionMode.Decompress)) - using (var tar = TarArchive.CreateInputTarArchive(decompressionStream)) + using (var tar = TarArchive.CreateInputTarArchive(decompressionStream, Encoding.Unicode)) { // Extract to directory tar.ExtractContents(InstallationDirectory.FullName); @@ -109,6 +111,47 @@ public async Task Install() using (var fs = VersionInfoFile.OpenWrite()) versionInfo.Serialize(fs); + var updaterDir = InstallationInfo.Current.UpdaterDirectory.FullName; + var updaterExecutable = Path.GetFileName(Assembly.GetEntryAssembly().Location).Replace(".dll", ".exe"); + var updater = Path.Join(updaterDir, updaterExecutable); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var startMenuFolder = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs"); + var shortcut = "OpenTabletDriver.lnk"; + + var desktop = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), shortcut); + var startMenu = Path.Join(startMenuFolder, shortcut); + var startup = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.Startup), shortcut); + var otd = Path.Join(InstallationDirectory.FullName, Launcher.AppName + ".exe"); + + CleanShortcuts(); + Directory.CreateDirectory(startMenuFolder); + Shortcut.Create(desktop, updater); + Shortcut.Create(startMenu, updater); + Shortcut.Create(startup, updater, Minimized: true); + + var otdRegistry = new Registry(UNINSTALL_REG_KEY) + { + Values = new Dictionary + { + { "DisplayName", "OpenTabletDriver" }, + { "DisplayVersion", release.TagName }, + { "DisplayIcon", otd }, + { "NoModify", "1" }, + { "NoRepair", "1" }, + { "UninstallString", $"\"{updater}\" --uninstall" } + } + }; + otdRegistry.Save(); + } + + if (!Directory.Exists(updaterDir)) + { + Directory.CreateDirectory(updaterDir); + CopyFolder(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), updaterDir); + new Launcher().Start(); + } + return true; } return false; @@ -120,11 +163,42 @@ public async Task Install() public void Uninstall() { base.OnReport(0); + + foreach (var process in Process.GetProcesses()) + { + if (process.ProcessName == Launcher.AppName || process.ProcessName == Launcher.DaemonName) + { + process.Kill(); + process.WaitForExit(); + } + } + if (InstallationDirectory.Exists) InstallationDirectory.Delete(true); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + CleanShortcuts(); + Registry.Delete(UNINSTALL_REG_KEY); + } base.OnReport(1); } + public void SelfUninstall() + { + // Defer updater removal to powershell + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var cmd = new PowerShellCommand(); + cmd.AddCommands( + "Start-Sleep 10", + $"Remove-Item -Recurse '{InstallationInfo.Current.UpdaterDirectory.FullName}'" + ); + cmd.Execute(); + } + Environment.Exit(0); + } + /// /// Get the currently installed version of OpenTabletDriver, or null if it is not installed. /// @@ -173,20 +247,34 @@ private void CleanDirectory(DirectoryInfo directory) directory.Create(); } + private void CleanShortcuts() + { + var startMenuFolder = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", "OpenTabletDriver"); + var shortcut = "OpenTabletDriver.lnk"; + var desktop = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), shortcut); + var startup = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.Startup), shortcut); + if (File.Exists(desktop)) + File.Delete(desktop); + if (File.Exists(startup)) + File.Delete(startup); + if (Directory.Exists(startMenuFolder)) + Directory.Delete(startMenuFolder, true); + } + private async Task CopyStreamWithProgress(int length, Stream source, Stream target) { var buffer = new byte[BufferSize]; int i = 0; while (true) { - base.OnReport((float)i / (float)length); + base.OnReport((double)i / length); - int bytesRead = await source.ReadAsync(buffer, 0, BufferSize); + int bytesRead = await source.ReadAsync(buffer.AsMemory(0, BufferSize)); if (bytesRead == 0) break; - await target.WriteAsync(buffer[0..bytesRead], 0, bytesRead); - i += BufferSize; + await target.WriteAsync(buffer[0..bytesRead].AsMemory(0, bytesRead)); + i += bytesRead; } // Reset stream position @@ -195,5 +283,23 @@ private async Task CopyStreamWithProgress(int length, Stream source, Stream targ // Report copy complete base.OnReport(1); } + + public void CopyFolder(string sourceDirectory, string targetDirectory) + { + var source = new DirectoryInfo(sourceDirectory); + var target = new DirectoryInfo(targetDirectory); + + if (!target.Exists) + target.Create(); + + foreach (var file in source.GetFiles()) + file.CopyTo(Path.Combine(target.FullName, file.Name), true); + + foreach (var sourceSubDir in source.GetDirectories()) + { + var targetSubDir = target.CreateSubdirectory(sourceSubDir.Name); + CopyFolder(sourceSubDir.FullName, targetSubDir.FullName); + } + } } } \ No newline at end of file diff --git a/InstallerLib/InstallerLib.csproj b/InstallerLib/InstallerLib.csproj index 7571df5..c0c10cb 100644 --- a/InstallerLib/InstallerLib.csproj +++ b/InstallerLib/InstallerLib.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1;net5 + net5 diff --git a/InstallerLib/Launcher.cs b/InstallerLib/Launcher.cs index 8595483..fd1fce0 100644 --- a/InstallerLib/Launcher.cs +++ b/InstallerLib/Launcher.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using System.Linq; using InstallerLib.Info; @@ -9,7 +10,7 @@ public class Launcher { public Launcher() { - AppArgs = new string[0]; + AppArgs = Array.Empty(); } internal const string DaemonName = "OpenTabletDriver.Daemon"; @@ -35,8 +36,6 @@ public void Start(params string[] args) $"{AppName}{SystemInfo.ExecutableFileExtension}"); var appBin = new FileInfo(appBinPath); - AppArgs = new string[0]; - AppProcess = new ProcessHandler(appBin); AppProcess.Start(AppArgs.Union(args).ToArray()); } diff --git a/InstallerLib/Platform/Windows/PowerShellCommand.cs b/InstallerLib/Platform/Windows/PowerShellCommand.cs new file mode 100644 index 0000000..b5a5525 --- /dev/null +++ b/InstallerLib/Platform/Windows/PowerShellCommand.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; + +namespace InstallerLib.Platform.Windows +{ + public class PowerShellCommand + { + private readonly List commands = new List(); + public bool AsAdmin { get; set; } + + public PowerShellCommand(bool asAdmin = false) + { + this.AsAdmin = asAdmin; + } + + public void AddCommands(params string[] commands) + { + this.commands.AddRange(commands); + } + + public void AddCommands(IEnumerable commands) + { + this.commands.AddRange(commands); + } + + public void Execute(bool wait = false) + { + var args = $"-Command {commands.Aggregate((cmds, cmd) => cmds + "; " + cmd)};"; + + var powershell = new Process() + { + StartInfo = new ProcessStartInfo("powershell", args) + { + UseShellExecute = AsAdmin, + Verb = AsAdmin ? "runas" : string.Empty, + CreateNoWindow = !AsAdmin, + WindowStyle = ProcessWindowStyle.Hidden + } + }; + + try + { + powershell.Start(); + if (wait) + powershell.WaitForExit(); + } + catch (Win32Exception e) when (e.NativeErrorCode == 1223) + { + throw new OperationCanceledException(); + } + } + } +} \ No newline at end of file diff --git a/InstallerLib/Platform/Windows/Registry.cs b/InstallerLib/Platform/Windows/Registry.cs new file mode 100644 index 0000000..f9ddd76 --- /dev/null +++ b/InstallerLib/Platform/Windows/Registry.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace InstallerLib.Platform.Windows +{ + public class Registry + { + public string Key { get; } + public Dictionary Values; + public bool IsUserEditable => userEditable(Key); + + public Registry(string registryKey) + { + Key = registryKey; + } + + public void Save() + { + var cmd = new PowerShellCommand(); + cmd.AddCommands($"New-Item {Key}"); + if (Values.Any()) + { + cmd.AddCommands(from property in Values + let cmdUnit = $"Set-ItemProperty -Path '{Key}' -Name '{property.Key}' -Value '{property.Value}'" + select cmdUnit); + } + cmd.Execute(); + } + + public static void Delete(string registryKey) + { + var isUserEditable = userEditable(registryKey); + var cmd = new PowerShellCommand(!isUserEditable); + cmd.AddCommands($"Remove-Item {registryKey}"); + cmd.Execute(); + } + + public void Delete() + { + Delete(Key); + } + + private static bool userEditable(string key) + { + return Regex.IsMatch(key, "^HKCU"); + } + } +} \ No newline at end of file diff --git a/InstallerLib/Platform/Windows/Shortcut.cs b/InstallerLib/Platform/Windows/Shortcut.cs new file mode 100644 index 0000000..ebfdd06 --- /dev/null +++ b/InstallerLib/Platform/Windows/Shortcut.cs @@ -0,0 +1,47 @@ +namespace InstallerLib.Platform.Windows +{ + public class Shortcut + { + public string FullName { get; set; } + public string Target { get; set; } + public string WorkingDirectory { get; set; } + public string Arguments { get; set; } + public bool Minimized { get; set; } + + public Shortcut(string shortcutFile, string target) + { + FullName = shortcutFile; + Target = target; + } + + public void Save() + { + var cmd = new PowerShellCommand(); + cmd.AddCommands( + "$wShell = New-Object -ComObject WScript.Shell", + $"$Shortcut = $wShell.CreateShortcut('{FullName}')", + $"$Shortcut.TargetPath = '{Target}'", + $"$Shortcut.WindowStyle = {(Minimized ? 7 : 4)}" + ); + + if (!string.IsNullOrEmpty(WorkingDirectory)) + cmd.AddCommands($"$Shortcut.WorkingDirectory = '{WorkingDirectory}'"); + if (!string.IsNullOrEmpty(Arguments)) + cmd.AddCommands($"$Shortcut.Arguments = '{Arguments}'"); + + cmd.AddCommands($"$Shortcut.Save()"); + cmd.Execute(); + } + + public static void Create(string FullName, string Target, string WorkingDirectory = null, string Arguments = null, bool Minimized = false) + { + var shortcut = new Shortcut(FullName, Target) + { + WorkingDirectory = WorkingDirectory, + Arguments = Arguments, + Minimized = Minimized + }; + shortcut.Save(); + } + } +} \ No newline at end of file diff --git a/InstallerLib/ProcessHandler.cs b/InstallerLib/ProcessHandler.cs index baaf2e7..2923c6b 100644 --- a/InstallerLib/ProcessHandler.cs +++ b/InstallerLib/ProcessHandler.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Diagnostics; using System.IO; @@ -12,42 +11,33 @@ public ProcessHandler(FileInfo binFile) BinaryFile = binFile; } - public FileInfo BinaryFile { private set; get; } - public Process Process { private set; get; } + private FileInfo BinaryFile { set; get; } + private Process Process { set; get; } public bool IsRunning { private set; get; } - public bool HideWindow { set; get; } = false; + public bool HideWindow { set; get; } public event EventHandler Exited; - public ConcurrentDictionary Log { private set; get; } = new ConcurrentDictionary(); - public void Start(params string[] args) { if (!BinaryFile.Exists) throw new FileNotFoundException("The binary file does not exist.", BinaryFile.FullName); - Log.Clear(); Process = new Process { StartInfo = new ProcessStartInfo { FileName = BinaryFile.FullName, Arguments = string.Join(' ', args), - RedirectStandardOutput = true, - RedirectStandardError = true, CreateNoWindow = HideWindow, WorkingDirectory = BinaryFile.Directory.FullName } }; - Process.OutputDataReceived += (s, e) => Log.TryAdd(DateTime.Now, e?.Data); - Process.ErrorDataReceived += (s, e) => Log.TryAdd(DateTime.Now, e?.Data); - Process.Exited += (s, e) => + Process.Exited += (s, e) => { Exited?.Invoke(this, Process.ExitCode); IsRunning = false; }; Process.Start(); - Process.BeginOutputReadLine(); - Process.BeginErrorReadLine(); IsRunning = true; } diff --git a/OpenTabletDriver.Installer.Gtk/OpenTabletDriver.Installer.Gtk.csproj b/OpenTabletDriver.Installer.Gtk/OpenTabletDriver.Installer.Gtk.csproj index c747dea..9c522cc 100644 --- a/OpenTabletDriver.Installer.Gtk/OpenTabletDriver.Installer.Gtk.csproj +++ b/OpenTabletDriver.Installer.Gtk/OpenTabletDriver.Installer.Gtk.csproj @@ -2,7 +2,7 @@ WinExe - netcoreapp3.1;net5 + net5 0.4.1 diff --git a/OpenTabletDriver.Installer.Wpf/OpenTabletDriver.Installer.Wpf.csproj b/OpenTabletDriver.Installer.Wpf/OpenTabletDriver.Installer.Wpf.csproj index c384612..5b45b44 100644 --- a/OpenTabletDriver.Installer.Wpf/OpenTabletDriver.Installer.Wpf.csproj +++ b/OpenTabletDriver.Installer.Wpf/OpenTabletDriver.Installer.Wpf.csproj @@ -2,7 +2,7 @@ WinExe - netcoreapp3.1;net5-windows + net5-windows ../OpenTabletDriver.Installer/Assets/otd.ico 0.4.1 @@ -12,7 +12,7 @@ - + diff --git a/OpenTabletDriver.Installer/MainForm.cs b/OpenTabletDriver.Installer/MainForm.cs index 8a1091e..7a73ea5 100644 --- a/OpenTabletDriver.Installer/MainForm.cs +++ b/OpenTabletDriver.Installer/MainForm.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; -using System.Timers; using Eto.Drawing; using Eto.Forms; using InstallerLib; @@ -12,269 +12,259 @@ namespace OpenTabletDriver.Installer { public partial class MainForm : Form - { - public MainForm() - { - Title = "OpenTabletDriver Updater"; - ClientSize = new Size(400, 350); - Icon = App.Logo.WithSize(App.Logo.Size); + { + public MainForm() + { + Title = "OpenTabletDriver Updater"; + ClientSize = new Size(400, 350); + Icon = App.Logo.WithSize(App.Logo.Size); - this.status = new StackLayout() - { - Orientation = Orientation.Vertical, - VerticalContentAlignment = VerticalAlignment.Center, - HorizontalContentAlignment = HorizontalAlignment.Center, - Padding = 10, - Spacing = 5, - }; + this.status = new StackLayout() + { + Orientation = Orientation.Vertical, + VerticalContentAlignment = VerticalAlignment.Center, + HorizontalContentAlignment = HorizontalAlignment.Center, + Padding = 10, + Spacing = 5, + }; - var showFolder = new Command { MenuText = "Show install folder...", ToolBarText = "Show install folder" }; - showFolder.Executed += (sender, e) => SystemInfo.Open(InstallationInfo.Current.InstallationDirectory); + var showFolder = new Command { MenuText = "Show install folder...", ToolBarText = "Show install folder" }; + showFolder.Executed += (sender, e) => SystemInfo.Open(InstallationInfo.Current.InstallationDirectory); - var quitCommand = new Command { MenuText = "Quit", Shortcut = Application.Instance.CommonModifier | Keys.Q }; - quitCommand.Executed += (sender, e) => Application.Instance.Quit(); + var quitCommand = new Command { MenuText = "Quit", Shortcut = Application.Instance.CommonModifier | Keys.Q }; + quitCommand.Executed += (sender, e) => Application.Instance.Quit(); - this.startButton = new Button((sender, e) => Start()) - { - Text = "Start" - }; + this.startButton = new Button((sender, e) => Start()) + { + Text = "Start" + }; - this.installButton = new Button(async (sender, e) => await Install()) - { - Text = "Install" - }; + this.installButton = new Button(async (sender, e) => await Install()) + { + Text = "Install" + }; - this.uninstallButton = new Button((sender, e) => Uninstall()) - { - Text = "Uninstall" - }; - - this.updateButton = new Button(async (sender, e) => await Install(isUpdate: true)) - { - Text = "Update" - }; + this.uninstallButton = new Button((sender, e) => Uninstall()) + { + Text = "Uninstall" + }; + + this.updateButton = new Button(async (sender, e) => await Install(isUpdate: true)) + { + Text = "Update" + }; - // create menu - Menu = new MenuBar - { - Items = - { - // File submenu - new ButtonMenuItem - { - Text = "&File", - Items = - { - showFolder - } - }, - }, - ApplicationItems = - { - // application (OS X) or file menu (others) - }, - QuitItem = quitCommand - }; + // create menu + Menu = new MenuBar + { + Items = + { + // File submenu + new ButtonMenuItem + { + Text = "&File", + Items = + { + showFolder + } + }, + }, + ApplicationItems = + { + // application (OS X) or file menu (others) + }, + QuitItem = quitCommand + }; - PerformMigration(); - UpdateControls(autostart: true); - } + if (App.Current.Arguments.Contains("--uninstall")) + { + Uninstall(); + App.Current.Installer.SelfUninstall(); + } - public async void UpdateControls(bool autostart = false) - { - if (!shownInstallerUpdate && await InstallerUpdater.CheckForUpdate()) - { - var result = MessageBox.Show( - "An update is available for the installer." + Environment.NewLine + - "Do you wish to be directed to the latest release?", - "Installer Update", - MessageBoxButtons.YesNo, - MessageBoxType.Information - ); - switch (result) - { - case DialogResult.Yes: + PerformMigration(); + UpdateControls(autostart: true); + } + + public async void UpdateControls(bool autostart = false) + { + if (!shownInstallerUpdate && await InstallerUpdater.CheckForUpdate()) + { + var result = MessageBox.Show( + "An update is available for the installer." + Environment.NewLine + + "Do you wish to be directed to the latest release?", + "Installer Update", + MessageBoxButtons.YesNo, + MessageBoxType.Information + ); + switch (result) + { + case DialogResult.Yes: SystemInfo.Open(GitHubInfo.InstallerReleaseUrl); - Application.Instance.Quit(); - break; - } - autostart = false; - shownInstallerUpdate = true; - } + Application.Instance.Quit(); + break; + } + autostart = false; + shownInstallerUpdate = true; + } - bool installed = App.Current.Installer.IsInstalled; - bool update = await App.Current.Installer.CheckForUpdate(); - - var buttons = new List