Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cdddb18
IBiDi
nvborisenko Feb 21, 2026
40a888e
Interface in event args
nvborisenko Feb 21, 2026
7a33df8
Session
nvborisenko Feb 21, 2026
0e21970
EventDispatcher
nvborisenko Feb 22, 2026
fd4a896
Expose interface
nvborisenko Feb 22, 2026
5f0d776
BrowsingContext interfaces
nvborisenko Feb 22, 2026
c697469
Hide implementation
nvborisenko Feb 22, 2026
ed0a256
License headers
nvborisenko Feb 22, 2026
46912b3
Return BiDi.ConnectAsync() as public
nvborisenko Feb 22, 2026
dc9068d
Try to resolve circular deps
nvborisenko Feb 22, 2026
64222d0
Session end result
nvborisenko Feb 22, 2026
be4c11f
Unsubscribe twice issue, delegate event deserialization
nvborisenko Feb 22, 2026
915eab4
It is not JsonException, it is validation errors
nvborisenko Feb 22, 2026
3eb4844
Avoid massive usings
nvborisenko Feb 22, 2026
a0a6cbd
Remove unnecessary TryGetEventTypeInfo
nvborisenko Feb 22, 2026
5d32fec
Unsubscribe
nvborisenko Feb 22, 2026
87170d2
Deferred events deserialization
nvborisenko Feb 22, 2026
4524428
Ignore OperationCanceledException on dispose
nvborisenko Feb 22, 2026
97251c7
Only when expected
nvborisenko Feb 22, 2026
0091636
BiDi private ctor
nvborisenko Feb 22, 2026
07901f4
Cancel/await/dispose token source
nvborisenko Feb 22, 2026
8c9a519
Clean paramsReader
nvborisenko Feb 22, 2026
b5b5935
Dispose in ctor
nvborisenko Feb 22, 2026
6f844c7
Hide AsModule IDE
nvborisenko Feb 22, 2026
4be64ca
Merge remote-tracking branch 'upstream/trunk' into bidi-interface
nvborisenko Feb 22, 2026
a47cc97
Format
nvborisenko Feb 22, 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
61 changes: 34 additions & 27 deletions dotnet/src/webdriver/BiDi/BiDi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,65 +20,72 @@
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
using OpenQA.Selenium.BiDi.Browser;
using OpenQA.Selenium.BiDi.BrowsingContext;
using OpenQA.Selenium.BiDi.Emulation;
using OpenQA.Selenium.BiDi.Input;
using OpenQA.Selenium.BiDi.Json.Converters;
using OpenQA.Selenium.BiDi.Log;
using OpenQA.Selenium.BiDi.Network;
using OpenQA.Selenium.BiDi.Script;
using OpenQA.Selenium.BiDi.Session;
using OpenQA.Selenium.BiDi.Storage;
using OpenQA.Selenium.BiDi.WebExtension;

namespace OpenQA.Selenium.BiDi;

public sealed class BiDi : IAsyncDisposable
public sealed class BiDi : IBiDi
{
private readonly ConcurrentDictionary<Type, Module> _modules = new();

private BiDi(string url)
{
var uri = new Uri(url);
private Broker Broker { get; set; } = null!;

Comment thread
nvborisenko marked this conversation as resolved.
Broker = new Broker(this, uri);
}
internal ISessionModule Session => AsModule<SessionModule>();

private Broker Broker { get; }
public IBrowsingContextModule BrowsingContext => AsModule<BrowsingContextModule>();

internal Session.SessionModule SessionModule => AsModule<Session.SessionModule>();
public IBrowserModule Browser => AsModule<BrowserModule>();

public BrowsingContext.BrowsingContextModule BrowsingContext => AsModule<BrowsingContext.BrowsingContextModule>();
public INetworkModule Network => AsModule<NetworkModule>();

public Browser.BrowserModule Browser => AsModule<Browser.BrowserModule>();
public IInputModule Input => AsModule<InputModule>();

public Network.NetworkModule Network => AsModule<Network.NetworkModule>();
public IScriptModule Script => AsModule<ScriptModule>();

public Input.InputModule Input => AsModule<Input.InputModule>();
public ILogModule Log => AsModule<LogModule>();

public Script.ScriptModule Script => AsModule<Script.ScriptModule>();
public IStorageModule Storage => AsModule<StorageModule>();

public Log.LogModule Log => AsModule<Log.LogModule>();
public IWebExtensionModule WebExtension => AsModule<WebExtensionModule>();

public Storage.StorageModule Storage => AsModule<Storage.StorageModule>();
public IEmulationModule Emulation => AsModule<EmulationModule>();

public WebExtension.WebExtensionModule WebExtension => AsModule<WebExtension.WebExtensionModule>();
public static async Task<IBiDi> ConnectAsync(string url, BiDiOptions? options = null, CancellationToken cancellationToken = default)
Comment thread
nvborisenko marked this conversation as resolved.
{
Comment thread
nvborisenko marked this conversation as resolved.
var transport = new WebSocketTransport(new Uri(url));

public Emulation.EmulationModule Emulation => AsModule<Emulation.EmulationModule>();
await transport.ConnectAsync(cancellationToken).ConfigureAwait(false);

public static async Task<BiDi> ConnectAsync(string url, BiDiOptions? options = null, CancellationToken cancellationToken = default)
{
var bidi = new BiDi(url);
BiDi bidi = new();
Comment thread
nvborisenko marked this conversation as resolved.
Outdated

await bidi.Broker.ConnectAsync(cancellationToken).ConfigureAwait(false);
bidi.Broker = new Broker(transport, bidi, () => bidi.Session);
Comment thread
nvborisenko marked this conversation as resolved.

Comment thread
nvborisenko marked this conversation as resolved.
return bidi;
}

public Task<Session.StatusResult> StatusAsync(Session.StatusOptions? options = null, CancellationToken cancellationToken = default)
public Task<StatusResult> StatusAsync(StatusOptions? options = null, CancellationToken cancellationToken = default)
{
return SessionModule.StatusAsync(options, cancellationToken);
return Session.StatusAsync(options, cancellationToken);
}

public Task<Session.NewResult> NewAsync(Session.CapabilitiesRequest capabilities, Session.NewOptions? options = null, CancellationToken cancellationToken = default)
public Task<NewResult> NewAsync(CapabilitiesRequest capabilities, NewOptions? options = null, CancellationToken cancellationToken = default)
{
return SessionModule.NewAsync(capabilities, options, cancellationToken);
return Session.NewAsync(capabilities, options, cancellationToken);
}

public Task EndAsync(Session.EndOptions? options = null, CancellationToken cancellationToken = default)
public Task<EndResult> EndAsync(EndOptions? options = null, CancellationToken cancellationToken = default)
{
return SessionModule.EndAsync(options, cancellationToken);
return Session.EndAsync(options, cancellationToken);
}

public async ValueTask DisposeAsync()
Expand Down
169 changes: 49 additions & 120 deletions dotnet/src/webdriver/BiDi/Broker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Threading.Channels;
using OpenQA.Selenium.BiDi.Session;
using OpenQA.Selenium.Internal.Logging;

namespace OpenQA.Selenium.BiDi;
Expand All @@ -29,107 +29,33 @@ internal sealed class Broker : IAsyncDisposable
{
private readonly ILogger _logger = Internal.Logging.Log.GetLogger<Broker>();

private readonly BiDi _bidi;
private readonly ITransport _transport;
private readonly EventDispatcher _eventDispatcher;
private readonly IBiDi _bidi;

private readonly ConcurrentDictionary<long, CommandInfo> _pendingCommands = new();
private readonly Channel<EventInfo> _pendingEvents = Channel.CreateUnbounded<EventInfo>(new()
{
SingleReader = true,
SingleWriter = true
});
private readonly Dictionary<string, JsonTypeInfo> _eventTypesMap = [];

private readonly ConcurrentDictionary<string, List<EventHandler>> _eventHandlers = new();

private long _currentCommandId;

private static readonly TaskFactory _myTaskFactory = new(CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskContinuationOptions.None, TaskScheduler.Default);

private Task? _receivingMessageTask;
private Task? _eventEmitterTask;
private CancellationTokenSource? _receiveMessagesCancellationTokenSource;
private readonly Task _receivingMessageTask;
private readonly CancellationTokenSource _receiveMessagesCancellationTokenSource;

internal Broker(BiDi bidi, Uri url)
public Broker(ITransport transport, IBiDi bidi, Func<ISessionModule> sessionProvider)
{
_transport = transport;
_bidi = bidi;
_transport = new WebSocketTransport(url);
}

public async Task ConnectAsync(CancellationToken cancellationToken)
{
await _transport.ConnectAsync(cancellationToken).ConfigureAwait(false);
_eventDispatcher = new EventDispatcher(sessionProvider);

_receiveMessagesCancellationTokenSource = new CancellationTokenSource();
_receivingMessageTask = _myTaskFactory.StartNew(async () => await ReceiveMessagesAsync(_receiveMessagesCancellationTokenSource.Token), TaskCreationOptions.LongRunning).Unwrap();
_eventEmitterTask = _myTaskFactory.StartNew(ProcessEventsAwaiterAsync).Unwrap();
}

private async Task ReceiveMessagesAsync(CancellationToken cancellationToken)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
var data = await _transport.ReceiveAsync(cancellationToken).ConfigureAwait(false);

try
{
ProcessReceivedMessage(data);
}
catch (Exception ex)
{
if (_logger.IsEnabled(LogEventLevel.Error))
{
_logger.Error($"Unhandled error occurred while processing remote message: {ex}");
}
}
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
if (_logger.IsEnabled(LogEventLevel.Error))
{
_logger.Error($"Unhandled error occurred while receiving remote messages: {ex}");
}

throw;
}
}

private async Task ProcessEventsAwaiterAsync()
public Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo<TEventArgs> jsonTypeInfo, CancellationToken cancellationToken)
where TEventArgs : EventArgs
{
var reader = _pendingEvents.Reader;
while (await reader.WaitToReadAsync().ConfigureAwait(false))
{
while (reader.TryRead(out var result))
{
try
{
if (_eventHandlers.TryGetValue(result.Method, out var eventHandlers))
{
if (eventHandlers is not null)
{
foreach (var handler in eventHandlers.ToArray()) // copy handlers avoiding modified collection while iterating
{
var args = result.Params;

args.BiDi = _bidi;

await handler.InvokeAsync(args).ConfigureAwait(false);
}
}
}
}
catch (Exception ex)
{
if (_logger.IsEnabled(LogEventLevel.Error))
{
_logger.Error($"Unhandled error processing BiDi event handler: {ex}");
}
}
}
}
return _eventDispatcher.SubscribeAsync(eventName, eventHandler, options, jsonTypeInfo, cancellationToken);
}

public async Task<TResult> ExecuteCommandAsync<TCommand, TResult>(TCommand command, CommandOptions? options, JsonTypeInfo<TCommand> jsonCommandTypeInfo, JsonTypeInfo<TResult> jsonResultTypeInfo, CancellationToken cancellationToken)
Expand Down Expand Up @@ -157,39 +83,14 @@ public async Task<TResult> ExecuteCommandAsync<TCommand, TResult>(TCommand comma
return (TResult)await tcs.Task.ConfigureAwait(false);
Comment thread
nvborisenko marked this conversation as resolved.
}

public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo<TEventArgs> jsonTypeInfo, CancellationToken cancellationToken)
where TEventArgs : EventArgs
{
_eventTypesMap[eventName] = jsonTypeInfo;

var handlers = _eventHandlers.GetOrAdd(eventName, (a) => []);

var subscribeResult = await _bidi.SessionModule.SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false);

handlers.Add(eventHandler);

return new Subscription(subscribeResult.Subscription, this, eventHandler);
}

public async Task UnsubscribeAsync(Subscription subscription, CancellationToken cancellationToken)
{
var eventHandlers = _eventHandlers[subscription.EventHandler.EventName];

eventHandlers.Remove(subscription.EventHandler);

await _bidi.SessionModule.UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false);
}

public async ValueTask DisposeAsync()
{
_pendingEvents.Writer.Complete();
_receiveMessagesCancellationTokenSource.Cancel();
_receiveMessagesCancellationTokenSource.Dispose();

Comment thread
nvborisenko marked this conversation as resolved.
_receiveMessagesCancellationTokenSource?.Cancel();
await _eventDispatcher.DisposeAsync().ConfigureAwait(false);

if (_eventEmitterTask is not null)
{
await _eventEmitterTask.ConfigureAwait(false);
}
await _receivingMessageTask.ConfigureAwait(false);
Comment thread
nvborisenko marked this conversation as resolved.
Outdated
Comment thread
nvborisenko marked this conversation as resolved.
Outdated

_transport.Dispose();
Comment thread
nvborisenko marked this conversation as resolved.

Expand Down Expand Up @@ -284,13 +185,11 @@ private void ProcessReceivedMessage(byte[]? data)
case "event":
if (method is null) throw new JsonException("The remote end responded with 'event' message type, but missed required 'method' property.");

if (_eventTypesMap.TryGetValue(method, out var eventInfo))
if (_eventDispatcher.TryGetEventTypeInfo(method, out var eventInfo) && eventInfo is not null)
{
var eventArgs = (EventArgs)JsonSerializer.Deserialize(ref paramsReader, eventInfo)!;

eventArgs.BiDi = _bidi;

_pendingEvents.Writer.TryWrite(new EventInfo(method, eventArgs));
_eventDispatcher.EnqueueEvent(method, eventArgs, _bidi);
}
else
{
Expand All @@ -316,7 +215,37 @@ private void ProcessReceivedMessage(byte[]? data)
}
}

private readonly record struct CommandInfo(TaskCompletionSource<EmptyResult> TaskCompletionSource, JsonTypeInfo JsonResultTypeInfo);
private async Task ReceiveMessagesAsync(CancellationToken cancellationToken)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
var data = await _transport.ReceiveAsync(cancellationToken).ConfigureAwait(false);

try
{
ProcessReceivedMessage(data);
}
catch (Exception ex)
{
if (_logger.IsEnabled(LogEventLevel.Error))
{
_logger.Error($"Unhandled error occurred while processing remote message: {ex}");
}
}
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
if (_logger.IsEnabled(LogEventLevel.Error))
{
_logger.Error($"Unhandled error occurred while receiving remote messages: {ex}");
}

throw;
}
}

private readonly record struct EventInfo(string Method, EventArgs Params);
private readonly record struct CommandInfo(TaskCompletionSource<EmptyResult> TaskCompletionSource, JsonTypeInfo JsonResultTypeInfo);
}
4 changes: 2 additions & 2 deletions dotnet/src/webdriver/BiDi/Browser/BrowserModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

namespace OpenQA.Selenium.BiDi.Browser;

public sealed class BrowserModule : Module
public sealed class BrowserModule : Module, IBrowserModule
{
private BrowserJsonSerializerContext _jsonContext = null!;

Expand Down Expand Up @@ -77,7 +77,7 @@ public async Task<SetDownloadBehaviorResult> SetDownloadBehaviorDeniedAsync(SetD
return await ExecuteCommandAsync(new SetDownloadBehaviorCommand(@params), options, _jsonContext.SetDownloadBehaviorCommand, _jsonContext.SetDownloadBehaviorResult, cancellationToken).ConfigureAwait(false);
}

protected override void Initialize(BiDi bidi, JsonSerializerOptions jsonSerializerOptions)
protected override void Initialize(IBiDi bidi, JsonSerializerOptions jsonSerializerOptions)
{
jsonSerializerOptions.Converters.Add(new BrowserUserContextConverter(bidi));

Expand Down
32 changes: 32 additions & 0 deletions dotnet/src/webdriver/BiDi/Browser/IBrowserModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// <copyright file="IBrowserModule.cs" company="Selenium Committers">
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
// </copyright>

namespace OpenQA.Selenium.BiDi.Browser;

public interface IBrowserModule
{
Task<CloseResult> CloseAsync(CloseOptions? options = null, CancellationToken cancellationToken = default);
Task<CreateUserContextResult> CreateUserContextAsync(CreateUserContextOptions? options = null, CancellationToken cancellationToken = default);
Task<GetClientWindowsResult> GetClientWindowsAsync(GetClientWindowsOptions? options = null, CancellationToken cancellationToken = default);
Task<GetUserContextsResult> GetUserContextsAsync(GetUserContextsOptions? options = null, CancellationToken cancellationToken = default);
Task<RemoveUserContextResult> RemoveUserContextAsync(UserContext userContext, RemoveUserContextOptions? options = null, CancellationToken cancellationToken = default);
Task<SetDownloadBehaviorResult> SetDownloadBehaviorAllowedAsync(string destinationFolder, SetDownloadBehaviorOptions? options = null, CancellationToken cancellationToken = default);
Task<SetDownloadBehaviorResult> SetDownloadBehaviorAllowedAsync(SetDownloadBehaviorOptions? options = null, CancellationToken cancellationToken = default);
Task<SetDownloadBehaviorResult> SetDownloadBehaviorDeniedAsync(SetDownloadBehaviorOptions? options = null, CancellationToken cancellationToken = default);
}
6 changes: 3 additions & 3 deletions dotnet/src/webdriver/BiDi/Browser/UserContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ namespace OpenQA.Selenium.BiDi.Browser;

public sealed record UserContext
{
public UserContext(BiDi bidi, string id)
public UserContext(IBiDi bidi, string id)
: this(id)
{
BiDi = bidi ?? throw new ArgumentNullException(nameof(bidi));
Expand All @@ -38,10 +38,10 @@ internal UserContext(string id)

internal string Id { get; }

private BiDi? _bidi;
private IBiDi? _bidi;

[JsonIgnore]
public BiDi BiDi
public IBiDi BiDi
{
get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated.");
internal set => _bidi = value;
Expand Down
Loading
Loading