Skip to content

Commit

Permalink
[dotnet] Added function to enable halting targets until runtime.runIf…
Browse files Browse the repository at this point in the history
…WaitingForDebugger is invoked (#13330)

* Use .GetType() instead of typeof() for determining the response type of a command. typeof() determines the type compile time.

* Duplicated TryGetCommandResponseType function

* Added function to DevToolsSession.cs to enable waiting for new targets until runtime.runIfWaitingForDebugger is invoked.

* Fix copy paste issue

* Moved WaitForDebuggerOnStart function to DevToolsOptions class.

* Refactored DevToolsOptions usage to remove redundant functions from interface. Marked old functions with only protocol version as obsolete.

* Refactor to remove duplicate code for auto detecting ProtocolVersion.

* Fixed comments.

---------

Co-authored-by: Nikolay Borisenko <[email protected]>
  • Loading branch information
EdwinVanVliet and nvborisenko authored Jan 9, 2024
1 parent 4b6af2e commit f7fd6d3
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 23 deletions.
28 changes: 19 additions & 9 deletions dotnet/src/webdriver/Chromium/ChromiumDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -273,15 +273,14 @@ public object ExecuteCdpCommand(string commandName, Dictionary<string, object> c
/// <returns>The active session to use to communicate with the Chromium Developer Tools debugging protocol.</returns>
public DevToolsSession GetDevToolsSession()
{
return GetDevToolsSession(DevToolsSession.AutoDetectDevToolsProtocolVersion);
return GetDevToolsSession(new DevToolsOptions() { ProtocolVersion = DevToolsSession.AutoDetectDevToolsProtocolVersion });
}

/// <summary>
/// Creates a session to communicate with a browser using the Chromium Developer Tools debugging protocol.
/// </summary>
/// <param name="devToolsProtocolVersion">The version of the Chromium Developer Tools protocol to use. Defaults to autodetect the protocol version.</param>
/// <returns>The active session to use to communicate with the Chromium Developer Tools debugging protocol.</returns>
public DevToolsSession GetDevToolsSession(int devToolsProtocolVersion)
public DevToolsSession GetDevToolsSession(DevToolsOptions options)
{
if (this.devToolsSession == null)
{
Expand All @@ -290,22 +289,22 @@ public DevToolsSession GetDevToolsSession(int devToolsProtocolVersion)
throw new WebDriverException("Cannot find " + this.optionsCapabilityName + " capability for driver");
}

Dictionary<string, object> options = this.Capabilities.GetCapability(this.optionsCapabilityName) as Dictionary<string, object>;
if (options == null)
Dictionary<string, object> optionsCapability = this.Capabilities.GetCapability(this.optionsCapabilityName) as Dictionary<string, object>;
if (optionsCapability == null)
{
throw new WebDriverException("Found " + this.optionsCapabilityName + " capability, but is not an object");
}

if (!options.ContainsKey("debuggerAddress"))
if (!optionsCapability.ContainsKey("debuggerAddress"))
{
throw new WebDriverException("Did not find debuggerAddress capability in " + this.optionsCapabilityName);
}

string debuggerAddress = options["debuggerAddress"].ToString();
string debuggerAddress = optionsCapability["debuggerAddress"].ToString();
try
{
DevToolsSession session = new DevToolsSession(debuggerAddress);
Task.Run(async () => await session.StartSession(devToolsProtocolVersion)).GetAwaiter().GetResult();
DevToolsSession session = new DevToolsSession(debuggerAddress, options);
Task.Run(async () => await session.StartSession()).GetAwaiter().GetResult();
this.devToolsSession = session;
}
catch (Exception e)
Expand All @@ -317,6 +316,17 @@ public DevToolsSession GetDevToolsSession(int devToolsProtocolVersion)
return this.devToolsSession;
}

/// <summary>
/// Creates a session to communicate with a browser using the Chromium Developer Tools debugging protocol.
/// </summary>
/// <param name="devToolsProtocolVersion">The version of the Chromium Developer Tools protocol to use. Defaults to autodetect the protocol version.</param>
/// <returns>The active session to use to communicate with the Chromium Developer Tools debugging protocol.</returns>
[Obsolete("Use GetDevToolsSession(DevToolsOptions options)")]
public DevToolsSession GetDevToolsSession(int devToolsProtocolVersion)
{
return GetDevToolsSession(new DevToolsOptions() { ProtocolVersion = devToolsProtocolVersion });
}

/// <summary>
/// Closes a DevTools session.
/// </summary>
Expand Down
11 changes: 11 additions & 0 deletions dotnet/src/webdriver/DevTools/CommandResponseTypeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,16 @@ public bool TryGetCommandResponseType<T>(out Type commandResponseType)
{
return commandResponseTypeDictionary.TryGetValue(typeof(T), out commandResponseType);
}

/// <summary>
/// Gets the command response type corresponding to the specified command type.
/// </summary>
/// <param name="command">The type of command for which to retrieve the response type.</param>
/// <param name="commandResponseType">The returned response type.</param>
/// <returns><see langword="true"/> if the specified command type has a mapped response type; otherwise, <see langword="false"/>.</returns>
public bool TryGetCommandResponseType(ICommand command, out Type commandResponseType)
{
return commandResponseTypeDictionary.TryGetValue(command.GetType(), out commandResponseType);
}
}
}
37 changes: 37 additions & 0 deletions dotnet/src/webdriver/DevTools/DevToolsOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// <copyright file="DevToolsOptions.cs" company="WebDriver 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.DevTools
{
/// <summary>
/// Contains options configuring the DevTools session.
/// </summary>
public class DevToolsOptions
{
/// <summary>
/// Enables or disables waiting for the debugger when creating a new target. By default WaitForDebuggerOnStart is disabled.
/// If enabled, all targets will be halted until the runtime.runIfWaitingForDebugger is invoked.
/// </summary>
public bool WaitForDebuggerOnStart { get; set; }
/// <summary>
/// The specific version of the Developer Tools debugging protocol to use.
/// If left NULL the protocol version will be determined automatically based on the browser version.
/// </summary>
public int? ProtocolVersion { get; set; }
}
}
80 changes: 75 additions & 5 deletions dotnet/src/webdriver/DevTools/DevToolsSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,29 @@ public class DevToolsSession : IDevToolsSession
private long currentCommandId = 0;

private DevToolsDomains domains;
private readonly DevToolsOptions options;

/// <summary>
/// Initializes a new instance of the DevToolsSession class, using the specified WebSocket endpoint.
/// </summary>
/// <param name="endpointAddress"></param>
public DevToolsSession(string endpointAddress)
[Obsolete("Use DevToolsSession(string endpointAddress, DevToolsOptions options)")]
public DevToolsSession(string endpointAddress) : this(endpointAddress, new DevToolsOptions()) { }

/// <summary>
/// Initializes a new instance of the DevToolsSession class, using the specified WebSocket endpoint and specified DevTools options.
/// </summary>
/// <param name="endpointAddress"></param>
/// <param name="options"></param>
/// <exception cref="ArgumentNullException"></exception>
public DevToolsSession(string endpointAddress, DevToolsOptions options)
{
if (string.IsNullOrWhiteSpace(endpointAddress))
{
throw new ArgumentNullException(nameof(endpointAddress));
}

this.options = options ?? throw new ArgumentNullException(nameof(options));
this.CommandTimeout = TimeSpan.FromSeconds(30);
this.debuggerEndpoint = endpointAddress;
if (endpointAddress.StartsWith("ws", StringComparison.InvariantCultureIgnoreCase))
Expand Down Expand Up @@ -149,6 +160,39 @@ public T GetVersionSpecificDomains<T>() where T : DevToolsSessionDomains
}

if (!this.domains.VersionSpecificDomains.ResponseTypeMap.TryGetCommandResponseType<TCommand>(out Type commandResponseType))
{
throw new InvalidOperationException($"Type {command.GetType()} does not correspond to a known command response type.");
}

return result.ToObject(commandResponseType) as ICommandResponse<TCommand>;
}

/// <summary>
/// Sends the specified command and returns the associated command response.
/// </summary>
/// <typeparam name="TCommand">A command object implementing the <see cref="ICommand"/> interface.</typeparam>
/// <param name="command">The command to be sent.</param>
/// <param name="sessionId">The target session of the command</param>
/// <param name="cancellationToken">A CancellationToken object to allow for cancellation of the command.</param>
/// <param name="millisecondsTimeout">The execution timeout of the command in milliseconds.</param>
/// <param name="throwExceptionIfResponseNotReceived"><see langword="true"/> to throw an exception if a response is not received; otherwise, <see langword="false"/>.</param>
/// <returns>The command response object implementing the <see cref="ICommandResponse{T}"/> interface.</returns>
public async Task<ICommandResponse<TCommand>> SendCommand<TCommand>(TCommand command, string sessionId, CancellationToken cancellationToken = default(CancellationToken), int? millisecondsTimeout = null, bool throwExceptionIfResponseNotReceived = true)
where TCommand : ICommand
{
if (command == null)
{
throw new ArgumentNullException(nameof(command));
}

var result = await SendCommand(command.CommandName, sessionId, JToken.FromObject(command), cancellationToken, millisecondsTimeout, throwExceptionIfResponseNotReceived).ConfigureAwait(false);

if (result == null)
{
return null;
}

if (!this.domains.VersionSpecificDomains.ResponseTypeMap.TryGetCommandResponseType(command, out Type commandResponseType))
{
throw new InvalidOperationException($"Type {typeof(TCommand)} does not correspond to a known command response type.");
}
Expand Down Expand Up @@ -195,7 +239,23 @@ public T GetVersionSpecificDomains<T>() where T : DevToolsSessionDomains
/// <param name="throwExceptionIfResponseNotReceived"><see langword="true"/> to throw an exception if a response is not received; otherwise, <see langword="false"/>.</param>
/// <returns>The command response object implementing the <see cref="ICommandResponse{T}"/> interface.</returns>
//[DebuggerStepThrough]
public async Task<JToken> SendCommand(string commandName, JToken commandParameters, CancellationToken cancellationToken = default(CancellationToken), int? millisecondsTimeout = null, bool throwExceptionIfResponseNotReceived = true)
public Task<JToken> SendCommand(string commandName, JToken commandParameters, CancellationToken cancellationToken = default(CancellationToken), int? millisecondsTimeout = null, bool throwExceptionIfResponseNotReceived = true)
{
return SendCommand(commandName, ActiveSessionId, commandParameters, cancellationToken, millisecondsTimeout, throwExceptionIfResponseNotReceived);
}

/// <summary>
/// Returns a JToken based on a command created with the specified command name and params.
/// </summary>
/// <param name="commandName">The name of the command to send.</param>
/// <param name="sessionId">The sessionId of the command.</param>
/// <param name="commandParameters">The parameters of the command as a JToken object</param>
/// <param name="cancellationToken">A CancellationToken object to allow for cancellation of the command.</param>
/// <param name="millisecondsTimeout">The execution timeout of the command in milliseconds.</param>
/// <param name="throwExceptionIfResponseNotReceived"><see langword="true"/> to throw an exception if a response is not received; otherwise, <see langword="false"/>.</param>
/// <returns>The command response object implementing the <see cref="ICommandResponse{T}"/> interface.</returns>
//[DebuggerStepThrough]
public async Task<JToken> SendCommand(string commandName, string sessionId, JToken commandParameters, CancellationToken cancellationToken = default(CancellationToken), int? millisecondsTimeout = null, bool throwExceptionIfResponseNotReceived = true)
{
if (millisecondsTimeout.HasValue == false)
{
Expand All @@ -208,7 +268,7 @@ public T GetVersionSpecificDomains<T>() where T : DevToolsSessionDomains
await this.InitializeSession().ConfigureAwait(false);
}

var message = new DevToolsCommandData(Interlocked.Increment(ref this.currentCommandId), this.ActiveSessionId, commandName, commandParameters);
var message = new DevToolsCommandData(Interlocked.Increment(ref this.currentCommandId), sessionId, commandName, commandParameters);

if (this.connection != null && this.connection.IsActive)
{
Expand Down Expand Up @@ -267,10 +327,10 @@ public void Dispose()
/// <summary>
/// Asynchronously starts the session.
/// </summary>
/// <param name="requestedProtocolVersion">The requested version of the protocol to use in communicating with the browswer.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
internal async Task StartSession(int requestedProtocolVersion)
internal async Task StartSession()
{
var requestedProtocolVersion = options.ProtocolVersion.HasValue ? options.ProtocolVersion.Value : AutoDetectDevToolsProtocolVersion;
int protocolVersion = await InitializeProtocol(requestedProtocolVersion).ConfigureAwait(false);
this.domains = DevToolsDomains.InitializeDomains(protocolVersion, this);
await this.InitializeSocketConnection().ConfigureAwait(false);
Expand Down Expand Up @@ -399,6 +459,16 @@ private async Task InitializeSession()
await this.domains.Target.SetAutoAttach().ConfigureAwait(false);
LogTrace("AutoAttach is set.", this.attachedTargetId);

// The Target domain needs to send Sessionless commands! Else the waitForDebugger setting in setAutoAttach wont work!
if (options.WaitForDebuggerOnStart)
{
var setAutoAttachCommand = domains.Target.CreateSetAutoAttachCommand(true);
var setDiscoverTargetsCommand = domains.Target.CreateDiscoverTargetsCommand();

await SendCommand(setAutoAttachCommand, string.Empty, default(CancellationToken), null, true).ConfigureAwait(false);
await SendCommand(setDiscoverTargetsCommand, string.Empty, default(CancellationToken), null, true).ConfigureAwait(false);
}

this.domains.Target.TargetDetached += this.OnTargetDetached;
}

Expand Down
10 changes: 10 additions & 0 deletions dotnet/src/webdriver/DevTools/IDevTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
// limitations under the License.
// </copyright>

using System;

namespace OpenQA.Selenium.DevTools
{
/// <summary>
Expand All @@ -34,11 +36,19 @@ public interface IDevTools
/// <returns>The active session to use to communicate with the Developer Tools debugging protocol.</returns>
DevToolsSession GetDevToolsSession();

/// <summary>
/// Creates a session to communicate with a browser using a specific version of the Developer Tools debugging protocol.
/// </summary>
/// <param name="options">The options for the DevToolsSession to use.</param>
/// <returns>The active session to use to communicate with the Developer Tools debugging protocol.</returns>
DevToolsSession GetDevToolsSession(DevToolsOptions options);

/// <summary>
/// Creates a session to communicate with a browser using a specific version of the Developer Tools debugging protocol.
/// </summary>
/// <param name="protocolVersion">The specific version of the Developer Tools debugging protocol to use.</param>
/// <returns>The active session to use to communicate with the Developer Tools debugging protocol.</returns>
[Obsolete("Use GetDevToolsSession(DevToolsOptions options)")]
DevToolsSession GetDevToolsSession(int protocolVersion);

/// <summary>
Expand Down
4 changes: 4 additions & 0 deletions dotnet/src/webdriver/DevTools/Target.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ public abstract class Target
/// <returns>A task that represents the asynchronous operation.</returns>
public abstract Task SetAutoAttach();

internal abstract ICommand CreateSetAutoAttachCommand(bool waitForDebuggerOnStart);

internal abstract ICommand CreateDiscoverTargetsCommand();

/// <summary>
/// Raises the TargetDetached event.
/// </summary>
Expand Down
18 changes: 18 additions & 0 deletions dotnet/src/webdriver/DevTools/v118/V118Target.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,5 +140,23 @@ private void OnAttachedToTarget(object sender, AttachedToTargetEventArgs e)
WaitingForDebugger = e.WaitingForDebugger
});
}

internal override ICommand CreateSetAutoAttachCommand(bool waitForDebuggerOnStart)
{
return new SetAutoAttachCommandSettings
{
AutoAttach = true,
Flatten = true,
WaitForDebuggerOnStart = waitForDebuggerOnStart
};
}

internal override ICommand CreateDiscoverTargetsCommand()
{
return new SetDiscoverTargetsCommandSettings
{
Discover = true
};
}
}
}
18 changes: 18 additions & 0 deletions dotnet/src/webdriver/DevTools/v119/V119Target.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,5 +140,23 @@ private void OnAttachedToTarget(object sender, AttachedToTargetEventArgs e)
WaitingForDebugger = e.WaitingForDebugger
});
}

internal override ICommand CreateSetAutoAttachCommand(bool waitForDebuggerOnStart)
{
return new SetAutoAttachCommandSettings
{
AutoAttach = true,
Flatten = true,
WaitForDebuggerOnStart = waitForDebuggerOnStart
};
}

internal override ICommand CreateDiscoverTargetsCommand()
{
return new SetDiscoverTargetsCommandSettings
{
Discover = true
};
}
}
}
18 changes: 18 additions & 0 deletions dotnet/src/webdriver/DevTools/v120/V120Target.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,5 +140,23 @@ private void OnAttachedToTarget(object sender, AttachedToTargetEventArgs e)
WaitingForDebugger = e.WaitingForDebugger
});
}

internal override ICommand CreateSetAutoAttachCommand(bool waitForDebuggerOnStart)
{
return new SetAutoAttachCommandSettings
{
AutoAttach = true,
Flatten = true,
WaitForDebuggerOnStart = waitForDebuggerOnStart
};
}

internal override ICommand CreateDiscoverTargetsCommand()
{
return new SetDiscoverTargetsCommandSettings
{
Discover = true
};
}
}
}
18 changes: 18 additions & 0 deletions dotnet/src/webdriver/DevTools/v85/V85Target.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,23 @@ private void OnAttachedToTarget(object sender, AttachedToTargetEventArgs e)
WaitingForDebugger = e.WaitingForDebugger
});
}

internal override ICommand CreateSetAutoAttachCommand(bool waitForDebuggerOnStart)
{
return new SetAutoAttachCommandSettings
{
AutoAttach = true,
Flatten = true,
WaitForDebuggerOnStart = waitForDebuggerOnStart
};
}

internal override ICommand CreateDiscoverTargetsCommand()
{
return new SetDiscoverTargetsCommandSettings
{
Discover = true
};
}
}
}
Loading

0 comments on commit f7fd6d3

Please sign in to comment.