From b52371ea792d900d8af00438ff8ac56d016a6446 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sat, 11 Apr 2026 17:45:51 +0200 Subject: [PATCH] Fix backend deployment Playwright restore issue --- JobTrackerApi/Dockerfile | 5 + .../Services/PlaywrightCvPdfExporter.cs | 156 +++++++++++++++--- JobTrackerBackend/JobTrackerBackend.csproj | 1 - 3 files changed, 139 insertions(+), 23 deletions(-) diff --git a/JobTrackerApi/Dockerfile b/JobTrackerApi/Dockerfile index c68cf11..0c44489 100644 --- a/JobTrackerApi/Dockerfile +++ b/JobTrackerApi/Dockerfile @@ -16,6 +16,11 @@ FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime WORKDIR /app ENV ASPNETCORE_URLS=http://+:8080 +ENV CV_PDF_BROWSER_PATH=/usr/bin/chromium + +RUN apt-get update \ + && apt-get install -y --no-install-recommends chromium \ + && rm -rf /var/lib/apt/lists/* RUN mkdir -p /data diff --git a/JobTrackerApi/Services/PlaywrightCvPdfExporter.cs b/JobTrackerApi/Services/PlaywrightCvPdfExporter.cs index 740f158..4248d0e 100644 --- a/JobTrackerApi/Services/PlaywrightCvPdfExporter.cs +++ b/JobTrackerApi/Services/PlaywrightCvPdfExporter.cs @@ -1,4 +1,5 @@ -using Microsoft.Playwright; +using System.Diagnostics; +using System.Text; namespace JobTrackerApi.Services; @@ -11,6 +12,18 @@ public interface ICvPdfExporter public sealed class PlaywrightCvPdfExporter : ICvPdfExporter { + private static readonly string[] BrowserCandidates = + { + "chromium", + "chromium-browser", + "google-chrome", + "google-chrome-stable", + "/usr/bin/chromium", + "/usr/bin/chromium-browser", + "/usr/bin/google-chrome", + "/usr/bin/google-chrome-stable" + }; + private readonly AppPaths _paths; private readonly ILogger _logger; @@ -25,42 +38,141 @@ public sealed class PlaywrightCvPdfExporter : ICvPdfExporter var now = DateTimeOffset.UtcNow; var folder = Path.Combine(_paths.CvExportsRoot, now.ToString("yyyyMMdd")); Directory.CreateDirectory(folder); + var fileName = string.IsNullOrWhiteSpace(renderResult.SuggestedFileName) ? $"tailored-cv-{now:yyyyMMddHHmmss}.pdf" : renderResult.SuggestedFileName; var storagePath = Path.Combine(folder, fileName); + var tempRoot = Path.Combine(Path.GetTempPath(), "jobtracker-cv-pdf", Guid.NewGuid().ToString("n")); + var htmlPath = Path.Combine(tempRoot, "document.html"); + var userDataDir = Path.Combine(tempRoot, "profile"); + + Directory.CreateDirectory(tempRoot); + Directory.CreateDirectory(userDataDir); + try { - using var playwright = await Playwright.CreateAsync(); - await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions + await File.WriteAllTextAsync(htmlPath, renderResult.Html ?? string.Empty, Encoding.UTF8, cancellationToken); + + var browserPath = ResolveBrowserPath(); + if (string.IsNullOrWhiteSpace(browserPath)) { - Headless = true, - }); - var page = await browser.NewPageAsync(); - await page.SetContentAsync(renderResult.Html, new PageSetContentOptions + throw new InvalidOperationException("CV PDF export is unavailable. Install Chromium/Google Chrome or set CV_PDF_BROWSER_PATH."); + } + + var arguments = BuildArguments(userDataDir, storagePath, htmlPath); + var startInfo = new ProcessStartInfo(); + startInfo.FileName = browserPath; + startInfo.Arguments = arguments; + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.UseShellExecute = false; + startInfo.CreateNoWindow = true; + + using var process = new Process(); + process.StartInfo = startInfo; + process.Start(); + await process.WaitForExitAsync(cancellationToken); + + var stdout = await process.StandardOutput.ReadToEndAsync(); + var stderr = await process.StandardError.ReadToEndAsync(); + + if (process.ExitCode != 0) { - WaitUntil = WaitUntilState.Load, - }); - var bytes = await page.PdfAsync(new PagePdfOptions + throw new InvalidOperationException($"CV PDF export failed via browser CLI. ExitCode={process.ExitCode}. Stdout={stdout}. Stderr={stderr}"); + } + + if (!File.Exists(storagePath)) { - Format = "A4", - PrintBackground = true, - Margin = new() - { - Top = "0", - Right = "0", - Bottom = "0", - Left = "0", - } - }); - await File.WriteAllBytesAsync(storagePath, bytes, cancellationToken); + throw new InvalidOperationException($"CV PDF export did not create the expected file at {storagePath}."); + } + + var bytes = await File.ReadAllBytesAsync(storagePath, cancellationToken); return new CvPdfArtifact(fileName, storagePath, bytes); } catch (Exception ex) { _logger.LogError(ex, "Failed to export CV PDF to {Path}", storagePath); - throw new InvalidOperationException("CV PDF export is unavailable. Ensure Chromium is installed for Playwright on this machine.", ex); + throw; + } + finally + { + TryDeleteDirectory(tempRoot); } } + + private static string BuildArguments(string userDataDir, string storagePath, string htmlPath) + { + var parts = new List + { + "--headless=new", + "--disable-gpu", + "--no-sandbox", + "--disable-dev-shm-usage", + "--allow-file-access-from-files", + "--enable-local-file-accesses", + "--user-data-dir=" + Quote(userDataDir), + "--print-to-pdf=" + Quote(storagePath), + Quote(htmlPath) + }; + + return string.Join(' ', parts); + } + + private static string? ResolveBrowserPath() + { + var configured = Environment.GetEnvironmentVariable("CV_PDF_BROWSER_PATH"); + if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured)) + { + return configured; + } + + foreach (var candidate in BrowserCandidates) + { + if (Path.IsPathRooted(candidate)) + { + if (File.Exists(candidate)) return candidate; + continue; + } + + var resolved = FindOnPath(candidate); + if (!string.IsNullOrWhiteSpace(resolved)) return resolved; + } + + return null; + } + + private static string? FindOnPath(string fileName) + { + var path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + var parts = path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + foreach (var dir in parts) + { + var fullPath = Path.Combine(dir, fileName); + if (File.Exists(fullPath)) return fullPath; + } + + return null; + } + + private static void TryDeleteDirectory(string path) + { + try + { + if (Directory.Exists(path)) + { + Directory.Delete(path, recursive: true); + } + } + catch + { + // best effort temp cleanup + } + } + + private static string Quote(string value) + { + return '"' + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + '"'; + } } diff --git a/JobTrackerBackend/JobTrackerBackend.csproj b/JobTrackerBackend/JobTrackerBackend.csproj index 70bb2cc..77b0598 100644 --- a/JobTrackerBackend/JobTrackerBackend.csproj +++ b/JobTrackerBackend/JobTrackerBackend.csproj @@ -27,7 +27,6 @@ all -