Skip to content

HtmlScreenshot: headless Chrome launches use a private, temporary user-data-dir - #320

Open
hesong12 wants to merge 1 commit into
iOfficeAI:mainfrom
hesong12:fix/officecli-headless-user-data-dir
Open

HtmlScreenshot: headless Chrome launches use a private, temporary user-data-dir#320
hesong12 wants to merge 1 commit into
iOfficeAI:mainfrom
hesong12:fix/officecli-headless-user-data-dir

Conversation

@hesong12

Copy link
Copy Markdown

Files: src/officecli/Core/HtmlScreenshot.cs (DumpDom, CaptureChromeSized,
TryChrome, RunChromeCapture)

Summary

Every headless Chrome launch in this file (DumpDom, CaptureChromeSized,
TryChrome, RunChromeCapture) previously relied on Chrome's own default
profile-directory resolution. Two consequences:

  1. A screenshot can contend with a Chrome window the host already has open
    on the same default profile.
  2. It cannot run at all under a sandbox that denies writes to the default
    profile directory (a real constraint for hosts that run OfficeCLI inside
    a restricted exec sandbox).

This PR adds a fresh, private --user-data-dir under the OS temp directory
to all four headless launch sites, and deletes it once that specific launch
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 */ }
}

Each call site: generate the dir path before building the argument list, add
--user-data-dir={dir} alongside the existing --headless=new flags, and
delete it in a finally around the existing try/catch (or a new
try/finally where the site had none) so it's cleaned up on every exit path
— success, failure, or the existing timeout-kill branch.

Why

Same sandboxed-host motivation as the two sibling PRs: the host needing this
runs OfficeCLI's headless-screenshot path inside a restricted environment
and cannot guarantee the default Chrome profile directory is writable or
uncontended.

Validation

Toolchain: same scratch .NET 10 SDK as the sibling PRs. dotnet build src/officecli -c Debug succeeds.

Ran a screenshot while another chrome-family process already had the real
default profile directory (~/Library/Application Support/Google/Chrome)
open (a background --headless=new instance against the unmodified
default profile, holding its SingletonSocket), confirming this branch's
screenshot still succeeds:

officecli create t.docx
officecli add t.docx /body --type paragraph --prop text=hello
officecli close t.docx
officecli view t.docx screenshot -o t.png --json
# → {"success":true,"data":".../t.png", ...}

Result PNG: 1600×1200, 8-bit RGB, 9296 bytes — see attached U3-screenshot.png
(identical render to the sibling PR's baseline "hello" document, confirming
no regression).

Directly observed the fix taking effect (not just "it happened to work"):
polled the OS temp directory once a second while the screenshot ran and
caught the profile directory mid-flight —

officecli-chrome-9e74a0025e2b4486a6da60bd19c016f3

— matching the officecli-chrome-<guid> pattern in NewChromeUserDataDir(),
confirming the flag is actually being passed and used, not merely present in
the diff. After the command completed normally:

ls "$TMPDIR" | grep '^officecli-chrome-'
# → (nothing — no leftover profile dir)

Note on the "before" comparison: the concurrent-headless-instance repro
above did not reproduce a hard failure on the unmodified main branch
with this Chrome version (149.0.7827.22 / 151.0.7922.138) — a second
--headless=new instance against an already-open default profile completed
without contention on this build. The fix is still correct and worth
landing on its own merits (verified via direct observation of the isolated
profile directory's creation and cleanup above): it removes any dependency
on the host's default-profile availability at all, which is what makes
screenshotting work under a sandbox that denies writes to that directory —
the harder and more relevant constraint for the sandboxed host this change
is for. Did not attempt a foreground (non-headless, visibly-windowed) Chrome
instance for this test, to avoid interacting with the tester's real,
in-use browser session.

Compatibility

Additive/isolating only — every headless launch site behaves exactly as
before except for the added, self-cleaning profile directory. No public API
change; HasChromeFamily() / FindChrome() are untouched.

Every headless launch previously used Chrome's own default profile
resolution, so a screenshot could contend with a Chrome window the
host already has open, and it could not run at all under a sandbox
that denies writes to the default profile directory. Point every
headless launch (DumpDom, CaptureChromeSized, TryChrome,
RunChromeCapture) at a fresh --user-data-dir under the temp directory
and delete it once that launch exits.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 540c91059a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return (p.ExitCode == 0, errTask.GetAwaiter().GetResult());
}
catch { return (false, null); }
finally { CleanupChromeUserDataDir(userDataDir); }

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 Chrome to exit before deleting its profile

When a Chrome capture times out, both this method and the RunBinary path call Kill(true) and return immediately; process termination is asynchronous, so this finally can attempt Directory.Delete while Chrome or its children still hold files in the profile. On platforms such as Windows the deletion then fails, and because CleanupChromeUserDataDir suppresses the exception, repeated timed-out captures permanently accumulate these supposedly temporary directories. Wait for the killed process tree to exit before running the cleanup.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant