Skip to content
Draft
Show file tree
Hide file tree
Changes from 13 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
76 changes: 57 additions & 19 deletions dotnet/src/webdriver/BiDi/BiDi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@
// under the License.
// </copyright>

using OpenQA.Selenium.BiDi.Communication;
using OpenQA.Selenium.BiDi.Communication.Json;
using OpenQA.Selenium.BiDi.Communication.Json.Converters;
using System;
using System.Threading.Tasks;
using OpenQA.Selenium.BiDi.Communication;

namespace OpenQA.Selenium.BiDi;

public class BiDi : IAsyncDisposable
{
private readonly Broker _broker;
private readonly bool _ownsConnection;
protected BiDiConnection BiDiConnection { get; }

private readonly Lazy<Modules.Session.SessionModule> _sessionModule;
private readonly Lazy<Modules.BrowsingContext.BrowsingContextModule> _browsingContextModule;
private readonly Lazy<Modules.Browser.BrowserModule> _browserModule;
private readonly Lazy<Modules.Network.NetworkModule> _networkModule;
Expand All @@ -36,23 +38,49 @@ public class BiDi : IAsyncDisposable
private readonly Lazy<Modules.Log.LogModule> _logModule;
private readonly Lazy<Modules.Storage.StorageModule> _storageModule;

internal BiDi(string url)
private BiDi(BiDiConnection connection) : this()
{
_ownsConnection = false;
BiDiConnection = connection;
AddBiDiModuleJsonInfo(connection);
}

private BiDi(string url) : this()
{
_ownsConnection = true;
BiDiConnection = new BiDiConnection(new Uri(url));
AddBiDiModuleJsonInfo(BiDiConnection);
}

private BiDi()
{
var uri = new Uri(url);

_broker = new Broker(this, uri);

_sessionModule = new Lazy<Modules.Session.SessionModule>(() => new Modules.Session.SessionModule(_broker));
_browsingContextModule = new Lazy<Modules.BrowsingContext.BrowsingContextModule>(() => new Modules.BrowsingContext.BrowsingContextModule(_broker));
_browserModule = new Lazy<Modules.Browser.BrowserModule>(() => new Modules.Browser.BrowserModule(_broker));
_networkModule = new Lazy<Modules.Network.NetworkModule>(() => new Modules.Network.NetworkModule(_broker));
_inputModule = new Lazy<Modules.Input.InputModule>(() => new Modules.Input.InputModule(_broker));
_scriptModule = new Lazy<Modules.Script.ScriptModule>(() => new Modules.Script.ScriptModule(_broker));
_logModule = new Lazy<Modules.Log.LogModule>(() => new Modules.Log.LogModule(_broker));
_storageModule = new Lazy<Modules.Storage.StorageModule>(() => new Modules.Storage.StorageModule(_broker));
_browsingContextModule = new Lazy<Modules.BrowsingContext.BrowsingContextModule>(() => new Modules.BrowsingContext.BrowsingContextModule(BiDiConnection));
_browserModule = new Lazy<Modules.Browser.BrowserModule>(() => new Modules.Browser.BrowserModule(BiDiConnection));
_networkModule = new Lazy<Modules.Network.NetworkModule>(() => new Modules.Network.NetworkModule(BiDiConnection));
_inputModule = new Lazy<Modules.Input.InputModule>(() => new Modules.Input.InputModule(BiDiConnection));
_scriptModule = new Lazy<Modules.Script.ScriptModule>(() => new Modules.Script.ScriptModule(BiDiConnection));
_logModule = new Lazy<Modules.Log.LogModule>(() => new Modules.Log.LogModule(BiDiConnection));
_storageModule = new Lazy<Modules.Storage.StorageModule>(() => new Modules.Storage.StorageModule(BiDiConnection));
}

internal Modules.Session.SessionModule SessionModule => _sessionModule.Value;
private BiDiConnection AddBiDiModuleJsonInfo(BiDiConnection connection)
{
connection.AddSerializerContextAndConverters(BiDiJsonSerializerContext.Default,
[
new BrowsingContextConverter(this),
new BrowserUserContextConverter(this),
new InterceptConverter(this),
new RequestConverter(this),
new HandleConverter(this),
new InternalIdConverter(this),
new PreloadScriptConverter(this),
new RealmConverter(this),
]);

return connection;
}

internal Modules.Session.SessionModule SessionModule => BiDiConnection.SessionModule;
public Modules.BrowsingContext.BrowsingContextModule BrowsingContext => _browsingContextModule.Value;
public Modules.Browser.BrowserModule Browser => _browserModule.Value;
public Modules.Network.NetworkModule Network => _networkModule.Value;
Expand All @@ -70,11 +98,18 @@ public static async Task<BiDi> ConnectAsync(string url)
{
var bidi = new BiDi(url);

await bidi._broker.ConnectAsync(default).ConfigureAwait(false);
await bidi.BiDiConnection.ConnectAsync(default).ConfigureAwait(false);

return bidi;
}

public static ValueTask<BiDi> AttachAsync(BiDiConnection connection)
{
var bidi = new BiDi(connection);

return new ValueTask<BiDi>(bidi);
}

public Task EndAsync(Modules.Session.EndOptions? options = null)
{
return SessionModule.EndAsync(options);
Expand All @@ -88,6 +123,9 @@ public async ValueTask DisposeAsync()

protected virtual async ValueTask DisposeAsyncCore()
{
await _broker.DisposeAsync().ConfigureAwait(false);
if (_ownsConnection)
{
await BiDiConnection.DisposeAsync().ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <copyright file="Broker.cs" company="Selenium Committers">
// <copyright file="BiDiConnection.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
Expand All @@ -18,25 +18,24 @@
// </copyright>

using OpenQA.Selenium.BiDi.Communication.Json;
using OpenQA.Selenium.BiDi.Communication.Json.Converters;
using OpenQA.Selenium.BiDi.Communication.Transport;
using OpenQA.Selenium.Internal.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;

namespace OpenQA.Selenium.BiDi.Communication;

public class Broker : IAsyncDisposable
public class BiDiConnection : IAsyncDisposable
{
private readonly ILogger _logger = Log.GetLogger<Broker>();

private readonly BiDi _bidi;
private readonly ILogger _logger = Log.GetLogger<BiDiConnection>();
private readonly ITransport _transport;

private readonly ConcurrentDictionary<int, TaskCompletionSource<JsonElement>> _pendingCommands = new();
Expand All @@ -52,61 +51,42 @@ public class Broker : IAsyncDisposable
private Task? _eventEmitterTask;
private CancellationTokenSource? _receiveMessagesCancellationTokenSource;

private readonly BiDiJsonSerializerContext _jsonSerializerContext;
private readonly JsonSerializerOptions _jsonSerializerContext;
private readonly Lazy<Modules.Session.SessionModule> _sessionModule;

internal Modules.Session.SessionModule SessionModule => _sessionModule.Value;

internal Broker(BiDi bidi, Uri url)
public BiDiConnection(Uri url)
{
_bidi = bidi;
_transport = new WebSocketTransport(url);
_jsonSerializerContext = BiDiConnectionJsonSerializerContext.CreateOptions();
_sessionModule = new Lazy<Modules.Session.SessionModule>(() => new Modules.Session.SessionModule(this));
}

var jsonSerializerOptions = new JsonSerializerOptions
[RequiresUnreferencedCode("Enables reflection-based JSON serialization. Use a source-generated JsonSerializerContext for AOT safety.")]
[RequiresDynamicCode("Enables reflection-based JSON serialization. Use a source-generated JsonSerializerContext for AOT safety.")]
public void EnableReflectionBasedJson()
{
if (_jsonSerializerContext.IsReadOnly)
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,

// BiDi returns special numbers such as "NaN" as strings
// Additionally, -0 is returned as a string "-0"
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals | JsonNumberHandling.AllowReadingFromString,
Converters =
{
new BrowsingContextConverter(_bidi),
new BrowserUserContextConverter(bidi),
new BrowserClientWindowConverter(),
new NavigationConverter(),
new InterceptConverter(_bidi),
new RequestConverter(_bidi),
new ChannelConverter(),
new HandleConverter(_bidi),
new InternalIdConverter(_bidi),
new PreloadScriptConverter(_bidi),
new RealmConverter(_bidi),
new RealmTypeConverter(),
new DateTimeOffsetConverter(),
new PrintPageRangeConverter(),
new InputOriginConverter(),
new SubscriptionConverter(),
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),

// https://github.com/dotnet/runtime/issues/72604
new Json.Converters.Polymorphic.MessageConverter(),
new Json.Converters.Polymorphic.EvaluateResultConverter(),
new Json.Converters.Polymorphic.RemoteValueConverter(),
new Json.Converters.Polymorphic.RealmInfoConverter(),
new Json.Converters.Polymorphic.LogEntryConverter(),
//

// Enumerable
new Json.Converters.Enumerable.GetCookiesResultConverter(),
new Json.Converters.Enumerable.LocateNodesResultConverter(),
new Json.Converters.Enumerable.InputSourceActionsConverter(),
new Json.Converters.Enumerable.GetUserContextsResultConverter(),
new Json.Converters.Enumerable.GetClientWindowsResultConverter(),
new Json.Converters.Enumerable.GetRealmsResultConverter(),
}
};
throw new InvalidOperationException("Cannot add JSON serializer context after ConnectAsync has been called");
}

_jsonSerializerContext.TypeInfoResolverChain.Add(new DefaultJsonTypeInfoResolver());
}

public void AddSerializerContextAndConverters(JsonSerializerContext context, IList<JsonConverter>? converters = null)
{
if (_jsonSerializerContext.IsReadOnly)
{
throw new InvalidOperationException("Cannot add JSON serializer context after ConnectAsync has been called");
}

_jsonSerializerContext = new BiDiJsonSerializerContext(jsonSerializerOptions);
_jsonSerializerContext.TypeInfoResolverChain.Add(context);
foreach (JsonConverter converter in converters ?? [])
{
_jsonSerializerContext.Converters.Add(converter);
}
}

public async Task ConnectAsync(CancellationToken cancellationToken)
Expand All @@ -124,21 +104,32 @@ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken)
{
var data = await _transport.ReceiveAsync(cancellationToken).ConfigureAwait(false);

var message = JsonSerializer.Deserialize(new ReadOnlySpan<byte>(data), _jsonSerializerContext.Message);
var messageTypeInfo = (JsonTypeInfo<Message>)_jsonSerializerContext.GetTypeInfo(typeof(Message));
var message = JsonSerializer.Deserialize(new ReadOnlySpan<byte>(data), messageTypeInfo);

switch (message)
{
case MessageSuccess messageSuccess:
_pendingCommands[messageSuccess.Id].SetResult(messageSuccess.Result);
_pendingCommands.TryRemove(messageSuccess.Id, out _);
break;

case MessageEvent messageEvent:
_pendingEvents.Add(messageEvent);
break;

case MessageError mesageError:
_pendingCommands[mesageError.Id].SetException(new BiDiException($"{mesageError.Error}: {mesageError.Message}"));
_pendingCommands.TryRemove(mesageError.Id, out _);
break;

default:
if (_logger.IsEnabled(LogEventLevel.Warn))
{
_logger.Warn($"Received invalid message type: {message}");
}

break;
}
}
}
Expand All @@ -157,7 +148,7 @@ private async Task ProcessEventsAwaiterAsync()
{
var args = (EventArgs)result.Params.Deserialize(handler.EventArgsType, _jsonSerializerContext)!;

args.BiDi = _bidi;
args.BiDi = this;

// handle browsing context subscriber
if (handler.Contexts is not null && args is BrowsingContextEventArgs browsingContextEventArgs && handler.Contexts.Contains(browsingContextEventArgs.Context))
Expand Down Expand Up @@ -226,7 +217,7 @@ public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, Act

if (options is BrowsingContextsSubscriptionOptions browsingContextsOptions)
{
var subscribeResult = await _bidi.SessionModule.SubscribeAsync([eventName], new() { Contexts = browsingContextsOptions.Contexts }).ConfigureAwait(false);
var subscribeResult = await SessionModule.SubscribeAsync([eventName], new() { Contexts = browsingContextsOptions.Contexts }).ConfigureAwait(false);

var eventHandler = new SyncEventHandler<TEventArgs>(eventName, action, browsingContextsOptions?.Contexts);

Expand All @@ -236,7 +227,7 @@ public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, Act
}
else
{
var subscribeResult = await _bidi.SessionModule.SubscribeAsync([eventName]).ConfigureAwait(false);
var subscribeResult = await SessionModule.SubscribeAsync([eventName]).ConfigureAwait(false);

var eventHandler = new SyncEventHandler<TEventArgs>(eventName, action);

Expand All @@ -253,7 +244,7 @@ public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, Fun

if (options is BrowsingContextsSubscriptionOptions browsingContextsOptions)
{
var subscribeResult = await _bidi.SessionModule.SubscribeAsync([eventName], new() { Contexts = browsingContextsOptions.Contexts }).ConfigureAwait(false);
var subscribeResult = await SessionModule.SubscribeAsync([eventName], new() { Contexts = browsingContextsOptions.Contexts }).ConfigureAwait(false);

var eventHandler = new AsyncEventHandler<TEventArgs>(eventName, func, browsingContextsOptions.Contexts);

Expand All @@ -263,7 +254,7 @@ public async Task<Subscription> SubscribeAsync<TEventArgs>(string eventName, Fun
}
else
{
var subscribeResult = await _bidi.SessionModule.SubscribeAsync([eventName]).ConfigureAwait(false);
var subscribeResult = await SessionModule.SubscribeAsync([eventName]).ConfigureAwait(false);

var eventHandler = new AsyncEventHandler<TEventArgs>(eventName, func);

Expand All @@ -281,22 +272,22 @@ public async Task UnsubscribeAsync(Modules.Session.Subscription subscription, Ev

if (subscription is not null)
{
await _bidi.SessionModule.UnsubscribeAsync([subscription]).ConfigureAwait(false);
await SessionModule.UnsubscribeAsync([subscription]).ConfigureAwait(false);
}
else
{
if (eventHandler.Contexts is not null)
{
if (!eventHandlers.Any(h => eventHandler.Contexts.Equals(h.Contexts)) && !eventHandlers.Any(h => h.Contexts is null))
{
await _bidi.SessionModule.UnsubscribeAsync([eventHandler.EventName], new() { Contexts = eventHandler.Contexts }).ConfigureAwait(false);
await SessionModule.UnsubscribeAsync([eventHandler.EventName], new() { Contexts = eventHandler.Contexts }).ConfigureAwait(false);
}
}
else
{
if (!eventHandlers.Any(h => h.Contexts is not null) && !eventHandlers.Any(h => h.Contexts is null))
{
await _bidi.SessionModule.UnsubscribeAsync([eventHandler.EventName]).ConfigureAwait(false);
await SessionModule.UnsubscribeAsync([eventHandler.EventName]).ConfigureAwait(false);
}
}
}
Expand Down
Loading
Loading