using System.Threading.Tasks; using System.Linq; using System.IO; using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Sockeye.Util; using Sockeye.Api.ControllerHelpers; using Sockeye.Models; using EnumsNET; using PuppeteerSharp; using Newtonsoft.Json.Linq; using System; namespace Sockeye.Biz { internal class ReportBiz : BizObject, IJobObject, ISearchAbleObject { internal ReportBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) { ct = dbcontext; UserId = currentUserId; UserTranslationId = userTranslationId; CurrentUserRoles = UserRoles; BizType = SockType.Report; } internal static ReportBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) { if (httpContext != null) return new ReportBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); else return new ReportBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin); } //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ExistsAsync(long id) { return await ct.Report.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task CreateAsync(Report newObject) { await ValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.Report.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct); await SearchIndexAsync(newObject, true); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// //IMPORT // internal async Task ImportAsync(JObject o, bool skipIfAlreadyPresent = false) { //Report newObject = new Report(); var newObject = o.ToObject(); var proposedName = (string)o["Name"]; string newUniqueName = proposedName; bool NotUnique = true; long l = 1; do { NotUnique = await ct.Report.AnyAsync(z => z.Name == newUniqueName); if (NotUnique) { if (!skipIfAlreadyPresent) newUniqueName = Util.StringUtil.UniqueNameBuilder(proposedName, l++, 255); else { return true; } } } while (NotUnique); newObject.Name = newUniqueName; await ValidateAsync(newObject, null); if (HasErrors) return false; await ct.Report.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct); if (skipIfAlreadyPresent) { ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger(); log.LogInformation($"Stock report '{proposedName}' imported"); } return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //GET // internal async Task GetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.Report.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task PutAsync(Report putObject) { var dbObject = await GetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await ValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await ExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, SockEvent.Modified), ct); await SearchIndexAsync(putObject, false); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task DeleteAsync(long id) { using (var transaction = await ct.Database.BeginTransactionAsync()) { var dbObject = await GetAsync(id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND); return false; } await ValidateCanDelete(dbObject); if (HasErrors) return false; { var IDList = await ct.Review.AsNoTracking().Where(x => x.SockType == SockType.Report && x.ObjectId == id).Select(x => x.Id).ToListAsync(); if (IDList.Count() > 0) { ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles); foreach (long ItemId in IDList) if (!await b.DeleteAsync(ItemId, transaction)) { AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}"); return false; } } } ct.Report.Remove(dbObject); await ct.SaveChangesAsync(); await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Name, ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct); await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); await transaction.CommitAsync(); return true; } } //////////////////////////////////////////////////////////////////////////////////////////////// //GET LIST // internal async Task> GetReportListAsync(SockType aType) { var rpts = await ct.Report.AsNoTracking().Where(z => z.SockType == aType && z.Active == true).Select(z => new { id = z.Id, name = z.Name, roles = z.Roles }).OrderBy(z => z.name).ToListAsync(); var ret = new List(); foreach (var item in rpts) { if (CurrentUserRoles.HasAnyFlags(item.roles)) { ret.Add(new NameIdItem() { Name = item.name, Id = item.id }); } } return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //SEARCH // private async Task SearchIndexAsync(Report obj, bool isNew) { var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType); DigestSearchText(obj, SearchParams); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task GetSearchResultSummary(long id, SockType specificType) { var obj = await GetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); DigestSearchText(obj, SearchParams); return SearchParams; } public void DigestSearchText(Report obj, Search.SearchIndexProcessObjectParameters searchParams) { if (obj != null) searchParams.AddText(obj.Notes) .AddText(obj.Name) .AddText(obj.Template) .AddText(obj.Style) .AddText(obj.JsPrerender) .AddText(obj.JsHelpers) .AddText(obj.HeaderTemplate) .AddText(obj.FooterTemplate); } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task ValidateAsync(Report proposedObj, Report currentObj) { bool isNew = currentObj == null; //Name required if (string.IsNullOrWhiteSpace(proposedObj.Name)) AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name"); //If name is otherwise OK, check that name is unique if (!PropertyHasErrors("Name")) { //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false //NOTE: unlike other objects reports can have the same name as long as the type differs if (await ct.Report.AnyAsync(z => z.Name == proposedObj.Name && z.SockType == proposedObj.SockType && z.Id != proposedObj.Id)) { AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name"); } } } private async Task ValidateCanDelete(Report inObj) { //Referential integrity error if (await ct.NotifySubscription.AnyAsync(z => z.LinkReportId == inObj.Id) == true) { //Note: errorbox will ensure it appears in the general errror box and not field specific //the translation key is to indicate what the linked object is that is causing the error AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("NotifySubscription")); } } //////////////////////////////////////////////////////////////////////////////////////////////// //REPORT DATA //Data fetched to return to report render or for designer for Client report design usage public async Task GetReportDataForReportDesigner(DataListSelectedRequest selectedRequest) { var log = Sockeye.Util.ApplicationLogging.CreateLogger("ReportBiz::GetReportDataForReportDesigner"); AuthorizationRoles effectiveRoles = CurrentUserRoles; if (selectedRequest.SockType == SockType.NoType) { AddError(ApiErrorCode.VALIDATION_REQUIRED, null, $"SockType is required"); return null; } //Do we need to rehydrate the ID List from a DataList? if (selectedRequest.SelectedRowIds.Length == 0) selectedRequest.SelectedRowIds = await DataListSelectedProcessingOptions.RehydrateIdList(selectedRequest, ct, effectiveRoles, log, UserId, UserTranslationId); log.LogDebug($"Instantiating biz object handler for {selectedRequest.SockType}"); var biz = BizObjectFactory.GetBizObject(selectedRequest.SockType, ct, UserId, CurrentUserRoles, UserTranslationId); log.LogDebug($"Fetching data for {selectedRequest.SelectedRowIds.Length} {selectedRequest.SockType} items"); return await ((IReportAbleObject)biz).GetReportData(selectedRequest, Guid.Empty);//Guid.empty signifies it's not a job calling it } public async Task GetReportData(DataListSelectedRequest selectedRequest, Guid jobId, bool requestIsCustomerWorkOrderReport = false) { var log = Sockeye.Util.ApplicationLogging.CreateLogger("ReportBiz::GetReportData"); AuthorizationRoles effectiveRoles = CurrentUserRoles; if (selectedRequest.SockType == SockType.NoType) { AddError(ApiErrorCode.VALIDATION_REQUIRED, null, $"SockType is required"); return null; } if (!requestIsCustomerWorkOrderReport && !Sockeye.Api.ControllerHelpers.Authorized.HasReadFullRole(effectiveRoles, selectedRequest.SockType)) { AddError(ApiErrorCode.NOT_AUTHORIZED, null, $"User not authorized for {selectedRequest.SockType} type object"); return null; } //Do we need to rehydrate the ID List from a DataList? if (selectedRequest.SelectedRowIds.Length == 0) selectedRequest.SelectedRowIds = await DataListSelectedProcessingOptions.RehydrateIdList(selectedRequest, ct, effectiveRoles, log, UserId, UserTranslationId); if (!ReportRenderManager.KeepGoing(jobId)) return null; log.LogDebug($"Instantiating biz object handler for {selectedRequest.SockType}"); var biz = BizObjectFactory.GetBizObject(selectedRequest.SockType, ct, UserId, CurrentUserRoles, UserTranslationId); log.LogDebug($"Fetching data for {selectedRequest.SelectedRowIds.Length} {selectedRequest.SockType} items"); return await ((IReportAbleObject)biz).GetReportData(selectedRequest, jobId); } //////////////////////////////////////////////////////////////////////////////////////////////// //RENDER // public async Task RequestRenderReport(DataListReportRequest reportRequest, DateTime renderTimeOutExpiry, string apiUrl, string userName) { var log = Sockeye.Util.ApplicationLogging.CreateLogger("ReportBiz::RequestRenderReport"); log.LogDebug($"report id {reportRequest.ReportId}, timeout @ {renderTimeOutExpiry.ToString()}"); //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.SockType == SockType.NoType) { reportRequest.SockType = report.SockType; } AuthorizationRoles effectiveRoles = CurrentUserRoles; if ( !Sockeye.Api.ControllerHelpers.Authorized.HasReadFullRole(effectiveRoles, report.SockType)) { log.LogDebug($"bail: user unauthorized"); AddError(ApiErrorCode.NOT_AUTHORIZED, null, $"User not authorized for {report.SockType} 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); var JobName = $"LT:Report id: \"{reportRequest.ReportId}\" LT:{reportRequest.SockType} ({reportRequest.SelectedRowIds.LongLength}) LT:User {userName}"; JObject o = JObject.FromObject(new { reportRequest = reportRequest, apiUrl = apiUrl, userName = userName }); OpsJob j = new OpsJob(); j.Name = JobName; j.SockType = SockType.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, SockType.ServerJob, SockEvent.Created, JobName), ct); return j.GId; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// } public async Task DoRenderJob(OpsJob job) { var log = Sockeye.Util.ApplicationLogging.CreateLogger("ReportBiz::DoRenderJob"); await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running); ReportRenderManager.AddJob(job.GId, log); // await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.JobType}"); //rehydrate job objects log.LogDebug($"Start; rehydrate job {job.Name}"); JObject jobData = JObject.Parse(job.JobInfo); var reportRequest = jobData["reportRequest"].ToObject(); var apiUrl = jobData["apiUrl"].ToObject(); var userName = jobData["userName"].ToObject(); var report = await ct.Report.FirstOrDefaultAsync(z => z.Id == reportRequest.ReportId); if (report == null) { await JobsBiz.LogJobAsync(job.GId, $"rendererror:error,\"LT:ErrorAPI2010 LT:Report({reportRequest.ReportId})\""); await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed); return; } if (!ReportRenderManager.KeepGoing(job.GId)) return; //Get data log.LogDebug("Getting report data now"); // var watch = new Stopwatch(); // watch.Start(); var ReportData = await GetReportData(reportRequest, job.GId); // watch.Stop(); // log.LogInformation($"GetReportData took {watch.ElapsedMilliseconds}ms to execute"); //THIS is here to catch scenario where report data returned null because it expired, not because of an issue if (!ReportRenderManager.KeepGoing(job.GId)) return; //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, $"rendererror:error,\"{this.GetErrorsAsString()}\"");//?? circle back on this one await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed); } if (!ReportRenderManager.KeepGoing(job.GId)) return; //initialization log.LogDebug("Initializing report rendering system"); bool AutoDownloadChromium = true; if (string.IsNullOrWhiteSpace(ServerBootConfig.SOCKEYE_REPORT_RENDER_BROWSER_PATH)) { log.LogDebug($"Using default Chromium browser (downloaded)"); } else { log.LogDebug($"Using user specified Chromium browser at {ServerBootConfig.SOCKEYE_REPORT_RENDER_BROWSER_PATH}"); AutoDownloadChromium = false; } var ReportJSFolderPath = Path.Combine(ServerBootConfig.SOCKEYE_CONTENT_ROOT_PATH, "resource", "rpt"); //Keep for debugging headfully //var lo = new LaunchOptions { Headless = false }; var lo = new LaunchOptions { Headless = true }; if (!AutoDownloadChromium) { lo.ExecutablePath = ServerBootConfig.SOCKEYE_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 { 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 "; //Keep for debugging headfully // var DefaultArgs = "--no-sandbox"; var DefaultArgs = "--headless --no-sandbox"; if (!string.IsNullOrWhiteSpace(ServerBootConfig.SOCKEYE_REPORT_RENDER_BROWSER_PARAMS)) { log.LogDebug($"SOCKEYE_REPORT_RENDER_BROWSER_PARAMS will be used: {ServerBootConfig.SOCKEYE_REPORT_RENDER_BROWSER_PARAMS}"); lo.Args = new string[] { $"{ServerBootConfig.SOCKEYE_REPORT_RENDER_BROWSER_PARAMS}" };// SOCKEYE_REPORT_RENDER_BROWSER_PARAMS } else { log.LogDebug($"SOCKEYE_REPORT_RENDER_BROWSER_PARAMS not set, using defaults '{DefaultArgs}'"); lo.Args = new string[] { DefaultArgs }; } System.Text.StringBuilder PageLog = new System.Text.StringBuilder(); if (!ReportRenderManager.KeepGoing(job.GId)) return; //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(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) => { 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 (!ReportRenderManager.KeepGoing(job.GId)) return; //Add Handlebars JS for compiling and presenting //https://handlebarsjs.com/ await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-hb.js") }); //add Marked for markdown processing //https://github.com/markedjs/marked await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-md.js") }); //add DOM Purify for markdown template sanitization processing //https://github.com/cure53/DOMPurify await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-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, "sock-bc.js") }); //add stock helpers await page.AddScriptTagAsync(new AddTagOptions() { Path = Path.Combine(ReportJSFolderPath, "sock-report.js") }); //execute to add to handlebars await page.EvaluateExpressionAsync("ayRegisterHelpers();"); 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 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 }); 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"; //case 4209 //latitude and longitude are the only nullable fields in global biz settings and need to be converted to empty strings if null string sLatitude = "null"; string sLongitude = "null"; if (ServerGlobalBizSettings.Cache.Latitude != null) sLatitude = ServerGlobalBizSettings.Cache.Latitude.ToString(); if (ServerGlobalBizSettings.Cache.Longitude != null) sLongitude = ServerGlobalBizSettings.Cache.Longitude.ToString(); var serverMeta = $"{{ayApiUrl:`{apiUrl}`, HasSmallLogo:{HasSmallLogo}, HasMediumLogo:{HasMediumLogo}, HasLargeLogo:{HasLargeLogo},CompanyName: `Ground Zero Tech-Works Inc.`,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}`,CompanyAddressPostal:`{ServerGlobalBizSettings.Cache.AddressPostal}`,CompanyLatitude:{sLatitude},CompanyLongitude:{sLongitude}}}"; 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.SockType.ToString()); if (FormCustomization != null) { CustomFieldsTemplate = FormCustomization.Template; } //Report meta data var reportMeta = $"{{Id:{report.Id},Name:`{report.Name}`,Notes:`{report.Notes}`,SockType:`{report.SockType}`,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 (!ReportRenderManager.KeepGoing(job.GId)) return; //prePareData / preRender var ReportDataObject = $"{{ ayReportData:{ReportData}, ayReportMetaData:{reportMeta}, ayClientMetaData:{clientMeta}, ayServerMetaData:{serverMeta} }}"; //case 4209 log.LogDebug($"ReportData object about to be pre-rendered is:{ReportDataObject}"); log.LogDebug($"PageLog before render:{PageLog.ToString()}"); log.LogDebug($"Calling ayPreRender..."); await page.WaitForExpressionAsync($"ayPreRender({ReportDataObject})"); log.LogDebug($"ayPreRender completed"); if (!ReportRenderManager.KeepGoing(job.GId)) return; //compile the template log.LogDebug($"Calling Handlebars.compile..."); var compileScript = $"Handlebars.compile(`{report.Template}`)(PreParedReportDataObject);"; if (!ReportRenderManager.KeepGoing(job.GId)) return; var compiledHTML = await page.EvaluateExpressionAsync(compileScript); if (!ReportRenderManager.KeepGoing(job.GId)) return; //render report as HTML log.LogDebug($"Setting render page content style and compiled HTML"); await page.SetContentAsync($"{compiledHTML}"); 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; 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 (!ReportRenderManager.KeepGoing(job.GId)) return; //render to pdf and return log.LogDebug($"Calling render page contents to PDF"); await page.PdfAsync(outputFullPath, PdfOptions); 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"); var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { reportfilename = outputFileName }, Newtonsoft.Json.Formatting.None); await JobsBiz.LogJobAsync(job.GId, json); 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 - report was cancelled by user OR timed out"); //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()); var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { rendererror = new { pagelog = PageLog.ToString(), exception = ExceptionUtil.ExtractAllExceptionMessages(ex) } }, Newtonsoft.Json.Formatting.None); await JobsBiz.LogJobAsync(job.GId, json); } else { var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { rendererror = new { exception = ExceptionUtil.ExtractAllExceptionMessages(ex) } }, Newtonsoft.Json.Formatting.None); await JobsBiz.LogJobAsync(job.GId, json); } 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.RemoveJob to stop tracking this job/process"); await ReportRenderManager.RemoveJob(job.GId, log, false); } } static async Task HandleTimeOut(OpsJob job, ILogger log, DataListReportRequest reportRequest, string userName) { log.LogDebug($"Report render cancelled by user OR exceeded timeout setting of {ServerBootConfig.SOCKEYE_REPORT_RENDERING_TIMEOUT} minutes, report id: {reportRequest.ReportId}, record count:{reportRequest.SelectedRowIds.LongLength}, user:{userName}"); var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { rendererror = new { timeout = true, timeoutsetting = ServerBootConfig.SOCKEYE_REPORT_RENDERING_TIMEOUT } }, Newtonsoft.Json.Formatting.None); //"{\"rendererror\":{\"timeout\":1}}" await JobsBiz.LogJobAsync(job.GId, json); await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Failed); } } public async Task CancelJob(Guid jobId) { await ReportRenderManager.RemoveJob(jobId, Sockeye.Util.ApplicationLogging.CreateLogger("ReportBiz::CancelJob"), true); } //////////////////////////////////////////////////////////////////////////////////////////////// //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 }//eons