// 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
{
///
/// DevTool Client
///
public partial class DevToolsClient : IDevToolsMessageObserver, IDevToolsClient
{
private readonly ConcurrentDictionary queuedCommandResults = new ConcurrentDictionary();
private readonly ConcurrentDictionary eventHandlers = new ConcurrentDictionary();
private IBrowser browser;
private IRegistration devToolsRegistration;
private bool devToolsAttached;
private SynchronizationContext syncContext;
private int disposeCount;
///
public event EventHandler DevToolsEvent;
///
public event EventHandler DevToolsEventError;
///
/// Capture the current so
/// continuation executes on the original calling thread. If
/// is null for
///
/// then the continuation will be run on the CEF UI Thread (by default
/// this is not the same as the WPF/WinForms UI Thread).
///
public bool CaptureSyncContext { get; set; }
///
/// When not null provided
/// will be used to run the contination. Defaults to null
/// Setting this property will change
/// to false.
///
public SynchronizationContext SyncContext
{
get { return syncContext; }
set
{
CaptureSyncContext = false;
syncContext = value;
}
}
///
/// DevToolsClient
///
/// Browser associated with this DevTools client
public DevToolsClient(IBrowser browser)
{
this.browser = browser;
CaptureSyncContext = true;
}
///
/// Store a reference to the IRegistration that's returned when
/// you register an observer.
///
/// registration
public void SetDevToolsObserverRegistration(IRegistration devToolsRegistration)
{
this.devToolsRegistration = devToolsRegistration;
}
///
public void AddEventHandler(string eventName, EventHandler eventHandler) where T : EventArgs
{
var eventProxy = eventHandlers.GetOrAdd(eventName, _ => new EventProxy(DeserializeJsonEvent));
var p = (EventProxy)eventProxy;
p.AddHandler(eventHandler);
}
///
public bool RemoveEventHandler(string eventName, EventHandler eventHandler) where T : EventArgs
{
if (eventHandlers.TryGetValue(eventName, out IEventProxy eventProxy))
{
var p = ((EventProxy)eventProxy);
if (p.RemoveHandler(eventHandler))
{
return !eventHandlers.TryRemove(eventName, out _);
}
}
return true;
}
///
/// 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 dictionary contents.
///
/// is the method name
/// are the method parameters represented as a dictionary,
/// which may be empty.
/// return a Task that can be awaited to obtain the method result
public Task ExecuteDevToolsMethodAsync(string method, IDictionary parameters = null)
{
return ExecuteDevToolsMethodAsync(method, parameters);
}
///
/// 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 dictionary contents.
///
/// The type into which the result will be deserialzed.
/// is the method name
/// are the method parameters represented as a dictionary,
/// which may be empty.
/// return a Task that can be awaited to obtain the method result
public Task ExecuteDevToolsMethodAsync(string method, IDictionary 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(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 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);
}
}
///
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();
}
}
}
///
void IDevToolsMessageObserver.OnDevToolsAgentAttached(IBrowser browser)
{
devToolsAttached = true;
}
///
void IDevToolsMessageObserver.OnDevToolsAgentDetached(IBrowser browser)
{
devToolsAttached = false;
}
///
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);
}
}
///
bool IDevToolsMessageObserver.OnDevToolsMessage(IBrowser browser, Stream message)
{
return false;
}
///
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(result);
errorObj.MessageId = messageId;
context.SetException(new DevToolsClientException("DevTools Client Error :" + errorObj.Message, errorObj));
}
}
}
///
/// 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
///
/// Object type
/// event Name
/// JSON stream
/// object of type
private static T DeserializeJsonEvent(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);
}
///
/// 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
///
/// Object type
/// JSON stream
/// object of type
private static T DeserializeJson(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
///
/// 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
///
/// Object type
/// JSON stream
/// object of type
private static object DeserializeJson(Type type, Stream stream)
{
#if NETCOREAPP
// TODO: use synchronus Deserialize(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();
}
}
}
}