diff --git a/ArchiSteamFarm.CustomPlugins.ExamplePlugin/CatAPI.cs b/ArchiSteamFarm.CustomPlugins.ExamplePlugin/CatAPI.cs index 9ad415cc4..aabdbf82c 100644 --- a/ArchiSteamFarm.CustomPlugins.ExamplePlugin/CatAPI.cs +++ b/ArchiSteamFarm.CustomPlugins.ExamplePlugin/CatAPI.cs @@ -22,6 +22,7 @@ using System; using System.Collections.Immutable; using System.Linq; +using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Web; using ArchiSteamFarm.Web.Responses; @@ -34,12 +35,12 @@ namespace ArchiSteamFarm.CustomPlugins.ExamplePlugin; internal static class CatAPI { private const string URL = "https://api.thecatapi.com"; - internal static async Task GetRandomCatURL(WebBrowser webBrowser) { + internal static async Task GetRandomCatURL(WebBrowser webBrowser, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(webBrowser); Uri request = new($"{URL}/v1/images/search"); - ObjectResponse>? response = await webBrowser.UrlGetToJsonObject>(request).ConfigureAwait(false); + ObjectResponse>? response = await webBrowser.UrlGetToJsonObject>(request, cancellationToken: cancellationToken).ConfigureAwait(false); return response?.Content?.FirstOrDefault()?.URL; } diff --git a/ArchiSteamFarm/Helpers/CrossProcessFileBasedSemaphore.cs b/ArchiSteamFarm/Helpers/CrossProcessFileBasedSemaphore.cs index 900b16ae7..7ef8db4ca 100644 --- a/ArchiSteamFarm/Helpers/CrossProcessFileBasedSemaphore.cs +++ b/ArchiSteamFarm/Helpers/CrossProcessFileBasedSemaphore.cs @@ -77,8 +77,8 @@ internal sealed class CrossProcessFileBasedSemaphore : IAsyncDisposable, ICrossP LocalSemaphore.Release(); } - async Task ICrossProcessSemaphore.WaitAsync() { - await LocalSemaphore.WaitAsync().ConfigureAwait(false); + async Task ICrossProcessSemaphore.WaitAsync(CancellationToken cancellationToken) { + await LocalSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); bool success = false; @@ -99,7 +99,7 @@ internal sealed class CrossProcessFileBasedSemaphore : IAsyncDisposable, ICrossP return; } } catch (IOException) { - await Task.Delay(SpinLockDelay).ConfigureAwait(false); + await Task.Delay(SpinLockDelay, cancellationToken).ConfigureAwait(false); } } } finally { @@ -109,10 +109,10 @@ internal sealed class CrossProcessFileBasedSemaphore : IAsyncDisposable, ICrossP } } - async Task ICrossProcessSemaphore.WaitAsync(int millisecondsTimeout) { + async Task ICrossProcessSemaphore.WaitAsync(int millisecondsTimeout, CancellationToken cancellationToken) { Stopwatch stopwatch = Stopwatch.StartNew(); - if (!await LocalSemaphore.WaitAsync(millisecondsTimeout).ConfigureAwait(false)) { + if (!await LocalSemaphore.WaitAsync(millisecondsTimeout, cancellationToken).ConfigureAwait(false)) { stopwatch.Stop(); return false; @@ -149,7 +149,7 @@ internal sealed class CrossProcessFileBasedSemaphore : IAsyncDisposable, ICrossP return false; } - await Task.Delay(SpinLockDelay).ConfigureAwait(false); + await Task.Delay(SpinLockDelay, cancellationToken).ConfigureAwait(false); millisecondsTimeout -= SpinLockDelay; } } diff --git a/ArchiSteamFarm/Helpers/ICrossProcessSemaphore.cs b/ArchiSteamFarm/Helpers/ICrossProcessSemaphore.cs index ea9f75156..32ed9bef4 100644 --- a/ArchiSteamFarm/Helpers/ICrossProcessSemaphore.cs +++ b/ArchiSteamFarm/Helpers/ICrossProcessSemaphore.cs @@ -19,6 +19,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -27,6 +28,6 @@ namespace ArchiSteamFarm.Helpers; [PublicAPI] public interface ICrossProcessSemaphore { void Release(); - Task WaitAsync(); - Task WaitAsync(int millisecondsTimeout); + Task WaitAsync(CancellationToken cancellationToken = default); + Task WaitAsync(int millisecondsTimeout, CancellationToken cancellationToken = default); } diff --git a/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs b/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs index 1fa92ebd9..8c374eb7a 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/GitHubController.cs @@ -24,6 +24,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Net; +using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.IPC.Responses; using ArchiSteamFarm.Localization; @@ -44,7 +45,9 @@ public sealed class GitHubController : ArchiController { [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)] [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.ServiceUnavailable)] public async Task> GitHubReleaseGet() { - GitHub.ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(false).ConfigureAwait(false); + CancellationToken cancellationToken = HttpContext.RequestAborted; + + GitHub.ReleaseResponse? releaseResponse = await GitHub.GetLatestRelease(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))); } @@ -62,11 +65,13 @@ public sealed class GitHubController : ArchiController { public async Task> GitHubReleaseGet(string version) { ArgumentException.ThrowIfNullOrEmpty(version); + CancellationToken cancellationToken = HttpContext.RequestAborted; + GitHub.ReleaseResponse? releaseResponse; switch (version.ToUpperInvariant()) { case "LATEST": - releaseResponse = await GitHub.GetLatestRelease().ConfigureAwait(false); + releaseResponse = await GitHub.GetLatestRelease(cancellationToken: cancellationToken).ConfigureAwait(false); break; default: @@ -74,7 +79,7 @@ public sealed class GitHubController : ArchiController { return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(version)))); } - releaseResponse = await GitHub.GetRelease(parsedVersion.ToString(4)).ConfigureAwait(false); + releaseResponse = await GitHub.GetRelease(parsedVersion.ToString(4), cancellationToken).ConfigureAwait(false); break; } @@ -95,7 +100,9 @@ public sealed class GitHubController : ArchiController { public async Task> GitHubWikiHistoryGet(string page) { ArgumentException.ThrowIfNullOrEmpty(page); - Dictionary? revisions = await GitHub.GetWikiHistory(page).ConfigureAwait(false); + CancellationToken cancellationToken = HttpContext.RequestAborted; + + Dictionary? revisions = await GitHub.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))); } @@ -114,7 +121,9 @@ public sealed class GitHubController : ArchiController { public async Task> GitHubWikiPageGet(string page, [FromQuery] string? revision = null) { ArgumentException.ThrowIfNullOrEmpty(page); - string? html = await GitHub.GetWikiPage(page, revision).ConfigureAwait(false); + CancellationToken cancellationToken = HttpContext.RequestAborted; + + string? html = await GitHub.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/Steam/Integration/ArchiWebHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs index 4ecb9e2a4..e88106afd 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs @@ -795,7 +795,7 @@ public sealed class ArchiWebHandler : IDisposable { } [PublicAPI] - public async Task UrlGetToHtmlDocumentWithSession(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) { + public async Task UrlGetToHtmlDocumentWithSession(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ArgumentOutOfRangeException.ThrowIfZero(maxTries); ArgumentOutOfRangeException.ThrowIfNegative(rateLimitingDelay); @@ -810,7 +810,7 @@ public sealed class ArchiWebHandler : IDisposable { if (sessionExpired.GetValueOrDefault(true)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlGetToHtmlDocumentWithSession(request, headers, referer, requestOptions, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlGetToHtmlDocumentWithSession(request, headers, referer, requestOptions, true, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -820,7 +820,7 @@ public sealed class ArchiWebHandler : IDisposable { } } else { // If session refresh is already in progress, just wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); + await SessionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); SessionSemaphore.Release(); } @@ -828,7 +828,7 @@ public sealed class ArchiWebHandler : IDisposable { byte connectionTimeout = ASF.GlobalConfig?.ConnectionTimeout ?? GlobalConfig.DefaultConnectionTimeout; for (byte i = 0; (i < connectionTimeout) && !Initialized && Bot.IsConnectedAndLoggedOn; i++) { - await Task.Delay(1000).ConfigureAwait(false); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); } if (!Initialized) { @@ -842,7 +842,7 @@ public sealed class ArchiWebHandler : IDisposable { Uri host = new(request.GetLeftPart(UriPartial.Authority)); // ReSharper disable once AccessToModifiedClosure - evaluated fully before returning - HtmlDocumentResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlGetToHtmlDocument(request, headers, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false); + HtmlDocumentResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlGetToHtmlDocument(request, headers, referer, requestOptions, maxTries, rateLimitingDelay, cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); if (response == null) { return null; @@ -850,7 +850,7 @@ public sealed class ArchiWebHandler : IDisposable { if (IsSessionExpiredUri(response.FinalUri)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlGetToHtmlDocumentWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlGetToHtmlDocumentWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -870,14 +870,14 @@ public sealed class ArchiWebHandler : IDisposable { return null; } - return await UrlGetToHtmlDocumentWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false); + return await UrlGetToHtmlDocumentWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh, cancellationToken).ConfigureAwait(false); } return response; } [PublicAPI] - public async Task?> UrlGetToJsonObjectWithSession(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) { + public async Task?> UrlGetToJsonObjectWithSession(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ArgumentOutOfRangeException.ThrowIfZero(maxTries); ArgumentOutOfRangeException.ThrowIfNegative(rateLimitingDelay); @@ -892,7 +892,7 @@ public sealed class ArchiWebHandler : IDisposable { if (sessionExpired.GetValueOrDefault(true)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlGetToJsonObjectWithSession(request, headers, referer, requestOptions, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlGetToJsonObjectWithSession(request, headers, referer, requestOptions, true, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -902,7 +902,7 @@ public sealed class ArchiWebHandler : IDisposable { } } else { // If session refresh is already in progress, just wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); + await SessionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); SessionSemaphore.Release(); } @@ -910,7 +910,7 @@ public sealed class ArchiWebHandler : IDisposable { byte connectionTimeout = ASF.GlobalConfig?.ConnectionTimeout ?? GlobalConfig.DefaultConnectionTimeout; for (byte i = 0; (i < connectionTimeout) && !Initialized && Bot.IsConnectedAndLoggedOn; i++) { - await Task.Delay(1000).ConfigureAwait(false); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); } if (!Initialized) { @@ -924,7 +924,7 @@ public sealed class ArchiWebHandler : IDisposable { Uri host = new(request.GetLeftPart(UriPartial.Authority)); // ReSharper disable once AccessToModifiedClosure - evaluated fully before returning - ObjectResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlGetToJsonObject(request, headers, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false); + ObjectResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlGetToJsonObject(request, headers, referer, requestOptions, maxTries, rateLimitingDelay, cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); if (response == null) { return default(ObjectResponse?); @@ -932,7 +932,7 @@ public sealed class ArchiWebHandler : IDisposable { if (IsSessionExpiredUri(response.FinalUri)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlGetToJsonObjectWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlGetToJsonObjectWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -952,14 +952,14 @@ public sealed class ArchiWebHandler : IDisposable { return null; } - return await UrlGetToJsonObjectWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false); + return await UrlGetToJsonObjectWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh, cancellationToken).ConfigureAwait(false); } return response; } [PublicAPI] - public async Task UrlHeadWithSession(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) { + public async Task UrlHeadWithSession(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ArgumentOutOfRangeException.ThrowIfZero(maxTries); ArgumentOutOfRangeException.ThrowIfNegative(rateLimitingDelay); @@ -974,7 +974,7 @@ public sealed class ArchiWebHandler : IDisposable { if (sessionExpired.GetValueOrDefault(true)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlHeadWithSession(request, headers, referer, requestOptions, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlHeadWithSession(request, headers, referer, requestOptions, true, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -984,7 +984,7 @@ public sealed class ArchiWebHandler : IDisposable { } } else { // If session refresh is already in progress, just wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); + await SessionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); SessionSemaphore.Release(); } @@ -992,7 +992,7 @@ public sealed class ArchiWebHandler : IDisposable { byte connectionTimeout = ASF.GlobalConfig?.ConnectionTimeout ?? GlobalConfig.DefaultConnectionTimeout; for (byte i = 0; (i < connectionTimeout) && !Initialized && Bot.IsConnectedAndLoggedOn; i++) { - await Task.Delay(1000).ConfigureAwait(false); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); } if (!Initialized) { @@ -1006,7 +1006,7 @@ public sealed class ArchiWebHandler : IDisposable { Uri host = new(request.GetLeftPart(UriPartial.Authority)); // ReSharper disable once AccessToModifiedClosure - evaluated fully before returning - BasicResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlHead(request, headers, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false); + BasicResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlHead(request, headers, referer, requestOptions, maxTries, rateLimitingDelay, cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); if (response == null) { return false; @@ -1014,7 +1014,7 @@ public sealed class ArchiWebHandler : IDisposable { if (IsSessionExpiredUri(response.FinalUri)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlHeadWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlHeadWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1034,14 +1034,14 @@ public sealed class ArchiWebHandler : IDisposable { return false; } - return await UrlHeadWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false); + return await UrlHeadWithSession(request, headers, referer, requestOptions, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh, cancellationToken).ConfigureAwait(false); } return true; } [PublicAPI] - public async Task UrlPostToHtmlDocumentWithSession(Uri request, IReadOnlyCollection>? headers = null, IDictionary? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) { + public async Task UrlPostToHtmlDocumentWithSession(Uri request, IReadOnlyCollection>? headers = null, IDictionary? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); if (!Enum.IsDefined(session)) { @@ -1061,7 +1061,7 @@ public sealed class ArchiWebHandler : IDisposable { if (sessionExpired.GetValueOrDefault(true)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlPostToHtmlDocumentWithSession(request, headers, data, referer, requestOptions, session, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlPostToHtmlDocumentWithSession(request, headers, data, referer, requestOptions, session, true, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1071,7 +1071,7 @@ public sealed class ArchiWebHandler : IDisposable { } } else { // If session refresh is already in progress, just wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); + await SessionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); SessionSemaphore.Release(); } @@ -1079,7 +1079,7 @@ public sealed class ArchiWebHandler : IDisposable { byte connectionTimeout = ASF.GlobalConfig?.ConnectionTimeout ?? GlobalConfig.DefaultConnectionTimeout; for (byte i = 0; (i < connectionTimeout) && !Initialized && Bot.IsConnectedAndLoggedOn; i++) { - await Task.Delay(1000).ConfigureAwait(false); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); } if (!Initialized) { @@ -1116,7 +1116,7 @@ public sealed class ArchiWebHandler : IDisposable { } // ReSharper disable once AccessToModifiedClosure - evaluated fully before returning - HtmlDocumentResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPostToHtmlDocument(request, headers, data, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false); + HtmlDocumentResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPostToHtmlDocument(request, headers, data, referer, requestOptions, maxTries, rateLimitingDelay, cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); if (response == null) { return null; @@ -1124,7 +1124,7 @@ public sealed class ArchiWebHandler : IDisposable { if (IsSessionExpiredUri(response.FinalUri)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlPostToHtmlDocumentWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlPostToHtmlDocumentWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1144,14 +1144,14 @@ public sealed class ArchiWebHandler : IDisposable { return null; } - return await UrlPostToHtmlDocumentWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false); + return await UrlPostToHtmlDocumentWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh, cancellationToken).ConfigureAwait(false); } return response; } [PublicAPI] - public async Task?> UrlPostToJsonObjectWithSession(Uri request, IReadOnlyCollection>? headers = null, IDictionary? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) { + public async Task?> UrlPostToJsonObjectWithSession(Uri request, IReadOnlyCollection>? headers = null, IDictionary? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); if (!Enum.IsDefined(session)) { @@ -1171,7 +1171,7 @@ public sealed class ArchiWebHandler : IDisposable { if (sessionExpired.GetValueOrDefault(true)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlPostToJsonObjectWithSession(request, headers, data, referer, requestOptions, session, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlPostToJsonObjectWithSession(request, headers, data, referer, requestOptions, session, true, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1181,7 +1181,7 @@ public sealed class ArchiWebHandler : IDisposable { } } else { // If session refresh is already in progress, just wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); + await SessionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); SessionSemaphore.Release(); } @@ -1189,7 +1189,7 @@ public sealed class ArchiWebHandler : IDisposable { byte connectionTimeout = ASF.GlobalConfig?.ConnectionTimeout ?? GlobalConfig.DefaultConnectionTimeout; for (byte i = 0; (i < connectionTimeout) && !Initialized && Bot.IsConnectedAndLoggedOn; i++) { - await Task.Delay(1000).ConfigureAwait(false); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); } if (!Initialized) { @@ -1226,7 +1226,7 @@ public sealed class ArchiWebHandler : IDisposable { } // ReSharper disable once AccessToModifiedClosure - evaluated fully before returning - ObjectResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPostToJsonObject>(request, headers, data, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false); + ObjectResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPostToJsonObject>(request, headers, data, referer, requestOptions, maxTries, rateLimitingDelay, cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); if (response == null) { return null; @@ -1234,7 +1234,7 @@ public sealed class ArchiWebHandler : IDisposable { if (IsSessionExpiredUri(response.FinalUri)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlPostToJsonObjectWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlPostToJsonObjectWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1254,14 +1254,14 @@ public sealed class ArchiWebHandler : IDisposable { return null; } - return await UrlPostToJsonObjectWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false); + return await UrlPostToJsonObjectWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh, cancellationToken).ConfigureAwait(false); } return response; } [PublicAPI] - public async Task?> UrlPostToJsonObjectWithSession(Uri request, IReadOnlyCollection>? headers = null, ICollection>? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) { + public async Task?> UrlPostToJsonObjectWithSession(Uri request, IReadOnlyCollection>? headers = null, ICollection>? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); if (!Enum.IsDefined(session)) { @@ -1281,7 +1281,7 @@ public sealed class ArchiWebHandler : IDisposable { if (sessionExpired.GetValueOrDefault(true)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlPostToJsonObjectWithSession(request, headers, data, referer, requestOptions, session, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlPostToJsonObjectWithSession(request, headers, data, referer, requestOptions, session, true, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1291,7 +1291,7 @@ public sealed class ArchiWebHandler : IDisposable { } } else { // If session refresh is already in progress, just wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); + await SessionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); SessionSemaphore.Release(); } @@ -1299,7 +1299,7 @@ public sealed class ArchiWebHandler : IDisposable { byte connectionTimeout = ASF.GlobalConfig?.ConnectionTimeout ?? GlobalConfig.DefaultConnectionTimeout; for (byte i = 0; (i < connectionTimeout) && !Initialized && Bot.IsConnectedAndLoggedOn; i++) { - await Task.Delay(1000).ConfigureAwait(false); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); } if (!Initialized) { @@ -1339,7 +1339,7 @@ public sealed class ArchiWebHandler : IDisposable { } // ReSharper disable once AccessToModifiedClosure - evaluated fully before returning - ObjectResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPostToJsonObject>>(request, headers, data, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false); + ObjectResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPostToJsonObject>>(request, headers, data, referer, requestOptions, maxTries, rateLimitingDelay, cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); if (response == null) { return null; @@ -1347,7 +1347,7 @@ public sealed class ArchiWebHandler : IDisposable { if (IsSessionExpiredUri(response.FinalUri)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlPostToJsonObjectWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlPostToJsonObjectWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1367,14 +1367,14 @@ public sealed class ArchiWebHandler : IDisposable { return null; } - return await UrlPostToJsonObjectWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false); + return await UrlPostToJsonObjectWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh, cancellationToken).ConfigureAwait(false); } return response; } [PublicAPI] - public async Task UrlPostWithSession(Uri request, IReadOnlyCollection>? headers = null, IDictionary? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true) { + public async Task UrlPostWithSession(Uri request, IReadOnlyCollection>? headers = null, IDictionary? data = null, Uri? referer = null, WebBrowser.ERequestOptions requestOptions = WebBrowser.ERequestOptions.None, ESession session = ESession.Lowercase, bool checkSessionPreemptively = true, byte maxTries = WebBrowser.MaxTries, int rateLimitingDelay = 0, bool allowSessionRefresh = true, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); if (!Enum.IsDefined(session)) { @@ -1394,7 +1394,7 @@ public sealed class ArchiWebHandler : IDisposable { if (sessionExpired.GetValueOrDefault(true)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlPostWithSession(request, headers, data, referer, requestOptions, session, true, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlPostWithSession(request, headers, data, referer, requestOptions, session, true, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1404,7 +1404,7 @@ public sealed class ArchiWebHandler : IDisposable { } } else { // If session refresh is already in progress, just wait for it - await SessionSemaphore.WaitAsync().ConfigureAwait(false); + await SessionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); SessionSemaphore.Release(); } @@ -1412,7 +1412,7 @@ public sealed class ArchiWebHandler : IDisposable { byte connectionTimeout = ASF.GlobalConfig?.ConnectionTimeout ?? GlobalConfig.DefaultConnectionTimeout; for (byte i = 0; (i < connectionTimeout) && !Initialized && Bot.IsConnectedAndLoggedOn; i++) { - await Task.Delay(1000).ConfigureAwait(false); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); } if (!Initialized) { @@ -1449,7 +1449,7 @@ public sealed class ArchiWebHandler : IDisposable { } // ReSharper disable once AccessToModifiedClosure - evaluated fully before returning - BasicResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPost(request, headers, data, referer, requestOptions, maxTries, rateLimitingDelay).ConfigureAwait(false)).ConfigureAwait(false); + BasicResponse? response = await WebLimitRequest(host, async () => await WebBrowser.UrlPost(request, headers, data, referer, requestOptions, maxTries, rateLimitingDelay, cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); if (response == null) { return false; @@ -1457,7 +1457,7 @@ public sealed class ArchiWebHandler : IDisposable { if (IsSessionExpiredUri(response.FinalUri)) { if (allowSessionRefresh && await RefreshSession().ConfigureAwait(false)) { - return await UrlPostWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, false).ConfigureAwait(false); + return await UrlPostWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, false, cancellationToken).ConfigureAwait(false); } Bot.ArchiLogger.LogGenericWarning(Strings.WarningFailed); @@ -1477,14 +1477,14 @@ public sealed class ArchiWebHandler : IDisposable { return false; } - return await UrlPostWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh).ConfigureAwait(false); + return await UrlPostWithSession(request, headers, data, referer, requestOptions, session, checkSessionPreemptively, maxTries, rateLimitingDelay, allowSessionRefresh, cancellationToken).ConfigureAwait(false); } return true; } [PublicAPI] - public static async Task WebLimitRequest(Uri service, Func> function) { + public static async Task WebLimitRequest(Uri service, Func> function, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(service); ArgumentNullException.ThrowIfNull(function); @@ -1508,15 +1508,16 @@ public sealed class ArchiWebHandler : IDisposable { } // Sending a request opens a new connection - await limiters.OpenConnectionsSemaphore.WaitAsync().ConfigureAwait(false); + await limiters.OpenConnectionsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { // It also increases number of requests - await limiters.RateLimitingSemaphore.WaitAsync().ConfigureAwait(false); + await limiters.RateLimitingSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); // We release rate-limiter semaphore regardless of our task completion, since we use that one only to guarantee rate-limiting of their creation Utilities.InBackground( async () => { + // ReSharper disable once MethodSupportsCancellation - we must always wait given time before releasing semaphore await Task.Delay(WebLimiterDelay).ConfigureAwait(false); limiters.RateLimitingSemaphore.Release(); } diff --git a/ArchiSteamFarm/Web/GitHub.cs b/ArchiSteamFarm/Web/GitHub.cs index 32df3e8bb..87df42ede 100644 --- a/ArchiSteamFarm/Web/GitHub.cs +++ b/ArchiSteamFarm/Web/GitHub.cs @@ -27,6 +27,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Threading; using System.Threading.Tasks; using AngleSharp.Dom; using ArchiSteamFarm.Core; @@ -40,27 +41,27 @@ using Newtonsoft.Json; namespace ArchiSteamFarm.Web; internal static class GitHub { - internal static async Task GetLatestRelease(bool stable = true) { + internal static async Task GetLatestRelease(bool stable = true, CancellationToken cancellationToken = default) { Uri request = new($"{SharedInfo.GithubReleaseURL}{(stable ? "/latest" : "?per_page=1")}"); if (stable) { - return await GetReleaseFromURL(request).ConfigureAwait(false); + return await GetReleaseFromURL(request, cancellationToken).ConfigureAwait(false); } - ImmutableList? response = await GetReleasesFromURL(request).ConfigureAwait(false); + ImmutableList? response = await GetReleasesFromURL(request, cancellationToken).ConfigureAwait(false); return response?.FirstOrDefault(); } - internal static async Task GetRelease(string version) { + internal static async Task GetRelease(string version, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(version); Uri request = new($"{SharedInfo.GithubReleaseURL}/tags/{version}"); - return await GetReleaseFromURL(request).ConfigureAwait(false); + return await GetReleaseFromURL(request, cancellationToken).ConfigureAwait(false); } - internal static async Task?> GetWikiHistory(string page) { + internal static async Task?> GetWikiHistory(string page, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(page); if (ASF.WebBrowser == null) { @@ -69,7 +70,7 @@ internal static class GitHub { Uri request = new($"{SharedInfo.ProjectURL}/wiki/{page}/_history"); - using HtmlDocumentResponse? response = await ASF.WebBrowser.UrlGetToHtmlDocument(request, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors).ConfigureAwait(false); + using HtmlDocumentResponse? response = await ASF.WebBrowser.UrlGetToHtmlDocument(request, requestOptions: WebBrowser.ERequestOptions.ReturnClientErrors | WebBrowser.ERequestOptions.AllowInvalidBodyOnErrors, cancellationToken: cancellationToken).ConfigureAwait(false); if (response?.StatusCode.IsClientErrorCode() == true) { return response.StatusCode switch { @@ -131,7 +132,7 @@ internal static class GitHub { return result; } - internal static async Task GetWikiPage(string page, string? revision = null) { + internal static async Task GetWikiPage(string page, string? revision = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(page); if (ASF.WebBrowser == null) { @@ -140,7 +141,7 @@ internal static class GitHub { Uri request = new($"{SharedInfo.ProjectURL}/wiki/{page}{(!string.IsNullOrEmpty(revision) ? $"/{revision}" : "")}"); - using HtmlDocumentResponse? response = await ASF.WebBrowser.UrlGetToHtmlDocument(request).ConfigureAwait(false); + using HtmlDocumentResponse? response = await ASF.WebBrowser.UrlGetToHtmlDocument(request, cancellationToken: cancellationToken).ConfigureAwait(false); if (response?.Content == null) { return null; @@ -166,26 +167,26 @@ internal static class GitHub { return result; } - private static async Task GetReleaseFromURL(Uri request) { + private static async Task GetReleaseFromURL(Uri request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); if (ASF.WebBrowser == null) { throw new InvalidOperationException(nameof(ASF.WebBrowser)); } - ObjectResponse? response = await ASF.WebBrowser.UrlGetToJsonObject(request).ConfigureAwait(false); + ObjectResponse? response = await ASF.WebBrowser.UrlGetToJsonObject(request, cancellationToken: cancellationToken).ConfigureAwait(false); return response?.Content; } - private static async Task?> GetReleasesFromURL(Uri request) { + private static async Task?> GetReleasesFromURL(Uri request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); if (ASF.WebBrowser == null) { throw new InvalidOperationException(nameof(ASF.WebBrowser)); } - ObjectResponse>? response = await ASF.WebBrowser.UrlGetToJsonObject>(request).ConfigureAwait(false); + ObjectResponse>? response = await ASF.WebBrowser.UrlGetToJsonObject>(request, cancellationToken: cancellationToken).ConfigureAwait(false); return response?.Content; } diff --git a/ArchiSteamFarm/Web/Responses/HtmlDocumentResponse.cs b/ArchiSteamFarm/Web/Responses/HtmlDocumentResponse.cs index adc3c5856..f76e09de9 100644 --- a/ArchiSteamFarm/Web/Responses/HtmlDocumentResponse.cs +++ b/ArchiSteamFarm/Web/Responses/HtmlDocumentResponse.cs @@ -20,6 +20,7 @@ // limitations under the License. using System; +using System.Threading; using System.Threading.Tasks; using AngleSharp; using AngleSharp.Dom; @@ -43,12 +44,12 @@ public sealed class HtmlDocumentResponse : BasicResponse, IDisposable { public void Dispose() => Content?.Dispose(); [PublicAPI] - public static async Task Create(StreamResponse streamResponse) { + public static async Task Create(StreamResponse streamResponse, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(streamResponse); IBrowsingContext context = BrowsingContext.New(); - IDocument document = await context.OpenAsync(request => request.Content(streamResponse.Content, true)).ConfigureAwait(false); + IDocument document = await context.OpenAsync(request => request.Content(streamResponse.Content, true), cancellationToken).ConfigureAwait(false); return new HtmlDocumentResponse(streamResponse, document); } diff --git a/ArchiSteamFarm/Web/WebBrowser.cs b/ArchiSteamFarm/Web/WebBrowser.cs index d7380f6ec..7392ede39 100644 --- a/ArchiSteamFarm/Web/WebBrowser.cs +++ b/ArchiSteamFarm/Web/WebBrowser.cs @@ -29,6 +29,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; +using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Localization; @@ -111,17 +112,17 @@ public sealed class WebBrowser : IDisposable { } [PublicAPI] - public async Task UrlGetToBinary(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0, IProgress? progressReporter = null) { + public async Task UrlGetToBinary(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0, IProgress? progressReporter = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ArgumentOutOfRangeException.ThrowIfZero(maxTries); ArgumentOutOfRangeException.ThrowIfNegative(rateLimitingDelay); for (byte i = 0; i < maxTries; i++) { if ((i > 0) && (rateLimitingDelay > 0)) { - await Task.Delay(rateLimitingDelay).ConfigureAwait(false); + await Task.Delay(rateLimitingDelay, cancellationToken).ConfigureAwait(false); } - StreamResponse? response = await UrlGetToStream(request, headers, referer, requestOptions | ERequestOptions.ReturnClientErrors, 1, rateLimitingDelay).ConfigureAwait(false); + StreamResponse? response = await UrlGetToStream(request, headers, referer, requestOptions | ERequestOptions.ReturnClientErrors, 1, rateLimitingDelay, cancellationToken).ConfigureAwait(false); if (response == null) { // Request timed out, try again @@ -173,13 +174,13 @@ public sealed class WebBrowser : IDisposable { try { while (response.Content.CanRead) { - int read = await response.Content.ReadAsync(buffer.AsMemory(0, buffer.Length)).ConfigureAwait(false); + int read = await response.Content.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false); if (read == 0) { break; } - await ms.WriteAsync(buffer.AsMemory(0, read)).ConfigureAwait(false); + await ms.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); if ((progressReporter == null) || (batchIncreaseSize == 0) || (batch >= 99)) { continue; @@ -192,6 +193,8 @@ public sealed class WebBrowser : IDisposable { progressReporter.Report(++batch); } } + } catch (OperationCanceledException) { + throw; } catch (Exception e) { ArchiLogger.LogGenericWarningException(e); ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFailingRequest, request)); @@ -215,17 +218,17 @@ public sealed class WebBrowser : IDisposable { } [PublicAPI] - public async Task UrlGetToHtmlDocument(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0) { + public async Task UrlGetToHtmlDocument(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ArgumentOutOfRangeException.ThrowIfZero(maxTries); ArgumentOutOfRangeException.ThrowIfNegative(rateLimitingDelay); for (byte i = 0; i < maxTries; i++) { if ((i > 0) && (rateLimitingDelay > 0)) { - await Task.Delay(rateLimitingDelay).ConfigureAwait(false); + await Task.Delay(rateLimitingDelay, cancellationToken).ConfigureAwait(false); } - StreamResponse? response = await UrlGetToStream(request, headers, referer, requestOptions | ERequestOptions.ReturnClientErrors, 1, rateLimitingDelay).ConfigureAwait(false); + StreamResponse? response = await UrlGetToStream(request, headers, referer, requestOptions | ERequestOptions.ReturnClientErrors, 1, rateLimitingDelay, cancellationToken).ConfigureAwait(false); if (response == null) { // Request timed out, try again @@ -256,7 +259,9 @@ public sealed class WebBrowser : IDisposable { } try { - return await HtmlDocumentResponse.Create(response).ConfigureAwait(false); + return await HtmlDocumentResponse.Create(response, cancellationToken).ConfigureAwait(false); + } catch (OperationCanceledException) { + throw; } catch (Exception e) { if ((requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnSuccess) && response.StatusCode.IsSuccessCode()) || (requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnErrors) && !response.StatusCode.IsSuccessCode())) { return new HtmlDocumentResponse(response); @@ -275,17 +280,17 @@ public sealed class WebBrowser : IDisposable { } [PublicAPI] - public async Task?> UrlGetToJsonObject(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0) { + public async Task?> UrlGetToJsonObject(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ArgumentOutOfRangeException.ThrowIfZero(maxTries); ArgumentOutOfRangeException.ThrowIfNegative(rateLimitingDelay); for (byte i = 0; i < maxTries; i++) { if ((i > 0) && (rateLimitingDelay > 0)) { - await Task.Delay(rateLimitingDelay).ConfigureAwait(false); + await Task.Delay(rateLimitingDelay, cancellationToken).ConfigureAwait(false); } - StreamResponse? response = await UrlGetToStream(request, headers, referer, requestOptions | ERequestOptions.ReturnClientErrors, 1, rateLimitingDelay).ConfigureAwait(false); + StreamResponse? response = await UrlGetToStream(request, headers, referer, requestOptions | ERequestOptions.ReturnClientErrors, 1, rateLimitingDelay, cancellationToken).ConfigureAwait(false); if (response == null) { // Request timed out, try again @@ -329,6 +334,8 @@ public sealed class WebBrowser : IDisposable { obj = serializer.Deserialize(jsonReader); } + } catch (OperationCanceledException) { + throw; } catch (Exception e) { if ((requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnSuccess) && response.StatusCode.IsSuccessCode()) || (requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnErrors) && !response.StatusCode.IsSuccessCode())) { return new ObjectResponse(response); @@ -361,17 +368,17 @@ public sealed class WebBrowser : IDisposable { } [PublicAPI] - public async Task UrlGetToStream(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0) { + public async Task UrlGetToStream(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ArgumentOutOfRangeException.ThrowIfZero(maxTries); ArgumentOutOfRangeException.ThrowIfNegative(rateLimitingDelay); for (byte i = 0; i < maxTries; i++) { if ((i > 0) && (rateLimitingDelay > 0)) { - await Task.Delay(rateLimitingDelay).ConfigureAwait(false); + await Task.Delay(rateLimitingDelay, cancellationToken).ConfigureAwait(false); } - HttpResponseMessage? response = await InternalGet(request, headers, referer, requestOptions, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + HttpResponseMessage? response = await InternalGet(request, headers, referer, requestOptions, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); if (response == null) { // Request timed out, try again @@ -396,7 +403,7 @@ public sealed class WebBrowser : IDisposable { } } - return new StreamResponse(response, await response.Content.ReadAsStreamAsync().ConfigureAwait(false)); + return new StreamResponse(response, await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)); } ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, maxTries)); @@ -406,17 +413,17 @@ public sealed class WebBrowser : IDisposable { } [PublicAPI] - public async Task UrlHead(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0) { + public async Task UrlHead(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ArgumentOutOfRangeException.ThrowIfZero(maxTries); ArgumentOutOfRangeException.ThrowIfNegative(rateLimitingDelay); for (byte i = 0; i < maxTries; i++) { if ((i > 0) && (rateLimitingDelay > 0)) { - await Task.Delay(rateLimitingDelay).ConfigureAwait(false); + await Task.Delay(rateLimitingDelay, cancellationToken).ConfigureAwait(false); } - using HttpResponseMessage? response = await InternalHead(request, headers, referer, requestOptions).ConfigureAwait(false); + using HttpResponseMessage? response = await InternalHead(request, headers, referer, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); if (response == null) { continue; @@ -450,17 +457,17 @@ public sealed class WebBrowser : IDisposable { } [PublicAPI] - public async Task UrlPost(Uri request, IReadOnlyCollection>? headers = null, T? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0) where T : class { + public async Task UrlPost(Uri request, IReadOnlyCollection>? headers = null, T? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0, CancellationToken cancellationToken = default) where T : class { ArgumentNullException.ThrowIfNull(request); ArgumentOutOfRangeException.ThrowIfZero(maxTries); ArgumentOutOfRangeException.ThrowIfNegative(rateLimitingDelay); for (byte i = 0; i < maxTries; i++) { if ((i > 0) && (rateLimitingDelay > 0)) { - await Task.Delay(rateLimitingDelay).ConfigureAwait(false); + await Task.Delay(rateLimitingDelay, cancellationToken).ConfigureAwait(false); } - using HttpResponseMessage? response = await InternalPost(request, headers, data, referer, requestOptions).ConfigureAwait(false); + using HttpResponseMessage? response = await InternalPost(request, headers, data, referer, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); if (response == null) { continue; @@ -494,17 +501,17 @@ public sealed class WebBrowser : IDisposable { } [PublicAPI] - public async Task UrlPostToHtmlDocument(Uri request, IReadOnlyCollection>? headers = null, T? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0) where T : class { + public async Task UrlPostToHtmlDocument(Uri request, IReadOnlyCollection>? headers = null, T? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0, CancellationToken cancellationToken = default) where T : class { ArgumentNullException.ThrowIfNull(request); ArgumentOutOfRangeException.ThrowIfZero(maxTries); ArgumentOutOfRangeException.ThrowIfNegative(rateLimitingDelay); for (byte i = 0; i < maxTries; i++) { if ((i > 0) && (rateLimitingDelay > 0)) { - await Task.Delay(rateLimitingDelay).ConfigureAwait(false); + await Task.Delay(rateLimitingDelay, cancellationToken).ConfigureAwait(false); } - StreamResponse? response = await UrlPostToStream(request, headers, data, referer, requestOptions | ERequestOptions.ReturnClientErrors, 1, rateLimitingDelay).ConfigureAwait(false); + StreamResponse? response = await UrlPostToStream(request, headers, data, referer, requestOptions | ERequestOptions.ReturnClientErrors, 1, rateLimitingDelay, cancellationToken).ConfigureAwait(false); if (response == null) { // Request timed out, try again @@ -535,7 +542,9 @@ public sealed class WebBrowser : IDisposable { } try { - return await HtmlDocumentResponse.Create(response).ConfigureAwait(false); + return await HtmlDocumentResponse.Create(response, cancellationToken).ConfigureAwait(false); + } catch (OperationCanceledException) { + throw; } catch (Exception e) { if ((requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnSuccess) && response.StatusCode.IsSuccessCode()) || (requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnErrors) && !response.StatusCode.IsSuccessCode())) { return new HtmlDocumentResponse(response); @@ -554,17 +563,17 @@ public sealed class WebBrowser : IDisposable { } [PublicAPI] - public async Task?> UrlPostToJsonObject(Uri request, IReadOnlyCollection>? headers = null, TData? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0) where TData : class { + public async Task?> UrlPostToJsonObject(Uri request, IReadOnlyCollection>? headers = null, TData? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0, CancellationToken cancellationToken = default) where TData : class { ArgumentNullException.ThrowIfNull(request); ArgumentOutOfRangeException.ThrowIfZero(maxTries); ArgumentOutOfRangeException.ThrowIfNegative(rateLimitingDelay); for (byte i = 0; i < maxTries; i++) { if ((i > 0) && (rateLimitingDelay > 0)) { - await Task.Delay(rateLimitingDelay).ConfigureAwait(false); + await Task.Delay(rateLimitingDelay, cancellationToken).ConfigureAwait(false); } - StreamResponse? response = await UrlPostToStream(request, headers, data, referer, requestOptions | ERequestOptions.ReturnClientErrors, 1, rateLimitingDelay).ConfigureAwait(false); + StreamResponse? response = await UrlPostToStream(request, headers, data, referer, requestOptions | ERequestOptions.ReturnClientErrors, 1, rateLimitingDelay, cancellationToken).ConfigureAwait(false); if (response == null) { // Request timed out, try again @@ -608,6 +617,8 @@ public sealed class WebBrowser : IDisposable { obj = serializer.Deserialize(jsonReader); } + } catch (OperationCanceledException) { + throw; } catch (Exception e) { if ((requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnSuccess) && response.StatusCode.IsSuccessCode()) || (requestOptions.HasFlag(ERequestOptions.AllowInvalidBodyOnErrors) && !response.StatusCode.IsSuccessCode())) { return new ObjectResponse(response); @@ -640,17 +651,17 @@ public sealed class WebBrowser : IDisposable { } [PublicAPI] - public async Task UrlPostToStream(Uri request, IReadOnlyCollection>? headers = null, T? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0) where T : class { + public async Task UrlPostToStream(Uri request, IReadOnlyCollection>? headers = null, T? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, byte maxTries = MaxTries, int rateLimitingDelay = 0, CancellationToken cancellationToken = default) where T : class { ArgumentNullException.ThrowIfNull(request); ArgumentOutOfRangeException.ThrowIfZero(maxTries); ArgumentOutOfRangeException.ThrowIfNegative(rateLimitingDelay); for (byte i = 0; i < maxTries; i++) { if ((i > 0) && (rateLimitingDelay > 0)) { - await Task.Delay(rateLimitingDelay).ConfigureAwait(false); + await Task.Delay(rateLimitingDelay, cancellationToken).ConfigureAwait(false); } - HttpResponseMessage? response = await InternalPost(request, headers, data, referer, requestOptions, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + HttpResponseMessage? response = await InternalPost(request, headers, data, referer, requestOptions, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); if (response == null) { // Request timed out, try again @@ -675,7 +686,7 @@ public sealed class WebBrowser : IDisposable { } } - return new StreamResponse(response, await response.Content.ReadAsStreamAsync().ConfigureAwait(false)); + return new StreamResponse(response, await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)); } ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorRequestFailedTooManyTimes, maxTries)); @@ -698,25 +709,25 @@ public sealed class WebBrowser : IDisposable { ServicePointManager.ReusePort = true; } - private async Task InternalGet(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) { + private async Task InternalGet(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - return await InternalRequest(request, HttpMethod.Get, headers, null, referer, requestOptions, httpCompletionOption).ConfigureAwait(false); + return await InternalRequest(request, HttpMethod.Get, headers, null, referer, requestOptions, httpCompletionOption, cancellationToken: cancellationToken).ConfigureAwait(false); } - private async Task InternalHead(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) { + private async Task InternalHead(Uri request, IReadOnlyCollection>? headers = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - return await InternalRequest(request, HttpMethod.Head, headers, null, referer, requestOptions, httpCompletionOption).ConfigureAwait(false); + return await InternalRequest(request, HttpMethod.Head, headers, null, referer, requestOptions, httpCompletionOption, cancellationToken: cancellationToken).ConfigureAwait(false); } - private async Task InternalPost(Uri request, IReadOnlyCollection>? headers = null, T? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) where T : class { + private async Task InternalPost(Uri request, IReadOnlyCollection>? headers = null, T? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) where T : class { ArgumentNullException.ThrowIfNull(request); - return await InternalRequest(request, HttpMethod.Post, headers, data, referer, requestOptions, httpCompletionOption).ConfigureAwait(false); + return await InternalRequest(request, HttpMethod.Post, headers, data, referer, requestOptions, httpCompletionOption, cancellationToken: cancellationToken).ConfigureAwait(false); } - private async Task InternalRequest(Uri request, HttpMethod httpMethod, IReadOnlyCollection>? headers = null, T? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, byte maxRedirections = MaxTries) where T : class { + private async Task InternalRequest(Uri request, HttpMethod httpMethod, IReadOnlyCollection>? headers = null, T? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, byte maxRedirections = MaxTries, CancellationToken cancellationToken = default) where T : class { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(httpMethod); @@ -778,7 +789,9 @@ public sealed class WebBrowser : IDisposable { } try { - response = await HttpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false); + response = await HttpClient.SendAsync(requestMessage, httpCompletionOption, cancellationToken).ConfigureAwait(false); + } catch (OperationCanceledException) { + throw; } catch (Exception e) { ArchiLogger.LogGenericDebuggingException(e); @@ -867,7 +880,7 @@ public sealed class WebBrowser : IDisposable { if (response.StatusCode.IsClientErrorCode()) { if (Debugging.IsUserDebugging) { - ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.Content, await response.Content.ReadAsStringAsync().ConfigureAwait(false))); + ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.Content, await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false))); } // Do not retry on client errors @@ -876,7 +889,7 @@ public sealed class WebBrowser : IDisposable { if (requestOptions.HasFlag(ERequestOptions.ReturnServerErrors) && response.StatusCode.IsServerErrorCode()) { if (Debugging.IsUserDebugging) { - ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.Content, await response.Content.ReadAsStringAsync().ConfigureAwait(false))); + ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.Content, await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false))); } // Do not retry on server errors in this case @@ -885,7 +898,7 @@ public sealed class WebBrowser : IDisposable { using (response) { if (Debugging.IsUserDebugging) { - ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.Content, await response.Content.ReadAsStringAsync().ConfigureAwait(false))); + ArchiLogger.LogGenericDebug(string.Format(CultureInfo.CurrentCulture, Strings.Content, await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false))); } return null;