diff --git a/StreamDeckSimHub.Installer/Actions/AbstractInstallerAction.cs b/StreamDeckSimHub.Installer/Actions/AbstractInstallerAction.cs index bbbeba5..26c0fd4 100644 --- a/StreamDeckSimHub.Installer/Actions/AbstractInstallerAction.cs +++ b/StreamDeckSimHub.Installer/Actions/AbstractInstallerAction.cs @@ -62,6 +62,11 @@ public async Task Execute() protected abstract Task ExecuteInternal(); + protected void LogInfo(string message) + { + _logger.Info(message); + } + protected void SetAndLogInfo(string message) { Message = message; diff --git a/StreamDeckSimHub.Installer/Actions/CheckDotnetRuntime.cs b/StreamDeckSimHub.Installer/Actions/CheckDotnetRuntime.cs new file mode 100644 index 0000000..f22dbe2 --- /dev/null +++ b/StreamDeckSimHub.Installer/Actions/CheckDotnetRuntime.cs @@ -0,0 +1,112 @@ +// Copyright (C) 2024 Martin Renner +// LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) + +using System; +using System.IO; +using System.Net; +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using StreamDeckSimHub.Installer.Tools; + +namespace StreamDeckSimHub.Installer.Actions +{ + public class CheckDotnetRuntime : AbstractInstallerAction + { + private readonly Regex _dotnetDesktop = new Regex(@"Microsoft.WindowsDesktop.App (\d+\.\d+\.\d+).*", RegexOptions.IgnoreCase); + private readonly Version _dotnetRequired = new Version(8, 0, 10); + private const string BaseUrl = "https://download.visualstudio.microsoft.com/download/pr/f398d462-9d4e-4b9c-abd3-86c54262869a/4a8e3a10ca0a9903a989578140ef0499/"; + private const string InstallerName = "windowsdesktop-runtime-8.0.10-win-x64.exe"; + private const string InstallerHash = "914fb306fb1308c59e293d86c75fc4cca2cc72163c2af3e6eed0a30bec0a54a8f95d22ec6084fd9e1579cb0576ffa0942f513b7b4c6b4c3a2bc942fe21f0461d"; + private readonly string _installerFile = Path.Combine(Path.GetTempPath(), InstallerName); + + public override string Name => "Checking .NET Desktop Runtime version"; + + protected override async Task ExecuteInternal() + { + if (FindRuntime()) return ActionResult.Success; + + if (!await DownloadRuntime()) return ActionResult.Error; + + return Install() ? ActionResult.Success : ActionResult.Warning; + } + + private bool FindRuntime() + { + try + { + var exitCode = ProcessTools.RunCommand($"dotnet --list-runtimes", out var output); + LogInfo($"\"dotnet --list-runtimes\" exited with code {exitCode}"); + foreach (var line in output) + { + var match = _dotnetDesktop.Match(line); + if (match.Groups.Count >= 2) + { + var candidate = new Version(match.Groups[1].Value); + if (candidate >= _dotnetRequired) + { + SetAndLogInfo($"Found .NET Desktop Runtime version {candidate}"); + return true; + } + } + } + + SetAndLogInfo(".NET Desktop Runtime not found"); + return false; + } + catch (Exception e) + { + SetAndLogError(e, "Failed to determine installed .NET Desktop Runtime"); + return false; + } + } + + private async Task DownloadRuntime() + { + try + { + SetAndLogInfo($"Downloading .NET Desktop Runtime {_dotnetRequired}"); + + var webClient = new WebClient(); + await webClient.DownloadFileTaskAsync(BaseUrl + InstallerName, _installerFile); + using (var sha512 = SHA512.Create()) + { + using (var fileStream = File.OpenRead(_installerFile)) + { + var calculatedChecksum = sha512.ComputeHash(fileStream); + var calculatedChecksumString = BitConverter.ToString(calculatedChecksum).Replace("-", string.Empty).ToLowerInvariant(); + if (calculatedChecksumString != InstallerHash) + { + SetAndLogError("Invalid checksum for downloaded file"); + return false; + } + } + } + + return true; + } + catch (Exception e) + { + SetAndLogError(e, "Failed to download .NET Desktop Runtime"); + return false; + } + } + + private bool Install() + { + SetAndLogInfo($"Installing .NET Desktop Runtime {_dotnetRequired}"); + // see https://learn.microsoft.com/en-us/dotnet/core/install/windows#command-line-options + var exitCode = ProcessTools.RunCommand($"{_installerFile} /install /quiet /norestart", out var output); + LogInfo($"Installer exited with code {exitCode}"); + File.Delete(_installerFile); + if (exitCode == 0 || exitCode == 3010) + { + SetAndLogInfo($"Installed .NET Desktop Runtime {_dotnetRequired}"); + return true; + } + + SetAndLogInfo($"Possible probleme while installing .NET Desktop Runtime {_dotnetRequired}: Exit code {exitCode}"); + return false; + } + } +} \ No newline at end of file diff --git a/StreamDeckSimHub.Installer/MainWindow.xaml b/StreamDeckSimHub.Installer/MainWindow.xaml index 9f3516a..06f2b9e 100644 --- a/StreamDeckSimHub.Installer/MainWindow.xaml +++ b/StreamDeckSimHub.Installer/MainWindow.xaml @@ -6,7 +6,7 @@ xmlns:local="clr-namespace:StreamDeckSimHub.Installer" xmlns:localActions="clr-namespace:StreamDeckSimHub.Installer.Actions" mc:Ignorable="d" - Title="Stream Deck SimHub Plugin Installer" MinHeight="580" Width="600" SizeToContent="Height"> + Title="Stream Deck SimHub Plugin Installer" MinHeight="620" Width="600" SizeToContent="Height"> @@ -37,6 +37,7 @@ + diff --git a/StreamDeckSimHub.Installer/MainWindowViewModel.cs b/StreamDeckSimHub.Installer/MainWindowViewModel.cs index 8895731..165ad25 100644 --- a/StreamDeckSimHub.Installer/MainWindowViewModel.cs +++ b/StreamDeckSimHub.Installer/MainWindowViewModel.cs @@ -4,16 +4,18 @@ using System; using System.Collections.ObjectModel; using System.Threading.Tasks; +using System.Windows; using System.Windows.Media; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using NLog; using StreamDeckSimHub.Installer.Actions; namespace StreamDeckSimHub.Installer { public class MainWindowViewModel : ObservableObject { - private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Brush SuccessBrush = Brushes.Green; private static readonly Brush WarningBrush = Brushes.Orange; private static readonly Brush ErrorBrush = Brushes.Red; @@ -33,13 +35,27 @@ public class MainWindowViewModel : ObservableObject public IAsyncRelayCommand InstallCommand => _installCommand ?? (_installCommand = new AsyncRelayCommand(Install)); private async Task Install() + { + // Execute installation in a new task to make the UI more responsive. + await Task.Run(InstallTask); + } + + private async Task InstallTask() { ClearResultText(); - InstallerSteps.Clear(); + Application.Current.Dispatcher.Invoke(() => InstallerSteps.Clear()); Logger.Info("========== Starting installation =========="); + var checkDotnet = new CheckDotnetRuntime(); + Application.Current.Dispatcher.Invoke(() => InstallerSteps.Add(checkDotnet)); + if (await checkDotnet.Execute() == ActionResult.Error) + { + SetErrorResultText(); + return; + } + var stopStreamDeck = new StopStreamDeckSoftware(); - InstallerSteps.Add(stopStreamDeck); + Application.Current.Dispatcher.Invoke(() => InstallerSteps.Add(stopStreamDeck)); if (await stopStreamDeck.Execute() == ActionResult.Error) { SetErrorResultText(); @@ -48,15 +64,15 @@ private async Task Install() var result = ActionResult.Success; var installStreamDeckPlugin = new InstallStreamDeckPlugin(); - InstallerSteps.Add(installStreamDeckPlugin); + Application.Current.Dispatcher.Invoke(() => InstallerSteps.Add(installStreamDeckPlugin)); result = SetHigherResultLevel(await installStreamDeckPlugin.Execute(), result); var startStreamDeck = new StartStreamDeckSoftware(); - InstallerSteps.Add(startStreamDeck); + Application.Current.Dispatcher.Invoke(() => InstallerSteps.Add(startStreamDeck)); result = SetHigherResultLevel(await startStreamDeck.Execute(), result); var verifySimHubPlugin = new VerifySimHubPlugin(); - InstallerSteps.Add(verifySimHubPlugin); + Application.Current.Dispatcher.Invoke(() => InstallerSteps.Add(verifySimHubPlugin)); result = SetHigherResultLevel(await verifySimHubPlugin.Execute(), result); switch (result) diff --git a/StreamDeckSimHub.Installer/StreamDeckSimHub.Installer.csproj b/StreamDeckSimHub.Installer/StreamDeckSimHub.Installer.csproj index 70e53ab..1b02e49 100644 --- a/StreamDeckSimHub.Installer/StreamDeckSimHub.Installer.csproj +++ b/StreamDeckSimHub.Installer/StreamDeckSimHub.Installer.csproj @@ -66,6 +66,7 @@ MSBuild:Compile Designer + App.xaml Code diff --git a/StreamDeckSimHub.Installer/Tools/ProcessTools.cs b/StreamDeckSimHub.Installer/Tools/ProcessTools.cs index f24a75d..c15658e 100644 --- a/StreamDeckSimHub.Installer/Tools/ProcessTools.cs +++ b/StreamDeckSimHub.Installer/Tools/ProcessTools.cs @@ -1,6 +1,7 @@ // Copyright (C) 2024 Martin Renner // LGPL-3.0-or-later (see file COPYING and COPYING.LESSER) +using System; using System.Diagnostics; using System.IO; using System.Linq; @@ -36,5 +37,24 @@ public static void StartProcess(string fileName, string workingDirectory = null) process.StartInfo.UseShellExecute = true; process.Start(); } + + /// + /// Runs a command via cmd.exe and returns its exit code as well as its output as a string array - each line + /// one entry in the array. + /// + public static int RunCommand(string command, out string[] output) + { + var process = new Process(); + process.StartInfo.FileName = "cmd.exe"; + process.StartInfo.Arguments = $"/c {command}"; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + process.StartInfo.RedirectStandardOutput = true; + process.Start(); + var stdout = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + output = stdout.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); + return process.ExitCode; + } } } \ No newline at end of file