diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6732295..2e5b1c3 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "csharpier": { - "version": "0.29.1", + "version": "0.30.1", "commands": [ "dotnet-csharpier" ], diff --git a/.editorconfig b/.editorconfig index 3f80c7c..0d2cc07 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,3 +16,10 @@ insert_final_newline = true [*.md] trim_trailing_whitespace = false + +[*.{cs,vb}] +csharp_style_namespace_declarations = file_scoped +csharp_style_var_for_built_in_types = true +csharp_style_var_elsewhere = true + + diff --git a/.github/workflows/build-on-pull-request.yml b/.github/workflows/build-on-pull-request.yml index 81a7618..ac31d3e 100644 --- a/.github/workflows/build-on-pull-request.yml +++ b/.github/workflows/build-on-pull-request.yml @@ -16,7 +16,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.x' + dotnet-version: '9.x' - name: Restore dotnet tools run: dotnet tool restore diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a33b8a4..e70ac46 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@main # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL @@ -57,9 +57,9 @@ jobs: - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.x' + dotnet-version: '9.x' - name: Build with dotnet run: dotnet build --configuration Release diff --git a/.github/workflows/main-deploy-nuget.yml b/.github/workflows/main-deploy-nuget.yml index 8c67cfa..e0b8b9e 100644 --- a/.github/workflows/main-deploy-nuget.yml +++ b/.github/workflows/main-deploy-nuget.yml @@ -14,7 +14,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.x' + dotnet-version: '9.x' - name: Package BlazorReports run: dotnet pack src/BlazorReports diff --git a/BlazorReports.sln b/BlazorReports.sln index 920a23a..fb6f2d8 100644 --- a/BlazorReports.sln +++ b/BlazorReports.sln @@ -16,6 +16,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{C5 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleReportServer", "examples\SimpleReportServer\SimpleReportServer.csproj", "{1C60A5A2-46DB-419D-BED1-5C97465076D0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleTemplates", "examples\ExampleTemplates\ExampleTemplates.csproj", "{B0C3AFB8-AA52-4947-863F-9DEEC9D889AE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,6 +44,10 @@ Global {1C60A5A2-46DB-419D-BED1-5C97465076D0}.Debug|Any CPU.Build.0 = Debug|Any CPU {1C60A5A2-46DB-419D-BED1-5C97465076D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {1C60A5A2-46DB-419D-BED1-5C97465076D0}.Release|Any CPU.Build.0 = Release|Any CPU + {B0C3AFB8-AA52-4947-863F-9DEEC9D889AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0C3AFB8-AA52-4947-863F-9DEEC9D889AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0C3AFB8-AA52-4947-863F-9DEEC9D889AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0C3AFB8-AA52-4947-863F-9DEEC9D889AE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {2AA5462E-57F7-4A4B-A1BB-1BFF901A7C22} = {84E7AF5B-BDD3-4FB3-A798-7E8541EE7A1E} @@ -49,5 +55,6 @@ Global {DAF00FAE-4B76-4FA6-9690-14F12DB123DB} = {84E7AF5B-BDD3-4FB3-A798-7E8541EE7A1E} {35F37EF3-9BFD-4C4A-ACDA-761D62E3E940} = {84E7AF5B-BDD3-4FB3-A798-7E8541EE7A1E} {1C60A5A2-46DB-419D-BED1-5C97465076D0} = {C5BFD20E-2F4E-4EA8-AC79-82E91F93E70F} + {B0C3AFB8-AA52-4947-863F-9DEEC9D889AE} = {C5BFD20E-2F4E-4EA8-AC79-82E91F93E70F} EndGlobalSection EndGlobal diff --git a/examples/ExampleTemplates/Components/ExampleHeader.razor b/examples/ExampleTemplates/Components/ExampleHeader.razor new file mode 100644 index 0000000..c9051cb --- /dev/null +++ b/examples/ExampleTemplates/Components/ExampleHeader.razor @@ -0,0 +1,7 @@ +
+

Testing Header

+
+ +@code { + +} diff --git a/examples/ExampleTemplates/ExampleTemplates.csproj b/examples/ExampleTemplates/ExampleTemplates.csproj new file mode 100644 index 0000000..12d8ada --- /dev/null +++ b/examples/ExampleTemplates/ExampleTemplates.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + true + + + + + + + + + + + + diff --git a/examples/ExampleTemplates/Reports/ReportWithRepeatingHeaderPerPage.razor b/examples/ExampleTemplates/Reports/ReportWithRepeatingHeaderPerPage.razor new file mode 100644 index 0000000..a27e99b --- /dev/null +++ b/examples/ExampleTemplates/Reports/ReportWithRepeatingHeaderPerPage.razor @@ -0,0 +1,50 @@ +@* This component will render a repeating header on each page when printed *@ + +@* This component will render a repeating header on each page when printed *@ + + + + + +@code { + +} diff --git a/examples/ExampleTemplates/_Imports.razor b/examples/ExampleTemplates/_Imports.razor new file mode 100644 index 0000000..7728512 --- /dev/null +++ b/examples/ExampleTemplates/_Imports.razor @@ -0,0 +1 @@ +@using Microsoft.AspNetCore.Components.Web diff --git a/examples/ExampleTemplates/wwwroot/background.png b/examples/ExampleTemplates/wwwroot/background.png new file mode 100644 index 0000000..e15a3bd Binary files /dev/null and b/examples/ExampleTemplates/wwwroot/background.png differ diff --git a/examples/ExampleTemplates/wwwroot/exampleJsInterop.js b/examples/ExampleTemplates/wwwroot/exampleJsInterop.js new file mode 100644 index 0000000..ea8d76a --- /dev/null +++ b/examples/ExampleTemplates/wwwroot/exampleJsInterop.js @@ -0,0 +1,6 @@ +// This is a JavaScript module that is loaded on demand. It can export any number of +// functions, and may import other JavaScript modules if required. + +export function showPrompt(message) { + return prompt(message, 'Type anything here'); +} diff --git a/examples/SimpleReportServer/Program.cs b/examples/SimpleReportServer/Program.cs index 17a379b..d4eaaa8 100644 --- a/examples/SimpleReportServer/Program.cs +++ b/examples/SimpleReportServer/Program.cs @@ -1,12 +1,16 @@ using BlazorReports.Extensions; using BlazorReports.Models; +using ExampleTemplates.Reports; using SimpleReportServer; var builder = WebApplication.CreateSlimBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -builder.Services.AddBlazorReports(); +builder.Services.AddBlazorReports(opts => +{ + opts.BrowserOptions.ResponseTimeout = TimeSpan.FromSeconds(90); +}); var app = builder.Build(); @@ -16,12 +20,17 @@ app.UseSwaggerUI(); } -app.MapGroup("reports").MapBlazorReport(); -app.MapGroup("reports") - .MapBlazorReport(opts => - { - opts.ReportName = "HelloReportHtml"; - opts.OutputFormat = ReportOutputFormat.Html; - }); +var reportsGroup = app.MapGroup("reports"); + +reportsGroup.MapBlazorReport(); +reportsGroup.MapBlazorReport(opts => +{ + opts.ReportName = "HelloReportHtml"; + opts.OutputFormat = ReportOutputFormat.Html; +}); +reportsGroup.MapBlazorReport(opts => +{ + opts.OutputFormat = ReportOutputFormat.Pdf; +}); app.Run(); diff --git a/examples/SimpleReportServer/SimpleReportServer.csproj b/examples/SimpleReportServer/SimpleReportServer.csproj index 4d5d235..20323c7 100644 --- a/examples/SimpleReportServer/SimpleReportServer.csproj +++ b/examples/SimpleReportServer/SimpleReportServer.csproj @@ -1,16 +1,19 @@ - net8.0 + net9.0 + true + $(NoWarn);CS1591 - - + + + diff --git a/src/BlazorReports.Client/BlazorReports.Client.csproj b/src/BlazorReports.Client/BlazorReports.Client.csproj index 292a753..9a1b332 100644 --- a/src/BlazorReports.Client/BlazorReports.Client.csproj +++ b/src/BlazorReports.Client/BlazorReports.Client.csproj @@ -1,14 +1,12 @@  - - net6.0 - enable - enable - + + net8.0 + - - - - - + + + + + diff --git a/src/BlazorReports.Components/BlazorReports.Components.csproj b/src/BlazorReports.Components/BlazorReports.Components.csproj index 89c122a..1e41b8c 100644 --- a/src/BlazorReports.Components/BlazorReports.Components.csproj +++ b/src/BlazorReports.Components/BlazorReports.Components.csproj @@ -1,34 +1,27 @@ - net6.0;net7.0;net8.0 - enable - enable + net8.0;net9.0 true - true $(NoWarn);NU5104 Blazor Reports Components Blazor components for BlazorReports. Generate PDF reports using Blazor Components. Easily create a report server or generate reports from existing projects. - + - + - - - - - - + + - - + + diff --git a/src/BlazorReports.Viewer/BlazorReports.Viewer.csproj b/src/BlazorReports.Viewer/BlazorReports.Viewer.csproj index c46ef51..308f30e 100644 --- a/src/BlazorReports.Viewer/BlazorReports.Viewer.csproj +++ b/src/BlazorReports.Viewer/BlazorReports.Viewer.csproj @@ -2,8 +2,6 @@ net8.0 - enable - enable diff --git a/src/BlazorReports/BlazorReports.csproj b/src/BlazorReports/BlazorReports.csproj index 9900985..6939b4c 100644 --- a/src/BlazorReports/BlazorReports.csproj +++ b/src/BlazorReports/BlazorReports.csproj @@ -1,9 +1,7 @@  - net8.0 - enable - enable + net8.0;net9.0 true Blazor Reports Generate PDF reports using Blazor Components. Easily create a report server or generate reports from existing projects. @@ -19,7 +17,7 @@ - + diff --git a/src/BlazorReports/Extensions/ReportExtensions.cs b/src/BlazorReports/Extensions/ReportExtensions.cs index 44078b2..3d360a6 100644 --- a/src/BlazorReports/Extensions/ReportExtensions.cs +++ b/src/BlazorReports/Extensions/ReportExtensions.cs @@ -193,7 +193,7 @@ private static BlazorReportRegistrationOptions GetReportRegistrationOptions( Action? setupAction = null ) { - var options = new BlazorReportRegistrationOptions(); + BlazorReportRegistrationOptions options = new(); var globalOptions = serviceScope .ServiceProvider.GetRequiredService>() .Value; diff --git a/src/BlazorReports/Helpers/MimeTypes.cs b/src/BlazorReports/Helpers/MimeTypes.cs index 9eb14c7..7601e0f 100644 --- a/src/BlazorReports/Helpers/MimeTypes.cs +++ b/src/BlazorReports/Helpers/MimeTypes.cs @@ -5,18 +5,17 @@ /// internal static class MimeTypes { - private static readonly Dictionary MimeTypesDictionary = - new() - { - { ".txt", "text/plain" }, - { ".pdf", "application/pdf" }, - { ".csv", "text/csv" }, - { ".png", "image/png" }, - { ".jpg", "image/jpeg" }, - { ".jpeg", "image/jpeg" }, - { ".gif", "image/gif" }, - { ".webp", "image/webp" }, - }; + private static readonly Dictionary MimeTypesDictionary = new() + { + { ".txt", "text/plain" }, + { ".pdf", "application/pdf" }, + { ".csv", "text/csv" }, + { ".png", "image/png" }, + { ".jpg", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".gif", "image/gif" }, + { ".webp", "image/webp" }, + }; private const string UnknownMimeType = "application/octet-stream"; @@ -30,16 +29,8 @@ public static string GetMimeType(string fileName) { var extension = Path.GetExtension(fileName).ToLowerInvariant(); - if (string.IsNullOrEmpty(extension)) - { - return UnknownMimeType; - } - - if (!MimeTypesDictionary.TryGetValue(extension, out var mimeType)) - { - return UnknownMimeType; - } - - return mimeType; + return string.IsNullOrEmpty(extension) + ? UnknownMimeType + : MimeTypesDictionary.GetValueOrDefault(extension, UnknownMimeType); } } diff --git a/src/BlazorReports/Models/BlazorReportRegistry.cs b/src/BlazorReports/Models/BlazorReportRegistry.cs index 012b5b9..fec0360 100644 --- a/src/BlazorReports/Models/BlazorReportRegistry.cs +++ b/src/BlazorReports/Models/BlazorReportRegistry.cs @@ -22,7 +22,7 @@ public BlazorReportRegistry(IOptions options) if (!string.IsNullOrWhiteSpace(options.Value.AssetsPath)) { var assetsPath = options.Value.AssetsPath; - var assetsDirectory = new DirectoryInfo(assetsPath); + DirectoryInfo assetsDirectory = new(assetsPath); if (assetsDirectory.Exists) { foreach (var file in assetsDirectory.GetFiles()) @@ -51,12 +51,12 @@ public BlazorReportRegistry(IOptions options) /// /// The global assets for the BlazorReportRegistry. /// - public Dictionary GlobalAssets { get; set; } = new(); + public Dictionary GlobalAssets { get; set; } = []; /// /// The BlazorReport objects for the BlazorReportRegistry. /// - public Dictionary Reports { get; } = new(); + public Dictionary Reports { get; } = []; /// /// Adds a report to the BlazorReportRegistry. @@ -71,10 +71,13 @@ public BlazorReport AddReport(BlazorReportRegistrationOptions? options = null var normalizedReportName = reportNameToUse.ToLowerInvariant().Trim(); if (Reports.ContainsKey(normalizedReportName)) + { throw new InvalidOperationException( $"Report with name {normalizedReportName} already exists" ); - var blazorReport = new BlazorReport + } + + BlazorReport blazorReport = new() { OutputFormat = options?.OutputFormat ?? ReportOutputFormat.Pdf, Name = reportNameToUse, @@ -90,7 +93,7 @@ public BlazorReport AddReport(BlazorReportRegistrationOptions? options = null if (!string.IsNullOrEmpty(options?.AssetsPath)) { var assetsPath = options.AssetsPath; - var assetsDirectory = new DirectoryInfo(assetsPath); + DirectoryInfo assetsDirectory = new(assetsPath); if (assetsDirectory.Exists) { foreach (var file in assetsDirectory.GetFiles()) @@ -120,10 +123,13 @@ public BlazorReport AddReport(BlazorReportRegistrationOptions? options = var normalizedReportName = reportNameToUse.ToLowerInvariant().Trim(); if (Reports.ContainsKey(normalizedReportName)) + { throw new InvalidOperationException( $"Report with name {normalizedReportName} already exists" ); - var blazorReport = new BlazorReport + } + + BlazorReport blazorReport = new() { OutputFormat = options?.OutputFormat ?? ReportOutputFormat.Pdf, Name = reportNameToUse, @@ -139,7 +145,7 @@ public BlazorReport AddReport(BlazorReportRegistrationOptions? options = if (!string.IsNullOrEmpty(options?.AssetsPath)) { var assetsPath = options.AssetsPath; - var assetsDirectory = new DirectoryInfo(assetsPath); + DirectoryInfo assetsDirectory = new(assetsPath); if (assetsDirectory.Exists) { foreach (var file in assetsDirectory.GetFiles()) diff --git a/src/BlazorReports/Services/BrowserServices/Browser.cs b/src/BlazorReports/Services/BrowserServices/Browser.cs index afcf6f3..ed94142 100644 --- a/src/BlazorReports/Services/BrowserServices/Browser.cs +++ b/src/BlazorReports/Services/BrowserServices/Browser.cs @@ -79,7 +79,9 @@ out browserPage } if (operationCancelled) + { return new OperationCancelledProblem(); + } if (retryCount >= maxRetryCount) { @@ -108,7 +110,9 @@ out browserPage finally { if (browserPage is not null && !browserPagedDisposed) + { ReturnBrowserPage(browserPage); + } } return new Success(); @@ -169,11 +173,14 @@ private async ValueTask DisposeBrowserPage(BrowserPage browserPage) try { if (_browserPagePool.Contains(browserPage)) + { return; + } + await browserPage.DisposeAsync(); _currentBrowserPagePoolSize--; - var closeTargetMessage = new BrowserMessage("Target.closeTarget"); + BrowserMessage closeTargetMessage = new("Target.closeTarget"); closeTargetMessage.Parameters.Add("targetId", browserPage.TargetId); await connection.ConnectAsync(); connection.SendAsync(closeTargetMessage); @@ -191,7 +198,7 @@ private async ValueTask DisposeBrowserPage(BrowserPage browserPage) /// The browser page private async ValueTask CreateBrowserPage(CancellationToken stoppingToken = default) { - var createTargetMessage = new BrowserMessage("Target.createTarget"); + BrowserMessage createTargetMessage = new("Target.createTarget"); createTargetMessage.Parameters.Add("url", "about:blank"); await connection.ConnectAsync(stoppingToken); return await connection.SendAsync( @@ -220,11 +227,16 @@ public async ValueTask DisposeAsync() LogMessages.BrowserDispose(logger, chromiumProcess.Id); _poolLock.Dispose(); foreach (var browserPage in _browserPagePool) + { await browserPage.DisposeAsync(); + } + chromiumProcess.Kill(); chromiumProcess.Dispose(); await connection.DisposeAsync(); if (dataDirectory.Exists) + { Directory.Delete(dataDirectory.FullName, true); + } } } diff --git a/src/BlazorReports/Services/BrowserServices/BrowserPage.cs b/src/BlazorReports/Services/BrowserServices/BrowserPage.cs index 322978f..e2df393 100644 --- a/src/BlazorReports/Services/BrowserServices/BrowserPage.cs +++ b/src/BlazorReports/Services/BrowserServices/BrowserPage.cs @@ -30,8 +30,9 @@ Connection connection /// The id of the page in the browser /// internal readonly string TargetId = targetId; - private readonly CustomFromBase64Transform _transform = - new(FromBase64TransformMode.IgnoreWhiteSpaces); + private readonly CustomFromBase64Transform _transform = new( + FromBase64TransformMode.IgnoreWhiteSpaces + ); /// /// Displays the HTML in the browser @@ -43,14 +44,16 @@ internal async Task DisplayHtml(string html, CancellationToken stoppingToken = d { await connection.ConnectAsync(stoppingToken); if (string.IsNullOrWhiteSpace(html)) + { throw new ArgumentException("Value cannot be null or whitespace.", nameof(html)); + } // Enables or disables the cache - var cacheMessage = new BrowserMessage("Network.setCacheDisabled"); + BrowserMessage cacheMessage = new("Network.setCacheDisabled"); cacheMessage.Parameters.Add("cacheDisabled", false); connection.SendAsync(cacheMessage); - var getPageFrameTreeMessage = new BrowserMessage("Page.getFrameTree"); + BrowserMessage getPageFrameTreeMessage = new("Page.getFrameTree"); await connection.SendAsync( getPageFrameTreeMessage, PageGetFrameTreeResponseSerializationContext @@ -58,7 +61,7 @@ await connection.SendAsync( .BrowserResultResponsePageGetFrameTreeResponse, response => { - var pageSetDocumentContentMessage = new BrowserMessage("Page.setDocumentContent"); + BrowserMessage pageSetDocumentContentMessage = new("Page.setDocumentContent"); pageSetDocumentContentMessage.Parameters.Add("frameId", response.Result.FrameTree.Frame.Id); pageSetDocumentContentMessage.Parameters.Add("html", html); connection.SendAsync(pageSetDocumentContentMessage); @@ -84,9 +87,11 @@ await connection.SendAsync( async pagePrintToPdfResponse => { if (string.IsNullOrEmpty(pagePrintToPdfResponse.Result.Stream)) + { return; + } - var ioReadMessage = new BrowserMessage("IO.read"); + BrowserMessage ioReadMessage = new("IO.read"); ioReadMessage.Parameters.Add("handle", pagePrintToPdfResponse.Result.Stream); ioReadMessage.Parameters.Add("size", 50 * 1024); @@ -94,7 +99,10 @@ await connection.SendAsync( while (true) { if (finished) + { break; + } + await connection.SendAsync( ioReadMessage, IoReadResponseSerializationContext.Default.BrowserResultResponseIoReadResponse, @@ -128,7 +136,7 @@ private static BrowserMessage CreatePrintToPdfBrowserMessage( BlazorReportsPageSettings pageSettings ) { - var message = new BrowserMessage("Page.printToPDF"); + BrowserMessage message = new("Page.printToPDF"); message.Parameters.Add( "landscape", pageSettings.Orientation == BlazorReportsPageOrientation.Landscape @@ -140,6 +148,45 @@ BlazorReportsPageSettings pageSettings message.Parameters.Add("marginLeft", pageSettings.MarginLeft); message.Parameters.Add("marginRight", pageSettings.MarginRight); message.Parameters.Add("printBackground", !pageSettings.IgnoreBackground); + message.Parameters.Add("displayHeaderFooter", true); + message.Parameters.Add( + "headerTemplate", + """ + + + + """ + // """ + // + // """ + ); message.Parameters.Add("transferMode", "ReturnAsStream"); return message; @@ -152,7 +199,10 @@ CancellationToken stoppingToken ) { if (data.Length == 0) + { return; + } + var sharedPool = ArrayPool.Shared; var dataBytes = sharedPool.Rent(data.Length); var inputBlock = sharedPool.Rent(CustomFromBase64Transform.InputBlockSize); @@ -181,7 +231,10 @@ CancellationToken stoppingToken writer.Advance(count); var flushResult = await writer.FlushAsync(stoppingToken); if (flushResult.IsCanceled || flushResult.IsCompleted) + { break; + } + writerBuffer = writer.GetMemory(count); // Get a new buffer after advancing } } @@ -196,7 +249,7 @@ CancellationToken stoppingToken private async ValueTask ClosePdfStream(string stream, CancellationToken stoppingToken = default) { await connection.ConnectAsync(stoppingToken); - var ioCloseMessage = new BrowserMessage("IO.close"); + BrowserMessage ioCloseMessage = new("IO.close"); ioCloseMessage.Parameters.Add("handle", stream); connection.SendAsync(ioCloseMessage); } diff --git a/src/BlazorReports/Services/BrowserServices/BrowserService.cs b/src/BlazorReports/Services/BrowserServices/BrowserService.cs index 83cf6ee..39f3ade 100644 --- a/src/BlazorReports/Services/BrowserServices/BrowserService.cs +++ b/src/BlazorReports/Services/BrowserServices/BrowserService.cs @@ -159,6 +159,8 @@ public async ValueTask DisposeAsync() _browserPoolLock.Dispose(); _browserStartLock.Dispose(); while (_browserQueue.TryDequeue(out var browser)) + { await browser.DisposeAsync(); + } } } diff --git a/src/BlazorReports/Services/BrowserServices/Connection.cs b/src/BlazorReports/Services/BrowserServices/Connection.cs index e582c33..9f6325d 100644 --- a/src/BlazorReports/Services/BrowserServices/Connection.cs +++ b/src/BlazorReports/Services/BrowserServices/Connection.cs @@ -15,7 +15,14 @@ namespace BlazorReports.Services.BrowserServices; /// /// Represents a connection to the browser /// -internal sealed class Connection : IAsyncDisposable +/// +/// The constructor of the connection +/// +/// The uri of the connection +/// The response timeout +/// The logger +internal sealed class Connection(Uri uri, TimeSpan responseTimeout, ILogger logger) + : IAsyncDisposable { private ClientWebSocket _webSocket = new(); private readonly ArrayPool _bufferPool = ArrayPool.Shared; @@ -29,26 +36,11 @@ internal sealed class Connection : IAsyncDisposable private Task? _receiveTask; private readonly SemaphoreSlim _connectionLock = new(1, 1); private readonly CancellationTokenSource _cts = new(); - private readonly TimeSpan _responseTimeout; - private readonly ILogger _logger; /// /// The uri of the connection /// - public readonly Uri Uri; - - /// - /// The constructor of the connection - /// - /// The uri of the connection - /// The response timeout - /// The logger - public Connection(Uri uri, TimeSpan responseTimeout, ILogger logger) - { - Uri = uri; - _responseTimeout = responseTimeout; - _logger = logger; - } + public readonly Uri Uri = uri; public async Task InitializeAsync(CancellationToken stoppingToken = default) { @@ -56,7 +48,9 @@ public async Task InitializeAsync(CancellationToken stoppingToken = default) try { if (_webSocket.State is not WebSocketState.None) + { return; + } await _webSocket.ConnectAsync(Uri, stoppingToken); @@ -80,7 +74,9 @@ public async ValueTask> ConnectAsync( ) { if (_webSocket.State is WebSocketState.Open) + { return new Success(); + } await _connectionLock.WaitAsync(stoppingToken); @@ -102,13 +98,15 @@ public async ValueTask> ConnectAsync( retries--; // decrease remaining retries if (retries > 0) // don't delay if no more retries left + { await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken); // wait for 3 seconds before next attempt + } } } if (_webSocket.State is not WebSocketState.Open) { - LogMessages.UnableToEstablishWebSocketConnection(_logger, Uri); + LogMessages.UnableToEstablishWebSocketConnection(logger, Uri); return new ConnectionProblem(); } @@ -130,14 +128,16 @@ private async Task ProcessSendQueueAsync() await _sendSignal.WaitAsync(_cts.Token); if (!_sendQueue.TryDequeue(out var message)) + { continue; + } var buffer = JsonSerializer.SerializeToUtf8Bytes( message, BrowserMessageSerializationContext.Default.BrowserMessage ); bufferToSend = _bufferPool.Rent(buffer.Length); - var bufferToSendMemory = new Memory(bufferToSend); + Memory bufferToSendMemory = new(bufferToSend); buffer.CopyTo(bufferToSendMemory); await _webSocket.SendAsync( bufferToSendMemory[..buffer.Length], @@ -149,19 +149,21 @@ await _webSocket.SendAsync( } catch (OperationCanceledException) { - LogMessages.SendQueueProcessingCancelled(_logger, Uri); + LogMessages.SendQueueProcessingCancelled(logger, Uri); } finally { if (bufferToSend is not null) + { _bufferPool.Return(bufferToSend); + } } } private async Task ProcessResponsesAsync() { var bufferToReceive = _bufferPool.Rent(BufferSize); - var bufferToReceiveMemory = new Memory(bufferToReceive); + Memory bufferToReceiveMemory = new(bufferToReceive); try { @@ -170,10 +172,12 @@ private async Task ProcessResponsesAsync() var result = await _webSocket.ReceiveAsync(bufferToReceiveMemory, _cts.Token); var messageReceived = bufferToReceiveMemory[..result.Count]; - var jsonDoc = JsonDocument.Parse(messageReceived); + JsonDocument jsonDoc = JsonDocument.Parse(messageReceived); var root = jsonDoc.RootElement; if (!root.TryGetProperty("id", out var methodElement)) + { continue; + } var id = methodElement.GetInt32(); @@ -185,7 +189,7 @@ private async Task ProcessResponsesAsync() } catch (OperationCanceledException) { - LogMessages.ReceiveQueueProcessingCancelled(_logger, Uri); + LogMessages.ReceiveQueueProcessingCancelled(logger, Uri); } catch (WebSocketException) { @@ -193,7 +197,7 @@ private async Task ProcessResponsesAsync() } catch (Exception ex) { - LogMessages.ReceiveQueueProcessingError(_logger, ex, Uri); + LogMessages.ReceiveQueueProcessingError(logger, ex, Uri); } finally { @@ -210,11 +214,13 @@ CancellationToken stoppingToken _sendQueue.Enqueue(message); _sendSignal.Release(); - var tcs = new TaskCompletionSource(); + TaskCompletionSource tcs = new(); _responseTasks[message.Id] = tcs; - using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); - timeoutCts.CancelAfter(_responseTimeout); + using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource( + stoppingToken + ); + timeoutCts.CancelAfter(responseTimeout); if (await Task.WhenAny(tcs.Task, Task.Delay(-1, timeoutCts.Token)) == tcs.Task) { @@ -236,10 +242,9 @@ public async ValueTask SendAsync( var response = await SendMessageAsync(message, stoppingToken); var parsedMessage = response.RootElement.Deserialize(returnDataJsonTypeInfo); - if (parsedMessage is null) - throw new JsonException("Could not deserialize response"); - - return parsedMessage; + return parsedMessage is null + ? throw new JsonException("Could not deserialize response") + : parsedMessage; } /// @@ -259,10 +264,9 @@ public async ValueTask SendAsync( var response = await SendMessageAsync(message, stoppingToken); var parsedMessage = response.RootElement.Deserialize(returnDataJsonTypeInfo); - if (parsedMessage is null) - throw new JsonException("Could not deserialize response"); - - return await responseHandler(parsedMessage); + return parsedMessage is null + ? throw new JsonException("Could not deserialize response") + : await responseHandler(parsedMessage); } /// @@ -280,11 +284,9 @@ public async ValueTask SendAsync( ) { var response = await SendMessageAsync(message, stoppingToken); - var parsedMessage = response.RootElement.Deserialize(returnDataJsonTypeInfo); - - if (parsedMessage is null) - throw new JsonException("Could not deserialize response"); - + var parsedMessage = + response.RootElement.Deserialize(returnDataJsonTypeInfo) + ?? throw new JsonException("Could not deserialize response"); responseAction(parsedMessage); } @@ -306,17 +308,15 @@ public async ValueTask SendAsync( _sendQueue.Enqueue(message); _sendSignal.Release(); - var tcs = new TaskCompletionSource(); + TaskCompletionSource tcs = new(); _responseTasks[message.Id] = tcs; - if (await Task.WhenAny(tcs.Task, Task.Delay(_responseTimeout, stoppingToken)) == tcs.Task) + if (await Task.WhenAny(tcs.Task, Task.Delay(responseTimeout, stoppingToken)) == tcs.Task) { var response = await tcs.Task; - var parsedMessage = response.RootElement.Deserialize(returnDataJsonTypeInfo); - - if (parsedMessage is null) - throw new JsonException("Could not deserialize response"); - + var parsedMessage = + response.RootElement.Deserialize(returnDataJsonTypeInfo) + ?? throw new JsonException("Could not deserialize response"); await responseAction(parsedMessage); } else @@ -340,9 +340,14 @@ public async ValueTask DisposeAsync() await _cts.CancelAsync(); if (_sendTask is not null) + { await _sendTask; + } + if (_receiveTask is not null) + { await _receiveTask; + } _webSocket.Dispose(); _sendSignal.Dispose(); diff --git a/src/BlazorReports/Services/BrowserServices/CustomFromBase64Transform.cs b/src/BlazorReports/Services/BrowserServices/CustomFromBase64Transform.cs index b226aa9..4896695 100644 --- a/src/BlazorReports/Services/BrowserServices/CustomFromBase64Transform.cs +++ b/src/BlazorReports/Services/BrowserServices/CustomFromBase64Transform.cs @@ -54,7 +54,7 @@ int outputOffset return 0; } - ConvertFromBase64(transformBuffer, outputBuffer[outputOffset..], out int written); + ConvertFromBase64(transformBuffer, outputBuffer[outputOffset..], out var written); ReturnToCryptoPool(transformBufferArray, transformBuffer.Length); @@ -175,7 +175,10 @@ internal static class CryptoPool { private const int ClearAll = -1; - internal static byte[] Rent(int minimumLength) => ArrayPool.Shared.Rent(minimumLength); + internal static byte[] Rent(int minimumLength) + { + return ArrayPool.Shared.Rent(minimumLength); + } internal static void Return(byte[] array, int clearSize = ClearAll) { diff --git a/src/BlazorReports/Services/BrowserServices/Factories/BrowserFactory.cs b/src/BlazorReports/Services/BrowserServices/Factories/BrowserFactory.cs index cd6e302..0e97f65 100644 --- a/src/BlazorReports/Services/BrowserServices/Factories/BrowserFactory.cs +++ b/src/BlazorReports/Services/BrowserServices/Factories/BrowserFactory.cs @@ -36,18 +36,22 @@ public async ValueTask> CreateBrowser() : BrowserFinder.Find(browserOptions.Browser); if (!File.Exists(browserExecutableLocation)) + { throw new FileNotFoundException( $"Could not find browser in location '{browserExecutableLocation}'" ); + } var temporaryPath = Path.GetTempPath(); var devToolsDirectory = Path.Combine(temporaryPath, Guid.NewGuid().ToString()); Directory.CreateDirectory(devToolsDirectory); - var devToolsActivePortDirectory = new DirectoryInfo(devToolsDirectory); + DirectoryInfo devToolsActivePortDirectory = new(devToolsDirectory); var devToolsActivePortFile = Path.Combine(devToolsDirectory, "DevToolsActivePort"); if (File.Exists(devToolsActivePortFile)) + { File.Delete(devToolsActivePortFile); + } var chromiumProcess = CreateChromiumProcess( browserExecutableLocation, @@ -82,7 +86,7 @@ public async ValueTask> CreateBrowser() LogMessages.BrowserDataDirectoryUsed(browserFactoryLogger, devToolsDirectory); - var uri = new Uri($"ws://127.0.0.1:{lines[0]}{lines[1]}"); + Uri uri = new($"ws://127.0.0.1:{lines[0]}{lines[1]}"); var connection = await connectionFactory.CreateConnection(uri, browserOptions.ResponseTimeout); return new Browser( chromiumProcess, @@ -107,9 +111,9 @@ private Process CreateChromiumProcess( BlazorReportsBrowserOptions browserOptions ) { - var chromiumProcess = new Process(); - var defaultChromiumArgument = new List - { + Process chromiumProcess = new(); + List defaultChromiumArgument = + [ "--headless=new", "--disable-gpu", "--hide-scrollbars", @@ -127,18 +131,22 @@ BlazorReportsBrowserOptions browserOptions "--disable-crash-reporter", "--remote-debugging-port=\"0\"", $"--user-data-dir=\"{devToolsDirectory}\"", - }; + ]; if (browserOptions.NoSandbox) + { defaultChromiumArgument.Add("--no-sandbox"); + } if (browserOptions.DisableDevShmUsage) + { defaultChromiumArgument.Add("--disable-dev-shm-usage"); + } var chromiumArguments = string.Join(" ", defaultChromiumArgument); LogMessages.StartingChromiumProcess(browserFactoryLogger, chromiumArguments); - var processStartInfo = new ProcessStartInfo + ProcessStartInfo processStartInfo = new() { FileName = chromiumExeFileName, Arguments = chromiumArguments, @@ -159,9 +167,14 @@ private void ChromiumProcess_Exited(object? sender, EventArgs e) { // Log errors with details if (sender is not Process process) + { return; + } + if (process.ExitCode == 0) + { return; + } var exception = Marshal.GetExceptionForHR(process.ExitCode); LogMessages.ChromiumProcessCrashed(browserFactoryLogger, exception, process.ExitCode); @@ -173,22 +186,27 @@ DirectoryInfo devToolsActivePortDirectory ) { if (devToolsActivePortDirectory is null || !devToolsActivePortDirectory.Exists) + { throw new DirectoryNotFoundException($"The {nameof(devToolsActivePortDirectory)} is null"); + } - var watcher = new FileSystemWatcher + FileSystemWatcher watcher = new() { Path = devToolsActivePortDirectory.FullName, Filter = Path.GetFileName(devToolsActivePortFile), EnableRaisingEvents = true, }; - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); // 10 second timeout - var tcs = new TaskCompletionSource(); + CancellationTokenSource cts = new(TimeSpan.FromSeconds(10)); // 10 second timeout + TaskCompletionSource tcs = new(); void CreatedHandler(object s, FileSystemEventArgs e) { if (e.ChangeType != WatcherChangeTypes.Created) + { return; + } + HandleFileCreationAsync(devToolsActivePortFile, tcs, 5, 2).ConfigureAwait(false); } @@ -258,11 +276,15 @@ int expectedLines catch (IOException) { if (++retryCount == maxRetries) + { tcs.TrySetException( new IOException($"Unable to read file '{filePath}' after {maxRetries} attempts") ); + } else + { await Task.Delay(TimeSpan.FromMilliseconds(100 * retryCount)); // Exponential backoff + } } catch (Exception ex) { diff --git a/src/BlazorReports/Services/BrowserServices/Factories/BrowserPageFactory.cs b/src/BlazorReports/Services/BrowserServices/Factories/BrowserPageFactory.cs index ba5e3e9..50178a4 100644 --- a/src/BlazorReports/Services/BrowserServices/Factories/BrowserPageFactory.cs +++ b/src/BlazorReports/Services/BrowserServices/Factories/BrowserPageFactory.cs @@ -22,7 +22,7 @@ public async ValueTask CreateBrowserPage(string targetId, Uri pageU pageUri, options.Value.BrowserOptions.ResponseTimeout ); - var browserPage = new BrowserPage(logger, targetId, pageConnection); + BrowserPage browserPage = new(logger, targetId, pageConnection); return browserPage; } } diff --git a/src/BlazorReports/Services/BrowserServices/Factories/ConnectionFactory.cs b/src/BlazorReports/Services/BrowserServices/Factories/ConnectionFactory.cs index 40a485d..7ba4fde 100644 --- a/src/BlazorReports/Services/BrowserServices/Factories/ConnectionFactory.cs +++ b/src/BlazorReports/Services/BrowserServices/Factories/ConnectionFactory.cs @@ -16,7 +16,7 @@ internal sealed class ConnectionFactory(ILogger logger) : IConnectio /// The connection public async ValueTask CreateConnection(Uri uri, TimeSpan responseTimeout) { - var connection = new Connection(uri, responseTimeout, logger); + Connection connection = new(uri, responseTimeout, logger); await connection.InitializeAsync(); return connection; } diff --git a/src/BlazorReports/Services/BrowserServices/Helpers/ChromeFinder.cs b/src/BlazorReports/Services/BrowserServices/Helpers/ChromeFinder.cs index 1f2768f..c641686 100644 --- a/src/BlazorReports/Services/BrowserServices/Helpers/ChromeFinder.cs +++ b/src/BlazorReports/Services/BrowserServices/Helpers/ChromeFinder.cs @@ -20,7 +20,7 @@ internal static class ChromeFinder private const string ChromeExecutableNameMac1 = "Google Chrome.app/Contents/MacOS/Google Chrome"; private const string ChromeExecutableNameMac2 = "Chromium.app/Contents/MacOS/Chromium"; private static readonly string[] LinuxDirectoryLocations = - { + [ "/usr/local/sbin", "/usr/local/bin", "/usr/sbin", @@ -28,7 +28,7 @@ internal static class ChromeFinder "/sbin", "/bin", "/opt/microsoft/edge", - }; + ]; /// /// Tries to find Chrome @@ -51,7 +51,7 @@ internal static class ChromeFinder return pathFromCurrentDirectory; } - var directories = new List(); + List directories = []; GetApplicationDirectories(directories); foreach (var exeName in exeNames) @@ -100,7 +100,10 @@ private static void GetApplicationDirectories(List directories) private static string? GetPathFromRegistry() { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { return null; + } + var key = Registry .GetValue( @"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe", @@ -109,15 +112,18 @@ private static void GetApplicationDirectories(List directories) ) ?.ToString(); - if (key == null) + if (key is null) + { return null; + } + var path = Path.Combine(key, ChromeExecutableNameWin); return File.Exists(path) ? path : null; } private static List GetExeNames() { - var exeNames = new List(); + List exeNames = []; if (IsWindows) { @@ -126,18 +132,17 @@ private static List GetExeNames() else if (IsLinux) { exeNames.AddRange( - new[] - { + [ ChromeExecutableNameLinux1, ChromeExecutableNameLinux2, ChromeExecutableNameLinux3, ChromeExecutableNameLinux4, - } + ] ); } else if (IsMacOs) { - exeNames.AddRange(new[] { ChromeExecutableNameMac1, ChromeExecutableNameMac2 }); + exeNames.AddRange([ChromeExecutableNameMac1, ChromeExecutableNameMac2]); } return exeNames; diff --git a/src/BlazorReports/Services/BrowserServices/Helpers/EdgeFinder.cs b/src/BlazorReports/Services/BrowserServices/Helpers/EdgeFinder.cs index 68b6bef..cc0ea02 100644 --- a/src/BlazorReports/Services/BrowserServices/Helpers/EdgeFinder.cs +++ b/src/BlazorReports/Services/BrowserServices/Helpers/EdgeFinder.cs @@ -19,7 +19,7 @@ internal static class EdgeFinder private const string ChromeExecutableNameMac = "Microsoft Edge.app/Contents/MacOS/Microsoft Edge"; private static readonly string[] LinuxDirectoryLocations = - { + [ "/usr/local/sbin", "/usr/local/bin", "/usr/sbin", @@ -27,7 +27,7 @@ internal static class EdgeFinder "/sbin", "/bin", "/opt/microsoft/edge", - }; + ]; /// /// Tries to find Chrome @@ -50,7 +50,7 @@ internal static class EdgeFinder return pathFromCurrentDirectory; } - var directories = new List(); + List directories = []; GetApplicationDirectories(directories); foreach (var exeName in exeNames) @@ -99,7 +99,10 @@ private static void GetApplicationDirectories(List directories) private static string? GetPathFromRegistry() { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { return null; + } + var key = Registry .GetValue( @"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe", @@ -108,15 +111,18 @@ private static void GetApplicationDirectories(List directories) ) ?.ToString(); - if (key == null) + if (key is null) + { return null; + } + var path = Path.Combine(key, EdgeExecutableNameWin); return File.Exists(path) ? path : null; } private static List GetExeNames() { - var exeNames = new List(); + List exeNames = []; if (IsWindows) { @@ -125,12 +131,12 @@ private static List GetExeNames() else if (IsLinux) { exeNames.AddRange( - new[] { ChromeExecutableNameLinux1, ChromeExecutableNameLinux2, ChromeExecutableNameLinux3 } + [ChromeExecutableNameLinux1, ChromeExecutableNameLinux2, ChromeExecutableNameLinux3] ); } else if (IsMacOs) { - exeNames.AddRange(new[] { ChromeExecutableNameMac }); + exeNames.AddRange([ChromeExecutableNameMac]); } return exeNames; diff --git a/src/BlazorReports/Services/BrowserServices/Requests/BrowserMessage.cs b/src/BlazorReports/Services/BrowserServices/Requests/BrowserMessage.cs index ea82118..e04123b 100644 --- a/src/BlazorReports/Services/BrowserServices/Requests/BrowserMessage.cs +++ b/src/BlazorReports/Services/BrowserServices/Requests/BrowserMessage.cs @@ -22,7 +22,7 @@ internal sealed class BrowserMessage(string method) /// The parameters that we want to feed into the browser /// [JsonPropertyName("params")] - public Dictionary Parameters { get; } = new(); + public Dictionary Parameters { get; } = []; } /// diff --git a/src/BlazorReports/Services/BrowserServices/Responses/PageGetFrameTreeResponse.cs b/src/BlazorReports/Services/BrowserServices/Responses/PageGetFrameTreeResponse.cs index bd810eb..2c94801 100644 --- a/src/BlazorReports/Services/BrowserServices/Responses/PageGetFrameTreeResponse.cs +++ b/src/BlazorReports/Services/BrowserServices/Responses/PageGetFrameTreeResponse.cs @@ -14,5 +14,4 @@ public sealed record PageGetFrameTreeResponse(BrowserFrameTree FrameTree); /// [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] [JsonSerializable(typeof(BrowserResultResponse))] -internal sealed partial class PageGetFrameTreeResponseSerializationContext - : JsonSerializerContext { } +internal sealed partial class PageGetFrameTreeResponseSerializationContext : JsonSerializerContext; diff --git a/src/BlazorReports/Services/ReportService.cs b/src/BlazorReports/Services/ReportService.cs index c851820..6ba3aae 100644 --- a/src/BlazorReports/Services/ReportService.cs +++ b/src/BlazorReports/Services/ReportService.cs @@ -55,17 +55,17 @@ public async ValueTask< baseStyles = reportRegistry.BaseStyles; } - var componentParameters = new Dictionary + Dictionary componentParameters = new() { { "BaseStyles", baseStyles }, { "Data", data }, { "GlobalAssets", reportRegistry.GlobalAssets }, }; - await using var htmlRenderer = new HtmlRenderer(scope.ServiceProvider, loggerFactory); + await using HtmlRenderer htmlRenderer = new(scope.ServiceProvider, loggerFactory); var html = await htmlRenderer.Dispatcher.InvokeAsync(async () => { - var parameters = ParameterView.FromDictionary(componentParameters); + ParameterView parameters = ParameterView.FromDictionary(componentParameters); var output = await htmlRenderer.RenderComponentAsync(parameters); return output.ToHtmlString(); }); @@ -110,7 +110,7 @@ public async ValueTask< baseStyles = reportRegistry.BaseStyles; } - var childComponentParameters = new Dictionary(); + Dictionary childComponentParameters = []; if ( blazorReport.Component.BaseType == typeof(BlazorReportsBase) && reportRegistry.GlobalAssets.Count != 0 @@ -129,7 +129,7 @@ public async ValueTask< childComponentParameters.Add("Data", data); } - var baseComponentParameters = new Dictionary(); + Dictionary baseComponentParameters = []; if (!string.IsNullOrEmpty(baseStyles)) { baseComponentParameters.Add("BaseStyles", baseStyles); @@ -138,13 +138,13 @@ public async ValueTask< baseComponentParameters.Add("ChildComponentType", blazorReport.Component); baseComponentParameters.Add("ChildComponentParameters", childComponentParameters); - await using var htmlRenderer = new HtmlRenderer(scope.ServiceProvider, loggerFactory); + await using HtmlRenderer htmlRenderer = new(scope.ServiceProvider, loggerFactory); if (blazorReport.OutputFormat == ReportOutputFormat.Pdf) { var html = await htmlRenderer.Dispatcher.InvokeAsync(async () => { - var parameters = ParameterView.FromDictionary(baseComponentParameters); + ParameterView parameters = ParameterView.FromDictionary(baseComponentParameters); var output = await htmlRenderer.RenderComponentAsync(parameters); return output.ToHtmlString(); }); @@ -157,7 +157,7 @@ public async ValueTask< { var html = await htmlRenderer.Dispatcher.InvokeAsync(async () => { - var parameters = ParameterView.FromDictionary(baseComponentParameters); + ParameterView parameters = ParameterView.FromDictionary(baseComponentParameters); var output = await htmlRenderer.RenderComponentAsync(parameters); return output.ToHtmlString(); });