From ef4e675527a06993a74e8d57d55e2aca4440cae1 Mon Sep 17 00:00:00 2001 From: Ruben Date: Mon, 28 Oct 2024 15:44:09 +0100 Subject: [PATCH] Preliminary version update functionality. --- src/Directory.Build.props | 4 + .../PicView.Avalonia.MacOS.csproj | 11 +- .../PicView.Avalonia.Win32.csproj | 11 +- src/PicView.Avalonia/PicView.Avalonia.csproj | 8 +- src/PicView.Avalonia/Update/UpdateInfo.cs | 10 + src/PicView.Avalonia/Update/UpdateManager.cs | 269 ++++++++++++++++++ src/PicView.Avalonia/Views/AboutView.axaml.cs | 7 +- src/PicView.Core/Config/VersionHelper.cs | 27 +- src/PicView.Tests/PicView.Tests.csproj | 2 +- 9 files changed, 316 insertions(+), 33 deletions(-) create mode 100644 src/PicView.Avalonia/Update/UpdateInfo.cs create mode 100644 src/PicView.Avalonia/Update/UpdateManager.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5d8722ee0..9f9df67c1 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,5 +2,9 @@ enable 11.0.2 + 3.0.0 + rc-preview-5 + 3.0.0.5 + 3.0 diff --git a/src/PicView.Avalonia.MacOS/PicView.Avalonia.MacOS.csproj b/src/PicView.Avalonia.MacOS/PicView.Avalonia.MacOS.csproj index 76388dd4f..d76ea2777 100644 --- a/src/PicView.Avalonia.MacOS/PicView.Avalonia.MacOS.csproj +++ b/src/PicView.Avalonia.MacOS/PicView.Avalonia.MacOS.csproj @@ -15,8 +15,6 @@ full true preview - 3.0 - 3.0 Ruben Hyldgaard Negendahl Ruben Hyldgaard Negendahl © Ruben Hyldgaard Negendahl @@ -28,11 +26,6 @@ none - - 3.0.0 - rc-preview-4 - - x64 @@ -43,8 +36,8 @@ - - + + diff --git a/src/PicView.Avalonia.Win32/PicView.Avalonia.Win32.csproj b/src/PicView.Avalonia.Win32/PicView.Avalonia.Win32.csproj index 61c730239..c75e11790 100644 --- a/src/PicView.Avalonia.Win32/PicView.Avalonia.Win32.csproj +++ b/src/PicView.Avalonia.Win32/PicView.Avalonia.Win32.csproj @@ -22,8 +22,6 @@ PicView PicView.Avalonia.Win32.Program preview - 3.0 - 3.0 none @@ -34,11 +32,6 @@ - - 3.0.0 - rc-preview-4 - - x64 @@ -48,8 +41,8 @@ - - + + diff --git a/src/PicView.Avalonia/PicView.Avalonia.csproj b/src/PicView.Avalonia/PicView.Avalonia.csproj index 0983484a1..7544ef1c0 100644 --- a/src/PicView.Avalonia/PicView.Avalonia.csproj +++ b/src/PicView.Avalonia/PicView.Avalonia.csproj @@ -97,11 +97,11 @@ - - - + + + - + diff --git a/src/PicView.Avalonia/Update/UpdateInfo.cs b/src/PicView.Avalonia/Update/UpdateInfo.cs new file mode 100644 index 000000000..905bb019b --- /dev/null +++ b/src/PicView.Avalonia/Update/UpdateInfo.cs @@ -0,0 +1,10 @@ +namespace PicView.Avalonia.Update; + +public class UpdateInfo +{ + public string Version { get; set; } + public string X64Portable { get; set; } + public string X64Install { get; set; } + public string Arm64Portable { get; set; } + public string Arm64Install { get; set; } +} diff --git a/src/PicView.Avalonia/Update/UpdateManager.cs b/src/PicView.Avalonia/Update/UpdateManager.cs new file mode 100644 index 000000000..f04b34f80 --- /dev/null +++ b/src/PicView.Avalonia/Update/UpdateManager.cs @@ -0,0 +1,269 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Win32; +using PicView.Avalonia.UI; +using PicView.Avalonia.ViewModels; +using PicView.Core.Config; +using PicView.Core.FileHandling; + +namespace PicView.Avalonia.Update; + +[JsonSourceGenerationOptions(AllowTrailingCommas = true)] +[JsonSerializable(typeof(UpdateInfo))] +public partial class UpdateSourceGenerationContext : JsonSerializerContext; + +public static class UpdateManager +{ + public static void CheckForUpdates() + { + } + + public static async Task UpdateCurrentVersion(MainViewModel vm) + { + var currentDirectory = new DirectoryInfo(Environment.ProcessPath); + var currentVersion = VersionHelper.GetCurrentVersionAsVersion(); + var url = "https://picview.org/update.json"; + var tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + "PicView"); + Directory.CreateDirectory(tempPath); + var tempJsonFileDestination = Path.Combine(tempPath, "update.json"); + +#if DEBUG + // Change it to lower to test it + currentVersion = new Version("3.0.0.3"); +#endif + + // Download the JSON file + using var jsonFileDownloader = new HttpHelper.HttpClientDownloadWithProgress(url, tempJsonFileDestination); + try + { + await jsonFileDownloader.StartDownloadAsync(); + } + catch (Exception e) + { +#if DEBUG + Console.WriteLine(e); +#endif + await TooltipHelper.ShowTooltipMessageAsync(e.Message); + return; + } + + // Read and deserialize the JSON + UpdateInfo? updateInfo; + try + { + var jsonString = await File.ReadAllTextAsync(tempJsonFileDestination); + if (JsonSerializer.Deserialize( + jsonString, typeof(UpdateInfo), + UpdateSourceGenerationContext.Default) is not UpdateInfo serializedUpdateInfo) + { +#if DEBUG + Console.WriteLine("Update information is missing or corrupted."); +#endif + await TooltipHelper.ShowTooltipMessageAsync("Update information is missing or corrupted."); + return; + } + + updateInfo = serializedUpdateInfo; + } + catch (Exception e) + { +#if DEBUG + Console.WriteLine(e); +#endif + await TooltipHelper.ShowTooltipMessageAsync("Failed to parse update information: \n" + e.Message); + return; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var isAdmin = false; + try + { + isAdmin = currentDirectory.GetAccessControl().AreAccessRulesProtected; + } + catch (Exception) + { + isAdmin = false; + } + + var isInstalled = CheckIfIsInstalled(); + + var architecture = RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => isInstalled ? InstalledArchitecture.X64Install : InstalledArchitecture.X64Portable, + Architecture.Arm64 => isInstalled + ? InstalledArchitecture.Arm64Install + : InstalledArchitecture.Arm64Portable, + _ => InstalledArchitecture.X64Install + }; + + var remoteVersion = new Version(updateInfo.Version); + if (remoteVersion <= currentVersion) + { + return; + } + + if (isAdmin) + { + // Restart the application as admin + var process = new Process + { + StartInfo = new ProcessStartInfo + { + Verb = "runas", + UseShellExecute = true, + FileName = "PicView.exe", + Arguments = $"update,{architecture},{currentVersion},{tempPath}", + WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory + } + }; + process.Start(); + Environment.Exit(0); + } + else + { + switch (architecture) + { + case InstalledArchitecture.Arm64Install: + var fileName = Path.GetFileName(updateInfo.X64Install); + var tempFileDownloadPath = Path.Combine(tempPath, fileName); + await StartFileDownloader(vm, updateInfo.Arm64Install, tempFileDownloadPath); + var process = new Process + { + StartInfo = new ProcessStartInfo + { + Verb = "runas", + UseShellExecute = true, + FileName = tempFileDownloadPath + } + }; + process.Start(); + return; + case InstalledArchitecture.Arm64Portable: + fileName = Path.GetFileName(updateInfo.X64Portable); + tempFileDownloadPath = Path.Combine(tempPath, fileName); + await StartFileDownloader(vm, updateInfo.Arm64Portable, tempFileDownloadPath); + vm.PlatformService.LocateOnDisk(tempFileDownloadPath); + return; + case InstalledArchitecture.X64Install: + fileName = Path.GetFileName(updateInfo.X64Install); + tempFileDownloadPath = Path.Combine(tempPath, fileName); + await StartFileDownloader(vm, updateInfo.X64Install, tempFileDownloadPath); + process = new Process + { + StartInfo = new ProcessStartInfo + { + Verb = "runas", + UseShellExecute = true, + FileName = tempFileDownloadPath + } + }; + process.Start(); + return; + case InstalledArchitecture.X64Portable: + fileName = Path.GetFileName(updateInfo.X64Portable); + tempFileDownloadPath = Path.Combine(tempPath, fileName); + await StartFileDownloader(vm, updateInfo.X64Portable, tempFileDownloadPath); + vm.PlatformService.LocateOnDisk(tempFileDownloadPath); + return; + } + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // TODO + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + // TODO + } + } + + private static bool CheckIfIsInstalled() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return false; + } + + var x64Path = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "PicView.exe"; + if (File.Exists(x64Path)) + { + return true; + } + + const string registryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"; + try + { + using var key = Registry.LocalMachine.OpenSubKey(registryKey); + if (key == null) + { + return false; + } + + foreach (var subKeyName in key.GetSubKeyNames()) + { + using var subKey = key.OpenSubKey(subKeyName); + + var installDir = subKey?.GetValue("InstallLocation")?.ToString(); + if (installDir == null) + { + continue; + } + + return Path.Exists(Path.Combine(installDir, "PicView.exe")); + } + } + catch (Exception e) + { +#if DEBUG + Trace.WriteLine($"{nameof(CheckIfIsInstalled)} exception, \n {e.Message}"); +#endif + return false; + } + + return false; + } + + private static async Task StartFileDownloader(MainViewModel vm, string downloadUrl, string tempPath) + { + vm.PlatformService.StopTaskbarProgress(); + using var jsonFileDownloader = new HttpHelper.HttpClientDownloadWithProgress(downloadUrl, tempPath); + try + { + jsonFileDownloader.ProgressChanged += (size, downloaded, percentage) => + ProgressChanged(vm, size, downloaded, percentage); + await jsonFileDownloader.StartDownloadAsync(); + } + catch (Exception e) + { +#if DEBUG + Console.WriteLine(e); +#endif + await TooltipHelper.ShowTooltipMessageAsync(e.Message); + } + finally + { + vm.PlatformService.StopTaskbarProgress(); + } + } + + private static void ProgressChanged(MainViewModel vm, long? totalfilesize, long? totalbytesdownloaded, double? progresspercentage) + { + if (totalfilesize is null || totalbytesdownloaded is null || progresspercentage is null) + { + return; + } + vm.PlatformService.SetTaskbarProgress((ulong)totalbytesdownloaded, (ulong)totalfilesize); + } + + private enum InstalledArchitecture + { + X64Portable, + X64Install, + Arm64Portable, + Arm64Install + } +} \ No newline at end of file diff --git a/src/PicView.Avalonia/Views/AboutView.axaml.cs b/src/PicView.Avalonia/Views/AboutView.axaml.cs index b94b2d7ab..aee095bf2 100644 --- a/src/PicView.Avalonia/Views/AboutView.axaml.cs +++ b/src/PicView.Avalonia/Views/AboutView.axaml.cs @@ -1,8 +1,9 @@ using Avalonia.Controls; using Avalonia.Media; using Avalonia.Styling; +using PicView.Avalonia.Update; +using PicView.Avalonia.ViewModels; using PicView.Core.Config; -using PicView.Core.ProcessHandling; namespace PicView.Avalonia.Views; @@ -41,9 +42,9 @@ public AboutView() }; // TODO: replace with auto update service - UpdateButton.Click += (_, _) => + UpdateButton.Click += async (_, _) => { - ProcessHelper.OpenLink("https://picview.org/avalonia-download"); + await UpdateManager.UpdateCurrentVersion(DataContext as MainViewModel); }; }; } diff --git a/src/PicView.Core/Config/VersionHelper.cs b/src/PicView.Core/Config/VersionHelper.cs index 3921a8aa1..7c0446321 100644 --- a/src/PicView.Core/Config/VersionHelper.cs +++ b/src/PicView.Core/Config/VersionHelper.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; -using System.Reflection; -using PicView.Core.ProcessHandling; +using System.Reflection; namespace PicView.Core.Config; @@ -10,10 +8,9 @@ public static class VersionHelper { try { - var loc = ProcessHelper.GetPathToProcess(); - var fvi = FileVersionInfo.GetVersionInfo(loc); - var productVersion = fvi.ProductVersion; - return productVersion[..productVersion.IndexOf('+')]; + var assembly = Assembly.GetExecutingAssembly(); + var informationVersion = assembly.GetCustomAttribute().InformationalVersion; + return informationVersion[..informationVersion.IndexOf('+')]; } catch (Exception e) { @@ -25,4 +22,20 @@ public static class VersionHelper return $"{assemblyVersion.Major}.{assemblyVersion.Minor}.{assemblyVersion.Build}.{assemblyVersion.Revision}"; } } + + public static Version? GetCurrentVersionAsVersion() + { + try + { + var assembly = Assembly.GetExecutingAssembly(); + return assembly.GetName().Version; + } + catch (Exception e) + { +#if DEBUG + Console.WriteLine(e); +#endif + return null; + } + } } \ No newline at end of file diff --git a/src/PicView.Tests/PicView.Tests.csproj b/src/PicView.Tests/PicView.Tests.csproj index 66517dce8..04563b437 100644 --- a/src/PicView.Tests/PicView.Tests.csproj +++ b/src/PicView.Tests/PicView.Tests.csproj @@ -10,7 +10,7 @@ - +