Skip to content
This repository was archived by the owner on Jan 12, 2022. It is now read-only.
2 changes: 1 addition & 1 deletion InstallerLib.Tests/InstallerLib.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net5</TargetFrameworks>
<TargetFrameworks>net5</TargetFrameworks>
<IsPackable>false</IsPackable>
</PropertyGroup>

Expand Down
10 changes: 9 additions & 1 deletion InstallerLib/Info/InstallationInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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;
Expand All @@ -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
Expand Down
126 changes: 116 additions & 10 deletions InstallerLib/Installer.cs
Original file line number Diff line number Diff line change
@@ -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<double>
{
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;

Expand All @@ -38,7 +40,7 @@ public async Task<bool> 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")
Expand All @@ -60,7 +62,7 @@ public async Task<bool> 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);
Expand Down Expand Up @@ -109,6 +111,47 @@ public async Task<bool> 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<string, string>
{
{ "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;
Expand All @@ -120,11 +163,42 @@ public async Task<bool> 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);
}

/// <summary>
/// Get the currently installed version of OpenTabletDriver, or null if it is not installed.
/// </summary>
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}
}
}
2 changes: 1 addition & 1 deletion InstallerLib/InstallerLib.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net5</TargetFrameworks>
<TargetFrameworks>net5</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
Expand Down
5 changes: 2 additions & 3 deletions InstallerLib/Launcher.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using InstallerLib.Info;
Expand All @@ -9,7 +10,7 @@ public class Launcher
{
public Launcher()
{
AppArgs = new string[0];
AppArgs = Array.Empty<string>();
}

internal const string DaemonName = "OpenTabletDriver.Daemon";
Expand All @@ -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());
}
Expand Down
56 changes: 56 additions & 0 deletions InstallerLib/Platform/Windows/PowerShellCommand.cs
Original file line number Diff line number Diff line change
@@ -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<string> commands = new List<string>();
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<string> 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();
}
}
}
}
49 changes: 49 additions & 0 deletions InstallerLib/Platform/Windows/Registry.cs
Original file line number Diff line number Diff line change
@@ -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<string, string> 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");
}
}
}
Loading