Skip to content

Commit 19ad79e

Browse files
authored
Implement ConnectionTransport (#710)
1 parent e3f9d91 commit 19ad79e

File tree

9 files changed

+274
-87
lines changed

9 files changed

+274
-87
lines changed

lib/PuppeteerSharp.Tests/TestConstants.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ public static class TestConstants
3838
Headless = Convert.ToBoolean(Environment.GetEnvironmentVariable("HEADLESS") ?? "true"),
3939
Args = new[] { "--no-sandbox" },
4040
Timeout = 0,
41-
LogProcess = true
41+
LogProcess = true,
42+
#if NETCOREAPP
43+
EnqueueTransportMessages = false
44+
#else
45+
EnqueueTransportMessages = true
46+
#endif
4247
};
4348

4449
public static LaunchOptions BrowserWithExtensionOptions() => new LaunchOptions

lib/PuppeteerSharp/ConnectOptions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace PuppeteerSharp
55
{
66
using System.Threading;
77
using System.Threading.Tasks;
8+
using PuppeteerSharp.Transport;
89

910
/// <summary>
1011
/// Options for connecting to an existing browser.
@@ -39,6 +40,7 @@ public class ConnectOptions : IBrowserOptions, IConnectionOptions
3940

4041
/// <summary>
4142
/// Optional factory for <see cref="WebSocket"/> implementations.
43+
/// If <see cref="Transport"/> is set this property will be ignored.
4244
/// </summary>
4345
public Func<Uri, IConnectionOptions, CancellationToken, Task<WebSocket>> WebSocketFactory { get; set; }
4446

@@ -47,5 +49,18 @@ public class ConnectOptions : IBrowserOptions, IConnectionOptions
4749
/// </summary>
4850
/// <value>The default Viewport.</value>
4951
public ViewPortOptions DefaultViewport { get; set; } = ViewPortOptions.Default;
52+
53+
/// <summary>
54+
/// Optional connection transport.
55+
/// </summary>
56+
public IConnectionTransport Transport { get; set; }
57+
/// <summary>
58+
/// If not <see cref="Transport"/> is set this will be use to determine is the default <see cref="WebSocketTransport"/> will enqueue messages.
59+
/// </summary>
60+
/// <remarks>
61+
/// It's set to <c>true</c> by default because it's the safest way to send commands to Chromium.
62+
/// Setting this to <c>false</c> proved to work in .NET Core but it tends to fail on .NET Framework.
63+
/// </remarks>
64+
public bool EnqueueTransportMessages { get; set; } = true;
5065
}
5166
}

lib/PuppeteerSharp/Connection.cs

Lines changed: 34 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Newtonsoft.Json.Linq;
1212
using PuppeteerSharp.Helpers;
1313
using PuppeteerSharp.Messaging;
14+
using PuppeteerSharp.Transport;
1415

1516
namespace PuppeteerSharp
1617
{
@@ -21,28 +22,26 @@ public class Connection : IDisposable, IConnection
2122
{
2223
private readonly ILogger _logger;
2324

24-
internal Connection(string url, int delay, WebSocket ws, ILoggerFactory loggerFactory = null)
25+
internal Connection(string url, int delay, IConnectionTransport transport, ILoggerFactory loggerFactory = null)
2526
{
2627
LoggerFactory = loggerFactory ?? new LoggerFactory();
2728
Url = url;
2829
Delay = delay;
29-
WebSocket = ws;
30+
Transport = transport;
3031

3132
_logger = LoggerFactory.CreateLogger<Connection>();
32-
_socketQueue = new TaskQueue();
33-
_callbacks = new ConcurrentDictionary<int, MessageTask>( );
33+
34+
Transport.MessageReceived += Transport_MessageReceived;
35+
Transport.Closed += Transport_Closed;
36+
_callbacks = new ConcurrentDictionary<int, MessageTask>();
3437
_sessions = new ConcurrentDictionary<string, CDPSession>();
35-
_websocketReaderCancellationSource = new CancellationTokenSource();
3638

37-
Task.Factory.StartNew(GetResponseAsync);
3839
}
3940

4041
#region Private Members
4142
private int _lastId;
4243
private readonly ConcurrentDictionary<int, MessageTask> _callbacks;
4344
private readonly ConcurrentDictionary<string, CDPSession> _sessions;
44-
private readonly TaskQueue _socketQueue;
45-
private readonly CancellationTokenSource _websocketReaderCancellationSource;
4645
#endregion
4746

4847
#region Properties
@@ -57,10 +56,10 @@ internal Connection(string url, int delay, WebSocket ws, ILoggerFactory loggerFa
5756
/// <value>The delay.</value>
5857
public int Delay { get; }
5958
/// <summary>
60-
/// Gets the WebSocket.
59+
/// Gets the Connection transport.
6160
/// </summary>
62-
/// <value>The web socket.</value>
63-
public WebSocket WebSocket { get; }
61+
/// <value>Connection transport.</value>
62+
public IConnectionTransport Transport { get; }
6463
/// <summary>
6564
/// Occurs when the connection is closed.
6665
/// </summary>
@@ -114,10 +113,7 @@ internal async Task<JObject> SendAsync(string method, dynamic args = null, bool
114113
_callbacks[id] = callback;
115114
}
116115

117-
var encoded = Encoding.UTF8.GetBytes(message);
118-
var buffer = new ArraySegment<byte>(encoded, 0, encoded.Length);
119-
await _socketQueue.Enqueue(() => WebSocket.SendAsync(buffer, WebSocketMessageType.Text, true, default)).ConfigureAwait(false);
120-
116+
await Transport.SendAsync(message).ConfigureAwait(false);
121117
return waitForCallback ? await callback.TaskWrapper.Task.ConfigureAwait(false) : null;
122118
}
123119

@@ -149,7 +145,7 @@ private void OnClose()
149145
}
150146
IsClosed = true;
151147

152-
_websocketReaderCancellationSource.Cancel();
148+
Transport.StopReading();
153149
Closed?.Invoke(this, new EventArgs());
154150

155151
foreach (var session in _sessions.Values.ToArray())
@@ -178,73 +174,15 @@ internal static IConnection FromSession(CDPSession session)
178174
}
179175
#region Private Methods
180176

181-
/// <summary>
182-
/// Starts listening the socket
183-
/// </summary>
184-
/// <returns>The start.</returns>
185-
private async Task<object> GetResponseAsync()
177+
private async void Transport_MessageReceived(object sender, MessageReceivedEventArgs e)
186178
{
187-
var buffer = new byte[2048];
179+
var response = e.Message;
180+
JObject obj = null;
188181

189-
//If it's not in the list we wait for it
190-
while (true)
182+
if (response.Length > 0 && Delay > 0)
191183
{
192-
if (IsClosed)
193-
{
194-
OnClose();
195-
return null;
196-
}
197-
198-
var endOfMessage = false;
199-
var response = new StringBuilder();
200-
201-
while (!endOfMessage)
202-
{
203-
WebSocketReceiveResult result;
204-
try
205-
{
206-
result = await WebSocket.ReceiveAsync(
207-
new ArraySegment<byte>(buffer),
208-
_websocketReaderCancellationSource.Token).ConfigureAwait(false);
209-
}
210-
catch (OperationCanceledException)
211-
{
212-
return null;
213-
}
214-
catch (Exception)
215-
{
216-
OnClose();
217-
return null;
218-
}
219-
220-
endOfMessage = result.EndOfMessage;
221-
222-
if (result.MessageType == WebSocketMessageType.Text)
223-
{
224-
response.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
225-
}
226-
else if (result.MessageType == WebSocketMessageType.Close)
227-
{
228-
OnClose();
229-
return null;
230-
}
231-
}
232-
233-
if (response.Length > 0)
234-
{
235-
if (Delay > 0)
236-
{
237-
await Task.Delay(Delay).ConfigureAwait(false);
238-
}
239-
240-
ProcessResponse(response.ToString());
241-
}
184+
await Task.Delay(Delay).ConfigureAwait(false);
242185
}
243-
}
244-
245-
private void ProcessResponse(string response)
246-
{
247-
JObject obj = null;
248186

249187
try
250188
{
@@ -307,6 +245,9 @@ private void ProcessResponse(string response)
307245
}
308246
}
309247
}
248+
249+
void Transport_Closed(object sender, EventArgs e) => OnClose();
250+
310251
#endregion
311252

312253
#region Static Methods
@@ -324,16 +265,23 @@ private void ProcessResponse(string response)
324265

325266
internal static async Task<Connection> Create(string url, IConnectionOptions connectionOptions, ILoggerFactory loggerFactory = null)
326267
{
327-
var ws = await (connectionOptions.WebSocketFactory ?? DefaultWebSocketFactory)(
328-
new Uri(url),
329-
connectionOptions,
330-
default).ConfigureAwait(false);
331-
return new Connection(url, connectionOptions.SlowMo, ws, loggerFactory);
268+
var transport = connectionOptions.Transport;
269+
270+
if (transport == null)
271+
{
272+
var ws = await (connectionOptions.WebSocketFactory ?? DefaultWebSocketFactory)(
273+
new Uri(url),
274+
connectionOptions,
275+
default).ConfigureAwait(false);
276+
transport = new WebSocketTransport(ws, connectionOptions.EnqueueTransportMessages);
277+
}
278+
279+
return new Connection(url, connectionOptions.SlowMo, transport, loggerFactory);
332280
}
333281

334282
/// <summary>
335283
/// Releases all resource used by the <see cref="Connection"/> object.
336-
/// It will raise the <see cref="Closed"/> event and dispose <see cref="WebSocket"/>.
284+
/// It will raise the <see cref="Closed"/> event and dispose <see cref="Transport"/>.
337285
/// </summary>
338286
/// <remarks>Call <see cref="Dispose"/> when you are finished using the <see cref="Connection"/>. The
339287
/// <see cref="Dispose"/> method leaves the <see cref="Connection"/> in an unusable state.
@@ -343,7 +291,7 @@ internal static async Task<Connection> Create(string url, IConnectionOptions con
343291
public void Dispose()
344292
{
345293
OnClose();
346-
WebSocket.Dispose();
294+
Transport.Dispose();
347295
}
348296
#endregion
349297

lib/PuppeteerSharp/IConnectionOptions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Net.WebSockets;
33
using System.Threading;
44
using System.Threading.Tasks;
5+
using PuppeteerSharp.Transport;
56

67
namespace PuppeteerSharp
78
{
@@ -23,7 +24,18 @@ public interface IConnectionOptions
2324

2425
/// <summary>
2526
/// Optional factory for <see cref="WebSocket"/> implementations.
27+
/// If <see cref="Transport"/> is set this property will be ignored.
2628
/// </summary>
2729
Func<Uri, IConnectionOptions, CancellationToken, Task<WebSocket>> WebSocketFactory { get; set; }
30+
31+
/// <summary>
32+
/// Optional connection transport.
33+
/// </summary>
34+
IConnectionTransport Transport { get; set; }
35+
36+
/// <summary>
37+
/// If not <see cref="Transport"/> is set this will be use to determine is the default <see cref="WebSocketTransport"/> will enqueue messages.
38+
/// </summary>
39+
bool EnqueueTransportMessages { get; set; }
2840
}
2941
}

lib/PuppeteerSharp/LaunchOptions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Net.WebSockets;
44
using System.Threading;
55
using System.Threading.Tasks;
6+
using PuppeteerSharp.Transport;
67

78
namespace PuppeteerSharp
89
{
@@ -95,13 +96,28 @@ public string[] IgnoredDefaultArgs
9596

9697
/// <summary>
9798
/// Optional factory for <see cref="WebSocket"/> implementations.
99+
/// If <see cref="Transport"/> is set this property will be ignored.
98100
/// </summary>
99101
public Func<Uri, IConnectionOptions, CancellationToken, Task<WebSocket>> WebSocketFactory { get; set; }
100102

103+
/// <summary>
104+
/// Optional connection transport.
105+
/// </summary>
106+
public IConnectionTransport Transport { get; set; }
107+
101108
/// <summary>
102109
/// Gets or sets the default Viewport.
103110
/// </summary>
104111
/// <value>The default Viewport.</value>
105112
public ViewPortOptions DefaultViewport { get; set; } = ViewPortOptions.Default;
113+
114+
/// <summary>
115+
/// If not <see cref="Transport"/> is set this will be use to determine is the default <see cref="WebSocketTransport"/> will enqueue messages.
116+
/// </summary>
117+
/// <remarks>
118+
/// It's set to <c>true</c> by default because it's the safest way to send commands to Chromium.
119+
/// Setting this to <c>false</c> proved to work in .NET Core but it tends to fail on .NET Framework.
120+
/// </remarks>
121+
public bool EnqueueTransportMessages { get; set; } = true;
106122
}
107123
}

lib/PuppeteerSharp/PuppeteerSharp.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,7 @@
5959
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.0.2" />
6060
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.1" />
6161
</ItemGroup>
62+
<ItemGroup>
63+
<Folder Include="Transport\" />
64+
</ItemGroup>
6265
</Project>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace PuppeteerSharp.Transport
6+
{
7+
/// <summary>
8+
/// Connection transport abstraction.
9+
/// </summary>
10+
public interface IConnectionTransport : IDisposable
11+
{
12+
/// <summary>
13+
/// Gets a value indicating whether this <see cref="PuppeteerSharp.Transport.IConnectionTransport"/> is closed.
14+
/// </summary>
15+
bool IsClosed { get; }
16+
/// <summary>
17+
/// Stops reading incoming data.
18+
/// </summary>
19+
void StopReading();
20+
/// <summary>
21+
/// Sends a message using the transport.
22+
/// </summary>
23+
/// <returns>The task.</returns>
24+
/// <param name="message">Message to send.</param>
25+
Task SendAsync(string message);
26+
/// <summary>
27+
/// Occurs when the transport is closed.
28+
/// </summary>
29+
event EventHandler Closed;
30+
/// <summary>
31+
/// Occurs when a message is received.
32+
/// </summary>
33+
event EventHandler<MessageReceivedEventArgs> MessageReceived;
34+
}
35+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
3+
namespace PuppeteerSharp.Transport
4+
{
5+
/// <summary>
6+
/// Message received event arguments.
7+
/// <see cref="IConnectionTransport.MessageReceived"/>
8+
/// </summary>
9+
public class MessageReceivedEventArgs : EventArgs
10+
{
11+
/// <summary>
12+
/// Initializes a new instance of the <see cref="PuppeteerSharp.Transport.MessageReceivedEventArgs"/> class.
13+
/// </summary>
14+
/// <param name="message">Message.</param>
15+
public MessageReceivedEventArgs(string message) => Message = message;
16+
/// <summary>
17+
/// Transport message.
18+
/// </summary>
19+
public string Message { get; }
20+
}
21+
}

0 commit comments

Comments
 (0)