Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,20 @@ jobs:
8.0.x
9.0.x
10.0.x
- name: Install Firefox Nightly
run: |
wget -q "https://download.mozilla.org/?product=firefox-nightly-latest-ssl&os=linux64&lang=en-US" -O firefox-nightly.tar.xz
tar xJf firefox-nightly.tar.xz
echo "$PWD/firefox" >> $GITHUB_PATH
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal -p:CollectCoverage=true -p:CoverletOutputFormat=lcov -p:CoverletOutput=../coverage/lcov
env:
FIREFOX_EXECUTABLE: firefox
CHROME_EXECUTABLE: google-chrome-stable
- name: Upload code coverage
uses: coverallsapp/github-action@master
with:
Expand Down
86 changes: 70 additions & 16 deletions src/WebDriverBiDi.Client/Launchers/ChromiumTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

namespace WebDriverBiDi.Client.Launchers;

using System.IO;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using WebDriverBiDi;
using WebDriverBiDi.Protocol;

Expand Down Expand Up @@ -64,11 +64,11 @@ protected override byte[] SerializeCommand(Command command)
// carefully that this does double-convert from byte array to string
// and back, and that is intentional given the usage here.
byte[] commandBytes = base.SerializeCommand(command);
string serializedCommand = JsonSerializer.Serialize(Encoding.UTF8.GetString(commandBytes));
string serializedCommand = JsonEncodeString(Encoding.UTF8.GetString(commandBytes));
DevToolsProtocolCommand wrapperCommand = new(this.GetNextCommandId(), "Runtime.evaluate");
wrapperCommand.Parameters["expression"] = @$"window.onBidiMessage({serializedCommand})";
wrapperCommand.SessionId = this.sessionId;
return JsonSerializer.SerializeToUtf8Bytes(wrapperCommand);
return wrapperCommand.SerializeToUtf8Bytes();
}

/// <summary>
Expand Down Expand Up @@ -127,7 +127,7 @@ private async Task InitializeBiDi()
DevToolsProtocolCommand command = new(this.GetNextCommandId(), "Target.createTarget");
command.Parameters["url"] = "about:blank";
command.Parameters["hidden"] = true;
await this.Connection.SendDataAsync(JsonSerializer.SerializeToUtf8Bytes(command));
await this.Connection.SendDataAsync(command.SerializeToUtf8Bytes());
syncEvent.Wait(TimeSpan.FromSeconds(3));
syncEvent.Reset();
if (document is not null)
Expand All @@ -144,7 +144,7 @@ private async Task InitializeBiDi()
command = new DevToolsProtocolCommand(this.GetNextCommandId(), "Target.attachToTarget");
command.Parameters["targetId"] = this.mapperTabTargetId;
command.Parameters["flatten"] = true;
await this.Connection.SendDataAsync(JsonSerializer.SerializeToUtf8Bytes(command)).ConfigureAwait(false);
await this.Connection.SendDataAsync(command.SerializeToUtf8Bytes()).ConfigureAwait(false);
syncEvent.Wait(TimeSpan.FromSeconds(3));
syncEvent.Reset();

Expand All @@ -156,22 +156,22 @@ private async Task InitializeBiDi()
command = new DevToolsProtocolCommand(this.GetNextCommandId(), "Runtime.evaluate");
command.Parameters["expression"] = "document.body.click()";
command.Parameters["userGesture"] = true;
await this.Connection.SendDataAsync(JsonSerializer.SerializeToUtf8Bytes(command)).ConfigureAwait(false);
await this.Connection.SendDataAsync(command.SerializeToUtf8Bytes()).ConfigureAwait(false);
syncEvent.Wait(TimeSpan.FromSeconds(3));
syncEvent.Reset();

// Enable the Runtime CDP domain.
command = new DevToolsProtocolCommand(this.GetNextCommandId(), "Runtime.enable");
command.SessionId = this.sessionId;
await this.Connection.SendDataAsync(JsonSerializer.SerializeToUtf8Bytes(command)).ConfigureAwait(false);
await this.Connection.SendDataAsync(command.SerializeToUtf8Bytes()).ConfigureAwait(false);
syncEvent.Wait(TimeSpan.FromSeconds(3));
syncEvent.Reset();

// Expose CDP for the mapper tab.
command = new DevToolsProtocolCommand(this.GetNextCommandId(), "Target.exposeDevToolsProtocol");
command.Parameters["bindingName"] = "cdp";
command.Parameters["targetId"] = this.mapperTabTargetId;
await this.Connection.SendDataAsync(JsonSerializer.SerializeToUtf8Bytes(command)).ConfigureAwait(false);
await this.Connection.SendDataAsync(command.SerializeToUtf8Bytes()).ConfigureAwait(false);
syncEvent.Wait(TimeSpan.FromSeconds(3));
syncEvent.Reset();

Expand All @@ -193,7 +193,7 @@ private async Task InitializeBiDi()
command = new DevToolsProtocolCommand(this.GetNextCommandId(), "Runtime.evaluate");
command.Parameters["expression"] = mapperScript;
command.SessionId = this.sessionId;
await this.Connection.SendDataAsync(JsonSerializer.SerializeToUtf8Bytes(command)).ConfigureAwait(false);
await this.Connection.SendDataAsync(command.SerializeToUtf8Bytes()).ConfigureAwait(false);
syncEvent.Wait(TimeSpan.FromSeconds(3));
syncEvent.Reset();

Expand All @@ -202,15 +202,15 @@ private async Task InitializeBiDi()
command.Parameters["expression"] = @$"window.runMapperInstance(""{this.mapperTabTargetId}"")";
command.Parameters["awaitPromise"] = true;
command.SessionId = this.sessionId;
await this.Connection.SendDataAsync(JsonSerializer.SerializeToUtf8Bytes(command)).ConfigureAwait(false);
await this.Connection.SendDataAsync(command.SerializeToUtf8Bytes()).ConfigureAwait(false);
syncEvent.Wait(TimeSpan.FromSeconds(3));
syncEvent.Reset();

// Add a binding to be notified when a response is sent from the BiDi-to-CDP mapper code.
command = new DevToolsProtocolCommand(this.GetNextCommandId(), "Runtime.addBinding");
command.Parameters["name"] = "sendBidiResponse";
command.SessionId = this.sessionId;
await this.Connection.SendDataAsync(JsonSerializer.SerializeToUtf8Bytes(command));
await this.Connection.SendDataAsync(command.SerializeToUtf8Bytes());
syncEvent.Wait(TimeSpan.FromSeconds(3));
syncEvent.Reset();
}
Expand All @@ -219,6 +219,21 @@ private async Task InitializeBiDi()
observer.Unobserve();
}

/// <summary>
/// Produces a JSON-encoded (quoted and escaped) string using Utf8JsonWriter,
/// replacing the AOT-incompatible <c>JsonSerializer.Serialize(string)</c>.
/// </summary>
private static string JsonEncodeString(string value)
{
using MemoryStream stream = new();
using (Utf8JsonWriter writer = new(stream))
{
writer.WriteStringValue(value);
}

return Encoding.UTF8.GetString(stream.ToArray());
}

private class DevToolsProtocolCommand
{
private readonly long id;
Expand All @@ -232,17 +247,56 @@ public DevToolsProtocolCommand(long id, string method)
this.method = method;
}

[JsonPropertyName("id")]
public long Id => this.id;

[JsonPropertyName("method")]
public string Method => this.method;

[JsonPropertyName("params")]
public Dictionary<string, object> Parameters => this.parameters;

[JsonPropertyName("sessionId")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? SessionId { get => this.sessionId; set => this.sessionId = value; }

/// <summary>
/// Serializes this command to UTF-8 JSON bytes using Utf8JsonWriter,
/// avoiding the AOT-incompatible <c>JsonSerializer.SerializeToUtf8Bytes</c>.
/// </summary>
public byte[] SerializeToUtf8Bytes()
{
using MemoryStream stream = new();
using (Utf8JsonWriter writer = new(stream))
{
writer.WriteStartObject();
writer.WriteNumber("id", this.id);
writer.WriteString("method", this.method);
writer.WriteStartObject("params");
foreach (KeyValuePair<string, object> kvp in this.parameters)
{
switch (kvp.Value)
{
case string s:
writer.WriteString(kvp.Key, s);
break;
case bool b:
writer.WriteBoolean(kvp.Key, b);
break;
case long l:
writer.WriteNumber(kvp.Key, l);
break;
default:
writer.WriteString(kvp.Key, kvp.Value?.ToString() ?? string.Empty);
break;
}
}

writer.WriteEndObject();
if (this.sessionId is not null)
{
writer.WriteString("sessionId", this.sessionId);
}

writer.WriteEndObject();
}

return stream.ToArray();
}
}
}
17 changes: 15 additions & 2 deletions src/WebDriverBiDi.Client/Launchers/FirefoxLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace WebDriverBiDi.Client.Launchers;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Globalization;
using System.Text.RegularExpressions;
using WebDriverBiDi;
using WebDriverBiDi.Protocol;
Expand Down Expand Up @@ -493,13 +493,26 @@ private void CreateProfile()
List<string> preferenceList = [];
foreach (KeyValuePair<string, object> preferencePair in defaultPreferences)
{
preferenceList.Add($"user_pref({JsonSerializer.Serialize(preferencePair.Key)}, {JsonSerializer.Serialize(preferencePair.Value)});");
preferenceList.Add($"user_pref({FormatJsValue(preferencePair.Key)}, {FormatJsValue(preferencePair.Value)});");
}

File.WriteAllText(Path.Combine(this.userDataDirectory, "user.js"), string.Join("\n", preferenceList));
File.WriteAllText(Path.Combine(this.userDataDirectory, "prefs.js"), string.Empty);
}

private static string FormatJsValue(object value)
{
return value switch
{
string s => "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"",
bool b => b ? "true" : "false",
int i => i.ToString(CultureInfo.InvariantCulture),
long l => l.ToString(CultureInfo.InvariantCulture),
double d => d.ToString(CultureInfo.InvariantCulture),
_ => throw new ArgumentException($"Unsupported preference value type: {value.GetType().Name}", nameof(value)),
};
}

/// <summary>
/// Asynchronously waits for the initialization of the browser launcher.
/// </summary>
Expand Down
Loading
Loading