Skip to content

Commit 25bc019

Browse files
authored
Introduced workers (#575)
1 parent 2000ca6 commit 25bc019

File tree

18 files changed

+485
-92
lines changed

18 files changed

+485
-92
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Worker test</title>
5+
</head>
6+
<body>
7+
<script>
8+
var worker = new Worker('worker.js');
9+
worker.onmessage = function(message) {
10+
console.log(message.data);
11+
};
12+
</script>
13+
</body>
14+
</html>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
console.log('hello from the worker');
2+
3+
function workerFunction() {
4+
return 'worker function result';
5+
}
6+
7+
self.addEventListener('message', event => {
8+
console.log('got this data: ' + event.data);
9+
});
10+
11+
(async function() {
12+
while (true) {
13+
self.postMessage(workerFunction.toString());
14+
await new Promise(x => setTimeout(x, 100));
15+
}
16+
})();

lib/PuppeteerSharp.Tests/TestConstants.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ namespace PuppeteerSharp.Tests
1010
{
1111
public static class TestConstants
1212
{
13-
public const int Port = 8907;
13+
public const int Port = 8081;
1414
public const int HttpsPort = Port + 1;
15-
public const string ServerUrl = "http://localhost:8907";
16-
public const string ServerIpUrl = "http://127.0.0.1:8907";
17-
public const string HttpsPrefix = "https://localhost:8908";
15+
public const string ServerUrl = "http://localhost:8081";
16+
public const string ServerIpUrl = "http://127.0.0.1:8081";
17+
public const string HttpsPrefix = "https://localhost:8082";
1818
public const string AboutBlank = "about:blank";
19-
public static readonly string CrossProcessHttpPrefix = "http://127.0.0.1:8907";
19+
public static readonly string CrossProcessHttpPrefix = "http://127.0.0.1:8081";
2020
public static readonly string EmptyPage = $"{ServerUrl}/empty.html";
2121
public static readonly string CrossProcessUrl = ServerIpUrl;
22-
public static readonly string ExtensionPath = Path.Combine(Directory.GetCurrentDirectory(), "Assets","simple-extension");
22+
public static readonly string ExtensionPath = Path.Combine(Directory.GetCurrentDirectory(), "Assets", "simple-extension");
2323

2424
public static readonly DeviceDescriptor IPhone = DeviceDescriptors.Get(DeviceDescriptorName.IPhone6);
2525
public static readonly DeviceDescriptor IPhone6Landscape = DeviceDescriptors.Get(DeviceDescriptorName.IPhone6Landscape);
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System.Threading.Tasks;
2+
using Xunit;
3+
using Xunit.Abstractions;
4+
5+
namespace PuppeteerSharp.Tests.WorkerTests
6+
{
7+
[Collection("PuppeteerLoaderFixture collection")]
8+
public class WorkerTests : PuppeteerPageBaseTest
9+
{
10+
public WorkerTests(ITestOutputHelper output) : base(output)
11+
{
12+
}
13+
14+
[Fact]
15+
public async Task PageWorkers()
16+
{
17+
var pageCreatedCompletion = new TaskCompletionSource<bool>();
18+
Page.WorkerCreated += (sender, e) => pageCreatedCompletion.TrySetResult(true);
19+
await Task.WhenAll(
20+
pageCreatedCompletion.Task,
21+
Page.GoToAsync(TestConstants.ServerUrl + "/worker/worker.html"));
22+
var worker = Page.Workers[0];
23+
Assert.Contains("worker.js", worker.Url);
24+
25+
Assert.Equal("worker function result", await worker.EvaluateExpressionAsync<string>("self.workerFunction()"));
26+
27+
await Page.GoToAsync(TestConstants.EmptyPage);
28+
Assert.Empty(Page.Workers);
29+
}
30+
31+
[Fact]
32+
public async Task ShouldEmitCreatedAndDestroyedEvents()
33+
{
34+
var workerCreatedTcs = new TaskCompletionSource<Worker>();
35+
Page.WorkerCreated += (sender, e) => workerCreatedTcs.TrySetResult(e.Worker);
36+
37+
var workerObj = await Page.EvaluateFunctionHandleAsync("() => new Worker('data:text/javascript,1')");
38+
var worker = await workerCreatedTcs.Task;
39+
var workerDestroyedTcs = new TaskCompletionSource<Worker>();
40+
Page.WorkerDestroyed += (sender, e) => workerDestroyedTcs.TrySetResult(e.Worker);
41+
await Page.EvaluateFunctionAsync("workerObj => workerObj.terminate()", workerObj);
42+
Assert.Same(worker, await workerDestroyedTcs.Task);
43+
}
44+
45+
[Fact]
46+
public async Task ShouldReportConsoleLogs()
47+
{
48+
var consoleTcs = new TaskCompletionSource<ConsoleMessage>();
49+
Page.Console += (sender, e) => consoleTcs.TrySetResult(e.Message);
50+
51+
await Page.EvaluateFunctionAsync("() => new Worker(`data:text/javascript,console.log(1)`)");
52+
53+
var log = await consoleTcs.Task;
54+
Assert.Equal("1", log.Text);
55+
}
56+
57+
[Fact]
58+
public async Task ShouldHaveJSHandlesForConsoleLogs()
59+
{
60+
var consoleTcs = new TaskCompletionSource<ConsoleMessage>();
61+
Page.Console += (sender, e) =>
62+
{
63+
consoleTcs.TrySetResult(e.Message);
64+
};
65+
await Page.EvaluateFunctionAsync("() => new Worker(`data:text/javascript,console.log(1, 2, 3, this)`)");
66+
var log = await consoleTcs.Task;
67+
Assert.Equal("1 2 3 JSHandle@object", log.Text);
68+
Assert.Equal(4, log.Args.Count);
69+
var json = await (await log.Args[3].GetPropertyAsync("origin")).JsonValueAsync<object>();
70+
Assert.Equal("null", json);
71+
}
72+
73+
[Fact]
74+
public async Task ShouldHaveAnExecutionContext()
75+
{
76+
var workerCreatedTcs = new TaskCompletionSource<Worker>();
77+
Page.WorkerCreated += (sender, e) => workerCreatedTcs.TrySetResult(e.Worker);
78+
79+
await Page.EvaluateFunctionAsync("() => new Worker(`data:text/javascript,console.log(1)`)");
80+
var worker = await workerCreatedTcs.Task;
81+
Assert.Equal(2, await worker.EvaluateExpressionAsync<int>("1+1"));
82+
}
83+
}
84+
}

lib/PuppeteerSharp/Browser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public Browser(
154154
/// </summary>
155155
/// <returns>An Array of all active targets</returns>
156156
public Target[] Targets() => TargetsMap.Values.Where(target => target.IsInitialized).ToArray();
157-
157+
158158
/// <summary>
159159
/// Creates a new incognito browser context. This won't share cookies/cache with other browser contexts.
160160
/// </summary>
@@ -349,7 +349,7 @@ private async Task CreateTargetAsync(TargetCreatedResponse e)
349349

350350
var target = new Target(
351351
e.TargetInfo,
352-
() => Connection.CreateSessionAsync(e.TargetInfo.TargetId),
352+
info => Connection.CreateSessionAsync(info),
353353
context);
354354

355355
if (TargetsMap.ContainsKey(e.TargetInfo.TargetId))

lib/PuppeteerSharp/CDPSession.cs

Lines changed: 58 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using Newtonsoft.Json.Linq;
66
using Microsoft.Extensions.Logging;
7+
using PuppeteerSharp.Helpers;
78

89
namespace PuppeteerSharp
910
{
@@ -33,30 +34,33 @@ namespace PuppeteerSharp
3334
/// });
3435
/// ]]></code>
3536
/// </summary>
36-
public class CDPSession : IDisposable
37+
public class CDPSession : IConnection
3738
{
38-
internal CDPSession(Connection connection, string targetId, string sessionId)
39+
internal CDPSession(IConnection connection, TargetType targetType, string sessionId, ILoggerFactory loggerFactory = null)
3940
{
41+
LoggerFactory = loggerFactory ?? new LoggerFactory();
4042
Connection = connection;
41-
TargetId = targetId;
43+
TargetType = targetType;
4244
SessionId = sessionId;
4345

4446
_callbacks = new Dictionary<int, MessageTask>();
4547
_logger = Connection.LoggerFactory.CreateLogger<CDPSession>();
48+
_sessions = new Dictionary<string, CDPSession>();
4649
}
4750

4851
#region Private Members
4952
private int _lastId;
5053
private readonly Dictionary<int, MessageTask> _callbacks;
5154
private readonly ILogger _logger;
55+
private readonly Dictionary<string, CDPSession> _sessions;
5256
#endregion
5357

5458
#region Properties
5559
/// <summary>
56-
/// Gets the target identifier.
60+
/// Gets the target type.
5761
/// </summary>
58-
/// <value>The target identifier.</value>
59-
public string TargetId { get; }
62+
/// <value>The target type.</value>
63+
public TargetType TargetType { get; }
6064
/// <summary>
6165
/// Gets the session identifier.
6266
/// </summary>
@@ -66,7 +70,7 @@ internal CDPSession(Connection connection, string targetId, string sessionId)
6670
/// Gets the connection.
6771
/// </summary>
6872
/// <value>The connection.</value>
69-
public Connection Connection { get; private set; }
73+
internal IConnection Connection { get; private set; }
7074
/// <summary>
7175
/// Occurs when message received from Chromium.
7276
/// </summary>
@@ -80,6 +84,12 @@ internal CDPSession(Connection connection, string targetId, string sessionId)
8084
/// </summary>
8185
/// <value><c>true</c> if is closed; otherwise, <c>false</c>.</value>
8286
public bool IsClosed { get; internal set; }
87+
88+
/// <summary>
89+
/// Gets the logger factory.
90+
/// </summary>
91+
/// <value>The logger factory.</value>
92+
public ILoggerFactory LoggerFactory { get; }
8393
#endregion
8494

8595
#region Public Methods
@@ -90,7 +100,13 @@ internal async Task<T> SendAsync<T>(string method, dynamic args = null)
90100
return JsonConvert.DeserializeObject<T>(content);
91101
}
92102

93-
internal Task<dynamic> SendAsync(string method, dynamic args = null)
103+
/// <summary>
104+
/// Sends a message to chromium.
105+
/// </summary>
106+
/// <returns>The async.</returns>
107+
/// <param name="method">Method to call.</param>
108+
/// <param name="args">Method arguments.</param>
109+
public Task<dynamic> SendAsync(string method, dynamic args = null)
94110
{
95111
return SendAsync(method, false, args ?? new { });
96112
}
@@ -99,9 +115,9 @@ internal async Task<dynamic> SendAsync(string method, bool rawContent, dynamic a
99115
{
100116
if (Connection == null)
101117
{
102-
throw new Exception($"Protocol error ({method}): Session closed. Most likely the page has been closed.");
118+
throw new Exception($"Protocol error ({method}): Session closed. Most likely the {TargetType} has been closed.");
103119
}
104-
int id = ++_lastId;
120+
var id = ++_lastId;
105121
var message = JsonConvert.SerializeObject(new Dictionary<string, object>
106122
{
107123
{"id", id},
@@ -150,23 +166,6 @@ public Task DetachAsync()
150166

151167
#region Private Methods
152168

153-
/// <summary>
154-
/// Releases all resource used by the <see cref="CDPSession"/> object by sending a ""Target.closeTarget"
155-
/// using the <see cref="Connection.SendAsync(string, dynamic)"/> method.
156-
/// </summary>
157-
/// <remarks>Call <see cref="Dispose"/> when you are finished using the <see cref="CDPSession"/>. The
158-
/// <see cref="Dispose"/> method leaves the <see cref="CDPSession"/> in an unusable state.
159-
/// After calling <see cref="Dispose"/>, you must release all references to the
160-
/// <see cref="CDPSession"/> so the garbage collector can reclaim the memory that the
161-
/// <see cref="CDPSession"/> was occupying.</remarks>
162-
public void Dispose()
163-
{
164-
Connection.SendAsync("Target.closeTarget", new Dictionary<string, object>
165-
{
166-
["targetId"] = TargetId
167-
}).GetAwaiter().GetResult();
168-
}
169-
170169
internal void OnMessage(string message)
171170
{
172171
dynamic obj = JsonConvert.DeserializeObject(message);
@@ -197,15 +196,33 @@ internal void OnMessage(string message)
197196
}
198197
}
199198
}
200-
else if (obj.method == "Tracing.tracingComplete")
201-
{
202-
TracingComplete?.Invoke(this, new TracingCompleteEventArgs
203-
{
204-
Stream = objAsJObject["params"].Value<string>("stream")
205-
});
206-
}
207199
else
208200
{
201+
if (obj.method == "Tracing.tracingComplete")
202+
{
203+
TracingComplete?.Invoke(this, new TracingCompleteEventArgs
204+
{
205+
Stream = objAsJObject["params"].Value<string>("stream")
206+
});
207+
}
208+
else if (obj.method == "Target.receivedMessageFromTarget")
209+
{
210+
var session = _sessions.GetValueOrDefault(objAsJObject["params"]["sessionId"].ToString());
211+
if (session != null)
212+
{
213+
session.OnMessage(objAsJObject["params"]["message"].ToString());
214+
}
215+
}
216+
else if (obj.method == "Target.detachedFromTarget")
217+
{
218+
var session = _sessions.GetValueOrDefault(objAsJObject["params"]["sessionId"].ToString());
219+
if (!(session?.IsClosed ?? true))
220+
{
221+
session.OnClosed();
222+
_sessions.Remove(objAsJObject["params"]["sessionId"].ToString());
223+
}
224+
}
225+
209226
MessageReceived?.Invoke(this, new MessageEventArgs
210227
{
211228
MessageID = obj.method,
@@ -227,6 +244,12 @@ internal void OnClosed()
227244
Connection = null;
228245
}
229246

247+
internal CDPSession CreateSession(TargetType targetType, string sessionId)
248+
{
249+
var session = new CDPSession(this, targetType, sessionId);
250+
_sessions[sessionId] = session;
251+
return session;
252+
}
230253
#endregion
231254
}
232-
}
255+
}

lib/PuppeteerSharp/Connection.cs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace PuppeteerSharp
1515
/// <summary>
1616
/// A connection handles the communication with a Chromium browser
1717
/// </summary>
18-
public class Connection : IDisposable
18+
public class Connection : IDisposable, IConnection
1919
{
2020
private readonly ILogger _logger;
2121

@@ -75,13 +75,23 @@ internal Connection(string url, int delay, ClientWebSocket ws, ILoggerFactory lo
7575
/// <value><c>true</c> if is closed; otherwise, <c>false</c>.</value>
7676
public bool IsClosed { get; internal set; }
7777

78-
internal ILoggerFactory LoggerFactory { get; }
78+
/// <summary>
79+
/// Gets the logger factory.
80+
/// </summary>
81+
/// <value>The logger factory.</value>
82+
public ILoggerFactory LoggerFactory { get; }
7983

8084
#endregion
8185

8286
#region Public Methods
8387

84-
internal async Task<dynamic> SendAsync(string method, dynamic args = null)
88+
/// <summary>
89+
/// Sends a message to chromium.
90+
/// </summary>
91+
/// <returns>The async.</returns>
92+
/// <param name="method">Method to call.</param>
93+
/// <param name="args">Method arguments.</param>
94+
public async Task<dynamic> SendAsync(string method, dynamic args = null)
8595
{
8696
var id = ++_lastId;
8797
var message = JsonConvert.SerializeObject(new Dictionary<string, object>
@@ -110,17 +120,20 @@ internal async Task<dynamic> SendAsync(string method, dynamic args = null)
110120

111121
return await _responses[id].TaskWrapper.Task.ConfigureAwait(false);
112122
}
113-
123+
114124
internal async Task<T> SendAsync<T>(string method, dynamic args = null)
115125
{
116126
JToken response = await SendAsync(method, args);
117127
return response.ToObject<T>();
118128
}
119129

120-
internal async Task<CDPSession> CreateSessionAsync(string targetId)
130+
internal async Task<CDPSession> CreateSessionAsync(TargetInfo targetInfo)
121131
{
122-
string sessionId = (await SendAsync("Target.attachToTarget", new { targetId }).ConfigureAwait(false)).sessionId;
123-
var session = new CDPSession(this, targetId, sessionId);
132+
string sessionId = (await SendAsync("Target.attachToTarget", new
133+
{
134+
targetId = targetInfo.TargetId
135+
}).ConfigureAwait(false)).sessionId;
136+
var session = new CDPSession(this, targetInfo.Type, sessionId);
124137
_sessions.Add(sessionId, session);
125138
return session;
126139
}

0 commit comments

Comments
 (0)