using System;
using System.Threading.Tasks;
using Xilium.CefGlue.Common.Events;
using Xilium.CefGlue.Common.Handlers;
using Xilium.CefGlue.Common.Helpers;
using Xilium.CefGlue.Common.Helpers.Logger;
using Xilium.CefGlue.Common.JavascriptExecution;
using Xilium.CefGlue.Common.ObjectBinding;
using Xilium.CefGlue.Common.Platform;
using Xilium.CefGlue.Common.Shared.Helpers;
using Xilium.CefGlue.Common.Shared.RendererProcessCommunication;
using Xilium.CefGlue.Platform.Windows;
namespace Xilium.CefGlue.Common
{
internal class CommonBrowserAdapter : ICefBrowserHost, IDisposable
{
private const string DefaultUrl = "about:blank";
private readonly object _eventsEmitter;
private readonly string _name;
protected readonly ILogger _logger;
private string _initialUrl = DefaultUrl;
private string _title;
private string _tooltip;
private int _maxNativeMethodsParallelCalls = int.MaxValue;
private CefBrowser _browser;
private CommonCefClient _cefClient;
private PipeServer _crashServerPipe;
private string _crashServerPipeName;
private JavascriptExecutionEngine _javascriptExecutionEngine;
private NativeObjectMethodDispatcher _objectMethodDispatcher;
private readonly NativeObjectRegistry _objectRegistry = new NativeObjectRegistry();
private object _disposeLock = new object();
public CommonBrowserAdapter(object eventsEmitter, string name, IControl control, ILogger logger)
{
_eventsEmitter = eventsEmitter;
_name = name;
_logger = logger;
Control = control;
control.GotFocus += HandleGotFocus;
control.SizeChanged += HandleControlSizeChanged;
if (_logger.IsInfoEnabled)
{
_logger.Info($"Browser adapter created (Id:{GetHashCode()}");
}
}
~CommonBrowserAdapter()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
}
public void Dispose(bool disposing)
{
var disposeLock = _disposeLock;
if (disposeLock == null)
{
return; // already disposed
}
lock (disposeLock)
{
if (_disposeLock == null)
{
return; // already disposed
}
_disposeLock = null;
}
if (_logger.IsInfoEnabled)
{
_logger.Info($"Browser adapter disposed (Id:{GetHashCode()}");
}
var browserHost = BrowserHost;
if (browserHost != null)
{
if (disposing)
{
browserHost.CloseBrowser(true);
}
}
if (disposing)
{
InnerDispose();
GC.SuppressFinalize(this);
}
}
protected virtual void InnerDispose() { }
public event LoadStartEventHandler LoadStart;
public event LoadEndEventHandler LoadEnd;
public event LoadingStateChangeEventHandler LoadingStateChange;
public event LoadErrorEventHandler LoadError;
public event Action Initialized;
public event AddressChangedEventHandler AddressChanged;
public event TitleChangedEventHandler TitleChanged;
public event ConsoleMessageEventHandler ConsoleMessage;
public event StatusMessageEventHandler StatusMessage;
public event JavascriptContextLifetimeEventHandler JavascriptContextCreated;
public event JavascriptContextLifetimeEventHandler JavascriptContextReleased;
public event JavascriptUncaughtExceptionEventHandler JavascriptUncaughtException;
public event AsyncUnhandledExceptionEventHandler UnhandledException;
public string Address { get => _browser?.GetMainFrame().Url ?? _initialUrl; set => NavigateTo(value); }
#region Cef Handlers
public ContextMenuHandler ContextMenuHandler { get; set; }
public DialogHandler DialogHandler { get; set; }
public DownloadHandler DownloadHandler { get; set; }
public DragHandler DragHandler { get; set; }
public FindHandler FindHandler { get; set; }
public FocusHandler FocusHandler { get; set; }
public KeyboardHandler KeyboardHandler { get; set; }
public RequestHandler RequestHandler { get; set; }
public LifeSpanHandler LifeSpanHandler { get; set; }
public DisplayHandler DisplayHandler { get; set; }
public RenderHandler RenderHandler { get; set; }
public JSDialogHandler JSDialogHandler { get; set; }
#endregion
protected virtual IControl Control { get; }
protected CefBrowserHost BrowserHost { get; private set; }
protected bool IsBrowserCreated { get; private set; }
public bool IsInitialized => _browser != null;
public bool IsLoading => _browser?.IsLoading ?? false;
public string Title => _title;
public double ZoomLevel
{
get => BrowserHost?.GetZoomLevel() ?? 0;
set => BrowserHost?.SetZoomLevel(value);
}
public bool IsJavascriptEngineInitialized => _javascriptExecutionEngine?.IsMainFrameContextInitialized == true;
public int MaxNativeMethodsParallelCalls
{
get => _maxNativeMethodsParallelCalls;
set
{
if (_objectMethodDispatcher != null)
{
throw new InvalidOperationException($"Cannot set {nameof(MaxNativeMethodsParallelCalls)} after browser has been initialized");
}
_maxNativeMethodsParallelCalls = value;
}
}
public CefBrowserSettings Settings { get; } = new CefBrowserSettings();
public CefBrowser Browser => _browser;
private void NavigateTo(string url)
{
// Remove leading whitespace from the URL
url = url.TrimStart();
/// to play safe, load url must be called after which runs on CefThreadId.UI,
/// otherwise the navigation will be aborted
ActionTask.Run(() =>
{
if (_browser != null)
{
_browser?.GetMainFrame()?.LoadUrl(url);
}
else if (!string.IsNullOrEmpty(url))
{
_initialUrl = url;
if (IsBrowserCreated)
{
// browser was already created, but not completely initialized, we have to queue url load
void OnBrowserInitialized()
{
Initialized -= OnBrowserInitialized;
ActionTask.Run(() =>
{
_browser?.GetMainFrame()?.LoadUrl(_initialUrl);
_initialUrl = null;
});
}
Initialized += OnBrowserInitialized;
}
}
});
}
public bool CanGoBack()
{
return _browser?.CanGoBack ?? false;
}
public void GoBack()
{
_browser?.GoBack();
}
public bool CanGoForward()
{
return _browser?.CanGoForward ?? false;
}
public void GoForward()
{
_browser?.GoForward();
}
public void Reload(bool ignoreCache)
{
if (ignoreCache)
{
_browser?.ReloadIgnoreCache();
}
else
{
_browser?.Reload();
}
}
public void ExecuteJavaScript(string code, string url, int line)
{
_browser?.GetMainFrame().ExecuteJavaScript(code, url, line);
}
public Task EvaluateJavaScript(string code, string url, int line, string frameName = null, TimeSpan? timeout = null)
{
var frame = frameName != null ? _browser?.GetFrame(frameName) : _browser?.GetMainFrame();
if (frame != null)
{
return EvaluateJavaScript(code, url, line, frame, timeout);
}
return Task.FromResult(default);
}
public Task EvaluateJavaScript(string code, string url, int line, CefFrame frame, TimeSpan? timeout = null)
{
if (frame.IsValid && _javascriptExecutionEngine != null)
{
return _javascriptExecutionEngine.Evaluate(code, url, line, frame, timeout);
}
return Task.FromResult(default);
}
public void ShowDeveloperTools()
{
var windowInfo = CefWindowInfo.Create();
if (CefRuntime.Platform != CefRuntimePlatform.MacOSX)
{
// don't know why but I can't do this on macosx
windowInfo.SetAsPopup(BrowserHost?.GetWindowHandle() ?? IntPtr.Zero, "DevTools");
}
BrowserHost?.ShowDevTools(windowInfo, _cefClient, new CefBrowserSettings(), new CefPoint());
}
public void CloseDeveloperTools()
{
BrowserHost?.CloseDevTools();
}
public void RegisterJavascriptObject(object targetObject, string name, JavascriptObjectMethodCallHandler methodHandler = null)
{
_objectRegistry.Register(targetObject, name, methodHandler);
}
public void UnregisterJavascriptObject(string name)
{
_objectRegistry.Unregister(name);
}
public bool IsJavascriptObjectRegistered(string name)
{
return _objectRegistry.Get(name) != null;
}
public bool CreateBrowser(int width, int height)
{
if (IsBrowserCreated || width < 0 || height < 0)
{
return false;
}
var hostViewHandle = Control.GetHostViewHandle(width, height);
if (hostViewHandle == null)
{
return false;
}
IsBrowserCreated = true;
var windowInfo = CefWindowInfo.Create();
SetupBrowserView(windowInfo, width, height, hostViewHandle.Value);
var cefClient = CreateCefClient();
cefClient.Dispatcher.RegisterMessageHandler(Messages.UnhandledException.Name, OnBrowserProcessUnhandledException);
_cefClient = cefClient;
using (var extraInfo = CefDictionaryValue.Create())
{
// send the name of the crash (side) pipe to the render process
_crashServerPipeName = Guid.NewGuid().ToString();
extraInfo.SetString(Constants.CrashPipeNameKey, _crashServerPipeName);
// This is the first time the window is being rendered, so create it.
CefBrowserHost.CreateBrowser(windowInfo, cefClient, Settings, _initialUrl, extraInfo);
}
return true;
}
protected virtual CommonCefClient CreateCefClient()
{
return new CommonCefClient(this, null, _logger);
}
protected virtual void SetupBrowserView(CefWindowInfo windowInfo, int width, int height, IntPtr hostViewHandle)
{
windowInfo.StyleEx |= WindowStyleEx.WS_EX_NOACTIVATE; // disable window activation (prevent stealing focus)
windowInfo.SetAsChild(hostViewHandle, new CefRectangle(0, 0, width, height));
}
private void OnJavascriptExecutionEngineContextCreated(CefFrame frame)
{
JavascriptContextCreated?.Invoke(_eventsEmitter, new JavascriptContextLifetimeEventArgs(frame));
}
private void OnJavascriptExecutionEngineContextReleased(CefFrame frame)
{
JavascriptContextReleased?.Invoke(_eventsEmitter, new JavascriptContextLifetimeEventArgs(frame));
}
private void OnJavascriptExecutionEngineUncaughtException(JavascriptUncaughtExceptionEventArgs args)
{
JavascriptUncaughtException?.Invoke(_eventsEmitter, args);
}
protected void WithErrorHandling(string scopeName, Action action)
{
try
{
action();
}
catch (Exception ex)
{
HandleException(scopeName, ex);
}
}
protected void HandleException(string scopeName, Exception exception)
{
_logger.ErrorException($"{_name} : Caught exception in {scopeName}()", exception);
UnhandledException?.Invoke(_eventsEmitter, new AsyncUnhandledExceptionEventArgs(exception));
}
protected virtual void HandleGotFocus()
{
WithErrorHandling(nameof(HandleGotFocus), () =>
{
BrowserHost?.SetFocus(true);
});
}
protected virtual void HandleControlSizeChanged(CefSize size)
{
var created = CreateBrowser(size.Width, size.Height);
if (created)
{
Control.SizeChanged -= HandleControlSizeChanged;
}
}
private void OnBrowserProcessUnhandledException(MessageReceivedEventArgs e)
{
var exceptionDetails = Messages.UnhandledException.FromCefMessage(e.Message);
FireBrowserProcessUnhandledExceptionHandler(exceptionDetails.ExceptionType, exceptionDetails.Message, exceptionDetails.StackTrace);
}
private void OnChildProcessCrashed(string message)
{
WithErrorHandling(nameof(OnChildProcessCrashed), () =>
{
var exception = SerializableException.DeserializeFromString(message);
FireBrowserProcessUnhandledExceptionHandler(exception.ExceptionType, exception.Message, exception.StackTrace);
});
}
private void FireBrowserProcessUnhandledExceptionHandler(string exceptionType, string message, string stackTrace)
{
var exception = new RenderProcessUnhandledException(exceptionType, message, stackTrace);
_logger.ErrorException("Browser process unhandled exception", exception);
UnhandledException?.Invoke(_eventsEmitter, new AsyncUnhandledExceptionEventArgs(exception));
}
private void OnBrowserCreated(CefBrowser browser)
{
if (_browser != null)
{
// Make sure we don't initialize ourselves more than once. That seems to break things.
return;
}
WithErrorHandling((nameof(OnBrowserCreated)), () =>
{
_browser = browser;
_crashServerPipe = new PipeServer(_crashServerPipeName);
_crashServerPipe.MessageReceived += OnChildProcessCrashed;
var browserHost = browser.GetHost();
BrowserHost = browserHost;
var dispatcher = _cefClient?.Dispatcher;
if (dispatcher != null)
{
var javascriptExecutionEngine = new JavascriptExecutionEngine(dispatcher);
javascriptExecutionEngine.ContextCreated += OnJavascriptExecutionEngineContextCreated;
javascriptExecutionEngine.ContextReleased += OnJavascriptExecutionEngineContextReleased;
javascriptExecutionEngine.UncaughtException += OnJavascriptExecutionEngineUncaughtException;
_javascriptExecutionEngine = javascriptExecutionEngine;
_objectRegistry.SetBrowser(browser);
_objectMethodDispatcher = new NativeObjectMethodDispatcher(dispatcher, _objectRegistry, MaxNativeMethodsParallelCalls);
}
OnBrowserHostCreated(browserHost);
Initialized?.Invoke();
});
}
protected virtual void OnBrowserHostCreated(CefBrowserHost browserHost)
{
Control.InitializeRender(browserHost.GetWindowHandle());
}
protected virtual bool OnBrowserClose(CefBrowser browser)
{
if (browser.IsPopup)
{
// popup such as devtools, let it close its window
return false;
}
Control.DestroyRender();
Cleanup(browser);
return true;
}
protected void Cleanup(CefBrowser browser)
{
_crashServerPipe?.Dispose();
browser.Dispose();
BrowserHost = null;
_cefClient = null;
_browser = null;
}
#region ICefBrowserHost
void ICefBrowserHost.HandleBrowserCreated(CefBrowser browser)
{
WithErrorHandling((nameof(ICefBrowserHost.HandleBrowserDestroyed)), () =>
{
OnBrowserCreated(browser);
});
}
void ICefBrowserHost.HandleBrowserDestroyed(CefBrowser browser)
{
WithErrorHandling((nameof(ICefBrowserHost.HandleBrowserDestroyed)), () =>
{
_objectMethodDispatcher?.Dispose();
_objectMethodDispatcher = null;
});
}
bool ICefBrowserHost.HandleBrowserClose(CefBrowser browser)
{
var result = false;
WithErrorHandling((nameof(ICefBrowserHost.HandleBrowserClose)), () =>
{
result = OnBrowserClose(browser);
});
return result;
}
bool ICefBrowserHost.HandleTooltip(CefBrowser browser, string text)
{
WithErrorHandling((nameof(ICefBrowserHost.HandleTooltip)), () =>
{
if (_tooltip == text)
{
return;
}
_tooltip = text;
Control.SetTooltip(text);
});
return true;
}
void ICefBrowserHost.HandleAddressChange(CefBrowser browser, CefFrame frame, string url)
{
if (browser.IsPopup || !frame.IsMain)
{
return;
}
AddressChanged?.Invoke(_eventsEmitter, url);
}
void ICefBrowserHost.HandleTitleChange(CefBrowser browser, string title)
{
if (browser.IsPopup)
{
return;
}
_title = title;
TitleChanged?.Invoke(_eventsEmitter, title);
}
void ICefBrowserHost.HandleStatusMessage(CefBrowser browser, string value)
{
StatusMessage?.Invoke(_eventsEmitter, value);
}
bool ICefBrowserHost.HandleConsoleMessage(CefBrowser browser, CefLogSeverity level, string message, string source, int line)
{
var handler = ConsoleMessage;
if (handler != null)
{
var args = new ConsoleMessageEventArgs(level, message, source, line);
ConsoleMessage?.Invoke(_eventsEmitter, args);
return !args.OutputToConsole;
}
return false;
}
void ICefBrowserHost.HandleLoadStart(CefBrowser browser, CefFrame frame, CefTransitionType transitionType)
{
LoadStart?.Invoke(_eventsEmitter, new LoadStartEventArgs(frame));
}
void ICefBrowserHost.HandleLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode)
{
LoadEnd?.Invoke(_eventsEmitter, new LoadEndEventArgs(frame, httpStatusCode));
}
void ICefBrowserHost.HandleLoadError(CefBrowser browser, CefFrame frame, CefErrorCode errorCode, string errorText, string failedUrl)
{
LoadError?.Invoke(_eventsEmitter, new LoadErrorEventArgs(frame, errorCode, errorText, failedUrl));
}
void ICefBrowserHost.HandleLoadingStateChange(CefBrowser browser, bool isLoading, bool canGoBack, bool canGoForward)
{
LoadingStateChange?.Invoke(_eventsEmitter, new LoadingStateChangeEventArgs(isLoading, canGoBack, canGoForward));
}
void ICefBrowserHost.HandleOpenContextMenu(CefContextMenuParams parameters, CefMenuModel model, CefRunContextMenuCallback callback)
{
Control.OpenContextMenu(MenuEntry.FromCefModel(model), parameters.X, parameters.Y, callback);
}
void ICefBrowserHost.HandleCloseContextMenu()
{
Control.CloseContextMenu();
}
void ICefBrowserHost.HandleException(Exception exception)
{
HandleException("Unknown", exception);
}
#endregion
}
}