Skip to content

Add BrowserUserDataMode to separate user data dir from profile#16457

Open
davidfowl wants to merge 24 commits intomainfrom
davidfowl/browser-profiles
Open

Add BrowserUserDataMode to separate user data dir from profile#16457
davidfowl wants to merge 24 commits intomainfrom
davidfowl/browser-profiles

Conversation

@davidfowl
Copy link
Copy Markdown
Contributor

@davidfowl davidfowl commented Apr 25, 2026

Description

Follow-up to #16310 (tracked browser logs). This PR separates "which Chromium user data directory to use" from "which profile inside that directory to use", then finishes the shared-browser path so browser logs can run against the user's real browser state without racing Chromium's singleton process.

What's new

  • Adds BrowserUserDataMode:
    • Shared (default): use the browser's real user data directory so sessions can use real cookies, sign-in state, extensions, and profiles.
    • Isolated: use a temporary user data directory for a clean, throwaway browser state.
  • Adds WithBrowserLogs(..., userDataMode: ...) and configuration via Aspire:Hosting:BrowserLogs[:{ResourceName}]:UserDataMode.
  • Moves browser/profile/user-data-mode resolution into BrowserConfiguration, with explicit builder overrides grouped in BrowserConfigurationOverrides, so initial resource properties and command-time launches use the same precedence.
  • Makes default browser selection mode-aware: shared mode prefers Edge because branded Google Chrome blocks remote debugging against its default user data directory; isolated mode keeps the Chrome-first behavior.
  • Adds a clear guardrail for Shared + Google Chrome default user data directory.
  • Surfaces effective user-data mode, browser executable, browser host ownership, active sessions, last error, target URL, target id, and CDP endpoint in the browser logs resource properties.
  • Adds resource logs for host resolution, host ownership/adoption, target attach, connection loss, reconnect attempts, reconnect exhaustion, and host termination.
  • Refreshes ATS code-generation snapshots for TypeScript, Go, Python, Java, and Rust so generated SDKs include BrowserUserDataMode and the new withBrowserLogs argument.

Shared-browser architecture

  • Introduces BrowserHostRegistry, keyed by normalized (browser executable, user data root), with exactly-once BrowserHostLease release.
  • Adds owned vs adopted browser hosts:
    • Owned hosts spawn Chromium with --remote-debugging-address=127.0.0.1 and --remote-debugging-port=0, write endpoint metadata, and clean up the spawned browser when the last lease is released.
    • Adopted hosts reuse a previously validated debug-enabled browser and never terminate the user's browser process.
  • Adds aspire-debug-endpoint.json discovery/validation. Metadata is treated as a hint, not truth: schema, executable path, user data root, PID liveness, profile compatibility, and /json/version are validated before adoption; stale metadata is deleted.
  • Moves page ownership into per-target sessions. Each tracked launch creates or reuses a tab, attaches to that target, and disposes by calling Target.closeTarget only.
  • Removes the active Browser.close path so adopted/shared browser sessions cannot accidentally close the user's browser.
  • Adds target lifecycle handling for Target.targetDestroyed, Target.targetCrashed, Target.detachedFromTarget, and Inspector.detached, using the event payload (params.targetId / params.sessionId) for correlation.
  • Adds code comments describing the roles of the host registry, endpoint discovery, owned/adopted hosts, target sessions, dashboard manager, and CDP target discovery subscription.
  • Adds component-level tests for protocol lifecycle parsing, target lifecycle completion mapping, host registry sharing/profile rejection, dashboard Last error clearing, and adopted-browser resource diagnostics.

Behavior

effective UserDataMode profile behavior
Shared omitted use the real user data dir; Chromium picks the configured startup/default profile
Shared set use the real user data dir; force that profile via --profile-directory
Isolated omitted use a temp user data dir
Isolated set invalid; reject

Reliability notes

  • Endpoint startup fails fast if the browser process exits before writing DevToolsActivePort.
  • Stale DevToolsActivePort and Aspire endpoint metadata are not reused blindly.
  • WebSocket close is bounded.
  • Browser process and target-session disposables are cleaned up on setup failures.
  • Shared user-data singleton conflicts now produce a clear error instead of a long timeout.
  • Malformed endpoint metadata is treated as stale instead of crashing discovery.
  • Failed launches and failed target sessions keep a Last error property and unhealthy health report so the dashboard keeps the diagnostic visible even when another tracked browser session is still running.

Validation

  • dotnet build src/Aspire.Hosting/Aspire.Hosting.csproj /p:TreatWarningsAsErrors=false
  • dotnet test tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj -- --filter-class "*.BrowserLogsProtocolTests" --filter-class "*.BrowserLogsSessionManagerTests" --filter-class "*.BrowserLogsBuilderExtensionsTests" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true" — 57 passed
  • dotnet test tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj -- --filter-class "*.BrowserLogsBuilderExtensionsTests" --filter-class "*.BrowserLogsSessionManagerTests" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true" — passed after the BrowserConfiguration cleanup
  • dotnet test for TwoPassScanning_GeneratesWithEnvironmentOnTestRedisBuilder in TypeScript, Go, Python, Java, and Rust code-generation test projects — 5 passed
  • git diff --check
  • Playground smoke test with playground/BrowserTelemetry:
    • Started the playground in isolated AppHost mode.
    • Verified web-browser-logs defaults to Shared mode and Edge on macOS.
    • Verified the real-profile non-debuggable singleton conflict returns a clear failure when Edge is already open normally.
    • Restarted with Aspire__Hosting__BrowserLogs__UserDataMode=Isolated.
    • Verified open-tracked-browser succeeded, web-browser-logs became healthy, and browser network/console logs flowed into the resource.
    • Verified a previous shared-mode smoke reused the same browser process/debug endpoint for a second tracked launch while creating a separate target.
  • Reran transient failed CI jobs; PR checks are green.

Fixes # (issue)

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No
  • Does the change require an update in our Aspire docs?

davidfowl and others added 2 commits April 24, 2026 21:47
Introduce a BrowserUserDataMode enum (Shared/Isolated) on the tracked
browser launch path so callers can choose between launching against the
browser's real user data directory (like clicking the browser icon) or
a temporary, isolated user data directory.

- WithBrowserLogs(...) gains a userDataMode parameter
- Configuration key: Aspire:Hosting:BrowserLogs[:Resource]:UserDataMode
- Default is Isolated to preserve current behavior
- Setting Profile while UserDataMode is Isolated is rejected
- Shared + omitted profile lets Chromium pick its default profile
- Shared + profile resolves the profile inside the real user data dir
- Surface effective user data mode as a resource property

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Flip BrowserUserDataMode default from Isolated to Shared so the tracked
  browser behaves like a normal browser launch by default.
- Add a remarks section on BrowserUserDataMode.Shared explaining the
  Chromium singleton/SingletonLock + DevToolsActivePort interaction so
  callers understand why a second launch against an already-running real
  browser cannot establish a CDP endpoint.
- Update the default-mode test accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 25, 2026 05:48
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 25, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16457

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 16457"

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new user-data directory selection mode for tracked browser log sessions, separating “which Chromium user data dir to use” from “which profile inside that dir to use” to support either reusing real browser state or running from a clean temporary state.

Changes:

  • Introduces BrowserUserDataMode (Shared default / Isolated) and threads it through WithBrowserLogs(...), configuration resolution, and resource properties.
  • Updates browser launch behavior to use the real user data directory in Shared mode, including profile directory resolution via directory entries and Chromium “Local State” metadata.
  • Expands unit tests to cover user data mode defaults/overrides and profile directory resolution behaviors.
Show a summary per file
File Description
tests/Aspire.Hosting.Tests/BrowserLogsSessionManagerTests.cs Adds coverage for profile directory resolution (case-insensitive + Local State display-name mapping + ambiguity).
tests/Aspire.Hosting.Tests/BrowserLogsBuilderExtensionsTests.cs Adds coverage for default Shared mode, config parsing, explicit override precedence, and Isolated + profile rejection.
src/Aspire.Hosting/BrowserLogs/BrowserLogsRunningSession.cs Implements user-data-mode-aware user data dir selection and profile directory resolution; passes resolved directory name to --profile-directory.
src/Aspire.Hosting/BrowserLogs/BrowserLogsResource.cs Adds the public BrowserUserDataMode enum and stores resolved/override mode on the resource.
src/Aspire.Hosting/BrowserLogs/BrowserLogsBuilderExtensions.cs Extends public API + configuration parsing/validation and surfaces “User data mode” as a resource property.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 1

Comment thread src/Aspire.Hosting/BrowserLogs/BrowserLogsResource.cs Outdated
@davidfowl davidfowl marked this pull request as draft April 25, 2026 05:58
davidfowl and others added 18 commits April 24, 2026 23:04
Adds parsing for CDP events used to detect when a tracked target ends:
- Target.targetDestroyed
- Target.targetCrashed
- Target.detachedFromTarget
- Inspector.detached

Also adds command-name constants (Target.closeTarget, Target.setDiscoverTargets)
that the upcoming host abstraction will use to manage tracked targets without
calling Browser.close.

This is purely additive; nothing wires these events up yet.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Defines the IBrowserHost interface plus BrowserHostIdentity and the
BrowserHostOwnership enum. The interface separates 'a browser instance to
attach CDP to' from 'a tracked log session' so the same host can back many
sessions and so disposal can do the right thing for adopted browsers.

Key invariants documented in the file:
- Adopted hosts must never close the user's browser
- Profile directory participates in identity (different profiles = different
  process)
- Acquire/ReleaseAsync refcount, last release disposes
- Completion task surfaces process exit / CDP socket close so sessions can
  fail fast

No callers yet; this commit only adds the contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t routing

Code-review feedback from gpt-5.4, gpt-5.3-codex, and claude-opus-4.7 on
the Phase 2 foundation commits, plus a mechanical pass applying the same
lens James used on the original feature PR (#16310).

BrowserHostIdentity changes:
- Removed ProfileDirectory from the identity. Chromium's singleton is keyed
  by user-data-dir, not by profile, so different profiles under the same
  user data root share a browser process. Profile selection is per-target,
  not per-host. The previous doc comment claiming otherwise was wrong.
- Replaced record-struct synthesized equality (which is ordinal
  case-sensitive on strings) with explicit Equals/GetHashCode using
  StringComparer.OrdinalIgnoreCase on Windows / Ordinal elsewhere. Without
  this, paths that differ only in casing would create duplicate hosts on
  Windows.
- Constructor now normalizes paths via Path.GetFullPath +
  TrimEndingDirectorySeparator so 'C:/foo' and 'C:\foo\' collapse to the
  same identity.
- GetHashCode is null-safe against default(BrowserHostIdentity) since
  StringComparer.GetHashCode(null) throws.

Protocol changes:
- Documented above the new Target.* event records that the SUBJECT of these
  events lives in params (targetId / params.sessionId), not the envelope
  sessionId. The existing dispatch in BrowserLogsRunningSession filters on
  envelope sessionId only, so any future fanout layer that naively reuses
  that filter would silently drop these events. The comment makes the
  routing requirement unmissable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Apply the reliability fixes identified from the architecture review before
continuing the larger adoption refactor:

- Bound ChromeDevToolsConnection.DisposeAsync websocket close with a 3 second
  timeout so shutdown cannot hang indefinitely on a wedged browser.
- Add Target.closeTarget and Target.setDiscoverTargets helpers and enable
  target discovery after each browser-CDP connection, including reconnects.
- Add explicit Target lifecycle correlation helpers so future fanout routes
  targetDestroyed/targetCrashed by targetId and detachedFromTarget by
  params.sessionId instead of the usually-null envelope sessionId.
- Make shared-mode default browser selection prefer Edge. Isolated mode keeps
  Chrome-first behavior. This avoids Chrome's default-profile remote-debugging
  guardrail for the default Shared experience.
- Fail fast when Shared mode explicitly targets Google Chrome's default user
  data directory, with guidance to use Edge or Isolated mode.
- Reshape the browser host contract around leases and target-session creation
  rather than public Acquire/Release refcount methods. This keeps exactly-once
  release and target ownership in the abstraction that will support adopted
  browsers.

Adds targeted tests for the mode-aware default browser choice, browser host
identity normalization/default safety, lease idempotency, and the Chrome
shared-profile guard helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor tracked browser logs around shared browser hosts and per-target sessions so shared user-data mode can reuse an existing debug-enabled Chromium instance without ever closing the user's browser. Add endpoint metadata discovery, owned/adopted host ownership, target lifecycle monitoring, and stale metadata handling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add architecture comments for the shared/adopted browser pieces and target discovery. Surface browser host ownership and last-error diagnostics in resource state, preserve failed-session diagnostics, and log host/target/reconnect failures through resource logs. Harden endpoint metadata validation for malformed adoption hints.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refresh ATS code-generation snapshots after adding BrowserUserDataMode to WithBrowserLogs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

davidfowl and others added 4 commits April 25, 2026 15:16
Rename the per-tab browser logs session abstraction around pages, document the host/context/page model, and detect Chromium's Windows lockfile for non-debuggable shared profiles.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Track active registry lock users so BrowserHostRegistry can dispose its SemaphoreSlim without breaking late lease releases after registry disposal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

🎬 CLI E2E Test Recordings — 75 recordings uploaded (commit 960ef62)

View all recordings
Status Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_DefaultSelection_InstallsSkillOnly ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View Recording
CertificatesClean_RemovesCertificates ▶️ View Recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View Recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View Recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunEmptyAppHostProject ▶️ View Recording
CreateAndRunJavaEmptyAppHostProject ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateJavaAppHostWithViteApp ▶️ View Recording
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain ▶️ View Recording
DashboardRunWithOtelTracesReturnsNoTraces ▶️ View Recording
DeployK8sBasicApiService ▶️ View Recording
DeployK8sWithGarnet ▶️ View Recording
DeployK8sWithMongoDB ▶️ View Recording
DeployK8sWithMySql ▶️ View Recording
DeployK8sWithPostgres ▶️ View Recording
DeployK8sWithRabbitMQ ▶️ View Recording
DeployK8sWithRedis ▶️ View Recording
DeployK8sWithSqlServer ▶️ View Recording
DeployK8sWithValkey ▶️ View Recording
DeployTypeScriptAppToKubernetes ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance ▶️ View Recording
DoListStepsShowsPipelineSteps ▶️ View Recording
DocsCommand_RendersInteractiveMarkdownFromLocalSource ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View Recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View Recording
GlobalMigration_PreservesAllValueTypes ▶️ View Recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View Recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View Recording
InitTypeScriptAppHost_AugmentsExistingViteRepoAtRoot ▶️ View Recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View Recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
OtelLogsReturnsStructuredLogsFromStarterAppCore ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
PublishWithConfigureEnvFileUpdatesEnvOutput ▶️ View Recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View Recording
PublishWithoutOutputPathUsesAppHostDirectoryDefault ▶️ View Recording
RestoreGeneratesSdkFiles ▶️ View Recording
RestoreGeneratesSdkFiles_WithConfiguredToolchain ▶️ View Recording
RestoreRefreshesGeneratedSdkAfterAddingIntegration ▶️ View Recording
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes ▶️ View Recording
RunFromParentDirectory_UsesExistingConfigNearAppHost ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ▶️ View Recording
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording
UnAwaitedChainsCompileWithAutoResolvePromises ▶️ View Recording

📹 Recordings uploaded automatically from CI run #24943130688

@davidfowl davidfowl marked this pull request as ready for review April 26, 2026 00:09
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.

2 participants