Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
016dbfb
Add BrowserUserDataMode to separate user data dir from profile
davidfowl Apr 25, 2026
1cdce4e
Make Shared the default user data mode and document singleton caveat
davidfowl Apr 25, 2026
b00e33a
Add Target lifecycle and Inspector.detached protocol parsing
davidfowl Apr 25, 2026
1fb3f58
Add IBrowserHost abstraction with ownership distinction
davidfowl Apr 25, 2026
71f2f07
Apply review feedback to BrowserHostIdentity and document Target even…
davidfowl Apr 25, 2026
e5d4fc4
Harden browser logs foundation for shared browser adoption
davidfowl Apr 25, 2026
e6a1f56
Support shared browser host adoption for browser logs
davidfowl Apr 25, 2026
bbcaa81
Improve browser logs diagnostics
davidfowl Apr 25, 2026
11d5f9b
Update browser logs codegen snapshots
davidfowl Apr 25, 2026
64169bf
Add browser logs component tests
davidfowl Apr 25, 2026
ac96567
Reuse browser endpoint probe HttpClient
davidfowl Apr 25, 2026
ef381b0
Use typed browser endpoint probe response
davidfowl Apr 25, 2026
3f259b1
Remove redundant adopted browser disposal guard
davidfowl Apr 25, 2026
fba86ac
Document browser host registry decisions
davidfowl Apr 25, 2026
ba981ec
Add browser observation notes to registry comments
davidfowl Apr 25, 2026
46edb61
Document browser log runtime behavior
davidfowl Apr 25, 2026
07c9794
Name browser logs timeout values
davidfowl Apr 25, 2026
c812643
Rename browser logs CDP transport types
davidfowl Apr 25, 2026
adb4bbe
Document browser endpoint discovery flow
davidfowl Apr 25, 2026
8b664f4
Link Chromium singleton documentation
davidfowl Apr 25, 2026
57baf51
Clarify browser page session model
davidfowl Apr 25, 2026
ef4423d
Dispose browser host registry lock
davidfowl Apr 25, 2026
cb4bada
Move browser logs configuration resolution
davidfowl Apr 25, 2026
960ef62
Clean up browser configuration state
davidfowl Apr 25, 2026
d39fc3a
Move Chromium browser resolution helpers
davidfowl Apr 26, 2026
5a50201
Document browser logs JSON formats
davidfowl Apr 26, 2026
57cea6f
Localize browser logs failure messages
davidfowl Apr 26, 2026
ce82bc8
Move browser launch helpers out of running session
davidfowl Apr 26, 2026
37573bb
Clean up browser logs helper ownership
davidfowl Apr 26, 2026
7de3f07
Address browser logs review feedback
davidfowl Apr 26, 2026
16bf244
Add browser logs baseline coverage
davidfowl Apr 26, 2026
92ae583
Test browser logs CDP connection
davidfowl Apr 26, 2026
1e0574b
Test browser logs running session glue
davidfowl Apr 26, 2026
faa9cbb
Test browser page session reconnect
davidfowl Apr 26, 2026
2a0694b
Redefine Shared/Isolated as Aspire-managed persistent profiles
davidfowl Apr 27, 2026
fb3fd5c
Address PR review feedback
davidfowl Apr 27, 2026
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
150 changes: 150 additions & 0 deletions src/Aspire.Hosting/BrowserLogs/BrowserConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.Configuration;

namespace Aspire.Hosting;

/// <summary>
/// Selects the Chromium user data directory used by tracked browser sessions.
/// </summary>
public enum BrowserUserDataMode
Comment thread
davidfowl marked this conversation as resolved.
Outdated
{
/// <summary>
/// Use the browser's real user data directory so the tracked session behaves like a persistent browser context
/// with real cookies, sessions, extensions, and profile selection.
/// </summary>
/// <remarks>
/// NOTE: Aspire can adopt a shared browser only when it previously launched that browser with remote debugging
/// enabled. If a normal non-debuggable browser is already using the selected user data directory, the tracked
/// session fails with guidance instead of opening a second browser against the same profile store. Google Chrome
/// also blocks remote debugging against its default user data directory; use Microsoft Edge or <see cref="Isolated"/>
/// mode when Chrome is selected.
/// </remarks>
Shared,

/// <summary>
/// Launch the tracked browser against a temporary user data directory, like a disposable persistent browser
/// context, so the session starts from clean state and does not affect the user's normal browser profiles.
/// </summary>
Isolated,
}

/// <summary>
/// Resolved browser configuration used for one tracked browser session.
/// </summary>
/// <remarks>
/// Resolution keeps "which browser/profile did the caller ask for?" separate from "which user data directory
/// does that imply?". The later user-data-directory decision belongs to <see cref="BrowserHostRegistry"/>, where
/// the resolved browser executable path is available.
/// </remarks>
internal readonly record struct BrowserConfiguration(string Browser, string? Profile, BrowserUserDataMode UserDataMode)
{
/// <summary>
/// The default mode matches a normal browser launch by using the browser's real user data directory.
/// </summary>
internal const BrowserUserDataMode DefaultUserDataMode = BrowserUserDataMode.Shared;

/// <summary>
/// Resolves explicit method arguments, resource-scoped configuration, global configuration, and defaults.
/// </summary>
internal static BrowserConfiguration Resolve(
IConfiguration configuration,
string resourceName,
BrowserConfigurationOverrides overrides)
{
ArgumentNullException.ThrowIfNull(configuration);
ArgumentException.ThrowIfNullOrWhiteSpace(resourceName);

var browserLogsSection = configuration.GetSection(BrowserLogsBuilderExtensions.BrowserLogsConfigurationSectionName);
var resourceSection = browserLogsSection.GetSection(resourceName);

// Resolution order is explicit argument -> resource-specific config -> global browser-log config -> default.
// Resolve user-data mode before browser so the browser default can prefer Edge for shared state and Chrome for
// disposable isolated state.
var resolvedProfile = overrides.Profile
?? resourceSection[BrowserLogsBuilderExtensions.ProfileConfigurationKey]
?? browserLogsSection[BrowserLogsBuilderExtensions.ProfileConfigurationKey];
var resolvedUserDataMode = overrides.UserDataMode
?? ParseUserDataMode(resourceSection[BrowserLogsBuilderExtensions.UserDataModeConfigurationKey])
?? ParseUserDataMode(browserLogsSection[BrowserLogsBuilderExtensions.UserDataModeConfigurationKey])
?? DefaultUserDataMode;
var resolvedBrowser = overrides.Browser
?? resourceSection[BrowserLogsBuilderExtensions.BrowserConfigurationKey]
?? browserLogsSection[BrowserLogsBuilderExtensions.BrowserConfigurationKey]
?? GetDefaultBrowser(resolvedUserDataMode);

if (string.IsNullOrWhiteSpace(resolvedBrowser))
{
throw new InvalidOperationException("Tracked browser configuration resolved an empty browser value.");
}

if (resolvedProfile is not null && string.IsNullOrWhiteSpace(resolvedProfile))
{
throw new InvalidOperationException("Tracked browser configuration resolved an empty profile value.");
}

if (resolvedUserDataMode == BrowserUserDataMode.Isolated && resolvedProfile is not null)
{
throw new InvalidOperationException(
$"Tracked browser configuration set '{BrowserLogsBuilderExtensions.ProfileConfigurationKey}' to '{resolvedProfile}' while '{BrowserLogsBuilderExtensions.UserDataModeConfigurationKey}' is '{BrowserUserDataMode.Isolated}'. " +
$"Profiles can only be selected when '{BrowserLogsBuilderExtensions.UserDataModeConfigurationKey}' is '{BrowserUserDataMode.Shared}'.");
}

return new BrowserConfiguration(resolvedBrowser, resolvedProfile, resolvedUserDataMode);
}

/// <summary>
/// Selects the default browser for the default user data mode.
/// </summary>
internal static string GetDefaultBrowser(Func<string, string?> resolveBrowserExecutable) =>
GetDefaultBrowser(DefaultUserDataMode, resolveBrowserExecutable);

/// <summary>
/// Selects the default browser for the effective user data mode.
/// </summary>
internal static string GetDefaultBrowser(BrowserUserDataMode userDataMode, Func<string, string?> resolveBrowserExecutable)
{
if (userDataMode == BrowserUserDataMode.Shared &&
resolveBrowserExecutable("msedge") is not null)
{
return "msedge";
}

if (resolveBrowserExecutable("chrome") is not null)
{
return "chrome";
}

if (resolveBrowserExecutable("msedge") is not null)
{
return "msedge";
}

return "chrome";
}

private static BrowserUserDataMode? ParseUserDataMode(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}

if (Enum.TryParse<BrowserUserDataMode>(value, ignoreCase: true, out var parsed))
{
return parsed;
}

throw new InvalidOperationException(
$"Tracked browser configuration value '{value}' is not a valid '{BrowserLogsBuilderExtensions.UserDataModeConfigurationKey}'. Expected '{BrowserUserDataMode.Shared}' or '{BrowserUserDataMode.Isolated}'.");
}

private static string GetDefaultBrowser(BrowserUserDataMode userDataMode) =>
GetDefaultBrowser(userDataMode, BrowserLogsRunningSession.TryResolveBrowserExecutable);
}

/// <summary>
/// Explicit browser configuration values supplied by the resource builder.
/// </summary>
internal readonly record struct BrowserConfigurationOverrides(string? Browser, string? Profile, BrowserUserDataMode? UserDataMode);
Loading
Loading