diff --git a/ChromiumHtmlToPdfLib/Browser.cs b/ChromiumHtmlToPdfLib/Browser.cs index e0b84aa..92deb50 100644 --- a/ChromiumHtmlToPdfLib/Browser.cs +++ b/ChromiumHtmlToPdfLib/Browser.cs @@ -54,7 +54,7 @@ namespace ChromiumHtmlToPdfLib; /// See https://chromedevtools.github.io/devtools-protocol/ /// #if (NETSTANDARD2_0) -public class Browser : IDisposable +internal class Browser : IDisposable #else internal class Browser : IDisposable, IAsyncDisposable #endif @@ -82,27 +82,42 @@ internal class Browser : IDisposable, IAsyncDisposable #endregion #region Constructor + /// + /// Makes this object and sets the Chromium remote debugging url + /// + /// + /// The websocket connection to the browser + /// The websocket connection to devtools page + private Browser(Logger? logger, Connection browserConnection, Connection pageConnection) + { + _logger = logger; + _browserConnection = browserConnection; + _pageConnection = pageConnection; + } + /// /// Makes this object and sets the Chromium remote debugging url /// /// The websocket to the browser /// Websocket open timeout in milliseconds /// - internal Browser(Uri browser, int timeout, Logger? logger) + /// + public static async Task Create(Uri browser, int timeout, Logger? logger, CancellationToken cancellationToken) { - _logger = logger; // Open a websocket to the browser - _browserConnection = new Connection(browser.ToString(), timeout, logger); + var browserConnection = await Connection.Create(browser.ToString(), timeout, logger, cancellationToken).ConfigureAwait(false); var message = new Message { Method = "Target.createTarget" }; message.Parameters.Add("url", "about:blank"); - var result = _browserConnection.SendForResponseAsync(message).GetAwaiter().GetResult(); + var result = await browserConnection.SendForResponseAsync(message, cancellationToken).ConfigureAwait(false); var page = Page.FromJson(result); var pageUrl = $"{browser.Scheme}://{browser.Host}:{browser.Port}/devtools/page/{page.Result.TargetId}"; // Open a websocket to the page - _pageConnection = new Connection(pageUrl, timeout, logger); + var pageConnection = await Connection.Create(pageUrl, timeout, logger, cancellationToken).ConfigureAwait(false); + + return new Browser(logger, browserConnection, pageConnection); } #endregion @@ -117,7 +132,7 @@ private enum PageLoadingState /// The page is loading /// Loading, - + /// /// Waiting for the network to be idle /// @@ -152,11 +167,6 @@ private enum PageLoadingState /// When true then caching will be enabled /// /// - /// - /// If a is set then - /// the method will raise a if the - /// reaches zero before finishing navigation - /// /// /// When set a timeout will be started after the DomContentLoaded /// event has fired. After a timeout the NavigateTo method will exit as if the page @@ -167,18 +177,16 @@ private enum PageLoadingState /// When enabled the method will wait for the network to be idle /// /// Raised when an error is returned by Chromium - /// Raised when reaches zero - internal async Task NavigateToAsync( + public async Task NavigateToAsync( List safeUrls, bool useCache, - ConvertUri? uri = null, - string? html = null, - CountdownTimer? countdownTimer = null, - int? mediaLoadTimeout = null, - List? urlBlacklist = null, - bool logNetworkTraffic = false, - bool waitForNetworkIdle = false, - CancellationToken cancellationToken = default) + ConvertUri? uri, + string? html, + int? mediaLoadTimeout, + List? urlBlacklist, + bool logNetworkTraffic, + bool waitForNetworkIdle, + CancellationToken cancellationToken) { var navigationError = string.Empty; var navigationErrorTemplate = string.Empty; @@ -243,7 +251,7 @@ internal async Task NavigateToAsync( var pageNavigateMessage = new Message { Method = "Page.navigate" }; pageNavigateMessage.AddParameter("url", uri.ToString()); _logger?.Info("Navigating to url '{uri}'", uri); - await _pageConnection.SendAsync(pageNavigateMessage).ConfigureAwait(false); + await _pageConnection.SendAsync(pageNavigateMessage, cancellationToken).ConfigureAwait(false); } else if (!string.IsNullOrWhiteSpace(html)) { @@ -257,7 +265,7 @@ internal async Task NavigateToAsync( var pageSetDocumentContent = new Message { Method = "Page.setDocumentContent" }; pageSetDocumentContent.AddParameter("frameId", frameResult.Result.FrameTree.Frame.Id); pageSetDocumentContent.AddParameter("html", html); - await _pageConnection.SendAsync(pageSetDocumentContent).ConfigureAwait(false); + await _pageConnection.SendAsync(pageSetDocumentContent, cancellationToken).ConfigureAwait(false); // When using setDocumentContent a Page.frameNavigated event is never fired, so we have to set the waitForNetworkIdle to true our self pageLoadingState = PageLoadingState.WaitForNetworkIdle; _logger?.Info("Document content set"); @@ -267,7 +275,7 @@ internal async Task NavigateToAsync( var mediaLoadTimeoutStopwatch = new Stopwatch(); - while (pageLoadingState != PageLoadingState.MediaLoadTimeout && + while (pageLoadingState != PageLoadingState.MediaLoadTimeout && pageLoadingState != PageLoadingState.BlockedByClient && // ReSharper disable once ConditionIsAlwaysTrueOrFalse pageLoadingState != PageLoadingState.Closed && @@ -449,7 +457,7 @@ internal async Task NavigateToAsync( break; } } - + if (mediaLoadTimeoutStopwatch.IsRunning && mediaLoadTimeoutStopwatch.ElapsedMilliseconds >= mediaLoadTimeout!.Value) { @@ -458,8 +466,7 @@ internal async Task NavigateToAsync( pageLoadingState = PageLoadingState.MediaLoadTimeout; } - if (countdownTimer is { MillisecondsLeft: 0 }) - throw new ConversionTimedOutException($"The {nameof(NavigateToAsync)} method timed out"); + cancellationToken.ThrowIfCancellationRequested(); } if (pageLoadingState == PageLoadingState.MediaLoadTimeout) @@ -512,20 +519,6 @@ void PageConnectionClosed(object? o, EventArgs eventArgs) } #endregion - #region WaitForWindowStatus - /// - /// Waits until the javascript window.status is returning the given - /// - /// The case-insensitive status - /// Continue after reaching the set timeout in milliseconds - /// true when window status matched, false when timing out - /// Raised when an error is returned by Chromium - public bool WaitForWindowStatus(string status, int timeout = 60000) - { - return WaitForWindowStatusAsync(status, timeout).ConfigureAwait(false).GetAwaiter().GetResult(); - } - #endregion - #region WaitForWindowStatusAsync /// /// Waits until the javascript window.status is returning the given @@ -535,7 +528,7 @@ public bool WaitForWindowStatus(string status, int timeout = 60000) /// /// true when window status matched, false when timing out /// Raised when an error is returned by Chromium - public async Task WaitForWindowStatusAsync(string status, int timeout = 60000, CancellationToken cancellationToken = default) + public async Task WaitForWindowStatusAsync(string status, int timeout, CancellationToken cancellationToken) { var message = new Message { Method = "Runtime.evaluate" }; message.AddParameter("expression", "window.status;"); @@ -560,7 +553,7 @@ public async Task WaitForWindowStatusAsync(string status, int timeout = 60 await Task.Delay(10, cancellationToken).ConfigureAwait(false); - if (stopWatch.ElapsedMilliseconds >= timeout) + if (stopWatch.ElapsedMilliseconds >= timeout) break; } @@ -569,18 +562,6 @@ public async Task WaitForWindowStatusAsync(string status, int timeout = 60 } #endregion - #region RunJavascript - /// - /// Runs the given javascript after the page has been fully loaded - /// - /// The javascript to run - /// Raised when an error is returned by Chromium - public void RunJavascript(string script) - { - RunJavascriptAsync(script).GetAwaiter().GetResult(); - } - #endregion - #region RunJavascriptAsync /// /// Runs the given javascript after the page has been fully loaded @@ -588,7 +569,7 @@ public void RunJavascript(string script) /// The javascript to run /// /// Raised when an error is returned by Chromium - public async Task RunJavascriptAsync(string script, CancellationToken cancellationToken = default) + public async Task RunJavascriptAsync(string script, CancellationToken cancellationToken) { var message = new Message { Method = "Runtime.evaluate" }; message.AddParameter("expression", script); @@ -619,25 +600,16 @@ public async Task RunJavascriptAsync(string script, CancellationToken cancellati /// /// Instructs Chromium to capture a snapshot from the loaded page /// - /// - /// If a is set then - /// the method will raise a in the - /// reaches zero before finishing the printing to pdf - /// /// /// /// See https://chromedevtools.github.io/devtools-protocol/tot/Page#method-captureSnapshot /// /// - internal async Task CaptureSnapshotAsync( - CountdownTimer? countdownTimer = null, - CancellationToken cancellationToken = default) + public async Task CaptureSnapshotAsync(CancellationToken cancellationToken) { var message = new Message { Method = "Page.captureSnapshot" }; - var result = countdownTimer == null - ? await _pageConnection.SendForResponseAsync(message, cancellationToken).ConfigureAwait(false) - : await _pageConnection.SendForResponseAsync(message, new CancellationTokenSource(countdownTimer.MillisecondsLeft).Token).ConfigureAwait(false); + var result = await _pageConnection.SendForResponseAsync(message, cancellationToken).ConfigureAwait(false); return SnapshotResponse.FromJson(result); } @@ -651,21 +623,12 @@ internal async Task CaptureSnapshotAsync( /// /// /// - /// - /// If a is set then - /// the method will raise a in the - /// reaches zero before finishing the printing to pdf - /// /// /// /// See https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF /// /// Raised when Chromium returns an empty string - /// Raised when reaches zero - internal async Task PrintToPdfAsync(Stream outputStream, - PageSettings pageSettings, - CountdownTimer? countdownTimer = null, - CancellationToken cancellationToken = default) + public async Task PrintToPdfAsync(Stream outputStream, PageSettings pageSettings, CancellationToken cancellationToken) { var message = new Message { Method = "Page.printToPDF" }; message.AddParameter("landscape", pageSettings.Landscape); @@ -688,9 +651,7 @@ internal async Task PrintToPdfAsync(Stream outputStream, _logger?.Info("Sending PDF request to Chromium"); - var result = countdownTimer == null - ? await _pageConnection.SendForResponseAsync(message, cancellationToken).ConfigureAwait(false) - : await _pageConnection.SendForResponseAsync(message, new CancellationTokenSource(countdownTimer.MillisecondsLeft).Token).ConfigureAwait(false); + var result = await _pageConnection.SendForResponseAsync(message, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(result)) throw new ConversionException("Conversion failed ... did not get the expected response from Chromium"); @@ -707,19 +668,17 @@ internal async Task PrintToPdfAsync(Stream outputStream, throw new ConversionException("The output stream is not writable, please provide a writable stream"); _logger?.Info("Resetting output stream to position 0"); - + message = new Message { Method = "IO.read" }; message.AddParameter("handle", printToPdfResponse.Result!.Stream!); message.AddParameter("size", 1048576); // Get the pdf in chunks of 1MB - + _logger?.Info("Reading generated PDF from IO stream with handle id {stream}", printToPdfResponse.Result.Stream); outputStream.Position = 0; while (true) { - result = countdownTimer == null - ? await _pageConnection.SendForResponseAsync(message, cancellationToken).ConfigureAwait(false) - : await _pageConnection.SendForResponseAsync(message, new CancellationTokenSource(countdownTimer.MillisecondsLeft).Token).ConfigureAwait(false); + result = await _pageConnection.SendForResponseAsync(message, cancellationToken).ConfigureAwait(false); var ioReadResponse = IoReadResponse.FromJson(result); var bytes = ioReadResponse.Result.Bytes; @@ -748,19 +707,13 @@ internal async Task PrintToPdfAsync(Stream outputStream, /// /// Instructs Chromium to take a screenshot from the page /// - /// /// /// /// Raised when Chromium returns an empty string - /// Raised when reaches zero - internal async Task CaptureScreenshotAsync( - CountdownTimer? countdownTimer = null, - CancellationToken cancellationToken = default) + public async Task CaptureScreenshotAsync(CancellationToken cancellationToken) { var message = new Message { Method = "Page.captureScreenshot" }; - var result = countdownTimer == null - ? await _pageConnection.SendForResponseAsync(message, cancellationToken).ConfigureAwait(false) - : await _pageConnection.SendForResponseAsync(message, new CancellationTokenSource(countdownTimer.MillisecondsLeft).Token).ConfigureAwait(false); + var result = await _pageConnection.SendForResponseAsync(message, cancellationToken).ConfigureAwait(false); var captureScreenshotResponse = CaptureScreenshotResponse.FromJson(result); @@ -777,7 +730,7 @@ internal async Task CaptureScreenshotAsync( /// /// /// - internal async Task CloseAsync(CancellationToken cancellationToken) + private async Task CloseAsync(CancellationToken cancellationToken) { var message = new Message { Method = "Browser.close" }; await _browserConnection.SendForResponseAsync(message, cancellationToken).ConfigureAwait(false); diff --git a/ChromiumHtmlToPdfLib/Connection.cs b/ChromiumHtmlToPdfLib/Connection.cs index f5174a1..9c7b61e 100644 --- a/ChromiumHtmlToPdfLib/Connection.cs +++ b/ChromiumHtmlToPdfLib/Connection.cs @@ -75,7 +75,7 @@ public class Connection : IDisposable, IAsyncDisposable /// /// The url of the websocket /// - private readonly string _url; + private readonly Uri _url; /// /// The current message id @@ -88,14 +88,14 @@ public class Connection : IDisposable, IAsyncDisposable private readonly ClientWebSocket _webSocket; /// - /// Websocket open timeout in milliseconds + /// Websocket operation timeout in milliseconds /// private readonly int _timeout; /// /// Task to await for completion. /// - private readonly Task _receiveTask; + private Task? _receiveTask; /// /// Keeps track is we already disposed our resources @@ -110,15 +110,26 @@ public class Connection : IDisposable, IAsyncDisposable /// The url /// Websocket open timeout in milliseconds /// - internal Connection(string url, int timeout, Logger? logger) + private Connection(string url, int timeout, Logger? logger) { - _url = url; + _url = new Uri(url); _timeout = timeout; _logger = logger; _logger?.Info("Creating new websocket connection to url '{url}'", url); _webSocket = new ClientWebSocket(); _receiveLoopCts = new CancellationTokenSource(); - OpenWebSocketAsync().GetAwaiter().GetResult(); + } + + internal static async Task Create(string url, int timeout, Logger? logger, CancellationToken cancellationToken) + { + var connection = new Connection(url, timeout, logger); + await connection.StartAsync(cancellationToken).ConfigureAwait(false); + return connection; + } + + private async Task StartAsync(CancellationToken cancellationToken) + { + await OpenWebSocketAsync(cancellationToken).ConfigureAwait(false); _receiveTask = Task.Factory.StartNew(ReceiveLoop, new ReceiveLoopState(_logger, _webSocket, OnMessageReceived, _receiveLoopCts.Token), _receiveLoopCts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } #endregion @@ -186,21 +197,16 @@ private static async Task ReceiveLoop(object? stateData) #endregion #region OpenWebSocket - private async Task OpenWebSocketAsync() + private async Task OpenWebSocketAsync(CancellationToken cancellationToken) { if (_webSocket.State is WebSocketState.Open or WebSocketState.Connecting) return; _logger?.Info("Opening websocket connection with a timeout of {timeout} milliseconds", _timeout); - try - { - await _webSocket.ConnectAsync(new Uri(_url), new CancellationTokenSource(_timeout).Token).ConfigureAwait(false); - _logger?.Info("Websocket opened"); - } - catch (Exception exception) - { - WebSocketOnError(_logger, new ErrorEventArgs(exception)); - } + using var timeoutCts = new CancellationTokenSource(_timeout); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + await _webSocket.ConnectAsync(_url, linkedCts.Token).ConfigureAwait(false); + _logger?.Info("Websocket opened"); } #endregion @@ -210,9 +216,9 @@ private static void WebSocketOnMessageReceived(Logger? logger, Action on var response = e.Message; var error = CheckForError(response); - + if (!string.IsNullOrEmpty(error)) - logger?.Error("{error}", error); + logger?.Error("Chrome returned error: {error}", error); onMessageReceived(response); } @@ -224,7 +230,7 @@ private void OnMessageReceived(string response) private static void WebSocketOnError(Logger? logger, ErrorEventArgs e) { - logger?.Error(e.Exception, "{exception}", ExceptionHelpers.GetInnerException(e.Exception)); + logger?.Error(e.Exception, "WebSocket operation failed with {exception}", ExceptionHelpers.GetInnerException(e.Exception)); } private void WebSocketOnClosed(EventArgs e) @@ -240,19 +246,17 @@ private void WebSocketOnClosed(EventArgs e) /// The message to send /// /// Response given by - internal async Task SendForResponseAsync(Message message, CancellationToken cancellationToken = default) + internal async Task SendForResponseAsync(Message message, CancellationToken cancellationToken) { _messageId += 1; message.Id = _messageId; - await OpenWebSocketAsync().ConfigureAwait(false); - var tcs = new TaskCompletionSource(); var receivedHandler = new EventHandler((_, data) => { var messageBase = MessageBase.FromJson(data); - if (messageBase.Id == message.Id) + if (messageBase.Id == message.Id) tcs.SetResult(data); }); @@ -260,10 +264,10 @@ internal async Task SendForResponseAsync(Message message, CancellationTo try { - if (cancellationToken == default) - cancellationToken = new CancellationTokenSource(_timeout).Token; + using var timeoutCts = new CancellationTokenSource(_timeout); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); - await _webSocket.SendAsync(MessageToBytes(message), WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false); + await _webSocket.SendAsync(MessageToBytes(message), WebSocketMessageType.Text, true, linkedCts.Token).ConfigureAwait(false); tcs.Task.Wait(cancellationToken); return cancellationToken.IsCancellationRequested ? string.Empty : tcs.Task.Result; @@ -286,17 +290,18 @@ internal async Task SendForResponseAsync(Message message, CancellationTo /// Sends a message to the and awaits no response /// /// The message to send + /// The message to send /// - internal async Task SendAsync(Message message) + internal async Task SendAsync(Message message, CancellationToken cancellationToken) { _messageId += 1; message.Id = _messageId; - await OpenWebSocketAsync().ConfigureAwait(false); - try { - await _webSocket.SendAsync(MessageToBytes(message), WebSocketMessageType.Text, true, new CancellationTokenSource(_timeout).Token).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(_timeout); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + await _webSocket.SendAsync(MessageToBytes(message), WebSocketMessageType.Text, true, linkedCts.Token).ConfigureAwait(false); } catch (Exception exception) { @@ -342,7 +347,10 @@ public async Task InternalDisposeAsync() _receiveLoopCts.Cancel(); - await (await _receiveTask.ConfigureAwait(false)).ConfigureAwait(false); + if (_receiveTask != null) + await (await _receiveTask.ConfigureAwait(false)).ConfigureAwait(false); + + _receiveLoopCts.Dispose(); _logger?.Info("Disposing websocket connection to url '{url}'", _url); @@ -352,8 +360,8 @@ public async Task InternalDisposeAsync() try { - var timeoutToken = new CancellationTokenSource(5000).Token; - await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Done", timeoutToken).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(5000); + await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Done", timeoutCts.Token).ConfigureAwait(false); } catch (Exception exception) { diff --git a/ChromiumHtmlToPdfLib/Converter.cs b/ChromiumHtmlToPdfLib/Converter.cs index cfd6f03..a484ca9 100644 --- a/ChromiumHtmlToPdfLib/Converter.cs +++ b/ChromiumHtmlToPdfLib/Converter.cs @@ -191,8 +191,6 @@ private enum OutputFormat /// private string? _instanceId; - private CancellationTokenSource? _cancellationTokenSource; - /// /// /// @@ -269,7 +267,7 @@ public string? InstanceId _instanceId = value; if (_logger != null) _logger.InstanceId = value; - } + } } /// @@ -759,7 +757,7 @@ private async Task StartChromiumHeadlessAsync(CancellationToken cancellationToke var lines = await ReadDevToolsActiveFileAsync(cancellationToken).ConfigureAwait(false); var uri = new Uri($"ws://127.0.0.1:{lines[0]}{lines[1]}"); // DevToolsActivePort - ConnectToDevProtocol(uri, "dev tools active port file"); + await ConnectToDevProtocol(uri, "dev tools active port file", cancellationToken).ConfigureAwait(false); chromiumWaitSignal.Release(); } @@ -802,7 +800,6 @@ void OnChromiumProcessOnExited(object? o, EventArgs eventArgs) var exception = ExceptionHelpers.GetInnerException(Marshal.GetExceptionForHR(_chromiumProcess.ExitCode)); chromeException = $"{BrowserName} exited unexpectedly{(!string.IsNullOrWhiteSpace(exception) ? $", {exception}" : string.Empty)}"; - _cancellationTokenSource?.Cancel(); } finally { @@ -816,10 +813,6 @@ void OnChromiumProcessOnErrorDataReceived(object _, DataReceivedEventArgs args) if (string.IsNullOrEmpty(args.Data) || args.Data.StartsWith("[")) return; _logger?.Error("Received Chromium error data: '{error}'", args.Data); - - if (!args.Data.StartsWith("DevTools listening on")) return; - var uri = new Uri(args.Data.Replace("DevTools listening on ", string.Empty)); - ConnectToDevProtocol(uri, "data received from error stream"); // ReSharper disable once AccessToDisposedClosure chromiumWaitSignal.Release(); } @@ -896,10 +889,11 @@ private async Task ReadDevToolsActiveFileAsync(CancellationToken cance /// /// The uri to connect to /// From where we did get the uri - private void ConnectToDevProtocol(Uri uri, string readUriFrom) + /// + private async Task ConnectToDevProtocol(Uri uri, string readUriFrom, CancellationToken cancellationToken) { _logger?.Info("Connecting to dev protocol on uri '{uri}', got uri from {readUriFrom}", uri, readUriFrom); - _browser = new Browser(uri, WebSocketTimeout, _logger); + _browser = await Browser.Create(uri, WebSocketTimeout, _logger, cancellationToken).ConfigureAwait(false); _logger?.Info("Connected to dev protocol"); } #endregion @@ -907,7 +901,7 @@ private void ConnectToDevProtocol(Uri uri, string readUriFrom) #region CheckIfOutputFolderExists /// /// Checks if the path to the given exists. - /// An is thrown when the path is not valid + /// A is thrown when the path is not valid /// /// /// @@ -929,7 +923,7 @@ public void ResetChromiumArguments() _logger?.Info("Resetting Chromium arguments to default"); _defaultChromiumArgument = []; - + AddChromiumArgument("--headless=new"); // Use the new headless mode AddChromiumArgument("--block-new-web-contents"); // All pop-ups and calls to window.open will fail. AddChromiumArgument("--hide-scrollbars"); // Hide scrollbars from screenshots @@ -948,7 +942,7 @@ public void ResetChromiumArguments() AddChromiumArgument("--enable-automation"); AddChromiumArgument("--no-pings"); // Disable sending hyperlink auditing pings AddChromiumArgument("--noerrdialogs"); // Suppresses all error dialogs when present. - AddChromiumArgument("--run-all-compositor-stages-before-draw"); + AddChromiumArgument("--run-all-compositor-stages-before-draw"); AddChromiumArgument("--remote-debugging-port", "0"); // With a value of 0, Chrome will automatically select a useable port and will set navigator.webdriver to true. if (EnableChromiumLogging) @@ -981,7 +975,7 @@ public void RemoveChromiumArgument(string argument) if (argument.StartsWith("--headless")) throw new ArgumentException("Can't remove '--headless' argument, this argument is always needed"); - + switch (argument) { case "--no-first-run": @@ -1357,16 +1351,13 @@ private async Task ConvertAsync( object input, Stream outputStream, PageSettings pageSettings, - string? waitForWindowStatus = null, - int waitForWindowsStatusTimeout = 60000, - int? conversionTimeout = null, - int? mediaLoadTimeout = null, - ILogger? logger = null, - CancellationToken cancellationToken = default) + string? waitForWindowStatus, + int waitForWindowsStatusTimeout, + int? conversionTimeout, + int? mediaLoadTimeout, + ILogger? logger, + CancellationToken cancellationToken) { - if (cancellationToken == default) - _cancellationTokenSource = new CancellationTokenSource(); - if (_logger == null) _logger = new Logger(logger, InstanceId); else if (logger != null) @@ -1485,17 +1476,12 @@ private async Task ConvertAsync( await StartChromiumHeadlessAsync(cancellationToken).ConfigureAwait(false); - CountdownTimer? countdownTimer = null; - - if (conversionTimeout.HasValue) + if (conversionTimeout != null) { if (conversionTimeout <= 1) - throw new ArgumentOutOfRangeException($"The value for {nameof(countdownTimer)} has to be a value equal to 1 or greater"); + throw new ArgumentOutOfRangeException($"The value for {nameof(conversionTimeout)} has to be a value equal to 1 or greater"); _logger?.Info("Conversion timeout set to {timeout} milliseconds", conversionTimeout.Value); - - countdownTimer = new CountdownTimer(conversionTimeout.Value); - countdownTimer.Start(); } if (inputUri != null) @@ -1504,86 +1490,90 @@ private async Task ConvertAsync( else _logger?.Info("Loading url {url}", inputUri); - await _browser!.NavigateToAsync(safeUrls, _useCache, inputUri, html, countdownTimer, mediaLoadTimeout, _urlBlacklist, LogNetworkTraffic, WaitForNetworkIdle, cancellationToken).ConfigureAwait(false); + await _browser!.NavigateToAsync(safeUrls, _useCache, inputUri, html, mediaLoadTimeout, _urlBlacklist, LogNetworkTraffic, WaitForNetworkIdle, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(waitForWindowStatus)) { - if (conversionTimeout.HasValue) - { - _logger?.Info("Conversion timeout paused because we are waiting for a window.status"); - countdownTimer!.Stop(); - } - _logger?.Info("Waiting for window.status '{status}' or a timeout of {timeout} milliseconds", waitForWindowStatus, waitForWindowsStatusTimeout); var match = await _browser.WaitForWindowStatusAsync(waitForWindowStatus, waitForWindowsStatusTimeout, cancellationToken).ConfigureAwait(false); if (!match) _logger?.Info("Waiting timed out"); else _logger?.Info("Window status equaled {status}", waitForWindowStatus); - - if (conversionTimeout.HasValue) - { - _logger?.Info("Conversion timeout started again because we are done waiting for a window.status"); - countdownTimer!.Start(); - } } if (inputUri != null) _logger?.Info($"{(inputUri.IsFile ? "File" : "Url")} loaded"); - if (!string.IsNullOrWhiteSpace(RunJavascript)) - { - _logger?.Info("Start running javascript"); - _logger?.Info($"Javascript code:{Environment.NewLine}{{code}}", RunJavascript); - await _browser.RunJavascriptAsync(RunJavascript!, cancellationToken).ConfigureAwait(false); - _logger?.Info("Done running javascript"); - } + using var timeoutCts = conversionTimeout == null ? null : new CancellationTokenSource(conversionTimeout.Value); + using var linkedCts = timeoutCts == null ? null : CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + cancellationToken = linkedCts?.Token ?? cancellationToken; - if (CaptureSnapshot) + try { - if (SnapshotStream == null) - throw new ConversionException("The property CaptureSnapshot has been set to true but there is no stream assigned to the SnapshotStream"); + if (!string.IsNullOrWhiteSpace(RunJavascript)) + { + _logger?.Info("Start running javascript"); + _logger?.Info($"Javascript code:{Environment.NewLine}{{code}}", RunJavascript); + await _browser.RunJavascriptAsync(RunJavascript!, cancellationToken).ConfigureAwait(false); + _logger?.Info("Done running javascript"); + } - _logger?.Info("Taking snapshot of the page"); + if (CaptureSnapshot) + { + if (SnapshotStream == null) + throw new ConversionException("The property CaptureSnapshot has been set to true but there is no stream assigned to the SnapshotStream"); - var snapshot = await _browser.CaptureSnapshotAsync(countdownTimer, cancellationToken).ConfigureAwait(false); - using var memoryStream = new MemoryStream(snapshot.Bytes); - memoryStream.Position = 0; + _logger?.Info("Taking snapshot of the page"); + + var snapshot = await _browser.CaptureSnapshotAsync(cancellationToken).ConfigureAwait(false); + using var memoryStream = new MemoryStream(snapshot.Bytes); + memoryStream.Position = 0; #if (NETSTANDARD2_0) await memoryStream.CopyToAsync(SnapshotStream).ConfigureAwait(false); #else - await memoryStream.CopyToAsync(SnapshotStream, cancellationToken).ConfigureAwait(false); + await memoryStream.CopyToAsync(SnapshotStream, cancellationToken).ConfigureAwait(false); #endif - _logger?.Info("Taken"); - } + _logger?.Info("Taken"); + } - switch (outputFormat) - { - case OutputFormat.Pdf: - _logger?.Info("Converting to PDF"); - await _browser.PrintToPdfAsync(outputStream, pageSettings, countdownTimer, cancellationToken).ConfigureAwait(false); + switch (outputFormat) + { + case OutputFormat.Pdf: + _logger?.Info("Converting to PDF"); + await _browser.PrintToPdfAsync(outputStream, pageSettings, cancellationToken).ConfigureAwait(false); - break; + break; - case OutputFormat.Image: - { - _logger?.Info("Converting to image"); + case OutputFormat.Image: + { + _logger?.Info("Converting to image"); - var snapshot = await _browser.CaptureScreenshotAsync(countdownTimer, cancellationToken).ConfigureAwait(false); - using var memoryStream = new MemoryStream(snapshot.Bytes); - memoryStream.Position = 0; + var snapshot = await _browser.CaptureScreenshotAsync(cancellationToken).ConfigureAwait(false); + using var memoryStream = new MemoryStream(snapshot.Bytes); + memoryStream.Position = 0; #if (NETSTANDARD2_0) await memoryStream.CopyToAsync(outputStream).ConfigureAwait(false); #else - await memoryStream.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false); + await memoryStream.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false); #endif - break; + break; + } } } + catch (TaskCanceledException) + { + if (timeoutCts?.Token.IsCancellationRequested == true) + { + throw new ConversionTimedOutException($"The {nameof(ConvertAsync)} method timed out"); + } + + throw; + } _logger?.Info("Converted"); } @@ -1654,7 +1644,7 @@ private void WriteSnapShot(string outputFile) /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -1684,16 +1674,16 @@ public void ConvertToPdf( ILogger? logger = null) { ConvertToPdfAsync( - inputUri, - outputStream, - pageSettings, - waitForWindowStatus, + inputUri, + outputStream, + pageSettings, + waitForWindowStatus, waitForWindowsStatusTimeout, conversionTimeout, mediaLoadTimeout, logger).GetAwaiter().GetResult(); } - + /// /// Converts the given to PDF /// @@ -1708,7 +1698,7 @@ public void ConvertToPdf( /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -1764,7 +1754,7 @@ public void ConvertToPdf( /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -1807,7 +1797,7 @@ public void ConvertToPdf( mediaLoadTimeout, logger).GetAwaiter().GetResult(); } - + /// /// Converts the given to PDF /// @@ -1822,7 +1812,7 @@ public void ConvertToPdf( /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -1882,7 +1872,7 @@ public void ConvertToPdf( /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -1922,7 +1912,7 @@ await ConvertAsync( waitForWindowsStatusTimeout, conversionTimeout, mediaLoadTimeout, - logger, + logger, cancellationToken).ConfigureAwait(false); } @@ -1940,7 +1930,7 @@ await ConvertAsync( /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -1979,7 +1969,7 @@ public async Task ConvertToPdfAsync( SnapshotStream = new MemoryStream(); using var memoryStream = new MemoryStream(); - + await ConvertToPdfAsync(inputUri, memoryStream, pageSettings, waitForWindowStatus, waitForWindowsStatusTimeout, conversionTimeout, mediaLoadTimeout, logger, cancellationToken).ConfigureAwait(false); @@ -2013,7 +2003,7 @@ await ConvertToPdfAsync(inputUri, memoryStream, pageSettings, waitForWindowStatu /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -2075,7 +2065,7 @@ await ConvertAsync( /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -2155,7 +2145,7 @@ await ConvertToPdfAsync(html, memoryStream, pageSettings, waitForWindowStatus, /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -2209,7 +2199,7 @@ public void ConvertToImage( /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -2267,7 +2257,7 @@ public void ConvertToImage( /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -2324,7 +2314,7 @@ public void ConvertToImage( /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -2387,7 +2377,7 @@ public void ConvertToImage( /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -2427,7 +2417,7 @@ await ConvertAsync( waitForWindowsStatusTimeout, conversionTimeout, mediaLoadTimeout, - logger, + logger, cancellationToken).ConfigureAwait(false); } @@ -2445,7 +2435,7 @@ await ConvertAsync( /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -2489,7 +2479,7 @@ await ConvertAsync( waitForWindowsStatusTimeout, conversionTimeout, mediaLoadTimeout, - logger, + logger, cancellationToken).ConfigureAwait(false); } @@ -2507,7 +2497,7 @@ await ConvertAsync( /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -2547,7 +2537,7 @@ public async Task ConvertToImageAsync( SnapshotStream = new MemoryStream(); using var memoryStream = new MemoryStream(); - + await ConvertToImageAsync(inputUri, memoryStream, pageSettings, waitForWindowStatus, waitForWindowsStatusTimeout, conversionTimeout, mediaLoadTimeout, logger, cancellationToken).ConfigureAwait(false); @@ -2580,7 +2570,7 @@ await ConvertToImageAsync(inputUri, memoryStream, pageSettings, waitForWindowSta /// /// /// - /// An conversion timeout in milliseconds, if the conversion fails + /// A conversion timeout in milliseconds, if the conversion fails /// to finished in the set amount of time then an is raised /// /// @@ -2623,7 +2613,7 @@ public async Task ConvertToImageAsync( SnapshotStream = new MemoryStream(); using var memoryStream = new MemoryStream(); - + await ConvertToImageAsync(html, memoryStream, pageSettings, waitForWindowStatus, waitForWindowsStatusTimeout, conversionTimeout, mediaLoadTimeout, logger, cancellationToken).ConfigureAwait(false); @@ -2687,7 +2677,7 @@ private void KillProcessAndChildren(int processId) catch (Exception exception) { if (!exception.Message.Contains("is not running")) - _logger?.Error(exception, "{error}", exception.Message); + _logger?.Error(exception, "Failed to kill pocess: {error}", exception.Message); } } #endregion diff --git a/ChromiumHtmlToPdfLib/FileCache/BasicFileCacheManager.cs b/ChromiumHtmlToPdfLib/FileCache/BasicFileCacheManager.cs index de53db0..3bb4eb9 100644 --- a/ChromiumHtmlToPdfLib/FileCache/BasicFileCacheManager.cs +++ b/ChromiumHtmlToPdfLib/FileCache/BasicFileCacheManager.cs @@ -14,7 +14,7 @@ internal class BasicFileCacheManager(string cacheDir, string cacheSubFolder, str /// /// /// - public override IEnumerable GetKeys(string? regionName = null) + public override IEnumerable GetKeys(string? regionName) { var region = ""; if (string.IsNullOrEmpty(regionName) == false) region = regionName; @@ -33,7 +33,7 @@ public override IEnumerable GetKeys(string? regionName = null) /// /// /// - public override string GetCachePath(string fileName, string? regionName = null) + public override string GetCachePath(string fileName, string? regionName) { regionName ??= string.Empty; var directory = Path.Combine(CacheDir, CacheSubFolder, regionName); @@ -50,7 +50,7 @@ public override string GetCachePath(string fileName, string? regionName = null) /// /// /// - public override string GetPolicyPath(string key, string? regionName = null) + public override string GetPolicyPath(string key, string? regionName) { regionName ??= string.Empty; var directory = Path.Combine(CacheDir, PolicySubFolder, regionName); @@ -59,4 +59,4 @@ public override string GetPolicyPath(string key, string? regionName = null) return filePath; } #endregion -} \ No newline at end of file +} diff --git a/ChromiumHtmlToPdfLib/FileCache/FileCache.cs b/ChromiumHtmlToPdfLib/FileCache/FileCache.cs index f1fd95a..8d865c1 100644 --- a/ChromiumHtmlToPdfLib/FileCache/FileCache.cs +++ b/ChromiumHtmlToPdfLib/FileCache/FileCache.cs @@ -175,8 +175,7 @@ private set #endregion #region WriteHelper - private void WriteHelper(PayloadMode mode, string key, FileCachePayload data, string? regionName = null, - bool policyUpdateOnly = false) + private void WriteHelper(PayloadMode mode, string key, FileCachePayload data, string? regionName, bool policyUpdateOnly) { CurrentCacheSize += CacheManager.WriteFile(mode, key, data, regionName, policyUpdateOnly); @@ -265,7 +264,7 @@ public CacheItemReference(string key, string? region, string cachePath, string p /// public FileCache(FileCacheManagers manager) { - Init(manager); + Init(manager, false, new()); } /// @@ -388,8 +387,8 @@ public FileCache( [MemberNotNull(nameof(_binder), nameof(CacheDir), nameof(DefaultPolicy), nameof(CacheManager))] private void Init( FileCacheManagers manager, - bool calculateCacheSize = false, - TimeSpan cleanInterval = new()) + bool calculateCacheSize, + TimeSpan cleanInterval) { _name = $"FileCache_{_nameCounter}"; _nameCounter++; @@ -527,7 +526,6 @@ public long ShrinkCacheToSize(long newSize, string? regionName = null) CacheResized(this, new FileCacheEventArgs(originalSize - removed, MaxCacheSize)); // return the final size of the cache (or region) - return originalSize - removed; } #endregion @@ -574,7 +572,7 @@ public long CleanCache(string? regionName = null) Remove(key, region); // CT note: Remove will update CurrentCacheSize removed += ci.Length; } - catch (Exception) + catch (Exception) { // Skip if the file cannot be accessed } @@ -596,13 +594,13 @@ public long CleanCache(string? regionName = null) /// specified amount (in bytes). /// /// The amount of data that was actually removed - private long DeleteOldestFiles(long amount, string? regionName = null) + private long DeleteOldestFiles(long amount, string? regionName) { // Verify that we actually need to shrink if (amount <= 0) return 0; //Heap of all CacheReferences - var cacheReferences = new PriorityQueue(); + var cacheReferences = new PriorityQueue(null); var regions = !string.IsNullOrEmpty(regionName) @@ -838,7 +836,7 @@ public IEnumerable GetKeys(string? regionName = null) var cachePolicy = new SerializableCacheItemPolicy(policy); var newPayload = new FileCachePayload(value, cachePolicy); - WriteHelper(PayloadWriteMode, key, newPayload, regionName); + WriteHelper(PayloadWriteMode, key, newPayload, regionName, false); //As documented in the spec (http://msdn.microsoft.com/en-us/library/dd780602.aspx), return the old //cached value or null @@ -1022,7 +1020,7 @@ public override long GetCount(string? regionName = null) var enumerator = new List>(); var keys = CacheManager.GetKeys(regionName); - foreach (var key in keys) + foreach (var key in keys) enumerator.Add(new KeyValuePair(key, Get(key, regionName))); // ReSharper disable once NotDisposedResourceIsReturned return enumerator.GetEnumerator(); @@ -1145,4 +1143,4 @@ public override object? this[string key] set => Set(key, value, DefaultPolicy, DefaultRegion); } #endregion -} \ No newline at end of file +} diff --git a/ChromiumHtmlToPdfLib/FileCache/FileCacheManager.cs b/ChromiumHtmlToPdfLib/FileCache/FileCacheManager.cs index 47130df..dc1e34c 100644 --- a/ChromiumHtmlToPdfLib/FileCache/FileCacheManager.cs +++ b/ChromiumHtmlToPdfLib/FileCache/FileCacheManager.cs @@ -548,7 +548,7 @@ protected FileStream GetStream(string path, FileMode mode, FileAccess access, Fi } return stream; - } + } #endregion #region DeleteFile @@ -597,4 +597,4 @@ protected class LocalCacheBinder : SerializationBinder } } #endregion -} \ No newline at end of file +} diff --git a/ChromiumHtmlToPdfLib/FileCache/HashedFileCacheManager.cs b/ChromiumHtmlToPdfLib/FileCache/HashedFileCacheManager.cs index fb09ed5..065d0d9 100644 --- a/ChromiumHtmlToPdfLib/FileCache/HashedFileCacheManager.cs +++ b/ChromiumHtmlToPdfLib/FileCache/HashedFileCacheManager.cs @@ -38,7 +38,7 @@ public static string ComputeHash(string key) /// /// /// - private string GetFileName(string key, string? regionName = null) + private string GetFileName(string key, string? regionName) { regionName ??= string.Empty; @@ -87,7 +87,7 @@ private string GetFileName(string key, string? regionName = null) /// /// /// - public override string GetCachePath(string key, string? regionName = null) + public override string GetCachePath(string key, string? regionName) { regionName ??= string.Empty; var directory = Path.Combine(CacheDir, CacheSubFolder, regionName); @@ -102,7 +102,7 @@ public override string GetCachePath(string key, string? regionName = null) /// Returns a list of keys for a given region. /// /// - public override IEnumerable GetKeys(string? regionName = null) + public override IEnumerable GetKeys(string? regionName) { var region = string.Empty; if (string.IsNullOrEmpty(regionName) == false) region = regionName; @@ -132,7 +132,7 @@ public override IEnumerable GetKeys(string? regionName = null) /// /// /// - public override string GetPolicyPath(string key, string? regionName = null) + public override string GetPolicyPath(string key, string? regionName) { regionName ??= string.Empty; var directory = Path.Combine(CacheDir, PolicySubFolder, regionName); @@ -141,4 +141,4 @@ public override string GetPolicyPath(string key, string? regionName = null) return filePath; } #endregion -} \ No newline at end of file +} diff --git a/ChromiumHtmlToPdfLib/FileCache/PriortyQueue.cs b/ChromiumHtmlToPdfLib/FileCache/PriortyQueue.cs index b744870..2b9decd 100644 --- a/ChromiumHtmlToPdfLib/FileCache/PriortyQueue.cs +++ b/ChromiumHtmlToPdfLib/FileCache/PriortyQueue.cs @@ -31,7 +31,7 @@ internal class PriorityQueue where T : IComparable /// /// The comparer to use. The default comparer will make the smallest item the root of the heap. /// - public PriorityQueue(IComparer? comparer = null) + public PriorityQueue(IComparer? comparer) { _items = []; _comparer = comparer ?? new GenericComparer(); @@ -42,7 +42,7 @@ public PriorityQueue(IComparer? comparer = null) /// /// The unsorted list of items /// The comparer to use. The default comparer will make the smallest item the root of the heap. - public PriorityQueue(List unsorted, IComparer? comparer = null) : this(comparer) + public PriorityQueue(List unsorted, IComparer? comparer) : this(comparer) { foreach (var t in unsorted) _items.Add(t); @@ -194,7 +194,7 @@ public T Dequeue() return top; } #endregion - + #region GenericComparer private class GenericComparer : IComparer where TInner : IComparable { @@ -208,4 +208,4 @@ public int Compare(TInner? x, TInner? y) } } #endregion -} \ No newline at end of file +} diff --git a/ChromiumHtmlToPdfLib/Helpers/CountdownTimer.cs b/ChromiumHtmlToPdfLib/Helpers/CountdownTimer.cs deleted file mode 100644 index 4326973..0000000 --- a/ChromiumHtmlToPdfLib/Helpers/CountdownTimer.cs +++ /dev/null @@ -1,112 +0,0 @@ -// -// CountdownTimer.cs -// -// Author: Kees van Spelde -// -// Copyright (c) 2017-2024 Magic-Sessions. (www.magic-sessions.com) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -using System.Diagnostics; -// ReSharper disable UnusedMember.Global - -namespace ChromiumHtmlToPdfLib.Helpers; - -internal class CountdownTimer -{ - #region Fields - private readonly Stopwatch _stopwatch; - private readonly int _timeoutMilliseconds; - #endregion - - #region Properties - /// - /// Returns the milliseconds that are left before the countdown reaches zero - /// - public int MillisecondsLeft - { - get - { - if (!_stopwatch.IsRunning) - return 0; - - var value = _timeoutMilliseconds - (int)_stopwatch.ElapsedMilliseconds; - return value <= 0 ? 0 : value; - } - } - - /// - /// Returns true when the countdown timer is running - /// - public bool IsRunning => _stopwatch.IsRunning; - #endregion - - #region Constructor - /// - /// Makes this object and sets the timeout in milliseconds - /// - /// Timeout in milliseconds - internal CountdownTimer(int timeoutMilliseconds) - { - _stopwatch = new Stopwatch(); - _timeoutMilliseconds = timeoutMilliseconds; - } - #endregion - - #region Reset - /// - /// Stops the countdown and reset - /// - public void Reset() - { - _stopwatch.Reset(); - } - #endregion - - #region Restart - /// - /// Stops time interval measurement, resets the elapsed time to zero, and starts measuring elapsed time. - /// - public void Restart() - { - _stopwatch.Restart(); - } - #endregion - - #region Start - /// - /// Starts, or resumes, measuring elapsed time for an interval. - /// - public void Start() - { - _stopwatch.Start(); - } - #endregion - - #region Stop - /// - /// Stops measuring elapsed time for an interval. - /// - public void Stop() - { - _stopwatch.Stop(); - } - #endregion -} \ No newline at end of file diff --git a/ChromiumHtmlToPdfLib/Helpers/DocumentHelper.cs b/ChromiumHtmlToPdfLib/Helpers/DocumentHelper.cs index 8e6192e..501380d 100644 --- a/ChromiumHtmlToPdfLib/Helpers/DocumentHelper.cs +++ b/ChromiumHtmlToPdfLib/Helpers/DocumentHelper.cs @@ -166,7 +166,7 @@ private int TimeLeft public DocumentHelper( DirectoryInfo? tempDirectory, bool useCache, - FileSystemInfo cacheDirectory, + FileSystemInfo cacheDirectory, long cacheSize, IWebProxy? webProxy, int? imageLoadTimeout, @@ -216,9 +216,9 @@ private int ParseValue(string value) public async Task SanitizeHtmlAsync(ConvertUri inputUri, HtmlSanitizer? sanitizer, List safeUrls, CancellationToken cancellationToken) { #if (NETSTANDARD2_0) - using var webpage = inputUri.IsFile ? OpenFileStream(inputUri.OriginalString) : await OpenDownloadStream(inputUri).ConfigureAwait(false); + using var webpage = inputUri.IsFile ? OpenFileStream(inputUri.OriginalString) : await OpenDownloadStream(inputUri, false, cancellationToken).ConfigureAwait(false); #else - await using var webpage = inputUri.IsFile ? OpenFileStream(inputUri.OriginalString) : await OpenDownloadStream(inputUri).ConfigureAwait(false); + await using var webpage = inputUri.IsFile ? OpenFileStream(inputUri.OriginalString) : await OpenDownloadStream(inputUri, false, cancellationToken).ConfigureAwait(false); #endif var htmlChanged = false; var context = BrowsingContext.New(Config); @@ -236,7 +236,7 @@ public async Task SanitizeHtmlAsync(ConvertUri inputUri, Htm _logger?.Error(exception, "Exception occurred in AngleSharp: {exception}", ExceptionHelpers.GetInnerException(exception)); return new SanitizeHtmlResult(false, inputUri, safeUrls); } - + _logger?.Info("Sanitizing HTML"); sanitizer ??= new HtmlSanitizer(); @@ -413,9 +413,9 @@ internal void ResetTimeout() public async Task FitPageToContentAsync(ConvertUri inputUri, CancellationToken cancellationToken) { #if (NETSTANDARD2_0) - using var webpage = inputUri.IsFile ? OpenFileStream(inputUri.OriginalString) : await OpenDownloadStream(inputUri).ConfigureAwait(false); + using var webpage = inputUri.IsFile ? OpenFileStream(inputUri.OriginalString) : await OpenDownloadStream(inputUri, false, cancellationToken).ConfigureAwait(false); #else - await using var webpage = inputUri.IsFile ? OpenFileStream(inputUri.OriginalString) : await OpenDownloadStream(inputUri).ConfigureAwait(false); + await using var webpage = inputUri.IsFile ? OpenFileStream(inputUri.OriginalString) : await OpenDownloadStream(inputUri, false, cancellationToken).ConfigureAwait(false); #endif using var context = BrowsingContext.New(Config); @@ -431,7 +431,7 @@ public async Task FitPageToContentAsync(ConvertUri input if (document is not Document htmlElementDocument) throw new InvalidCastException("Could not cast document to Document"); - + var styleElement = new HtmlElement(htmlElementDocument, "style") { InnerHtml = "html, body " + Environment.NewLine + @@ -495,7 +495,7 @@ public async Task FitPageToContentAsync(ConvertUri input using var fileStream = new FileStream(outputFile, FileMode.CreateNew, FileAccess.Write); #else await using var fileStream = new FileStream(outputFile, FileMode.CreateNew, FileAccess.Write); -#endif +#endif if (inputUri.Encoding != null) { #if (NETSTANDARD2_0) @@ -556,11 +556,11 @@ public async Task ValidateImagesAsync( { using var graphics = Graphics.FromHwnd(IntPtr.Zero); #if (NETSTANDARD2_0) - using var webpage = inputUri.IsFile ? OpenFileStream(inputUri.OriginalString) : await OpenDownloadStream(inputUri).ConfigureAwait(false); + using var webpage = inputUri.IsFile ? OpenFileStream(inputUri.OriginalString) : await OpenDownloadStream(inputUri, false, cancellationToken).ConfigureAwait(false); #else - await using var webpage = inputUri.IsFile ? OpenFileStream(inputUri.OriginalString) : await OpenDownloadStream(inputUri).ConfigureAwait(false); + await using var webpage = inputUri.IsFile ? OpenFileStream(inputUri.OriginalString) : await OpenDownloadStream(inputUri, false, cancellationToken).ConfigureAwait(false); #endif - + _logger?.Info("DPI settings for image, x: '{dpiX}' and y: '{dpiY}'", graphics.DpiX, graphics.DpiY); var maxWidth = (pageSettings.PaperWidth - pageSettings.MarginLeft - pageSettings.MarginRight) * graphics.DpiX; var maxHeight = (pageSettings.PaperHeight - pageSettings.MarginTop - pageSettings.MarginBottom) * graphics.DpiY; @@ -635,11 +635,11 @@ public async Task ValidateImagesAsync( if (rotate) { - image = await GetImageAsync(htmlImage.Source, localDirectory).ConfigureAwait(false); + image = await GetImageAsync(htmlImage.Source, localDirectory, cancellationToken).ConfigureAwait(false); if (image == null) continue; - if (RotateImageByExifOrientationData(image)) + if (RotateImageByExifOrientationData(image, true)) { htmlImage.DisplayWidth = image.Width; htmlImage.DisplayHeight = image.Height; @@ -685,7 +685,7 @@ public async Task ValidateImagesAsync( // If we don't know the image size then get if from the image itself if (width <= 0 || height <= 0) { - image ??= await GetImageAsync(htmlImage.Source, localDirectory).ConfigureAwait(false); + image ??= await GetImageAsync(htmlImage.Source, localDirectory, cancellationToken).ConfigureAwait(false); if (image == null) continue; width = image.Width; height = image.Height; @@ -695,9 +695,9 @@ public async Task ValidateImagesAsync( { // If we did not load the image already then load it - image ??= await GetImageAsync(htmlImage.Source, localDirectory).ConfigureAwait(false); + image ??= await GetImageAsync(htmlImage.Source, localDirectory, cancellationToken).ConfigureAwait(false); if (image == null) continue; - + var ratio = maxWidth / image.Width; _logger?.Info("Rescaling image with current width {width}, height {height} and ratio {ratio}", image.Width, image.Height, ratio); @@ -729,8 +729,8 @@ public async Task ValidateImagesAsync( foreach (var unchangedImage in unchangedImages) { - using var image = await GetImageAsync(unchangedImage.Source!, localDirectory).ConfigureAwait(false); - + using var image = await GetImageAsync(unchangedImage.Source!, localDirectory, cancellationToken).ConfigureAwait(false); + if (image == null) { _logger?.Warn("Could not load unchanged image from location '{location}'", unchangedImage.Source); @@ -768,8 +768,8 @@ public async Task ValidateImagesAsync( using var fileStream = new FileStream(outputFile, FileMode.CreateNew, FileAccess.Write); #else await using var fileStream = new FileStream(outputFile, FileMode.CreateNew, FileAccess.Write); -#endif - +#endif + if (inputUri.Encoding != null) { #if (NETSTANDARD2_0) @@ -788,7 +788,7 @@ public async Task ValidateImagesAsync( #endif document.ToHtml(textWriter, new HtmlMarkupFormatter()); } - + _logger?.Info("Changed webpage written"); return new ValidateImagesResult(true, outputUri); @@ -807,8 +807,9 @@ public async Task ValidateImagesAsync( /// /// /// + /// /// - private async Task GetImageAsync(string imageSource, string? localDirectory) + private async Task GetImageAsync(string imageSource, string? localDirectory, CancellationToken cancellationToken) { if (imageSource.StartsWith("data:", StringComparison.InvariantCultureIgnoreCase)) { @@ -857,17 +858,17 @@ public async Task ValidateImagesAsync( case "http": { #if (NETSTANDARD2_0) - using var webStream = await OpenDownloadStream(imageUri, true).ConfigureAwait(false); + using var webStream = await OpenDownloadStream(imageUri, true, cancellationToken).ConfigureAwait(false); #else - await using var webStream = await OpenDownloadStream(imageUri, true).ConfigureAwait(false); + await using var webStream = await OpenDownloadStream(imageUri, true, cancellationToken).ConfigureAwait(false); #endif if (webStream == null) return null; var extension = Path.GetExtension(imageUri.AbsolutePath); - if (extension.ToLowerInvariant() != ".svg") + if (extension.ToLowerInvariant() != ".svg") return Image.FromStream(webStream, true, false); - + var svgDocument = SvgDocument.Open(webStream); return svgDocument.Draw(); } @@ -900,7 +901,7 @@ public async Task ValidateImagesAsync( /// (default is false) /// /// Returns true when the image is rotated - private bool RotateImageByExifOrientationData(Image image, bool updateExifData = true) + private bool RotateImageByExifOrientationData(Image image, bool updateExifData) { const int orientationId = 0x0112; if (!((IList)image.PropertyIdList).Contains(orientationId)) return false; @@ -984,8 +985,9 @@ private bool RotateImageByExifOrientationData(Image image, bool updateExifData = /// /// /// + /// /// - private async Task OpenDownloadStream(Uri sourceUri, bool checkTimeout = false) + private async Task OpenDownloadStream(Uri sourceUri, bool checkTimeout, CancellationToken cancellationToken) { try { @@ -1009,9 +1011,9 @@ private bool RotateImageByExifOrientationData(Image image, bool updateExifData = } else _logger?.Info("Opening stream to url '{url}'", sourceUri); - - var response = await client.GetAsync(sourceUri).ConfigureAwait(false); - + + var response = await client.GetAsync(sourceUri, cancellationToken).ConfigureAwait(false); + return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); } catch (Exception exception) diff --git a/ChromiumHtmlToPdfLib/Helpers/FileCacheHandler.cs b/ChromiumHtmlToPdfLib/Helpers/FileCacheHandler.cs index 4ad6e59..4665f9a 100644 --- a/ChromiumHtmlToPdfLib/Helpers/FileCacheHandler.cs +++ b/ChromiumHtmlToPdfLib/Helpers/FileCacheHandler.cs @@ -93,7 +93,7 @@ private FileCache.FileCache FileCache AccessTimeout = TimeSpan.FromSeconds(10), DefaultPolicy = new CacheItemPolicy { SlidingExpiration = TimeSpan.FromDays(1) }, }; - + return _fileCache; } } @@ -108,15 +108,15 @@ private FileCache.FileCache FileCache /// The cache size when is set to true, otherwise null /// internal FileCacheHandler( - bool useCache, - FileSystemInfo cacheDirectory, + bool useCache, + FileSystemInfo cacheDirectory, long cacheSize, Logger? logger) { _useCache = useCache; - + if (!useCache) return; - + _cacheDirectory = new DirectoryInfo(Path.Combine(cacheDirectory.FullName, "DocumentHelper")); _logger = logger; @@ -137,12 +137,12 @@ internal FileCacheHandler( /// /// /// - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (!_useCache) { IsFromCache = false; - return base.SendAsync(request, cancellationToken); + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } var key = request.RequestUri!.ToString(); @@ -160,22 +160,27 @@ protected override Task SendAsync(HttpRequestMessage reques _logger?.Info("Returned item from cache"); - return Task.FromResult(cachedResponse); + return cachedResponse; } IsFromCache = false; - + var response = base.SendAsync(request, cancellationToken).Result; var memoryStream = new MemoryStream(); - - response.Content.ReadAsStreamAsync().GetAwaiter().GetResult().CopyTo(memoryStream); - + + var contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); +#if (NETSTANDARD2_0) + await contentStream.CopyToAsync(memoryStream).ConfigureAwait(false); +#else + await contentStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); +#endif + FileCache.Add(key, memoryStream.ToArray(), new CacheItemPolicy { SlidingExpiration = TimeSpan.FromDays(1) }); _logger?.Info("Added item to cache"); response.Content = new StreamContent(new MemoryStream(memoryStream.ToArray())); - return Task.FromResult(response); + return response; } #endregion } diff --git a/ChromiumHtmlToPdfLib/Helpers/PreWrapper.cs b/ChromiumHtmlToPdfLib/Helpers/PreWrapper.cs index b2f1e73..5aa2cf8 100644 --- a/ChromiumHtmlToPdfLib/Helpers/PreWrapper.cs +++ b/ChromiumHtmlToPdfLib/Helpers/PreWrapper.cs @@ -141,7 +141,7 @@ public string WrapFile(string inputFile, Encoding? encoding) if (result.Detected.Encoding != null) { encoding = result.Detected.Encoding; - _logger?.Info("{statusLog}", result.Detected.StatusLog); + _logger?.Info("Encoding detection status log: {statusLog}", result.Detected.StatusLog); } else { diff --git a/ChromiumHtmlToPdfLib/Loggers/Logger.cs b/ChromiumHtmlToPdfLib/Loggers/Logger.cs index d6be05c..e1771b8 100644 --- a/ChromiumHtmlToPdfLib/Loggers/Logger.cs +++ b/ChromiumHtmlToPdfLib/Loggers/Logger.cs @@ -40,7 +40,7 @@ internal class Logger /// When set then logging is written to this ILogger instance /// public ILogger? InternalLogger { get; set; } - + /// /// A unique id that can be used to identify the logging of the converter when /// calling the code from multiple threads and writing all the logging to the same file diff --git a/ChromiumHtmlToPdfLib/Loggers/Stream.cs b/ChromiumHtmlToPdfLib/Loggers/Stream.cs index 58d5105..d8606d5 100644 --- a/ChromiumHtmlToPdfLib/Loggers/Stream.cs +++ b/ChromiumHtmlToPdfLib/Loggers/Stream.cs @@ -119,4 +119,4 @@ public void Dispose() _stream = null; } #endregion -} \ No newline at end of file +} diff --git a/ChromiumHtmlToPdfLib/Protocol/Evaluate.cs b/ChromiumHtmlToPdfLib/Protocol/Evaluate.cs index 28dafcf..6481324 100644 --- a/ChromiumHtmlToPdfLib/Protocol/Evaluate.cs +++ b/ChromiumHtmlToPdfLib/Protocol/Evaluate.cs @@ -66,7 +66,7 @@ internal class Evaluate : MessageBase internal class EvaluateResult { #region Propreties - [JsonProperty("result")] + [JsonProperty("result")] public EvaluateInnerResult? Result { get; set; } #endregion } @@ -77,31 +77,31 @@ internal class EvaluateResult internal class EvaluateInnerResult { #region Properties - [JsonProperty("type")] + [JsonProperty("type")] public string? Type { get; set; } - [JsonProperty("subtype")] + [JsonProperty("subtype")] public string? SubType { get; set; } - [JsonProperty("className")] + [JsonProperty("className")] public string? ClassName { get; set; } - [JsonProperty("value")] + [JsonProperty("value")] public string? Value { get; set; } - - [JsonProperty("unserializableValue")] + + [JsonProperty("unserializableValue")] public string? UnserializableValue { get; set; } - - [JsonProperty("deepSerializedValue")] + + [JsonProperty("deepSerializedValue")] public string? DeepSerializedValue { get; set; } - - [JsonProperty("preview")] + + [JsonProperty("preview")] public string? Preview { get; set; } - - [JsonProperty("customPreview")] + + [JsonProperty("customPreview")] public string? CustomPreview { get; set; } #endregion - + #region ToString /// /// Returns a string representation of this object @@ -138,4 +138,4 @@ public override string ToString() return stringBuilder.ToString(); } #endregion -} \ No newline at end of file +} diff --git a/README.md b/README.md index 931541c..8e1d1ae 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The easiest way to install ChromiumHtmlToPdf is via NuGet (Yes I know the nuget In Visual Studio's Package Manager Console, simply enter the following command: - Install-Package ChromeHtmlToPdf + Install-Package ChromeHtmlToPdf ### Converting a file or url from code @@ -133,7 +133,7 @@ google-chrome --no-sandbox --user-data-dir Pre compiled binaries ===================== -You can find pre compiled binaries for Windows, Linux and macOS over here +You can find pre compiled binaries for Windows, Linux and macOS over here Latest version (.net 6) --------------- @@ -141,7 +141,7 @@ https://github.com/Sicos1977/ChromiumHtmlToPdf/releases/download/4.2.1/ChromiumH .NET 6.0 for the console app --------------------------------- -The console app needs .NET 6 to run, you can download this framework from here +The console app needs .NET 6 to run, you can download this framework from here https://dotnet.microsoft.com/en-us/download/dotnet/6.0 @@ -161,7 +161,7 @@ https://github.com/Sicos1977/ChromiumHtmlToPdf/releases/download/2.0.11/ChromeHt .NET Core 3.1 for the console app (end of life) --------------------------------- -The console app needs .NET Core 3.1 to run, you can download this framework from here +The console app needs .NET Core 3.1 to run, you can download this framework from here https://dotnet.microsoft.com/en-us/download/dotnet/3.1 @@ -181,7 +181,7 @@ Logging From version 2.5.0 ChromiumHtmlToPdfLib uses the Microsoft ILogger interface (https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.ilogger?view=dotnet-plat-ext-5.0). You can use any logging library that uses this interface. -ChromiumHtmlToPdfLib has some build in loggers that can be found in the ```ChromiumHtmlToPdfLib.Logger``` namespace. +ChromiumHtmlToPdfLib has some build in loggers that can be found in the ```ChromiumHtmlToPdfLib.Logger``` namespace. For example @@ -194,7 +194,7 @@ var logger = !string.IsNullOrWhiteSpace() Setting a common Google Chrome or Microsoft Edge cache directory ================================================================ -You can not share a cache directory between a Google Chrome or Microsoft Edge instances because the first instance that is using the cache directory will lock it for its own use. The most efficient way to make optimal use of a cache directory is to create one for each instance that you are running. +You can not share a cache directory between a Google Chrome or Microsoft Edge instances because the first instance that is using the cache directory will lock it for its own use. The most efficient way to make optimal use of a cache directory is to create one for each instance that you are running. I'm using Google Chrome from a WCF service and used the class below to make optimal use of cache directories. The class will create an instance id that I use to create a cache directory for each running Chrome instance. When the instance shuts down the instance id is put back in a stack so that the next executing instance can use this directory again.