Skip to content

Commit

Permalink
Create ReportSheetCache instance with ILoggerFactory
Browse files Browse the repository at this point in the history
Note: Puppeteer can currently create PDF files only with Chrome
  • Loading branch information
axunonb committed Sep 23, 2024
1 parent d9453b7 commit 7e607b0
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 14 deletions.
62 changes: 49 additions & 13 deletions League/Caching/ReportSheetCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ namespace League.Caching;

#pragma warning disable S2083 // reason: False positive due to CancellationToken in GetOrCreatePdf
#pragma warning disable CA3003 // reason: False positive due to CancellationToken in GetOrCreatePdf

/// <summary>
/// Represents a cache for report sheets.
/// </summary>
public class ReportSheetCache
{
private readonly ITenantContext _tenantContext;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly string _pathToChromium;
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger<ReportSheetCache> _logger;

/// <summary>
Expand All @@ -28,24 +33,45 @@ public class ReportSheetCache
/// </summary>
public const string ReportSheetFilenameTemplate = "Sheet_{0}_{1}_{2}.pdf";

public ReportSheetCache(ITenantContext tenantContext, IConfiguration configuration, IWebHostEnvironment webHostEnvironment, ILogger<ReportSheetCache> logger)
/// <summary>
/// Initializes a new instance of the <see cref="ReportSheetCache"/> class.
/// </summary>
/// <param name="tenantContext"></param>
/// <param name="configuration"></param>
/// <param name="webHostEnvironment"></param>
/// <param name="loggerFactory"></param>
public ReportSheetCache(ITenantContext tenantContext, IConfiguration configuration, IWebHostEnvironment webHostEnvironment, ILoggerFactory loggerFactory)
{
_tenantContext = tenantContext;
_webHostEnvironment = webHostEnvironment;
_pathToChromium = Path.Combine(webHostEnvironment.ContentRootPath, configuration["Chromium:ExecutablePath"] ?? string.Empty);
_logger = logger;
_loggerFactory = loggerFactory;
_logger = loggerFactory.CreateLogger<ReportSheetCache>();
}

/// <summary>
/// Gets or sets a value indicating whether to use Puppeteer for generating the report sheet,
/// instead of Chromium command line.
/// </summary>
public bool UsePuppeteer { get; set; } = false;

private void EnsureCacheFolder()
{
var cacheFolder = Path.Combine(_webHostEnvironment.WebRootPath, ReportSheetCacheFolder);
if (!Directory.Exists(cacheFolder))
{
_logger.LogDebug("Cache folder '{CacheFolder}' created", cacheFolder);
Directory.CreateDirectory(cacheFolder);
_logger.LogDebug("Cache folder '{CacheFolder}' created", cacheFolder);
}
}

/// <summary>
/// Gets or creates a PDF file for a match report sheet.
/// </summary>
/// <param name="data"></param>
/// <param name="html"></param>
/// <param name="cancellationToken"></param>
/// <returns>A <see cref="Stream"/> of the PDF file.</returns>
public async Task<Stream> GetOrCreatePdf(MatchReportSheetRow data, string html, CancellationToken cancellationToken)
{
EnsureCacheFolder();
Expand All @@ -55,9 +81,11 @@ public async Task<Stream> GetOrCreatePdf(MatchReportSheetRow data, string html,
if (!File.Exists(cacheFile) || IsOutdated(cacheFile, data.ModifiedOn))
{
_logger.LogDebug("Create new match report for tenant '{Tenant}', match '{MatchId}'", _tenantContext.Identifier, data.Id);
// cacheFile = await GetReportSheetChromium(data.Id, html, cancellationToken);
cacheFile = await GetReportSheetPuppeteer(data.Id, html, cancellationToken);
// GetReportSheetPuppeteer(...) still throws on production server

cacheFile = UsePuppeteer
? await GetReportSheetPuppeteer(data.Id, html, cancellationToken)
: await GetReportSheetChromium(data.Id, html, cancellationToken);

if (cacheFile == null) return Stream.Null;
}

Expand Down Expand Up @@ -122,15 +150,15 @@ private string GetPathToCacheFile(long matchId)
Headless = true,
Browser = PuppeteerSharp.SupportedBrowser.Chromium,
// Alternative: --use-cmd-decoder=validating
Args = new[]
{ "--no-sandbox", "--disable-gpu", "--disable-extensions", "--use-cmd-decoder=passthrough" },
// Args = new[] // removed on 2024-09-23
// { "--no-sandbox", "--disable-gpu", "--disable-extensions", "--use-cmd-decoder=passthrough" },
ExecutablePath = _pathToChromium,
Timeout = 5000
};
// Use Puppeteer as a wrapper for the browser, which can generate PDF from HTML
// Start command line arguments set by Puppeteer:
// --allow-pre-commit-input --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-sync --enable-automation --enable-blink-features=IdleDetection --enable-features=NetworkServiceInProcess2 --export-tagged-pdf --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --headless
await using var browser = await PuppeteerSharp.Puppeteer.LaunchAsync(options).ConfigureAwait(false);
await using var browser = await PuppeteerSharp.Puppeteer.LaunchAsync(options, _loggerFactory).ConfigureAwait(false);
await using var page = await browser.NewPageAsync().ConfigureAwait(false);

await page.SetContentAsync(html); // Bootstrap 5 is loaded from CDN
Expand Down Expand Up @@ -159,11 +187,19 @@ at League.Controllers.Match.ReportSheet(Int64 id, CancellationToken cancellation
|url: https://volleyball-liga.de/augsburg/match/reportsheet/3188|action: ReportSheet
*/
var fullPath = GetPathToCacheFile(matchId);
try
{
// page.PdfDataAsync times out after 180,000ms (3 minutes)
var bytes = await page.PdfDataAsync(new PuppeteerSharp.PdfOptions
{ Scale = 1.0M, Format = PuppeteerSharp.Media.PaperFormat.A4 }).ConfigureAwait(false);

var bytes = await page.PdfDataAsync(new PuppeteerSharp.PdfOptions
{ Scale = 1.0M, Format = PuppeteerSharp.Media.PaperFormat.A4 }).ConfigureAwait(false);

await File.WriteAllBytesAsync(fullPath, bytes, cancellationToken);
await File.WriteAllBytesAsync(fullPath, bytes, cancellationToken);
}
catch(Exception ex)
{
_logger.LogError(ex, "Error creating PDF file with Puppeteer for match ID '{MatchId}'", matchId);
await File.WriteAllBytesAsync(fullPath, Array.Empty<byte>(), cancellationToken);
}

return fullPath;
}
Expand Down
2 changes: 1 addition & 1 deletion League/Controllers/Match.cs
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,7 @@ private async Task<EnterResultViewModel> GetEnterResultViewModel(MatchEntity mat
public async Task<IActionResult> ReportSheet(long id, [FromServices] ReportSheetCache cache, CancellationToken cancellationToken)
{
MatchReportSheetRow? model = null;
cache.UsePuppeteer = false;

try
{
Expand All @@ -730,7 +731,6 @@ public async Task<IActionResult> ReportSheet(long id, [FromServices] ReportSheet
var html = await _razorViewToStringRenderer.RenderViewToStringAsync(
$"~/Views/{nameof(Match)}/{ViewNames.Match.ReportSheet}.cshtml", model);

//var cache = new ReportSheetCache(_tenantContext, _configuration, _webHostEnvironment);
var stream = await cache.GetOrCreatePdf(model, html, cancellationToken);
_logger.LogInformation("PDF file returned for tenant '{Tenant}' and match id '{MatchId}'", _tenantContext.Identifier, id);
return new FileStreamResult(stream, "application/pdf");
Expand Down

0 comments on commit 7e607b0

Please sign in to comment.