// 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(); } } } }