From 6f55098d7ccb7d87e9883344f6144ec89fd171ef Mon Sep 17 00:00:00 2001 From: maxisoft Date: Sun, 13 Oct 2024 22:11:05 +0200 Subject: [PATCH 01/10] Added logging on GithubPluginUpdater --- ASFFreeGames/ASFFreeGamesPlugin.cs | 5 ++- ASFFreeGames/Github/GithubPluginUpdater.cs | 37 +++++++++++++++++----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/ASFFreeGames/ASFFreeGamesPlugin.cs b/ASFFreeGames/ASFFreeGamesPlugin.cs index 1d7647e..4aa96dc 100644 --- a/ASFFreeGames/ASFFreeGamesPlugin.cs +++ b/ASFFreeGames/ASFFreeGamesPlugin.cs @@ -212,12 +212,15 @@ private async Task RemoveBot(Bot bot) { private void StartTimerIfNeeded() => CollectIntervalManager.StartTimerIfNeeded(); ~ASFFreeGamesPlugin() => CollectIntervalManager.Dispose(); - public readonly GithubPluginUpdater Updater = new(new Lazy(GetVersion)); + + #region IGitHubPluginUpdates implementation + private readonly GithubPluginUpdater Updater = new(new Lazy(GetVersion)); string IGitHubPluginUpdates.RepositoryName => GithubPluginUpdater.RepositoryName; bool IGitHubPluginUpdates.CanUpdate => Updater.CanUpdate; Task IGitHubPluginUpdates.GetTargetReleaseURL(Version asfVersion, string asfVariant, bool asfUpdate, bool stable, bool forced) => Updater.GetTargetReleaseURL(asfVersion, asfVariant, asfUpdate, stable, forced); + #endregion } #pragma warning restore CA1812 // ASF uses this class during runtime diff --git a/ASFFreeGames/Github/GithubPluginUpdater.cs b/ASFFreeGames/Github/GithubPluginUpdater.cs index c343d1b..6697919 100644 --- a/ASFFreeGames/Github/GithubPluginUpdater.cs +++ b/ASFFreeGames/Github/GithubPluginUpdater.cs @@ -14,16 +14,34 @@ public class GithubPluginUpdater(Lazy version) { private Version CurrentVersion => version.Value; + private static void LogGenericError(string message) { + if (string.IsNullOrEmpty(message)) { + return; + } + + ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericError($"{nameof(GithubPluginUpdater)}: {message}"); + } + + private static void LogGenericDebug(string message) { + if (string.IsNullOrEmpty(message)) { + return; + } + + ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericDebug($"{nameof(GithubPluginUpdater)}: {message}"); + } + public async Task GetTargetReleaseURL(Version asfVersion, string asfVariant, bool asfUpdate, bool stable, bool forced) { ArgumentNullException.ThrowIfNull(asfVersion); ArgumentException.ThrowIfNullOrEmpty(asfVariant); if (!CanUpdate) { + LogGenericDebug("CanUpdate is false"); + return null; } if (string.IsNullOrEmpty(RepositoryName)) { - //ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericError(Strings.FormatWarningFailedWithError(nameof(RepositoryName))); + LogGenericError("RepositoryName is null or empty"); return null; } @@ -31,15 +49,20 @@ public class GithubPluginUpdater(Lazy version) { ReleaseResponse? releaseResponse = await GitHubService.GetLatestRelease(RepositoryName).ConfigureAwait(false); if (releaseResponse == null) { + LogGenericError("GetLatestRelease returned null"); + return null; } if (releaseResponse.IsPreRelease) { + LogGenericError("GetLatestRelease returned pre-release"); + return null; } - if (stable && !((releaseResponse.PublishedAt - DateTime.UtcNow).Duration() > TimeSpan.FromHours(3))) { - // Skip updates that are too recent + if (stable && ((releaseResponse.PublishedAt - DateTime.UtcNow).Duration() < TimeSpan.FromHours(3))) { + LogGenericDebug("GetLatestRelease returned too recent"); + return null; } @@ -48,14 +71,12 @@ public class GithubPluginUpdater(Lazy version) { if (!forced && (CurrentVersion >= newVersion)) { // Allow same version to be re-updated when we're updating ASF release and more than one asset is found - potential compatibility difference if ((CurrentVersion > newVersion) || !asfUpdate || (releaseResponse.Assets.Count(static asset => asset.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) < 2)) { - //ASF.ArchiLogger.LogGenericInfo(Strings.FormatPluginUpdateNotFound(Name, Version, newVersion)); - return null; } } if (releaseResponse.Assets.Count == 0) { - //ASF.ArchiLogger.LogGenericWarning(Strings.FormatPluginUpdateNoAssetFound(Name, Version, newVersion)); + LogGenericError($"GetLatestRelease for version {newVersion} returned no assets"); return null; } @@ -63,12 +84,12 @@ public class GithubPluginUpdater(Lazy version) { ReleaseAsset? asset = releaseResponse.Assets.FirstOrDefault(static asset => asset.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) && (asset.Size > (1 << 18))); if ((asset == null) || !releaseResponse.Assets.Contains(asset)) { - //ASF.ArchiLogger.LogGenericWarning(Strings.FormatPluginUpdateNoAssetFound(Name, Version, newVersion)); + LogGenericError($"GetLatestRelease for version {newVersion} returned no valid assets"); return null; } - //.ArchiLogger.LogGenericInfo(Strings.FormatPluginUpdateFound(Name, Version, newVersion)); + LogGenericDebug($"GetLatestRelease for version {newVersion} returned asset {asset.Name} with url {asset.DownloadURL}"); return asset.DownloadURL; } From a5dd91b833edba41fb129374451328679df070b1 Mon Sep 17 00:00:00 2001 From: maxisoft Date: Tue, 3 Dec 2024 14:57:49 +0100 Subject: [PATCH 02/10] fixing #110 by removing ToArray() calls --- ASFFreeGames/Commands/FreeGamesCommand.cs | 15 ++++++++++++--- .../Strategies/RedlibListFreeGamesStrategy.cs | 9 ++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/ASFFreeGames/Commands/FreeGamesCommand.cs b/ASFFreeGames/Commands/FreeGamesCommand.cs index 0396f60..8805a15 100644 --- a/ASFFreeGames/Commands/FreeGamesCommand.cs +++ b/ASFFreeGames/Commands/FreeGamesCommand.cs @@ -156,9 +156,18 @@ public void Dispose() { private async ValueTask HandleInternalCollectCommand(Bot? bot, string[] args, CancellationToken cancellationToken) { Dictionary botMap = Context.Bots.ToDictionary(static b => b.BotName.Trim(), static b => b, StringComparer.InvariantCultureIgnoreCase); - Bot[] bots = args.Skip(2).Select(botName => botMap.GetValueOrDefault(botName.Trim())).Where(static b => b is not null).ToArray()!; + List bots = []; - if (bots.Length == 0) { + for (int i = 2; i < args.Length; i++) { + string botName = args[i].Trim(); + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (botMap.TryGetValue(botName, out Bot? savedBot) && savedBot is not null) { + bots.Add(savedBot); + } + } + + if (bots.Count == 0) { if (bot is null) { return null; } @@ -168,7 +177,7 @@ public void Dispose() { int collected = await CollectGames(bots, ECollectGameRequestSource.Scheduled, cancellationToken).ConfigureAwait(false); - return FormatBotResponse(bot, $"Collected a total of {collected} free game(s)" + (bots.Length > 1 ? $" on {bots.Length} bots" : $" on {bots.FirstOrDefault()?.BotName}")); + return FormatBotResponse(bot, $"Collected a total of {collected} free game(s)" + (bots.Count > 1 ? $" on {bots.Count} bots" : $" on {bots.FirstOrDefault()?.BotName}")); } private async Task SaveOptions(CancellationToken cancellationToken) { diff --git a/ASFFreeGames/FreeGames/Strategies/RedlibListFreeGamesStrategy.cs b/ASFFreeGames/FreeGames/Strategies/RedlibListFreeGamesStrategy.cs index a43df1b..9538c5c 100644 --- a/ASFFreeGames/FreeGames/Strategies/RedlibListFreeGamesStrategy.cs +++ b/ASFFreeGames/FreeGames/Strategies/RedlibListFreeGamesStrategy.cs @@ -155,7 +155,14 @@ private async Task> DoDownloadUsingInstance long dateMillis = date.ToUnixTimeMilliseconds(); - return entries.Select(entry => entry.ToRedditGameEntry(dateMillis)).ToArray(); + List redditGameEntries = []; + + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (RedlibGameEntry entry in entries) { + redditGameEntries.Add(entry.ToRedditGameEntry(dateMillis)); + } + + return redditGameEntries; } private async Task> DownloadUsingInstance(SimpleHttpClient client, Uri uri, uint retry, CancellationToken cancellationToken) { From c42f3568193569fe71282c818f68476b182d2b17 Mon Sep 17 00:00:00 2001 From: maxisoft Date: Tue, 3 Dec 2024 15:15:26 +0100 Subject: [PATCH 03/10] Use ThreadLocal instead of AsyncLocal + remove async for quick & dirty fix --- ASFFreeGames/ASFFreeGamesPlugin.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ASFFreeGames/ASFFreeGamesPlugin.cs b/ASFFreeGames/ASFFreeGamesPlugin.cs index 4aa96dc..a34723a 100644 --- a/ASFFreeGames/ASFFreeGamesPlugin.cs +++ b/ASFFreeGames/ASFFreeGamesPlugin.cs @@ -40,7 +40,7 @@ internal static PluginContext Context { } // ReSharper disable once InconsistentNaming - private static readonly AsyncLocal _context = new(); + private static readonly ThreadLocal _context = new(); private static CancellationToken CancellationToken => Context.CancellationToken; public string Name => StaticName; @@ -107,7 +107,7 @@ public async Task OnASFInit(IReadOnlyDictionary? additional public Task OnUpdateProceeding(Version currentVersion, Version newVersion) => Task.CompletedTask; - public async void CollectGamesOnClock(object? source) { + public void CollectGamesOnClock(object? source) { CollectIntervalManager.RandomlyChangeCollectInterval(source); if (!Context.Valid || ((Bots.Count > 0) && (Context.Bots.Count != Bots.Count))) { @@ -141,7 +141,9 @@ public async void CollectGamesOnClock(object? source) { if (!cts.IsCancellationRequested) { string cmd = $"FREEGAMES {FreeGamesCommand.CollectInternalCommandString} " + string.Join(' ', reorderedBots.Select(static bot => bot.BotName)); - await OnBotCommand(null!, EAccess.None, cmd, cmd.Split()).ConfigureAwait(false); +#pragma warning disable CS1998 + OnBotCommand(null!, EAccess.None, cmd, cmd.Split()).GetAwaiter().GetResult(); // TODO use async +#pragma warning restore CS1998 } } } From cd227b66145d70f5001278cded090af37ad7ac14 Mon Sep 17 00:00:00 2001 From: maxisoft Date: Tue, 3 Dec 2024 15:49:02 +0100 Subject: [PATCH 04/10] Use a workaround class To replace AsyncLocal --- ASFFreeGames/ASFFreeGamesPlugin.cs | 2 +- ASFFreeGames/Utils/Workarounds/AsyncLocal.cs | 84 ++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 ASFFreeGames/Utils/Workarounds/AsyncLocal.cs diff --git a/ASFFreeGames/ASFFreeGamesPlugin.cs b/ASFFreeGames/ASFFreeGamesPlugin.cs index a34723a..483b7da 100644 --- a/ASFFreeGames/ASFFreeGamesPlugin.cs +++ b/ASFFreeGames/ASFFreeGamesPlugin.cs @@ -40,7 +40,7 @@ internal static PluginContext Context { } // ReSharper disable once InconsistentNaming - private static readonly ThreadLocal _context = new(); + private static readonly Utils.Workarounds.AsyncLocal _context = new(); private static CancellationToken CancellationToken => Context.CancellationToken; public string Name => StaticName; diff --git a/ASFFreeGames/Utils/Workarounds/AsyncLocal.cs b/ASFFreeGames/Utils/Workarounds/AsyncLocal.cs new file mode 100644 index 0000000..737df59 --- /dev/null +++ b/ASFFreeGames/Utils/Workarounds/AsyncLocal.cs @@ -0,0 +1,84 @@ +using System; +using System.Reflection; + +namespace Maxisoft.ASF.Utils.Workarounds; + +public sealed class AsyncLocal { + // ReSharper disable once StaticMemberInGenericType + private static readonly Type? AsyncLocalType; + +#pragma warning disable CA1810 + static AsyncLocal() { +#pragma warning restore CA1810 + try { + AsyncLocalType = Type.GetType("System.Threading.AsyncLocal`1") + ?.MakeGenericType(typeof(T)); + } + catch (InvalidOperationException) { + // ignore + } + + try { + AsyncLocalType ??= Type.GetType("System.Threading.AsyncLocal") + ?.MakeGenericType(typeof(T)); + } + + catch (InvalidOperationException) { + // ignore + } + } + + private readonly object? Delegate; + private T? NonSafeValue; + + /// Instantiates an instance that does not receive change notifications. + public AsyncLocal() { + if (AsyncLocalType is not null) { + try { + Delegate = Activator.CreateInstance(AsyncLocalType)!; + } + catch (Exception) { + // ignored + } + } + } + + /// Gets or sets the value of the ambient data. + /// The value of the ambient data. If no value has been set, the returned value is default(T). + public T? Value { + get { + if (Delegate is not null) { + try { + PropertyInfo? property = Delegate.GetType().GetProperty("Value"); + + if (property is not null) { + return (T) property.GetValue(Delegate)!; + } + } + catch (Exception) { + // ignored + } + } + + return (T) NonSafeValue!; + } + set { + if (Delegate is not null) { + try { + PropertyInfo? property = Delegate.GetType().GetProperty("Value"); + + if (property is not null) { + property.SetValue(Delegate, value); + + return; + } + } + catch (Exception) { + // ignored + } + } + + NonSafeValue = value; + } + } +} From 9529b5017f7c1e75721076ff518cce93cd151211 Mon Sep 17 00:00:00 2001 From: maxisoft Date: Tue, 3 Dec 2024 16:21:39 +0100 Subject: [PATCH 05/10] Remove AsArray calls --- ASFFreeGames/Redlib/Instances/RedlibInstanceList.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ASFFreeGames/Redlib/Instances/RedlibInstanceList.cs b/ASFFreeGames/Redlib/Instances/RedlibInstanceList.cs index fe89a4e..7ec6264 100644 --- a/ASFFreeGames/Redlib/Instances/RedlibInstanceList.cs +++ b/ASFFreeGames/Redlib/Instances/RedlibInstanceList.cs @@ -89,10 +89,10 @@ internal static List ParseUrls(JsonNode json) { return []; } - List uris = new(instances.AsArray().Count); + List uris = new(((JsonArray) instances).Count); // ReSharper disable once LoopCanBePartlyConvertedToQuery - foreach (JsonNode? instance in instances.AsArray()) { + foreach (JsonNode? instance in (JsonArray) instances) { JsonNode? url = instance?["url"]; if (Uri.TryCreate(url?.GetValue() ?? "", UriKind.Absolute, out Uri? instanceUri) && instanceUri.Scheme is "http" or "https") { From 3aa01e5cd397f68b5d0ffee1a6ef21cf4e99e828 Mon Sep 17 00:00:00 2001 From: maxisoft Date: Tue, 3 Dec 2024 19:01:46 +0100 Subject: [PATCH 06/10] Bugfix RedditHelper for issue #110 --- ASFFreeGames/Reddit/RedditHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ASFFreeGames/Reddit/RedditHelper.cs b/ASFFreeGames/Reddit/RedditHelper.cs index c5f2128..97597da 100644 --- a/ASFFreeGames/Reddit/RedditHelper.cs +++ b/ASFFreeGames/Reddit/RedditHelper.cs @@ -43,7 +43,7 @@ IReadOnlyCollection returnValue() { } // ReSharper disable once LoopCanBePartlyConvertedToQuery - foreach (JsonNode? comment in children.AsArray()) { + foreach (JsonNode? comment in (JsonArray) children) { JsonNode? commentData = comment?["data"]; if (commentData is null) { From ca6c40766016880b890763acbc8038b3aa683081 Mon Sep 17 00:00:00 2001 From: maxisoft Date: Tue, 3 Dec 2024 19:02:28 +0100 Subject: [PATCH 07/10] BugFix ASFFreeGamesOptionsLoader #110 --- .../Configurations/ASFFreeGamesOptionsLoader.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ASFFreeGames/Configurations/ASFFreeGamesOptionsLoader.cs b/ASFFreeGames/Configurations/ASFFreeGamesOptionsLoader.cs index 93e8835..805bebc 100644 --- a/ASFFreeGames/Configurations/ASFFreeGamesOptionsLoader.cs +++ b/ASFFreeGames/Configurations/ASFFreeGamesOptionsLoader.cs @@ -59,13 +59,12 @@ public static async Task Save(ASFFreeGamesOptions options, CancellationToken can try { #pragma warning disable CAC001 #pragma warning disable CA2007 - - // Use FileOptions.Asynchronous when creating a file stream for async operations - await using FileStream fs = new(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 4096, FileOptions.Asynchronous); + await using FileStream fs = new(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); #pragma warning restore CA2007 #pragma warning restore CAC001 - using IMemoryOwner buffer = MemoryPool.Shared.Rent(checked(fs.Length > 0 ? (int) fs.Length + 1 : 1 << 15)); - int read = await fs.ReadAsync(buffer.Memory, cancellationToken).ConfigureAwait(false); + byte[] buffer = new byte[fs.Length > 0 ? (int) fs.Length + 1 : 1 << 15]; + + int read = await fs.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); try { fs.Position = 0; @@ -76,7 +75,8 @@ public static async Task Save(ASFFreeGamesOptions options, CancellationToken can catch (Exception) { fs.Position = 0; - await fs.WriteAsync(buffer.Memory[..read], cancellationToken).ConfigureAwait(false); + + await fs.WriteAsync(((ReadOnlyMemory) buffer)[..read], cancellationToken).ConfigureAwait(false); fs.SetLength(read); throw; From 57116da7dfd65da49e0af544ce9031c5dfa18495 Mon Sep 17 00:00:00 2001 From: maxisoft Date: Tue, 3 Dec 2024 19:02:47 +0100 Subject: [PATCH 08/10] Bump version to 1.8.1 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 8ee0ef4..e8ce76b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ ASFFreeGames - 1.8.0.0 + 1.8.1.0 net8.0 From 3073fc37b9e3971569de45460ed50f411222d3aa Mon Sep 17 00:00:00 2001 From: maxisoft Date: Tue, 3 Dec 2024 19:11:50 +0100 Subject: [PATCH 09/10] remove severity of CA1859 --- .editorconfig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.editorconfig b/.editorconfig index 7fc0707..7b0a17e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,6 +10,9 @@ indent_style = tab insert_final_newline = true trim_trailing_whitespace = true +[*.{cs,vb}] + dotnet_diagnostic.CA1859.severity = none + ############################### # C# Coding Conventions # ############################### From 2d49d753bee6592722aff4a8b6e48e109e2e867c Mon Sep 17 00:00:00 2001 From: maxisoft Date: Wed, 4 Dec 2024 10:08:13 +0100 Subject: [PATCH 10/10] Remove windows for publish actions as it's buggy right now --- .editorconfig | 2 +- .github/workflows/publish.yml | 13 +++++++------ ASFFreeGames/ASFFreeGamesPlugin.cs | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.editorconfig b/.editorconfig index 7b0a17e..fb9b357 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,7 @@ insert_final_newline = true trim_trailing_whitespace = true [*.{cs,vb}] - dotnet_diagnostic.CA1859.severity = none +dotnet_diagnostic.CA1859.severity = none ############################### # C# Coding Conventions # diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3278f7d..e070d13 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,7 +16,11 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, ubuntu-latest, windows-latest] + os: [ + macos-latest, + ubuntu-latest, + #windows-latest + ] runs-on: ${{ matrix.os }} @@ -190,13 +194,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4.2.2 - # TODO: It'd be perfect if we could match final artifacts to the platform they target, so e.g. linux build comes from the linux machine - # However, that is currently impossible due to https://github.com/dotnet/msbuild/issues/3897 - # Therefore, we'll (sadly) pull artifacts from Windows machine only for now - - name: Download generic artifact from windows-latest + - name: Download generic artifact from ubuntu-latest uses: actions/download-artifact@v4.1.8 with: - name: windows-latest_${{ env.PLUGIN_NAME }}-generic + name: ubuntu-latest_${{ env.PLUGIN_NAME }}-generic path: out - name: Unzip and copy generic artifact diff --git a/ASFFreeGames/ASFFreeGamesPlugin.cs b/ASFFreeGames/ASFFreeGamesPlugin.cs index 483b7da..0515d56 100644 --- a/ASFFreeGames/ASFFreeGamesPlugin.cs +++ b/ASFFreeGames/ASFFreeGamesPlugin.cs @@ -107,7 +107,7 @@ public async Task OnASFInit(IReadOnlyDictionary? additional public Task OnUpdateProceeding(Version currentVersion, Version newVersion) => Task.CompletedTask; - public void CollectGamesOnClock(object? source) { + public async void CollectGamesOnClock(object? source) { CollectIntervalManager.RandomlyChangeCollectInterval(source); if (!Context.Valid || ((Bots.Count > 0) && (Context.Bots.Count != Bots.Count))) { @@ -142,7 +142,7 @@ public void CollectGamesOnClock(object? source) { if (!cts.IsCancellationRequested) { string cmd = $"FREEGAMES {FreeGamesCommand.CollectInternalCommandString} " + string.Join(' ', reorderedBots.Select(static bot => bot.BotName)); #pragma warning disable CS1998 - OnBotCommand(null!, EAccess.None, cmd, cmd.Split()).GetAwaiter().GetResult(); // TODO use async + await OnBotCommand(null!, EAccess.None, cmd, cmd.Split()).ConfigureAwait(false); #pragma warning restore CS1998 } }