diff --git a/generate_upload_package.ps1 b/generate_upload_package.ps1 index 490dae1..828e328 100644 --- a/generate_upload_package.ps1 +++ b/generate_upload_package.ps1 @@ -1,6 +1,27 @@ -if (Test-Path -Path .\UltimatePDF_ExternalLogic.zip -PathType Leaf) { - Remove-Item -Path .\UltimatePDF_ExternalLogic.zip -Force +$zipPath = ".\UltimatePDF_ExternalLogic.zip" +$projectPath = "src\UltimatePDF_ExternalLogic\UltimatePDF_ExternalLogic.csproj" +$publishDir = ".\src\UltimatePDF_ExternalLogic\bin\Release\net10.0\linux-x64\publish\" + +if (Test-Path -Path $zipPath -PathType Leaf) { + Write-Host "Removing existing package: $zipPath" + Remove-Item -Path $zipPath -Force } + +Write-Host "Setting execution policy for current user (Unrestricted)..." Set-ExecutionPolicy -Scope CurrentUser Unrestricted -dotnet publish src\UltimatePDF_ExternalLogic.sln -c Release -r linux-x64 --self-contained false -Compress-Archive -Path .\src\UltimatePDF_ExternalLogic\bin\Release\net10.0\linux-x64\publish\* -Update -DestinationPath UltimatePDF_ExternalLogic.zip \ No newline at end of file + +# Only the main project is needed to produce the upload package (test projects aren't part of it). +Write-Host "Publishing $projectPath for linux-x64 (Release, framework-dependent)..." +dotnet publish $projectPath -c Release -r linux-x64 --self-contained false + +# dotnet publish doesn't throw in PowerShell on failure, so without this check a failed build +# would silently get packaged from stale (or missing) publish output. +if ($LASTEXITCODE -ne 0) { + Write-Host "dotnet publish failed with exit code $LASTEXITCODE. Aborting package creation." -ForegroundColor Red + exit $LASTEXITCODE +} + +Write-Host "Compressing publish output into $zipPath..." +Compress-Archive -Path (Join-Path $publishDir '*') -Update -DestinationPath $zipPath + +Write-Host "Package created: $zipPath" -ForegroundColor Green \ No newline at end of file diff --git a/src/UltimatePDF_ExternalLogic/BrowserExecution/BrowserInstancePool.cs b/src/UltimatePDF_ExternalLogic/BrowserExecution/BrowserInstancePool.cs index bdc6274..3875c43 100644 --- a/src/UltimatePDF_ExternalLogic/BrowserExecution/BrowserInstancePool.cs +++ b/src/UltimatePDF_ExternalLogic/BrowserExecution/BrowserInstancePool.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -7,41 +8,42 @@ using PuppeteerSharp; using UltimatePDF_ExternalLogic.Utils; -namespace OutSystems.UltimatePDF_ExternalLogic.BrowserExecution { - public class BrowserInstancePool { +namespace OutSystems.UltimatePDF_ExternalLogic.BrowserExecution; +public class BrowserInstancePool { - private readonly SemaphoreSlim mutex = new(1, 1); + private readonly SemaphoreSlim mutex = new(1, 1); - private static readonly List pool = new(); + private static readonly List pool = new(); - public BrowserInstancePool() { - } - - private async Task NewBrowserInstance(Logger logger) { - await mutex.WaitAsync(); - try { - var instance = pool.FirstOrDefault(i => i.IsHealthy); + public BrowserInstancePool() { + } - if (instance == null) { - logger.Log("Create new Browser Instance"); + private async Task NewBrowserInstance(Logger logger) { + using var activity = Activity.Current?.Source.StartActivity("BrowserInstancePool.NewBrowserInstance"); + await mutex.WaitAsync(); + try { + var instance = pool.FirstOrDefault(i => i.IsHealthy); - var browserLauncher = new HeadlessChromiumPuppeteerLauncher(logger.GetLoggerFactory("browser.txt")); - var browser = await browserLauncher.LaunchAsync(); - instance = new PooledBrowserInstance(browser); - pool.Add(instance); - } + if (instance == null) { + logger.Log("Create new Browser Instance"); - return instance; - } finally { - mutex.Release(); + var browserLauncher = new HeadlessChromiumPuppeteerLauncher(logger.GetLoggerFactory("browser.txt")); + var browser = await browserLauncher.LaunchAsync(); + instance = new PooledBrowserInstance(browser); + pool.Add(instance); } - } - public async Task NewPooledPage(Logger logger) { - var instance = await NewBrowserInstance(logger); - var page = await instance.Browser.NewPageAsync(); - var pooledPage = new PooledPage(page, logger); - return pooledPage; + return instance; + } finally { + mutex.Release(); } } + + public async Task NewPooledPage(Logger logger) { + using var activity = Activity.Current?.Source.StartActivity("BrowserInstancePool.NewPooledPage"); + var instance = await NewBrowserInstance(logger); + var page = await instance.Browser.NewPageAsync(); + var pooledPage = new PooledPage(page, logger); + return pooledPage; + } } diff --git a/src/UltimatePDF_ExternalLogic/BrowserExecution/ODCUltimatePDFExecutionContext.cs b/src/UltimatePDF_ExternalLogic/BrowserExecution/ODCUltimatePDFExecutionContext.cs index 506e3d3..617442e 100644 --- a/src/UltimatePDF_ExternalLogic/BrowserExecution/ODCUltimatePDFExecutionContext.cs +++ b/src/UltimatePDF_ExternalLogic/BrowserExecution/ODCUltimatePDFExecutionContext.cs @@ -10,202 +10,210 @@ using PuppeteerSharp; using PuppeteerSharp.Media; -namespace OutSystems.UltimatePDF_ExternalLogic.BrowserExecution { - internal class UltimatePDFExecutionContext { +namespace OutSystems.UltimatePDF_ExternalLogic.BrowserExecution; +internal class UltimatePDFExecutionContext { - private const string USER_AGENT_SUFFIX = "UltimatePDF/1.0"; + private const string USER_AGENT_SUFFIX = "UltimatePDF/1.0"; - private static readonly BrowserInstancePool pool = new(); + private static readonly BrowserInstancePool pool = new(); - public async static Task PrintPDF( - Uri uri, string baseUrl, string locale, string timezone, IEnumerable cookies, - ViewPortOptions viewport, PdfOptions options, int timeoutSeconds, Logger logger) { + public async static Task PrintPDF( + Uri uri, string baseUrl, string locale, string timezone, IEnumerable cookies, + ViewPortOptions viewport, PdfOptions options, int timeoutSeconds, Logger logger) { - logger.Log("Page open... " + uri); + using var activity = Activity.Current?.Source.StartActivity("UltimatePDFExecutionContext.PrintPDF"); - Stopwatch sw = new(); - sw.Start(); + logger.Log("Page open... " + uri); - using var pooled = await pool.NewPooledPage(logger); - await SetupPage(pooled.Page, uri, viewport, locale, timezone, cookies, - timeoutSeconds, logger, baseUrl); + Stopwatch sw = new(); + sw.Start(); - logger.Log("Page opened in " + sw.ElapsedMilliseconds + "ms"); - await pooled.Page.WaitForSelectorAsync(":root:not(.ultimate-pdf-is-not-ready)"); - logger.Log("Page opened and ready in " + sw.ElapsedMilliseconds + "ms"); + using var pooled = await pool.NewPooledPage(logger); + await SetupPage(pooled.Page, uri, viewport, locale, timezone, cookies, + timeoutSeconds, logger, baseUrl); - if (!string.IsNullOrEmpty(baseUrl)) { - await pooled.Page.EvaluateExpressionAsync("window?.UltimatePDF?.setBaseUrl?.('" + HttpUtility.JavaScriptStringEncode(baseUrl) + "')"); - } - - if (logger.IsEnabled) { - string html = await pooled.Page.GetContentAsync(); - logger.Attach("input.html", Encoding.UTF8.GetBytes(html)); - } + logger.Log("Page opened in " + sw.ElapsedMilliseconds + "ms"); + await pooled.Page.WaitForSelectorAsync(":root:not(.ultimate-pdf-is-not-ready)"); + logger.Log("Page opened and ready in " + sw.ElapsedMilliseconds + "ms"); - Pipeline pipeline = new Pipeline(); - await pipeline.Initialize(pooled.Page); - byte[] pdf; + if (!string.IsNullOrEmpty(baseUrl)) { + await pooled.Page.EvaluateExpressionAsync("window?.UltimatePDF?.setBaseUrl?.('" + HttpUtility.JavaScriptStringEncode(baseUrl) + "')"); + } - if (pipeline.HasLayouts) { - logger.Log("Using UltimatePDF layout pipeline"); + if (logger.IsEnabled) { + string html = await pooled.Page.GetContentAsync(); + logger.Attach("input.html", Encoding.UTF8.GetBytes(html)); + } - pdf = await pipeline.Render(pooled.Page, logger); - logger.Attach("output.pdf", pdf); - } else { - await InjectCustomStylesAsync(pooled.Page, ref options); - pdf = await pooled.Page.PdfDataAsync(options); - logger.Attach("output.pdf", pdf); - } + Pipeline pipeline = new Pipeline(); + await pipeline.Initialize(pooled.Page); + byte[] pdf; - await pooled.Page.CloseAsync(); - await pooled.Page.Browser.CloseAsync(); + if (pipeline.HasLayouts) { + logger.Log("Using UltimatePDF layout pipeline"); - return pdf; + pdf = await pipeline.Render(pooled.Page, logger); + logger.Attach("output.pdf", pdf); + } else { + await InjectCustomStylesAsync(pooled.Page, ref options); + pdf = await pooled.Page.PdfDataAsync(options); + logger.Attach("output.pdf", pdf); } - public async static Task ScreenshotPNG( - Uri uri, string baseUrl, string locale, string timezone, IEnumerable cookies, - ViewPortOptions viewport, ScreenshotOptions options, int timeoutSeconds, Logger logger) { + await pooled.Page.CloseAsync(); + await pooled.Page.Browser.CloseAsync(); - logger.Log("Page open..."); + return pdf; + } - var sw = new Stopwatch(); - sw.Start(); + public async static Task ScreenshotPNG( + Uri uri, string baseUrl, string locale, string timezone, IEnumerable cookies, + ViewPortOptions viewport, ScreenshotOptions options, int timeoutSeconds, Logger logger) { - using var pooled = await pool.NewPooledPage(logger); - await SetupPage(pooled.Page, uri, viewport, locale, timezone, cookies, timeoutSeconds, logger, baseUrl); + using var activity = Activity.Current?.Source.StartActivity("UltimatePDFExecutionContext.ScreenshotPNG"); - if (logger.IsEnabled) { - string html = await pooled.Page.GetContentAsync(); - logger.Attach("input.html", Encoding.UTF8.GetBytes(html)); - } + logger.Log("Page open..."); + + var sw = new Stopwatch(); + sw.Start(); - byte[] png = await pooled.Page.ScreenshotDataAsync(options); - logger.Attach("output.png", png); + using var pooled = await pool.NewPooledPage(logger); + await SetupPage(pooled.Page, uri, viewport, locale, timezone, cookies, timeoutSeconds, logger, baseUrl); - return png; + if (logger.IsEnabled) { + string html = await pooled.Page.GetContentAsync(); + logger.Attach("input.html", Encoding.UTF8.GetBytes(html)); } - private static async Task SetupPage( - IPage page, Uri uri, ViewPortOptions viewport, string locale, string timezone, - IEnumerable cookies, int timeout, Logger logger, string baseUrl) { + byte[] png = await pooled.Page.ScreenshotDataAsync(options); + logger.Attach("output.png", png); - await page.SetViewportAsync(viewport); + return png; + } - string originalUserAgent = await page.Browser.GetUserAgentAsync(); - await page.SetUserAgentAsync($"{originalUserAgent} {USER_AGENT_SUFFIX}"); + private static async Task SetupPage( + IPage page, Uri uri, ViewPortOptions viewport, string locale, string timezone, + IEnumerable cookies, int timeout, Logger logger, string baseUrl) { - if (!string.IsNullOrEmpty(locale)) { - await page.SetExtraHttpHeadersAsync(GetLocaleHeaders(locale)); - await page.EvaluateExpressionOnNewDocumentAsync(GetLocaleExpressionToEvaluate(locale)); - } + using var activity = Activity.Current?.Source.StartActivity("UltimatePDFExecutionContext.SetupPage"); - if (!string.IsNullOrEmpty(timezone)) { - await page.EmulateTimezoneAsync(timezone); - } + await page.SetViewportAsync(viewport); - if (cookies.Any()) { - await page.SetCookieAsync(cookies.ToArray()); - } + string originalUserAgent = await page.Browser.GetUserAgentAsync(); + await page.SetUserAgentAsync($"{originalUserAgent} {USER_AGENT_SUFFIX}"); - var navigationOptions = new NavigationOptions() { - WaitUntil = new WaitUntilNavigation[] { - WaitUntilNavigation.DOMContentLoaded, - WaitUntilNavigation.Load, - WaitUntilNavigation.Networkidle0 - } - }; + if (!string.IsNullOrEmpty(locale)) { + await page.SetExtraHttpHeadersAsync(GetLocaleHeaders(locale)); + await page.EvaluateExpressionOnNewDocumentAsync(GetLocaleExpressionToEvaluate(locale)); + } - if (timeout > 0) { - navigationOptions.Timeout = timeout * 1000; - } + if (!string.IsNullOrEmpty(timezone)) { + await page.EmulateTimezoneAsync(timezone); + } - lock (page.Browser) { - logger.Log($"Go to... {uri}"); - page.GoToAsync(uri.ToString(), navigationOptions).Wait(); - } + if (cookies.Any()) { + await page.SetCookieAsync(cookies.ToArray()); + } - if (!string.IsNullOrEmpty(locale)) { - logger.Log($"Set locale to {locale}"); - await page.EvaluateExpressionAsync("window?.UltimatePDF?.runtime?.setLocale?.('" + HttpUtility.JavaScriptStringEncode(locale) + "')"); + var navigationOptions = new NavigationOptions() { + WaitUntil = new WaitUntilNavigation[] { + WaitUntilNavigation.DOMContentLoaded, + WaitUntilNavigation.Load, + WaitUntilNavigation.Networkidle0 } + }; - // used for the Async Screen client actions - await page.WaitForSelectorAsync(":root:not(.ultimate-pdf-is-not-ready)"); + if (timeout > 0) { + navigationOptions.Timeout = timeout * 1000; + } - if (!string.IsNullOrEmpty(baseUrl)) { - logger.Log($"Set base url to {baseUrl}"); - await page.EvaluateExpressionAsync("window?.UltimatePDF?.setBaseUrl?.('" + HttpUtility.JavaScriptStringEncode(baseUrl) + "')"); - } + lock (page.Browser) { + logger.Log($"Go to... {uri}"); + page.GoToAsync(uri.ToString(), navigationOptions).Wait(); } - private static Dictionary GetLocaleHeaders(string locale) { - var headers = new Dictionary(); + if (!string.IsNullOrEmpty(locale)) { + logger.Log($"Set locale to {locale}"); + await page.EvaluateExpressionAsync("window?.UltimatePDF?.runtime?.setLocale?.('" + HttpUtility.JavaScriptStringEncode(locale) + "')"); + } - int separatorIndex = locale.IndexOf("-"); - if (separatorIndex > 0) { - headers.Add("Accept-Language", string.Concat(locale, ",", locale[0..separatorIndex])); - } else { - headers.Add("Accept-Language", locale); - } + // used for the Async Screen client actions + await page.WaitForSelectorAsync(":root:not(.ultimate-pdf-is-not-ready)"); - return headers; - } - - private static string GetLocaleExpressionToEvaluate(string locale) { - return @" - Object.defineProperty(navigator, 'language', { - get: function() { return '" + HttpUtility.JavaScriptStringEncode(locale) + @"'; } - }); - Object.defineProperty(navigator, 'languages', { - get: function() { - var separatorIndex = this.language.indexOf('-'); - if (separatorIndex > 0) { - return [ this.language, this.language.substr(0, separatorIndex) ]; - } else { - return [ this.language ]; - } + if (!string.IsNullOrEmpty(baseUrl)) { + logger.Log($"Set base url to {baseUrl}"); + await page.EvaluateExpressionAsync("window?.UltimatePDF?.setBaseUrl?.('" + HttpUtility.JavaScriptStringEncode(baseUrl) + "')"); + } + } + + private static Dictionary GetLocaleHeaders(string locale) { + using var activity = Activity.Current?.Source.StartActivity("UltimatePDFExecutionContext.GetLocaleHeaders"); + var headers = new Dictionary(); + + int separatorIndex = locale.IndexOf("-"); + if (separatorIndex > 0) { + headers.Add("Accept-Language", string.Concat(locale, ",", locale[0..separatorIndex])); + } else { + headers.Add("Accept-Language", locale); + } + + return headers; + } + + private static string GetLocaleExpressionToEvaluate(string locale) { + using var activity = Activity.Current?.Source.StartActivity("UltimatePDFExecutionContext.GetLocaleExpressionToEvaluate"); + return @" + Object.defineProperty(navigator, 'language', { + get: function() { return '" + HttpUtility.JavaScriptStringEncode(locale) + @"'; } + }); + Object.defineProperty(navigator, 'languages', { + get: function() { + var separatorIndex = this.language.indexOf('-'); + if (separatorIndex > 0) { + return [ this.language, this.language.substr(0, separatorIndex) ]; + } else { + return [ this.language ]; } - }); - "; - } - - private static Task InjectCustomStylesAsync(IPage page, ref PdfOptions options) { - /* - * It seems that Puppeteer is not overriding the page styles from the print stylesheet. - * As a workaround, we inject a