From 9e0ca8f11c4c796df2ca79019c1ae23f14b37057 Mon Sep 17 00:00:00 2001 From: Archi Date: Sat, 9 Mar 2024 22:15:42 +0100 Subject: [PATCH 01/32] Initial implementation of plugin updates --- ArchiSteamFarm/Core/ASF.cs | 504 +++++++----------- ArchiSteamFarm/Core/Utilities.cs | 180 ++++++- .../IPC/Controllers/Api/GitHubController.cs | 10 +- .../IPC/Responses/GitHubReleaseResponse.cs | 4 +- .../Plugins/Interfaces/IPluginUpdates.cs | 62 +++ ArchiSteamFarm/Plugins/PluginsCore.cs | 98 ++++ ArchiSteamFarm/SharedInfo.cs | 1 - ArchiSteamFarm/Steam/Interaction/Actions.cs | 14 +- .../Web/GitHub/Data/ReleaseAsset.cs | 47 ++ .../Web/GitHub/Data/ReleaseResponse.cs | 150 ++++++ ArchiSteamFarm/Web/{ => GitHub}/GitHub.cs | 157 +----- 11 files changed, 736 insertions(+), 491 deletions(-) create mode 100644 ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs create mode 100644 ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs create mode 100644 ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs rename ArchiSteamFarm/Web/{ => GitHub}/GitHub.cs (59%) diff --git a/ArchiSteamFarm/Core/ASF.cs b/ArchiSteamFarm/Core/ASF.cs index 2a1d4e1eb07d7..20db1eed22253 100644 --- a/ArchiSteamFarm/Core/ASF.cs +++ b/ArchiSteamFarm/Core/ASF.cs @@ -41,6 +41,8 @@ using ArchiSteamFarm.Steam.Integration; using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; +using ArchiSteamFarm.Web.GitHub; +using ArchiSteamFarm.Web.GitHub.Data; using ArchiSteamFarm.Web.Responses; using JetBrains.Annotations; using SteamKit2; @@ -113,6 +115,10 @@ internal static async Task Init() { WebBrowser = new WebBrowser(ArchiLogger, GlobalConfig.WebProxy, true); + if (!await PluginsCore.InitPlugins().ConfigureAwait(false)) { + return false; + } + await UpdateAndRestart().ConfigureAwait(false); if (!Program.IgnoreUnsupportedEnvironment && !await ProtectAgainstCrashes().ConfigureAwait(false)) { @@ -123,10 +129,6 @@ internal static async Task Init() { Program.AllowCrashFileRemoval = true; - if (!await PluginsCore.InitPlugins().ConfigureAwait(false)) { - return false; - } - await PluginsCore.OnASFInitModules(GlobalConfig.AdditionalProperties).ConfigureAwait(false); await InitRateLimiters().ConfigureAwait(false); @@ -185,7 +187,7 @@ internal static async Task RestartOrExit() { } } - internal static async Task Update(GlobalConfig.EUpdateChannel? channel = null, bool updateOverride = false) { + internal static async Task<(Version? NewVersion, bool RestartNeeded)> Update(GlobalConfig.EUpdateChannel? channel = null, bool updateOverride = false) { if (channel.HasValue && !Enum.IsDefined(channel.Value)) { throw new InvalidEnumArgumentException(nameof(channel), (int) channel, typeof(GlobalConfig.EUpdateChannel)); } @@ -194,203 +196,16 @@ internal static async Task RestartOrExit() { throw new InvalidOperationException(nameof(GlobalConfig)); } - if (WebBrowser == null) { - throw new InvalidOperationException(nameof(WebBrowser)); - } + Version? newVersion = await UpdateASF(channel, updateOverride).ConfigureAwait(false); - channel ??= GlobalConfig.UpdateChannel; + bool restartNeeded = newVersion > SharedInfo.Version; - if (!SharedInfo.BuildInfo.CanUpdate || (channel == GlobalConfig.EUpdateChannel.None)) { - return null; + if (!restartNeeded) { + // ASF wasn't updated as part of the process, update the plugins alone + restartNeeded = await PluginsCore.UpdatePlugins(SharedInfo.Version, (channel ?? GlobalConfig.UpdateChannel) == GlobalConfig.EUpdateChannel.Stable).ConfigureAwait(false); } - await UpdateSemaphore.WaitAsync().ConfigureAwait(false); - - try { - // If backup directory from previous update exists, it's a good idea to purge it now - string backupDirectory = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.UpdateDirectory); - - if (Directory.Exists(backupDirectory)) { - ArchiLogger.LogGenericInfo(Strings.UpdateCleanup); - - for (byte i = 0; (i < WebBrowser.MaxTries) && Directory.Exists(backupDirectory); i++) { - if (i > 0) { - // It's entirely possible that old process is still running, wait a short moment for eventual cleanup - await Task.Delay(5000).ConfigureAwait(false); - } - - try { - Directory.Delete(backupDirectory, true); - } catch (Exception e) { - ArchiLogger.LogGenericDebuggingException(e); - - continue; - } - - break; - } - - if (Directory.Exists(backupDirectory)) { - ArchiLogger.LogGenericError(Strings.WarningFailed); - - return null; - } - - ArchiLogger.LogGenericInfo(Strings.Done); - } - - ArchiLogger.LogGenericInfo(Strings.UpdateCheckingNewVersion); - - GitHub.ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(channel == GlobalConfig.EUpdateChannel.Stable).ConfigureAwait(false); - - if (releaseResponse == null) { - ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); - - return null; - } - - if (string.IsNullOrEmpty(releaseResponse.Tag)) { - ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); - - return null; - } - - Version newVersion = new(releaseResponse.Tag); - - ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.UpdateVersionInfo, SharedInfo.Version, newVersion)); - - if (SharedInfo.Version >= newVersion) { - return newVersion; - } - - if (!updateOverride && (GlobalConfig.UpdatePeriod == 0)) { - ArchiLogger.LogGenericInfo(Strings.UpdateNewVersionAvailable); - await Task.Delay(SharedInfo.ShortInformationDelay).ConfigureAwait(false); - - return null; - } - - // Auto update logic starts here - if (releaseResponse.Assets.IsEmpty) { - ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssets); - - return null; - } - - string targetFile = $"{SharedInfo.ASF}-{SharedInfo.BuildInfo.Variant}.zip"; - GitHub.ReleaseResponse.Asset? binaryAsset = releaseResponse.Assets.FirstOrDefault(asset => !string.IsNullOrEmpty(asset.Name) && asset.Name.Equals(targetFile, StringComparison.OrdinalIgnoreCase)); - - if (binaryAsset == null) { - ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssetForThisVersion); - - return null; - } - - if (binaryAsset.DownloadURL == null) { - ArchiLogger.LogNullError(binaryAsset.DownloadURL); - - return null; - } - - ArchiLogger.LogGenericInfo(Strings.FetchingChecksumFromRemoteServer); - - string? remoteChecksum = await ArchiNet.FetchBuildChecksum(newVersion, SharedInfo.BuildInfo.Variant).ConfigureAwait(false); - - switch (remoteChecksum) { - case null: - // Timeout or error, refuse to update as a security measure - return null; - case "": - // Unknown checksum, release too new or actual malicious build published, no need to scare the user as it's 99.99% the first - ArchiLogger.LogGenericWarning(Strings.ChecksumMissing); - - return SharedInfo.Version; - } - - if (!string.IsNullOrEmpty(releaseResponse.ChangelogPlainText)) { - ArchiLogger.LogGenericInfo(releaseResponse.ChangelogPlainText); - } - - ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.UpdateDownloadingNewVersion, newVersion, binaryAsset.Size / 1024 / 1024)); - - Progress progressReporter = new(); - - progressReporter.ProgressChanged += OnProgressChanged; - - BinaryResponse? response; - - try { - response = await WebBrowser.UrlGetToBinary(binaryAsset.DownloadURL, progressReporter: progressReporter).ConfigureAwait(false); - } finally { - progressReporter.ProgressChanged -= OnProgressChanged; - } - - if (response?.Content == null) { - return null; - } - - ArchiLogger.LogGenericInfo(Strings.VerifyingChecksumWithRemoteServer); - - byte[] responseBytes = response.Content as byte[] ?? response.Content.ToArray(); - - string checksum = Utilities.GenerateChecksumFor(responseBytes); - - if (!checksum.Equals(remoteChecksum, StringComparison.OrdinalIgnoreCase)) { - ArchiLogger.LogGenericError(Strings.ChecksumWrong); - - return SharedInfo.Version; - } - - await PluginsCore.OnUpdateProceeding(newVersion).ConfigureAwait(false); - - bool kestrelWasRunning = ArchiKestrel.IsRunning; - - if (kestrelWasRunning) { - // We disable ArchiKestrel here as the update process moves the core files and might result in IPC crash - // TODO: It might fail if the update was triggered from the API, this should be something to improve in the future, by changing the structure into request -> return response -> finish update - try { - await ArchiKestrel.Stop().ConfigureAwait(false); - } catch (Exception e) { - ArchiLogger.LogGenericWarningException(e); - } - } - - ArchiLogger.LogGenericInfo(Strings.PatchingFiles); - - MemoryStream ms = new(responseBytes); - - try { - await using (ms.ConfigureAwait(false)) { - using ZipArchive zipArchive = new(ms); - - if (!UpdateFromArchive(zipArchive, SharedInfo.HomeDirectory)) { - ArchiLogger.LogGenericError(Strings.WarningFailed); - } - } - } catch (Exception e) { - ArchiLogger.LogGenericException(e); - - if (kestrelWasRunning) { - // We've temporarily disabled ArchiKestrel but the update has failed, let's bring it back up - // We can't even be sure if it's possible to bring it back up in this state, but it's worth trying anyway - try { - await ArchiKestrel.Start().ConfigureAwait(false); - } catch (Exception ex) { - ArchiLogger.LogGenericWarningException(ex); - } - } - - return null; - } - - ArchiLogger.LogGenericInfo(Strings.UpdateFinished); - - await PluginsCore.OnUpdateFinished(newVersion).ConfigureAwait(false); - - return newVersion; - } finally { - UpdateSemaphore.Release(); - } + return (newVersion, restartNeeded); } private static async Task CanHandleWriteEvent(string filePath) { @@ -800,16 +615,6 @@ private static async Task OnDeletedJsonConfigFile(string name, string fullPath) } } - private static void OnProgressChanged(object? sender, byte progressPercentage) { - const byte printEveryPercentage = 10; - - if (progressPercentage % printEveryPercentage != 0) { - return; - } - - ArchiLogger.LogGenericDebug($"{progressPercentage}%..."); - } - private static async void OnRenamed(object sender, RenamedEventArgs e) { // This function can be called with a possibility of OldName or (new) Name being null, we have to take it into account ArgumentNullException.ThrowIfNull(sender); @@ -932,150 +737,247 @@ private static async Task UpdateAndRestart() { ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.AutoUpdateCheckInfo, autoUpdatePeriod.ToHumanReadable())); } - Version? newVersion = await Update().ConfigureAwait(false); + (Version? newVersion, bool restartNeeded) = await Update().ConfigureAwait(false); - if (newVersion == null) { - return; + if (SharedInfo.Version > newVersion) { + // User is running version newer than their channel allows + ArchiLogger.LogGenericWarning(Strings.WarningPreReleaseVersion); + await Task.Delay(SharedInfo.InformationDelay).ConfigureAwait(false); } - if (SharedInfo.Version >= newVersion) { - if (SharedInfo.Version > newVersion) { - ArchiLogger.LogGenericWarning(Strings.WarningPreReleaseVersion); - await Task.Delay(SharedInfo.InformationDelay).ConfigureAwait(false); - } - + if (!restartNeeded) { return; } // Allow crash file recovery, if needed - Program.AllowCrashFileRemoval = true; + if (newVersion > SharedInfo.Version) { + Program.AllowCrashFileRemoval = true; + } await RestartOrExit().ConfigureAwait(false); } - private static bool UpdateFromArchive(ZipArchive archive, string targetDirectory) { - ArgumentNullException.ThrowIfNull(archive); - ArgumentException.ThrowIfNullOrEmpty(targetDirectory); + private static async Task UpdateASF(GlobalConfig.EUpdateChannel? channel = null, bool updateOverride = false) { + if (channel.HasValue && !Enum.IsDefined(channel.Value)) { + throw new InvalidEnumArgumentException(nameof(channel), (int) channel, typeof(GlobalConfig.EUpdateChannel)); + } - if (SharedInfo.HomeDirectory == AppContext.BaseDirectory) { - // We're running a build that includes our dependencies in ASF's home - // Before actually moving files in update procedure, let's minimize the risk of some assembly not being loaded that we may need in the process - LoadAllAssemblies(); - } else { - // This is a tricky one, for some reason we might need to preload some selected assemblies even in OS-specific builds that normally should be self-contained... - // It's as if the executable file was directly mapped to memory and moving it out of the original path caused the whole thing to crash - // TODO: This is a total hack, I wish we could get to the bottom of this hole and find out what is really going on there in regards to the above - LoadAssembliesNeededBeforeUpdate(); + if (GlobalConfig == null) { + throw new InvalidOperationException(nameof(GlobalConfig)); } - // Firstly we'll move all our existing files to a backup directory - string backupDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectory); + if (WebBrowser == null) { + throw new InvalidOperationException(nameof(WebBrowser)); + } - foreach (string file in Directory.EnumerateFiles(targetDirectory, "*", SearchOption.AllDirectories)) { - string fileName = Path.GetFileName(file); + channel ??= GlobalConfig.UpdateChannel; + + if (!SharedInfo.BuildInfo.CanUpdate || (channel == GlobalConfig.EUpdateChannel.None)) { + return null; + } + + await UpdateSemaphore.WaitAsync().ConfigureAwait(false); + + try { + // If backup directory from previous update exists, it's a good idea to purge it now + string backupDirectory = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.UpdateDirectory); + + if (Directory.Exists(backupDirectory)) { + ArchiLogger.LogGenericInfo(Strings.UpdateCleanup); + + for (byte i = 0; (i < WebBrowser.MaxTries) && Directory.Exists(backupDirectory); i++) { + if (i > 0) { + // It's entirely possible that old process is still running, wait a short moment for eventual cleanup + await Task.Delay(5000).ConfigureAwait(false); + } - if (string.IsNullOrEmpty(fileName)) { - ArchiLogger.LogNullError(fileName); + try { + Directory.Delete(backupDirectory, true); + } catch (Exception e) { + ArchiLogger.LogGenericDebuggingException(e); - return false; + continue; + } + + break; + } + + if (Directory.Exists(backupDirectory)) { + ArchiLogger.LogGenericError(Strings.WarningFailed); + + return null; + } + + ArchiLogger.LogGenericInfo(Strings.Done); } - string relativeFilePath = Path.GetRelativePath(targetDirectory, file); + ArchiLogger.LogGenericInfo(Strings.UpdateCheckingNewVersion); + + ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(SharedInfo.GithubRepo, channel == GlobalConfig.EUpdateChannel.Stable).ConfigureAwait(false); - if (string.IsNullOrEmpty(relativeFilePath)) { - ArchiLogger.LogNullError(relativeFilePath); + if (releaseResponse == null) { + ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); - return false; + return null; } - string? relativeDirectoryName = Path.GetDirectoryName(relativeFilePath); + if (string.IsNullOrEmpty(releaseResponse.Tag)) { + ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); - switch (relativeDirectoryName) { - case null: - ArchiLogger.LogNullError(relativeDirectoryName); + return null; + } - return false; - case "": - // No directory, root folder - switch (fileName) { - case Logging.NLogConfigurationFile: - case SharedInfo.LogFile: - // Files with those names in root directory we want to keep - continue; - } + Version newVersion = new(releaseResponse.Tag); - break; - case SharedInfo.ArchivalLogsDirectory: - case SharedInfo.ConfigDirectory: - case SharedInfo.DebugDirectory: - case SharedInfo.PluginsDirectory: - case SharedInfo.UpdateDirectory: - // Files in those directories we want to keep in their current place - continue; - default: - // Files in subdirectories of those directories we want to keep as well - if (Utilities.RelativeDirectoryStartsWith(relativeDirectoryName, SharedInfo.ArchivalLogsDirectory, SharedInfo.ConfigDirectory, SharedInfo.DebugDirectory, SharedInfo.PluginsDirectory, SharedInfo.UpdateDirectory)) { - continue; - } + ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.UpdateVersionInfo, SharedInfo.Version, newVersion)); - break; + if (SharedInfo.Version >= newVersion) { + return newVersion; } - string targetBackupDirectory = relativeDirectoryName.Length > 0 ? Path.Combine(backupDirectory, relativeDirectoryName) : backupDirectory; - Directory.CreateDirectory(targetBackupDirectory); + if (!updateOverride && (GlobalConfig.UpdatePeriod == 0)) { + ArchiLogger.LogGenericInfo(Strings.UpdateNewVersionAvailable); + await Task.Delay(SharedInfo.ShortInformationDelay).ConfigureAwait(false); + + return null; + } - string targetBackupFile = Path.Combine(targetBackupDirectory, fileName); + // Auto update logic starts here + if (releaseResponse.Assets.IsEmpty) { + ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssets); - File.Move(file, targetBackupFile, true); - } + return null; + } - // We can now get rid of directories that are empty - Utilities.DeleteEmptyDirectoriesRecursively(targetDirectory); + string targetFile = $"{SharedInfo.ASF}-{SharedInfo.BuildInfo.Variant}.zip"; + ReleaseAsset? binaryAsset = releaseResponse.Assets.FirstOrDefault(asset => !string.IsNullOrEmpty(asset.Name) && asset.Name.Equals(targetFile, StringComparison.OrdinalIgnoreCase)); - if (!Directory.Exists(targetDirectory)) { - Directory.CreateDirectory(targetDirectory); - } + if (binaryAsset == null) { + ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssetForThisVersion); + + return null; + } - // Now enumerate over files in the zip archive, skip directory entries that we're not interested in (we can create them ourselves if needed) - foreach (ZipArchiveEntry zipFile in archive.Entries.Where(static zipFile => !string.IsNullOrEmpty(zipFile.Name))) { - string file = Path.GetFullPath(Path.Combine(targetDirectory, zipFile.FullName)); + ArchiLogger.LogGenericInfo(Strings.FetchingChecksumFromRemoteServer); + + string? remoteChecksum = await ArchiNet.FetchBuildChecksum(newVersion, SharedInfo.BuildInfo.Variant).ConfigureAwait(false); - if (!file.StartsWith(targetDirectory, StringComparison.Ordinal)) { - throw new InvalidOperationException(nameof(file)); + switch (remoteChecksum) { + case null: + // Timeout or error, refuse to update as a security measure + return null; + case "": + // Unknown checksum, release too new or actual malicious build published, no need to scare the user as it's 99.99% the first + ArchiLogger.LogGenericWarning(Strings.ChecksumMissing); + + return SharedInfo.Version; + } + + if (!string.IsNullOrEmpty(releaseResponse.ChangelogPlainText)) { + ArchiLogger.LogGenericInfo(releaseResponse.ChangelogPlainText); } - if (File.Exists(file)) { - // This is possible only with files that we decided to leave in place during our backup function - string targetBackupFile = $"{file}.bak"; + ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.UpdateDownloadingNewVersion, newVersion, binaryAsset.Size / 1024 / 1024)); + + Progress progressReporter = new(); + + progressReporter.ProgressChanged += Utilities.OnProgressChanged; - File.Move(file, targetBackupFile, true); + BinaryResponse? response; + + try { + response = await WebBrowser.UrlGetToBinary(binaryAsset.DownloadURL, progressReporter: progressReporter).ConfigureAwait(false); + } finally { + progressReporter.ProgressChanged -= Utilities.OnProgressChanged; + } + + if (response?.Content == null) { + return null; + } + + ArchiLogger.LogGenericInfo(Strings.VerifyingChecksumWithRemoteServer); + + byte[] responseBytes = response.Content as byte[] ?? response.Content.ToArray(); + + string checksum = Utilities.GenerateChecksumFor(responseBytes); + + if (!checksum.Equals(remoteChecksum, StringComparison.OrdinalIgnoreCase)) { + ArchiLogger.LogGenericError(Strings.ChecksumWrong); + + return SharedInfo.Version; } - // Check if this file requires its own folder - if (zipFile.Name != zipFile.FullName) { - string? directory = Path.GetDirectoryName(file); + await PluginsCore.OnUpdateProceeding(newVersion).ConfigureAwait(false); - if (string.IsNullOrEmpty(directory)) { - ArchiLogger.LogNullError(directory); + bool kestrelWasRunning = ArchiKestrel.IsRunning; - return false; + if (kestrelWasRunning) { + // We disable ArchiKestrel here as the update process moves the core files and might result in IPC crash + // TODO: It might fail if the update was triggered from the API, this should be something to improve in the future, by changing the structure into request -> return response -> finish update + try { + await ArchiKestrel.Stop().ConfigureAwait(false); + } catch (Exception e) { + ArchiLogger.LogGenericWarningException(e); } + } - if (!Directory.Exists(directory)) { - Directory.CreateDirectory(directory); + ArchiLogger.LogGenericInfo(Strings.PatchingFiles); + + try { + MemoryStream memoryStream = new(responseBytes); + + await using (memoryStream.ConfigureAwait(false)) { + using ZipArchive zipArchive = new(memoryStream); + + if (!await UpdateFromArchive(newVersion, channel == GlobalConfig.EUpdateChannel.Stable, zipArchive).ConfigureAwait(false)) { + ArchiLogger.LogGenericError(Strings.WarningFailed); + } } + } catch (Exception e) { + ArchiLogger.LogGenericException(e); - // We're not interested in extracting placeholder files (but we still want directories created for them, done above) - switch (zipFile.Name) { - case ".gitkeep": - continue; + if (kestrelWasRunning) { + // We've temporarily disabled ArchiKestrel but the update has failed, let's bring it back up + // We can't even be sure if it's possible to bring it back up in this state, but it's worth trying anyway + try { + await ArchiKestrel.Start().ConfigureAwait(false); + } catch (Exception ex) { + ArchiLogger.LogGenericWarningException(ex); + } } + + return null; } - zipFile.ExtractToFile(file); + ArchiLogger.LogGenericInfo(Strings.UpdateFinished); + + await PluginsCore.OnUpdateFinished(newVersion).ConfigureAwait(false); + + return newVersion; + } finally { + UpdateSemaphore.Release(); } + } - return true; + private static async Task UpdateFromArchive(Version newVersion, bool stable, ZipArchive zipArchive) { + ArgumentNullException.ThrowIfNull(newVersion); + ArgumentNullException.ThrowIfNull(zipArchive); + + if (SharedInfo.HomeDirectory == AppContext.BaseDirectory) { + // We're running a build that includes our dependencies in ASF's home + // Before actually moving files in update procedure, let's minimize the risk of some assembly not being loaded that we may need in the process + LoadAllAssemblies(); + } else { + // This is a tricky one, for some reason we might need to preload some selected assemblies even in OS-specific builds that normally should be self-contained... + // It's as if the executable file was directly mapped to memory and moving it out of the original path caused the whole thing to crash + // TODO: This is a total hack, I wish we could get to the bottom of this hole and find out what is really going on there in regards to the above + LoadAssembliesNeededBeforeUpdate(); + } + + // We're ready to start update process, handle any plugin updates ready for new version + await PluginsCore.UpdatePlugins(newVersion, stable).ConfigureAwait(false); + + return Utilities.UpdateFromArchive(zipArchive, SharedInfo.HomeDirectory); } [PublicAPI] diff --git a/ArchiSteamFarm/Core/Utilities.cs b/ArchiSteamFarm/Core/Utilities.cs index acecc40561d18..20df093b84c47 100644 --- a/ArchiSteamFarm/Core/Utilities.cs +++ b/ArchiSteamFarm/Core/Utilities.cs @@ -26,6 +26,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net; using System.Resources; @@ -35,6 +36,7 @@ using AngleSharp.Dom; using AngleSharp.XPath; using ArchiSteamFarm.Localization; +using ArchiSteamFarm.NLog; using ArchiSteamFarm.Storage; using Humanizer; using Humanizer.Localisation; @@ -263,26 +265,6 @@ public static bool TryReadJsonWebToken(string token, [NotNullWhen(true)] out Jso return true; } - internal static void DeleteEmptyDirectoriesRecursively(string directory) { - ArgumentException.ThrowIfNullOrEmpty(directory); - - if (!Directory.Exists(directory)) { - return; - } - - try { - foreach (string subDirectory in Directory.EnumerateDirectories(directory)) { - DeleteEmptyDirectoriesRecursively(subDirectory); - } - - if (!Directory.EnumerateFileSystemEntries(directory).Any()) { - Directory.Delete(directory); - } - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - } - } - internal static ulong MathAdd(ulong first, int second) { if (second >= 0) { return first + (uint) second; @@ -291,16 +273,14 @@ internal static ulong MathAdd(ulong first, int second) { return first - (uint) -second; } - internal static bool RelativeDirectoryStartsWith(string directory, params string[] prefixes) { - ArgumentException.ThrowIfNullOrEmpty(directory); + internal static void OnProgressChanged(object? sender, byte progressPercentage) { + const byte printEveryPercentage = 10; -#pragma warning disable CA1508 // False positive, params could be null when explicitly set - if ((prefixes == null) || (prefixes.Length == 0)) { -#pragma warning restore CA1508 // False positive, params could be null when explicitly set - throw new ArgumentNullException(nameof(prefixes)); + if (progressPercentage % printEveryPercentage != 0) { + return; } - return (from prefix in prefixes where directory.Length > prefix.Length let pathSeparator = directory[prefix.Length] where (pathSeparator == Path.DirectorySeparatorChar) || (pathSeparator == Path.AltDirectorySeparatorChar) select prefix).Any(prefix => directory.StartsWith(prefix, StringComparison.Ordinal)); + ASF.ArchiLogger.LogGenericDebug($"{progressPercentage}%..."); } internal static (bool IsWeak, string? Reason) TestPasswordStrength(string password, ISet? additionallyForbiddenPhrases = null) { @@ -337,6 +317,120 @@ internal static (bool IsWeak, string? Reason) TestPasswordStrength(string passwo return (result.Score < 4, suggestions is { Count: > 0 } ? string.Join(' ', suggestions.Where(static suggestion => suggestion.Length > 0)) : null); } + internal static bool UpdateFromArchive(ZipArchive zipArchive, string targetDirectory) { + ArgumentNullException.ThrowIfNull(zipArchive); + ArgumentException.ThrowIfNullOrEmpty(targetDirectory); + + // Firstly we'll move all our existing files to a backup directory + string backupDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectory); + + foreach (string file in Directory.EnumerateFiles(targetDirectory, "*", SearchOption.AllDirectories)) { + string fileName = Path.GetFileName(file); + + if (string.IsNullOrEmpty(fileName)) { + ASF.ArchiLogger.LogNullError(fileName); + + return false; + } + + string relativeFilePath = Path.GetRelativePath(targetDirectory, file); + + if (string.IsNullOrEmpty(relativeFilePath)) { + ASF.ArchiLogger.LogNullError(relativeFilePath); + + return false; + } + + string? relativeDirectoryName = Path.GetDirectoryName(relativeFilePath); + + switch (relativeDirectoryName) { + case null: + ASF.ArchiLogger.LogNullError(relativeDirectoryName); + + return false; + case "": + // No directory, root folder + switch (fileName) { + case Logging.NLogConfigurationFile: + case SharedInfo.LogFile: + // Files with those names in root directory we want to keep + continue; + } + + break; + case SharedInfo.ArchivalLogsDirectory: + case SharedInfo.ConfigDirectory: + case SharedInfo.DebugDirectory: + case SharedInfo.PluginsDirectory: + case SharedInfo.UpdateDirectory: + // Files in those directories we want to keep in their current place + continue; + default: + // Files in subdirectories of those directories we want to keep as well + if (RelativeDirectoryStartsWith(relativeDirectoryName, SharedInfo.ArchivalLogsDirectory, SharedInfo.ConfigDirectory, SharedInfo.DebugDirectory, SharedInfo.PluginsDirectory, SharedInfo.UpdateDirectory)) { + continue; + } + + break; + } + + string targetBackupDirectory = relativeDirectoryName.Length > 0 ? Path.Combine(backupDirectory, relativeDirectoryName) : backupDirectory; + Directory.CreateDirectory(targetBackupDirectory); + + string targetBackupFile = Path.Combine(targetBackupDirectory, fileName); + + File.Move(file, targetBackupFile, true); + } + + // We can now get rid of directories that are empty + DeleteEmptyDirectoriesRecursively(targetDirectory); + + if (!Directory.Exists(targetDirectory)) { + Directory.CreateDirectory(targetDirectory); + } + + // Now enumerate over files in the zip archive, skip directory entries that we're not interested in (we can create them ourselves if needed) + foreach (ZipArchiveEntry zipFile in zipArchive.Entries.Where(static zipFile => !string.IsNullOrEmpty(zipFile.Name))) { + string file = Path.GetFullPath(Path.Combine(targetDirectory, zipFile.FullName)); + + if (!file.StartsWith(targetDirectory, StringComparison.Ordinal)) { + throw new InvalidOperationException(nameof(file)); + } + + if (File.Exists(file)) { + // This is possible only with files that we decided to leave in place during our backup function + string targetBackupFile = $"{file}.bak"; + + File.Move(file, targetBackupFile, true); + } + + // Check if this file requires its own folder + if (zipFile.Name != zipFile.FullName) { + string? directory = Path.GetDirectoryName(file); + + if (string.IsNullOrEmpty(directory)) { + ASF.ArchiLogger.LogNullError(directory); + + return false; + } + + if (!Directory.Exists(directory)) { + Directory.CreateDirectory(directory); + } + + // We're not interested in extracting placeholder files (but we still want directories created for them, done above) + switch (zipFile.Name) { + case ".gitkeep": + continue; + } + } + + zipFile.ExtractToFile(file); + } + + return true; + } + internal static void WarnAboutIncompleteTranslation(ResourceManager resourceManager) { ArgumentNullException.ThrowIfNull(resourceManager); @@ -391,4 +485,36 @@ internal static void WarnAboutIncompleteTranslation(ResourceManager resourceMana ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.TranslationIncomplete, $"{CultureInfo.CurrentUICulture.Name} ({CultureInfo.CurrentUICulture.EnglishName})", translationCompleteness.ToString("P1", CultureInfo.CurrentCulture))); } } + + private static void DeleteEmptyDirectoriesRecursively(string directory) { + ArgumentException.ThrowIfNullOrEmpty(directory); + + if (!Directory.Exists(directory)) { + return; + } + + try { + foreach (string subDirectory in Directory.EnumerateDirectories(directory)) { + DeleteEmptyDirectoriesRecursively(subDirectory); + } + + if (!Directory.EnumerateFileSystemEntries(directory).Any()) { + Directory.Delete(directory); + } + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + } + } + + private static bool RelativeDirectoryStartsWith(string directory, params string[] prefixes) { + ArgumentException.ThrowIfNullOrEmpty(directory); + +#pragma warning disable CA1508 // False positive, params could be null when explicitly set + if ((prefixes == null) || (prefixes.Length == 0)) { +#pragma warning restore CA1508 // False positive, params could be null when explicitly set + throw new ArgumentNullException(nameof(prefixes)); + } + + return (from prefix in prefixes where directory.Length > prefix.Length let pathSeparator = directory[prefix.Length] where (pathSeparator == Path.DirectorySeparatorChar) || (pathSeparator == Path.AltDirectorySeparatorChar) select prefix).Any(prefix => directory.StartsWith(prefix, StringComparison.Ordinal)); + } } diff --git a/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs b/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs index 85e4b2cd804e3..f9bddcd587794 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs @@ -29,6 +29,8 @@ using ArchiSteamFarm.IPC.Responses; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Web; +using ArchiSteamFarm.Web.GitHub; +using ArchiSteamFarm.Web.GitHub.Data; using Microsoft.AspNetCore.Mvc; namespace ArchiSteamFarm.IPC.Controllers.Api; @@ -47,7 +49,7 @@ public sealed class GitHubController : ArchiController { public async Task> GitHubReleaseGet() { CancellationToken cancellationToken = HttpContext.RequestAborted; - GitHub.ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(false, cancellationToken).ConfigureAwait(false); + ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(SharedInfo.GithubRepo, false, cancellationToken).ConfigureAwait(false); return releaseResponse != null ? Ok(new GenericResponse(new GitHubReleaseResponse(releaseResponse))) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))); } @@ -67,11 +69,11 @@ public async Task> GitHubReleaseGet(string version CancellationToken cancellationToken = HttpContext.RequestAborted; - GitHub.ReleaseResponse? releaseResponse; + ReleaseResponse? releaseResponse; switch (version.ToUpperInvariant()) { case "LATEST": - releaseResponse = await GitHub.GetLatestRelease(cancellationToken: cancellationToken).ConfigureAwait(false); + releaseResponse = await GitHub.GetLatestRelease(SharedInfo.GithubRepo, cancellationToken: cancellationToken).ConfigureAwait(false); break; default: @@ -79,7 +81,7 @@ public async Task> GitHubReleaseGet(string version return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(version)))); } - releaseResponse = await GitHub.GetRelease(parsedVersion.ToString(4), cancellationToken).ConfigureAwait(false); + releaseResponse = await GitHub.GetRelease(SharedInfo.GithubRepo, parsedVersion.ToString(4), cancellationToken).ConfigureAwait(false); break; } diff --git a/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs b/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs index 9c658b623cdbb..af02965437902 100644 --- a/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs +++ b/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs @@ -22,7 +22,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -using ArchiSteamFarm.Web; +using ArchiSteamFarm.Web.GitHub.Data; namespace ArchiSteamFarm.IPC.Responses; @@ -59,7 +59,7 @@ public sealed class GitHubReleaseResponse { [Required] public string Version { get; private init; } - internal GitHubReleaseResponse(GitHub.ReleaseResponse releaseResponse) { + internal GitHubReleaseResponse(ReleaseResponse releaseResponse) { ArgumentNullException.ThrowIfNull(releaseResponse); ChangelogHTML = releaseResponse.ChangelogHTML ?? ""; diff --git a/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs new file mode 100644 index 0000000000000..6102481f3c0c2 --- /dev/null +++ b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs @@ -0,0 +1,62 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using ArchiSteamFarm.Web.GitHub.Data; +using JetBrains.Annotations; + +namespace ArchiSteamFarm.Plugins.Interfaces; + +[PublicAPI] +public interface IPluginUpdates : IPlugin { + /// + /// ASF will use this property as a target for GitHub updates. GitHub repository specified here must have valid releases that will be used for updates. + /// + /// Repository name in format of {Author}/{Repository}. + /// JustArchiNET/ArchiSteamFarm + string RepositoryName { get; } + + /// + /// ASF will call this function for determining the target asset name to update to. This asset should be available in specified release. It's permitted to return null/empty if you want to cancel update to given version. + /// + /// Target ASF version that plugin update should be compatible with. In rare cases, this might not match currently running ASF version, in particular when updating to newer release and checking if any plugins are compatible with it. + /// ASF variant of current instance, which may be useful if you're providing different versions for different ASF variants. + /// The target (new) version of the plugin found available in . + /// Available release assets for auto-update. Those come directly from your release on GitHub. + /// Target release asset from that should be used for auto-update. You may return null if the update is unavailable, for example, because ASF version/variant is determined unsupported, or due to any other custom reason. + Task GetTargetReleaseAsset(Version asfVersion, string asfVariant, Version newPluginVersion, IReadOnlyCollection releaseAssets); + + /// + /// ASF will call this method after update to a particular ASF version has been finished, just before restart of the process. + /// + /// The current (old) version of ASF program. + /// The target (new) version of ASF program. + Task OnUpdateFinished(Version currentVersion, Version newVersion); + + /// + /// ASF will call this method before proceeding with an update to a particular ASF version. + /// + /// The current (old) version of ASF program. + /// The target (new) version of ASF program. + Task OnUpdateProceeding(Version currentVersion, Version newVersion); +} diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index dc037bb85aea7..5a7819298e5a6 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -29,6 +29,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.IO.Compression; using System.Linq; using System.Reflection; using System.Security.Cryptography; @@ -43,6 +44,9 @@ using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Exchange; using ArchiSteamFarm.Steam.Integration.Callbacks; +using ArchiSteamFarm.Web.GitHub; +using ArchiSteamFarm.Web.GitHub.Data; +using ArchiSteamFarm.Web.Responses; using JetBrains.Annotations; using SteamKit2; @@ -655,6 +659,100 @@ internal static async Task OnUpdateProceeding(Version newVersion) { } } + internal static async Task UpdatePlugins(Version asfVersion, bool stable) { + if (ActivePlugins.Count == 0) { + return false; + } + + if (ASF.WebBrowser == null) { + throw new InvalidOperationException(nameof(ASF.WebBrowser)); + } + + bool restartNeeded = false; + + // We update plugins one-by-one to limit memory pressure from potentially big release assets + foreach (IPluginUpdates plugin in ActivePlugins.OfType()) { + string repoName = plugin.RepositoryName; + + if (string.IsNullOrEmpty(repoName)) { + continue; + } + + Console.WriteLine($"Checking update for {plugin.Name} plugin..."); + + ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(repoName, stable).ConfigureAwait(false); + + if (releaseResponse == null) { + continue; + } + + Version newVersion = new(releaseResponse.Tag); + + if (plugin.Version >= newVersion) { + Console.WriteLine($"No update available for {plugin.Name} plugin: {plugin.Version} >= {newVersion}."); + + continue; + } + + Console.WriteLine($"Updating {plugin.Name} plugin from version {plugin.Version} to {newVersion}..."); + + ReleaseAsset? asset = await plugin.GetTargetReleaseAsset(asfVersion, SharedInfo.BuildInfo.Variant, newVersion, releaseResponse.Assets).ConfigureAwait(false); + + if ((asset == null) || !releaseResponse.Assets.Contains(asset)) { + continue; + } + + Progress progressReporter = new(); + + progressReporter.ProgressChanged += Utilities.OnProgressChanged; + + BinaryResponse? response; + + try { + response = await ASF.WebBrowser.UrlGetToBinary(asset.DownloadURL, progressReporter: progressReporter).ConfigureAwait(false); + } finally { + progressReporter.ProgressChanged -= Utilities.OnProgressChanged; + } + + if (response?.Content == null) { + continue; + } + + ASF.ArchiLogger.LogGenericInfo(Strings.PatchingFiles); + + string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location); + + if (string.IsNullOrEmpty(assemblyDirectory)) { + // Invalid path provided + continue; + } + + byte[] responseBytes = response.Content as byte[] ?? response.Content.ToArray(); + + try { + MemoryStream memoryStream = new(responseBytes); + + await using (memoryStream.ConfigureAwait(false)) { + using ZipArchive zipArchive = new(memoryStream); + + if (!Utilities.UpdateFromArchive(zipArchive, assemblyDirectory)) { + ASF.ArchiLogger.LogGenericError(Strings.WarningFailed); + + continue; + } + } + + restartNeeded = true; + + Console.WriteLine($"Updating {plugin.Name} plugin has succeeded, the changes will be loaded on the next ASF launch."); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + } + } + + return restartNeeded; + } + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")] private static HashSet? LoadAssembliesFrom(string path) { ArgumentException.ThrowIfNullOrEmpty(path); diff --git a/ArchiSteamFarm/SharedInfo.cs b/ArchiSteamFarm/SharedInfo.cs index 6045dc1c977de..c0ed885235e6b 100644 --- a/ArchiSteamFarm/SharedInfo.cs +++ b/ArchiSteamFarm/SharedInfo.cs @@ -45,7 +45,6 @@ public static class SharedInfo { internal const string EnvironmentVariableCryptKeyFile = $"{EnvironmentVariableCryptKey}_FILE"; internal const string EnvironmentVariableNetworkGroup = $"{ASF}_NETWORK_GROUP"; internal const string EnvironmentVariablePath = $"{ASF}_PATH"; - internal const string GithubReleaseURL = $"https://api.github.com/repos/{GithubRepo}/releases"; internal const string GithubRepo = $"JustArchiNET/{AssemblyName}"; internal const string GlobalConfigFileName = $"{ASF}{JsonConfigExtension}"; internal const string GlobalCrashFileName = $"{ASF}.crash"; diff --git a/ArchiSteamFarm/Steam/Interaction/Actions.cs b/ArchiSteamFarm/Steam/Interaction/Actions.cs index 900fa18eceef1..b6b79c2da0225 100644 --- a/ArchiSteamFarm/Steam/Interaction/Actions.cs +++ b/ArchiSteamFarm/Steam/Interaction/Actions.cs @@ -473,19 +473,17 @@ static async () => { throw new InvalidEnumArgumentException(nameof(channel), (int) channel, typeof(GlobalConfig.EUpdateChannel)); } - Version? version = await ASF.Update(channel, true).ConfigureAwait(false); + (Version? newVersion, bool restartNeeded) = await ASF.Update(channel, true).ConfigureAwait(false); - if (version == null) { - return (false, null, null); + if (restartNeeded) { + Utilities.InBackground(ASF.RestartOrExit); } - if (SharedInfo.Version >= version) { - return (false, $"V{SharedInfo.Version} ≥ V{version}", version); + if (newVersion == null) { + return (false, null, null); } - Utilities.InBackground(ASF.RestartOrExit); - - return (true, null, version); + return newVersion > SharedInfo.Version ? (true, null, newVersion) : (false, $"V{SharedInfo.Version} ≥ V{newVersion}", newVersion); } internal async Task AcceptDigitalGiftCards() { diff --git a/ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs b/ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs new file mode 100644 index 0000000000000..e8a2340026fb0 --- /dev/null +++ b/ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs @@ -0,0 +1,47 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace ArchiSteamFarm.Web.GitHub.Data; + +[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] +public sealed class ReleaseAsset { + [JsonInclude] + [JsonPropertyName("name")] + [JsonRequired] + public string Name { get; private init; } = ""; + + [JsonInclude] + [JsonPropertyName("browser_download_url")] + [JsonRequired] + internal Uri DownloadURL { get; private init; } = null!; + + [JsonInclude] + [JsonPropertyName("size")] + [JsonRequired] + internal uint Size { get; private init; } + + [JsonConstructor] + private ReleaseAsset() { } +} diff --git a/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs b/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs new file mode 100644 index 0000000000000..78ca8c97987d8 --- /dev/null +++ b/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs @@ -0,0 +1,150 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text.Json.Serialization; +using ArchiSteamFarm.Core; +using Markdig; +using Markdig.Renderers; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; + +namespace ArchiSteamFarm.Web.GitHub.Data; + +[SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] +internal sealed class ReleaseResponse { + internal string? ChangelogHTML { + get { + if (BackingChangelogHTML != null) { + return BackingChangelogHTML; + } + + if (Changelog == null) { + ASF.ArchiLogger.LogNullError(Changelog); + + return null; + } + + using StringWriter writer = new(); + + HtmlRenderer renderer = new(writer); + + renderer.Render(Changelog); + writer.Flush(); + + return BackingChangelogHTML = writer.ToString(); + } + } + + internal string? ChangelogPlainText { + get { + if (BackingChangelogPlainText != null) { + return BackingChangelogPlainText; + } + + if (Changelog == null) { + ASF.ArchiLogger.LogNullError(Changelog); + + return null; + } + + using StringWriter writer = new(); + + HtmlRenderer renderer = new(writer) { + EnableHtmlForBlock = false, + EnableHtmlForInline = false, + EnableHtmlEscape = false + }; + + renderer.Render(Changelog); + writer.Flush(); + + return BackingChangelogPlainText = writer.ToString(); + } + } + + private MarkdownDocument? Changelog { + get { + if (BackingChangelog != null) { + return BackingChangelog; + } + + if (string.IsNullOrEmpty(MarkdownBody)) { + ASF.ArchiLogger.LogNullError(MarkdownBody); + + return null; + } + + return BackingChangelog = ExtractChangelogFromBody(MarkdownBody); + } + } + + [JsonInclude] + [JsonPropertyName("assets")] + [JsonRequired] + internal ImmutableHashSet Assets { get; private init; } = ImmutableHashSet.Empty; + + [JsonInclude] + [JsonPropertyName("prerelease")] + [JsonRequired] + internal bool IsPreRelease { get; private init; } + + [JsonInclude] + [JsonPropertyName("published_at")] + [JsonRequired] + internal DateTime PublishedAt { get; private init; } + + [JsonInclude] + [JsonPropertyName("tag_name")] + [JsonRequired] + internal string Tag { get; private init; } = ""; + + private MarkdownDocument? BackingChangelog; + private string? BackingChangelogHTML; + private string? BackingChangelogPlainText; + + [JsonInclude] + [JsonPropertyName("body")] + [JsonRequired] + private string? MarkdownBody { get; init; } = ""; + + [JsonConstructor] + private ReleaseResponse() { } + + private static MarkdownDocument ExtractChangelogFromBody(string markdownText) { + ArgumentException.ThrowIfNullOrEmpty(markdownText); + + MarkdownDocument markdownDocument = Markdown.Parse(markdownText); + MarkdownDocument result = []; + + foreach (Block block in markdownDocument.SkipWhile(static block => block is not HeadingBlock { Inline.FirstChild: LiteralInline literalInline } || (literalInline.Content.ToString()?.Equals("Changelog", StringComparison.OrdinalIgnoreCase) != true)).Skip(1).TakeWhile(static block => block is not ThematicBreakBlock).ToList()) { + // All blocks that we're interested in must be removed from original markdownDocument firstly + markdownDocument.Remove(block); + result.Add(block); + } + + return result; + } +} diff --git a/ArchiSteamFarm/Web/GitHub.cs b/ArchiSteamFarm/Web/GitHub/GitHub.cs similarity index 59% rename from ArchiSteamFarm/Web/GitHub.cs rename to ArchiSteamFarm/Web/GitHub/GitHub.cs index 7225c7b9a93a6..fb61b88dca7a5 100644 --- a/ArchiSteamFarm/Web/GitHub.cs +++ b/ArchiSteamFarm/Web/GitHub/GitHub.cs @@ -22,27 +22,23 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.IO; using System.Linq; using System.Net; -using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using AngleSharp.Dom; using ArchiSteamFarm.Core; +using ArchiSteamFarm.Web.GitHub.Data; using ArchiSteamFarm.Web.Responses; -using Markdig; -using Markdig.Renderers; -using Markdig.Syntax; -using Markdig.Syntax.Inlines; -namespace ArchiSteamFarm.Web; +namespace ArchiSteamFarm.Web.GitHub; internal static class GitHub { - internal static async Task GetLatestRelease(bool stable = true, CancellationToken cancellationToken = default) { - Uri request = new($"{SharedInfo.GithubReleaseURL}{(stable ? "/latest" : "?per_page=1")}"); + internal static async Task GetLatestRelease(string repoName, bool stable = true, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(repoName); + + Uri request = new($"https://api.github.com/repos/{repoName}/releases{(stable ? "/latest" : "?per_page=1")}"); if (stable) { return await GetReleaseFromURL(request, cancellationToken).ConfigureAwait(false); @@ -53,10 +49,11 @@ internal static class GitHub { return response?.FirstOrDefault(); } - internal static async Task GetRelease(string version, CancellationToken cancellationToken = default) { + internal static async Task GetRelease(string repoName, string version, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(repoName); ArgumentException.ThrowIfNullOrEmpty(version); - Uri request = new($"{SharedInfo.GithubReleaseURL}/tags/{version}"); + Uri request = new($"https://api.github.com/repos/{repoName}/releases/tags/{version}"); return await GetReleaseFromURL(request, cancellationToken).ConfigureAwait(false); } @@ -152,21 +149,6 @@ internal static class GitHub { return markdownBodyNode?.InnerHtml.Trim() ?? ""; } - private static MarkdownDocument ExtractChangelogFromBody(string markdownText) { - ArgumentException.ThrowIfNullOrEmpty(markdownText); - - MarkdownDocument markdownDocument = Markdown.Parse(markdownText); - MarkdownDocument result = []; - - foreach (Block block in markdownDocument.SkipWhile(static block => block is not HeadingBlock { Inline.FirstChild: LiteralInline literalInline } || (literalInline.Content.ToString()?.Equals("Changelog", StringComparison.OrdinalIgnoreCase) != true)).Skip(1).TakeWhile(static block => block is not ThematicBreakBlock).ToList()) { - // All blocks that we're interested in must be removed from original markdownDocument firstly - markdownDocument.Remove(block); - result.Add(block); - } - - return result; - } - private static async Task GetReleaseFromURL(Uri request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); @@ -190,125 +172,4 @@ private static MarkdownDocument ExtractChangelogFromBody(string markdownText) { return response?.Content; } - - [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] - internal sealed class ReleaseResponse { - internal string? ChangelogHTML { - get { - if (BackingChangelogHTML != null) { - return BackingChangelogHTML; - } - - if (Changelog == null) { - ASF.ArchiLogger.LogNullError(Changelog); - - return null; - } - - using StringWriter writer = new(); - - HtmlRenderer renderer = new(writer); - - renderer.Render(Changelog); - writer.Flush(); - - return BackingChangelogHTML = writer.ToString(); - } - } - - internal string? ChangelogPlainText { - get { - if (BackingChangelogPlainText != null) { - return BackingChangelogPlainText; - } - - if (Changelog == null) { - ASF.ArchiLogger.LogNullError(Changelog); - - return null; - } - - using StringWriter writer = new(); - - HtmlRenderer renderer = new(writer) { - EnableHtmlForBlock = false, - EnableHtmlForInline = false, - EnableHtmlEscape = false - }; - - renderer.Render(Changelog); - writer.Flush(); - - return BackingChangelogPlainText = writer.ToString(); - } - } - - private MarkdownDocument? Changelog { - get { - if (BackingChangelog != null) { - return BackingChangelog; - } - - if (string.IsNullOrEmpty(MarkdownBody)) { - ASF.ArchiLogger.LogNullError(MarkdownBody); - - return null; - } - - return BackingChangelog = ExtractChangelogFromBody(MarkdownBody); - } - } - - [JsonInclude] - [JsonPropertyName("assets")] - [JsonRequired] - internal ImmutableHashSet Assets { get; private init; } = ImmutableHashSet.Empty; - - [JsonInclude] - [JsonPropertyName("prerelease")] - [JsonRequired] - internal bool IsPreRelease { get; private init; } - - [JsonInclude] - [JsonPropertyName("published_at")] - [JsonRequired] - internal DateTime PublishedAt { get; private init; } - - [JsonInclude] - [JsonPropertyName("tag_name")] - [JsonRequired] - internal string Tag { get; private init; } = ""; - - private MarkdownDocument? BackingChangelog; - private string? BackingChangelogHTML; - private string? BackingChangelogPlainText; - - [JsonInclude] - [JsonPropertyName("body")] - [JsonRequired] - private string? MarkdownBody { get; init; } = ""; - - [JsonConstructor] - private ReleaseResponse() { } - - internal sealed class Asset { - [JsonInclude] - [JsonPropertyName("browser_download_url")] - [JsonRequired] - internal Uri? DownloadURL { get; private init; } - - [JsonInclude] - [JsonPropertyName("name")] - [JsonRequired] - internal string? Name { get; private init; } - - [JsonInclude] - [JsonPropertyName("size")] - [JsonRequired] - internal uint Size { get; private init; } - - [JsonConstructor] - private Asset() { } - } - } } From 733fb82107d738413cfdb40277728e9cb203320e Mon Sep 17 00:00:00 2001 From: Archi Date: Sat, 9 Mar 2024 22:17:41 +0100 Subject: [PATCH 02/32] Update PluginsCore.cs --- ArchiSteamFarm/Plugins/PluginsCore.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index 5a7819298e5a6..be611b84051c0 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -678,7 +678,7 @@ internal static async Task UpdatePlugins(Version asfVersion, bool stable) continue; } - Console.WriteLine($"Checking update for {plugin.Name} plugin..."); + ASF.ArchiLogger.LogGenericInfo($"Checking update for {plugin.Name} plugin..."); ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(repoName, stable).ConfigureAwait(false); @@ -689,12 +689,12 @@ internal static async Task UpdatePlugins(Version asfVersion, bool stable) Version newVersion = new(releaseResponse.Tag); if (plugin.Version >= newVersion) { - Console.WriteLine($"No update available for {plugin.Name} plugin: {plugin.Version} >= {newVersion}."); + ASF.ArchiLogger.LogGenericInfo($"No update available for {plugin.Name} plugin: {plugin.Version} >= {newVersion}."); continue; } - Console.WriteLine($"Updating {plugin.Name} plugin from version {plugin.Version} to {newVersion}..."); + ASF.ArchiLogger.LogGenericInfo($"Updating {plugin.Name} plugin from version {plugin.Version} to {newVersion}..."); ReleaseAsset? asset = await plugin.GetTargetReleaseAsset(asfVersion, SharedInfo.BuildInfo.Variant, newVersion, releaseResponse.Assets).ConfigureAwait(false); @@ -744,7 +744,7 @@ internal static async Task UpdatePlugins(Version asfVersion, bool stable) restartNeeded = true; - Console.WriteLine($"Updating {plugin.Name} plugin has succeeded, the changes will be loaded on the next ASF launch."); + ASF.ArchiLogger.LogGenericInfo($"Updating {plugin.Name} plugin has succeeded, the changes will be loaded on the next ASF launch."); } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); } From c746317fd05b61193dd4dbcbaa8c22add01f1891 Mon Sep 17 00:00:00 2001 From: Archi Date: Sat, 9 Mar 2024 22:25:32 +0100 Subject: [PATCH 03/32] Update IPluginUpdates.cs --- ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs index 6102481f3c0c2..e5fbbafaa9118 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs @@ -43,7 +43,7 @@ public interface IPluginUpdates : IPlugin { /// ASF variant of current instance, which may be useful if you're providing different versions for different ASF variants. /// The target (new) version of the plugin found available in . /// Available release assets for auto-update. Those come directly from your release on GitHub. - /// Target release asset from that should be used for auto-update. You may return null if the update is unavailable, for example, because ASF version/variant is determined unsupported, or due to any other custom reason. + /// Target release asset from those provided that should be used for auto-update. You may return null if the update is unavailable, for example, because ASF version/variant is determined unsupported, or due to any other custom reason. Task GetTargetReleaseAsset(Version asfVersion, string asfVariant, Version newPluginVersion, IReadOnlyCollection releaseAssets); /// From 8fa277d48b8143ea2038bd0c971f1c59e47e2502 Mon Sep 17 00:00:00 2001 From: Archi Date: Sat, 9 Mar 2024 22:34:16 +0100 Subject: [PATCH 04/32] Update PluginsCore.cs --- ArchiSteamFarm/Plugins/PluginsCore.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index be611b84051c0..7c75a71c20a7e 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -659,6 +659,7 @@ internal static async Task OnUpdateProceeding(Version newVersion) { } } + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3000", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")] internal static async Task UpdatePlugins(Version asfVersion, bool stable) { if (ActivePlugins.Count == 0) { return false; From d30dcb569d5cb87cf05412f0f41deb4e6a9e6cff Mon Sep 17 00:00:00 2001 From: Archi Date: Sat, 9 Mar 2024 23:47:59 +0100 Subject: [PATCH 05/32] Make it work --- ArchiSteamFarm/IPC/ArchiKestrel.cs | 3 +- .../Localization/Strings.Designer.cs | 6 ++ ArchiSteamFarm/Localization/Strings.resx | 4 + .../Plugins/Interfaces/IPluginUpdates.cs | 12 +-- ArchiSteamFarm/Plugins/PluginsCore.cs | 98 ++++++++++++------- 5 files changed, 77 insertions(+), 46 deletions(-) diff --git a/ArchiSteamFarm/IPC/ArchiKestrel.cs b/ArchiSteamFarm/IPC/ArchiKestrel.cs index 69893e1289e68..e8ff8cd4de0f6 100644 --- a/ArchiSteamFarm/IPC/ArchiKestrel.cs +++ b/ArchiSteamFarm/IPC/ArchiKestrel.cs @@ -172,8 +172,7 @@ private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeF string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location); if (string.IsNullOrEmpty(assemblyDirectory)) { - // Invalid path provided - continue; + throw new InvalidOperationException(nameof(assemblyDirectory)); } physicalPath = Path.Combine(assemblyDirectory, plugin.PhysicalPath); diff --git a/ArchiSteamFarm/Localization/Strings.Designer.cs b/ArchiSteamFarm/Localization/Strings.Designer.cs index 84e20f77aa9f8..f4fbce26c12e4 100644 --- a/ArchiSteamFarm/Localization/Strings.Designer.cs +++ b/ArchiSteamFarm/Localization/Strings.Designer.cs @@ -1238,5 +1238,11 @@ public static string IdlingGameNotPossiblePrivate { return ResourceManager.GetString("IdlingGameNotPossiblePrivate", resourceCulture); } } + + public static string WarningSkipping { + get { + return ResourceManager.GetString("WarningSkipping", resourceCulture); + } + } } } diff --git a/ArchiSteamFarm/Localization/Strings.resx b/ArchiSteamFarm/Localization/Strings.resx index 990837d3340c7..be52d3d00c844 100644 --- a/ArchiSteamFarm/Localization/Strings.resx +++ b/ArchiSteamFarm/Localization/Strings.resx @@ -764,4 +764,8 @@ Process uptime: {1} Farming {0} ({1}) is disabled, as that game is currently marked as private. If you intend from ASF to farm that game, then consider changing its privacy settings. {0} will be replaced by game's ID (number), {1} will be replaced by game's name + + Skipping: {0}... + {0} will be replaced by text value (string) of entry being skipped. + diff --git a/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs index e5fbbafaa9118..fad89ad77b1b8 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs @@ -47,16 +47,16 @@ public interface IPluginUpdates : IPlugin { Task GetTargetReleaseAsset(Version asfVersion, string asfVariant, Version newPluginVersion, IReadOnlyCollection releaseAssets); /// - /// ASF will call this method after update to a particular ASF version has been finished, just before restart of the process. + /// ASF will call this method after update to a particular plugin version has been finished, just before restart of the process. /// - /// The current (old) version of ASF program. - /// The target (new) version of ASF program. + /// The current (old) version of plugin assembly. + /// The target (new) version of plugin assembly. Task OnUpdateFinished(Version currentVersion, Version newVersion); /// - /// ASF will call this method before proceeding with an update to a particular ASF version. + /// ASF will call this method before proceeding with an update to a particular plugin version. /// - /// The current (old) version of ASF program. - /// The target (new) version of ASF program. + /// The current (old) version of plugin assembly. + /// The target (new) version of plugin assembly. Task OnUpdateProceeding(Version currentVersion, Version newVersion); } diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index 7c75a71c20a7e..c21068e5ef275 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -673,69 +673,81 @@ internal static async Task UpdatePlugins(Version asfVersion, bool stable) // We update plugins one-by-one to limit memory pressure from potentially big release assets foreach (IPluginUpdates plugin in ActivePlugins.OfType()) { - string repoName = plugin.RepositoryName; + try { + ASF.ArchiLogger.LogGenericInfo($"Checking update for {plugin.Name} plugin..."); - if (string.IsNullOrEmpty(repoName)) { - continue; - } + string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location); - ASF.ArchiLogger.LogGenericInfo($"Checking update for {plugin.Name} plugin..."); + if (string.IsNullOrEmpty(assemblyDirectory)) { + throw new InvalidOperationException(nameof(assemblyDirectory)); + } - ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(repoName, stable).ConfigureAwait(false); + string backupDirectory = Path.Combine(assemblyDirectory, SharedInfo.UpdateDirectory); - if (releaseResponse == null) { - continue; - } + if (Directory.Exists(backupDirectory)) { + ASF.ArchiLogger.LogGenericInfo(Strings.UpdateCleanup); - Version newVersion = new(releaseResponse.Tag); + Directory.Delete(backupDirectory, true); + } - if (plugin.Version >= newVersion) { - ASF.ArchiLogger.LogGenericInfo($"No update available for {plugin.Name} plugin: {plugin.Version} >= {newVersion}."); + string repoName = plugin.RepositoryName; - continue; - } + if (string.IsNullOrEmpty(repoName)) { + ASF.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(plugin.RepositoryName))); - ASF.ArchiLogger.LogGenericInfo($"Updating {plugin.Name} plugin from version {plugin.Version} to {newVersion}..."); + continue; + } - ReleaseAsset? asset = await plugin.GetTargetReleaseAsset(asfVersion, SharedInfo.BuildInfo.Variant, newVersion, releaseResponse.Assets).ConfigureAwait(false); + ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(repoName, stable).ConfigureAwait(false); - if ((asset == null) || !releaseResponse.Assets.Contains(asset)) { - continue; - } + if (releaseResponse == null) { + continue; + } - Progress progressReporter = new(); + Version pluginVersion = plugin.Version; + Version newVersion = new(releaseResponse.Tag); - progressReporter.ProgressChanged += Utilities.OnProgressChanged; + if (pluginVersion >= newVersion) { + ASF.ArchiLogger.LogGenericInfo($"No update available for {plugin.Name} plugin: {pluginVersion} >= {newVersion}."); - BinaryResponse? response; + continue; + } - try { - response = await ASF.WebBrowser.UrlGetToBinary(asset.DownloadURL, progressReporter: progressReporter).ConfigureAwait(false); - } finally { - progressReporter.ProgressChanged -= Utilities.OnProgressChanged; - } + ASF.ArchiLogger.LogGenericInfo($"Updating {plugin.Name} plugin from version {pluginVersion} to {newVersion}..."); - if (response?.Content == null) { - continue; - } + ReleaseAsset? asset = await plugin.GetTargetReleaseAsset(asfVersion, SharedInfo.BuildInfo.Variant, newVersion, releaseResponse.Assets).ConfigureAwait(false); - ASF.ArchiLogger.LogGenericInfo(Strings.PatchingFiles); + if ((asset == null) || !releaseResponse.Assets.Contains(asset)) { + continue; + } - string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location); + Progress progressReporter = new(); - if (string.IsNullOrEmpty(assemblyDirectory)) { - // Invalid path provided - continue; - } + progressReporter.ProgressChanged += Utilities.OnProgressChanged; - byte[] responseBytes = response.Content as byte[] ?? response.Content.ToArray(); + BinaryResponse? response; + + try { + response = await ASF.WebBrowser.UrlGetToBinary(asset.DownloadURL, progressReporter: progressReporter).ConfigureAwait(false); + } finally { + progressReporter.ProgressChanged -= Utilities.OnProgressChanged; + } + + if (response?.Content == null) { + continue; + } + + ASF.ArchiLogger.LogGenericInfo(Strings.PatchingFiles); + + byte[] responseBytes = response.Content as byte[] ?? response.Content.ToArray(); - try { MemoryStream memoryStream = new(responseBytes); await using (memoryStream.ConfigureAwait(false)) { using ZipArchive zipArchive = new(memoryStream); + await plugin.OnUpdateProceeding(pluginVersion, newVersion).ConfigureAwait(false); + if (!Utilities.UpdateFromArchive(zipArchive, assemblyDirectory)) { ASF.ArchiLogger.LogGenericError(Strings.WarningFailed); @@ -746,6 +758,8 @@ internal static async Task UpdatePlugins(Version asfVersion, bool stable) restartNeeded = true; ASF.ArchiLogger.LogGenericInfo($"Updating {plugin.Name} plugin has succeeded, the changes will be loaded on the next ASF launch."); + + await plugin.OnUpdateFinished(pluginVersion, newVersion).ConfigureAwait(false); } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); } @@ -766,6 +780,14 @@ internal static async Task UpdatePlugins(Version asfVersion, bool stable) try { foreach (string assemblyPath in Directory.EnumerateFiles(path, "*.dll", SearchOption.AllDirectories)) { + string? assemblyDirectoryName = Path.GetFileName(Path.GetDirectoryName(assemblyPath)); + + if (assemblyDirectoryName == SharedInfo.UpdateDirectory) { + ASF.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningSkipping, assemblyPath)); + + continue; + } + Assembly assembly; try { From bccd1bb2b887f9fdd4e406b14c7e020295a18818 Mon Sep 17 00:00:00 2001 From: Archi Date: Sat, 9 Mar 2024 23:57:47 +0100 Subject: [PATCH 06/32] Misc --- ArchiSteamFarm/Web/WebBrowser.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/ArchiSteamFarm/Web/WebBrowser.cs b/ArchiSteamFarm/Web/WebBrowser.cs index ab50e12b397d1..7dd4a18e220d8 100644 --- a/ArchiSteamFarm/Web/WebBrowser.cs +++ b/ArchiSteamFarm/Web/WebBrowser.cs @@ -177,22 +177,18 @@ public HttpClient GenerateDisposableHttpClient(bool extendedTimeout = false) { while (response.Content.CanRead) { int read = await response.Content.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); - if (read == 0) { + if (read <= 0) { break; } - await ms.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); - - if ((progressReporter == null) || (batchIncreaseSize == 0) || (batch >= 99)) { - continue; - } - readThisBatch += read; while ((readThisBatch >= batchIncreaseSize) && (batch < 99)) { readThisBatch -= batchIncreaseSize; - progressReporter.Report(++batch); + progressReporter?.Report(++batch); } + + await ms.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; From f1b5a8d1724cb90839ea1bcfeaa9d603974e6c85 Mon Sep 17 00:00:00 2001 From: Archi Date: Sun, 10 Mar 2024 00:00:41 +0100 Subject: [PATCH 07/32] Revert "Misc" This reverts commit bccd1bb2b887f9fdd4e406b14c7e020295a18818. --- ArchiSteamFarm/Web/WebBrowser.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ArchiSteamFarm/Web/WebBrowser.cs b/ArchiSteamFarm/Web/WebBrowser.cs index 7dd4a18e220d8..ab50e12b397d1 100644 --- a/ArchiSteamFarm/Web/WebBrowser.cs +++ b/ArchiSteamFarm/Web/WebBrowser.cs @@ -177,18 +177,22 @@ public HttpClient GenerateDisposableHttpClient(bool extendedTimeout = false) { while (response.Content.CanRead) { int read = await response.Content.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); - if (read <= 0) { + if (read == 0) { break; } + await ms.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); + + if ((progressReporter == null) || (batchIncreaseSize == 0) || (batch >= 99)) { + continue; + } + readThisBatch += read; while ((readThisBatch >= batchIncreaseSize) && (batch < 99)) { readThisBatch -= batchIncreaseSize; - progressReporter?.Report(++batch); + progressReporter.Report(++batch); } - - await ms.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; From 008cd6c7c4c12287ff161a81976cad398fd55a7e Mon Sep 17 00:00:00 2001 From: Archi Date: Sun, 10 Mar 2024 00:21:27 +0100 Subject: [PATCH 08/32] Proper fix --- ArchiSteamFarm/Web/WebBrowser.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/ArchiSteamFarm/Web/WebBrowser.cs b/ArchiSteamFarm/Web/WebBrowser.cs index ab50e12b397d1..1539733b8907c 100644 --- a/ArchiSteamFarm/Web/WebBrowser.cs +++ b/ArchiSteamFarm/Web/WebBrowser.cs @@ -177,22 +177,20 @@ public HttpClient GenerateDisposableHttpClient(bool extendedTimeout = false) { while (response.Content.CanRead) { int read = await response.Content.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); - if (read == 0) { + if (read <= 0) { break; } - await ms.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); - - if ((progressReporter == null) || (batchIncreaseSize == 0) || (batch >= 99)) { - continue; - } - readThisBatch += read; - while ((readThisBatch >= batchIncreaseSize) && (batch < 99)) { - readThisBatch -= batchIncreaseSize; - progressReporter.Report(++batch); + for (; (readThisBatch >= batchIncreaseSize) && (batch < 99); readThisBatch -= batchIncreaseSize) { + // We need a copy of variable being passed when in for loops, as loop will proceed before our event is launched + byte progress = ++batch; + + progressReporter?.Report(progress); } + + await ms.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; From bd00a7218fd4eca64c69317262bcf29a8e75d2e8 Mon Sep 17 00:00:00 2001 From: Archi Date: Sun, 10 Mar 2024 19:17:29 +0100 Subject: [PATCH 09/32] Make plugin updates independent of GitHub --- ArchiSteamFarm/Core/ASF.cs | 21 ++-- .../Interfaces/IGitHubPluginUpdates.cs | 112 ++++++++++++++++++ .../Plugins/Interfaces/IPluginUpdates.cs | 27 ++--- ArchiSteamFarm/Plugins/PluginsCore.cs | 48 +++----- .../Web/GitHub/Data/ReleaseAsset.cs | 4 +- .../Web/GitHub/Data/ReleaseResponse.cs | 14 +-- ArchiSteamFarm/Web/GitHub/GitHub.cs | 15 ++- 7 files changed, 166 insertions(+), 75 deletions(-) create mode 100644 ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs diff --git a/ArchiSteamFarm/Core/ASF.cs b/ArchiSteamFarm/Core/ASF.cs index 20db1eed22253..45b15c498c839 100644 --- a/ArchiSteamFarm/Core/ASF.cs +++ b/ArchiSteamFarm/Core/ASF.cs @@ -187,22 +187,22 @@ internal static async Task RestartOrExit() { } } - internal static async Task<(Version? NewVersion, bool RestartNeeded)> Update(GlobalConfig.EUpdateChannel? channel = null, bool updateOverride = false) { - if (channel.HasValue && !Enum.IsDefined(channel.Value)) { - throw new InvalidEnumArgumentException(nameof(channel), (int) channel, typeof(GlobalConfig.EUpdateChannel)); + internal static async Task<(Version? NewVersion, bool RestartNeeded)> Update(GlobalConfig.EUpdateChannel? updateChannel = null, bool updateOverride = false) { + if (updateChannel.HasValue && !Enum.IsDefined(updateChannel.Value)) { + throw new InvalidEnumArgumentException(nameof(updateChannel), (int) updateChannel, typeof(GlobalConfig.EUpdateChannel)); } if (GlobalConfig == null) { throw new InvalidOperationException(nameof(GlobalConfig)); } - Version? newVersion = await UpdateASF(channel, updateOverride).ConfigureAwait(false); + Version? newVersion = await UpdateASF(updateChannel, updateOverride).ConfigureAwait(false); bool restartNeeded = newVersion > SharedInfo.Version; if (!restartNeeded) { // ASF wasn't updated as part of the process, update the plugins alone - restartNeeded = await PluginsCore.UpdatePlugins(SharedInfo.Version, (channel ?? GlobalConfig.UpdateChannel) == GlobalConfig.EUpdateChannel.Stable).ConfigureAwait(false); + restartNeeded = await PluginsCore.UpdatePlugins(SharedInfo.Version, updateChannel).ConfigureAwait(false); } return (newVersion, restartNeeded); @@ -929,7 +929,7 @@ private static async Task UpdateAndRestart() { await using (memoryStream.ConfigureAwait(false)) { using ZipArchive zipArchive = new(memoryStream); - if (!await UpdateFromArchive(newVersion, channel == GlobalConfig.EUpdateChannel.Stable, zipArchive).ConfigureAwait(false)) { + if (!await UpdateFromArchive(newVersion, channel.Value, zipArchive).ConfigureAwait(false)) { ArchiLogger.LogGenericError(Strings.WarningFailed); } } @@ -959,8 +959,13 @@ private static async Task UpdateAndRestart() { } } - private static async Task UpdateFromArchive(Version newVersion, bool stable, ZipArchive zipArchive) { + private static async Task UpdateFromArchive(Version newVersion, GlobalConfig.EUpdateChannel updateChannel, ZipArchive zipArchive) { ArgumentNullException.ThrowIfNull(newVersion); + + if (!Enum.IsDefined(updateChannel)) { + throw new InvalidEnumArgumentException(nameof(updateChannel), (int) updateChannel, typeof(GlobalConfig.EUpdateChannel)); + } + ArgumentNullException.ThrowIfNull(zipArchive); if (SharedInfo.HomeDirectory == AppContext.BaseDirectory) { @@ -975,7 +980,7 @@ private static async Task UpdateFromArchive(Version newVersion, bool stabl } // We're ready to start update process, handle any plugin updates ready for new version - await PluginsCore.UpdatePlugins(newVersion, stable).ConfigureAwait(false); + await PluginsCore.UpdatePlugins(newVersion, updateChannel).ConfigureAwait(false); return Utilities.UpdateFromArchive(zipArchive, SharedInfo.HomeDirectory); } diff --git a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs new file mode 100644 index 0000000000000..a7c889de7e7e9 --- /dev/null +++ b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs @@ -0,0 +1,112 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using ArchiSteamFarm.Core; +using ArchiSteamFarm.Localization; +using ArchiSteamFarm.Storage; +using ArchiSteamFarm.Web.GitHub; +using ArchiSteamFarm.Web.GitHub.Data; +using JetBrains.Annotations; + +namespace ArchiSteamFarm.Plugins.Interfaces; + +[PublicAPI] +public interface IGitHubPluginUpdates : IPluginUpdates { + /// + /// Boolean value that determines whether your plugin is able to update at the time of calling. You may provide false if, for example, you're inside a critical section and you don't want to update at this time, despite supporting updates otherwise. + /// + bool CanUpdate => true; + + /// + /// ASF will use this property as a target for GitHub updates. GitHub repository specified here must have valid releases that will be used for updates. + /// + /// Repository name in format of {Author}/{Repository}. + /// JustArchiNET/ArchiSteamFarm + string RepositoryName { get; } + +#pragma warning disable CA1033 // TODO + async Task IPluginUpdates.GetTargetReleaseURL(Version asfVersion, string asfVariant, GlobalConfig.EUpdateChannel updateChannel) { + ArgumentNullException.ThrowIfNull(asfVersion); + ArgumentException.ThrowIfNullOrEmpty(asfVariant); + + if (!Enum.IsDefined(updateChannel)) { + throw new InvalidEnumArgumentException(nameof(updateChannel), (int) updateChannel, typeof(GlobalConfig.EUpdateChannel)); + } + + if (!CanUpdate) { + return null; + } + + if (string.IsNullOrEmpty(RepositoryName)) { + ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(RepositoryName))); + + return null; + } + + ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(RepositoryName, updateChannel == GlobalConfig.EUpdateChannel.Stable).ConfigureAwait(false); + + if (releaseResponse == null) { + return null; + } + + Version newVersion = new(releaseResponse.Tag); + + if (Version >= newVersion) { + ASF.ArchiLogger.LogGenericInfo($"No update available for {Name} plugin: {Version} >= {newVersion}."); + + return null; + } + + ASF.ArchiLogger.LogGenericInfo($"Updating {Name} plugin from version {Version} to {newVersion}..."); + + ReleaseAsset? asset = await GetTargetReleaseAsset(asfVersion, asfVariant, newVersion, releaseResponse.Assets).ConfigureAwait(false); + + if ((asset == null) || !releaseResponse.Assets.Contains(asset)) { + return null; + } + + return asset.DownloadURL; + } +#pragma warning restore CA1033 // TODO + + /// + /// ASF will call this function for determining the target asset name to update to. This asset should be available in specified release. It's permitted to return null/empty if you want to cancel update to given version. Default implementation provides simple resolve based on flow from JustArchiNET/ASF-PluginTemplate repository. + /// + /// Target ASF version that plugin update should be compatible with. In rare cases, this might not match currently running ASF version, in particular when updating to newer release and checking if any plugins are compatible with it. + /// ASF variant of current instance, which may be useful if you're providing different versions for different ASF variants. + /// The target (new) version of the plugin found available in . + /// Available release assets for auto-update. Those come directly from your release on GitHub. + /// Target release asset from those provided that should be used for auto-update. You may return null if the update is unavailable, for example, because ASF version/variant is determined unsupported, or due to any other custom reason. + Task GetTargetReleaseAsset(Version asfVersion, string asfVariant, Version newPluginVersion, IReadOnlyCollection releaseAssets) { + ArgumentNullException.ThrowIfNull(asfVersion); + ArgumentException.ThrowIfNullOrEmpty(asfVariant); + ArgumentNullException.ThrowIfNull(newPluginVersion); + ArgumentNullException.ThrowIfNull(releaseAssets); + + return Task.FromResult(releaseAssets.FirstOrDefault(asset => asset.Name == $"{Name}.zip")); + } +} diff --git a/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs index fad89ad77b1b8..640b0358712b7 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs @@ -20,9 +20,8 @@ // limitations under the License. using System; -using System.Collections.Generic; using System.Threading.Tasks; -using ArchiSteamFarm.Web.GitHub.Data; +using ArchiSteamFarm.Storage; using JetBrains.Annotations; namespace ArchiSteamFarm.Plugins.Interfaces; @@ -30,33 +29,21 @@ namespace ArchiSteamFarm.Plugins.Interfaces; [PublicAPI] public interface IPluginUpdates : IPlugin { /// - /// ASF will use this property as a target for GitHub updates. GitHub repository specified here must have valid releases that will be used for updates. - /// - /// Repository name in format of {Author}/{Repository}. - /// JustArchiNET/ArchiSteamFarm - string RepositoryName { get; } - - /// - /// ASF will call this function for determining the target asset name to update to. This asset should be available in specified release. It's permitted to return null/empty if you want to cancel update to given version. + /// ASF will call this function for determining the target release asset URL to update to. /// /// Target ASF version that plugin update should be compatible with. In rare cases, this might not match currently running ASF version, in particular when updating to newer release and checking if any plugins are compatible with it. /// ASF variant of current instance, which may be useful if you're providing different versions for different ASF variants. - /// The target (new) version of the plugin found available in . - /// Available release assets for auto-update. Those come directly from your release on GitHub. - /// Target release asset from those provided that should be used for auto-update. You may return null if the update is unavailable, for example, because ASF version/variant is determined unsupported, or due to any other custom reason. - Task GetTargetReleaseAsset(Version asfVersion, string asfVariant, Version newPluginVersion, IReadOnlyCollection releaseAssets); + /// ASF update channel specified for this request. This might be different from the one specified in , as user might've specified other one for this request. + /// Target release asset URL that should be used for auto-update. It's permitted to return null/empty if you want to skip update, e.g. because no new version is available. + Task GetTargetReleaseURL(Version asfVersion, string asfVariant, GlobalConfig.EUpdateChannel updateChannel); /// /// ASF will call this method after update to a particular plugin version has been finished, just before restart of the process. /// - /// The current (old) version of plugin assembly. - /// The target (new) version of plugin assembly. - Task OnUpdateFinished(Version currentVersion, Version newVersion); + Task OnUpdateFinished(); /// /// ASF will call this method before proceeding with an update to a particular plugin version. /// - /// The current (old) version of plugin assembly. - /// The target (new) version of plugin assembly. - Task OnUpdateProceeding(Version currentVersion, Version newVersion); + Task OnUpdateProceeding(); } diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index c21068e5ef275..4e3e8ad43f4f6 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -44,8 +44,7 @@ using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Exchange; using ArchiSteamFarm.Steam.Integration.Callbacks; -using ArchiSteamFarm.Web.GitHub; -using ArchiSteamFarm.Web.GitHub.Data; +using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web.Responses; using JetBrains.Annotations; using SteamKit2; @@ -660,7 +659,13 @@ internal static async Task OnUpdateProceeding(Version newVersion) { } [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3000", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")] - internal static async Task UpdatePlugins(Version asfVersion, bool stable) { + internal static async Task UpdatePlugins(Version asfVersion, GlobalConfig.EUpdateChannel? updateChannel) { + ArgumentNullException.ThrowIfNull(asfVersion); + + if (updateChannel.HasValue && !Enum.IsDefined(updateChannel.Value)) { + throw new InvalidEnumArgumentException(nameof(updateChannel), (int) updateChannel, typeof(GlobalConfig.EUpdateChannel)); + } + if (ActivePlugins.Count == 0) { return false; } @@ -669,6 +674,8 @@ internal static async Task UpdatePlugins(Version asfVersion, bool stable) throw new InvalidOperationException(nameof(ASF.WebBrowser)); } + updateChannel ??= ASF.GlobalConfig?.UpdateChannel ?? GlobalConfig.DefaultUpdateChannel; + bool restartNeeded = false; // We update plugins one-by-one to limit memory pressure from potentially big release assets @@ -690,34 +697,9 @@ internal static async Task UpdatePlugins(Version asfVersion, bool stable) Directory.Delete(backupDirectory, true); } - string repoName = plugin.RepositoryName; - - if (string.IsNullOrEmpty(repoName)) { - ASF.ArchiLogger.LogGenericTrace(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(plugin.RepositoryName))); - - continue; - } - - ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(repoName, stable).ConfigureAwait(false); - - if (releaseResponse == null) { - continue; - } - - Version pluginVersion = plugin.Version; - Version newVersion = new(releaseResponse.Tag); - - if (pluginVersion >= newVersion) { - ASF.ArchiLogger.LogGenericInfo($"No update available for {plugin.Name} plugin: {pluginVersion} >= {newVersion}."); - - continue; - } - - ASF.ArchiLogger.LogGenericInfo($"Updating {plugin.Name} plugin from version {pluginVersion} to {newVersion}..."); - - ReleaseAsset? asset = await plugin.GetTargetReleaseAsset(asfVersion, SharedInfo.BuildInfo.Variant, newVersion, releaseResponse.Assets).ConfigureAwait(false); + Uri? releaseURL = await plugin.GetTargetReleaseURL(asfVersion, SharedInfo.BuildInfo.Variant, updateChannel.Value).ConfigureAwait(false); - if ((asset == null) || !releaseResponse.Assets.Contains(asset)) { + if (releaseURL == null) { continue; } @@ -728,7 +710,7 @@ internal static async Task UpdatePlugins(Version asfVersion, bool stable) BinaryResponse? response; try { - response = await ASF.WebBrowser.UrlGetToBinary(asset.DownloadURL, progressReporter: progressReporter).ConfigureAwait(false); + response = await ASF.WebBrowser.UrlGetToBinary(releaseURL, progressReporter: progressReporter).ConfigureAwait(false); } finally { progressReporter.ProgressChanged -= Utilities.OnProgressChanged; } @@ -746,7 +728,7 @@ internal static async Task UpdatePlugins(Version asfVersion, bool stable) await using (memoryStream.ConfigureAwait(false)) { using ZipArchive zipArchive = new(memoryStream); - await plugin.OnUpdateProceeding(pluginVersion, newVersion).ConfigureAwait(false); + await plugin.OnUpdateProceeding().ConfigureAwait(false); if (!Utilities.UpdateFromArchive(zipArchive, assemblyDirectory)) { ASF.ArchiLogger.LogGenericError(Strings.WarningFailed); @@ -759,7 +741,7 @@ internal static async Task UpdatePlugins(Version asfVersion, bool stable) ASF.ArchiLogger.LogGenericInfo($"Updating {plugin.Name} plugin has succeeded, the changes will be loaded on the next ASF launch."); - await plugin.OnUpdateFinished(pluginVersion, newVersion).ConfigureAwait(false); + await plugin.OnUpdateFinished().ConfigureAwait(false); } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); } diff --git a/ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs b/ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs index e8a2340026fb0..4b9f445a8aedb 100644 --- a/ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs +++ b/ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs @@ -35,12 +35,12 @@ public sealed class ReleaseAsset { [JsonInclude] [JsonPropertyName("browser_download_url")] [JsonRequired] - internal Uri DownloadURL { get; private init; } = null!; + public Uri DownloadURL { get; private init; } = null!; [JsonInclude] [JsonPropertyName("size")] [JsonRequired] - internal uint Size { get; private init; } + public uint Size { get; private init; } [JsonConstructor] private ReleaseAsset() { } diff --git a/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs b/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs index 78ca8c97987d8..047326123b226 100644 --- a/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs +++ b/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs @@ -34,8 +34,8 @@ namespace ArchiSteamFarm.Web.GitHub.Data; [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] -internal sealed class ReleaseResponse { - internal string? ChangelogHTML { +public sealed class ReleaseResponse { + public string? ChangelogHTML { get { if (BackingChangelogHTML != null) { return BackingChangelogHTML; @@ -58,7 +58,7 @@ internal string? ChangelogHTML { } } - internal string? ChangelogPlainText { + public string? ChangelogPlainText { get { if (BackingChangelogPlainText != null) { return BackingChangelogPlainText; @@ -104,22 +104,22 @@ private MarkdownDocument? Changelog { [JsonInclude] [JsonPropertyName("assets")] [JsonRequired] - internal ImmutableHashSet Assets { get; private init; } = ImmutableHashSet.Empty; + public ImmutableHashSet Assets { get; private init; } = ImmutableHashSet.Empty; [JsonInclude] [JsonPropertyName("prerelease")] [JsonRequired] - internal bool IsPreRelease { get; private init; } + public bool IsPreRelease { get; private init; } [JsonInclude] [JsonPropertyName("published_at")] [JsonRequired] - internal DateTime PublishedAt { get; private init; } + public DateTime PublishedAt { get; private init; } [JsonInclude] [JsonPropertyName("tag_name")] [JsonRequired] - internal string Tag { get; private init; } = ""; + public string Tag { get; private init; } = ""; private MarkdownDocument? BackingChangelog; private string? BackingChangelogHTML; diff --git a/ArchiSteamFarm/Web/GitHub/GitHub.cs b/ArchiSteamFarm/Web/GitHub/GitHub.cs index fb61b88dca7a5..9fbb5928c517b 100644 --- a/ArchiSteamFarm/Web/GitHub/GitHub.cs +++ b/ArchiSteamFarm/Web/GitHub/GitHub.cs @@ -31,11 +31,14 @@ using ArchiSteamFarm.Core; using ArchiSteamFarm.Web.GitHub.Data; using ArchiSteamFarm.Web.Responses; +using JetBrains.Annotations; namespace ArchiSteamFarm.Web.GitHub; -internal static class GitHub { - internal static async Task GetLatestRelease(string repoName, bool stable = true, CancellationToken cancellationToken = default) { +#pragma warning disable CA1724 // TODO +public static class GitHub { + [PublicAPI] + public static async Task GetLatestRelease(string repoName, bool stable = true, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(repoName); Uri request = new($"https://api.github.com/repos/{repoName}/releases{(stable ? "/latest" : "?per_page=1")}"); @@ -49,11 +52,12 @@ internal static class GitHub { return response?.FirstOrDefault(); } - internal static async Task GetRelease(string repoName, string version, CancellationToken cancellationToken = default) { + [PublicAPI] + public static async Task GetRelease(string repoName, string tag, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(repoName); - ArgumentException.ThrowIfNullOrEmpty(version); + ArgumentException.ThrowIfNullOrEmpty(tag); - Uri request = new($"https://api.github.com/repos/{repoName}/releases/tags/{version}"); + Uri request = new($"https://api.github.com/repos/{repoName}/releases/tags/{tag}"); return await GetReleaseFromURL(request, cancellationToken).ConfigureAwait(false); } @@ -173,3 +177,4 @@ internal static class GitHub { return response?.Content; } } +#pragma warning restore CA1724 // TODO From 4674db815bfbc043cf7bd3c7969a06972f72975d Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 00:52:30 +0100 Subject: [PATCH 10/32] Final touches --- ArchiSteamFarm/Core/ASF.cs | 14 +++++----- ArchiSteamFarm/Core/Utilities.cs | 10 ++++--- ArchiSteamFarm/IPC/ArchiKestrel.cs | 10 ++++--- .../IPC/Controllers/Api/GitHubController.cs | 14 +++++----- .../IPC/Responses/GitHubReleaseResponse.cs | 12 +++++---- .../Interfaces/IGitHubPluginUpdates.cs | 27 ++++++++++++------- .../Plugins/Interfaces/IPluginUpdates.cs | 16 ++++++----- ArchiSteamFarm/Plugins/PluginsCore.cs | 10 ++++--- ArchiSteamFarm/SharedInfo.cs | 10 ++++--- ArchiSteamFarm/Steam/Interaction/Actions.cs | 10 ++++--- .../{GitHub => Services}/Data/ReleaseAsset.cs | 20 +++++++------- .../Data/ReleaseResponse.cs | 15 +++++++---- .../Web/{GitHub => Services}/GitHub.cs | 16 +++++------ 13 files changed, 108 insertions(+), 76 deletions(-) rename ArchiSteamFarm/Web/{GitHub => Services}/Data/ReleaseAsset.cs (86%) rename ArchiSteamFarm/Web/{GitHub => Services}/Data/ReleaseResponse.cs (93%) rename ArchiSteamFarm/Web/{GitHub => Services}/GitHub.cs (95%) diff --git a/ArchiSteamFarm/Core/ASF.cs b/ArchiSteamFarm/Core/ASF.cs index 45b15c498c839..513b06cefe192 100644 --- a/ArchiSteamFarm/Core/ASF.cs +++ b/ArchiSteamFarm/Core/ASF.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -41,9 +43,9 @@ using ArchiSteamFarm.Steam.Integration; using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; -using ArchiSteamFarm.Web.GitHub; -using ArchiSteamFarm.Web.GitHub.Data; using ArchiSteamFarm.Web.Responses; +using ArchiSteamFarm.Web.Services; +using ArchiSteamFarm.Web.Services.Data; using JetBrains.Annotations; using SteamKit2; using SteamKit2.Discovery; diff --git a/ArchiSteamFarm/Core/Utilities.cs b/ArchiSteamFarm/Core/Utilities.cs index 20df093b84c47..5c228cfb708a5 100644 --- a/ArchiSteamFarm/Core/Utilities.cs +++ b/ArchiSteamFarm/Core/Utilities.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/ArchiSteamFarm/IPC/ArchiKestrel.cs b/ArchiSteamFarm/IPC/ArchiKestrel.cs index e8ff8cd4de0f6..1ee063355ccdc 100644 --- a/ArchiSteamFarm/IPC/ArchiKestrel.cs +++ b/ArchiSteamFarm/IPC/ArchiKestrel.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs b/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs index f9bddcd587794..d73754b50c7c6 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,8 +31,8 @@ using ArchiSteamFarm.IPC.Responses; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Web; -using ArchiSteamFarm.Web.GitHub; -using ArchiSteamFarm.Web.GitHub.Data; +using ArchiSteamFarm.Web.Services; +using ArchiSteamFarm.Web.Services.Data; using Microsoft.AspNetCore.Mvc; namespace ArchiSteamFarm.IPC.Controllers.Api; diff --git a/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs b/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs index af02965437902..f2d95c8529dec 100644 --- a/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs +++ b/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,7 +24,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -using ArchiSteamFarm.Web.GitHub.Data; +using ArchiSteamFarm.Web.Services.Data; namespace ArchiSteamFarm.IPC.Responses; diff --git a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs index a7c889de7e7e9..b56b8026b1f97 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -28,8 +30,8 @@ using ArchiSteamFarm.Core; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Storage; -using ArchiSteamFarm.Web.GitHub; -using ArchiSteamFarm.Web.GitHub.Data; +using ArchiSteamFarm.Web.Services; +using ArchiSteamFarm.Web.Services.Data; using JetBrains.Annotations; namespace ArchiSteamFarm.Plugins.Interfaces; @@ -48,8 +50,7 @@ public interface IGitHubPluginUpdates : IPluginUpdates { /// JustArchiNET/ArchiSteamFarm string RepositoryName { get; } -#pragma warning disable CA1033 // TODO - async Task IPluginUpdates.GetTargetReleaseURL(Version asfVersion, string asfVariant, GlobalConfig.EUpdateChannel updateChannel) { + Task IPluginUpdates.GetTargetReleaseURL(Version asfVersion, string asfVariant, GlobalConfig.EUpdateChannel updateChannel) { ArgumentNullException.ThrowIfNull(asfVersion); ArgumentException.ThrowIfNullOrEmpty(asfVariant); @@ -57,6 +58,13 @@ public interface IGitHubPluginUpdates : IPluginUpdates { throw new InvalidEnumArgumentException(nameof(updateChannel), (int) updateChannel, typeof(GlobalConfig.EUpdateChannel)); } + return GetTargetReleaseURL(asfVersion, asfVariant, updateChannel == GlobalConfig.EUpdateChannel.Stable); + } + + protected async Task GetTargetReleaseURL(Version asfVersion, string asfVariant, bool stable) { + ArgumentNullException.ThrowIfNull(asfVersion); + ArgumentException.ThrowIfNullOrEmpty(asfVariant); + if (!CanUpdate) { return null; } @@ -67,7 +75,7 @@ public interface IGitHubPluginUpdates : IPluginUpdates { return null; } - ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(RepositoryName, updateChannel == GlobalConfig.EUpdateChannel.Stable).ConfigureAwait(false); + ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(RepositoryName, stable).ConfigureAwait(false); if (releaseResponse == null) { return null; @@ -91,7 +99,6 @@ public interface IGitHubPluginUpdates : IPluginUpdates { return asset.DownloadURL; } -#pragma warning restore CA1033 // TODO /// /// ASF will call this function for determining the target asset name to update to. This asset should be available in specified release. It's permitted to return null/empty if you want to cancel update to given version. Default implementation provides simple resolve based on flow from JustArchiNET/ASF-PluginTemplate repository. diff --git a/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs index 640b0358712b7..e5611bb32a2e1 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -33,17 +35,17 @@ public interface IPluginUpdates : IPlugin { /// /// Target ASF version that plugin update should be compatible with. In rare cases, this might not match currently running ASF version, in particular when updating to newer release and checking if any plugins are compatible with it. /// ASF variant of current instance, which may be useful if you're providing different versions for different ASF variants. - /// ASF update channel specified for this request. This might be different from the one specified in , as user might've specified other one for this request. + /// ASF update channel specified for this request. This might be different from the one specified in , as user might've specified other one for this request. /// Target release asset URL that should be used for auto-update. It's permitted to return null/empty if you want to skip update, e.g. because no new version is available. Task GetTargetReleaseURL(Version asfVersion, string asfVariant, GlobalConfig.EUpdateChannel updateChannel); /// /// ASF will call this method after update to a particular plugin version has been finished, just before restart of the process. /// - Task OnUpdateFinished(); + Task OnUpdateFinished() => Task.CompletedTask; /// /// ASF will call this method before proceeding with an update to a particular plugin version. /// - Task OnUpdateProceeding(); + Task OnUpdateProceeding() => Task.CompletedTask; } diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index 4e3e8ad43f4f6..9bafa64135de8 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/ArchiSteamFarm/SharedInfo.cs b/ArchiSteamFarm/SharedInfo.cs index c0ed885235e6b..bbe5c9eefc45e 100644 --- a/ArchiSteamFarm/SharedInfo.cs +++ b/ArchiSteamFarm/SharedInfo.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/ArchiSteamFarm/Steam/Interaction/Actions.cs b/ArchiSteamFarm/Steam/Interaction/Actions.cs index b6b79c2da0225..796a4fd9f8171 100644 --- a/ArchiSteamFarm/Steam/Interaction/Actions.cs +++ b/ArchiSteamFarm/Steam/Interaction/Actions.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs b/ArchiSteamFarm/Web/Services/Data/ReleaseAsset.cs similarity index 86% rename from ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs rename to ArchiSteamFarm/Web/Services/Data/ReleaseAsset.cs index 4b9f445a8aedb..1d32f2c283886 100644 --- a/ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs +++ b/ArchiSteamFarm/Web/Services/Data/ReleaseAsset.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,19 +25,19 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -namespace ArchiSteamFarm.Web.GitHub.Data; +namespace ArchiSteamFarm.Web.Services.Data; [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] public sealed class ReleaseAsset { [JsonInclude] - [JsonPropertyName("name")] + [JsonPropertyName("browser_download_url")] [JsonRequired] - public string Name { get; private init; } = ""; + public Uri DownloadURL { get; private init; } = null!; [JsonInclude] - [JsonPropertyName("browser_download_url")] + [JsonPropertyName("name")] [JsonRequired] - public Uri DownloadURL { get; private init; } = null!; + public string Name { get; private init; } = ""; [JsonInclude] [JsonPropertyName("size")] diff --git a/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs b/ArchiSteamFarm/Web/Services/Data/ReleaseResponse.cs similarity index 93% rename from ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs rename to ArchiSteamFarm/Web/Services/Data/ReleaseResponse.cs index 047326123b226..60372f77ea996 100644 --- a/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs +++ b/ArchiSteamFarm/Web/Services/Data/ReleaseResponse.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -26,15 +28,17 @@ using System.Linq; using System.Text.Json.Serialization; using ArchiSteamFarm.Core; +using JetBrains.Annotations; using Markdig; using Markdig.Renderers; using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace ArchiSteamFarm.Web.GitHub.Data; +namespace ArchiSteamFarm.Web.Services.Data; [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] public sealed class ReleaseResponse { + [PublicAPI] public string? ChangelogHTML { get { if (BackingChangelogHTML != null) { @@ -58,6 +62,7 @@ public string? ChangelogHTML { } } + [PublicAPI] public string? ChangelogPlainText { get { if (BackingChangelogPlainText != null) { diff --git a/ArchiSteamFarm/Web/GitHub/GitHub.cs b/ArchiSteamFarm/Web/Services/GitHub.cs similarity index 95% rename from ArchiSteamFarm/Web/GitHub/GitHub.cs rename to ArchiSteamFarm/Web/Services/GitHub.cs index 9fbb5928c517b..73704ed416321 100644 --- a/ArchiSteamFarm/Web/GitHub/GitHub.cs +++ b/ArchiSteamFarm/Web/Services/GitHub.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,13 +31,12 @@ using System.Threading.Tasks; using AngleSharp.Dom; using ArchiSteamFarm.Core; -using ArchiSteamFarm.Web.GitHub.Data; using ArchiSteamFarm.Web.Responses; +using ArchiSteamFarm.Web.Services.Data; using JetBrains.Annotations; -namespace ArchiSteamFarm.Web.GitHub; +namespace ArchiSteamFarm.Web.Services; -#pragma warning disable CA1724 // TODO public static class GitHub { [PublicAPI] public static async Task GetLatestRelease(string repoName, bool stable = true, CancellationToken cancellationToken = default) { @@ -177,4 +178,3 @@ public static class GitHub { return response?.Content; } } -#pragma warning restore CA1724 // TODO From 55ac3afac6e273fb568f1318ef7cb9ad379a9bea Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 01:02:22 +0100 Subject: [PATCH 11/32] Misc --- ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs | 4 ++-- ArchiSteamFarm/Plugins/PluginsCore.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs index e5611bb32a2e1..fe9aaf786573c 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs @@ -42,10 +42,10 @@ public interface IPluginUpdates : IPlugin { /// /// ASF will call this method after update to a particular plugin version has been finished, just before restart of the process. /// - Task OnUpdateFinished() => Task.CompletedTask; + Task OnPluginUpdateFinished() => Task.CompletedTask; /// /// ASF will call this method before proceeding with an update to a particular plugin version. /// - Task OnUpdateProceeding() => Task.CompletedTask; + Task OnPluginUpdateProceeding() => Task.CompletedTask; } diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index 9bafa64135de8..f0e42a035714a 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -730,7 +730,7 @@ internal static async Task UpdatePlugins(Version asfVersion, GlobalConfig. await using (memoryStream.ConfigureAwait(false)) { using ZipArchive zipArchive = new(memoryStream); - await plugin.OnUpdateProceeding().ConfigureAwait(false); + await plugin.OnPluginUpdateProceeding().ConfigureAwait(false); if (!Utilities.UpdateFromArchive(zipArchive, assemblyDirectory)) { ASF.ArchiLogger.LogGenericError(Strings.WarningFailed); @@ -743,7 +743,7 @@ internal static async Task UpdatePlugins(Version asfVersion, GlobalConfig. ASF.ArchiLogger.LogGenericInfo($"Updating {plugin.Name} plugin has succeeded, the changes will be loaded on the next ASF launch."); - await plugin.OnUpdateFinished().ConfigureAwait(false); + await plugin.OnPluginUpdateFinished().ConfigureAwait(false); } catch (Exception e) { ASF.ArchiLogger.LogGenericException(e); } From cf1753b11eeee7cf25c6e9378c59fa90adce906d Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 01:23:19 +0100 Subject: [PATCH 12/32] Allow plugin creators for more flexibility in picking from GitHub releases --- ArchiSteamFarm/Web/Services/GitHub.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/ArchiSteamFarm/Web/Services/GitHub.cs b/ArchiSteamFarm/Web/Services/GitHub.cs index 73704ed416321..7844594913d00 100644 --- a/ArchiSteamFarm/Web/Services/GitHub.cs +++ b/ArchiSteamFarm/Web/Services/GitHub.cs @@ -42,13 +42,13 @@ public static class GitHub { public static async Task GetLatestRelease(string repoName, bool stable = true, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(repoName); - Uri request = new($"https://api.github.com/repos/{repoName}/releases{(stable ? "/latest" : "?per_page=1")}"); - if (stable) { + Uri request = new($"https://api.github.com/repos/{repoName}/releases/latest"); + return await GetReleaseFromURL(request, cancellationToken).ConfigureAwait(false); } - ImmutableList? response = await GetReleasesFromURL(request, cancellationToken).ConfigureAwait(false); + ImmutableList? response = await GetReleases(repoName, 1, cancellationToken).ConfigureAwait(false); return response?.FirstOrDefault(); } @@ -63,6 +63,17 @@ public static class GitHub { return await GetReleaseFromURL(request, cancellationToken).ConfigureAwait(false); } + [PublicAPI] + public static async Task?> GetReleases(string repoName, byte count = 10, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrEmpty(repoName); + ArgumentOutOfRangeException.ThrowIfZero(count); + ArgumentOutOfRangeException.ThrowIfGreaterThan(count, 100); + + Uri request = new($"https://api.github.com/repos/{repoName}/releases?per_page={count}"); + + return await GetReleasesFromURL(request, cancellationToken).ConfigureAwait(false); + } + internal static async Task?> GetWikiHistory(string page, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(page); From af19bcde77d5c58ba5478b85525596e451effbb0 Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 01:27:07 +0100 Subject: [PATCH 13/32] Misc rename --- ArchiSteamFarm/Core/ASF.cs | 6 +++--- .../IPC/Controllers/Api/GitHubController.cs | 14 +++++++------- .../IPC/Responses/GitHubReleaseResponse.cs | 2 +- .../Plugins/Interfaces/IGitHubPluginUpdates.cs | 6 +++--- .../Web/{Services => GitHub}/Data/ReleaseAsset.cs | 2 +- .../{Services => GitHub}/Data/ReleaseResponse.cs | 2 +- .../GitHub.cs => GitHub/GitHubService.cs} | 14 ++++++++------ 7 files changed, 24 insertions(+), 22 deletions(-) rename ArchiSteamFarm/Web/{Services => GitHub}/Data/ReleaseAsset.cs (97%) rename ArchiSteamFarm/Web/{Services => GitHub}/Data/ReleaseResponse.cs (99%) rename ArchiSteamFarm/Web/{Services/GitHub.cs => GitHub/GitHubService.cs} (94%) diff --git a/ArchiSteamFarm/Core/ASF.cs b/ArchiSteamFarm/Core/ASF.cs index 513b06cefe192..eed86cdf8128d 100644 --- a/ArchiSteamFarm/Core/ASF.cs +++ b/ArchiSteamFarm/Core/ASF.cs @@ -43,9 +43,9 @@ using ArchiSteamFarm.Steam.Integration; using ArchiSteamFarm.Storage; using ArchiSteamFarm.Web; +using ArchiSteamFarm.Web.GitHub; +using ArchiSteamFarm.Web.GitHub.Data; using ArchiSteamFarm.Web.Responses; -using ArchiSteamFarm.Web.Services; -using ArchiSteamFarm.Web.Services.Data; using JetBrains.Annotations; using SteamKit2; using SteamKit2.Discovery; @@ -815,7 +815,7 @@ private static async Task UpdateAndRestart() { ArchiLogger.LogGenericInfo(Strings.UpdateCheckingNewVersion); - ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(SharedInfo.GithubRepo, channel == GlobalConfig.EUpdateChannel.Stable).ConfigureAwait(false); + ReleaseResponse? releaseResponse = await GitHubService.GetLatestRelease(SharedInfo.GithubRepo, channel == GlobalConfig.EUpdateChannel.Stable).ConfigureAwait(false); if (releaseResponse == null) { ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); diff --git a/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs b/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs index d73754b50c7c6..12d46086effc2 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs @@ -31,8 +31,8 @@ using ArchiSteamFarm.IPC.Responses; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Web; -using ArchiSteamFarm.Web.Services; -using ArchiSteamFarm.Web.Services.Data; +using ArchiSteamFarm.Web.GitHub; +using ArchiSteamFarm.Web.GitHub.Data; using Microsoft.AspNetCore.Mvc; namespace ArchiSteamFarm.IPC.Controllers.Api; @@ -51,7 +51,7 @@ public sealed class GitHubController : ArchiController { public async Task> GitHubReleaseGet() { CancellationToken cancellationToken = HttpContext.RequestAborted; - ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(SharedInfo.GithubRepo, false, cancellationToken).ConfigureAwait(false); + ReleaseResponse? releaseResponse = await GitHubService.GetLatestRelease(SharedInfo.GithubRepo, false, cancellationToken).ConfigureAwait(false); return releaseResponse != null ? Ok(new GenericResponse(new GitHubReleaseResponse(releaseResponse))) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))); } @@ -75,7 +75,7 @@ public async Task> GitHubReleaseGet(string version switch (version.ToUpperInvariant()) { case "LATEST": - releaseResponse = await GitHub.GetLatestRelease(SharedInfo.GithubRepo, cancellationToken: cancellationToken).ConfigureAwait(false); + releaseResponse = await GitHubService.GetLatestRelease(SharedInfo.GithubRepo, cancellationToken: cancellationToken).ConfigureAwait(false); break; default: @@ -83,7 +83,7 @@ public async Task> GitHubReleaseGet(string version return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(version)))); } - releaseResponse = await GitHub.GetRelease(SharedInfo.GithubRepo, parsedVersion.ToString(4), cancellationToken).ConfigureAwait(false); + releaseResponse = await GitHubService.GetRelease(SharedInfo.GithubRepo, parsedVersion.ToString(4), cancellationToken).ConfigureAwait(false); break; } @@ -106,7 +106,7 @@ public async Task> GitHubWikiHistoryGet(string pag CancellationToken cancellationToken = HttpContext.RequestAborted; - Dictionary? revisions = await GitHub.GetWikiHistory(page, cancellationToken).ConfigureAwait(false); + Dictionary? revisions = await GitHubService.GetWikiHistory(page, cancellationToken).ConfigureAwait(false); return revisions != null ? revisions.Count > 0 ? Ok(new GenericResponse>(revisions.ToImmutableDictionary())) : BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(page)))) : StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))); } @@ -127,7 +127,7 @@ public async Task> GitHubWikiPageGet(string page, CancellationToken cancellationToken = HttpContext.RequestAborted; - string? html = await GitHub.GetWikiPage(page, revision, cancellationToken).ConfigureAwait(false); + string? html = await GitHubService.GetWikiPage(page, revision, cancellationToken).ConfigureAwait(false); return html switch { null => StatusCode((int) HttpStatusCode.ServiceUnavailable, new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))), diff --git a/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs b/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs index f2d95c8529dec..cab5bd7ffc57d 100644 --- a/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs +++ b/ArchiSteamFarm/IPC/Responses/GitHubReleaseResponse.cs @@ -24,7 +24,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -using ArchiSteamFarm.Web.Services.Data; +using ArchiSteamFarm.Web.GitHub.Data; namespace ArchiSteamFarm.IPC.Responses; diff --git a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs index b56b8026b1f97..d3833ced73a89 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs @@ -30,8 +30,8 @@ using ArchiSteamFarm.Core; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Storage; -using ArchiSteamFarm.Web.Services; -using ArchiSteamFarm.Web.Services.Data; +using ArchiSteamFarm.Web.GitHub; +using ArchiSteamFarm.Web.GitHub.Data; using JetBrains.Annotations; namespace ArchiSteamFarm.Plugins.Interfaces; @@ -75,7 +75,7 @@ public interface IGitHubPluginUpdates : IPluginUpdates { return null; } - ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(RepositoryName, stable).ConfigureAwait(false); + ReleaseResponse? releaseResponse = await GitHubService.GetLatestRelease(RepositoryName, stable).ConfigureAwait(false); if (releaseResponse == null) { return null; diff --git a/ArchiSteamFarm/Web/Services/Data/ReleaseAsset.cs b/ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs similarity index 97% rename from ArchiSteamFarm/Web/Services/Data/ReleaseAsset.cs rename to ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs index 1d32f2c283886..ff0137093f263 100644 --- a/ArchiSteamFarm/Web/Services/Data/ReleaseAsset.cs +++ b/ArchiSteamFarm/Web/GitHub/Data/ReleaseAsset.cs @@ -25,7 +25,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -namespace ArchiSteamFarm.Web.Services.Data; +namespace ArchiSteamFarm.Web.GitHub.Data; [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] public sealed class ReleaseAsset { diff --git a/ArchiSteamFarm/Web/Services/Data/ReleaseResponse.cs b/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs similarity index 99% rename from ArchiSteamFarm/Web/Services/Data/ReleaseResponse.cs rename to ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs index 60372f77ea996..8db6e184ce971 100644 --- a/ArchiSteamFarm/Web/Services/Data/ReleaseResponse.cs +++ b/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs @@ -34,7 +34,7 @@ using Markdig.Syntax; using Markdig.Syntax.Inlines; -namespace ArchiSteamFarm.Web.Services.Data; +namespace ArchiSteamFarm.Web.GitHub.Data; [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] public sealed class ReleaseResponse { diff --git a/ArchiSteamFarm/Web/Services/GitHub.cs b/ArchiSteamFarm/Web/GitHub/GitHubService.cs similarity index 94% rename from ArchiSteamFarm/Web/Services/GitHub.cs rename to ArchiSteamFarm/Web/GitHub/GitHubService.cs index 7844594913d00..3fab37403bb7b 100644 --- a/ArchiSteamFarm/Web/Services/GitHub.cs +++ b/ArchiSteamFarm/Web/GitHub/GitHubService.cs @@ -31,19 +31,21 @@ using System.Threading.Tasks; using AngleSharp.Dom; using ArchiSteamFarm.Core; +using ArchiSteamFarm.Web.GitHub.Data; using ArchiSteamFarm.Web.Responses; -using ArchiSteamFarm.Web.Services.Data; using JetBrains.Annotations; -namespace ArchiSteamFarm.Web.Services; +namespace ArchiSteamFarm.Web.GitHub; + +public static class GitHubService { + private static Uri URL => new("https://api.github.com"); -public static class GitHub { [PublicAPI] public static async Task GetLatestRelease(string repoName, bool stable = true, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(repoName); if (stable) { - Uri request = new($"https://api.github.com/repos/{repoName}/releases/latest"); + Uri request = new(URL, $"/repos/{repoName}/releases/latest"); return await GetReleaseFromURL(request, cancellationToken).ConfigureAwait(false); } @@ -58,7 +60,7 @@ public static class GitHub { ArgumentException.ThrowIfNullOrEmpty(repoName); ArgumentException.ThrowIfNullOrEmpty(tag); - Uri request = new($"https://api.github.com/repos/{repoName}/releases/tags/{tag}"); + Uri request = new(URL, $"/repos/{repoName}/releases/tags/{tag}"); return await GetReleaseFromURL(request, cancellationToken).ConfigureAwait(false); } @@ -69,7 +71,7 @@ public static class GitHub { ArgumentOutOfRangeException.ThrowIfZero(count); ArgumentOutOfRangeException.ThrowIfGreaterThan(count, 100); - Uri request = new($"https://api.github.com/repos/{repoName}/releases?per_page={count}"); + Uri request = new(URL, $"/repos/{repoName}/releases?per_page={count}"); return await GetReleasesFromURL(request, cancellationToken).ConfigureAwait(false); } From 4b3810eac9d1b47954ee04c40193f23dc58e29b1 Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 01:30:44 +0100 Subject: [PATCH 14/32] Make changelog internal again This is ASF implementation detail, make body available instead and let people implement changelogs themselves --- .../Web/GitHub/Data/ReleaseResponse.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs b/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs index 8db6e184ce971..3f035cde8de7d 100644 --- a/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs +++ b/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs @@ -28,7 +28,6 @@ using System.Linq; using System.Text.Json.Serialization; using ArchiSteamFarm.Core; -using JetBrains.Annotations; using Markdig; using Markdig.Renderers; using Markdig.Syntax; @@ -38,8 +37,7 @@ namespace ArchiSteamFarm.Web.GitHub.Data; [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] public sealed class ReleaseResponse { - [PublicAPI] - public string? ChangelogHTML { + internal string? ChangelogHTML { get { if (BackingChangelogHTML != null) { return BackingChangelogHTML; @@ -62,8 +60,7 @@ public string? ChangelogHTML { } } - [PublicAPI] - public string? ChangelogPlainText { + internal string? ChangelogPlainText { get { if (BackingChangelogPlainText != null) { return BackingChangelogPlainText; @@ -116,6 +113,11 @@ private MarkdownDocument? Changelog { [JsonRequired] public bool IsPreRelease { get; private init; } + [JsonInclude] + [JsonPropertyName("body")] + [JsonRequired] + public string? MarkdownBody { get; private init; } = ""; + [JsonInclude] [JsonPropertyName("published_at")] [JsonRequired] @@ -130,11 +132,6 @@ private MarkdownDocument? Changelog { private string? BackingChangelogHTML; private string? BackingChangelogPlainText; - [JsonInclude] - [JsonPropertyName("body")] - [JsonRequired] - private string? MarkdownBody { get; init; } = ""; - [JsonConstructor] private ReleaseResponse() { } From 44b4f1a771fc9d83ddd05af448ec4245320b0f23 Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 01:31:15 +0100 Subject: [PATCH 15/32] Misc --- ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs b/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs index 3f035cde8de7d..879f808c2b02a 100644 --- a/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs +++ b/ArchiSteamFarm/Web/GitHub/Data/ReleaseResponse.cs @@ -116,7 +116,7 @@ private MarkdownDocument? Changelog { [JsonInclude] [JsonPropertyName("body")] [JsonRequired] - public string? MarkdownBody { get; private init; } = ""; + public string MarkdownBody { get; private init; } = ""; [JsonInclude] [JsonPropertyName("published_at")] From 1cb0a2d48c896f3b5d976271282295f803991db9 Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 02:00:43 +0100 Subject: [PATCH 16/32] Add missing localization --- .../Localization/Strings.Designer.cs | 42 +++++++++++++++++++ ArchiSteamFarm/Localization/Strings.resx | 27 ++++++++++++ .../Interfaces/IGitHubPluginUpdates.cs | 8 ++-- ArchiSteamFarm/Plugins/PluginsCore.cs | 8 +++- 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/ArchiSteamFarm/Localization/Strings.Designer.cs b/ArchiSteamFarm/Localization/Strings.Designer.cs index f4fbce26c12e4..8d6103f6bc482 100644 --- a/ArchiSteamFarm/Localization/Strings.Designer.cs +++ b/ArchiSteamFarm/Localization/Strings.Designer.cs @@ -1244,5 +1244,47 @@ public static string WarningSkipping { return ResourceManager.GetString("WarningSkipping", resourceCulture); } } + + public static string PluginUpdatesChecking { + get { + return ResourceManager.GetString("PluginUpdatesChecking", resourceCulture); + } + } + + public static string PluginUpdateChecking { + get { + return ResourceManager.GetString("PluginUpdateChecking", resourceCulture); + } + } + + public static string PluginUpdateNotFound { + get { + return ResourceManager.GetString("PluginUpdateNotFound", resourceCulture); + } + } + + public static string PluginUpdateFound { + get { + return ResourceManager.GetString("PluginUpdateFound", resourceCulture); + } + } + + public static string PluginUpdateNoAssetFound { + get { + return ResourceManager.GetString("PluginUpdateNoAssetFound", resourceCulture); + } + } + + public static string PluginUpdateInProgress { + get { + return ResourceManager.GetString("PluginUpdateInProgress", resourceCulture); + } + } + + public static string PluginUpdateFinished { + get { + return ResourceManager.GetString("PluginUpdateFinished", resourceCulture); + } + } } } diff --git a/ArchiSteamFarm/Localization/Strings.resx b/ArchiSteamFarm/Localization/Strings.resx index be52d3d00c844..699a3614a4955 100644 --- a/ArchiSteamFarm/Localization/Strings.resx +++ b/ArchiSteamFarm/Localization/Strings.resx @@ -768,4 +768,31 @@ Process uptime: {1} Skipping: {0}... {0} will be replaced by text value (string) of entry being skipped. + + Checking for plugin updates... + + + Checking update for {0} plugin... + {0} will be replaced by plugin name (string). + + + No update available for {0} plugin: {1} ≥ {2}. + {0} will be replaced by plugin name (string), {1} will be replaced by current plugin's version, {2} will be replaced by remote plugin's version. + + + Found {0} plugin update from version {1} to {2}... + {0} will be replaced by plugin name (string), {1} will be replaced by current plugin's version, {2} will be replaced by remote plugin's version. + + + No asset available for {0} plugin update from version {1} to {2}, this usually means the update will be available at later time. + {0} will be replaced by plugin name (string), {1} will be replaced by current plugin's version, {2} will be replaced by remote plugin's version. + + + Updating {0} plugin... + {0} will be replaced by plugin name (string). + + + Updating {0} plugin has succeeded, the changes will be loaded on the next ASF launch. + {0} will be replaced by plugin name (string). + diff --git a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs index d3833ced73a89..e610c249d2793 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs @@ -84,19 +84,21 @@ public interface IGitHubPluginUpdates : IPluginUpdates { Version newVersion = new(releaseResponse.Tag); if (Version >= newVersion) { - ASF.ArchiLogger.LogGenericInfo($"No update available for {Name} plugin: {Version} >= {newVersion}."); + ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateNotFound, Name, Version, newVersion)); return null; } - ASF.ArchiLogger.LogGenericInfo($"Updating {Name} plugin from version {Version} to {newVersion}..."); - ReleaseAsset? asset = await GetTargetReleaseAsset(asfVersion, asfVariant, newVersion, releaseResponse.Assets).ConfigureAwait(false); if ((asset == null) || !releaseResponse.Assets.Contains(asset)) { + ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateNoAssetFound, Name, Version, newVersion)); + return null; } + ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateFound, Name, Version, newVersion)); + return asset.DownloadURL; } diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index f0e42a035714a..4efe36f9b204d 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -680,10 +680,12 @@ internal static async Task UpdatePlugins(Version asfVersion, GlobalConfig. bool restartNeeded = false; + ASF.ArchiLogger.LogGenericInfo(Strings.PluginUpdatesChecking); + // We update plugins one-by-one to limit memory pressure from potentially big release assets foreach (IPluginUpdates plugin in ActivePlugins.OfType()) { try { - ASF.ArchiLogger.LogGenericInfo($"Checking update for {plugin.Name} plugin..."); + ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateChecking, plugin.Name)); string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location); @@ -705,6 +707,8 @@ internal static async Task UpdatePlugins(Version asfVersion, GlobalConfig. continue; } + ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateInProgress, plugin.Name)); + Progress progressReporter = new(); progressReporter.ProgressChanged += Utilities.OnProgressChanged; @@ -741,7 +745,7 @@ internal static async Task UpdatePlugins(Version asfVersion, GlobalConfig. restartNeeded = true; - ASF.ArchiLogger.LogGenericInfo($"Updating {plugin.Name} plugin has succeeded, the changes will be loaded on the next ASF launch."); + ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateFinished, plugin.Name)); await plugin.OnPluginUpdateFinished().ConfigureAwait(false); } catch (Exception e) { From bba7b535db0801826ddbc235ce5813c3bd50ef00 Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 02:52:05 +0100 Subject: [PATCH 17/32] Add a way to disable plugin updates --- .../Localization/Strings.Designer.cs | 18 +++++++ ArchiSteamFarm/Localization/Strings.resx | 11 +++++ .../Interfaces/IGitHubPluginUpdates.cs | 42 ++++++++++------- .../Plugins/Interfaces/IPluginUpdates.cs | 11 +++-- ArchiSteamFarm/Plugins/PluginsCore.cs | 47 +++++++++++++++++-- ArchiSteamFarm/Storage/GlobalConfig.cs | 39 +++++++++++++-- 6 files changed, 140 insertions(+), 28 deletions(-) diff --git a/ArchiSteamFarm/Localization/Strings.Designer.cs b/ArchiSteamFarm/Localization/Strings.Designer.cs index 8d6103f6bc482..e5ab2610b9ee9 100644 --- a/ArchiSteamFarm/Localization/Strings.Designer.cs +++ b/ArchiSteamFarm/Localization/Strings.Designer.cs @@ -1286,5 +1286,23 @@ public static string PluginUpdateFinished { return ResourceManager.GetString("PluginUpdateFinished", resourceCulture); } } + + public static string PluginUpdateEnabled { + get { + return ResourceManager.GetString("PluginUpdateEnabled", resourceCulture); + } + } + + public static string PluginUpdateDisabled { + get { + return ResourceManager.GetString("PluginUpdateDisabled", resourceCulture); + } + } + + public static string CustomPluginUpdatesEnabled { + get { + return ResourceManager.GetString("CustomPluginUpdatesEnabled", resourceCulture); + } + } } } diff --git a/ArchiSteamFarm/Localization/Strings.resx b/ArchiSteamFarm/Localization/Strings.resx index 699a3614a4955..5e476ddf78afc 100644 --- a/ArchiSteamFarm/Localization/Strings.resx +++ b/ArchiSteamFarm/Localization/Strings.resx @@ -795,4 +795,15 @@ Process uptime: {1} Updating {0} plugin has succeeded, the changes will be loaded on the next ASF launch. {0} will be replaced by plugin name (string). + + {0}/{1} plugin has been registered and enabled for automatic updates. + {0} will be replaced by plugin name (string), {1} will be replaced by plugin assembly name (string). + + + {0}/{1} plugin has been disabled from automatic updates, despite supporting such feature. + {0} will be replaced by plugin name (string), {1} will be replaced by plugin assembly name (string). + + + Custom plugins have been registered for automatic updates. ASF team would like to remind you that, for your own safety, you should enable automatic updates only from trusted parties. If you didn't intend to do this, you can disable plugin updates in ASF global config. + diff --git a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs index e610c249d2793..13aaf98e2deb2 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs @@ -36,10 +36,18 @@ namespace ArchiSteamFarm.Plugins.Interfaces; +/// +/// +/// Implementing this interface allows your plugin to update from published releases on GitHub. +/// At the minimum you must provide . +/// If you're not following our ASF-PluginTemplate flow, that is, providing release asset named differently than "{PluginName}.zip" then you may also need to override function in order to select target asset based on custom rules. +/// If you have even more complex needs for updating your plugin, you should probably consider implementing base interface instead, where you can provide your own implementation, with optional help from our . +/// [PublicAPI] public interface IGitHubPluginUpdates : IPluginUpdates { /// /// Boolean value that determines whether your plugin is able to update at the time of calling. You may provide false if, for example, you're inside a critical section and you don't want to update at this time, despite supporting updates otherwise. + /// This effectively skips unnecessary request to GitHub if you're certain that you're not interested in any updates right now. /// bool CanUpdate => true; @@ -61,6 +69,23 @@ public interface IGitHubPluginUpdates : IPluginUpdates { return GetTargetReleaseURL(asfVersion, asfVariant, updateChannel == GlobalConfig.EUpdateChannel.Stable); } + /// + /// ASF will call this function for determining the target asset name to update to. This asset should be available in specified release. It's permitted to return null if you want to cancel update to given version. Default implementation provides simple resolve based on flow from JustArchiNET/ASF-PluginTemplate repository - you have a single {PluginName}.zip file to update to, with no additional conditions. + /// + /// Target ASF version that plugin update should be compatible with. In rare cases, this might not match currently running ASF version, in particular when updating to newer release and checking if any plugins are compatible with it. + /// ASF variant of current instance, which may be useful if you're providing different versions for different ASF variants. + /// The target (new) version of the plugin found available in . + /// Available release assets for auto-update. Those come directly from your release on GitHub. + /// Target release asset from those provided that should be used for auto-update. You may return null if the update is unavailable, for example, because ASF version/variant is determined unsupported, or due to any other reason. + Task GetTargetReleaseAsset(Version asfVersion, string asfVariant, Version newPluginVersion, IReadOnlyCollection releaseAssets) { + ArgumentNullException.ThrowIfNull(asfVersion); + ArgumentException.ThrowIfNullOrEmpty(asfVariant); + ArgumentNullException.ThrowIfNull(newPluginVersion); + ArgumentNullException.ThrowIfNull(releaseAssets); + + return Task.FromResult(releaseAssets.FirstOrDefault(asset => asset.Name == $"{Name}.zip")); + } + protected async Task GetTargetReleaseURL(Version asfVersion, string asfVariant, bool stable) { ArgumentNullException.ThrowIfNull(asfVersion); ArgumentException.ThrowIfNullOrEmpty(asfVariant); @@ -101,21 +126,4 @@ public interface IGitHubPluginUpdates : IPluginUpdates { return asset.DownloadURL; } - - /// - /// ASF will call this function for determining the target asset name to update to. This asset should be available in specified release. It's permitted to return null/empty if you want to cancel update to given version. Default implementation provides simple resolve based on flow from JustArchiNET/ASF-PluginTemplate repository. - /// - /// Target ASF version that plugin update should be compatible with. In rare cases, this might not match currently running ASF version, in particular when updating to newer release and checking if any plugins are compatible with it. - /// ASF variant of current instance, which may be useful if you're providing different versions for different ASF variants. - /// The target (new) version of the plugin found available in . - /// Available release assets for auto-update. Those come directly from your release on GitHub. - /// Target release asset from those provided that should be used for auto-update. You may return null if the update is unavailable, for example, because ASF version/variant is determined unsupported, or due to any other custom reason. - Task GetTargetReleaseAsset(Version asfVersion, string asfVariant, Version newPluginVersion, IReadOnlyCollection releaseAssets) { - ArgumentNullException.ThrowIfNull(asfVersion); - ArgumentException.ThrowIfNullOrEmpty(asfVariant); - ArgumentNullException.ThrowIfNull(newPluginVersion); - ArgumentNullException.ThrowIfNull(releaseAssets); - - return Task.FromResult(releaseAssets.FirstOrDefault(asset => asset.Name == $"{Name}.zip")); - } } diff --git a/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs index fe9aaf786573c..65abbc1650be7 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IPluginUpdates.cs @@ -28,6 +28,11 @@ namespace ArchiSteamFarm.Plugins.Interfaces; +/// +/// Implementing this interface allows you to provide custom logic for updating your plugin to newer version. +/// Plugin updates are happening on usual basis per configuration of auto-updates from ASF, as well as other triggers such as update command. +/// If you're using GitHub platform with plugin releases, you might be interested in instead. +/// [PublicAPI] public interface IPluginUpdates : IPlugin { /// @@ -36,16 +41,16 @@ public interface IPluginUpdates : IPlugin { /// Target ASF version that plugin update should be compatible with. In rare cases, this might not match currently running ASF version, in particular when updating to newer release and checking if any plugins are compatible with it. /// ASF variant of current instance, which may be useful if you're providing different versions for different ASF variants. /// ASF update channel specified for this request. This might be different from the one specified in , as user might've specified other one for this request. - /// Target release asset URL that should be used for auto-update. It's permitted to return null/empty if you want to skip update, e.g. because no new version is available. + /// Target release asset URL that should be used for auto-update. It's permitted to return null if you want to skip update, e.g. because no new version is available. Task GetTargetReleaseURL(Version asfVersion, string asfVariant, GlobalConfig.EUpdateChannel updateChannel); /// - /// ASF will call this method after update to a particular plugin version has been finished, just before restart of the process. + /// ASF will call this method after update to the new plugin version has been finished, just before restart of the process. /// Task OnPluginUpdateFinished() => Task.CompletedTask; /// - /// ASF will call this method before proceeding with an update to a particular plugin version. + /// ASF will call this method before proceeding with an update to the new plugin version. /// Task OnPluginUpdateProceeding() => Task.CompletedTask; } diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index 4efe36f9b204d..b843b7504521e 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -24,6 +24,7 @@ using System; using System.Collections.Frozen; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Composition; using System.Composition.Convention; @@ -59,6 +60,8 @@ public static class PluginsCore { [ImportMany] internal static FrozenSet ActivePlugins { get; private set; } = FrozenSet.Empty; + private static FrozenSet ActivePluginUpdates = FrozenSet.Empty; + [PublicAPI] public static async Task GetCrossProcessSemaphore(string objectName) { ArgumentException.ThrowIfNullOrEmpty(objectName); @@ -243,10 +246,10 @@ internal static async Task InitPlugins() { await Task.Delay(SharedInfo.InformationDelay).ConfigureAwait(false); activePlugins.ExceptWith(invalidPlugins); + } - if (activePlugins.Count == 0) { - return true; - } + if (activePlugins.Count == 0) { + return true; } ActivePlugins = activePlugins.ToFrozenSet(); @@ -258,6 +261,42 @@ internal static async Task InitPlugins() { Console.Title = SharedInfo.ProgramIdentifier; } + GlobalConfig.EPluginsUpdateMode pluginsUpdateMode = ASF.GlobalConfig?.PluginsUpdateMode ?? GlobalConfig.DefaultPluginsUpdateMode; + ImmutableHashSet pluginsUpdateList = ASF.GlobalConfig?.PluginsUpdateList ?? GlobalConfig.DefaultPluginsUpdateList; + + HashSet activePluginUpdates = new(); + + foreach (IPluginUpdates plugin in activePlugins.OfType()) { + string? pluginAssemblyName = plugin.GetType().Assembly.GetName().Name; + + if (string.IsNullOrEmpty(pluginAssemblyName)) { + ASF.ArchiLogger.LogNullError(nameof(pluginAssemblyName)); + + continue; + } + + switch (pluginsUpdateMode) { + case GlobalConfig.EPluginsUpdateMode.Blacklist when !pluginsUpdateList.Contains(pluginAssemblyName): + case GlobalConfig.EPluginsUpdateMode.Whitelist when pluginsUpdateList.Contains(pluginAssemblyName): + activePluginUpdates.Add(plugin); + + ASF.ArchiLogger.LogGenericInfo(string.Format(Strings.InteractiveConsoleEnabled, plugin.Name, pluginAssemblyName)); + + break; + case GlobalConfig.EPluginsUpdateMode.Blacklist when pluginsUpdateList.Contains(pluginAssemblyName): + case GlobalConfig.EPluginsUpdateMode.Whitelist when !pluginsUpdateList.Contains(pluginAssemblyName): + ASF.ArchiLogger.LogGenericInfo(string.Format(Strings.PluginUpdateDisabled, plugin.Name, pluginAssemblyName)); + + break; + } + } + + if (activePluginUpdates.Count > 0) { + ASF.ArchiLogger.LogGenericWarning(Strings.CustomPluginUpdatesEnabled); + + ActivePluginUpdates = activePluginUpdates.ToFrozenSet(); + } + return true; } @@ -683,7 +722,7 @@ internal static async Task UpdatePlugins(Version asfVersion, GlobalConfig. ASF.ArchiLogger.LogGenericInfo(Strings.PluginUpdatesChecking); // We update plugins one-by-one to limit memory pressure from potentially big release assets - foreach (IPluginUpdates plugin in ActivePlugins.OfType()) { + foreach (IPluginUpdates plugin in ActivePluginUpdates) { try { ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateChecking, plugin.Name)); diff --git a/ArchiSteamFarm/Storage/GlobalConfig.cs b/ArchiSteamFarm/Storage/GlobalConfig.cs index e898a8faeb37c..ff7698ebf31ad 100644 --- a/ArchiSteamFarm/Storage/GlobalConfig.cs +++ b/ArchiSteamFarm/Storage/GlobalConfig.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -107,6 +109,9 @@ public sealed class GlobalConfig { [PublicAPI] public const EOptimizationMode DefaultOptimizationMode = EOptimizationMode.MaxPerformance; + [PublicAPI] + public const EPluginsUpdateMode DefaultPluginsUpdateMode = EPluginsUpdateMode.Blacklist; + [PublicAPI] public const string? DefaultSteamMessagePrefix = "/me "; @@ -140,6 +145,9 @@ public sealed class GlobalConfig { [PublicAPI] public static readonly Guid? DefaultLicenseID; + [PublicAPI] + public static readonly ImmutableHashSet DefaultPluginsUpdateList = ImmutableHashSet.Empty; + private static readonly FrozenSet ForbiddenIPCPasswordPhrases = new HashSet(5, StringComparer.InvariantCultureIgnoreCase) { "ipc", "api", "gui", "asf-ui", "asf-gui" }.ToFrozenSet(StringComparer.InvariantCultureIgnoreCase); [JsonIgnore] @@ -277,6 +285,13 @@ internal set { [JsonInclude] public EOptimizationMode OptimizationMode { get; private init; } = DefaultOptimizationMode; + [JsonDisallowNull] + [JsonInclude] + public ImmutableHashSet PluginsUpdateList { get; private init; } = DefaultPluginsUpdateList; + + [JsonInclude] + public EPluginsUpdateMode PluginsUpdateMode { get; private init; } = DefaultPluginsUpdateMode; + [JsonInclude] [MaxLength(SteamChatMessage.MaxMessagePrefixBytes / SteamChatMessage.ReservedEscapeMessageBytes)] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "This is optional, supportive attribute, we don't care if it gets trimmed or not")] @@ -417,6 +432,12 @@ internal GlobalConfig() { } [UsedImplicitly] public bool ShouldSerializeOptimizationMode() => !Saving || (OptimizationMode != DefaultOptimizationMode); + [UsedImplicitly] + public bool ShouldSerializePluginsUpdateList() => !Saving || (PluginsUpdateList != DefaultPluginsUpdateList); + + [UsedImplicitly] + public bool ShouldSerializePluginsUpdateMode() => !Saving || (PluginsUpdateMode != DefaultPluginsUpdateMode); + [UsedImplicitly] public bool ShouldSerializeSSteamOwnerID() => !Saving; @@ -472,6 +493,10 @@ internal GlobalConfig() { } return (false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorConfigPropertyInvalid, nameof(OptimizationMode), OptimizationMode)); } + if (!Enum.IsDefined(PluginsUpdateMode)) { + return (false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorConfigPropertyInvalid, nameof(PluginsUpdateMode), PluginsUpdateMode)); + } + if (!string.IsNullOrEmpty(SteamMessagePrefix) && !SteamChatMessage.IsValidPrefix(SteamMessagePrefix)) { return (false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorConfigPropertyInvalid, nameof(SteamMessagePrefix), SteamMessagePrefix)); } @@ -577,6 +602,12 @@ public enum EOptimizationMode : byte { MinMemoryUsage } + [PublicAPI] + public enum EPluginsUpdateMode : byte { + Blacklist, + Whitelist + } + [PublicAPI] public enum EUpdateChannel : byte { None, From e728ca85060f1efeb6887de5873fa81d531260a3 Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 02:55:40 +0100 Subject: [PATCH 18/32] Update PluginsCore.cs --- ArchiSteamFarm/Plugins/PluginsCore.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index b843b7504521e..1c0c0400cb948 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -280,12 +280,12 @@ internal static async Task InitPlugins() { case GlobalConfig.EPluginsUpdateMode.Whitelist when pluginsUpdateList.Contains(pluginAssemblyName): activePluginUpdates.Add(plugin); - ASF.ArchiLogger.LogGenericInfo(string.Format(Strings.InteractiveConsoleEnabled, plugin.Name, pluginAssemblyName)); + ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateEnabled, plugin.Name, pluginAssemblyName)); break; case GlobalConfig.EPluginsUpdateMode.Blacklist when pluginsUpdateList.Contains(pluginAssemblyName): case GlobalConfig.EPluginsUpdateMode.Whitelist when !pluginsUpdateList.Contains(pluginAssemblyName): - ASF.ArchiLogger.LogGenericInfo(string.Format(Strings.PluginUpdateDisabled, plugin.Name, pluginAssemblyName)); + ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateDisabled, plugin.Name, pluginAssemblyName)); break; } From 2a3b41aa95ce56d3209ba9d36f73a29ce9cf6a5c Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 03:18:41 +0100 Subject: [PATCH 19/32] Update PluginsCore.cs --- ArchiSteamFarm/Plugins/PluginsCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index 1c0c0400cb948..6066f0db51610 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -264,7 +264,7 @@ internal static async Task InitPlugins() { GlobalConfig.EPluginsUpdateMode pluginsUpdateMode = ASF.GlobalConfig?.PluginsUpdateMode ?? GlobalConfig.DefaultPluginsUpdateMode; ImmutableHashSet pluginsUpdateList = ASF.GlobalConfig?.PluginsUpdateList ?? GlobalConfig.DefaultPluginsUpdateList; - HashSet activePluginUpdates = new(); + HashSet activePluginUpdates = []; foreach (IPluginUpdates plugin in activePlugins.OfType()) { string? pluginAssemblyName = plugin.GetType().Assembly.GetName().Name; From f0f4be43b44c06b476795382917cb241c6aa59a2 Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 03:59:57 +0100 Subject: [PATCH 20/32] Misc --- ArchiSteamFarm/Storage/GlobalConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ArchiSteamFarm/Storage/GlobalConfig.cs b/ArchiSteamFarm/Storage/GlobalConfig.cs index ff7698ebf31ad..3fe3344484b0f 100644 --- a/ArchiSteamFarm/Storage/GlobalConfig.cs +++ b/ArchiSteamFarm/Storage/GlobalConfig.cs @@ -433,7 +433,7 @@ internal GlobalConfig() { } public bool ShouldSerializeOptimizationMode() => !Saving || (OptimizationMode != DefaultOptimizationMode); [UsedImplicitly] - public bool ShouldSerializePluginsUpdateList() => !Saving || (PluginsUpdateList != DefaultPluginsUpdateList); + public bool ShouldSerializePluginsUpdateList() => !Saving || ((PluginsUpdateList != DefaultPluginsUpdateList) && !PluginsUpdateList.SetEquals(DefaultPluginsUpdateList)); [UsedImplicitly] public bool ShouldSerializePluginsUpdateMode() => !Saving || (PluginsUpdateMode != DefaultPluginsUpdateMode); From 4743dc0998a719a9ee9ed60028835ec84b52c42e Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 12:31:34 +0100 Subject: [PATCH 21/32] Update IGitHubPluginUpdates.cs --- .../Interfaces/IGitHubPluginUpdates.cs | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs index 13aaf98e2deb2..2bc2c3da45ac8 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs @@ -70,20 +70,58 @@ public interface IGitHubPluginUpdates : IPluginUpdates { } /// - /// ASF will call this function for determining the target asset name to update to. This asset should be available in specified release. It's permitted to return null if you want to cancel update to given version. Default implementation provides simple resolve based on flow from JustArchiNET/ASF-PluginTemplate repository - you have a single {PluginName}.zip file to update to, with no additional conditions. + /// ASF will call this function for determining the target asset name to update to. This asset should be available in specified release. It's permitted to return null if you want to cancel update to given version. Default implementation provides vastly universal generic matching, see remarks for more info. /// /// Target ASF version that plugin update should be compatible with. In rare cases, this might not match currently running ASF version, in particular when updating to newer release and checking if any plugins are compatible with it. /// ASF variant of current instance, which may be useful if you're providing different versions for different ASF variants. /// The target (new) version of the plugin found available in . /// Available release assets for auto-update. Those come directly from your release on GitHub. + /// + /// Default implementation will select release asset in following order: + /// - {PluginName}-V{Major}-{Minor}-{Build}-{Revision}.zip + /// - {PluginName}-V{Major}-{Minor}-{Build}.zip + /// - {PluginName}-V{Major}-{Minor}.zip + /// - {PluginName}-V{Major}.zip + /// - {PluginName}.zip + /// - *.zip, if exactly 1 release asset connected with the release + /// Where: + /// - {PluginName} is + /// - {Major} is target major ASF version (A from A.B.C.D) + /// - {Minor} is target major ASF version (B from A.B.C.D) + /// - {Build} is target major ASF version (C from A.B.C.D) + /// - {Revision} is target major ASF version (D from A.B.C.D) + /// - * is a wildcard matching any string value + /// /// Target release asset from those provided that should be used for auto-update. You may return null if the update is unavailable, for example, because ASF version/variant is determined unsupported, or due to any other reason. Task GetTargetReleaseAsset(Version asfVersion, string asfVariant, Version newPluginVersion, IReadOnlyCollection releaseAssets) { ArgumentNullException.ThrowIfNull(asfVersion); ArgumentException.ThrowIfNullOrEmpty(asfVariant); ArgumentNullException.ThrowIfNull(newPluginVersion); - ArgumentNullException.ThrowIfNull(releaseAssets); - return Task.FromResult(releaseAssets.FirstOrDefault(asset => asset.Name == $"{Name}.zip")); + if ((releaseAssets == null) || (releaseAssets.Count == 0)) { + throw new ArgumentNullException(nameof(releaseAssets)); + } + + Dictionary assetsByName = releaseAssets.ToDictionary(static asset => asset.Name); + + List matches = [ + $"{Name}-V{asfVersion.Major}-{asfVersion.Minor}-{asfVersion.Build}-{asfVersion.Revision}.zip", + $"{Name}-V{asfVersion.Major}-{asfVersion.Minor}-{asfVersion.Build}.zip", + $"{Name}-V{asfVersion.Major}-{asfVersion.Minor}.zip", + $"{Name}-V{asfVersion.Major}.zip", + $"{Name}.zip" + ]; + + foreach (string match in matches) { + if (assetsByName.TryGetValue(match, out ReleaseAsset? targetAsset)) { + return Task.FromResult(targetAsset); + } + } + + // The very last fallback in case user uses different naming scheme + HashSet zipAssets = releaseAssets.Where(static asset => asset.Name.EndsWith(".zip", StringComparison.Ordinal)).ToHashSet(); + + return Task.FromResult(zipAssets.Count == 1 ? zipAssets.First() : null); } protected async Task GetTargetReleaseURL(Version asfVersion, string asfVariant, bool stable) { @@ -114,6 +152,12 @@ public interface IGitHubPluginUpdates : IPluginUpdates { return null; } + if (releaseResponse.Assets.Count == 0) { + ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateNoAssetFound, Name, Version, newVersion)); + + return null; + } + ReleaseAsset? asset = await GetTargetReleaseAsset(asfVersion, asfVariant, newVersion, releaseResponse.Assets).ConfigureAwait(false); if ((asset == null) || !releaseResponse.Assets.Contains(asset)) { From dbd00fe41e377a63976cabf994f0878dec0a0343 Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 12:32:18 +0100 Subject: [PATCH 22/32] Update IGitHubPluginUpdates.cs --- ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs index 2bc2c3da45ac8..e0084e6b6fabe 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs @@ -87,9 +87,9 @@ public interface IGitHubPluginUpdates : IPluginUpdates { /// Where: /// - {PluginName} is /// - {Major} is target major ASF version (A from A.B.C.D) - /// - {Minor} is target major ASF version (B from A.B.C.D) - /// - {Build} is target major ASF version (C from A.B.C.D) - /// - {Revision} is target major ASF version (D from A.B.C.D) + /// - {Minor} is target minor ASF version (B from A.B.C.D) + /// - {Build} is target build (patch) ASF version (C from A.B.C.D) + /// - {Revision} is target revision ASF version (D from A.B.C.D) /// - * is a wildcard matching any string value /// /// Target release asset from those provided that should be used for auto-update. You may return null if the update is unavailable, for example, because ASF version/variant is determined unsupported, or due to any other reason. From 6e4695feb81d68bb53f8524f92e7cb8654913476 Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 12:33:27 +0100 Subject: [PATCH 23/32] Update IGitHubPluginUpdates.cs --- ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs index e0084e6b6fabe..11442be847bcc 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs @@ -83,7 +83,7 @@ public interface IGitHubPluginUpdates : IPluginUpdates { /// - {PluginName}-V{Major}-{Minor}.zip /// - {PluginName}-V{Major}.zip /// - {PluginName}.zip - /// - *.zip, if exactly 1 release asset connected with the release + /// - *.zip, if exactly 1 release asset matching in the release /// Where: /// - {PluginName} is /// - {Major} is target major ASF version (A from A.B.C.D) From e1d4f7bfc5b774630981452c017c421b29225319 Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 12:36:48 +0100 Subject: [PATCH 24/32] Update IGitHubPluginUpdates.cs --- ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs index 11442be847bcc..2412ffadb9408 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs @@ -91,6 +91,13 @@ public interface IGitHubPluginUpdates : IPluginUpdates { /// - {Build} is target build (patch) ASF version (C from A.B.C.D) /// - {Revision} is target revision ASF version (D from A.B.C.D) /// - * is a wildcard matching any string value + /// For example, when updating MyAwesomePlugin with ASF version V6.0.1.3, it will select first zip file from available ones in the following order: + /// - MyAwesomePlugin-V6.0.1.3.zip + /// - MyAwesomePlugin-V6.0.1.zip + /// - MyAwesomePlugin-V6.0.zip + /// - MyAwesomePlugin-V6.zip + /// - MyAwesomePlugin.zip + /// - *.zip /// /// Target release asset from those provided that should be used for auto-update. You may return null if the update is unavailable, for example, because ASF version/variant is determined unsupported, or due to any other reason. Task GetTargetReleaseAsset(Version asfVersion, string asfVariant, Version newPluginVersion, IReadOnlyCollection releaseAssets) { From 8976d2514f6af1414c001333254a2eedd47d032c Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 12:38:48 +0100 Subject: [PATCH 25/32] Make zip selection ignore case --- ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs index 2412ffadb9408..f4b023bfc9bb6 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs @@ -109,7 +109,7 @@ public interface IGitHubPluginUpdates : IPluginUpdates { throw new ArgumentNullException(nameof(releaseAssets)); } - Dictionary assetsByName = releaseAssets.ToDictionary(static asset => asset.Name); + Dictionary assetsByName = releaseAssets.ToDictionary(static asset => asset.Name, StringComparer.OrdinalIgnoreCase); List matches = [ $"{Name}-V{asfVersion.Major}-{asfVersion.Minor}-{asfVersion.Build}-{asfVersion.Revision}.zip", @@ -126,7 +126,7 @@ public interface IGitHubPluginUpdates : IPluginUpdates { } // The very last fallback in case user uses different naming scheme - HashSet zipAssets = releaseAssets.Where(static asset => asset.Name.EndsWith(".zip", StringComparison.Ordinal)).ToHashSet(); + HashSet zipAssets = releaseAssets.Where(static asset => asset.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)).ToHashSet(); return Task.FromResult(zipAssets.Count == 1 ? zipAssets.First() : null); } From 043858a805e605a7072fa855ab026460de850936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Domeradzki?= Date: Tue, 12 Mar 2024 12:54:05 +0100 Subject: [PATCH 26/32] Update ArchiSteamFarm/Core/Utilities.cs Co-authored-by: Vita Chumakova --- ArchiSteamFarm/Core/Utilities.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ArchiSteamFarm/Core/Utilities.cs b/ArchiSteamFarm/Core/Utilities.cs index 5c228cfb708a5..65253ef35bb43 100644 --- a/ArchiSteamFarm/Core/Utilities.cs +++ b/ArchiSteamFarm/Core/Utilities.cs @@ -517,6 +517,8 @@ private static bool RelativeDirectoryStartsWith(string directory, params string[ throw new ArgumentNullException(nameof(prefixes)); } - return (from prefix in prefixes where directory.Length > prefix.Length let pathSeparator = directory[prefix.Length] where (pathSeparator == Path.DirectorySeparatorChar) || (pathSeparator == Path.AltDirectorySeparatorChar) select prefix).Any(prefix => directory.StartsWith(prefix, StringComparison.Ordinal)); + HashSet separators = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]; + + return prefixes.Where(prefix => (directory.Length > prefix.Length) && separators.Contains(directory[prefix.Length])).Any(prefix => directory.StartsWith(prefix, StringComparison.Ordinal)); } } From da5e56e5ffe77fbcc1f2de7e1bd833810da6ca28 Mon Sep 17 00:00:00 2001 From: Archi Date: Tue, 12 Mar 2024 13:08:56 +0100 Subject: [PATCH 27/32] Misc error notify --- ArchiSteamFarm/Localization/Strings.Designer.cs | 6 ++++++ ArchiSteamFarm/Localization/Strings.resx | 4 ++++ ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs | 8 +++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ArchiSteamFarm/Localization/Strings.Designer.cs b/ArchiSteamFarm/Localization/Strings.Designer.cs index e5ab2610b9ee9..81460c56a079d 100644 --- a/ArchiSteamFarm/Localization/Strings.Designer.cs +++ b/ArchiSteamFarm/Localization/Strings.Designer.cs @@ -1275,6 +1275,12 @@ public static string PluginUpdateNoAssetFound { } } + public static string PluginUpdateConflictingAssetsFound { + get { + return ResourceManager.GetString("PluginUpdateConflictingAssetsFound", resourceCulture); + } + } + public static string PluginUpdateInProgress { get { return ResourceManager.GetString("PluginUpdateInProgress", resourceCulture); diff --git a/ArchiSteamFarm/Localization/Strings.resx b/ArchiSteamFarm/Localization/Strings.resx index 5e476ddf78afc..a65fa193f1f95 100644 --- a/ArchiSteamFarm/Localization/Strings.resx +++ b/ArchiSteamFarm/Localization/Strings.resx @@ -787,6 +787,10 @@ Process uptime: {1} No asset available for {0} plugin update from version {1} to {2}, this usually means the update will be available at later time. {0} will be replaced by plugin name (string), {1} will be replaced by current plugin's version, {2} will be replaced by remote plugin's version. + + No asset could be determined for {0} plugin update from version {1} to {2}. This can happen if the release is not finished yet - if it keeps happening, you should notify the plugin creator about that. + {0} will be replaced by plugin name (string), {1} will be replaced by current plugin's version, {2} will be replaced by remote plugin's version. + Updating {0} plugin... {0} will be replaced by plugin name (string). diff --git a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs index f4b023bfc9bb6..7b8a784933a74 100644 --- a/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs +++ b/ArchiSteamFarm/Plugins/Interfaces/IGitHubPluginUpdates.cs @@ -128,7 +128,13 @@ public interface IGitHubPluginUpdates : IPluginUpdates { // The very last fallback in case user uses different naming scheme HashSet zipAssets = releaseAssets.Where(static asset => asset.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)).ToHashSet(); - return Task.FromResult(zipAssets.Count == 1 ? zipAssets.First() : null); + if (zipAssets.Count == 1) { + return Task.FromResult(zipAssets.First()); + } + + ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateConflictingAssetsFound, Name, Version, newPluginVersion)); + + return Task.FromResult(null); } protected async Task GetTargetReleaseURL(Version asfVersion, string asfVariant, bool stable) { From a22b2305e4ad92b1afe05b7df6fdf3566953bf7f Mon Sep 17 00:00:00 2001 From: Archi Date: Wed, 13 Mar 2024 20:41:32 +0100 Subject: [PATCH 28/32] Add commands and finally call it a day --- ArchiSteamFarm/Core/ASF.cs | 2 +- ArchiSteamFarm/Plugins/PluginsCore.cs | 194 ++++++++++++------- ArchiSteamFarm/Steam/Interaction/Actions.cs | 31 +++ ArchiSteamFarm/Steam/Interaction/Commands.cs | 46 ++++- 4 files changed, 193 insertions(+), 80 deletions(-) diff --git a/ArchiSteamFarm/Core/ASF.cs b/ArchiSteamFarm/Core/ASF.cs index eed86cdf8128d..f016496f5e7bc 100644 --- a/ArchiSteamFarm/Core/ASF.cs +++ b/ArchiSteamFarm/Core/ASF.cs @@ -722,7 +722,7 @@ private static async Task UpdateAndRestart() { throw new InvalidOperationException(nameof(GlobalConfig)); } - if (!SharedInfo.BuildInfo.CanUpdate || (GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.None)) { + if (GlobalConfig.UpdateChannel == GlobalConfig.EUpdateChannel.None) { return; } diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index 6066f0db51610..572d975e6732f 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -162,6 +162,16 @@ internal static async Task GetChangeNumberToStartFrom() { return results.FirstOrDefault(static result => result != null); } + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")] + internal static HashSet GetPluginsForUpdate(IReadOnlyCollection pluginAssemblyNames) { + if ((pluginAssemblyNames == null) || (pluginAssemblyNames.Count == 0)) { + throw new ArgumentNullException(nameof(pluginAssemblyNames)); + } + + // We use ActivePlugins here, since we want to pick up also plugins removed from automatic updates + return ActivePlugins.OfType().Where(plugin => pluginAssemblyNames.Contains(plugin.GetType().Assembly.GetName().Name)).ToHashSet(); + } + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")] internal static async Task InitPlugins() { if (ActivePlugins.Count > 0) { @@ -699,100 +709,46 @@ internal static async Task OnUpdateProceeding(Version newVersion) { } } - [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3000", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")] - internal static async Task UpdatePlugins(Version asfVersion, GlobalConfig.EUpdateChannel? updateChannel) { + internal static async Task UpdatePlugins(Version asfVersion, GlobalConfig.EUpdateChannel? updateChannel = null) { ArgumentNullException.ThrowIfNull(asfVersion); if (updateChannel.HasValue && !Enum.IsDefined(updateChannel.Value)) { throw new InvalidEnumArgumentException(nameof(updateChannel), (int) updateChannel, typeof(GlobalConfig.EUpdateChannel)); } - if (ActivePlugins.Count == 0) { + if (ActivePluginUpdates.Count == 0) { return false; } + return await UpdatePlugins(asfVersion, ActivePluginUpdates, updateChannel).ConfigureAwait(false); + } + + internal static async Task UpdatePlugins(Version asfVersion, IReadOnlyCollection plugins, GlobalConfig.EUpdateChannel? updateChannel = null) { + ArgumentNullException.ThrowIfNull(asfVersion); + + if ((plugins == null) || (plugins.Count == 0)) { + throw new ArgumentNullException(nameof(plugins)); + } + + if (updateChannel.HasValue && !Enum.IsDefined(updateChannel.Value)) { + throw new InvalidEnumArgumentException(nameof(updateChannel), (int) updateChannel, typeof(GlobalConfig.EUpdateChannel)); + } + if (ASF.WebBrowser == null) { throw new InvalidOperationException(nameof(ASF.WebBrowser)); } updateChannel ??= ASF.GlobalConfig?.UpdateChannel ?? GlobalConfig.DefaultUpdateChannel; - bool restartNeeded = false; + if (updateChannel == GlobalConfig.EUpdateChannel.None) { + return false; + } ASF.ArchiLogger.LogGenericInfo(Strings.PluginUpdatesChecking); - // We update plugins one-by-one to limit memory pressure from potentially big release assets - foreach (IPluginUpdates plugin in ActivePluginUpdates) { - try { - ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateChecking, plugin.Name)); - - string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location); - - if (string.IsNullOrEmpty(assemblyDirectory)) { - throw new InvalidOperationException(nameof(assemblyDirectory)); - } - - string backupDirectory = Path.Combine(assemblyDirectory, SharedInfo.UpdateDirectory); + IList pluginUpdates = await Utilities.InParallel(plugins.Select(plugin => UpdatePlugin(asfVersion, plugin, updateChannel.Value))).ConfigureAwait(false); - if (Directory.Exists(backupDirectory)) { - ASF.ArchiLogger.LogGenericInfo(Strings.UpdateCleanup); - - Directory.Delete(backupDirectory, true); - } - - Uri? releaseURL = await plugin.GetTargetReleaseURL(asfVersion, SharedInfo.BuildInfo.Variant, updateChannel.Value).ConfigureAwait(false); - - if (releaseURL == null) { - continue; - } - - ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateInProgress, plugin.Name)); - - Progress progressReporter = new(); - - progressReporter.ProgressChanged += Utilities.OnProgressChanged; - - BinaryResponse? response; - - try { - response = await ASF.WebBrowser.UrlGetToBinary(releaseURL, progressReporter: progressReporter).ConfigureAwait(false); - } finally { - progressReporter.ProgressChanged -= Utilities.OnProgressChanged; - } - - if (response?.Content == null) { - continue; - } - - ASF.ArchiLogger.LogGenericInfo(Strings.PatchingFiles); - - byte[] responseBytes = response.Content as byte[] ?? response.Content.ToArray(); - - MemoryStream memoryStream = new(responseBytes); - - await using (memoryStream.ConfigureAwait(false)) { - using ZipArchive zipArchive = new(memoryStream); - - await plugin.OnPluginUpdateProceeding().ConfigureAwait(false); - - if (!Utilities.UpdateFromArchive(zipArchive, assemblyDirectory)) { - ASF.ArchiLogger.LogGenericError(Strings.WarningFailed); - - continue; - } - } - - restartNeeded = true; - - ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateFinished, plugin.Name)); - - await plugin.OnPluginUpdateFinished().ConfigureAwait(false); - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - } - } - - return restartNeeded; + return pluginUpdates.Any(static restartNeeded => restartNeeded); } [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")] @@ -836,4 +792,92 @@ internal static async Task UpdatePlugins(Version asfVersion, GlobalConfig. return assemblies; } + + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3000", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")] + private static async Task UpdatePlugin(Version asfVersion, IPluginUpdates plugin, GlobalConfig.EUpdateChannel updateChannel) { + ArgumentNullException.ThrowIfNull(asfVersion); + ArgumentNullException.ThrowIfNull(plugin); + + if (!Enum.IsDefined(updateChannel) || (updateChannel == GlobalConfig.EUpdateChannel.None)) { + throw new InvalidEnumArgumentException(nameof(updateChannel), (int) updateChannel, typeof(GlobalConfig.EUpdateChannel)); + } + + if (ASF.WebBrowser == null) { + throw new InvalidOperationException(nameof(ASF.WebBrowser)); + } + + try { + ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateChecking, plugin.Name)); + + string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location); + + if (string.IsNullOrEmpty(assemblyDirectory)) { + throw new InvalidOperationException(nameof(assemblyDirectory)); + } + + string backupDirectory = Path.Combine(assemblyDirectory, SharedInfo.UpdateDirectory); + + if (Directory.Exists(backupDirectory)) { + ASF.ArchiLogger.LogGenericInfo(Strings.UpdateCleanup); + + Directory.Delete(backupDirectory, true); + } + + Uri? releaseURL = await plugin.GetTargetReleaseURL(asfVersion, SharedInfo.BuildInfo.Variant, updateChannel).ConfigureAwait(false); + + if (releaseURL == null) { + return false; + } + + ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateInProgress, plugin.Name)); + + Progress progressReporter = new(); + + progressReporter.ProgressChanged += Utilities.OnProgressChanged; + + BinaryResponse? response; + + try { + response = await ASF.WebBrowser.UrlGetToBinary(releaseURL, progressReporter: progressReporter).ConfigureAwait(false); + } finally { + progressReporter.ProgressChanged -= Utilities.OnProgressChanged; + } + + if (response?.Content == null) { + return false; + } + + ASF.ArchiLogger.LogGenericInfo(Strings.PatchingFiles); + + byte[] responseBytes = response.Content as byte[] ?? response.Content.ToArray(); + + MemoryStream memoryStream = new(responseBytes); + + await using (memoryStream.ConfigureAwait(false)) { + using ZipArchive zipArchive = new(memoryStream); + + await plugin.OnPluginUpdateProceeding().ConfigureAwait(false); + + if (!Utilities.UpdateFromArchive(zipArchive, assemblyDirectory)) { + ASF.ArchiLogger.LogGenericError(Strings.WarningFailed); + + return false; + } + } + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + + return false; + } + + ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateFinished, plugin.Name)); + + try { + await plugin.OnPluginUpdateFinished().ConfigureAwait(false); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + } + + return true; + } } diff --git a/ArchiSteamFarm/Steam/Interaction/Actions.cs b/ArchiSteamFarm/Steam/Interaction/Actions.cs index 796a4fd9f8171..ee3eb832fef61 100644 --- a/ArchiSteamFarm/Steam/Interaction/Actions.cs +++ b/ArchiSteamFarm/Steam/Interaction/Actions.cs @@ -34,6 +34,8 @@ using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Localization; +using ArchiSteamFarm.Plugins; +using ArchiSteamFarm.Plugins.Interfaces; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Exchange; using ArchiSteamFarm.Steam.Storage; @@ -488,6 +490,35 @@ static async () => { return newVersion > SharedInfo.Version ? (true, null, newVersion) : (false, $"V{SharedInfo.Version} ≥ V{newVersion}", newVersion); } + [PublicAPI] + public static async Task<(bool Success, string? Message)> UpdatePlugins(IReadOnlyCollection plugins, GlobalConfig.EUpdateChannel? channel = null) { + if ((plugins == null) || (plugins.Count == 0)) { + throw new ArgumentNullException(nameof(plugins)); + } + + if (channel.HasValue && !Enum.IsDefined(channel.Value)) { + throw new InvalidEnumArgumentException(nameof(channel), (int) channel, typeof(GlobalConfig.EUpdateChannel)); + } + + HashSet pluginAssemblyNames = plugins.ToHashSet(StringComparer.OrdinalIgnoreCase); + + HashSet pluginsForUpdate = PluginsCore.GetPluginsForUpdate(pluginAssemblyNames); + + if (pluginsForUpdate.Count == 0) { + return (false, Strings.NothingFound); + } + + bool restartNeeded = await PluginsCore.UpdatePlugins(SharedInfo.Version, pluginsForUpdate, channel).ConfigureAwait(false); + + if (restartNeeded) { + Utilities.InBackground(ASF.RestartOrExit); + } + + string message = restartNeeded ? Strings.UpdateFinished : Strings.NothingFound; + + return (true, message); + } + internal async Task AcceptDigitalGiftCards() { if (!Bot.IsConnectedAndLoggedOn) { return; diff --git a/ArchiSteamFarm/Steam/Interaction/Commands.cs b/ArchiSteamFarm/Steam/Interaction/Commands.cs index 63adf803bcb9b..0d9c51bcb761b 100644 --- a/ArchiSteamFarm/Steam/Interaction/Commands.cs +++ b/ArchiSteamFarm/Steam/Interaction/Commands.cs @@ -1,18 +1,20 @@ +// ---------------------------------------------------------------------------------------------- // _ _ _ ____ _ _____ // / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ // / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ // / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | // /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// | +// ---------------------------------------------------------------------------------------------- +// // Copyright 2015-2024 Łukasz "JustArchi" Domeradzki // Contact: JustArchi@JustArchi.net -// | +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// | +// // http://www.apache.org/licenses/LICENSE-2.0 -// | +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -327,6 +329,10 @@ public static EAccess GetProxyAccess(Bot bot, EAccess access, ulong steamID = 0) return await ResponseUnpackBoosters(access, Utilities.GetArgsAsText(args, 1, ","), steamID).ConfigureAwait(false); case "UPDATE": return await ResponseUpdate(access, args[1]).ConfigureAwait(false); + case "UPDATEPLUGINS" when args.Length > 2: + return await ResponseUpdatePlugins(access, Utilities.GetArgsAsText(args, 2, ","), args[1]).ConfigureAwait(false); + case "UPDATEPLUGINS": + return await ResponseUpdatePlugins(access, args[1]).ConfigureAwait(false); default: string? pluginsResponse = await PluginsCore.OnBotCommand(Bot, access, message, args, steamID).ConfigureAwait(false); @@ -3161,6 +3167,38 @@ internal void OnNewLicenseList() { return FormatStaticResponse($"{(success ? Strings.Success : Strings.WarningFailed)}{(!string.IsNullOrEmpty(message) ? $" {message}" : version != null ? $" {version}" : "")}"); } + private static async Task ResponseUpdatePlugins(EAccess access, string pluginsText, string? channelText = null) { + if (!Enum.IsDefined(access)) { + throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); + } + + ArgumentException.ThrowIfNullOrEmpty(pluginsText); + + if (access < EAccess.Owner) { + return null; + } + + GlobalConfig.EUpdateChannel? channel = null; + + if (!string.IsNullOrEmpty(channelText)) { + if (!Enum.TryParse(channelText, true, out GlobalConfig.EUpdateChannel parsedChannel) || (parsedChannel == GlobalConfig.EUpdateChannel.None)) { + return FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(channelText))); + } + + channel = parsedChannel; + } + + string[] plugins = pluginsText.Split(SharedInfo.ListElementSeparators, StringSplitOptions.RemoveEmptyEntries); + + if (plugins.Length == 0) { + return FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsEmpty, nameof(plugins))); + } + + (bool success, string? message) = await Actions.UpdatePlugins(plugins, channel).ConfigureAwait(false); + + return FormatStaticResponse($"{(success ? Strings.Success : Strings.WarningFailed)}{(!string.IsNullOrEmpty(message) ? $" {message}" : "")}"); + } + private string? ResponseVersion(EAccess access) { if (!Enum.IsDefined(access)) { throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess)); From 62f52f767b1dd9b50561fafeb794e5828e1d772f Mon Sep 17 00:00:00 2001 From: Archi Date: Fri, 15 Mar 2024 13:42:20 +0100 Subject: [PATCH 29/32] Misc progress percentages text --- ArchiSteamFarm/Core/ASF.cs | 14 +++++++++++--- ArchiSteamFarm/Core/Utilities.cs | 7 +++++-- ArchiSteamFarm/Plugins/PluginsCore.cs | 20 +++++++++++++++----- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/ArchiSteamFarm/Core/ASF.cs b/ArchiSteamFarm/Core/ASF.cs index f016496f5e7bc..c94226d71c252 100644 --- a/ArchiSteamFarm/Core/ASF.cs +++ b/ArchiSteamFarm/Core/ASF.cs @@ -778,6 +778,8 @@ private static async Task UpdateAndRestart() { return null; } + string targetFile; + await UpdateSemaphore.WaitAsync().ConfigureAwait(false); try { @@ -851,7 +853,7 @@ private static async Task UpdateAndRestart() { return null; } - string targetFile = $"{SharedInfo.ASF}-{SharedInfo.BuildInfo.Variant}.zip"; + targetFile = $"{SharedInfo.ASF}-{SharedInfo.BuildInfo.Variant}.zip"; ReleaseAsset? binaryAsset = releaseResponse.Assets.FirstOrDefault(asset => !string.IsNullOrEmpty(asset.Name) && asset.Name.Equals(targetFile, StringComparison.OrdinalIgnoreCase)); if (binaryAsset == null) { @@ -883,14 +885,14 @@ private static async Task UpdateAndRestart() { Progress progressReporter = new(); - progressReporter.ProgressChanged += Utilities.OnProgressChanged; + progressReporter.ProgressChanged += onProgressChanged; BinaryResponse? response; try { response = await WebBrowser.UrlGetToBinary(binaryAsset.DownloadURL, progressReporter: progressReporter).ConfigureAwait(false); } finally { - progressReporter.ProgressChanged -= Utilities.OnProgressChanged; + progressReporter.ProgressChanged -= onProgressChanged; } if (response?.Content == null) { @@ -959,6 +961,12 @@ private static async Task UpdateAndRestart() { } finally { UpdateSemaphore.Release(); } + + void onProgressChanged(object? sender, byte progressPercentage) { + ArgumentOutOfRangeException.ThrowIfGreaterThan(progressPercentage, 100); + + Utilities.OnProgressChanged(targetFile, progressPercentage); + } } private static async Task UpdateFromArchive(Version newVersion, GlobalConfig.EUpdateChannel updateChannel, ZipArchive zipArchive) { diff --git a/ArchiSteamFarm/Core/Utilities.cs b/ArchiSteamFarm/Core/Utilities.cs index 65253ef35bb43..e4b3af256e329 100644 --- a/ArchiSteamFarm/Core/Utilities.cs +++ b/ArchiSteamFarm/Core/Utilities.cs @@ -275,14 +275,17 @@ internal static ulong MathAdd(ulong first, int second) { return first - (uint) -second; } - internal static void OnProgressChanged(object? sender, byte progressPercentage) { + internal static void OnProgressChanged(string fileName, byte progressPercentage) { + ArgumentNullException.ThrowIfNull(fileName); + ArgumentOutOfRangeException.ThrowIfGreaterThan(progressPercentage, 100); + const byte printEveryPercentage = 10; if (progressPercentage % printEveryPercentage != 0) { return; } - ASF.ArchiLogger.LogGenericDebug($"{progressPercentage}%..."); + ASF.ArchiLogger.LogGenericDebug($"{fileName} {progressPercentage}%..."); } internal static (bool IsWeak, string? Reason) TestPasswordStrength(string password, ISet? additionallyForbiddenPhrases = null) { diff --git a/ArchiSteamFarm/Plugins/PluginsCore.cs b/ArchiSteamFarm/Plugins/PluginsCore.cs index 572d975e6732f..17b0c39b4d702 100644 --- a/ArchiSteamFarm/Plugins/PluginsCore.cs +++ b/ArchiSteamFarm/Plugins/PluginsCore.cs @@ -806,8 +806,12 @@ private static async Task UpdatePlugin(Version asfVersion, IPluginUpdates throw new InvalidOperationException(nameof(ASF.WebBrowser)); } + string pluginName; + try { - ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateChecking, plugin.Name)); + pluginName = plugin.Name; + + ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateChecking, pluginName)); string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location); @@ -829,18 +833,18 @@ private static async Task UpdatePlugin(Version asfVersion, IPluginUpdates return false; } - ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateInProgress, plugin.Name)); + ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateInProgress, pluginName)); Progress progressReporter = new(); - progressReporter.ProgressChanged += Utilities.OnProgressChanged; + progressReporter.ProgressChanged += onProgressChanged; BinaryResponse? response; try { response = await ASF.WebBrowser.UrlGetToBinary(releaseURL, progressReporter: progressReporter).ConfigureAwait(false); } finally { - progressReporter.ProgressChanged -= Utilities.OnProgressChanged; + progressReporter.ProgressChanged -= onProgressChanged; } if (response?.Content == null) { @@ -870,7 +874,7 @@ private static async Task UpdatePlugin(Version asfVersion, IPluginUpdates return false; } - ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateFinished, plugin.Name)); + ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateFinished, pluginName)); try { await plugin.OnPluginUpdateFinished().ConfigureAwait(false); @@ -879,5 +883,11 @@ private static async Task UpdatePlugin(Version asfVersion, IPluginUpdates } return true; + + void onProgressChanged(object? sender, byte progressPercentage) { + ArgumentOutOfRangeException.ThrowIfGreaterThan(progressPercentage, 100); + + Utilities.OnProgressChanged(pluginName, progressPercentage); + } } } From f6cbe3172f8647e45eab5b0d309f77c1e173f94e Mon Sep 17 00:00:00 2001 From: Archi Date: Fri, 15 Mar 2024 13:43:32 +0100 Subject: [PATCH 30/32] Misc --- ArchiSteamFarm/Core/Utilities.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ArchiSteamFarm/Core/Utilities.cs b/ArchiSteamFarm/Core/Utilities.cs index e4b3af256e329..1f806827e4cf8 100644 --- a/ArchiSteamFarm/Core/Utilities.cs +++ b/ArchiSteamFarm/Core/Utilities.cs @@ -276,7 +276,7 @@ internal static ulong MathAdd(ulong first, int second) { } internal static void OnProgressChanged(string fileName, byte progressPercentage) { - ArgumentNullException.ThrowIfNull(fileName); + ArgumentException.ThrowIfNullOrEmpty(fileName); ArgumentOutOfRangeException.ThrowIfGreaterThan(progressPercentage, 100); const byte printEveryPercentage = 10; From e78ff22ab294b2ece273b73ab887e832d1b7fad3 Mon Sep 17 00:00:00 2001 From: Archi Date: Sat, 16 Mar 2024 23:33:49 +0100 Subject: [PATCH 31/32] Flip DefaultPluginsUpdateMode as per the voting --- ArchiSteamFarm/Storage/GlobalConfig.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ArchiSteamFarm/Storage/GlobalConfig.cs b/ArchiSteamFarm/Storage/GlobalConfig.cs index 3fe3344484b0f..60544a2fd2433 100644 --- a/ArchiSteamFarm/Storage/GlobalConfig.cs +++ b/ArchiSteamFarm/Storage/GlobalConfig.cs @@ -110,7 +110,7 @@ public sealed class GlobalConfig { public const EOptimizationMode DefaultOptimizationMode = EOptimizationMode.MaxPerformance; [PublicAPI] - public const EPluginsUpdateMode DefaultPluginsUpdateMode = EPluginsUpdateMode.Blacklist; + public const EPluginsUpdateMode DefaultPluginsUpdateMode = EPluginsUpdateMode.Whitelist; [PublicAPI] public const string? DefaultSteamMessagePrefix = "/me "; @@ -604,8 +604,8 @@ public enum EOptimizationMode : byte { [PublicAPI] public enum EPluginsUpdateMode : byte { - Blacklist, - Whitelist + Whitelist, + Blacklist } [PublicAPI] From 8764a145de12e88dedc26e593aa279058f573546 Mon Sep 17 00:00:00 2001 From: Archi Date: Sat, 16 Mar 2024 23:45:43 +0100 Subject: [PATCH 32/32] Misc --- ArchiSteamFarm/Web/WebBrowser.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ArchiSteamFarm/Web/WebBrowser.cs b/ArchiSteamFarm/Web/WebBrowser.cs index 1539733b8907c..477bdac00ebaf 100644 --- a/ArchiSteamFarm/Web/WebBrowser.cs +++ b/ArchiSteamFarm/Web/WebBrowser.cs @@ -181,13 +181,17 @@ public HttpClient GenerateDisposableHttpClient(bool extendedTimeout = false) { break; } - readThisBatch += read; + // Report progress in-between downloading only if file is big enough to justify it + // Current logic below will report progress if file is bigger than ~800 KB + if (batchIncreaseSize >= buffer.Length) { + readThisBatch += read; - for (; (readThisBatch >= batchIncreaseSize) && (batch < 99); readThisBatch -= batchIncreaseSize) { - // We need a copy of variable being passed when in for loops, as loop will proceed before our event is launched - byte progress = ++batch; + for (; (readThisBatch >= batchIncreaseSize) && (batch < 99); readThisBatch -= batchIncreaseSize) { + // We need a copy of variable being passed when in for loops, as loop will proceed before our event is launched + byte progress = ++batch; - progressReporter?.Report(progress); + progressReporter?.Report(progress); + } } await ms.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);