Skip to content

Commit

Permalink
feat: task service
Browse files Browse the repository at this point in the history
  • Loading branch information
GerardSmit committed Jan 28, 2024
1 parent 1e352ed commit 0090722
Show file tree
Hide file tree
Showing 27 changed files with 791 additions and 90 deletions.
27 changes: 27 additions & 0 deletions backend/MangaMagnet.Api/Controller/TestController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#if DEBUG
using Asp.Versioning;
using MangaMagnet.Core.Progress;
using Microsoft.AspNetCore.Mvc;

namespace MangaMagnet.Api.Controller;

[ApiController]
[Route("api/[controller]")]
[ApiVersion("1.0")]
public class TestController(ProgressService progressService) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> ProgressTask()
{
using var task = progressService.CreateTask("Test");

for (var i = 0; i < 100; i++)
{
task.Progress = i;
await Task.Delay(20);
}

return Ok();
}
}
#endif
97 changes: 97 additions & 0 deletions backend/MangaMagnet.Api/Middlewares/WebSocketMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Net.WebSockets;
using System.Text.Json;
using MangaMagnet.Api.Service;
using MangaMagnet.Core.Progress;
using MangaMagnet.Core.Util;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.Options;

namespace MangaMagnet.Api.Middlewares;

/// <summary>
/// Handles WebSocket requests.
/// </summary>
public class WebSocketMiddleware(WebSocketService webSocketService, ProgressService progressService, IOptions<JsonOptions> jsonOptions) : IMiddleware
{
/// <inheritdoc />
public Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (context.Request.Path != "/api/ws")
{
return next(context);
}

if (context.WebSockets.IsWebSocketRequest)
{
return HandleWebSocketRequest(context);
}

context.Response.StatusCode = 400;
return Task.CompletedTask;
}

private async Task SendCurrentTasksAsync(WebSocket webSocket)
{
var tasks = progressService.GetAllTasks();
var buffer = new PooledArrayBufferWriter<byte>();
var writer = new Utf8JsonWriter(buffer);
JsonSerializer.Serialize(writer, tasks, jsonOptions.Value.SerializerOptions);
await writer.FlushAsync();
await webSocket.SendAsync(buffer.WrittenMemory, WebSocketMessageType.Text, true, CancellationToken.None);
}

private async Task HandleWebSocketRequest(HttpContext context)
{
var id = Guid.NewGuid();
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();

try
{
var cancellationToken = context.RequestAborted;

await SendCurrentTasksAsync(webSocket);
webSocketService.AddSocket(id, webSocket);

var buffer = new byte[1024];
var result = await webSocket.ReceiveAsync(buffer, default);

while (!result.CloseStatus.HasValue)
{
result = await webSocket.ReceiveAsync(buffer, cancellationToken);

// This websocket is only for sending progress updates, so we don't need to handle any incoming messages.
}

await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, cancellationToken);
}
catch (WebSocketException e)
{
await TryCloseAsync(webSocket, WebSocketCloseStatus.ProtocolError);
}
catch (Exception)
{
await TryCloseAsync(webSocket, WebSocketCloseStatus.InternalServerError);
}
finally
{
webSocketService.RemoveSocket(id);
}
}

private async Task TryCloseAsync(WebSocket webSocket, WebSocketCloseStatus closeStatus, string closeStatusDescription = "")
{
if (webSocket.State is not (WebSocketState.Open or WebSocketState.CloseReceived or WebSocketState.CloseSent))
{
return;
}

try
{
await webSocket.CloseAsync(closeStatus, closeStatusDescription, CancellationToken.None);
}
catch
{
// Ignore
}
}
}
15 changes: 12 additions & 3 deletions backend/MangaMagnet.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
using Asp.Versioning;
using Asp.Versioning.ApiExplorer;
using AspNetCore.ExceptionHandler;
using MangaMagnet.Api;
using MangaMagnet.Api.Configurations;
using MangaMagnet.Api.Middlewares;
using MangaMagnet.Api.Models.Database;
using MangaMagnet.Api.Service;
using MangaMagnet.Api.Swagger;
using MangaMagnet.Core.Database;
using MangaMagnet.Core.Metadata;
using MangaMagnet.Core.Metadata.Providers.MangaDex;
using MangaMagnet.Core.Progress;
using MangaMagnet.Core.Progress.Models;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using Quartz;
Expand Down Expand Up @@ -59,9 +61,15 @@
builder.Services.AddSingleton<MangaDexConverterService>();
builder.Services.AddSingleton<IMetadataFetcher, MetadataFetcherService>();
builder.Services.AddSingleton<EntityConverterService>();
builder.Services.AddHostedService<BroadcastProgressService>();
builder.Services.AddScoped<MangaService>();
builder.Services.AddScoped<MetadataService>();

// Progress services
builder.Services.AddSingleton<ProgressService>();
builder.Services.AddSingleton<WebSocketService>();
builder.Services.AddSingleton<WebSocketMiddleware>();

// Add services to the container.
builder.Services.AddControllers()
.AddJsonOptions(o =>
Expand Down Expand Up @@ -94,6 +102,7 @@
builder.Services.AddSwaggerGen(c =>
{
c.SchemaFilter<NullabilitySchemaFilter>();
c.DocumentFilter<CustomModelDocumentFilter<ProgressTask>>();
});

builder.Services.AddCors(options =>
Expand Down Expand Up @@ -176,8 +185,6 @@
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();

app.UseDeveloperExceptionPage();

var apiVersionDescriptionProvider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
Expand All @@ -199,6 +206,8 @@

app.UseCors();

app.UseWebSockets();
app.UseMiddleware<WebSocketMiddleware>();
app.UseRouting();

app.UseAuthorization();
Expand Down
78 changes: 39 additions & 39 deletions backend/MangaMagnet.Api/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:34510",
"sslPort": 44336
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5248",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7140;http://localhost:5248",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:34510",
"sslPort": 44336
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5248",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7140;http://localhost:5248",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https-no-browser": {
"commandName": "Project",
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7140;http://localhost:5248",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
42 changes: 42 additions & 0 deletions backend/MangaMagnet.Api/Service/BroadcastProgressService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Buffers;
using System.Text.Json;
using MangaMagnet.Core.Progress;
using MangaMagnet.Core.Util;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.Options;

namespace MangaMagnet.Api.Service;

public class BroadcastProgressService(ProgressService progressService, WebSocketService webSocketService, IOptions<JsonOptions> jsonOptions) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(500));
var buffer = new PooledArrayBufferWriter<byte>();

while (await timer.WaitForNextTickAsync(stoppingToken))
{
var changedTasks = progressService.GetUpdatedTasksAndReset();

if (changedTasks.Count == 0)
{
continue;
}

var writer = new Utf8JsonWriter(buffer);
JsonSerializer.Serialize(writer, changedTasks, jsonOptions.Value.SerializerOptions);
await writer.FlushAsync(stoppingToken);

await webSocketService.SendToAllAsync(buffer.WrittenMemory, stoppingToken);

buffer.Reset();
}
}
catch (TaskCanceledException)
{
// Ignore
}
}
}
41 changes: 41 additions & 0 deletions backend/MangaMagnet.Api/Service/WebSocketService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Collections.Concurrent;
using System.Net.WebSockets;

namespace MangaMagnet.Api.Service;

public class WebSocketService
{
private readonly ConcurrentDictionary<Guid, WebSocket> _sockets = new();

/// <summary>
/// Add a new socket to the list.
/// </summary>
/// <param name="id">Unique identifier of the socket.</param>
/// <param name="socket">Socket to add.</param>
public void AddSocket(Guid id, WebSocket socket)
{
_sockets.TryAdd(id, socket);
}

/// <summary>
/// Remove a socket from the list.
/// </summary>
/// <param name="id">Unique identifier of the socket.</param>
public void RemoveSocket(Guid id)
{
_sockets.TryRemove(id, out _);
}

/// <summary>
/// Send message to all connected clients.
/// </summary>
/// <param name="memory">Message to send.</param>
/// <param name="stoppingToken">Cancellation token.</param>
public async Task SendToAllAsync(ReadOnlyMemory<byte> memory, CancellationToken stoppingToken = default)
{
foreach (var socket in _sockets.Values)
{
await socket.SendAsync(memory, WebSocketMessageType.Text, true, stoppingToken);
}
}
}
17 changes: 17 additions & 0 deletions backend/MangaMagnet.Api/Swagger/CustomModelDocumentFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace MangaMagnet.Api.Swagger;

/// <summary>
/// Registers a custom model to the swagger document.
/// </summary>
/// <typeparam name="T">The type of the model.</typeparam>
public sealed class CustomModelDocumentFilter<T> : IDocumentFilter where T : class
{
/// <inheritdoc />
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
context.SchemaGenerator.GenerateSchema(typeof(T), context.SchemaRepository);
}
}
Loading

0 comments on commit 0090722

Please sign in to comment.