diff --git a/ArchiSteamFarm/Core/ASF.cs b/ArchiSteamFarm/Core/ASF.cs index d0e96d58a..f5d0481d0 100644 --- a/ArchiSteamFarm/Core/ASF.cs +++ b/ArchiSteamFarm/Core/ASF.cs @@ -189,7 +189,7 @@ public static class ASF { } } - internal static async Task<(Version? NewVersion, bool RestartNeeded)> Update(GlobalConfig.EUpdateChannel? updateChannel = null, bool updateOverride = false) { + internal static async Task<(bool Updated, Version? NewVersion)> Update(GlobalConfig.EUpdateChannel? updateChannel = null, bool updateOverride = false, bool forced = false) { if (updateChannel.HasValue && !Enum.IsDefined(updateChannel.Value)) { throw new InvalidEnumArgumentException(nameof(updateChannel), (int) updateChannel, typeof(GlobalConfig.EUpdateChannel)); } @@ -198,16 +198,14 @@ public static class ASF { throw new InvalidOperationException(nameof(GlobalConfig)); } - Version? newVersion = await UpdateASF(updateChannel, updateOverride).ConfigureAwait(false); + (bool updated, Version? newVersion) = await UpdateASF(updateChannel, updateOverride, forced).ConfigureAwait(false); - bool restartNeeded = (newVersion != null) && (newVersion > SharedInfo.Version); - - if (!restartNeeded) { + if (!updated) { // ASF wasn't updated as part of the process, update the plugins alone - restartNeeded = await PluginsCore.UpdatePlugins(SharedInfo.Version, updateChannel).ConfigureAwait(false); + updated = await PluginsCore.UpdatePlugins(SharedInfo.Version, updateChannel).ConfigureAwait(false); } - return (newVersion, restartNeeded); + return (updated, newVersion); } private static async Task CanHandleWriteEvent(string filePath) { @@ -739,27 +737,25 @@ public static class ASF { ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.AutoUpdateCheckInfo, autoUpdatePeriod.ToHumanReadable())); } - (Version? newVersion, bool restartNeeded) = await Update().ConfigureAwait(false); + (bool updated, Version? newVersion) = await Update().ConfigureAwait(false); - if ((newVersion != null) && (SharedInfo.Version > newVersion)) { - // User is running version newer than their channel allows - ArchiLogger.LogGenericWarning(Strings.WarningPreReleaseVersion); - await Task.Delay(SharedInfo.InformationDelay).ConfigureAwait(false); - } + if (!updated) { + if ((newVersion != null) && (SharedInfo.Version > newVersion)) { + // User is running version newer than their channel allows + ArchiLogger.LogGenericWarning(Strings.WarningPreReleaseVersion); + await Task.Delay(SharedInfo.InformationDelay).ConfigureAwait(false); + } - if (!restartNeeded) { return; } // Allow crash file recovery, if needed - if ((newVersion != null) && (newVersion > SharedInfo.Version)) { - Program.AllowCrashFileRemoval = true; - } + Program.AllowCrashFileRemoval = true; await RestartOrExit().ConfigureAwait(false); } - private static async Task UpdateASF(GlobalConfig.EUpdateChannel? channel = null, bool updateOverride = false) { + private static async Task<(bool Updated, Version? NewVersion)> UpdateASF(GlobalConfig.EUpdateChannel? channel = null, bool updateOverride = false, bool forced = false) { if (channel.HasValue && !Enum.IsDefined(channel.Value)) { throw new InvalidEnumArgumentException(nameof(channel), (int) channel, typeof(GlobalConfig.EUpdateChannel)); } @@ -775,7 +771,7 @@ public static class ASF { channel ??= GlobalConfig.UpdateChannel; if (!SharedInfo.BuildInfo.CanUpdate || (channel == GlobalConfig.EUpdateChannel.None)) { - return null; + return (false, null); } string targetFile; @@ -809,7 +805,7 @@ public static class ASF { if (Directory.Exists(backupDirectory)) { ArchiLogger.LogGenericError(Strings.WarningFailed); - return null; + return (false, null); } ArchiLogger.LogGenericInfo(Strings.Done); @@ -822,35 +818,35 @@ public static class ASF { if (releaseResponse == null) { ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); - return null; + return (false, null); } if (string.IsNullOrEmpty(releaseResponse.Tag)) { ArchiLogger.LogGenericWarning(Strings.ErrorUpdateCheckFailed); - return null; + return (false, null); } Version newVersion = new(releaseResponse.Tag); ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.UpdateVersionInfo, SharedInfo.Version, newVersion)); - if (SharedInfo.Version >= newVersion) { - return newVersion; + if (!forced && (SharedInfo.Version >= newVersion)) { + return (false, newVersion); } if (!updateOverride && (GlobalConfig.UpdatePeriod == 0)) { ArchiLogger.LogGenericInfo(Strings.UpdateNewVersionAvailable); await Task.Delay(SharedInfo.ShortInformationDelay).ConfigureAwait(false); - return null; + return (false, newVersion); } // Auto update logic starts here if (releaseResponse.Assets.IsEmpty) { ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssets); - return null; + return (false, newVersion); } targetFile = $"{SharedInfo.ASF}-{SharedInfo.BuildInfo.Variant}.zip"; @@ -859,7 +855,7 @@ public static class ASF { if (binaryAsset == null) { ArchiLogger.LogGenericWarning(Strings.ErrorUpdateNoAssetForThisVersion); - return null; + return (false, newVersion); } ArchiLogger.LogGenericInfo(Strings.FetchingChecksumFromRemoteServer); @@ -869,12 +865,12 @@ public static class ASF { switch (remoteChecksum) { case null: // Timeout or error, refuse to update as a security measure - return null; + return (false, newVersion); 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; + return (false, newVersion); } if (!string.IsNullOrEmpty(releaseResponse.ChangelogPlainText)) { @@ -896,7 +892,7 @@ public static class ASF { } if (response?.Content == null) { - return null; + return (false, newVersion); } ArchiLogger.LogGenericInfo(Strings.VerifyingChecksumWithRemoteServer); @@ -908,7 +904,7 @@ public static class ASF { if (!checksum.Equals(remoteChecksum, StringComparison.OrdinalIgnoreCase)) { ArchiLogger.LogGenericError(Strings.ChecksumWrong); - return SharedInfo.Version; + return (false, newVersion); } await PluginsCore.OnUpdateProceeding(newVersion).ConfigureAwait(false); @@ -950,14 +946,14 @@ public static class ASF { } } - return null; + return (false, newVersion); } ArchiLogger.LogGenericInfo(Strings.UpdateFinished); await PluginsCore.OnUpdateFinished(newVersion).ConfigureAwait(false); - return newVersion; + return (true, newVersion); } finally { UpdateSemaphore.Release(); } diff --git a/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs b/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs index 28bfa8306..a1092e036 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs @@ -182,7 +182,7 @@ public sealed class ASFController : ArchiController { return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(request.Channel)))); } - (bool success, string? message, Version? version) = await Actions.Update(request.Channel).ConfigureAwait(false); + (bool success, string? message, Version? version) = await Actions.Update(request.Channel, request.Forced).ConfigureAwait(false); if (string.IsNullOrEmpty(message)) { message = success ? Strings.Success : Strings.WarningFailed; diff --git a/ArchiSteamFarm/IPC/Requests/UpdateRequest.cs b/ArchiSteamFarm/IPC/Requests/UpdateRequest.cs index 0592a4c85..91da0b52d 100644 --- a/ArchiSteamFarm/IPC/Requests/UpdateRequest.cs +++ b/ArchiSteamFarm/IPC/Requests/UpdateRequest.cs @@ -35,6 +35,12 @@ public sealed class UpdateRequest { [JsonInclude] public GlobalConfig.EUpdateChannel? Channel { get; private init; } + /// + /// Forced update. This allows ASF to potentially downgrade to previous version available on selected , which isn't permitted normally. + /// + [JsonInclude] + public bool Forced { get; private init; } + [JsonConstructor] private UpdateRequest() { } } diff --git a/ArchiSteamFarm/Steam/Interaction/Actions.cs b/ArchiSteamFarm/Steam/Interaction/Actions.cs index 043f541e0..8457296bd 100644 --- a/ArchiSteamFarm/Steam/Interaction/Actions.cs +++ b/ArchiSteamFarm/Steam/Interaction/Actions.cs @@ -472,22 +472,18 @@ public sealed class Actions : IAsyncDisposable, IDisposable { } [PublicAPI] - public static async Task<(bool Success, string? Message, Version? Version)> Update(GlobalConfig.EUpdateChannel? channel = null) { + public static async Task<(bool Success, string? Message, Version? Version)> Update(GlobalConfig.EUpdateChannel? channel = null, bool forced = false) { if (channel.HasValue && !Enum.IsDefined(channel.Value)) { throw new InvalidEnumArgumentException(nameof(channel), (int) channel, typeof(GlobalConfig.EUpdateChannel)); } - (Version? newVersion, bool restartNeeded) = await ASF.Update(channel, true).ConfigureAwait(false); + (bool updated, Version? newVersion) = await ASF.Update(channel, true, forced).ConfigureAwait(false); - if (restartNeeded) { + if (updated) { Utilities.InBackground(ASF.RestartOrExit); } - if (newVersion == null) { - return (false, null, null); - } - - return newVersion > SharedInfo.Version ? (true, null, newVersion) : (false, $"V{SharedInfo.Version} ≥ V{newVersion}", newVersion); + return updated ? (true, null, newVersion) : SharedInfo.Version >= newVersion ? (false, $"V{SharedInfo.Version} ≥ V{newVersion}", newVersion) : (false, null, newVersion); } [PublicAPI] diff --git a/ArchiSteamFarm/Steam/Interaction/Commands.cs b/ArchiSteamFarm/Steam/Interaction/Commands.cs index 432823a5a..17936e69d 100644 --- a/ArchiSteamFarm/Steam/Interaction/Commands.cs +++ b/ArchiSteamFarm/Steam/Interaction/Commands.cs @@ -3154,15 +3154,21 @@ public sealed class Commands { return null; } + bool forced = false; GlobalConfig.EUpdateChannel channel = ASF.GlobalConfig?.UpdateChannel ?? GlobalConfig.DefaultUpdateChannel; if (!string.IsNullOrEmpty(channelText)) { + if (channelText.EndsWith('!')) { + forced = true; + channelText = channelText[..^1]; + } + if (!Enum.TryParse(channelText, true, out channel) || (channel == GlobalConfig.EUpdateChannel.None)) { return FormatStaticResponse(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(channelText))); } } - (bool success, string? message, Version? version) = await Actions.Update(channel).ConfigureAwait(false); + (bool success, string? message, Version? version) = await Actions.Update(channel, forced).ConfigureAwait(false); return FormatStaticResponse($"{(success ? Strings.Success : Strings.WarningFailed)}{(!string.IsNullOrEmpty(message) ? $" {message}" : version != null ? $" {version}" : "")}"); }