Implement plugin updates with IPluginUpdates interface (#3151)

* Initial implementation of plugin updates

* Update PluginsCore.cs

* Update IPluginUpdates.cs

* Update PluginsCore.cs

* Make it work

* Misc

* Revert "Misc"

This reverts commit bccd1bb2b8.

* Proper fix

* Make plugin updates independent of GitHub

* Final touches

* Misc

* Allow plugin creators for more flexibility in picking from GitHub releases

* Misc rename

* Make changelog internal again

This is ASF implementation detail, make body available instead and let people implement changelogs themselves

* Misc

* Add missing localization

* Add a way to disable plugin updates

* Update PluginsCore.cs

* Update PluginsCore.cs

* Misc

* Update IGitHubPluginUpdates.cs

* Update IGitHubPluginUpdates.cs

* Update IGitHubPluginUpdates.cs

* Update IGitHubPluginUpdates.cs

* Make zip selection ignore case

* Update ArchiSteamFarm/Core/Utilities.cs

Co-authored-by: Vita Chumakova <me@ezhevita.dev>

* Misc error notify

* Add commands and finally call it a day

* Misc progress percentages text

* Misc

* Flip DefaultPluginsUpdateMode as per the voting

* Misc

---------

Co-authored-by: Vita Chumakova <me@ezhevita.dev>
This commit is contained in:
Łukasz Domeradzki
2024-03-16 23:56:57 +01:00
committed by GitHub
parent c874779c0d
commit aedede3ba4
18 changed files with 1389 additions and 587 deletions

View File

@@ -0,0 +1,186 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
//
// 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;
/// <inheritdoc />
/// <summary>
/// Implementing this interface allows your plugin to update from published releases on GitHub.
/// At the minimum you must provide <see cref="RepositoryName" />.
/// 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 <see cref="GetTargetReleaseAsset" /> 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 <see cref="IPluginUpdates" /> interface instead, where you can provide your own <see cref="GetTargetReleaseURL" /> implementation, with optional help from our <see cref="GitHubService" />.
/// </summary>
[PublicAPI]
public interface IGitHubPluginUpdates : IPluginUpdates {
/// <summary>
/// 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.
/// </summary>
bool CanUpdate => true;
/// <summary>
/// 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.
/// </summary>
/// <returns>Repository name in format of {Author}/{Repository}.</returns>
/// <example>JustArchiNET/ArchiSteamFarm</example>
string RepositoryName { get; }
Task<Uri?> 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));
}
return GetTargetReleaseURL(asfVersion, asfVariant, updateChannel == GlobalConfig.EUpdateChannel.Stable);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="asfVersion">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.</param>
/// <param name="asfVariant">ASF variant of current instance, which may be useful if you're providing different versions for different ASF variants.</param>
/// <param name="newPluginVersion">The target (new) version of the plugin found available in <see cref="RepositoryName" />.</param>
/// <param name="releaseAssets">Available release assets for auto-update. Those come directly from your release on GitHub.</param>
/// <remarks>
/// 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 matching in the release
/// Where:
/// - {PluginName} is <see cref="IPlugin.Name" />
/// - {Major} is target major ASF version (A 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
/// 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
/// </remarks>
/// <returns>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.</returns>
Task<ReleaseAsset?> GetTargetReleaseAsset(Version asfVersion, string asfVariant, Version newPluginVersion, IReadOnlyCollection<ReleaseAsset> releaseAssets) {
ArgumentNullException.ThrowIfNull(asfVersion);
ArgumentException.ThrowIfNullOrEmpty(asfVariant);
ArgumentNullException.ThrowIfNull(newPluginVersion);
if ((releaseAssets == null) || (releaseAssets.Count == 0)) {
throw new ArgumentNullException(nameof(releaseAssets));
}
Dictionary<string, ReleaseAsset> assetsByName = releaseAssets.ToDictionary(static asset => asset.Name, StringComparer.OrdinalIgnoreCase);
List<string> 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<ReleaseAsset?>(targetAsset);
}
}
// The very last fallback in case user uses different naming scheme
HashSet<ReleaseAsset> zipAssets = releaseAssets.Where(static asset => asset.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)).ToHashSet();
if (zipAssets.Count == 1) {
return Task.FromResult<ReleaseAsset?>(zipAssets.First());
}
ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateConflictingAssetsFound, Name, Version, newPluginVersion));
return Task.FromResult<ReleaseAsset?>(null);
}
protected async Task<Uri?> GetTargetReleaseURL(Version asfVersion, string asfVariant, bool stable) {
ArgumentNullException.ThrowIfNull(asfVersion);
ArgumentException.ThrowIfNullOrEmpty(asfVariant);
if (!CanUpdate) {
return null;
}
if (string.IsNullOrEmpty(RepositoryName)) {
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningFailedWithError, nameof(RepositoryName)));
return null;
}
ReleaseResponse? releaseResponse = await GitHubService.GetLatestRelease(RepositoryName, stable).ConfigureAwait(false);
if (releaseResponse == null) {
return null;
}
Version newVersion = new(releaseResponse.Tag);
if (Version >= newVersion) {
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateNotFound, Name, Version, newVersion));
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)) {
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;
}
}

View File

@@ -0,0 +1,56 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
//
// 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.Threading.Tasks;
using ArchiSteamFarm.Storage;
using JetBrains.Annotations;
namespace ArchiSteamFarm.Plugins.Interfaces;
/// <summary>
/// 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 <see cref="IGitHubPluginUpdates" /> instead.
/// </summary>
[PublicAPI]
public interface IPluginUpdates : IPlugin {
/// <summary>
/// ASF will call this function for determining the target release asset URL to update to.
/// </summary>
/// <param name="asfVersion">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.</param>
/// <param name="asfVariant">ASF variant of current instance, which may be useful if you're providing different versions for different ASF variants.</param>
/// <param name="updateChannel">ASF update channel specified for this request. This might be different from the one specified in <see cref="GlobalConfig" />, as user might've specified other one for this request.</param>
/// <returns>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.</returns>
Task<Uri?> GetTargetReleaseURL(Version asfVersion, string asfVariant, GlobalConfig.EUpdateChannel updateChannel);
/// <summary>
/// ASF will call this method after update to the new plugin version has been finished, just before restart of the process.
/// </summary>
Task OnPluginUpdateFinished() => Task.CompletedTask;
/// <summary>
/// ASF will call this method before proceeding with an update to the new plugin version.
/// </summary>
Task OnPluginUpdateProceeding() => Task.CompletedTask;
}

View File

@@ -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,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;
@@ -29,6 +32,7 @@ using System.Composition.Hosting;
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 +47,8 @@ using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Steam.Data;
using ArchiSteamFarm.Steam.Exchange;
using ArchiSteamFarm.Steam.Integration.Callbacks;
using ArchiSteamFarm.Storage;
using ArchiSteamFarm.Web.Responses;
using JetBrains.Annotations;
using SteamKit2;
@@ -54,6 +60,8 @@ public static class PluginsCore {
[ImportMany]
internal static FrozenSet<IPlugin> ActivePlugins { get; private set; } = FrozenSet<IPlugin>.Empty;
private static FrozenSet<IPluginUpdates> ActivePluginUpdates = FrozenSet<IPluginUpdates>.Empty;
[PublicAPI]
public static async Task<ICrossProcessSemaphore> GetCrossProcessSemaphore(string objectName) {
ArgumentException.ThrowIfNullOrEmpty(objectName);
@@ -154,6 +162,16 @@ public static class PluginsCore {
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<IPluginUpdates> GetPluginsForUpdate(IReadOnlyCollection<string> 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<IPluginUpdates>().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<bool> InitPlugins() {
if (ActivePlugins.Count > 0) {
@@ -238,10 +256,10 @@ public static class PluginsCore {
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();
@@ -253,6 +271,42 @@ public static class PluginsCore {
Console.Title = SharedInfo.ProgramIdentifier;
}
GlobalConfig.EPluginsUpdateMode pluginsUpdateMode = ASF.GlobalConfig?.PluginsUpdateMode ?? GlobalConfig.DefaultPluginsUpdateMode;
ImmutableHashSet<string> pluginsUpdateList = ASF.GlobalConfig?.PluginsUpdateList ?? GlobalConfig.DefaultPluginsUpdateList;
HashSet<IPluginUpdates> activePluginUpdates = [];
foreach (IPluginUpdates plugin in activePlugins.OfType<IPluginUpdates>()) {
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(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(CultureInfo.CurrentCulture, Strings.PluginUpdateDisabled, plugin.Name, pluginAssemblyName));
break;
}
}
if (activePluginUpdates.Count > 0) {
ASF.ArchiLogger.LogGenericWarning(Strings.CustomPluginUpdatesEnabled);
ActivePluginUpdates = activePluginUpdates.ToFrozenSet();
}
return true;
}
@@ -655,6 +709,48 @@ public static class PluginsCore {
}
}
internal static async Task<bool> 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 (ActivePluginUpdates.Count == 0) {
return false;
}
return await UpdatePlugins(asfVersion, ActivePluginUpdates, updateChannel).ConfigureAwait(false);
}
internal static async Task<bool> UpdatePlugins(Version asfVersion, IReadOnlyCollection<IPluginUpdates> 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;
if (updateChannel == GlobalConfig.EUpdateChannel.None) {
return false;
}
ASF.ArchiLogger.LogGenericInfo(Strings.PluginUpdatesChecking);
IList<bool> pluginUpdates = await Utilities.InParallel(plugins.Select(plugin => UpdatePlugin(asfVersion, plugin, updateChannel.Value))).ConfigureAwait(false);
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")]
private static HashSet<Assembly>? LoadAssembliesFrom(string path) {
ArgumentException.ThrowIfNullOrEmpty(path);
@@ -667,6 +763,14 @@ public static class PluginsCore {
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 {
@@ -688,4 +792,102 @@ public static class PluginsCore {
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<bool> 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));
}
string pluginName;
try {
pluginName = plugin.Name;
ASF.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.PluginUpdateChecking, pluginName));
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, pluginName));
Progress<byte> progressReporter = new();
progressReporter.ProgressChanged += onProgressChanged;
BinaryResponse? response;
try {
response = await ASF.WebBrowser.UrlGetToBinary(releaseURL, progressReporter: progressReporter).ConfigureAwait(false);
} finally {
progressReporter.ProgressChanged -= 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, pluginName));
try {
await plugin.OnPluginUpdateFinished().ConfigureAwait(false);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
}
return true;
void onProgressChanged(object? sender, byte progressPercentage) {
ArgumentOutOfRangeException.ThrowIfGreaterThan(progressPercentage, 100);
Utilities.OnProgressChanged(pluginName, progressPercentage);
}
}
}