diff --git a/src/officecli/Core/HtmlScreenshot.cs b/src/officecli/Core/HtmlScreenshot.cs index 787e77288..a89443d19 100644 --- a/src/officecli/Core/HtmlScreenshot.cs +++ b/src/officecli/Core/HtmlScreenshot.cs @@ -17,6 +17,20 @@ public sealed record Result(bool Ok, string Backend, string? Error); public sealed record PaginationResult(int TotalPages, Dictionary AnchorPageMap); + /// Path for a fresh, private headless-Chrome profile directory for one + /// launch. Keeps a headless run from touching — or blocking on a lock held by — + /// the caller's own default Chrome profile, and lets screenshotting work under + /// sandboxes that deny writes to the default profile directory. The directory + /// is created by Chrome itself on first use; callers delete it with + /// once the launch that created it exits. + private static string NewChromeUserDataDir() => + Path.Combine(Path.GetTempPath(), $"officecli-chrome-{Guid.NewGuid():N}"); + + private static void CleanupChromeUserDataDir(string dir) + { + try { Directory.Delete(dir, true); } catch { /* best effort */ } + } + /// Run a chromium-family browser in dump-dom mode against the given HTML /// and parse the document title for "PAGES:N|MAP:anchor=p,anchor=p,...". /// The HTML must set the title from JS after layout settles. @@ -32,11 +46,13 @@ public sealed record PaginationResult(int TotalPages, Dictionary An var url = new Uri(Path.GetFullPath(htmlPath)).AbsoluteUri + "#screenshot"; var bin = FindChrome(); if (bin == null) return null; + var userDataDir = NewChromeUserDataDir(); var args = new List { "--headless=new", "--disable-gpu", "--no-sandbox", + $"--user-data-dir={userDataDir}", "--virtual-time-budget=15000", "--timeout=20000", // wall-clock backstop: a stalled resource is not rescued by virtual time (issue #181) }; @@ -64,6 +80,7 @@ public sealed record PaginationResult(int TotalPages, Dictionary An return stdout; } catch { return null; } + finally { CleanupChromeUserDataDir(userDataDir); } } /// True when a chrome-family browser (Chrome/Chromium/Edge) is available. @@ -84,12 +101,14 @@ public static bool CaptureChromeSized(string htmlPath, string outPath, int w, in var outDir = Path.GetDirectoryName(outPath); if (!string.IsNullOrEmpty(outDir)) Directory.CreateDirectory(outDir); var url = new Uri(Path.GetFullPath(htmlPath)).AbsoluteUri + "#screenshot"; + var userDataDir = NewChromeUserDataDir(); var args = new[] { "--headless=new", "--disable-gpu", "--no-sandbox", "--hide-scrollbars", + $"--user-data-dir={userDataDir}", $"--force-device-scale-factor={scale}", $"--window-size={w},{h}", "--virtual-time-budget=15000", @@ -98,8 +117,12 @@ public static bool CaptureChromeSized(string htmlPath, string outPath, int w, in $"--screenshot={outPath}", url, }; - var (ok, _) = RunBinary(bin, args); - return ok && File.Exists(outPath) && new FileInfo(outPath).Length > 0; + try + { + var (ok, _) = RunBinary(bin, args); + return ok && File.Exists(outPath) && new FileInfo(outPath).Length > 0; + } + finally { CleanupChromeUserDataDir(userDataDir); } } public static PaginationResult? GetPaginationFromDom(string htmlPath, int timeoutMs = 60000) @@ -385,12 +408,14 @@ private static (bool, string?) TryChrome(string url, string outPath, int w, int { var bin = FindChrome(); if (bin == null) return (false, null); + var userDataDir = NewChromeUserDataDir(); var args = new[] { "--headless=new", "--disable-gpu", "--no-sandbox", "--hide-scrollbars", + $"--user-data-dir={userDataDir}", $"--window-size={w},{h}", // Without these caps, new-headless --screenshot waits for the // page's external resources (CDN fonts / KaTeX css+js) to settle; @@ -406,7 +431,8 @@ private static (bool, string?) TryChrome(string url, string outPath, int w, int $"--screenshot={outPath}", url, }; - return RunBinary(bin, args); + try { return RunBinary(bin, args); } + finally { CleanupChromeUserDataDir(userDataDir); } } private static string? FindChrome() @@ -542,6 +568,7 @@ private static (bool Ok, string? Stderr) RunChromeCapture(string htmlPath, strin if (bin == null) return (false, null); outPath = Path.GetFullPath(outPath); var url = new Uri(Path.GetFullPath(htmlPath)).AbsoluteUri + "#screenshot"; + var userDataDir = NewChromeUserDataDir(); try { var psi = new ProcessStartInfo @@ -563,6 +590,7 @@ private static (bool Ok, string? Stderr) RunChromeCapture(string htmlPath, strin "--hide-scrollbars", "--enable-logging=stderr", "--v=0", + $"--user-data-dir={userDataDir}", $"--force-device-scale-factor={scale}", $"--window-size={w},{h}", "--virtual-time-budget=15000", @@ -582,6 +610,7 @@ private static (bool Ok, string? Stderr) RunChromeCapture(string htmlPath, strin return (p.ExitCode == 0, errTask.GetAwaiter().GetResult()); } catch { return (false, null); } + finally { CleanupChromeUserDataDir(userDataDir); } } private static (bool, string?) RunBinary(string bin, string[] args)