diff --git a/server/AyaNova/Controllers/ReportController.cs b/server/AyaNova/Controllers/ReportController.cs index 58c268b9..4c28893d 100644 --- a/server/AyaNova/Controllers/ReportController.cs +++ b/server/AyaNova/Controllers/ReportController.cs @@ -241,6 +241,50 @@ namespace AyaNova.Api.Controllers } + + /// + /// Start Render Report job + /// + /// report id and object id values for object type specified in report template + /// From route path + /// Job Id + [HttpPost("render-job")] + public async Task RequestRenderReport([FromBody] DataListReportRequest reportRequest, ApiVersion apiVersion) + { + 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)); + + var httpConnectionFeature = HttpContext.Features.Get(); + var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/v8/"; + try + { + var result = await biz.RequestRenderReport(reportRequest, DateTime.UtcNow.AddMinutes(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT), API_URL,UserNameFromContext.Name(HttpContext.Items)); + 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)); + } + } + + /// /// Download a rendered report /// diff --git a/server/AyaNova/biz/JobType.cs b/server/AyaNova/biz/JobType.cs index 4eefb637..7d286b07 100644 --- a/server/AyaNova/biz/JobType.cs +++ b/server/AyaNova/biz/JobType.cs @@ -13,7 +13,9 @@ namespace AyaNova.Biz SeedTestData = 4, BatchCoreObjectOperation = 5, Backup = 6, - AttachmentMaintenance = 7 + AttachmentMaintenance = 7, + RenderReport=8, + ExportData=9 } diff --git a/server/AyaNova/biz/ReportBiz.cs b/server/AyaNova/biz/ReportBiz.cs index 49329d8f..b2ca3376 100644 --- a/server/AyaNova/biz/ReportBiz.cs +++ b/server/AyaNova/biz/ReportBiz.cs @@ -449,7 +449,7 @@ namespace AyaNova.Biz //*** 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"; + 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}"); @@ -469,7 +469,7 @@ namespace AyaNova.Biz 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()) + // using (var page = await browser.NewPageAsync()) { //track this process for timeout purposes ReportRenderManager.AddProcess(browser.Process.Id, renderTimeOutExpiry, log); @@ -591,8 +591,8 @@ namespace AyaNova.Biz //prePareData / preRender var ReportDataObject = $"{{ ayReportData:{ReportData}, ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}"; - - log.LogDebug($"PageLog before render:{PageLog.ToString()}"); + + log.LogDebug($"PageLog before render:{PageLog.ToString()}"); log.LogDebug($"Calling ayPreRender..."); await page.WaitForExpressionAsync($"ayPreRender({ReportDataObject})"); log.LogDebug($"ayPreRender completed"); @@ -762,14 +762,542 @@ namespace AyaNova.Biz } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //RENDER + // + + public async Task RequestRenderReport(DataListReportRequest reportRequest, DateTime renderTimeOutExpiry, string apiUrl, string userName) + { + var log = AyaNova.Util.ApplicationLogging.CreateLogger("ReportBiz::RequestRenderReport"); + log.LogDebug($"report 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; + } + + //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; + + + + + //Do we need to rehydrate the ID List from a DataList? + if (reportRequest.SelectedRowIds.Length == 0) + reportRequest.SelectedRowIds = await DataListSelectedProcessingOptions.RehydrateIdList(reportRequest, ct, effectiveRoles, log, UserId, UserTranslationId); + + + //todo: put in job add job handler to do the remainder of this request + + + var JobName = $"LT:Report id: \"{reportRequest.ReportId}\" LT:{reportRequest.AType} ({reportRequest.SelectedRowIds.LongLength}) LT:User {userName}"; + JObject o = JObject.FromObject(new + { + reportRequest = reportRequest, + requestIsCustomerWorkOrderReport = RequestIsCustomerWorkOrderReport, + apiUrl = apiUrl, + userName = userName + }); + + OpsJob j = new OpsJob(); + j.Name = JobName; + j.AType = AyaType.Report; + j.JobType = JobType.RenderReport; + j.SubType = JobSubType.NotSet; + j.Exclusive = false; + j.JobInfo = o.ToString(); + await JobsBiz.AddJobAsync(j); + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, 0, AyaType.ServerJob, AyaEvent.Created, JobName), ct); + return j.GId; + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + } + + + 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}"); + + //rehydrate job objects + log.LogDebug($"Rehydrating job {job.Name}"); + JObject jobData = JObject.Parse(job.JobInfo); + var reportRequest = jobData["reportRequest"].ToObject(); + 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); + if (report == null) + { + await JobsBiz.LogJobAsync(job.GId, $"LT:ErrorAPI2010 LT:Report({reportRequest.ReportId})"); + await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed); + return; + } + + //Get data + 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"); + await JobsBiz.LogJobAsync(job.GId, $"{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 (DateTime.UtcNow > renderTimeOutExpiry) + { + await HandleTimeOut(job, log, reportRequest, userName); + return; + } + //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 successfully, output filename is: {outputFileName}, logging to job for client"); + await JobsBiz.LogJobAsync(job.GId, $"ok:{outputFileName}"); + await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed); + return; + } + catch (ReportRenderTimeOutException) + { + await HandleTimeOut(job, log, reportRequest, userName); + return; + } + 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 + await HandleTimeOut(job, log, reportRequest, userName); + return; + } + 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()); + await JobsBiz.LogJobAsync(job.GId, $"rendererror:\"{PageLog.ToString()}\"");//parseable for print client + } + + await JobsBiz.LogJobAsync(job.GId, $"exception:\"{ExceptionUtil.ExtractAllExceptionMessages(ex)}\"");//parseable for print client + await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed); + + // var v=await page.GetContentAsync();//for debugging purposes + return; + } + 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); + } + } + + static async Task HandleTimeOut(OpsJob job, ILogger log, DataListReportRequest reportRequest, string userName) + { + 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:{userName}"); + await JobsBiz.LogJobAsync(job.GId, $"timeout,{ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT}");//parseable for print client + await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed); + } + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// //JOB / OPERATIONS // - + public async Task HandleJobAsync(OpsJob job) + { + switch (job.JobType) + { + case JobType.RenderReport: + await DoRenderJob(job); + break; + default: + throw new System.ArgumentOutOfRangeException($"ReportBiz.HandleJob-> Invalid job type{job.JobType.ToString()}"); + } + } //Other job handlers here... + ///////////////////////////////////////////////////////////////////// }//eoc