Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions src/officecli/Core/HtmlScreenshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public sealed record PaginationResult(int TotalPages, Dictionary<string, int> An
var url = new Uri(Path.GetFullPath(htmlPath)).AbsoluteUri + "#screenshot";
var bin = FindChrome();
if (bin == null) return null;
using var profile = CreatePrivateChromeProfile();
var args = new List<string>
{
"--headless=new",
Expand All @@ -40,6 +41,7 @@ public sealed record PaginationResult(int TotalPages, Dictionary<string, int> An
"--virtual-time-budget=15000",
"--timeout=20000", // wall-clock backstop: a stalled resource is not rescued by virtual time (issue #181)
};
AddChromeIsolationArguments(args, profile);
if (extraArgs != null) args.AddRange(extraArgs);
args.Add("--dump-dom");
args.Add(url);
Expand Down Expand Up @@ -84,7 +86,8 @@ 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 args = new[]
using var profile = CreatePrivateChromeProfile();
var args = new List<string>
{
"--headless=new",
"--disable-gpu",
Expand All @@ -98,6 +101,7 @@ public static bool CaptureChromeSized(string htmlPath, string outPath, int w, in
$"--screenshot={outPath}",
url,
};
AddChromeIsolationArguments(args, profile);
var (ok, _) = RunBinary(bin, args);
return ok && File.Exists(outPath) && new FileInfo(outPath).Length > 0;
}
Expand Down Expand Up @@ -385,7 +389,8 @@ private static (bool, string?) TryChrome(string url, string outPath, int w, int
{
var bin = FindChrome();
if (bin == null) return (false, null);
var args = new[]
using var profile = CreatePrivateChromeProfile();
var args = new List<string>
{
"--headless=new",
"--disable-gpu",
Expand All @@ -406,9 +411,43 @@ private static (bool, string?) TryChrome(string url, string outPath, int w, int
$"--screenshot={outPath}",
url,
};
AddChromeIsolationArguments(args, profile);
return RunBinary(bin, args);
}

/// <summary>
/// Gives every direct Chromium render an empty, disposable profile. This
/// prevents a headless renderer from contending with the user's browser
/// profile; on macOS the mock keychain also prevents Chrome Safe Storage
/// from opening or creating items in the user's login keychain.
/// </summary>
private sealed class PrivateChromeProfile : IDisposable
{
public string Path { get; }

public PrivateChromeProfile()
{
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(),
$"officecli-chrome-{Guid.NewGuid():N}");
Directory.CreateDirectory(Path);
}

public void Dispose()
{
try { Directory.Delete(Path, recursive: true); }
catch { /* temporary profile cleanup is best effort */ }
Comment on lines +437 to +438

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wait for timed-out Chrome processes before deleting profiles

When a Chrome invocation times out on Windows, both RunBinary and RunChromeCapture call Kill(true) and return without waiting for the process tree to exit. Dispose therefore immediately attempts this deletion while Chrome can still hold profile files open; the exception is swallowed and the unique officecli-chrome-* directory is permanently left behind, potentially leaking substantial temporary data after repeated render timeouts. Wait for termination or retry cleanup before abandoning the directory.

Useful? React with 👍 / 👎.

}
}

private static PrivateChromeProfile CreatePrivateChromeProfile() => new();

private static void AddChromeIsolationArguments(List<string> args, PrivateChromeProfile profile)
{
args.Add($"--user-data-dir={profile.Path}");
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
args.Add("--use-mock-keychain");
}

private static string? FindChrome()
{
string[] names = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser",
Expand Down Expand Up @@ -542,6 +581,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";
using var profile = CreatePrivateChromeProfile();
try
{
var psi = new ProcessStartInfo
Expand All @@ -555,7 +595,7 @@ private static (bool Ok, string? Stderr) RunChromeCapture(string htmlPath, strin
UseShellExecute = false,
CreateNoWindow = true,
};
foreach (var a in new[]
var args = new List<string>
{
"--headless=new",
"--disable-gpu",
Expand All @@ -569,7 +609,9 @@ private static (bool Ok, string? Stderr) RunChromeCapture(string htmlPath, strin
"--timeout=20000",
$"--screenshot={outPath}",
url,
}) psi.ArgumentList.Add(a);
};
AddChromeIsolationArguments(args, profile);
foreach (var a in args) psi.ArgumentList.Add(a);
using var p = Process.Start(psi);
if (p == null) return (false, null);
var outTask = p.StandardOutput.ReadToEndAsync();
Expand All @@ -584,7 +626,7 @@ private static (bool Ok, string? Stderr) RunChromeCapture(string htmlPath, strin
catch { return (false, null); }
}

private static (bool, string?) RunBinary(string bin, string[] args)
private static (bool, string?) RunBinary(string bin, IEnumerable<string> args)
{
try
{
Expand Down