From e2480b20c303e6d05ea43395d9ab956fbb01a1af Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Mon, 16 Oct 2023 14:38:31 -0300 Subject: [PATCH 1/3] fix: remove Microsoft.AspNetCore.StaticFiles --- source/Models/IFile.cs | 17 ++++------------- source/ServerHandlers/StaticFileRequest.cs | 16 +++++----------- source/SuCoS.csproj | 2 +- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/source/Models/IFile.cs b/source/Models/IFile.cs index d3f007c..c7cf50f 100644 --- a/source/Models/IFile.cs +++ b/source/Models/IFile.cs @@ -1,5 +1,7 @@ using System.IO; -using Microsoft.AspNetCore.StaticFiles; +using FolkerKinzel.MimeTypes; + +// using Microsoft.AspNetCore.StaticFiles; using SuCoS.Helpers; namespace SuCoS.Models; @@ -42,18 +44,7 @@ public interface IFile /// /// File MIME type. /// - string MimeType - { - get - { - var provider = new FileExtensionContentTypeProvider(); - if (!provider.TryGetContentType(SourceFullPath, out var contentType)) - { - contentType = "application/octet-stream"; - } - return contentType; - } - } + string MimeType => MimeString.FromFileName(SourceFullPath) ?? "application/octet-stream"; /// /// File size in bytes. diff --git a/source/ServerHandlers/StaticFileRequest.cs b/source/ServerHandlers/StaticFileRequest.cs index 9cf0e91..ef330df 100644 --- a/source/ServerHandlers/StaticFileRequest.cs +++ b/source/ServerHandlers/StaticFileRequest.cs @@ -2,7 +2,8 @@ using System; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.StaticFiles; +// using Microsoft.AspNetCore.StaticFiles; +using FolkerKinzel.MimeTypes; namespace SuCoS.ServerHandlers; @@ -43,7 +44,7 @@ public class StaticFileRequest : IServerHandlers { throw new ArgumentNullException(nameof(requestPath)); } - + var fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); if (context is null) { @@ -61,13 +62,6 @@ public class StaticFileRequest : IServerHandlers /// /// The path of the file. /// The content type of the file. - private static string GetContentType(string filePath) - { - var provider = new FileExtensionContentTypeProvider(); - if (!provider.TryGetContentType(filePath, out var contentType)) - { - contentType = "application/octet-stream"; - } - return contentType ?? "application/octet-stream"; - } + private static string GetContentType(string filePath) => + MimeString.FromFileName(filePath) ?? "application/octet-stream"; } diff --git a/source/SuCoS.csproj b/source/SuCoS.csproj index 62593b2..4abe6f1 100644 --- a/source/SuCoS.csproj +++ b/source/SuCoS.csproj @@ -14,9 +14,9 @@ + - -- GitLab From 2ff611a10560d8e8a2541ea7b205ed8d83632233 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Tue, 17 Oct 2023 08:54:31 -0300 Subject: [PATCH 2/3] fix: remove Microsoft.AspNetCore. using System.Net --- source/Helpers/SourceFileWatcher.cs | 2 +- source/Program.cs | 2 +- source/ServeCommand.cs | 212 ++++++++---------- source/ServerHandlers/IServerHandlers.cs | 4 +- source/ServerHandlers/PingRequests.cs | 8 +- .../ServerHandlers/RegisteredPageRequest.cs | 8 +- .../RegisteredPageResourceRequest.cs | 6 +- source/ServerHandlers/StaticFileRequest.cs | 12 +- source/SuCoS.csproj | 5 +- 9 files changed, 118 insertions(+), 141 deletions(-) diff --git a/source/Helpers/SourceFileWatcher.cs b/source/Helpers/SourceFileWatcher.cs index d26b5c2..3129fcd 100644 --- a/source/Helpers/SourceFileWatcher.cs +++ b/source/Helpers/SourceFileWatcher.cs @@ -44,7 +44,7 @@ public class SourceFileWatcher : IFileWatcher fileWatcher = new FileSystemWatcher { Path = SourceAbsolutePath, - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName, IncludeSubdirectories = true, EnableRaisingEvents = true }; diff --git a/source/Program.cs b/source/Program.cs index d067a46..a85f7da 100644 --- a/source/Program.cs +++ b/source/Program.cs @@ -115,7 +115,7 @@ public class Program }; var serveCommand = new ServeCommand(serverOptions, logger, new SourceFileWatcher()); - await serveCommand.RunServer(); + serveCommand.StartServer(); await Task.Delay(-1); // Wait forever. }, sourceOption, draftOption, futureOption, expiredOption, verboseOption); diff --git a/source/ServeCommand.cs b/source/ServeCommand.cs index eba8335..5307089 100644 --- a/source/ServeCommand.cs +++ b/source/ServeCommand.cs @@ -1,10 +1,8 @@ using System; using System.IO; +using System.Net; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Serilog; using SuCoS.Helpers; using SuCoS.Models.CommandLineOptions; @@ -28,23 +26,6 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// private readonly ServeOptions options; - /// - /// The global base URL for the server, combined with the port number. - /// This is constructed in the ServeCommand constructor using the provided base URL and port number, - /// and is subsequently used when starting or restarting the server. - /// - private readonly string baseURLGlobal; - - /// - /// An instance of IWebHost, which represents the running web server. - /// This instance is created every time the server starts or restarts and is used to manage - /// the lifecycle of the server. It's nullable because there may be periods when there is no running server, - /// such as during a restart operation or before the server has been initially started. - /// - private IWebHost? host; - - private IServerHandlers[]? handlers; - /// /// The FileSystemWatcher object that monitors the source directory for file changes. /// When a change is detected, this triggers a server restart to ensure the served content @@ -61,29 +42,37 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// private Timer? debounceTimer; - /// - /// A boolean flag indicating whether a server restart is currently in progress. - /// This is used to prevent overlapping restarts when file changes are detected in quick succession. - /// - private volatile bool restartInProgress; - /// /// A SemaphoreSlim used to ensure that server restarts due to file changes occur sequentially, /// not concurrently. This is necessary because a restart involves stopping the current server /// and starting a new one, which would not be thread-safe without some form of synchronization. /// - private readonly SemaphoreSlim restartServerLock = new(1, 1); + // private readonly SemaphoreSlim restartServerLock = new(1, 1); + private Task lastRestartTask = Task.CompletedTask; + + private HttpListener? listener; + + private IServerHandlers[]? handlers; private DateTime serverStartTime; + private Task? loop; + /// - /// Method to start the server explicitly + /// Constructor for the ServeCommand class. /// - /// - public async Task RunServer() + /// ServeOptions object specifying the serve options. + /// The logger instance. Injectable for testing + /// + public ServeCommand(ServeOptions options, ILogger logger, IFileWatcher fileWatcher) : base(options, logger) { - // Start the server! - await StartServer("http://localhost", 1122); + this.options = options ?? throw new ArgumentNullException(nameof(options)); + this.fileWatcher = fileWatcher ?? throw new ArgumentNullException(nameof(fileWatcher)); + + // Watch for file changes in the specified path + var SourceAbsolutePath = Path.GetFullPath(options.Source); + logger.Information("Watching for file changes in {SourceAbsolutePath}", SourceAbsolutePath); + fileWatcher.Start(SourceAbsolutePath, OnSourceFileChanged); } /// @@ -92,7 +81,7 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// The base URL for the server. /// The port number for the server. /// A Task representing the asynchronous operation. - private async Task StartServer(string baseURL, int port) + public void StartServer(string baseURL = baseURLDefault, int port = portDefault) { logger.Information("Starting server..."); @@ -108,87 +97,91 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable new RegisteredPageRequest(site), new RegisteredPageResourceRequest(site) }; + listener = new HttpListener(); + listener.Prefixes.Add($"{baseURL}:{port}/"); + listener.Start(); - host = new WebHostBuilder() - .UseKestrel() - .UseUrls(baseURLGlobal) - .UseContentRoot(options.Source) // Set the content root path - .UseEnvironment("Debug") // Set the hosting environment to Debug - .Configure(app => - { - app.Run(HandleRequest); // Call the custom method for handling requests - }) - .Build(); - - await host.StartAsync(); logger.Information("You site is live: {baseURL}:{port}", baseURL, port); + loop = Task.Run(async () => + { + while (listener is not null && listener.IsListening) + { + try + { + var context = await listener.GetContextAsync(); + await HandleRequest(context); + } + catch (HttpListenerException ex) + { + if (listener.IsListening) + { + logger.Error(ex, "Unexpected listener error."); + } + break; + } + catch (Exception ex) + { + if (listener.IsListening) + { + logger.Error(ex, "Error processing request."); + } + break; + } + } + }); } /// public void Dispose() { - host?.Dispose(); + listener?.Stop(); + listener?.Close(); fileWatcher.Stop(); debounceTimer?.Dispose(); GC.SuppressFinalize(this); } - /// - /// Constructor for the ServeCommand class. - /// - /// ServeOptions object specifying the serve options. - /// The logger instance. Injectable for testing - /// - public ServeCommand(ServeOptions options, ILogger logger, IFileWatcher fileWatcher) : base(options, logger) - { - this.options = options ?? throw new ArgumentNullException(nameof(options)); - this.fileWatcher = fileWatcher ?? throw new ArgumentNullException(nameof(fileWatcher)); - baseURLGlobal = $"{baseURLDefault}:{portDefault}"; - - // Watch for file changes in the specified path - var SourceAbsolutePath = Path.GetFullPath(options.Source); - logger.Information("Watching for file changes in {SourceAbsolutePath}", SourceAbsolutePath); - fileWatcher.Start(SourceAbsolutePath, OnSourceFileChanged); - } - /// /// Restarts the server asynchronously. /// private async Task RestartServer() { - // Check if another restart is already in progress - await restartServerLock.WaitAsync(); - - try + await lastRestartTask.ContinueWith(async _ => { - site = SiteHelper.Init(configFile, options, frontMatterParser, WhereParamsFilter, logger, stopwatch); + logger.Information($"Restarting server..."); - // Stop the server - if (host != null) + if (listener != null && listener.IsListening) { - await host.StopAsync(TimeSpan.FromSeconds(1)); - host.Dispose(); + listener.Stop(); + listener.Close(); + + if (loop is not null) + { + // Wait for the loop to finish processing any ongoing requests. + await loop; + loop.Dispose(); + } } - await StartServer("http://localhost", 1122); - } - finally - { - _ = restartServerLock.Release(); - } + + // Reinitialize the site + site = SiteHelper.Init(configFile, options, frontMatterParser, WhereParamsFilter, logger, stopwatch); + + StartServer(baseURLDefault, portDefault); + }); + + lastRestartTask = lastRestartTask.ContinueWith(t => t.Exception != null + ? throw t.Exception + : Task.CompletedTask); } /// /// Handles the HTTP request asynchronously. /// /// The HttpContext representing the current request. - private async Task HandleRequest(HttpContext context) + private async Task HandleRequest(HttpListenerContext context) { - var requestPath = context.Request.Path.Value; - if (string.IsNullOrEmpty(Path.GetExtension(context.Request.Path.Value)) && requestPath.Length > 1) - { - requestPath = requestPath.TrimEnd('/'); - } + var requestPath = context.Request.Url?.AbsolutePath ?? string.Empty; string? resultType = null; if (handlers is not null) @@ -197,7 +190,14 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable { if (item.Check(requestPath)) { - resultType = await item.Handle(context, requestPath, serverStartTime); + try + { + resultType = await item.Handle(context, requestPath, serverStartTime); + } + catch (Exception ex) + { + logger.Debug(ex, "Error handling the request."); + } break; } } @@ -211,10 +211,11 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable logger.Debug("Request {type}\tfor {RequestPath}", resultType, requestPath); } - private static async Task HandleNotFoundRequest(HttpContext context) + private static async Task HandleNotFoundRequest(HttpListenerContext context) { context.Response.StatusCode = 404; - await context.Response.WriteAsync("404 - File Not Found"); + using var writer = new StreamWriter(context.Response.OutputStream); + await writer.WriteAsync("404 - File Not Found"); } /// @@ -224,43 +225,16 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable /// The FileSystemEventArgs containing information about the file change. private void OnSourceFileChanged(object sender, FileSystemEventArgs e) { + if (e.FullPath.Contains("\\.git\\", StringComparison.InvariantCulture)) return; + // File changes are firing multiple events in a short time. // Debounce the event handler to prevent multiple events from firing in a short time debounceTimer?.Dispose(); debounceTimer = new Timer(DebounceCallback, e, TimeSpan.FromMilliseconds(1), Timeout.InfiniteTimeSpan); } - private void DebounceCallback(object? state) - { - if (state is not FileSystemEventArgs e) - { - return; - } - HandleFileChangeAsync(e).GetAwaiter().GetResult(); - } - - private async Task HandleFileChangeAsync(FileSystemEventArgs e) + private async void DebounceCallback(object? state) { - if (restartInProgress) - { - return; - } - - logger.Information("File change detected: {FullPath}", e.FullPath); - - restartInProgress = true; - try - { - await RestartServer(); - } - catch (Exception ex) - { - logger.Error(ex, "Failed to restart server."); - throw; - } - finally - { - restartInProgress = false; - } + await RestartServer(); } } diff --git a/source/ServerHandlers/IServerHandlers.cs b/source/ServerHandlers/IServerHandlers.cs index 3888700..715d617 100644 --- a/source/ServerHandlers/IServerHandlers.cs +++ b/source/ServerHandlers/IServerHandlers.cs @@ -1,6 +1,6 @@ using System; +using System.Net; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; namespace SuCoS.ServerHandlers; @@ -23,5 +23,5 @@ public interface IServerHandlers /// /// /// - Task Handle(HttpContext context, string requestPath, DateTime serverStartTime); + Task Handle(HttpListenerContext context, string requestPath, DateTime serverStartTime); } diff --git a/source/ServerHandlers/PingRequests.cs b/source/ServerHandlers/PingRequests.cs index 4ae76ff..a8f54ae 100644 --- a/source/ServerHandlers/PingRequests.cs +++ b/source/ServerHandlers/PingRequests.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; +using System.Net; +using System.IO; namespace SuCoS.ServerHandlers; @@ -16,14 +17,15 @@ public class PingRequests : IServerHandlers } /// - public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) + public async Task Handle(HttpListenerContext context, string requestPath, DateTime serverStartTime) { if (context is null) { throw new ArgumentNullException(nameof(context)); } var content = serverStartTime.ToString("o"); - await context.Response.WriteAsync(content); + using var writer = new StreamWriter(context.Response.OutputStream); + await writer.WriteAsync(content); return "ping"; } diff --git a/source/ServerHandlers/RegisteredPageRequest.cs b/source/ServerHandlers/RegisteredPageRequest.cs index b98870c..555d203 100644 --- a/source/ServerHandlers/RegisteredPageRequest.cs +++ b/source/ServerHandlers/RegisteredPageRequest.cs @@ -2,7 +2,7 @@ using System; using System.IO; using System.Reflection; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; +using System.Net; using SuCoS.Models; namespace SuCoS.ServerHandlers; @@ -34,18 +34,18 @@ public class RegisteredPageRequest : IServerHandlers } /// - public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) + public async Task Handle(HttpListenerContext context, string requestPath, DateTime serverStartTime) { if (context is null) { throw new ArgumentNullException(nameof(context)); } - if (site.OutputReferences.TryGetValue(requestPath, out var output) && output is IPage page) { var content = page.CompleteContent; content = InjectReloadScript(content); - await context.Response.WriteAsync(content); + using var writer = new StreamWriter(context.Response.OutputStream); + await writer.WriteAsync(content); return "dict"; } else diff --git a/source/ServerHandlers/RegisteredPageResourceRequest.cs b/source/ServerHandlers/RegisteredPageResourceRequest.cs index 595c02e..4afb0bd 100644 --- a/source/ServerHandlers/RegisteredPageResourceRequest.cs +++ b/source/ServerHandlers/RegisteredPageResourceRequest.cs @@ -2,7 +2,7 @@ using System; using System.IO; using System.Reflection; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; +using System.Net; using SuCoS.Models; namespace SuCoS.ServerHandlers; @@ -34,7 +34,7 @@ public class RegisteredPageResourceRequest : IServerHandlers } /// - public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) + public async Task Handle(HttpListenerContext context, string requestPath, DateTime serverStartTime) { if (context is null) { @@ -45,7 +45,7 @@ public class RegisteredPageResourceRequest : IServerHandlers { context.Response.ContentType = resource.MimeType; await using var fileStream = new FileStream(resource.SourceFullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - await fileStream.CopyToAsync(context.Response.Body); + await fileStream.CopyToAsync(context.Response.OutputStream); return "resource"; } else diff --git a/source/ServerHandlers/StaticFileRequest.cs b/source/ServerHandlers/StaticFileRequest.cs index ef330df..2ea8b40 100644 --- a/source/ServerHandlers/StaticFileRequest.cs +++ b/source/ServerHandlers/StaticFileRequest.cs @@ -1,8 +1,7 @@ using System; using System.IO; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -// using Microsoft.AspNetCore.StaticFiles; +using System.Net; using FolkerKinzel.MimeTypes; namespace SuCoS.ServerHandlers; @@ -38,21 +37,22 @@ public class StaticFileRequest : IServerHandlers } /// - public async Task Handle(HttpContext context, string requestPath, DateTime serverStartTime) + public async Task Handle(HttpListenerContext context, string requestPath, DateTime serverStartTime) { if (requestPath is null) { throw new ArgumentNullException(nameof(requestPath)); } - - var fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); if (context is null) { throw new ArgumentNullException(nameof(context)); } + + var fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); context.Response.ContentType = GetContentType(fileAbsolutePath!); await using var fileStream = new FileStream(fileAbsolutePath!, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - await fileStream.CopyToAsync(context.Response.Body); + context.Response.ContentLength64 = fileStream.Length; + await fileStream.CopyToAsync(context.Response.OutputStream); return inTheme ? "themeSt" : "static"; } diff --git a/source/SuCoS.csproj b/source/SuCoS.csproj index 4abe6f1..18fec05 100644 --- a/source/SuCoS.csproj +++ b/source/SuCoS.csproj @@ -16,11 +16,12 @@ - + - + + -- GitLab From a9bc1da20d101dd6a816e2bc1537bbfc37d101f5 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Tue, 17 Oct 2023 19:54:44 -0300 Subject: [PATCH 3/3] fix(test): IHttpListenerResponse to wrap the HttpListenerResponse --- source/ServeCommand.cs | 18 +++++++-- .../HttpListenerResponseWrapper.cs | 38 +++++++++++++++++++ .../ServerHandlers/IHttpListenerResponse.cs | 24 ++++++++++++ source/ServerHandlers/IServerHandlers.cs | 5 +-- source/ServerHandlers/PingRequests.cs | 11 +++--- .../ServerHandlers/RegisteredPageRequest.cs | 9 ++--- .../RegisteredPageResourceRequest.cs | 11 +++--- source/ServerHandlers/StaticFileRequest.cs | 14 +++---- .../ServerHandlers/PingRequestHandlerTests.cs | 12 +++--- .../RegisteredPageRequestHandlerTests.cs | 16 +++++--- .../StaticFileRequestHandlerTests.cs | 14 ++++--- 11 files changed, 126 insertions(+), 46 deletions(-) create mode 100644 source/ServerHandlers/HttpListenerResponseWrapper.cs create mode 100644 source/ServerHandlers/IHttpListenerResponse.cs diff --git a/source/ServeCommand.cs b/source/ServeCommand.cs index 5307089..e0b6668 100644 --- a/source/ServeCommand.cs +++ b/source/ServeCommand.cs @@ -1,8 +1,10 @@ using System; using System.IO; using System.Net; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualBasic; using Serilog; using SuCoS.Helpers; using SuCoS.Models.CommandLineOptions; @@ -186,13 +188,19 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable string? resultType = null; if (handlers is not null) { - foreach (var item in handlers) + try { - if (item.Check(requestPath)) + var response = new HttpListenerResponseWrapper(context.Response); + foreach (var item in handlers) { + if (!item.Check(requestPath)) + { + continue; + } + try { - resultType = await item.Handle(context, requestPath, serverStartTime); + resultType = await item.Handle(response, requestPath, serverStartTime); } catch (Exception ex) { @@ -201,6 +209,10 @@ public class ServeCommand : BaseGeneratorCommand, IDisposable break; } } + finally + { + context.Response.OutputStream.Close(); + } } if (resultType is null) diff --git a/source/ServerHandlers/HttpListenerResponseWrapper.cs b/source/ServerHandlers/HttpListenerResponseWrapper.cs new file mode 100644 index 0000000..5bd1cd6 --- /dev/null +++ b/source/ServerHandlers/HttpListenerResponseWrapper.cs @@ -0,0 +1,38 @@ +using System.IO; +using System.Net; + +namespace SuCoS.ServerHandlers; + +/// +/// Provides a wrapper for the . +/// +public class HttpListenerResponseWrapper : IHttpListenerResponse +{ + private readonly HttpListenerResponse response; + + /// + public Stream OutputStream => response.OutputStream; + + /// + public string? ContentType + { + get => response.ContentType; + set => response.ContentType = value; + } + + /// + public long ContentLength64 + { + get => response.ContentLength64; + set => response.ContentLength64 = value; + } + + /// + /// Wrap the HttpListenerResponse + /// + /// + public HttpListenerResponseWrapper(HttpListenerResponse response) + { + this.response = response; + } +} diff --git a/source/ServerHandlers/IHttpListenerResponse.cs b/source/ServerHandlers/IHttpListenerResponse.cs new file mode 100644 index 0000000..34f1a82 --- /dev/null +++ b/source/ServerHandlers/IHttpListenerResponse.cs @@ -0,0 +1,24 @@ +using System.IO; + +namespace SuCoS.ServerHandlers; + +/// +/// Defines the contract for an HTTP listener response. +/// +public interface IHttpListenerResponse +{ + /// + /// Gets the output stream for writing the response body. + /// + Stream OutputStream { get; } + + /// + /// Gets or sets the content type of the response. + /// + string? ContentType { get; set; } + + /// + /// Gets or sets the length of the content to be sent. + /// + long ContentLength64 { get; set; } +} diff --git a/source/ServerHandlers/IServerHandlers.cs b/source/ServerHandlers/IServerHandlers.cs index 715d617..a354f51 100644 --- a/source/ServerHandlers/IServerHandlers.cs +++ b/source/ServerHandlers/IServerHandlers.cs @@ -1,5 +1,4 @@ using System; -using System.Net; using System.Threading.Tasks; namespace SuCoS.ServerHandlers; @@ -19,9 +18,9 @@ public interface IServerHandlers /// /// Process the request /// - /// + /// /// /// /// - Task Handle(HttpListenerContext context, string requestPath, DateTime serverStartTime); + Task Handle(IHttpListenerResponse response, string requestPath, DateTime serverStartTime); } diff --git a/source/ServerHandlers/PingRequests.cs b/source/ServerHandlers/PingRequests.cs index a8f54ae..7465585 100644 --- a/source/ServerHandlers/PingRequests.cs +++ b/source/ServerHandlers/PingRequests.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using System.Net; using System.IO; namespace SuCoS.ServerHandlers; @@ -17,15 +16,17 @@ public class PingRequests : IServerHandlers } /// - public async Task Handle(HttpListenerContext context, string requestPath, DateTime serverStartTime) + public async Task Handle(IHttpListenerResponse response, string requestPath, DateTime serverStartTime) { - if (context is null) + if (response is null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(response)); } var content = serverStartTime.ToString("o"); - using var writer = new StreamWriter(context.Response.OutputStream); + + using var writer = new StreamWriter(response.OutputStream, leaveOpen: true); await writer.WriteAsync(content); + await writer.FlushAsync(); return "ping"; } diff --git a/source/ServerHandlers/RegisteredPageRequest.cs b/source/ServerHandlers/RegisteredPageRequest.cs index 555d203..ad6e48e 100644 --- a/source/ServerHandlers/RegisteredPageRequest.cs +++ b/source/ServerHandlers/RegisteredPageRequest.cs @@ -2,7 +2,6 @@ using System; using System.IO; using System.Reflection; using System.Threading.Tasks; -using System.Net; using SuCoS.Models; namespace SuCoS.ServerHandlers; @@ -34,17 +33,17 @@ public class RegisteredPageRequest : IServerHandlers } /// - public async Task Handle(HttpListenerContext context, string requestPath, DateTime serverStartTime) + public async Task Handle(IHttpListenerResponse response, string requestPath, DateTime serverStartTime) { - if (context is null) + if (response is null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(response)); } if (site.OutputReferences.TryGetValue(requestPath, out var output) && output is IPage page) { var content = page.CompleteContent; content = InjectReloadScript(content); - using var writer = new StreamWriter(context.Response.OutputStream); + using var writer = new StreamWriter(response.OutputStream, leaveOpen: true); await writer.WriteAsync(content); return "dict"; } diff --git a/source/ServerHandlers/RegisteredPageResourceRequest.cs b/source/ServerHandlers/RegisteredPageResourceRequest.cs index 4afb0bd..c61183a 100644 --- a/source/ServerHandlers/RegisteredPageResourceRequest.cs +++ b/source/ServerHandlers/RegisteredPageResourceRequest.cs @@ -2,7 +2,6 @@ using System; using System.IO; using System.Reflection; using System.Threading.Tasks; -using System.Net; using SuCoS.Models; namespace SuCoS.ServerHandlers; @@ -34,18 +33,18 @@ public class RegisteredPageResourceRequest : IServerHandlers } /// - public async Task Handle(HttpListenerContext context, string requestPath, DateTime serverStartTime) + public async Task Handle(IHttpListenerResponse response, string requestPath, DateTime serverStartTime) { - if (context is null) + if (response is null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(response)); } if (site.OutputReferences.TryGetValue(requestPath, out var output) && output is IResource resource) { - context.Response.ContentType = resource.MimeType; + response.ContentType = resource.MimeType; await using var fileStream = new FileStream(resource.SourceFullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - await fileStream.CopyToAsync(context.Response.OutputStream); + await fileStream.CopyToAsync(response.OutputStream); return "resource"; } else diff --git a/source/ServerHandlers/StaticFileRequest.cs b/source/ServerHandlers/StaticFileRequest.cs index 2ea8b40..16e6a0b 100644 --- a/source/ServerHandlers/StaticFileRequest.cs +++ b/source/ServerHandlers/StaticFileRequest.cs @@ -37,22 +37,22 @@ public class StaticFileRequest : IServerHandlers } /// - public async Task Handle(HttpListenerContext context, string requestPath, DateTime serverStartTime) + public async Task Handle(IHttpListenerResponse response, string requestPath, DateTime serverStartTime) { if (requestPath is null) { throw new ArgumentNullException(nameof(requestPath)); } - if (context is null) + if (response is null) { - throw new ArgumentNullException(nameof(context)); + throw new ArgumentNullException(nameof(response)); } - var fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); - context.Response.ContentType = GetContentType(fileAbsolutePath!); + var fileAbsolutePath = Path.Combine(basePath, requestPath.TrimStart('/')); + response.ContentType = GetContentType(fileAbsolutePath!); await using var fileStream = new FileStream(fileAbsolutePath!, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - context.Response.ContentLength64 = fileStream.Length; - await fileStream.CopyToAsync(context.Response.OutputStream); + response.ContentLength64 = fileStream.Length; + await fileStream.CopyToAsync(response.OutputStream); return inTheme ? "themeSt" : "static"; } diff --git a/test/ServerHandlers/PingRequestHandlerTests.cs b/test/ServerHandlers/PingRequestHandlerTests.cs index af5cfb2..0d7ad20 100644 --- a/test/ServerHandlers/PingRequestHandlerTests.cs +++ b/test/ServerHandlers/PingRequestHandlerTests.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using NSubstitute; using SuCoS.ServerHandlers; using Xunit; @@ -10,15 +10,14 @@ public class PingRequestHandlerTests : TestSetup public async Task Handle_ReturnsServerStartupTimestamp() { // Arrange - var context = new DefaultHttpContext(); - + var response = Substitute.For(); var stream = new MemoryStream(); - context.Response.Body = stream; + response.OutputStream.Returns(stream); var pingRequests = new PingRequests(); // Act - await pingRequests.Handle(context, "ping", todayDate); + var code = await pingRequests.Handle(response, "ping", todayDate); // Assert stream.Seek(0, SeekOrigin.Begin); @@ -26,8 +25,9 @@ public class PingRequestHandlerTests : TestSetup var content = await reader.ReadToEndAsync(); Assert.Equal(todayDate.ToString("o"), content); - } + Assert.Equal("ping", code); + } [Theory] [InlineData("/ping", true)] diff --git a/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs b/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs index eb2a175..bce4b15 100644 --- a/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs +++ b/test/ServerHandlers/RegisteredPageRequestHandlerTests.cs @@ -1,12 +1,15 @@ -using Microsoft.AspNetCore.Http; +using NSubstitute; using SuCoS.Models.CommandLineOptions; using SuCoS.ServerHandlers; +using System.Net; using Xunit; namespace Tests.ServerHandlers; public class RegisteredPageRequestHandlerTests : TestSetup { + private readonly HttpClient _httpClient = new HttpClient(); + [Theory] [InlineData("/", true)] [InlineData("/testPage", false)] @@ -40,20 +43,23 @@ public class RegisteredPageRequestHandlerTests : TestSetup }; var registeredPageRequest = new RegisteredPageRequest(site); - var context = new DefaultHttpContext(); + var response = Substitute.For(); var stream = new MemoryStream(); - context.Response.Body = stream; + response.OutputStream.Returns(stream); // Act site.ParseAndScanSourceFiles(Path.Combine(siteFullPath, "content")); registeredPageRequest.Check(requestPath); - await registeredPageRequest.Handle(context, requestPath, DateTime.Now); + var code = await registeredPageRequest.Handle(response, requestPath, DateTime.Now); // Assert stream.Seek(0, SeekOrigin.Begin); using var reader = new StreamReader(stream); var content = await reader.ReadToEndAsync(); + Assert.Equal("dict", code); + + // Assert // You may want to adjust this assertion depending on the actual format of your injected script if (contains) { @@ -66,7 +72,5 @@ public class RegisteredPageRequestHandlerTests : TestSetup Assert.DoesNotContain("", content, StringComparison.InvariantCulture); } Assert.Contains("Index Content", content, StringComparison.InvariantCulture); - } - } diff --git a/test/ServerHandlers/StaticFileRequestHandlerTests.cs b/test/ServerHandlers/StaticFileRequestHandlerTests.cs index eea246a..6e61839 100644 --- a/test/ServerHandlers/StaticFileRequestHandlerTests.cs +++ b/test/ServerHandlers/StaticFileRequestHandlerTests.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using NSubstitute; using SuCoS.ServerHandlers; using Xunit; @@ -7,6 +7,7 @@ namespace Tests.ServerHandlers; public class StaticFileRequestHandlerTests : TestSetup, IDisposable { private readonly string tempFilePath; + private readonly HttpClient _httpClient = new HttpClient(); public StaticFileRequestHandlerTests() : base() { @@ -26,7 +27,7 @@ public class StaticFileRequestHandlerTests : TestSetup, IDisposable var staticFileRequest = new StaticFileRequest(basePath, false); // Act - var result = staticFileRequest.Check(requestPath); + var result = staticFileRequest.Check(requestPath: requestPath); // Assert Assert.True(result); @@ -41,13 +42,14 @@ public class StaticFileRequestHandlerTests : TestSetup, IDisposable ?? throw new InvalidOperationException("Unable to determine directory of temporary file."); var staticFileRequest = new StaticFileRequest(basePath, true); - var context = new DefaultHttpContext(); + + var response = Substitute.For(); var stream = new MemoryStream(); - context.Response.Body = stream; + response.OutputStream.Returns(stream); // Act staticFileRequest.Check(requestPath); - await staticFileRequest.Handle(context, requestPath, DateTime.Now); + var code = await staticFileRequest.Handle(response, requestPath, DateTime.Now); // Assert stream.Seek(0, SeekOrigin.Begin); @@ -55,6 +57,8 @@ public class StaticFileRequestHandlerTests : TestSetup, IDisposable var content = await reader.ReadToEndAsync(); Assert.Equal("test", content); + + Assert.Equal("themeSt", code); } public void Dispose() -- GitLab