From 68332a7290d1caccf8e7d12b114595beb0b43002 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Mon, 27 Dec 2021 22:01:29 +0000 Subject: [PATCH] --- .../Controllers/JobOperationsController.cs | 1 + .../AyaNova/Controllers/ReportController.cs | 101 +- server/AyaNova/biz/JobsBiz.cs | 2 +- server/AyaNova/biz/ReportBiz.cs | 942 +++++++++--------- ...CoreJobReportRenderEngineProcessCleanup.cs | 6 +- server/AyaNova/util/ReportProcessManager.cs | 98 +- 6 files changed, 631 insertions(+), 519 deletions(-) diff --git a/server/AyaNova/Controllers/JobOperationsController.cs b/server/AyaNova/Controllers/JobOperationsController.cs index ff79595e..f5c2773b 100644 --- a/server/AyaNova/Controllers/JobOperationsController.cs +++ b/server/AyaNova/Controllers/JobOperationsController.cs @@ -99,6 +99,7 @@ namespace AyaNova.Api.Controllers return Ok(ApiOkResponse.Response(await JobsBiz.GetJobStatusAsync(gid))); } + /// diff --git a/server/AyaNova/Controllers/ReportController.cs b/server/AyaNova/Controllers/ReportController.cs index 702eed17..57587dec 100644 --- a/server/AyaNova/Controllers/ReportController.cs +++ b/server/AyaNova/Controllers/ReportController.cs @@ -195,50 +195,50 @@ namespace AyaNova.Api.Controllers } - /// - /// Render Report - /// - /// report id and object id values for object type specified in report template - /// From route path - /// downloadable pdf name - [HttpPost("render")] - public async Task RenderReport([FromBody] DataListReportRequest reportRequest, ApiVersion apiVersion) - { - if (!serverState.IsOpen) - return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + // /// + // /// Render Report + // /// + // /// report id and object id values for object type specified in report template + // /// From route path + // /// downloadable pdf name + // [HttpPost("render")] + // public async Task RenderReport([FromBody] DataListReportRequest reportRequest, ApiVersion apiVersion) + // { + // if (!serverState.IsOpen) + // return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); - //this is done by a recurring JOB now so no longer needed here - // //check for an kill any expired prior renders stuck around - // Util.ReportRenderManager.KillExpiredRenders(log); + // //this is done by a recurring JOB now so no longer needed here + // // //check for an kill any expired prior renders stuck around + // // Util.ReportRenderManager.KillExpiredRenders(log); - ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext); - if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) - return StatusCode(403, new ApiNotAuthorizedResponse()); - if (!ModelState.IsValid) - return BadRequest(new ApiErrorResponse(ModelState)); + // ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext); + // if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) + // return StatusCode(403, new ApiNotAuthorizedResponse()); + // if (!ModelState.IsValid) + // return BadRequest(new ApiErrorResponse(ModelState)); - var httpConnectionFeature = HttpContext.Features.Get(); - var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/v8/"; - try - { - var result = await biz.RenderReport(reportRequest, DateTime.UtcNow.AddMinutes(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT), API_URL); - if (string.IsNullOrWhiteSpace(result)) - return BadRequest(new ApiErrorResponse(biz.Errors)); - else - return Ok(ApiOkResponse.Response(result)); - } - catch (ReportRenderTimeOutException) - { - log.LogInformation($"Report render timeout, exceeded timeout setting of {ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT} minutes, report id: {reportRequest.ReportId}, record count:{reportRequest.SelectedRowIds.LongLength}, user:{UserNameFromContext.Name(HttpContext.Items)} "); - return Ok(ApiOkResponse.Response(new { timeout = true, timeoutconfigminutes = ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT })); - } - catch (System.Exception ex) - { - //The Javascript evaluation stack trace can be in the message making it long and internalized, - //however the info is useful as it can indicate exactly which function failed etc so sending it all back is best - return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, ex.Message)); - } - } + // var httpConnectionFeature = HttpContext.Features.Get(); + // var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/v8/"; + // try + // { + // var result = await biz.RenderReport(reportRequest, DateTime.UtcNow.AddMinutes(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT), API_URL); + // if (string.IsNullOrWhiteSpace(result)) + // return BadRequest(new ApiErrorResponse(biz.Errors)); + // else + // return Ok(ApiOkResponse.Response(result)); + // } + // catch (ReportRenderTimeOutException) + // { + // log.LogInformation($"Report render timeout, exceeded timeout setting of {ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT} minutes, report id: {reportRequest.ReportId}, record count:{reportRequest.SelectedRowIds.LongLength}, user:{UserNameFromContext.Name(HttpContext.Items)} "); + // return Ok(ApiOkResponse.Response(new { timeout = true, timeoutconfigminutes = ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT })); + // } + // catch (System.Exception ex) + // { + // //The Javascript evaluation stack trace can be in the message making it long and internalized, + // //however the info is useful as it can indicate exactly which function failed etc so sending it all back is best + // return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, null, ex.Message)); + // } + // } @@ -267,6 +267,25 @@ namespace AyaNova.Api.Controllers return Accepted(new { JobId = result }); } + /// + /// Attempt cancel render job + /// + /// + /// nothing + [HttpGet("request-cancel/{gid}")] + public async Task RequestCancelJob([FromRoute] Guid gid) + { + if (!serverState.IsOpen) + return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); + ReportBiz biz = ReportBiz.GetBiz(ct, HttpContext); + if (!Authorized.HasReadFullRole(HttpContext.Items, biz.BizType)) + return StatusCode(403, new ApiNotAuthorizedResponse()); + if (!ModelState.IsValid) + return BadRequest(new ApiErrorResponse(ModelState)); + await biz.CancelJob(gid); + return NoContent(); + } + /// /// Download a rendered report diff --git a/server/AyaNova/biz/JobsBiz.cs b/server/AyaNova/biz/JobsBiz.cs index 729383a3..1d9ca209 100644 --- a/server/AyaNova/biz/JobsBiz.cs +++ b/server/AyaNova/biz/JobsBiz.cs @@ -235,7 +235,7 @@ namespace AyaNova.Biz if (!KeepOnWorking()) return; //Check for and kill stuck report rendering engine processes - CoreJobReportRenderEngineProcessCleanup.DoWork(); + await CoreJobReportRenderEngineProcessCleanup.DoWork(); if (!KeepOnWorking()) return; diff --git a/server/AyaNova/biz/ReportBiz.cs b/server/AyaNova/biz/ReportBiz.cs index 081c9728..6d199e67 100644 --- a/server/AyaNova/biz/ReportBiz.cs +++ b/server/AyaNova/biz/ReportBiz.cs @@ -321,445 +321,445 @@ namespace AyaNova.Biz //RENDER // - public async Task RenderReport(DataListReportRequest reportRequest, DateTime renderTimeOutExpiry, string apiUrl) - { - var log = AyaNova.Util.ApplicationLogging.CreateLogger("RenderReport"); - log.LogDebug($"ReportBiz::RenderReport id {reportRequest.ReportId}, timeout @ {renderTimeOutExpiry.ToString()}"); - - - - //Customer User Report? - bool RequestIsCustomerWorkOrderReport = false; - if (reportRequest.ReportId == -100) - { - log.LogDebug("customer user report requested"); - //get the user and workorder data and set the actual report id or return null if not found - var woTags = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == reportRequest.SelectedRowIds[0]).Select(x => x.Tags).FirstOrDefaultAsync(); - if (woTags == null) return null; - var cr = await UserBiz.CustomerUserEffectiveRightsAsync(UserId, woTags); - if (cr.ThisWOEffectiveWOReportId == null) return null; - reportRequest.ReportId = (long)cr.ThisWOEffectiveWOReportId; - RequestIsCustomerWorkOrderReport = true; - } - - if (DateTime.UtcNow > renderTimeOutExpiry) - throw new ReportRenderTimeOutException(); - - //get report, vet security, see what we need before init in case of issue - log.LogDebug($"get report from db"); - var report = await ct.Report.FirstOrDefaultAsync(z => z.Id == reportRequest.ReportId); - if (report == null) - { - AddError(ApiErrorCode.NOT_FOUND, "id"); - return null; - } - - //If we get here via the /viewreport url in the client then there is no object type set so we need to set it here from the report - if (reportRequest.AType == AyaType.NoType) - { - reportRequest.AType = report.AType; - } - - - AuthorizationRoles effectiveRoles = CurrentUserRoles; - - - if (!RequestIsCustomerWorkOrderReport && !AyaNova.Api.ControllerHelpers.Authorized.HasReadFullRole(effectiveRoles, report.AType)) - { - log.LogDebug($"bail: user unauthorized"); - AddError(ApiErrorCode.NOT_AUTHORIZED, null, $"User not authorized for {report.AType} type object"); - return null; - } - - //Client meta data is required - if (reportRequest.ClientMeta == null) - { - log.LogDebug($"bail: ClientMeta parameter is missing"); - AddError(ApiErrorCode.VALIDATION_MISSING_PROPERTY, null, "ClientMeta parameter is missing and required to render report"); - return null; - } - - - - //includeWoItemDescendants? - reportRequest.IncludeWoItemDescendants = report.IncludeWoItemDescendants; - - //Get data - // #if (DEBUG) - // log.LogInformation($"DBG: ReportBiz::RenderReport GetReportData"); - // #endif - log.LogDebug("Getting report data now"); - var ReportData = await GetReportData(reportRequest, renderTimeOutExpiry, RequestIsCustomerWorkOrderReport); - //if GetReportData errored then will return null so need to return that as well here - if (ReportData == null) - { - log.LogDebug($"bail: ReportData == null"); - return null; - } - // #if (DEBUG) - // log.LogInformation($"DBG: ReportBiz::RenderReport got report data"); - // #endif - if (DateTime.UtcNow > renderTimeOutExpiry) - throw new ReportRenderTimeOutException(); - //initialization - log.LogDebug("Initializing report rendering system"); - bool AutoDownloadChromium = true; - if (string.IsNullOrWhiteSpace(ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PATH)) - { - log.LogDebug($"Using default Chromium browser (downloaded)"); - } - else - { - log.LogDebug($"Using user specified Chromium browser at {ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PATH}"); - AutoDownloadChromium = false; - } - - var ReportJSFolderPath = Path.Combine(ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH, "resource", "rpt"); - - var lo = new LaunchOptions { Headless = true }; - if (!AutoDownloadChromium) - { - lo.ExecutablePath = ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PATH; - /* - troubleshooting links: - https://developers.google.com/web/tools/puppeteer/troubleshooting - - These links might be worth looking at in future if diagnosing other issues: - https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-on-alpine - https://github.com/puppeteer/puppeteer/issues/1825 - const chromeFlags = [ - '--headless', - '--no-sandbox', - "--disable-gpu", - "--single-process", - "--no-zygote" - ] - */ - } - else - { - // #if (DEBUG) - // log.LogInformation($"DBG: ReportBiz::calling browserfetcher"); - // #endif - log.LogDebug($"Windows: Calling browserFetcher download async now:"); - await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultChromiumRevision); - } - - //Set Chromium args - //*** DANGER: --disable-dev-shm-usage will crash linux ayanova when it runs out of memory **** - //that was only suitable for dockerized scenario as it had an alt swap system - //var OriginalDefaultArgs = "--disable-dev-shm-usage --single-process --no-sandbox --disable-gpu --no-zygote "; - var DefaultArgs = "--headless --no-sandbox"; - if (!string.IsNullOrWhiteSpace(ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS)) - { - log.LogDebug($"AYANOVA_REPORT_RENDER_BROWSER_PARAMS will be used: {ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS}"); - lo.Args = new string[] { $"{ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS}" };// AYANOVA_REPORT_RENDER_BROWSER_PARAMS - } - else - { - log.LogDebug($"AYANOVA_REPORT_RENDER_BROWSER_PARAMS not set, using defaults '{DefaultArgs}'"); - lo.Args = new string[] { DefaultArgs }; - } - System.Text.StringBuilder PageLog = new System.Text.StringBuilder(); - - if (DateTime.UtcNow > renderTimeOutExpiry) - throw new ReportRenderTimeOutException(); - - //API DOCS http://www.puppeteersharp.com/api/index.html - log.LogDebug($"Launching headless Browser and new page now:"); - using (var browser = await Puppeteer.LaunchAsync(lo)) - using (var page = (await browser.PagesAsync())[0]) - // using (var page = await browser.NewPageAsync()) - { - //track this process for timeout purposes - ReportRenderManager.AddProcess(browser.Process.Id, renderTimeOutExpiry, log); - page.DefaultTimeout = 0;//infinite timeout as we are controlling how long the process can live for with the reportprocessmanager - try - { - //info and error logging - page.Console += async (sender, args) => - { - switch (args.Message.Type) - { - case ConsoleType.Error: - try - { - var errorArgs = await Task.WhenAll(args.Message.Args.Select(arg => arg.ExecutionContext.EvaluateFunctionAsync("(arg) => arg instanceof Error ? arg.message : arg", arg))); - PageLog.AppendLine($"ERROR: {args.Message.Text} args: [{string.Join(", ", errorArgs)}]"); - } - catch { } - break; - case ConsoleType.Warning: - PageLog.AppendLine($"WARNING: {args.Message.Text}"); - break; - default: - PageLog.AppendLine($"INFO: {args.Message.Text}"); - - - break; - } - }; - - log.LogDebug($"Preparing page: adding base reporting scripts to page"); - if (DateTime.UtcNow > renderTimeOutExpiry) - throw new ReportRenderTimeOutException(); - - //Add Handlebars JS for compiling and presenting - //https://handlebarsjs.com/ - await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-hb.js") }); - - //add Marked for markdown processing - //https://github.com/markedjs/marked - await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-md.js") }); - - //add DOM Purify for markdown template sanitization processing - //https://github.com/cure53/DOMPurify - await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-pf.js") }); - - //add Bar code library if our bar code helper is referenced - //https://github.com/metafloor/bwip-js - if (report.Template.Contains("ayBC ") || report.JsHelpers.Contains("ayBC ")) - await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-bc.js") }); - - //add stock helpers - await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-report.js") }); - - //execute to add to handlebars - await page.EvaluateExpressionAsync("ayRegisterHelpers();"); - if (DateTime.UtcNow > renderTimeOutExpiry) - throw new ReportRenderTimeOutException(); - log.LogDebug($"Preparing page: adding this report's scripts, style and templates to page"); - - //add report pre-render, helpers and style - if (string.IsNullOrWhiteSpace(report.JsPrerender)) - { - report.JsPrerender = "async function ayPrepareData(reportData){return reportData;}"; - } - await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsPrerender }); - - if (!string.IsNullOrWhiteSpace(report.JsHelpers)) - await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsHelpers }); - - if (!string.IsNullOrWhiteSpace(report.Style)) - await page.AddStyleTagAsync(new AddTagOptions() { Content = report.Style }); - - log.LogDebug($"Preparing page: adding Client meta data"); - - //Client meta data to JSON string - var clientMeta = reportRequest.ClientMeta.ToString(); - - log.LogDebug($"Preparing page: adding Server meta data"); - //Server meta data - var logo = await ct.Logo.AsNoTracking().SingleOrDefaultAsync(); - - var HasSmallLogo = "false"; - var HasMediumLogo = "false"; - var HasLargeLogo = "false"; - if (logo != null) - { - if (logo.Small != null) HasSmallLogo = "true"; - if (logo.Medium != null) HasMediumLogo = "true"; - if (logo.Large != null) HasLargeLogo = "true"; - } - var HasPostalAddress = !string.IsNullOrWhiteSpace(ServerGlobalBizSettings.Cache.PostAddress) ? "true" : "false"; - var HasStreetAddress = !string.IsNullOrWhiteSpace(ServerGlobalBizSettings.Cache.Address) ? "true" : "false"; - var serverMeta = $"{{ayApiUrl:`{apiUrl}`, HasSmallLogo:{HasSmallLogo}, HasMediumLogo:{HasMediumLogo}, HasLargeLogo:{HasLargeLogo},CompanyName: `{AyaNova.Core.License.ActiveKey.RegisteredTo}`,CompanyWebAddress:`{ServerGlobalBizSettings.Cache.WebAddress}`,CompanyEmailAddress:`{ServerGlobalBizSettings.Cache.EmailAddress}`,CompanyPhone1:`{ServerGlobalBizSettings.Cache.Phone1}`,CompanyPhone2:`{ServerGlobalBizSettings.Cache.Phone2}`,HasPostalAddress:{HasPostalAddress},CompanyPostAddress:`{ServerGlobalBizSettings.Cache.PostAddress}`,CompanyPostCity:`{ServerGlobalBizSettings.Cache.PostCity}`,CompanyPostRegion:`{ServerGlobalBizSettings.Cache.PostRegion}`,CompanyPostCountry:`{ServerGlobalBizSettings.Cache.PostCountry}`,CompanyPostCode:`{ServerGlobalBizSettings.Cache.PostCode}`,HasStreetAddress:{HasStreetAddress},CompanyAddress:`{ServerGlobalBizSettings.Cache.Address}`,CompanyCity:`{ServerGlobalBizSettings.Cache.City}`,CompanyRegion:`{ServerGlobalBizSettings.Cache.Region}`,CompanyCountry:`{ServerGlobalBizSettings.Cache.Country}`,CompanyLatitude:{ServerGlobalBizSettings.Cache.Latitude},CompanyLongitude:{ServerGlobalBizSettings.Cache.Longitude}}}"; - - log.LogDebug($"Preparing page: adding Report meta data"); - - //Custom fields definition for report usage - string CustomFieldsTemplate = "null"; - var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == report.AType.ToString()); - if (FormCustomization != null) - { - CustomFieldsTemplate = FormCustomization.Template; - } - - //Report meta data - var reportMeta = $"{{Id:{report.Id},Name:`{report.Name}`,Notes:`{report.Notes}`,AType:`{report.AType}`,CustomFieldsDefinition:{CustomFieldsTemplate},DataListKey:`{reportRequest.DataListKey}`,SelectedRowIds: `{string.Join(",", reportRequest.SelectedRowIds)}`}}"; - - - //duplicate meta data in report page wide variable for use by our internal functions - await page.AddScriptTagAsync(new AddTagOptions() { Content = $"var AYMETA={{ ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}" }); - - if (DateTime.UtcNow > renderTimeOutExpiry) - throw new ReportRenderTimeOutException(); - - - //#### DEBUGGING TOOL: view page contents - // var pagecontent = await page.GetContentAsync(); - - //prePareData / preRender - var ReportDataObject = $"{{ ayReportData:{ReportData}, ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}"; - - log.LogDebug($"PageLog before render:{PageLog.ToString()}"); - log.LogDebug($"Calling ayPreRender..."); - await page.WaitForExpressionAsync($"ayPreRender({ReportDataObject})"); - log.LogDebug($"ayPreRender completed"); - - if (DateTime.UtcNow > renderTimeOutExpiry) - throw new ReportRenderTimeOutException(); - //compile the template - log.LogDebug($"Calling Handlebars.compile..."); - - var compileScript = $"Handlebars.compile(`{report.Template}`)(PreParedReportDataObject);"; - if (DateTime.UtcNow > renderTimeOutExpiry) - throw new ReportRenderTimeOutException(); - var compiledHTML = await page.EvaluateExpressionAsync(compileScript); - if (DateTime.UtcNow > renderTimeOutExpiry) - throw new ReportRenderTimeOutException(); - //render report as HTML - log.LogDebug($"Setting page content to compiled HTML"); - - await page.SetContentAsync(compiledHTML); - - //add style (after page or it won't work) - if (!string.IsNullOrWhiteSpace(report.Style)) - { - log.LogDebug($"Adding report template Style CSS"); - await page.AddStyleTagAsync(new AddTagOptions { Content = report.Style }); - } - - string outputFileName = StringUtil.ReplaceLastOccurrence(FileUtil.NewRandomFileName, ".", "") + ".pdf"; - string outputFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputFileName); - - - //Set PDF options - //https://pptr.dev/#?product=Puppeteer&version=v5.3.0&show=api-pagepdfoptions - log.LogDebug($"Resolving PDF Options from report settings"); - - var PdfOptions = new PdfOptions() { }; - - PdfOptions.DisplayHeaderFooter = report.DisplayHeaderFooter; - if (report.DisplayHeaderFooter) - { - var ClientPDFDate = reportRequest.ClientMeta["PDFDate"].Value(); - var ClientPDFTime = reportRequest.ClientMeta["PDFTime"].Value(); - PdfOptions.HeaderTemplate = report.HeaderTemplate.Replace("PDFDate", ClientPDFDate).Replace("PDFTime", ClientPDFTime); - PdfOptions.FooterTemplate = report.FooterTemplate.Replace("PDFDate", ClientPDFDate).Replace("PDFTime", ClientPDFTime); - - } - - if (report.PaperFormat != ReportPaperFormat.NotSet) - { - switch (report.PaperFormat) - { - case ReportPaperFormat.A0: - PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A0; - break; - case ReportPaperFormat.A1: - PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A1; - break; - case ReportPaperFormat.A2: - PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A2; - break; - case ReportPaperFormat.A3: - PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A3; - break; - case ReportPaperFormat.A4: - PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A4; - break; - case ReportPaperFormat.A5: - PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A5; - break; - case ReportPaperFormat.A6: - PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A6; - break; - case ReportPaperFormat.Ledger: - PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.Ledger; - break; - case ReportPaperFormat.Legal: - PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.Legal; - break; - case ReportPaperFormat.Letter: - PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.Letter; - break; - case ReportPaperFormat.Tabloid: - PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.Tabloid; - break; - } - } - - PdfOptions.Landscape = report.Landscape; - if (!string.IsNullOrWhiteSpace(report.MarginOptionsBottom)) - PdfOptions.MarginOptions.Bottom = report.MarginOptionsBottom; - - if (!string.IsNullOrWhiteSpace(report.MarginOptionsLeft)) - PdfOptions.MarginOptions.Left = report.MarginOptionsLeft; - - if (!string.IsNullOrWhiteSpace(report.MarginOptionsRight)) - PdfOptions.MarginOptions.Right = report.MarginOptionsRight; - - if (!string.IsNullOrWhiteSpace(report.MarginOptionsTop)) - PdfOptions.MarginOptions.Top = report.MarginOptionsTop; - - //holding this back until figure it out - //it's not really a report property, but a print time / render property - //PdfOptions.PageRanges=report.PageRanges; - - PdfOptions.PreferCSSPageSize = report.PreferCSSPageSize; - PdfOptions.PrintBackground = report.PrintBackground; - //Defaults to 1. Scale amount must be between 0.1 and 2. - PdfOptions.Scale = report.Scale; - if (DateTime.UtcNow > renderTimeOutExpiry) - throw new ReportRenderTimeOutException(); - //render to pdf and return - log.LogDebug($"Calling render page contents to PDF"); - await page.PdfAsync(outputFullPath, PdfOptions);//###### TODO: SLOW NEEDS TIMEOUT HERE ONCE SUPPORTED, open bug: https://github.com/hardkoded/puppeteer-sharp/issues/1710 - - log.LogDebug($"Closing Page"); - await page.CloseAsync(); - log.LogDebug($"Closing Browser"); - await browser.CloseAsync(); - - log.LogDebug($"Render completed, returning results"); - return outputFileName; - } - catch (ReportRenderTimeOutException) - { - log.LogDebug("Caught ReportRendertimeOutException, re-throwing"); - throw; - } - catch (PuppeteerSharp.TargetClosedException) - { - log.LogDebug("Caught PuppeteerSharp.TargetClosedException throwing as ReportRendertimeOutException (timed out and closed from process sweeper)"); - //we closed it because the timeout hit and the CoreJobReportRenderEngineProcessCleanup job cleaned it out - //so return the error the client expects in this scenario - throw new ReportRenderTimeOutException(); - } - catch (Exception ex) - { - log.LogDebug(ex, $"Error during report rendering"); - //This is the error when a helper is used on the template but doesn't exist: - //Evaluation failed: d - //(it might also mean other things wrong with template) - if (PageLog.Length > 0) - { - log.LogInformation($"Exception caught while rendering report \"{report.Name}\", report Page console log:"); - log.LogInformation(PageLog.ToString()); - } - - // var v=await page.GetContentAsync();//for debugging purposes - throw; - } - finally - { - log.LogDebug($"reached finally block"); - if (!page.IsClosed) - { - log.LogDebug($"Page not closed in finally block, closing now"); - await page.CloseAsync(); - } - if (!browser.IsClosed) - { - log.LogDebug($"Browser not closed in finally block, closing now"); - await browser.CloseAsync(); - } - log.LogDebug($"Calling ReportRenderManager.RemoveProcess to stop tracking this process"); - ReportRenderManager.RemoveProcess(browser.Process.Id, log); - } - } - } + // public async Task RenderReport(DataListReportRequest reportRequest, DateTime renderTimeOutExpiry, string apiUrl) + // { + // var log = AyaNova.Util.ApplicationLogging.CreateLogger("RenderReport"); + // log.LogDebug($"ReportBiz::RenderReport id {reportRequest.ReportId}, timeout @ {renderTimeOutExpiry.ToString()}"); + + + + // //Customer User Report? + // bool RequestIsCustomerWorkOrderReport = false; + // if (reportRequest.ReportId == -100) + // { + // log.LogDebug("customer user report requested"); + // //get the user and workorder data and set the actual report id or return null if not found + // var woTags = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == reportRequest.SelectedRowIds[0]).Select(x => x.Tags).FirstOrDefaultAsync(); + // if (woTags == null) return null; + // var cr = await UserBiz.CustomerUserEffectiveRightsAsync(UserId, woTags); + // if (cr.ThisWOEffectiveWOReportId == null) return null; + // reportRequest.ReportId = (long)cr.ThisWOEffectiveWOReportId; + // RequestIsCustomerWorkOrderReport = true; + // } + + // if (DateTime.UtcNow > renderTimeOutExpiry) + // throw new ReportRenderTimeOutException(); + + // //get report, vet security, see what we need before init in case of issue + // log.LogDebug($"get report from db"); + // var report = await ct.Report.FirstOrDefaultAsync(z => z.Id == reportRequest.ReportId); + // if (report == null) + // { + // AddError(ApiErrorCode.NOT_FOUND, "id"); + // return null; + // } + + // //If we get here via the /viewreport url in the client then there is no object type set so we need to set it here from the report + // if (reportRequest.AType == AyaType.NoType) + // { + // reportRequest.AType = report.AType; + // } + + + // AuthorizationRoles effectiveRoles = CurrentUserRoles; + + + // if (!RequestIsCustomerWorkOrderReport && !AyaNova.Api.ControllerHelpers.Authorized.HasReadFullRole(effectiveRoles, report.AType)) + // { + // log.LogDebug($"bail: user unauthorized"); + // AddError(ApiErrorCode.NOT_AUTHORIZED, null, $"User not authorized for {report.AType} type object"); + // return null; + // } + + // //Client meta data is required + // if (reportRequest.ClientMeta == null) + // { + // log.LogDebug($"bail: ClientMeta parameter is missing"); + // AddError(ApiErrorCode.VALIDATION_MISSING_PROPERTY, null, "ClientMeta parameter is missing and required to render report"); + // return null; + // } + + + + // //includeWoItemDescendants? + // reportRequest.IncludeWoItemDescendants = report.IncludeWoItemDescendants; + + // //Get data + // // #if (DEBUG) + // // log.LogInformation($"DBG: ReportBiz::RenderReport GetReportData"); + // // #endif + // log.LogDebug("Getting report data now"); + // var ReportData = await GetReportData(reportRequest, renderTimeOutExpiry, RequestIsCustomerWorkOrderReport); + // //if GetReportData errored then will return null so need to return that as well here + // if (ReportData == null) + // { + // log.LogDebug($"bail: ReportData == null"); + // return null; + // } + // // #if (DEBUG) + // // log.LogInformation($"DBG: ReportBiz::RenderReport got report data"); + // // #endif + // if (DateTime.UtcNow > renderTimeOutExpiry) + // throw new ReportRenderTimeOutException(); + // //initialization + // log.LogDebug("Initializing report rendering system"); + // bool AutoDownloadChromium = true; + // if (string.IsNullOrWhiteSpace(ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PATH)) + // { + // log.LogDebug($"Using default Chromium browser (downloaded)"); + // } + // else + // { + // log.LogDebug($"Using user specified Chromium browser at {ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PATH}"); + // AutoDownloadChromium = false; + // } + + // var ReportJSFolderPath = Path.Combine(ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH, "resource", "rpt"); + + // var lo = new LaunchOptions { Headless = true }; + // if (!AutoDownloadChromium) + // { + // lo.ExecutablePath = ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PATH; + // /* + // troubleshooting links: + // https://developers.google.com/web/tools/puppeteer/troubleshooting + + // These links might be worth looking at in future if diagnosing other issues: + // https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-on-alpine + // https://github.com/puppeteer/puppeteer/issues/1825 + // const chromeFlags = [ + // '--headless', + // '--no-sandbox', + // "--disable-gpu", + // "--single-process", + // "--no-zygote" + // ] + // */ + // } + // else + // { + // // #if (DEBUG) + // // log.LogInformation($"DBG: ReportBiz::calling browserfetcher"); + // // #endif + // log.LogDebug($"Windows: Calling browserFetcher download async now:"); + // await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultChromiumRevision); + // } + + // //Set Chromium args + // //*** DANGER: --disable-dev-shm-usage will crash linux ayanova when it runs out of memory **** + // //that was only suitable for dockerized scenario as it had an alt swap system + // //var OriginalDefaultArgs = "--disable-dev-shm-usage --single-process --no-sandbox --disable-gpu --no-zygote "; + // var DefaultArgs = "--headless --no-sandbox"; + // if (!string.IsNullOrWhiteSpace(ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS)) + // { + // log.LogDebug($"AYANOVA_REPORT_RENDER_BROWSER_PARAMS will be used: {ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS}"); + // lo.Args = new string[] { $"{ServerBootConfig.AYANOVA_REPORT_RENDER_BROWSER_PARAMS}" };// AYANOVA_REPORT_RENDER_BROWSER_PARAMS + // } + // else + // { + // log.LogDebug($"AYANOVA_REPORT_RENDER_BROWSER_PARAMS not set, using defaults '{DefaultArgs}'"); + // lo.Args = new string[] { DefaultArgs }; + // } + // System.Text.StringBuilder PageLog = new System.Text.StringBuilder(); + + // if (DateTime.UtcNow > renderTimeOutExpiry) + // throw new ReportRenderTimeOutException(); + + // //API DOCS http://www.puppeteersharp.com/api/index.html + // log.LogDebug($"Launching headless Browser and new page now:"); + // using (var browser = await Puppeteer.LaunchAsync(lo)) + // using (var page = (await browser.PagesAsync())[0]) + // // using (var page = await browser.NewPageAsync()) + // { + // //track this process for timeout purposes + // ReportRenderManager.SetProcess(browser.Process.Id, renderTimeOutExpiry, log); + // page.DefaultTimeout = 0;//infinite timeout as we are controlling how long the process can live for with the reportprocessmanager + // try + // { + // //info and error logging + // page.Console += async (sender, args) => + // { + // switch (args.Message.Type) + // { + // case ConsoleType.Error: + // try + // { + // var errorArgs = await Task.WhenAll(args.Message.Args.Select(arg => arg.ExecutionContext.EvaluateFunctionAsync("(arg) => arg instanceof Error ? arg.message : arg", arg))); + // PageLog.AppendLine($"ERROR: {args.Message.Text} args: [{string.Join(", ", errorArgs)}]"); + // } + // catch { } + // break; + // case ConsoleType.Warning: + // PageLog.AppendLine($"WARNING: {args.Message.Text}"); + // break; + // default: + // PageLog.AppendLine($"INFO: {args.Message.Text}"); + + + // break; + // } + // }; + + // log.LogDebug($"Preparing page: adding base reporting scripts to page"); + // if (DateTime.UtcNow > renderTimeOutExpiry) + // throw new ReportRenderTimeOutException(); + + // //Add Handlebars JS for compiling and presenting + // //https://handlebarsjs.com/ + // await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-hb.js") }); + + // //add Marked for markdown processing + // //https://github.com/markedjs/marked + // await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-md.js") }); + + // //add DOM Purify for markdown template sanitization processing + // //https://github.com/cure53/DOMPurify + // await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-pf.js") }); + + // //add Bar code library if our bar code helper is referenced + // //https://github.com/metafloor/bwip-js + // if (report.Template.Contains("ayBC ") || report.JsHelpers.Contains("ayBC ")) + // await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-bc.js") }); + + // //add stock helpers + // await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "ay-report.js") }); + + // //execute to add to handlebars + // await page.EvaluateExpressionAsync("ayRegisterHelpers();"); + // if (DateTime.UtcNow > renderTimeOutExpiry) + // throw new ReportRenderTimeOutException(); + // log.LogDebug($"Preparing page: adding this report's scripts, style and templates to page"); + + // //add report pre-render, helpers and style + // if (string.IsNullOrWhiteSpace(report.JsPrerender)) + // { + // report.JsPrerender = "async function ayPrepareData(reportData){return reportData;}"; + // } + // await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsPrerender }); + + // if (!string.IsNullOrWhiteSpace(report.JsHelpers)) + // await page.AddScriptTagAsync(new AddTagOptions() { Content = report.JsHelpers }); + + // if (!string.IsNullOrWhiteSpace(report.Style)) + // await page.AddStyleTagAsync(new AddTagOptions() { Content = report.Style }); + + // log.LogDebug($"Preparing page: adding Client meta data"); + + // //Client meta data to JSON string + // var clientMeta = reportRequest.ClientMeta.ToString(); + + // log.LogDebug($"Preparing page: adding Server meta data"); + // //Server meta data + // var logo = await ct.Logo.AsNoTracking().SingleOrDefaultAsync(); + + // var HasSmallLogo = "false"; + // var HasMediumLogo = "false"; + // var HasLargeLogo = "false"; + // if (logo != null) + // { + // if (logo.Small != null) HasSmallLogo = "true"; + // if (logo.Medium != null) HasMediumLogo = "true"; + // if (logo.Large != null) HasLargeLogo = "true"; + // } + // var HasPostalAddress = !string.IsNullOrWhiteSpace(ServerGlobalBizSettings.Cache.PostAddress) ? "true" : "false"; + // var HasStreetAddress = !string.IsNullOrWhiteSpace(ServerGlobalBizSettings.Cache.Address) ? "true" : "false"; + // var serverMeta = $"{{ayApiUrl:`{apiUrl}`, HasSmallLogo:{HasSmallLogo}, HasMediumLogo:{HasMediumLogo}, HasLargeLogo:{HasLargeLogo},CompanyName: `{AyaNova.Core.License.ActiveKey.RegisteredTo}`,CompanyWebAddress:`{ServerGlobalBizSettings.Cache.WebAddress}`,CompanyEmailAddress:`{ServerGlobalBizSettings.Cache.EmailAddress}`,CompanyPhone1:`{ServerGlobalBizSettings.Cache.Phone1}`,CompanyPhone2:`{ServerGlobalBizSettings.Cache.Phone2}`,HasPostalAddress:{HasPostalAddress},CompanyPostAddress:`{ServerGlobalBizSettings.Cache.PostAddress}`,CompanyPostCity:`{ServerGlobalBizSettings.Cache.PostCity}`,CompanyPostRegion:`{ServerGlobalBizSettings.Cache.PostRegion}`,CompanyPostCountry:`{ServerGlobalBizSettings.Cache.PostCountry}`,CompanyPostCode:`{ServerGlobalBizSettings.Cache.PostCode}`,HasStreetAddress:{HasStreetAddress},CompanyAddress:`{ServerGlobalBizSettings.Cache.Address}`,CompanyCity:`{ServerGlobalBizSettings.Cache.City}`,CompanyRegion:`{ServerGlobalBizSettings.Cache.Region}`,CompanyCountry:`{ServerGlobalBizSettings.Cache.Country}`,CompanyLatitude:{ServerGlobalBizSettings.Cache.Latitude},CompanyLongitude:{ServerGlobalBizSettings.Cache.Longitude}}}"; + + // log.LogDebug($"Preparing page: adding Report meta data"); + + // //Custom fields definition for report usage + // string CustomFieldsTemplate = "null"; + // var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == report.AType.ToString()); + // if (FormCustomization != null) + // { + // CustomFieldsTemplate = FormCustomization.Template; + // } + + // //Report meta data + // var reportMeta = $"{{Id:{report.Id},Name:`{report.Name}`,Notes:`{report.Notes}`,AType:`{report.AType}`,CustomFieldsDefinition:{CustomFieldsTemplate},DataListKey:`{reportRequest.DataListKey}`,SelectedRowIds: `{string.Join(",", reportRequest.SelectedRowIds)}`}}"; + + + // //duplicate meta data in report page wide variable for use by our internal functions + // await page.AddScriptTagAsync(new AddTagOptions() { Content = $"var AYMETA={{ ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}" }); + + // if (DateTime.UtcNow > renderTimeOutExpiry) + // throw new ReportRenderTimeOutException(); + + + // //#### DEBUGGING TOOL: view page contents + // // var pagecontent = await page.GetContentAsync(); + + // //prePareData / preRender + // var ReportDataObject = $"{{ ayReportData:{ReportData}, ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}"; + + // log.LogDebug($"PageLog before render:{PageLog.ToString()}"); + // log.LogDebug($"Calling ayPreRender..."); + // await page.WaitForExpressionAsync($"ayPreRender({ReportDataObject})"); + // log.LogDebug($"ayPreRender completed"); + + // if (DateTime.UtcNow > renderTimeOutExpiry) + // throw new ReportRenderTimeOutException(); + // //compile the template + // log.LogDebug($"Calling Handlebars.compile..."); + + // var compileScript = $"Handlebars.compile(`{report.Template}`)(PreParedReportDataObject);"; + // if (DateTime.UtcNow > renderTimeOutExpiry) + // throw new ReportRenderTimeOutException(); + // var compiledHTML = await page.EvaluateExpressionAsync(compileScript); + // if (DateTime.UtcNow > renderTimeOutExpiry) + // throw new ReportRenderTimeOutException(); + // //render report as HTML + // log.LogDebug($"Setting page content to compiled HTML"); + + // await page.SetContentAsync(compiledHTML); + + // //add style (after page or it won't work) + // if (!string.IsNullOrWhiteSpace(report.Style)) + // { + // log.LogDebug($"Adding report template Style CSS"); + // await page.AddStyleTagAsync(new AddTagOptions { Content = report.Style }); + // } + + // string outputFileName = StringUtil.ReplaceLastOccurrence(FileUtil.NewRandomFileName, ".", "") + ".pdf"; + // string outputFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputFileName); + + + // //Set PDF options + // //https://pptr.dev/#?product=Puppeteer&version=v5.3.0&show=api-pagepdfoptions + // log.LogDebug($"Resolving PDF Options from report settings"); + + // var PdfOptions = new PdfOptions() { }; + + // PdfOptions.DisplayHeaderFooter = report.DisplayHeaderFooter; + // if (report.DisplayHeaderFooter) + // { + // var ClientPDFDate = reportRequest.ClientMeta["PDFDate"].Value(); + // var ClientPDFTime = reportRequest.ClientMeta["PDFTime"].Value(); + // PdfOptions.HeaderTemplate = report.HeaderTemplate.Replace("PDFDate", ClientPDFDate).Replace("PDFTime", ClientPDFTime); + // PdfOptions.FooterTemplate = report.FooterTemplate.Replace("PDFDate", ClientPDFDate).Replace("PDFTime", ClientPDFTime); + + // } + + // if (report.PaperFormat != ReportPaperFormat.NotSet) + // { + // switch (report.PaperFormat) + // { + // case ReportPaperFormat.A0: + // PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A0; + // break; + // case ReportPaperFormat.A1: + // PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A1; + // break; + // case ReportPaperFormat.A2: + // PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A2; + // break; + // case ReportPaperFormat.A3: + // PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A3; + // break; + // case ReportPaperFormat.A4: + // PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A4; + // break; + // case ReportPaperFormat.A5: + // PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A5; + // break; + // case ReportPaperFormat.A6: + // PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.A6; + // break; + // case ReportPaperFormat.Ledger: + // PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.Ledger; + // break; + // case ReportPaperFormat.Legal: + // PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.Legal; + // break; + // case ReportPaperFormat.Letter: + // PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.Letter; + // break; + // case ReportPaperFormat.Tabloid: + // PdfOptions.Format = PuppeteerSharp.Media.PaperFormat.Tabloid; + // break; + // } + // } + + // PdfOptions.Landscape = report.Landscape; + // if (!string.IsNullOrWhiteSpace(report.MarginOptionsBottom)) + // PdfOptions.MarginOptions.Bottom = report.MarginOptionsBottom; + + // if (!string.IsNullOrWhiteSpace(report.MarginOptionsLeft)) + // PdfOptions.MarginOptions.Left = report.MarginOptionsLeft; + + // if (!string.IsNullOrWhiteSpace(report.MarginOptionsRight)) + // PdfOptions.MarginOptions.Right = report.MarginOptionsRight; + + // if (!string.IsNullOrWhiteSpace(report.MarginOptionsTop)) + // PdfOptions.MarginOptions.Top = report.MarginOptionsTop; + + // //holding this back until figure it out + // //it's not really a report property, but a print time / render property + // //PdfOptions.PageRanges=report.PageRanges; + + // PdfOptions.PreferCSSPageSize = report.PreferCSSPageSize; + // PdfOptions.PrintBackground = report.PrintBackground; + // //Defaults to 1. Scale amount must be between 0.1 and 2. + // PdfOptions.Scale = report.Scale; + // if (DateTime.UtcNow > renderTimeOutExpiry) + // throw new ReportRenderTimeOutException(); + // //render to pdf and return + // log.LogDebug($"Calling render page contents to PDF"); + // await page.PdfAsync(outputFullPath, PdfOptions);//###### TODO: SLOW NEEDS TIMEOUT HERE ONCE SUPPORTED, open bug: https://github.com/hardkoded/puppeteer-sharp/issues/1710 + + // log.LogDebug($"Closing Page"); + // await page.CloseAsync(); + // log.LogDebug($"Closing Browser"); + // await browser.CloseAsync(); + + // log.LogDebug($"Render completed, returning results"); + // return outputFileName; + // } + // catch (ReportRenderTimeOutException) + // { + // log.LogDebug("Caught ReportRendertimeOutException, re-throwing"); + // throw; + // } + // catch (PuppeteerSharp.TargetClosedException) + // { + // log.LogDebug("Caught PuppeteerSharp.TargetClosedException throwing as ReportRendertimeOutException (timed out and closed from process sweeper)"); + // //we closed it because the timeout hit and the CoreJobReportRenderEngineProcessCleanup job cleaned it out + // //so return the error the client expects in this scenario + // throw new ReportRenderTimeOutException(); + // } + // catch (Exception ex) + // { + // log.LogDebug(ex, $"Error during report rendering"); + // //This is the error when a helper is used on the template but doesn't exist: + // //Evaluation failed: d + // //(it might also mean other things wrong with template) + // if (PageLog.Length > 0) + // { + // log.LogInformation($"Exception caught while rendering report \"{report.Name}\", report Page console log:"); + // log.LogInformation(PageLog.ToString()); + // } + + // // var v=await page.GetContentAsync();//for debugging purposes + // throw; + // } + // finally + // { + // log.LogDebug($"reached finally block"); + // if (!page.IsClosed) + // { + // log.LogDebug($"Page not closed in finally block, closing now"); + // await page.CloseAsync(); + // } + // if (!browser.IsClosed) + // { + // log.LogDebug($"Browser not closed in finally block, closing now"); + // await browser.CloseAsync(); + // } + // log.LogDebug($"Calling ReportRenderManager.RemoveProcess to stop tracking this process"); + // ReportRenderManager.RemoveProcess(browser.Process.Id, log); + // } + // } + // } @@ -863,9 +863,12 @@ namespace AyaNova.Biz public async Task DoRenderJob(OpsJob job) { + var log = AyaNova.Util.ApplicationLogging.CreateLogger("ReportBiz::DoRenderJob"); await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running); - // await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.JobType}"); + var renderTimeOutExpiry = DateTime.UtcNow.AddMinutes(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT); + ReportRenderManager.AddJob(job.GId, renderTimeOutExpiry, log); + // await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.JobType}"); //rehydrate job objects log.LogDebug($"Start; rehydrate job {job.Name}"); @@ -874,7 +877,7 @@ namespace AyaNova.Biz var RequestIsCustomerWorkOrderReport = jobData["requestIsCustomerWorkOrderReport"].ToObject(); var apiUrl = jobData["apiUrl"].ToObject(); var userName = jobData["userName"].ToObject(); - var renderTimeOutExpiry = DateTime.UtcNow.AddMinutes(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT); + var report = await ct.Report.FirstOrDefaultAsync(z => z.Id == reportRequest.ReportId); @@ -885,6 +888,11 @@ namespace AyaNova.Biz return; } + if (!ReportRenderManager.KeepGoing(job.GId)) + { + return; + } + //Get data log.LogDebug("Getting report data now"); var ReportData = await GetReportData(reportRequest, renderTimeOutExpiry, RequestIsCustomerWorkOrderReport); @@ -895,9 +903,12 @@ namespace AyaNova.Biz await JobsBiz.LogJobAsync(job.GId, $"rendererror:error,\"{this.GetErrorsAsString()}\"");//?? circle back on this one await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed); } - // #if (DEBUG) - // log.LogInformation($"DBG: ReportBiz::RenderReport got report data"); - // #endif + + if (!ReportRenderManager.KeepGoing(job.GId)) + { + return; + } + if (DateTime.UtcNow > renderTimeOutExpiry) { await HandleTimeOut(job, log, reportRequest, userName); @@ -947,6 +958,7 @@ namespace AyaNova.Biz await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultChromiumRevision); } + //Set Chromium args //*** DANGER: --disable-dev-shm-usage will crash linux ayanova when it runs out of memory **** //that was only suitable for dockerized scenario as it had an alt swap system @@ -966,6 +978,10 @@ namespace AyaNova.Biz if (DateTime.UtcNow > renderTimeOutExpiry) throw new ReportRenderTimeOutException(); + if (!ReportRenderManager.KeepGoing(job.GId)) + { + return; + } //API DOCS http://www.puppeteersharp.com/api/index.html log.LogDebug($"Launching headless Browser and new page now:"); @@ -974,10 +990,11 @@ namespace AyaNova.Biz // using (var page = await browser.NewPageAsync()) { //track this process for timeout purposes - ReportRenderManager.AddProcess(browser.Process.Id, renderTimeOutExpiry, log); + ReportRenderManager.SetProcess(job.GId, browser.Process.Id, log); page.DefaultTimeout = 0;//infinite timeout as we are controlling how long the process can live for with the reportprocessmanager try { + //info and error logging page.Console += async (sender, args) => { @@ -1005,6 +1022,10 @@ namespace AyaNova.Biz log.LogDebug($"Preparing page: adding base reporting scripts to page"); if (DateTime.UtcNow > renderTimeOutExpiry) throw new ReportRenderTimeOutException(); + if (!ReportRenderManager.KeepGoing(job.GId)) + { + return; + } //Add Handlebars JS for compiling and presenting //https://handlebarsjs.com/ @@ -1030,6 +1051,10 @@ namespace AyaNova.Biz await page.EvaluateExpressionAsync("ayRegisterHelpers();"); if (DateTime.UtcNow > renderTimeOutExpiry) throw new ReportRenderTimeOutException(); + if (!ReportRenderManager.KeepGoing(job.GId)) + { + return; + } log.LogDebug($"Preparing page: adding this report's scripts, style and templates to page"); //add report pre-render, helpers and style @@ -1086,7 +1111,10 @@ namespace AyaNova.Biz if (DateTime.UtcNow > renderTimeOutExpiry) throw new ReportRenderTimeOutException(); - + if (!ReportRenderManager.KeepGoing(job.GId)) + { + return; + } //#### DEBUGGING TOOL: view page contents // var pagecontent = await page.GetContentAsync(); @@ -1101,15 +1129,25 @@ namespace AyaNova.Biz if (DateTime.UtcNow > renderTimeOutExpiry) throw new ReportRenderTimeOutException(); + if (!ReportRenderManager.KeepGoing(job.GId)) + { + return; + } //compile the template log.LogDebug($"Calling Handlebars.compile..."); var compileScript = $"Handlebars.compile(`{report.Template}`)(PreParedReportDataObject);"; if (DateTime.UtcNow > renderTimeOutExpiry) throw new ReportRenderTimeOutException(); + if(!ReportRenderManager.KeepGoing(job.GId)){ + return; + } var compiledHTML = await page.EvaluateExpressionAsync(compileScript); if (DateTime.UtcNow > renderTimeOutExpiry) throw new ReportRenderTimeOutException(); + if(!ReportRenderManager.KeepGoing(job.GId)){ + return; + } //render report as HTML log.LogDebug($"Setting page content to compiled HTML"); @@ -1205,6 +1243,9 @@ namespace AyaNova.Biz PdfOptions.Scale = report.Scale; if (DateTime.UtcNow > renderTimeOutExpiry) throw new ReportRenderTimeOutException(); + if(!ReportRenderManager.KeepGoing(job.GId)){ + return; + } //render to pdf and return log.LogDebug($"Calling render page contents to PDF"); await page.PdfAsync(outputFullPath, PdfOptions);//###### TODO: SLOW NEEDS TIMEOUT HERE ONCE SUPPORTED, open bug: https://github.com/hardkoded/puppeteer-sharp/issues/1710 @@ -1264,8 +1305,8 @@ namespace AyaNova.Biz log.LogDebug($"Browser not closed in finally block, closing now"); await browser.CloseAsync(); } - log.LogDebug($"Calling ReportRenderManager.RemoveProcess to stop tracking this process"); - ReportRenderManager.RemoveProcess(browser.Process.Id, log); + log.LogDebug($"Calling ReportRenderManager.RemoveJob to stop tracking this job/process"); + await ReportRenderManager.RemoveJob(job.GId, log); } } @@ -1278,7 +1319,10 @@ namespace AyaNova.Biz } - + public async Task CancelJob(Guid jobId) + { + await ReportRenderManager.RemoveJob(jobId, AyaNova.Util.ApplicationLogging.CreateLogger("ReportBiz::CancelJob")); + } //////////////////////////////////////////////////////////////////////////////////////////////// //JOB / OPERATIONS // diff --git a/server/AyaNova/generator/CoreJobReportRenderEngineProcessCleanup.cs b/server/AyaNova/generator/CoreJobReportRenderEngineProcessCleanup.cs index ab9fa85c..611bf0ce 100644 --- a/server/AyaNova/generator/CoreJobReportRenderEngineProcessCleanup.cs +++ b/server/AyaNova/generator/CoreJobReportRenderEngineProcessCleanup.cs @@ -1,7 +1,7 @@ using System; using Microsoft.Extensions.Logging; using AyaNova.Util; - +using System.Threading.Tasks; namespace AyaNova.Biz { @@ -19,12 +19,12 @@ namespace AyaNova.Biz //////////////////////////////////////////////////////////////////////////////////////////////// // // - public static void DoWork() + public static async Task DoWork() { if (DateUtil.IsAfterDuration(_lastRun, tsRunEvery)) { log.LogDebug("Checking for expired processes"); - Util.ReportRenderManager.KillExpiredRenders(log); + await Util.ReportRenderManager.KillExpiredRenders(log); //FileUtil.CleanTemporaryFilesFolder(new TimeSpan(0,5,0));//erase any files found to be older than 5 minutes var now = DateTime.UtcNow; _lastRun = now; diff --git a/server/AyaNova/util/ReportProcessManager.cs b/server/AyaNova/util/ReportProcessManager.cs index af07fd22..3562680c 100644 --- a/server/AyaNova/util/ReportProcessManager.cs +++ b/server/AyaNova/util/ReportProcessManager.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Concurrent; using System.Diagnostics; +using System.Threading.Tasks; +using AyaNova.Biz; using Microsoft.Extensions.Logging; namespace AyaNova.Util @@ -9,7 +11,7 @@ namespace AyaNova.Util //Track processes and kill any that go past their expiry date internal static class ReportRenderManager { - + internal static ConcurrentBag _baginstances; static ReportRenderManager() @@ -21,16 +23,18 @@ namespace AyaNova.Util { internal int ReporterProcessId { get; set; } internal DateTime Expires { get; set; } + internal Guid JobId { get; set; } - internal ReportRenderInstanceInfo(int processId, DateTime expires) + internal ReportRenderInstanceInfo(Guid jobId, DateTime expires) { - ReporterProcessId = processId; + JobId = jobId; Expires = expires; + ReporterProcessId = -1; } } - internal static void KillExpiredRenders(ILogger log) + internal static async Task KillExpiredRenders(ILogger log) { log.LogDebug("Clear potential expired render jobs check"); //check for expired and remove @@ -40,35 +44,39 @@ namespace AyaNova.Util { if (i.Expires < dtNow) { - - log.LogDebug($"attempting close of expired process {i.ReporterProcessId}"); - - ForceCloseProcess(i, log); + log.LogDebug($"attempting close of expired process {i.ReporterProcessId} for job {i.JobId}"); + await CloseRenderProcess(i, log); } } } - internal static bool ForceCloseProcess(ReportRenderInstanceInfo instance, ILogger log) + internal static async Task CloseRenderProcess(ReportRenderInstanceInfo instance, ILogger log) { log.LogDebug($"ForceCloseProcess on report render instance id {instance.ReporterProcessId} expired {instance.Expires.ToString()} utc"); try { - var p = Process.GetProcessById(instance.ReporterProcessId); + //either way, clear the job so the client gets informed + await JobsBiz.LogJobAsync(instance.JobId, $"rendererror:timeout,{ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT}");//parseable for print client + await JobsBiz.UpdateJobStatusAsync(instance.JobId, JobStatus.Failed); - if (p != null) + if (instance.ReporterProcessId != -1)//if a job doesn't have a process id yet it will be -1 { - //we have an existing process - //try to kill it - p.Kill(true); - if (p.HasExited == false) + var p = Process.GetProcessById(instance.ReporterProcessId); + if (p != null) { - log.LogWarning($"Expired report render instance id {instance.ReporterProcessId} could not be force closed"); - return false;//can't kill it so can't free up a slot + //we have an existing process + //try to kill it + p.Kill(true); + if (p.HasExited == false) + { + log.LogWarning($"Expired report render instance id {instance.ReporterProcessId} could not be force closed"); + return false;//can't kill it so can't free up a slot + } } } //remove it from the list, it's either gone or killed at this point //this would not be unexpected since it will normally just close on it's own - //at the finally in render report + //at the finally block in render report _baginstances.TryTake(out instance); return true;//process that was there is now not there so while not perfect system we will consider it free @@ -83,21 +91,61 @@ namespace AyaNova.Util } } - internal static void AddProcess(int processId, DateTime expires, ILogger log) - { - log.LogDebug($"AddProcess - {processId} to the collection"); - _baginstances.Add(new ReportRenderInstanceInfo(processId, expires)); - log.LogInformation($"AddProcess - there are currently {_baginstances.Count} instances in the collection"); + internal static void AddJob(Guid jobId, DateTime expires, ILogger log) + { + log.LogDebug($"AddJob - {jobId} to the collection"); + _baginstances.Add(new ReportRenderInstanceInfo(jobId, expires)); + + log.LogInformation($"AddJob - there are currently {_baginstances.Count} instances in the collection"); } - internal static void RemoveProcess(int processId, ILogger log) + + internal static void SetProcess(Guid jobId, int processId, ILogger log) + { + log.LogDebug($"SetProcess - setting {jobId} to render process id {processId}"); + foreach (var i in _baginstances) + { + if (i.JobId == jobId) + { + i.ReporterProcessId = processId; + break; + } + } + } + + internal static async Task RemoveJob(Guid jobId, ILogger log) + { + foreach (var i in _baginstances) + { + if (i.JobId == jobId) + { + await CloseRenderProcess(i, log); + break; + } + } + } + + + internal static bool KeepGoing(Guid jobId) + { + foreach (var i in _baginstances) + { + if (i.JobId == jobId) + { + return true; + } + } + return false; + } + + internal static async Task RemoveProcess(int processId, ILogger log) { foreach (var i in _baginstances) { if (i.ReporterProcessId == processId) { - ForceCloseProcess(i, log); + await CloseRenderProcess(i, log); break; } }