From 4985e2535127c28d94b6bfcb73dd784d6944f34c Mon Sep 17 00:00:00 2001 From: Benjamin Date: Sun, 9 Nov 2025 11:42:47 -0600 Subject: [PATCH] Added auto updating --- .gitignore | 2 +- RyuUpdater/Program.cs | 42 +++++++++ .../Properties/launchSettings.example.json | 10 +++ RyuUpdater/RyuUpdater.csproj | 21 +++++ ShinRyuModManager-CE.sln | 6 ++ .../ShinRyuModManager-CE.csproj | 3 +- .../UserInterface/Assets/changelog.md | 5 ++ .../UserInterface/Updater/AutoUpdating.cs | 86 +++++++++++++++++++ .../UserInterface/Updater/PortableUpdater.cs | 33 +++++++ .../Updater/RyuUpdaterUpdater.cs | 17 ++++ .../UserInterface/Updater/SerilogWriter.cs | 12 +++ .../UserInterface/Views/MainWindow.axaml.cs | 3 + Utils/AssemblyVersion.cs | 16 +++- Utils/GamePath.cs | 2 +- 14 files changed, 252 insertions(+), 6 deletions(-) create mode 100644 RyuUpdater/Program.cs create mode 100644 RyuUpdater/Properties/launchSettings.example.json create mode 100644 RyuUpdater/RyuUpdater.csproj create mode 100644 ShinRyuModManager-CE/UserInterface/Updater/AutoUpdating.cs create mode 100644 ShinRyuModManager-CE/UserInterface/Updater/PortableUpdater.cs create mode 100644 ShinRyuModManager-CE/UserInterface/Updater/RyuUpdaterUpdater.cs create mode 100644 ShinRyuModManager-CE/UserInterface/Updater/SerilogWriter.cs diff --git a/.gitignore b/.gitignore index 33ec363..37a4aae 100644 --- a/.gitignore +++ b/.gitignore @@ -493,4 +493,4 @@ fabric.properties # *.ipr ### Custom ### -ShinRyuModManager-CE/Properties/launchSettings.json +launchSettings.json diff --git a/RyuUpdater/Program.cs b/RyuUpdater/Program.cs new file mode 100644 index 0000000..d0efb1d --- /dev/null +++ b/RyuUpdater/Program.cs @@ -0,0 +1,42 @@ +using System.Diagnostics; +using System.IO.Compression; + +namespace RyuUpdater; + +public static class Program { + private static async Task Main(string[] args) { + if (args.Length == 0) { + Console.WriteLine("This is a helper application for SRMM. Please don't run manually!\nPress any key to exit..."); + Console.ReadKey(); + + return; + } + + var pid = int.Parse(args[0]); + var updateFile = args[1]; + var targetDir = args[2]; + var srmmFileName = args[3]; + + var tempDir = new FileInfo(updateFile).Directory!.FullName; + + try { + if (pid != -1) { + await Process.GetProcessById(pid).WaitForExitAsync(); + } + } catch { + // ignore + } + + await Task.Delay(500); + + ZipFile.ExtractToDirectory(updateFile, targetDir, overwriteFiles: true); + Directory.Delete(tempDir, recursive: true); + + var srmmPath = Path.Combine(targetDir, srmmFileName); + + Process.Start(new ProcessStartInfo { + FileName = srmmPath, + UseShellExecute = true + }); + } +} diff --git a/RyuUpdater/Properties/launchSettings.example.json b/RyuUpdater/Properties/launchSettings.example.json new file mode 100644 index 0000000..7dc3e51 --- /dev/null +++ b/RyuUpdater/Properties/launchSettings.example.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "RyuUpdater": { + "commandName": "Project", + "commandLineArgs": " ", + "workingDirectory": "" // Path to folder containing exe + } + } +} diff --git a/RyuUpdater/RyuUpdater.csproj b/RyuUpdater/RyuUpdater.csproj new file mode 100644 index 0000000..41e9bdd --- /dev/null +++ b/RyuUpdater/RyuUpdater.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0;net8.0-windows + 12 + enable + disable + Debug;Release + x64 + false + ..\ShinRyuModManager-CE\UserInterface\Assets\Icons\SRMM_icon.ico + true + false + 1.0.0 + $(AssemblyVersion) + $(AssemblyVersion) + false + + + diff --git a/ShinRyuModManager-CE.sln b/ShinRyuModManager-CE.sln index 5198c4b..75f2815 100644 --- a/ShinRyuModManager-CE.sln +++ b/ShinRyuModManager-CE.sln @@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParLibrary.Tests", "ParLibr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utils", "Utils\Utils.csproj", "{7F2462E1-FB0A-4BFA-BBF9-91CD9DB1FC56}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RyuUpdater", "RyuUpdater\RyuUpdater.csproj", "{FDE3D69A-37FA-4187-9CAE-07EA4382D8A6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -30,5 +32,9 @@ Global {7F2462E1-FB0A-4BFA-BBF9-91CD9DB1FC56}.Debug|x64.Build.0 = Debug|x64 {7F2462E1-FB0A-4BFA-BBF9-91CD9DB1FC56}.Release|x64.ActiveCfg = Release|x64 {7F2462E1-FB0A-4BFA-BBF9-91CD9DB1FC56}.Release|x64.Build.0 = Release|x64 + {FDE3D69A-37FA-4187-9CAE-07EA4382D8A6}.Debug|x64.ActiveCfg = Debug|Any CPU + {FDE3D69A-37FA-4187-9CAE-07EA4382D8A6}.Debug|x64.Build.0 = Debug|Any CPU + {FDE3D69A-37FA-4187-9CAE-07EA4382D8A6}.Release|x64.ActiveCfg = Release|Any CPU + {FDE3D69A-37FA-4187-9CAE-07EA4382D8A6}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/ShinRyuModManager-CE/ShinRyuModManager-CE.csproj b/ShinRyuModManager-CE/ShinRyuModManager-CE.csproj index f4793fd..7b181cc 100644 --- a/ShinRyuModManager-CE/ShinRyuModManager-CE.csproj +++ b/ShinRyuModManager-CE/ShinRyuModManager-CE.csproj @@ -15,7 +15,7 @@ true - 1.1.12 + 1.2.0 $(AssemblyVersion) false @@ -45,6 +45,7 @@ + diff --git a/ShinRyuModManager-CE/UserInterface/Assets/changelog.md b/ShinRyuModManager-CE/UserInterface/Assets/changelog.md index 0271fd4..3284851 100644 --- a/ShinRyuModManager-CE/UserInterface/Assets/changelog.md +++ b/ShinRyuModManager-CE/UserInterface/Assets/changelog.md @@ -1,3 +1,8 @@ +> ### **%{color:orange} Version 1.2.0 %** ### +* Added auto updater + +--- + > ### **%{color:orange} Version 1.1.12 %** ### * Minor Cleanup * Updated libraries diff --git a/ShinRyuModManager-CE/UserInterface/Updater/AutoUpdating.cs b/ShinRyuModManager-CE/UserInterface/Updater/AutoUpdating.cs new file mode 100644 index 0000000..f91330a --- /dev/null +++ b/ShinRyuModManager-CE/UserInterface/Updater/AutoUpdating.cs @@ -0,0 +1,86 @@ +using System.IO.Compression; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using NetSparkleUpdater; +using NetSparkleUpdater.Enums; +using NetSparkleUpdater.SignatureVerifiers; +using NetSparkleUpdater.UI.Avalonia; +using Serilog; +using Utils; + +namespace ShinRyuModManager.UserInterface.Updater; + +public static class AutoUpdating { + private const string GH_PAGES_ROOT = "https://thetruecolonel.github.io/SRMM-AppCast"; + + private static string _tempDir; + private static SparkleUpdater _updater; + + public static void Init() { + _tempDir = Path.Combine(Environment.CurrentDirectory, "srmm_temp"); + + try { + HandleRyuUpdater(); + } catch (Exception ex) { + Log.Error(ex, "Problem trying to download RyuUpdater! Aborting auto updating..."); + + return; + } + + var suffix = AssemblyVersion.GetBuildSuffix(); + + if (string.Equals(suffix, "debug", StringComparison.OrdinalIgnoreCase)) { + return; // Don't need to be annoyed with "Update Now" when debugging + } + + var appcastUrl = $"{GH_PAGES_ROOT}/releases/appcast_{suffix}.xml"; + + // Update check for SRMM + _updater = new PortableUpdater(appcastUrl, new Ed25519Checker(SecurityMode.Unsafe)) { + UIFactory = new UIFactory { + HideReleaseNotes = true, + UseStaticUpdateWindowBackgroundColor = true, + UpdateWindowGridBackgroundBrush = new ImmutableSolidColorBrush(Color.Parse("#373535")) + }, + TmpDownloadFilePath = _tempDir, + TmpDownloadFileNameWithExtension = $"{Guid.NewGuid()}.zip", + RelaunchAfterUpdate = false, + LogWriter = new SerilogWriter() + }; + + _ = _updater.StartLoop(true, TimeSpan.FromMinutes(5)); + } + + private static void HandleRyuUpdater() { + string ryuUpdaterPath; + string updaterAppcastUrl; + string updaterLatestUrl; + + if (OperatingSystem.IsWindows()) { + ryuUpdaterPath = Path.Combine(Environment.CurrentDirectory, "RyuUpdater.exe"); + updaterAppcastUrl = $"{GH_PAGES_ROOT}/releases/appcast_ryuupdater-windows.xml"; + updaterLatestUrl = $"{GH_PAGES_ROOT}/updater/RyuUpdater-Windows-Latest.zip"; + } else { + ryuUpdaterPath = Path.Combine(Environment.CurrentDirectory, "RyuUpdater"); + updaterAppcastUrl = $"{GH_PAGES_ROOT}/releases/appcast_ryuupdater-linux.xml"; + updaterLatestUrl = $"{GH_PAGES_ROOT}/updater/RyuUpdater-Linux-Latest.zip"; + } + + // Pull grab latest version of updater if missing + if (!File.Exists(ryuUpdaterPath)) { + using var downloadStream = Utils.Client.GetStreamAsync(updaterLatestUrl).GetAwaiter().GetResult(); + + ZipFile.ExtractToDirectory(downloadStream, Environment.CurrentDirectory, overwriteFiles: true); + } + + // TODO: Linux doesn't store the required information for this to work on compiled binaries. To come back to. + /*// Update RyuUpdater quietly + var ryuUpdater = new RyuUpdaterUpdater(updaterAppcastUrl, new Ed25519Checker(SecurityMode.Unsafe), ryuUpdaterPath) { + UserInteractionMode = UserInteractionMode.DownloadAndInstall, + UIFactory = null, + TmpDownloadFilePath = _tempDir, + TmpDownloadFileNameWithExtension = $"{Guid.NewGuid()}.zip" + }; + _ = ryuUpdater.StartLoop(true);*/ + } +} diff --git a/ShinRyuModManager-CE/UserInterface/Updater/PortableUpdater.cs b/ShinRyuModManager-CE/UserInterface/Updater/PortableUpdater.cs new file mode 100644 index 0000000..3be152c --- /dev/null +++ b/ShinRyuModManager-CE/UserInterface/Updater/PortableUpdater.cs @@ -0,0 +1,33 @@ +using System.Diagnostics; +using NetSparkleUpdater; +using NetSparkleUpdater.Interfaces; + +namespace ShinRyuModManager.UserInterface.Updater; + +public sealed class PortableUpdater : SparkleUpdater { + public PortableUpdater(string appcastUrl, ISignatureVerifier signatureVerifier) : base(appcastUrl, signatureVerifier) { } + + protected override Task RunDownloadedInstaller(string downloadFilePath) { + var ryuPath = Path.Combine(Environment.CurrentDirectory, "RyuUpdater"); + + using var currentProcess = Process.GetCurrentProcess(); + + var pid = Environment.ProcessId; + var name = currentProcess.ProcessName; + + Process.Start(new ProcessStartInfo { + FileName = ryuPath, + ArgumentList = { + pid.ToString(), + downloadFilePath, + Environment.CurrentDirectory, + name + }, + UseShellExecute = false, + }); + + Environment.Exit(0x55504454); //UPDT + + return Task.CompletedTask; + } +} diff --git a/ShinRyuModManager-CE/UserInterface/Updater/RyuUpdaterUpdater.cs b/ShinRyuModManager-CE/UserInterface/Updater/RyuUpdaterUpdater.cs new file mode 100644 index 0000000..a4cc732 --- /dev/null +++ b/ShinRyuModManager-CE/UserInterface/Updater/RyuUpdaterUpdater.cs @@ -0,0 +1,17 @@ +using System.IO.Compression; +using NetSparkleUpdater; +using NetSparkleUpdater.Interfaces; + +namespace ShinRyuModManager.UserInterface.Updater; + +public class RyuUpdaterUpdater : SparkleUpdater { + public RyuUpdaterUpdater(string appcastUrl, ISignatureVerifier signatureVerifier) : base(appcastUrl, signatureVerifier) { } + public RyuUpdaterUpdater(string appcastUrl, ISignatureVerifier signatureVerifier, string referenceAssembly) : base(appcastUrl, signatureVerifier, referenceAssembly, null) { } + public RyuUpdaterUpdater(string appcastUrl, ISignatureVerifier signatureVerifier, string referenceAssembly, IUIFactory factory) : base(appcastUrl, signatureVerifier, referenceAssembly, factory) { } + + protected override Task RunDownloadedInstaller(string downloadFilePath) { + ZipFile.ExtractToDirectory(downloadFilePath, Environment.CurrentDirectory, overwriteFiles: true); + + return Task.CompletedTask; + } +} diff --git a/ShinRyuModManager-CE/UserInterface/Updater/SerilogWriter.cs b/ShinRyuModManager-CE/UserInterface/Updater/SerilogWriter.cs new file mode 100644 index 0000000..46e4019 --- /dev/null +++ b/ShinRyuModManager-CE/UserInterface/Updater/SerilogWriter.cs @@ -0,0 +1,12 @@ +using Serilog; +using ILogger = NetSparkleUpdater.Interfaces.ILogger; + +namespace ShinRyuModManager.UserInterface.Updater; + +public class SerilogWriter : ILogger { + public void PrintMessage(string message, params object[] arguments) { +#pragma warning disable CA2254 + Log.Information(message, arguments); +#pragma warning restore CA2254 + } +} diff --git a/ShinRyuModManager-CE/UserInterface/Views/MainWindow.axaml.cs b/ShinRyuModManager-CE/UserInterface/Views/MainWindow.axaml.cs index 8b24b5e..183adef 100644 --- a/ShinRyuModManager-CE/UserInterface/Views/MainWindow.axaml.cs +++ b/ShinRyuModManager-CE/UserInterface/Views/MainWindow.axaml.cs @@ -9,6 +9,7 @@ using Serilog.Events; using ShinRyuModManager.Helpers; using ShinRyuModManager.ModLoadOrder.Mods; +using ShinRyuModManager.UserInterface.Updater; using ShinRyuModManager.UserInterface.ViewModels; using Utils; using YamlDotNet.Core; @@ -23,6 +24,8 @@ public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); + + AutoUpdating.Init(); } private void Window_OnLoaded(object sender, RoutedEventArgs e) { diff --git a/Utils/AssemblyVersion.cs b/Utils/AssemblyVersion.cs index 5016087..2f98dfb 100644 --- a/Utils/AssemblyVersion.cs +++ b/Utils/AssemblyVersion.cs @@ -9,8 +9,18 @@ public static class AssemblyVersion { /// /// A . public static string GetVersion() { - var version = Assembly.GetEntryAssembly()!.GetCustomAttribute()?.InformationalVersion; - - return version; + return Assembly.GetEntryAssembly()!.GetCustomAttribute()?.InformationalVersion; + } + + public static string GetBuildVersion() { + var fullVersion = GetVersion(); + + return fullVersion[..(fullVersion.IndexOf('-'))]; + } + + public static string GetBuildSuffix() { + var fullVersion = GetVersion(); + + return fullVersion[(fullVersion.IndexOf('-') + 1)..]; } } diff --git a/Utils/GamePath.cs b/Utils/GamePath.cs index a7c6def..cfbc391 100644 --- a/Utils/GamePath.cs +++ b/Utils/GamePath.cs @@ -17,7 +17,7 @@ public static class GamePath { public static string GameExe { get; } static GamePath() { - FullGamePath = Directory.GetCurrentDirectory(); + FullGamePath = Environment.CurrentDirectory; DataPath = Path.Combine(FullGamePath, DATA); ModsPath = Path.Combine(FullGamePath, MODS); ExternalModsPath = Path.Combine(ModsPath, Constants.EXTERNAL_MODS);