// Copyright © 2020 The CefSharp Authors. All rights reserved.
|
//
|
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
|
|
using System;
|
using System.Collections.Concurrent;
|
using System.Collections.Generic;
|
using System.IO;
|
using System.Text;
|
using System.Threading;
|
using System.Threading.Tasks;
|
using CefSharp.Callback;
|
using CefSharp.Internals;
|
|
namespace CefSharp.DevTools
|
{
|
/// <summary>
|
/// DevTool Client
|
/// </summary>
|
public partial class DevToolsClient : IDevToolsMessageObserver, IDevToolsClient
|
{
|
private readonly ConcurrentDictionary<int, DevToolsMethodResponseContext> queuedCommandResults = new ConcurrentDictionary<int, DevToolsMethodResponseContext>();
|
private readonly ConcurrentDictionary<string, IEventProxy> eventHandlers = new ConcurrentDictionary<string, IEventProxy>();
|
private IBrowser browser;
|
private IRegistration devToolsRegistration;
|
private bool devToolsAttached;
|
private SynchronizationContext syncContext;
|
private int disposeCount;
|
|
/// <inheritdoc/>
|
public event EventHandler<DevToolsEventArgs> DevToolsEvent;
|
|
/// <inheritdoc/>
|
public event EventHandler<DevToolsErrorEventArgs> DevToolsEventError;
|
|
/// <summary>
|
/// Capture the current <see cref="SynchronizationContext"/> so
|
/// continuation executes on the original calling thread. If
|
/// <see cref="SynchronizationContext.Current"/> is null for
|
/// <see cref="ExecuteDevToolsMethodAsync(string, IDictionary{string, object})"/>
|
/// then the continuation will be run on the CEF UI Thread (by default
|
/// this is not the same as the WPF/WinForms UI Thread).
|
/// </summary>
|
public bool CaptureSyncContext { get; set; }
|
|
/// <summary>
|
/// When not null provided <see cref="SynchronizationContext"/>
|
/// will be used to run the contination. Defaults to null
|
/// Setting this property will change <see cref="CaptureSyncContext"/>
|
/// to false.
|
/// </summary>
|
public SynchronizationContext SyncContext
|
{
|
get { return syncContext; }
|
set
|
{
|
CaptureSyncContext = false;
|
syncContext = value;
|
}
|
}
|
|
/// <summary>
|
/// DevToolsClient
|
/// </summary>
|
/// <param name="browser">Browser associated with this DevTools client</param>
|
public DevToolsClient(IBrowser browser)
|
{
|
this.browser = browser;
|
|
CaptureSyncContext = true;
|
}
|
|
/// <summary>
|
/// Store a reference to the IRegistration that's returned when
|
/// you register an observer.
|
/// </summary>
|
/// <param name="devToolsRegistration">registration</param>
|
public void SetDevToolsObserverRegistration(IRegistration devToolsRegistration)
|
{
|
this.devToolsRegistration = devToolsRegistration;
|
}
|
|
/// <inheritdoc/>
|
public void AddEventHandler<T>(string eventName, EventHandler<T> eventHandler) where T : EventArgs
|
{
|
var eventProxy = eventHandlers.GetOrAdd(eventName, _ => new EventProxy<T>(DeserializeJsonEvent<T>));
|
|
var p = (EventProxy<T>)eventProxy;
|
|
p.AddHandler(eventHandler);
|
}
|
|
/// <inheritdoc/>
|
public bool RemoveEventHandler<T>(string eventName, EventHandler<T> eventHandler) where T : EventArgs
|
{
|
if (eventHandlers.TryGetValue(eventName, out IEventProxy eventProxy))
|
{
|
var p = ((EventProxy<T>)eventProxy);
|
|
if (p.RemoveHandler(eventHandler))
|
{
|
return !eventHandlers.TryRemove(eventName, out _);
|
}
|
}
|
|
return true;
|
}
|
|
/// <summary>
|
/// Execute a method call over the DevTools protocol. This method can be called on any thread.
|
/// See the DevTools protocol documentation at https://chromedevtools.github.io/devtools-protocol/ for details
|
/// of supported methods and the expected <paramref name="parameters"/> dictionary contents.
|
/// </summary>
|
/// <param name="method">is the method name</param>
|
/// <param name="parameters">are the method parameters represented as a dictionary,
|
/// which may be empty.</param>
|
/// <returns>return a Task that can be awaited to obtain the method result</returns>
|
public Task<DevToolsMethodResponse> ExecuteDevToolsMethodAsync(string method, IDictionary<string, object> parameters = null)
|
{
|
return ExecuteDevToolsMethodAsync<DevToolsMethodResponse>(method, parameters);
|
}
|
|
/// <summary>
|
/// Execute a method call over the DevTools protocol. This method can be called on any thread.
|
/// See the DevTools protocol documentation at https://chromedevtools.github.io/devtools-protocol/ for details
|
/// of supported methods and the expected <paramref name="parameters"/> dictionary contents.
|
/// </summary>
|
/// <typeparam name="T">The type into which the result will be deserialzed.</typeparam>
|
/// <param name="method">is the method name</param>
|
/// <param name="parameters">are the method parameters represented as a dictionary,
|
/// which may be empty.</param>
|
/// <returns>return a Task that can be awaited to obtain the method result</returns>
|
public Task<T> ExecuteDevToolsMethodAsync<T>(string method, IDictionary<string, object> parameters = null) where T : DevToolsDomainResponseBase
|
{
|
if (browser == null || browser.IsDisposed)
|
{
|
//TODO: Queue up commands where possible
|
throw new ObjectDisposedException(nameof(IBrowser));
|
}
|
|
var taskCompletionSource = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var methodResultContext = new DevToolsMethodResponseContext(
|
type: typeof(T),
|
setResult: o => taskCompletionSource.TrySetResult((T)o),
|
setException: taskCompletionSource.TrySetException,
|
syncContext: CaptureSyncContext ? SynchronizationContext.Current : SyncContext
|
);
|
|
var browserHost = browser.GetHost();
|
|
var messageId = browserHost.GetNextDevToolsMessageId();
|
|
if (!queuedCommandResults.TryAdd(messageId, methodResultContext))
|
{
|
throw new DevToolsClientException(string.Format("Unable to add MessageId {0} to queuedCommandResults ConcurrentDictionary.", messageId));
|
}
|
|
//Currently on CEF UI Thread we can directly execute
|
if (CefThread.CurrentlyOnUiThread)
|
{
|
ExecuteDevToolsMethod(browserHost, messageId, method, parameters, methodResultContext);
|
}
|
//ExecuteDevToolsMethod can only be called on the CEF UI Thread
|
else if (CefThread.CanExecuteOnUiThread)
|
{
|
CefThread.ExecuteOnUiThread(() =>
|
{
|
ExecuteDevToolsMethod(browserHost, messageId, method, parameters, methodResultContext);
|
});
|
}
|
else
|
{
|
queuedCommandResults.TryRemove(messageId, out methodResultContext);
|
throw new DevToolsClientException("Unable to invoke ExecuteDevToolsMethod on CEF UI Thread.");
|
}
|
|
return taskCompletionSource.Task;
|
}
|
|
private void ExecuteDevToolsMethod(IBrowserHost browserHost, int messageId, string method, IDictionary<string, object> parameters, DevToolsMethodResponseContext methodResultContext)
|
{
|
try
|
{
|
var returnedMessageId = browserHost.ExecuteDevToolsMethod(messageId, method, parameters);
|
if (returnedMessageId == 0)
|
{
|
throw new DevToolsClientException(string.Format("Failed to execute dev tools method {0}.", method));
|
}
|
else if (returnedMessageId != messageId)
|
{
|
//For some reason our message Id's don't match
|
throw new DevToolsClientException(string.Format("Generated MessageId {0} doesn't match returned Message Id {1}", returnedMessageId, messageId));
|
}
|
}
|
catch (Exception ex)
|
{
|
queuedCommandResults.TryRemove(messageId, out _);
|
methodResultContext.SetException(ex);
|
}
|
}
|
|
/// <inheritdoc/>
|
public void Dispose()
|
{
|
//Dispose can be called from different Threads
|
//CEF maintains a reference and the user
|
//maintains a reference, we in a rare case
|
//we end up disposing of #3725 twice from different
|
//threads. This will ensure our dispose only runs once.
|
if (Interlocked.Increment(ref disposeCount) == 1)
|
{
|
DevToolsEvent = null;
|
devToolsRegistration?.Dispose();
|
devToolsRegistration = null;
|
browser = null;
|
|
var events = eventHandlers.Values;
|
eventHandlers.Clear();
|
|
foreach (var evt in events)
|
{
|
evt.Dispose();
|
}
|
}
|
}
|
|
/// <inheritdoc/>
|
void IDevToolsMessageObserver.OnDevToolsAgentAttached(IBrowser browser)
|
{
|
devToolsAttached = true;
|
}
|
|
/// <inheritdoc/>
|
void IDevToolsMessageObserver.OnDevToolsAgentDetached(IBrowser browser)
|
{
|
devToolsAttached = false;
|
}
|
|
/// <inheritdoc/>
|
void IDevToolsMessageObserver.OnDevToolsEvent(IBrowser browser, string method, Stream parameters)
|
{
|
try
|
{
|
var evt = DevToolsEvent;
|
|
//Only parse the data if we have an event handler
|
if (evt != null)
|
{
|
var paramsAsJsonString = StreamToString(parameters, leaveOpen: true);
|
|
evt(this, new DevToolsEventArgs(method, paramsAsJsonString));
|
}
|
|
if (eventHandlers.TryGetValue(method, out IEventProxy eventProxy))
|
{
|
eventProxy.Raise(this, method, parameters, SyncContext);
|
}
|
}
|
catch (Exception ex)
|
{
|
var errorEvent = DevToolsEventError;
|
|
var json = "";
|
|
if (parameters.Length > 0)
|
{
|
parameters.Position = 0;
|
|
try
|
{
|
json = StreamToString(parameters, leaveOpen: false);
|
}
|
catch (Exception)
|
{
|
//TODO: do we somehow pass this exception to the user?
|
}
|
}
|
|
var args = new DevToolsErrorEventArgs(method, json, ex);
|
|
errorEvent?.Invoke(this, args);
|
}
|
}
|
|
/// <inheritdoc/>
|
bool IDevToolsMessageObserver.OnDevToolsMessage(IBrowser browser, Stream message)
|
{
|
return false;
|
}
|
|
/// <inheritdoc/>
|
void IDevToolsMessageObserver.OnDevToolsMethodResult(IBrowser browser, int messageId, bool success, Stream result)
|
{
|
DevToolsMethodResponseContext context;
|
if (queuedCommandResults.TryRemove(messageId, out context))
|
{
|
if (success)
|
{
|
if (context.Type == typeof(DevToolsMethodResponse) || context.Type == typeof(DevToolsDomainResponseBase))
|
{
|
context.SetResult(new DevToolsMethodResponse
|
{
|
Success = success,
|
MessageId = messageId,
|
ResponseAsJsonString = StreamToString(result),
|
});
|
}
|
else
|
{
|
try
|
{
|
context.SetResult(DeserializeJson(context.Type, result));
|
}
|
catch (Exception ex)
|
{
|
context.SetException(ex);
|
}
|
}
|
}
|
else
|
{
|
var errorObj = DeserializeJson<DevToolsDomainErrorResponse>(result);
|
errorObj.MessageId = messageId;
|
|
context.SetException(new DevToolsClientException("DevTools Client Error :" + errorObj.Message, errorObj));
|
}
|
}
|
}
|
|
|
/// <summary>
|
/// Deserialize the JSON stream into a .Net object.
|
/// For .Net Core/.Net 5.0 uses System.Text.Json
|
/// for .Net 4.6.2 uses System.Runtime.Serialization.Json
|
/// </summary>
|
/// <typeparam name="T">Object type</typeparam>
|
/// <param name="eventName">event Name</param>
|
/// <param name="stream">JSON stream</param>
|
/// <returns>object of type <typeparamref name="T"/></returns>
|
private static T DeserializeJsonEvent<T>(string eventName, Stream stream) where T : EventArgs
|
{
|
if (typeof(T) == typeof(EventArgs))
|
{
|
return (T)EventArgs.Empty;
|
}
|
|
if (typeof(T) == typeof(DevToolsEventArgs))
|
{
|
var paramsAsJsonString = StreamToString(stream, leaveOpen: true);
|
var args = new DevToolsEventArgs(eventName, paramsAsJsonString);
|
|
return (T)(object)args;
|
}
|
|
return (T)DeserializeJson(typeof(T), stream);
|
}
|
|
/// <summary>
|
/// Deserialize the JSON stream into a .Net object.
|
/// For .Net Core/.Net 5.0 uses System.Text.Json
|
/// for .Net 4.6.2 uses System.Runtime.Serialization.Json
|
/// </summary>
|
/// <typeparam name="T">Object type</typeparam>
|
/// <param name="stream">JSON stream</param>
|
/// <returns>object of type <typeparamref name="T"/></returns>
|
private static T DeserializeJson<T>(Stream stream)
|
{
|
return (T)DeserializeJson(typeof(T), stream);
|
}
|
|
#if NETCOREAPP
|
private static readonly System.Text.Json.JsonSerializerOptions DefaultJsonSerializerOptions = new System.Text.Json.JsonSerializerOptions
|
{
|
PropertyNameCaseInsensitive = true,
|
IgnoreNullValues = true,
|
Converters = { new CefSharp.Internals.Json.JsonEnumConverterFactory() },
|
};
|
#else
|
private static readonly System.Runtime.Serialization.Json.DataContractJsonSerializerSettings DefaultJsonSerializerSettings = new System.Runtime.Serialization.Json.DataContractJsonSerializerSettings
|
{
|
UseSimpleDictionaryFormat = true,
|
};
|
#endif
|
|
/// <summary>
|
/// Deserialize the JSON stream into a .Net object.
|
/// For .Net Core/.Net 5.0 uses System.Text.Json
|
/// for .Net 4.6.2 uses System.Runtime.Serialization.Json
|
/// </summary>
|
/// <param name="type">Object type</param>
|
/// <param name="stream">JSON stream</param>
|
/// <returns>object of type <paramref name="type"/></returns>
|
private static object DeserializeJson(Type type, Stream stream)
|
{
|
#if NETCOREAPP
|
// TODO: use synchronus Deserialize<T>(Stream) when System.Text.Json gets updated
|
var memoryStream = new MemoryStream((int)stream.Length);
|
stream.CopyTo(memoryStream);
|
|
return System.Text.Json.JsonSerializer.Deserialize(memoryStream.ToArray(), type, DefaultJsonSerializerOptions);
|
#else
|
var dcs = new System.Runtime.Serialization.Json.DataContractJsonSerializer(type, DefaultJsonSerializerSettings);
|
return dcs.ReadObject(stream);
|
#endif
|
}
|
|
private static string StreamToString(Stream stream, bool leaveOpen = false)
|
{
|
using (var streamReader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: leaveOpen))
|
{
|
return streamReader.ReadToEnd();
|
}
|
}
|
}
|
}
|