diff --git a/app/Server/Database/Export/ViewerJsonExport.cs b/app/Server/Database/Export/ViewerJsonExport.cs index f202a84..de912be 100644 --- a/app/Server/Database/Export/ViewerJsonExport.cs +++ b/app/Server/Database/Export/ViewerJsonExport.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using DHT.Server.Data; using DHT.Server.Data.Filters; @@ -13,14 +14,14 @@ namespace DHT.Server.Database.Export; static class ViewerJsonExport { private static readonly Log Log = Log.ForType(typeof(ViewerJsonExport)); - public static async Task Generate(Stream stream, IDatabaseFile db, MessageFilter? filter = null) { + public static async Task Generate(Stream stream, IDatabaseFile db, MessageFilter? filter = null, CancellationToken cancellationToken = default) { var perf = Log.Start(); var includedUserIds = new HashSet(); var includedChannelIds = new HashSet(); var includedServerIds = new HashSet(); - var includedMessages = await db.Messages.Get(filter).ToListAsync(); + var includedMessages = await db.Messages.Get(filter, cancellationToken).ToListAsync(cancellationToken); var includedChannels = new List(); foreach (var message in includedMessages) { @@ -28,15 +29,15 @@ static class ViewerJsonExport { includedChannelIds.Add(message.Channel); } - await foreach (var channel in db.Channels.Get()) { + await foreach (var channel in db.Channels.Get(cancellationToken)) { if (includedChannelIds.Contains(channel.Id)) { includedChannels.Add(channel); includedServerIds.Add(channel.Server); } } - var (users, userIndex, userIndices) = await GenerateUserList(db, includedUserIds); - var (servers, serverIndices) = await GenerateServerList(db, includedServerIds); + var (users, userIndex, userIndices) = await GenerateUserList(db, includedUserIds, cancellationToken); + var (servers, serverIndices) = await GenerateServerList(db, includedServerIds, cancellationToken); var channels = GenerateChannelList(includedChannels, serverIndices); perf.Step("Collect database data"); @@ -53,18 +54,18 @@ static class ViewerJsonExport { perf.Step("Generate value object"); - await JsonSerializer.SerializeAsync(stream, value, ViewerJsonContext.Default.ViewerJson); + await JsonSerializer.SerializeAsync(stream, value, ViewerJsonContext.Default.ViewerJson, cancellationToken); perf.Step("Serialize to JSON"); perf.End(); } - private static async Task<(Dictionary Users, List UserIndex, Dictionary UserIndices)> GenerateUserList(IDatabaseFile db, HashSet userIds) { + private static async Task<(Dictionary Users, List UserIndex, Dictionary UserIndices)> GenerateUserList(IDatabaseFile db, HashSet userIds, CancellationToken cancellationToken) { var users = new Dictionary(); var userIndex = new List(); var userIndices = new Dictionary(); - await foreach (var user in db.Users.Get()) { + await foreach (var user in db.Users.Get(cancellationToken)) { var id = user.Id; if (!userIds.Contains(id)) { continue; @@ -84,11 +85,11 @@ static class ViewerJsonExport { return (users, userIndex, userIndices); } - private static async Task<(List Servers, Dictionary ServerIndices)> GenerateServerList(IDatabaseFile db, HashSet serverIds) { + private static async Task<(List Servers, Dictionary ServerIndices)> GenerateServerList(IDatabaseFile db, HashSet serverIds, CancellationToken cancellationToken) { var servers = new List(); var serverIndices = new Dictionary(); - await foreach (var server in db.Servers.Get()) { + await foreach (var server in db.Servers.Get(cancellationToken)) { var id = server.Id; if (!serverIds.Contains(id)) { continue; diff --git a/app/Server/Database/Repositories/IChannelRepository.cs b/app/Server/Database/Repositories/IChannelRepository.cs index 5096a13..a830cd3 100644 --- a/app/Server/Database/Repositories/IChannelRepository.cs +++ b/app/Server/Database/Repositories/IChannelRepository.cs @@ -15,7 +15,7 @@ public interface IChannelRepository { Task Count(CancellationToken cancellationToken = default); - IAsyncEnumerable Get(); + IAsyncEnumerable Get(CancellationToken cancellationToken = default); internal sealed class Dummy : IChannelRepository { public IObservable TotalCount { get; } = Observable.Return(0L); @@ -28,7 +28,7 @@ public interface IChannelRepository { return Task.FromResult(0L); } - public IAsyncEnumerable Get() { + public IAsyncEnumerable Get(CancellationToken cancellationToken) { return AsyncEnumerable.Empty(); } } diff --git a/app/Server/Database/Repositories/IDownloadRepository.cs b/app/Server/Database/Repositories/IDownloadRepository.cs index fa697ed..a87dc93 100644 --- a/app/Server/Database/Repositories/IDownloadRepository.cs +++ b/app/Server/Database/Repositories/IDownloadRepository.cs @@ -24,7 +24,7 @@ public interface IDownloadRepository { Task GetDownloadData(string normalizedUrl, Func dataProcessor); - Task GetSuccessfulDownloadWithData(string normalizedUrl, Func dataProcessor); + Task GetSuccessfulDownloadWithData(string normalizedUrl, Func dataProcessor, CancellationToken cancellationToken = default); IAsyncEnumerable PullPendingDownloadItems(int count, DownloadItemFilter filter, CancellationToken cancellationToken = default); @@ -55,7 +55,7 @@ public interface IDownloadRepository { return Task.FromResult(false); } - public Task GetSuccessfulDownloadWithData(string normalizedUrl, Func dataProcessor) { + public Task GetSuccessfulDownloadWithData(string normalizedUrl, Func dataProcessor, CancellationToken cancellationToken) { return Task.FromResult(false); } diff --git a/app/Server/Database/Repositories/IMessageRepository.cs b/app/Server/Database/Repositories/IMessageRepository.cs index bb39c56..afe9042 100644 --- a/app/Server/Database/Repositories/IMessageRepository.cs +++ b/app/Server/Database/Repositories/IMessageRepository.cs @@ -16,7 +16,7 @@ public interface IMessageRepository { Task Count(MessageFilter? filter = null, CancellationToken cancellationToken = default); - IAsyncEnumerable Get(MessageFilter? filter = null); + IAsyncEnumerable Get(MessageFilter? filter = null, CancellationToken cancellationToken = default); IAsyncEnumerable GetIds(MessageFilter? filter = null); @@ -33,7 +33,7 @@ public interface IMessageRepository { return Task.FromResult(0L); } - public IAsyncEnumerable Get(MessageFilter? filter) { + public IAsyncEnumerable Get(MessageFilter? filter, CancellationToken cancellationToken) { return AsyncEnumerable.Empty(); } diff --git a/app/Server/Database/Repositories/IServerRepository.cs b/app/Server/Database/Repositories/IServerRepository.cs index 8d61fca..7007ba3 100644 --- a/app/Server/Database/Repositories/IServerRepository.cs +++ b/app/Server/Database/Repositories/IServerRepository.cs @@ -14,7 +14,7 @@ public interface IServerRepository { Task Count(CancellationToken cancellationToken = default); - IAsyncEnumerable Get(); + IAsyncEnumerable Get(CancellationToken cancellationToken = default); internal sealed class Dummy : IServerRepository { public IObservable TotalCount { get; } = Observable.Return(0L); @@ -27,7 +27,7 @@ public interface IServerRepository { return Task.FromResult(0L); } - public IAsyncEnumerable Get() { + public IAsyncEnumerable Get(CancellationToken cancellationToken) { return AsyncEnumerable.Empty(); } } diff --git a/app/Server/Database/Repositories/IUserRepository.cs b/app/Server/Database/Repositories/IUserRepository.cs index b16de3b..b76e875 100644 --- a/app/Server/Database/Repositories/IUserRepository.cs +++ b/app/Server/Database/Repositories/IUserRepository.cs @@ -15,7 +15,7 @@ public interface IUserRepository { Task Count(CancellationToken cancellationToken = default); - IAsyncEnumerable Get(); + IAsyncEnumerable Get(CancellationToken cancellationToken = default); internal sealed class Dummy : IUserRepository { public IObservable TotalCount { get; } = Observable.Return(0L); @@ -28,7 +28,7 @@ public interface IUserRepository { return Task.FromResult(0L); } - public IAsyncEnumerable Get() { + public IAsyncEnumerable Get(CancellationToken cancellationToken) { return AsyncEnumerable.Empty(); } } diff --git a/app/Server/Database/Sqlite/Repositories/SqliteChannelRepository.cs b/app/Server/Database/Sqlite/Repositories/SqliteChannelRepository.cs index b308fa1..52498d9 100644 --- a/app/Server/Database/Sqlite/Repositories/SqliteChannelRepository.cs +++ b/app/Server/Database/Sqlite/Repositories/SqliteChannelRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using DHT.Server.Data; @@ -54,13 +55,13 @@ sealed class SqliteChannelRepository : BaseSqliteRepository, IChannelRepository return await conn.ExecuteReaderAsync("SELECT COUNT(*) FROM channels", static reader => reader?.GetInt64(0) ?? 0L, cancellationToken); } - public async IAsyncEnumerable Get() { + public async IAsyncEnumerable Get([EnumeratorCancellation] CancellationToken cancellationToken) { await using var conn = await pool.Take(); await using var cmd = conn.Command("SELECT id, server, name, parent_id, position, topic, nsfw FROM channels"); - await using var reader = await cmd.ExecuteReaderAsync(); + await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); - while (await reader.ReadAsync()) { + while (await reader.ReadAsync(cancellationToken)) { yield return new Channel { Id = reader.GetUint64(0), Server = reader.GetUint64(1), diff --git a/app/Server/Database/Sqlite/Repositories/SqliteDownloadRepository.cs b/app/Server/Database/Sqlite/Repositories/SqliteDownloadRepository.cs index ca88a77..148f4ac 100644 --- a/app/Server/Database/Sqlite/Repositories/SqliteDownloadRepository.cs +++ b/app/Server/Database/Sqlite/Repositories/SqliteDownloadRepository.cs @@ -210,7 +210,7 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep return true; } - public async Task GetSuccessfulDownloadWithData(string normalizedUrl, Func dataProcessor) { + public async Task GetSuccessfulDownloadWithData(string normalizedUrl, Func dataProcessor, CancellationToken cancellationToken) { await using var conn = await pool.Take(); await using var cmd = conn.Command( @@ -228,8 +228,8 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep string? type; long rowid; - await using (var reader = await cmd.ExecuteReaderAsync()) { - if (!await reader.ReadAsync()) { + await using (var reader = await cmd.ExecuteReaderAsync(cancellationToken)) { + if (!await reader.ReadAsync(cancellationToken)) { return false; } @@ -239,7 +239,7 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep } await using (var blob = new SqliteBlob(conn.InnerConnection, "download_blobs", "blob", rowid, readOnly: true)) { - await dataProcessor(new Data.Download(normalizedUrl, downloadUrl, DownloadStatus.Success, type, (ulong) blob.Length), blob); + await dataProcessor(new Data.Download(normalizedUrl, downloadUrl, DownloadStatus.Success, type, (ulong) blob.Length), blob, cancellationToken); } return true; diff --git a/app/Server/Database/Sqlite/Repositories/SqliteMessageRepository.cs b/app/Server/Database/Sqlite/Repositories/SqliteMessageRepository.cs index 3cf7a1f..a1736f3 100644 --- a/app/Server/Database/Sqlite/Repositories/SqliteMessageRepository.cs +++ b/app/Server/Database/Sqlite/Repositories/SqliteMessageRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using DHT.Server.Data; @@ -213,7 +214,7 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository } } - public async IAsyncEnumerable Get(MessageFilter? filter) { + public async IAsyncEnumerable Get(MessageFilter? filter, [EnumeratorCancellation] CancellationToken cancellationToken) { await using var conn = await pool.Take(); const string AttachmentSql = @@ -269,9 +270,9 @@ sealed class SqliteMessageRepository : BaseSqliteRepository, IMessageRepository """ ); - await using var reader = await messageCmd.ExecuteReaderAsync(); + await using var reader = await messageCmd.ExecuteReaderAsync(cancellationToken); - while (await reader.ReadAsync()) { + while (await reader.ReadAsync(cancellationToken)) { ulong messageId = reader.GetUint64(0); yield return new Message { diff --git a/app/Server/Database/Sqlite/Repositories/SqliteServerRepository.cs b/app/Server/Database/Sqlite/Repositories/SqliteServerRepository.cs index 9932b7f..c83d24f 100644 --- a/app/Server/Database/Sqlite/Repositories/SqliteServerRepository.cs +++ b/app/Server/Database/Sqlite/Repositories/SqliteServerRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using DHT.Server.Data; @@ -46,13 +47,13 @@ sealed class SqliteServerRepository : BaseSqliteRepository, IServerRepository { return await conn.ExecuteReaderAsync("SELECT COUNT(*) FROM servers", static reader => reader?.GetInt64(0) ?? 0L, cancellationToken); } - public async IAsyncEnumerable Get() { + public async IAsyncEnumerable Get([EnumeratorCancellation] CancellationToken cancellationToken) { await using var conn = await pool.Take(); await using var cmd = conn.Command("SELECT id, name, type FROM servers"); - await using var reader = await cmd.ExecuteReaderAsync(); + await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); - while (await reader.ReadAsync()) { + while (await reader.ReadAsync(cancellationToken)) { yield return new Data.Server { Id = reader.GetUint64(0), Name = reader.GetString(1), diff --git a/app/Server/Database/Sqlite/Repositories/SqliteUserRepository.cs b/app/Server/Database/Sqlite/Repositories/SqliteUserRepository.cs index c3854dd..4aa4c92 100644 --- a/app/Server/Database/Sqlite/Repositories/SqliteUserRepository.cs +++ b/app/Server/Database/Sqlite/Repositories/SqliteUserRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using DHT.Server.Data; @@ -58,13 +59,13 @@ sealed class SqliteUserRepository : BaseSqliteRepository, IUserRepository { return await conn.ExecuteReaderAsync("SELECT COUNT(*) FROM users", static reader => reader?.GetInt64(0) ?? 0L, cancellationToken); } - public async IAsyncEnumerable Get() { + public async IAsyncEnumerable Get([EnumeratorCancellation] CancellationToken cancellationToken) { await using var conn = await pool.Take(); await using var cmd = conn.Command("SELECT id, name, avatar_url, discriminator FROM users"); - await using var reader = await cmd.ExecuteReaderAsync(); + await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); - while (await reader.ReadAsync()) { + while (await reader.ReadAsync(cancellationToken)) { yield return new User { Id = reader.GetUint64(0), Name = reader.GetString(1), diff --git a/app/Server/Endpoints/BaseEndpoint.cs b/app/Server/Endpoints/BaseEndpoint.cs index 5646cca..cbea1e0 100644 --- a/app/Server/Endpoints/BaseEndpoint.cs +++ b/app/Server/Endpoints/BaseEndpoint.cs @@ -1,6 +1,7 @@ using System; using System.Net; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using DHT.Server.Database; using DHT.Utils.Http; @@ -19,7 +20,9 @@ abstract class BaseEndpoint(IDatabaseFile db) { try { response.StatusCode = (int) HttpStatusCode.OK; - await Respond(ctx.Request, response); + await Respond(ctx.Request, response, ctx.RequestAborted); + } catch (OperationCanceledException) { + throw; } catch (HttpException e) { Log.Error(e); response.StatusCode = (int) e.StatusCode; @@ -35,7 +38,7 @@ abstract class BaseEndpoint(IDatabaseFile db) { } } - protected abstract Task Respond(HttpRequest request, HttpResponse response); + protected abstract Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken); protected static async Task ReadJson(HttpRequest request) { try { diff --git a/app/Server/Endpoints/GetDownloadedFileEndpoint.cs b/app/Server/Endpoints/GetDownloadedFileEndpoint.cs index 6eafbc0..6c2c12c5 100644 --- a/app/Server/Endpoints/GetDownloadedFileEndpoint.cs +++ b/app/Server/Endpoints/GetDownloadedFileEndpoint.cs @@ -1,4 +1,7 @@ +using System; +using System.IO; using System.Net; +using System.Threading; using System.Threading.Tasks; using DHT.Server.Database; using DHT.Server.Download; @@ -8,12 +11,16 @@ using Microsoft.AspNetCore.Http; namespace DHT.Server.Endpoints; sealed class GetDownloadedFileEndpoint(IDatabaseFile db) : BaseEndpoint(db) { - protected override async Task Respond(HttpRequest request, HttpResponse response) { + protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { string url = WebUtility.UrlDecode((string) request.RouteValues["url"]!); string normalizedUrl = DiscordCdn.NormalizeUrl(url); - if (!await Db.Downloads.GetSuccessfulDownloadWithData(normalizedUrl, (download, stream) => response.WriteStreamAsync(download.Type, download.Size, stream))) { + if (!await Db.Downloads.GetSuccessfulDownloadWithData(normalizedUrl, WriteDataTo(response), cancellationToken)) { response.Redirect(url, permanent: false); } } + + private static Func WriteDataTo(HttpResponse response) { + return (download, stream, cancellationToken) => response.WriteStreamAsync(download.Type, download.Size, stream, cancellationToken); + } } diff --git a/app/Server/Endpoints/GetTrackingScriptEndpoint.cs b/app/Server/Endpoints/GetTrackingScriptEndpoint.cs index 54cbf16..ea7a2bd 100644 --- a/app/Server/Endpoints/GetTrackingScriptEndpoint.cs +++ b/app/Server/Endpoints/GetTrackingScriptEndpoint.cs @@ -1,4 +1,5 @@ using System.Net.Mime; +using System.Threading; using System.Threading.Tasks; using System.Web; using DHT.Server.Database; @@ -10,7 +11,7 @@ using Microsoft.AspNetCore.Http; namespace DHT.Server.Endpoints; sealed class GetTrackingScriptEndpoint(IDatabaseFile db, ServerParameters parameters, ResourceLoader resources) : BaseEndpoint(db) { - protected override async Task Respond(HttpRequest request, HttpResponse response) { + protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { string bootstrap = await resources.ReadTextAsync("Tracker/bootstrap.js"); string script = bootstrap.Replace("= 0; /*[PORT]*/", "= " + parameters.Port + ";") .Replace("/*[TOKEN]*/", HttpUtility.JavaScriptStringEncode(parameters.Token)) @@ -20,6 +21,6 @@ sealed class GetTrackingScriptEndpoint(IDatabaseFile db, ServerParameters parame .Replace("/*[DEBUGGER]*/", request.Query.ContainsKey("debug") ? "debugger;" : ""); response.Headers.Append("X-DHT", "1"); - await response.WriteTextAsync(MediaTypeNames.Text.JavaScript, script); + await response.WriteTextAsync(MediaTypeNames.Text.JavaScript, script, cancellationToken); } } diff --git a/app/Server/Endpoints/GetViewerDataEndpoint.cs b/app/Server/Endpoints/GetViewerDataEndpoint.cs index ab01474..b3c8b66 100644 --- a/app/Server/Endpoints/GetViewerDataEndpoint.cs +++ b/app/Server/Endpoints/GetViewerDataEndpoint.cs @@ -1,6 +1,7 @@ using System; using System.Net; using System.Net.Mime; +using System.Threading; using System.Threading.Tasks; using DHT.Server.Database; using DHT.Server.Database.Export; @@ -11,7 +12,7 @@ using Microsoft.AspNetCore.Http; namespace DHT.Server.Endpoints; sealed class GetViewerDataEndpoint(IDatabaseFile db, ViewerSessions viewerSessions) : BaseEndpoint(db) { - protected override async Task Respond(HttpRequest request, HttpResponse response) { + protected override Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { if (!request.Query.TryGetValue("session", out var sessionIdValue) || sessionIdValue.Count != 1 || !Guid.TryParse(sessionIdValue[0], out Guid sessionId)) { throw new HttpException(HttpStatusCode.BadRequest, "Invalid session ID."); } @@ -19,6 +20,6 @@ sealed class GetViewerDataEndpoint(IDatabaseFile db, ViewerSessions viewerSessio response.ContentType = MediaTypeNames.Application.Json; var session = viewerSessions.Get(sessionId); - await ViewerJsonExport.Generate(response.Body, Db, session.MessageFilter); + return ViewerJsonExport.Generate(response.Body, Db, session.MessageFilter, cancellationToken); } } diff --git a/app/Server/Endpoints/TrackChannelEndpoint.cs b/app/Server/Endpoints/TrackChannelEndpoint.cs index 164761f..c2e804b 100644 --- a/app/Server/Endpoints/TrackChannelEndpoint.cs +++ b/app/Server/Endpoints/TrackChannelEndpoint.cs @@ -1,5 +1,6 @@ using System.Net; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using DHT.Server.Data; using DHT.Server.Database; @@ -9,7 +10,7 @@ using Microsoft.AspNetCore.Http; namespace DHT.Server.Endpoints; sealed class TrackChannelEndpoint(IDatabaseFile db) : BaseEndpoint(db) { - protected override async Task Respond(HttpRequest request, HttpResponse response) { + protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { var root = await ReadJson(request); var server = ReadServer(root.RequireObject("server"), "server"); var channel = ReadChannel(root.RequireObject("channel"), "channel", server.Id); diff --git a/app/Server/Endpoints/TrackMessagesEndpoint.cs b/app/Server/Endpoints/TrackMessagesEndpoint.cs index d1bf83d..46fb76b 100644 --- a/app/Server/Endpoints/TrackMessagesEndpoint.cs +++ b/app/Server/Endpoints/TrackMessagesEndpoint.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using DHT.Server.Data; using DHT.Server.Data.Filters; @@ -19,7 +20,7 @@ sealed class TrackMessagesEndpoint(IDatabaseFile db) : BaseEndpoint(db) { private const string HasNewMessages = "1"; private const string NoNewMessages = "0"; - protected override async Task Respond(HttpRequest request, HttpResponse response) { + protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { var root = await ReadJson(request); if (root.ValueKind != JsonValueKind.Array) { @@ -37,11 +38,11 @@ sealed class TrackMessagesEndpoint(IDatabaseFile db) : BaseEndpoint(db) { } var addedMessageFilter = new MessageFilter { MessageIds = addedMessageIds }; - bool anyNewMessages = await Db.Messages.Count(addedMessageFilter) < addedMessageIds.Count; + bool anyNewMessages = await Db.Messages.Count(addedMessageFilter, CancellationToken.None) < addedMessageIds.Count; await Db.Messages.Add(messages); - await response.WriteTextAsync(anyNewMessages ? HasNewMessages : NoNewMessages); + await response.WriteTextAsync(anyNewMessages ? HasNewMessages : NoNewMessages, cancellationToken); } private static Message ReadMessage(JsonElement json, string path) => new () { diff --git a/app/Server/Endpoints/TrackUsersEndpoint.cs b/app/Server/Endpoints/TrackUsersEndpoint.cs index 845ceb3..05fb579 100644 --- a/app/Server/Endpoints/TrackUsersEndpoint.cs +++ b/app/Server/Endpoints/TrackUsersEndpoint.cs @@ -1,5 +1,6 @@ using System.Net; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using DHT.Server.Data; using DHT.Server.Database; @@ -9,7 +10,7 @@ using Microsoft.AspNetCore.Http; namespace DHT.Server.Endpoints; sealed class TrackUsersEndpoint(IDatabaseFile db) : BaseEndpoint(db) { - protected override async Task Respond(HttpRequest request, HttpResponse response) { + protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { var root = await ReadJson(request); if (root.ValueKind != JsonValueKind.Array) { diff --git a/app/Server/Endpoints/ViewerEndpoint.cs b/app/Server/Endpoints/ViewerEndpoint.cs index 95470b3..2be07e8 100644 --- a/app/Server/Endpoints/ViewerEndpoint.cs +++ b/app/Server/Endpoints/ViewerEndpoint.cs @@ -16,13 +16,13 @@ sealed class ViewerEndpoint(IDatabaseFile db, ResourceLoader resources) : BaseEn private readonly Dictionary cache = new (); private readonly SemaphoreSlim cacheSemaphore = new (1); - protected override async Task Respond(HttpRequest request, HttpResponse response) { + protected override async Task Respond(HttpRequest request, HttpResponse response, CancellationToken cancellationToken) { string path = (string?) request.RouteValues["path"] ?? "index.html"; string resourcePath = "Viewer/" + path; byte[]? resourceBytes; - await cacheSemaphore.WaitAsync(); + await cacheSemaphore.WaitAsync(cancellationToken); try { if (!cache.TryGetValue(resourcePath, out resourceBytes)) { cache[resourcePath] = resourceBytes = await resources.ReadBytesAsyncIfExists(resourcePath); @@ -36,7 +36,7 @@ sealed class ViewerEndpoint(IDatabaseFile db, ResourceLoader resources) : BaseEn } else { var contentType = ContentTypeProvider.TryGetContentType(path, out string? type) ? type : null; - await response.WriteFileAsync(contentType, resourceBytes); + await response.WriteFileAsync(contentType, resourceBytes, cancellationToken); } } } diff --git a/app/Server/Service/Middlewares/ServerLoggingMiddleware.cs b/app/Server/Service/Middlewares/ServerLoggingMiddleware.cs index 1dbe349..cc6c597 100644 --- a/app/Server/Service/Middlewares/ServerLoggingMiddleware.cs +++ b/app/Server/Service/Middlewares/ServerLoggingMiddleware.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; using System.Threading.Tasks; using DHT.Utils.Logging; @@ -6,24 +7,34 @@ using Microsoft.AspNetCore.Http.Extensions; namespace DHT.Server.Service.Middlewares; -sealed class ServerLoggingMiddleware { +sealed class ServerLoggingMiddleware(RequestDelegate next) { private static readonly Log Log = Log.ForType(); - - private readonly RequestDelegate next; - - public ServerLoggingMiddleware(RequestDelegate next) { - this.next = next; - } public async Task InvokeAsync(HttpContext context) { var stopwatch = Stopwatch.StartNew(); - await next(context); + try { + await next(context); + } catch (OperationCanceledException) { + OnFinished(stopwatch, context); + throw; + } + + OnFinished(stopwatch, context); + } + + private static void OnFinished(Stopwatch stopwatch, HttpContext context) { stopwatch.Stop(); - + var request = context.Request; var requestLength = request.ContentLength ?? 0L; - var responseStatus = context.Response.StatusCode; var elapsedMs = stopwatch.ElapsedMilliseconds; - Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) returned " + responseStatus + ", took " + elapsedMs + " ms"); + + if (context.RequestAborted.IsCancellationRequested) { + Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) was cancelled after " + elapsedMs + " ms"); + } + else { + var responseStatus = context.Response.StatusCode; + Log.Debug("Request to " + request.GetEncodedPathAndQuery() + " (" + requestLength + " B) returned " + responseStatus + ", took " + elapsedMs + " ms"); + } } } diff --git a/app/Utils/Http/HttpExtensions.cs b/app/Utils/Http/HttpExtensions.cs index 7ca8145..250be27 100644 --- a/app/Utils/Http/HttpExtensions.cs +++ b/app/Utils/Http/HttpExtensions.cs @@ -1,33 +1,34 @@ using System.IO; using System.Net.Mime; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace DHT.Utils.Http; public static class HttpExtensions { - public static Task WriteTextAsync(this HttpResponse response, string text) { - return WriteTextAsync(response, MediaTypeNames.Text.Plain, text); + public static Task WriteTextAsync(this HttpResponse response, string text, CancellationToken cancellationToken) { + return WriteTextAsync(response, MediaTypeNames.Text.Plain, text, cancellationToken); } - public static async Task WriteTextAsync(this HttpResponse response, string contentType, string text) { + public static async Task WriteTextAsync(this HttpResponse response, string contentType, string text, CancellationToken cancellationToken) { response.ContentType = contentType; - await response.StartAsync(); - await response.WriteAsync(text, Encoding.UTF8); + await response.StartAsync(cancellationToken); + await response.WriteAsync(text, Encoding.UTF8, cancellationToken); } - public static async Task WriteFileAsync(this HttpResponse response, string? contentType, byte[] bytes) { + public static async Task WriteFileAsync(this HttpResponse response, string? contentType, byte[] bytes, CancellationToken cancellationToken) { response.ContentType = contentType ?? string.Empty; response.ContentLength = bytes.Length; - await response.StartAsync(); - await response.Body.WriteAsync(bytes); + await response.StartAsync(cancellationToken); + await response.Body.WriteAsync(bytes, cancellationToken); } - public static async Task WriteStreamAsync(this HttpResponse response, string? contentType, ulong? contentLength, Stream source) { + public static async Task WriteStreamAsync(this HttpResponse response, string? contentType, ulong? contentLength, Stream source, CancellationToken cancellationToken) { response.ContentType = contentType ?? string.Empty; response.ContentLength = (long?) contentLength; - await response.StartAsync(); - await source.CopyToAsync(response.Body); + await response.StartAsync(cancellationToken); + await source.CopyToAsync(response.Body, cancellationToken); } }