Skip to content

Commit a735c68

Browse files
committed
DevTools Client - Add Capture SyncContext by default
- Add option to capture the SyncContext - Add option to provide custom SyncContext (can just pass in UI SyncContext and all continuations will happen there). - Fix bug in ExecuteDevToolsMethodAsync sending the command twice when exeucted on the CEF UI thread
1 parent b99f38b commit a735c68

File tree

2 files changed

+96
-11
lines changed

2 files changed

+96
-11
lines changed

CefSharp/DevTools/DevToolsClient.cs

+71-11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Threading.Tasks;
1212
using CefSharp.Callback;
1313
using CefSharp.Internals;
14+
using CefSharp.Internals.Tasks;
1415

1516
namespace CefSharp.DevTools
1617
{
@@ -19,17 +20,44 @@ namespace CefSharp.DevTools
1920
/// </summary>
2021
public partial class DevToolsClient : IDevToolsMessageObserver, IDevToolsClient
2122
{
22-
private readonly ConcurrentDictionary<int, TaskCompletionSource<DevToolsMethodResponse>> queuedCommandResults = new ConcurrentDictionary<int, TaskCompletionSource<DevToolsMethodResponse>>();
23+
private readonly ConcurrentDictionary<int, SyncContextTaskCompletionSource<DevToolsMethodResponse>> queuedCommandResults = new ConcurrentDictionary<int, SyncContextTaskCompletionSource<DevToolsMethodResponse>>();
2324
private int lastMessageId;
2425
private IBrowser browser;
2526
private IRegistration devToolsRegistration;
2627
private bool devToolsAttached;
28+
private SynchronizationContext syncContext;
2729

2830
/// <summary>
2931
/// DevToolsEvent
3032
/// </summary>
3133
public EventHandler<DevToolsEventArgs> DevToolsEvent;
3234

35+
/// <summary>
36+
/// Capture the current <see cref="SynchronizationContext"/> so
37+
/// continuation executes on the original calling thread. If
38+
/// <see cref="SynchronizationContext.Current"/> is null for
39+
/// <see cref="ExecuteDevToolsMethodAsync(string, IDictionary{string, object})"/>
40+
/// then the continuation will be run on the CEF UI Thread (by default
41+
/// this is not the same as the WPF/WinForms UI Thread).
42+
/// </summary>
43+
public bool CaptureSyncContext { get; set; }
44+
45+
/// <summary>
46+
/// When not null provided <see cref="SynchronizationContext"/>
47+
/// will be used to run the contination. Defaults to null
48+
/// Setting this property will change <see cref="CaptureSyncContext"/>
49+
/// to false.
50+
/// </summary>
51+
public SynchronizationContext SyncContext
52+
{
53+
get { return syncContext; }
54+
set
55+
{
56+
CaptureSyncContext = false;
57+
syncContext = value;
58+
}
59+
}
60+
3361
/// <summary>
3462
/// DevToolsClient
3563
/// </summary>
@@ -39,8 +67,14 @@ public DevToolsClient(IBrowser browser)
3967
this.browser = browser;
4068

4169
lastMessageId = browser.Identifier * 100000;
70+
CaptureSyncContext = true;
4271
}
4372

73+
/// <summary>
74+
/// Store a reference to the IRegistration that's returned when
75+
/// you register an observer.
76+
/// </summary>
77+
/// <param name="devToolsRegistration">registration</param>
4478
public void SetDevToolsObserverRegistration(IRegistration devToolsRegistration)
4579
{
4680
this.devToolsRegistration = devToolsRegistration;
@@ -65,7 +99,9 @@ public async Task<DevToolsMethodResponse> ExecuteDevToolsMethodAsync(string meth
6599

66100
var messageId = Interlocked.Increment(ref lastMessageId);
67101

68-
var taskCompletionSource = new TaskCompletionSource<DevToolsMethodResponse>();
102+
var taskCompletionSource = new SyncContextTaskCompletionSource<DevToolsMethodResponse>();
103+
104+
taskCompletionSource.SyncContext = CaptureSyncContext ? SynchronizationContext.Current : syncContext;
69105

70106
if (!queuedCommandResults.TryAdd(messageId, taskCompletionSource))
71107
{
@@ -74,16 +110,22 @@ public async Task<DevToolsMethodResponse> ExecuteDevToolsMethodAsync(string meth
74110

75111
var browserHost = browser.GetHost();
76112

113+
//Currently on CEF UI Thread we can directly execute
77114
if (CefThread.CurrentlyOnUiThread)
78115
{
79116
var returnedMessageId = browserHost.ExecuteDevToolsMethod(messageId, method, parameters);
80117
if (returnedMessageId == 0)
81118
{
82119
return new DevToolsMethodResponse { Success = false };
83120
}
121+
else if(returnedMessageId != messageId)
122+
{
123+
//For some reason our message Id's don't match
124+
throw new DevToolsClientException(string.Format("Generated MessageId {0} doesn't match returned Message Id {1}", returnedMessageId, messageId));
125+
}
84126
}
85-
86-
if (CefThread.CanExecuteOnUiThread)
127+
//Not on CEF UI Thread we need to use
128+
else if (CefThread.CanExecuteOnUiThread)
87129
{
88130
var returnedMessageId = await CefThread.ExecuteOnUiThread(() =>
89131
{
@@ -94,6 +136,11 @@ public async Task<DevToolsMethodResponse> ExecuteDevToolsMethodAsync(string meth
94136
{
95137
return new DevToolsMethodResponse { Success = false };
96138
}
139+
else if (returnedMessageId != messageId)
140+
{
141+
//For some reason our message Id's don't match
142+
throw new DevToolsClientException(string.Format("Generated MessageId {0} doesn't match returned Message Id {1}", returnedMessageId, messageId));
143+
}
97144
}
98145

99146
return await taskCompletionSource.Task;
@@ -134,7 +181,8 @@ bool IDevToolsMessageObserver.OnDevToolsMessage(IBrowser browser, Stream message
134181

135182
void IDevToolsMessageObserver.OnDevToolsMethodResult(IBrowser browser, int messageId, bool success, Stream result)
136183
{
137-
TaskCompletionSource<DevToolsMethodResponse> taskCompletionSource = null;
184+
var uiThread = CefThread.CurrentlyOnUiThread;
185+
SyncContextTaskCompletionSource<DevToolsMethodResponse> taskCompletionSource = null;
138186

139187
if (queuedCommandResults.TryRemove(messageId, out taskCompletionSource))
140188
{
@@ -149,29 +197,41 @@ void IDevToolsMessageObserver.OnDevToolsMethodResult(IBrowser browser, int messa
149197

150198
result.CopyTo(memoryStream);
151199

152-
153200
methodResult.ResponseAsJsonString = Encoding.UTF8.GetString(memoryStream.ToArray());
154201

202+
Action execute = null;
203+
155204
if (success)
156205
{
157-
Task.Run(() =>
206+
execute = () =>
158207
{
159-
//Make sure continuation runs on Thread Pool
160208
taskCompletionSource.TrySetResult(methodResult);
161-
});
209+
};
162210
}
163211
else
164212
{
165-
Task.Run(() =>
213+
execute = () =>
166214
{
167215
var errorObj = methodResult.DeserializeJson<DevToolsDomainErrorResponse>();
168216
errorObj.MessageId = messageId;
169217

170218
//Make sure continuation runs on Thread Pool
171219
taskCompletionSource.TrySetException(new DevToolsClientException("DevTools Client Error :" + errorObj.Message, errorObj));
172-
});
220+
};
173221
}
174222

223+
var syncContext = taskCompletionSource.SyncContext;
224+
if (syncContext == null)
225+
{
226+
execute();
227+
}
228+
else
229+
{
230+
syncContext.Post(new SendOrPostCallback((o) =>
231+
{
232+
execute();
233+
}), null);
234+
}
175235
}
176236
}
177237
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright © 2020 The CefSharp Authors. All rights reserved.
2+
//
3+
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
4+
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
namespace CefSharp.Internals.Tasks
9+
{
10+
/// <summary>
11+
/// TaskCompletionSource that executes it's continuation on the captured
12+
/// <see cref="SynchronizationContext"/>. If <see cref="SyncContext"/> is null.
13+
/// then the current **executing** thread will be called. e.g. The thread that
14+
/// called <see cref="TaskCompletionSource{TResult}.TrySetResult(TResult)"/>
15+
/// (or other Set/Try set methods).
16+
/// </summary>
17+
/// <typeparam name="TResult">Result Type</typeparam>
18+
public class SyncContextTaskCompletionSource<TResult> : TaskCompletionSource<TResult>
19+
{
20+
/// <summary>
21+
/// Captured Sync Context
22+
/// </summary>
23+
public SynchronizationContext SyncContext { get; set; }
24+
}
25+
}

0 commit comments

Comments
 (0)