CommonBrowserAdapter.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. using System;
  2. using System.Threading.Tasks;
  3. using Xilium.CefGlue.Common.Events;
  4. using Xilium.CefGlue.Common.Handlers;
  5. using Xilium.CefGlue.Common.Helpers;
  6. using Xilium.CefGlue.Common.Helpers.Logger;
  7. using Xilium.CefGlue.Common.JavascriptExecution;
  8. using Xilium.CefGlue.Common.ObjectBinding;
  9. using Xilium.CefGlue.Common.Platform;
  10. using Xilium.CefGlue.Common.Shared.Helpers;
  11. using Xilium.CefGlue.Common.Shared.RendererProcessCommunication;
  12. using Xilium.CefGlue.Platform.Windows;
  13. namespace Xilium.CefGlue.Common
  14. {
  15. internal class CommonBrowserAdapter : ICefBrowserHost, IDisposable
  16. {
  17. private const string DefaultUrl = "about:blank";
  18. private readonly object _eventsEmitter;
  19. private readonly string _name;
  20. protected readonly ILogger _logger;
  21. private string _initialUrl = DefaultUrl;
  22. private string _title;
  23. private string _tooltip;
  24. private int _maxNativeMethodsParallelCalls = int.MaxValue;
  25. private CefBrowser _browser;
  26. private CommonCefClient _cefClient;
  27. private PipeServer _crashServerPipe;
  28. private string _crashServerPipeName;
  29. private JavascriptExecutionEngine _javascriptExecutionEngine;
  30. private NativeObjectMethodDispatcher _objectMethodDispatcher;
  31. private readonly NativeObjectRegistry _objectRegistry = new NativeObjectRegistry();
  32. private object _disposeLock = new object();
  33. public CommonBrowserAdapter(object eventsEmitter, string name, IControl control, ILogger logger)
  34. {
  35. _eventsEmitter = eventsEmitter;
  36. _name = name;
  37. _logger = logger;
  38. Control = control;
  39. control.GotFocus += HandleGotFocus;
  40. control.SizeChanged += HandleControlSizeChanged;
  41. if (_logger.IsInfoEnabled)
  42. {
  43. _logger.Info($"Browser adapter created (Id:{GetHashCode()}");
  44. }
  45. }
  46. ~CommonBrowserAdapter()
  47. {
  48. Dispose(false);
  49. }
  50. public void Dispose()
  51. {
  52. Dispose(true);
  53. }
  54. public void Dispose(bool disposing)
  55. {
  56. var disposeLock = _disposeLock;
  57. if (disposeLock == null)
  58. {
  59. return; // already disposed
  60. }
  61. lock (disposeLock)
  62. {
  63. if (_disposeLock == null)
  64. {
  65. return; // already disposed
  66. }
  67. _disposeLock = null;
  68. }
  69. if (_logger.IsInfoEnabled)
  70. {
  71. _logger.Info($"Browser adapter disposed (Id:{GetHashCode()}");
  72. }
  73. var browserHost = BrowserHost;
  74. if (browserHost != null)
  75. {
  76. if (disposing)
  77. {
  78. browserHost.CloseBrowser(true);
  79. }
  80. }
  81. if (disposing)
  82. {
  83. InnerDispose();
  84. GC.SuppressFinalize(this);
  85. }
  86. }
  87. protected virtual void InnerDispose() { }
  88. public event LoadStartEventHandler LoadStart;
  89. public event LoadEndEventHandler LoadEnd;
  90. public event LoadingStateChangeEventHandler LoadingStateChange;
  91. public event LoadErrorEventHandler LoadError;
  92. public event Action Initialized;
  93. public event AddressChangedEventHandler AddressChanged;
  94. public event TitleChangedEventHandler TitleChanged;
  95. public event ConsoleMessageEventHandler ConsoleMessage;
  96. public event StatusMessageEventHandler StatusMessage;
  97. public event JavascriptContextLifetimeEventHandler JavascriptContextCreated;
  98. public event JavascriptContextLifetimeEventHandler JavascriptContextReleased;
  99. public event JavascriptUncaughtExceptionEventHandler JavascriptUncaughtException;
  100. public event AsyncUnhandledExceptionEventHandler UnhandledException;
  101. public string Address { get => _browser?.GetMainFrame().Url ?? _initialUrl; set => NavigateTo(value); }
  102. #region Cef Handlers
  103. public ContextMenuHandler ContextMenuHandler { get; set; }
  104. public DialogHandler DialogHandler { get; set; }
  105. public DownloadHandler DownloadHandler { get; set; }
  106. public DragHandler DragHandler { get; set; }
  107. public FindHandler FindHandler { get; set; }
  108. public FocusHandler FocusHandler { get; set; }
  109. public KeyboardHandler KeyboardHandler { get; set; }
  110. public RequestHandler RequestHandler { get; set; }
  111. public LifeSpanHandler LifeSpanHandler { get; set; }
  112. public DisplayHandler DisplayHandler { get; set; }
  113. public RenderHandler RenderHandler { get; set; }
  114. public JSDialogHandler JSDialogHandler { get; set; }
  115. #endregion
  116. protected virtual IControl Control { get; }
  117. protected CefBrowserHost BrowserHost { get; private set; }
  118. protected bool IsBrowserCreated { get; private set; }
  119. public bool IsInitialized => _browser != null;
  120. public bool IsLoading => _browser?.IsLoading ?? false;
  121. public string Title => _title;
  122. public double ZoomLevel
  123. {
  124. get => BrowserHost?.GetZoomLevel() ?? 0;
  125. set => BrowserHost?.SetZoomLevel(value);
  126. }
  127. public bool IsJavascriptEngineInitialized => _javascriptExecutionEngine?.IsMainFrameContextInitialized == true;
  128. public int MaxNativeMethodsParallelCalls
  129. {
  130. get => _maxNativeMethodsParallelCalls;
  131. set
  132. {
  133. if (_objectMethodDispatcher != null)
  134. {
  135. throw new InvalidOperationException($"Cannot set {nameof(MaxNativeMethodsParallelCalls)} after browser has been initialized");
  136. }
  137. _maxNativeMethodsParallelCalls = value;
  138. }
  139. }
  140. public CefBrowserSettings Settings { get; } = new CefBrowserSettings();
  141. public CefBrowser Browser => _browser;
  142. private void NavigateTo(string url)
  143. {
  144. // Remove leading whitespace from the URL
  145. url = url.TrimStart();
  146. /// to play safe, load url must be called after <see cref="OnBrowserCreated(CefBrowser)"/> which runs on CefThreadId.UI,
  147. /// otherwise the navigation will be aborted
  148. ActionTask.Run(() =>
  149. {
  150. if (_browser != null)
  151. {
  152. _browser?.GetMainFrame()?.LoadUrl(url);
  153. }
  154. else if (!string.IsNullOrEmpty(url))
  155. {
  156. _initialUrl = url;
  157. if (IsBrowserCreated)
  158. {
  159. // browser was already created, but not completely initialized, we have to queue url load
  160. void OnBrowserInitialized()
  161. {
  162. Initialized -= OnBrowserInitialized;
  163. ActionTask.Run(() =>
  164. {
  165. _browser?.GetMainFrame()?.LoadUrl(_initialUrl);
  166. _initialUrl = null;
  167. });
  168. }
  169. Initialized += OnBrowserInitialized;
  170. }
  171. }
  172. });
  173. }
  174. public bool CanGoBack()
  175. {
  176. return _browser?.CanGoBack ?? false;
  177. }
  178. public void GoBack()
  179. {
  180. _browser?.GoBack();
  181. }
  182. public bool CanGoForward()
  183. {
  184. return _browser?.CanGoForward ?? false;
  185. }
  186. public void GoForward()
  187. {
  188. _browser?.GoForward();
  189. }
  190. public void Reload(bool ignoreCache)
  191. {
  192. if (ignoreCache)
  193. {
  194. _browser?.ReloadIgnoreCache();
  195. }
  196. else
  197. {
  198. _browser?.Reload();
  199. }
  200. }
  201. public void ExecuteJavaScript(string code, string url, int line)
  202. {
  203. _browser?.GetMainFrame().ExecuteJavaScript(code, url, line);
  204. }
  205. public Task<T> EvaluateJavaScript<T>(string code, string url, int line, string frameName = null, TimeSpan? timeout = null)
  206. {
  207. var frame = frameName != null ? _browser?.GetFrame(frameName) : _browser?.GetMainFrame();
  208. if (frame != null)
  209. {
  210. return EvaluateJavaScript<T>(code, url, line, frame, timeout);
  211. }
  212. return Task.FromResult<T>(default);
  213. }
  214. public Task<T> EvaluateJavaScript<T>(string code, string url, int line, CefFrame frame, TimeSpan? timeout = null)
  215. {
  216. if (frame.IsValid && _javascriptExecutionEngine != null)
  217. {
  218. return _javascriptExecutionEngine.Evaluate<T>(code, url, line, frame, timeout);
  219. }
  220. return Task.FromResult<T>(default);
  221. }
  222. public void ShowDeveloperTools()
  223. {
  224. var windowInfo = CefWindowInfo.Create();
  225. if (CefRuntime.Platform != CefRuntimePlatform.MacOSX)
  226. {
  227. // don't know why but I can't do this on macosx
  228. windowInfo.SetAsPopup(BrowserHost?.GetWindowHandle() ?? IntPtr.Zero, "DevTools");
  229. }
  230. BrowserHost?.ShowDevTools(windowInfo, _cefClient, new CefBrowserSettings(), new CefPoint());
  231. }
  232. public void CloseDeveloperTools()
  233. {
  234. BrowserHost?.CloseDevTools();
  235. }
  236. public void RegisterJavascriptObject(object targetObject, string name, JavascriptObjectMethodCallHandler methodHandler = null)
  237. {
  238. _objectRegistry.Register(targetObject, name, methodHandler);
  239. }
  240. public void UnregisterJavascriptObject(string name)
  241. {
  242. _objectRegistry.Unregister(name);
  243. }
  244. public bool IsJavascriptObjectRegistered(string name)
  245. {
  246. return _objectRegistry.Get(name) != null;
  247. }
  248. public bool CreateBrowser(int width, int height)
  249. {
  250. if (IsBrowserCreated || width < 0 || height < 0)
  251. {
  252. return false;
  253. }
  254. var hostViewHandle = Control.GetHostViewHandle(width, height);
  255. if (hostViewHandle == null)
  256. {
  257. return false;
  258. }
  259. IsBrowserCreated = true;
  260. var windowInfo = CefWindowInfo.Create();
  261. SetupBrowserView(windowInfo, width, height, hostViewHandle.Value);
  262. var cefClient = CreateCefClient();
  263. cefClient.Dispatcher.RegisterMessageHandler(Messages.UnhandledException.Name, OnBrowserProcessUnhandledException);
  264. _cefClient = cefClient;
  265. using (var extraInfo = CefDictionaryValue.Create())
  266. {
  267. // send the name of the crash (side) pipe to the render process
  268. _crashServerPipeName = Guid.NewGuid().ToString();
  269. extraInfo.SetString(Constants.CrashPipeNameKey, _crashServerPipeName);
  270. // This is the first time the window is being rendered, so create it.
  271. CefBrowserHost.CreateBrowser(windowInfo, cefClient, Settings, _initialUrl, extraInfo);
  272. }
  273. return true;
  274. }
  275. protected virtual CommonCefClient CreateCefClient()
  276. {
  277. return new CommonCefClient(this, null, _logger);
  278. }
  279. protected virtual void SetupBrowserView(CefWindowInfo windowInfo, int width, int height, IntPtr hostViewHandle)
  280. {
  281. windowInfo.StyleEx |= WindowStyleEx.WS_EX_NOACTIVATE; // disable window activation (prevent stealing focus)
  282. windowInfo.SetAsChild(hostViewHandle, new CefRectangle(0, 0, width, height));
  283. }
  284. private void OnJavascriptExecutionEngineContextCreated(CefFrame frame)
  285. {
  286. JavascriptContextCreated?.Invoke(_eventsEmitter, new JavascriptContextLifetimeEventArgs(frame));
  287. }
  288. private void OnJavascriptExecutionEngineContextReleased(CefFrame frame)
  289. {
  290. JavascriptContextReleased?.Invoke(_eventsEmitter, new JavascriptContextLifetimeEventArgs(frame));
  291. }
  292. private void OnJavascriptExecutionEngineUncaughtException(JavascriptUncaughtExceptionEventArgs args)
  293. {
  294. JavascriptUncaughtException?.Invoke(_eventsEmitter, args);
  295. }
  296. protected void WithErrorHandling(string scopeName, Action action)
  297. {
  298. try
  299. {
  300. action();
  301. }
  302. catch (Exception ex)
  303. {
  304. HandleException(scopeName, ex);
  305. }
  306. }
  307. protected void HandleException(string scopeName, Exception exception)
  308. {
  309. _logger.ErrorException($"{_name} : Caught exception in {scopeName}()", exception);
  310. UnhandledException?.Invoke(_eventsEmitter, new AsyncUnhandledExceptionEventArgs(exception));
  311. }
  312. protected virtual void HandleGotFocus()
  313. {
  314. WithErrorHandling(nameof(HandleGotFocus), () =>
  315. {
  316. BrowserHost?.SetFocus(true);
  317. });
  318. }
  319. protected virtual void HandleControlSizeChanged(CefSize size)
  320. {
  321. var created = CreateBrowser(size.Width, size.Height);
  322. if (created)
  323. {
  324. Control.SizeChanged -= HandleControlSizeChanged;
  325. }
  326. }
  327. private void OnBrowserProcessUnhandledException(MessageReceivedEventArgs e)
  328. {
  329. var exceptionDetails = Messages.UnhandledException.FromCefMessage(e.Message);
  330. FireBrowserProcessUnhandledExceptionHandler(exceptionDetails.ExceptionType, exceptionDetails.Message, exceptionDetails.StackTrace);
  331. }
  332. private void OnChildProcessCrashed(string message)
  333. {
  334. WithErrorHandling(nameof(OnChildProcessCrashed), () =>
  335. {
  336. var exception = SerializableException.DeserializeFromString(message);
  337. FireBrowserProcessUnhandledExceptionHandler(exception.ExceptionType, exception.Message, exception.StackTrace);
  338. });
  339. }
  340. private void FireBrowserProcessUnhandledExceptionHandler(string exceptionType, string message, string stackTrace)
  341. {
  342. var exception = new RenderProcessUnhandledException(exceptionType, message, stackTrace);
  343. _logger.ErrorException("Browser process unhandled exception", exception);
  344. UnhandledException?.Invoke(_eventsEmitter, new AsyncUnhandledExceptionEventArgs(exception));
  345. }
  346. private void OnBrowserCreated(CefBrowser browser)
  347. {
  348. if (_browser != null)
  349. {
  350. // Make sure we don't initialize ourselves more than once. That seems to break things.
  351. return;
  352. }
  353. WithErrorHandling((nameof(OnBrowserCreated)), () =>
  354. {
  355. _browser = browser;
  356. _crashServerPipe = new PipeServer(_crashServerPipeName);
  357. _crashServerPipe.MessageReceived += OnChildProcessCrashed;
  358. var browserHost = browser.GetHost();
  359. BrowserHost = browserHost;
  360. var dispatcher = _cefClient?.Dispatcher;
  361. if (dispatcher != null)
  362. {
  363. var javascriptExecutionEngine = new JavascriptExecutionEngine(dispatcher);
  364. javascriptExecutionEngine.ContextCreated += OnJavascriptExecutionEngineContextCreated;
  365. javascriptExecutionEngine.ContextReleased += OnJavascriptExecutionEngineContextReleased;
  366. javascriptExecutionEngine.UncaughtException += OnJavascriptExecutionEngineUncaughtException;
  367. _javascriptExecutionEngine = javascriptExecutionEngine;
  368. _objectRegistry.SetBrowser(browser);
  369. _objectMethodDispatcher = new NativeObjectMethodDispatcher(dispatcher, _objectRegistry, MaxNativeMethodsParallelCalls);
  370. }
  371. OnBrowserHostCreated(browserHost);
  372. Initialized?.Invoke();
  373. });
  374. }
  375. protected virtual void OnBrowserHostCreated(CefBrowserHost browserHost)
  376. {
  377. Control.InitializeRender(browserHost.GetWindowHandle());
  378. }
  379. protected virtual bool OnBrowserClose(CefBrowser browser)
  380. {
  381. if (browser.IsPopup)
  382. {
  383. // popup such as devtools, let it close its window
  384. return false;
  385. }
  386. Control.DestroyRender();
  387. Cleanup(browser);
  388. return true;
  389. }
  390. protected void Cleanup(CefBrowser browser)
  391. {
  392. _crashServerPipe?.Dispose();
  393. browser.Dispose();
  394. BrowserHost = null;
  395. _cefClient = null;
  396. _browser = null;
  397. }
  398. #region ICefBrowserHost
  399. void ICefBrowserHost.HandleBrowserCreated(CefBrowser browser)
  400. {
  401. WithErrorHandling((nameof(ICefBrowserHost.HandleBrowserDestroyed)), () =>
  402. {
  403. OnBrowserCreated(browser);
  404. });
  405. }
  406. void ICefBrowserHost.HandleBrowserDestroyed(CefBrowser browser)
  407. {
  408. WithErrorHandling((nameof(ICefBrowserHost.HandleBrowserDestroyed)), () =>
  409. {
  410. _objectMethodDispatcher?.Dispose();
  411. _objectMethodDispatcher = null;
  412. });
  413. }
  414. bool ICefBrowserHost.HandleBrowserClose(CefBrowser browser)
  415. {
  416. var result = false;
  417. WithErrorHandling((nameof(ICefBrowserHost.HandleBrowserClose)), () =>
  418. {
  419. result = OnBrowserClose(browser);
  420. });
  421. return result;
  422. }
  423. bool ICefBrowserHost.HandleTooltip(CefBrowser browser, string text)
  424. {
  425. WithErrorHandling((nameof(ICefBrowserHost.HandleTooltip)), () =>
  426. {
  427. if (_tooltip == text)
  428. {
  429. return;
  430. }
  431. _tooltip = text;
  432. Control.SetTooltip(text);
  433. });
  434. return true;
  435. }
  436. void ICefBrowserHost.HandleAddressChange(CefBrowser browser, CefFrame frame, string url)
  437. {
  438. if (browser.IsPopup || !frame.IsMain)
  439. {
  440. return;
  441. }
  442. AddressChanged?.Invoke(_eventsEmitter, url);
  443. }
  444. void ICefBrowserHost.HandleTitleChange(CefBrowser browser, string title)
  445. {
  446. if (browser.IsPopup)
  447. {
  448. return;
  449. }
  450. _title = title;
  451. TitleChanged?.Invoke(_eventsEmitter, title);
  452. }
  453. void ICefBrowserHost.HandleStatusMessage(CefBrowser browser, string value)
  454. {
  455. StatusMessage?.Invoke(_eventsEmitter, value);
  456. }
  457. bool ICefBrowserHost.HandleConsoleMessage(CefBrowser browser, CefLogSeverity level, string message, string source, int line)
  458. {
  459. var handler = ConsoleMessage;
  460. if (handler != null)
  461. {
  462. var args = new ConsoleMessageEventArgs(level, message, source, line);
  463. ConsoleMessage?.Invoke(_eventsEmitter, args);
  464. return !args.OutputToConsole;
  465. }
  466. return false;
  467. }
  468. void ICefBrowserHost.HandleLoadStart(CefBrowser browser, CefFrame frame, CefTransitionType transitionType)
  469. {
  470. LoadStart?.Invoke(_eventsEmitter, new LoadStartEventArgs(frame));
  471. }
  472. void ICefBrowserHost.HandleLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode)
  473. {
  474. LoadEnd?.Invoke(_eventsEmitter, new LoadEndEventArgs(frame, httpStatusCode));
  475. }
  476. void ICefBrowserHost.HandleLoadError(CefBrowser browser, CefFrame frame, CefErrorCode errorCode, string errorText, string failedUrl)
  477. {
  478. LoadError?.Invoke(_eventsEmitter, new LoadErrorEventArgs(frame, errorCode, errorText, failedUrl));
  479. }
  480. void ICefBrowserHost.HandleLoadingStateChange(CefBrowser browser, bool isLoading, bool canGoBack, bool canGoForward)
  481. {
  482. LoadingStateChange?.Invoke(_eventsEmitter, new LoadingStateChangeEventArgs(isLoading, canGoBack, canGoForward));
  483. }
  484. void ICefBrowserHost.HandleOpenContextMenu(CefContextMenuParams parameters, CefMenuModel model, CefRunContextMenuCallback callback)
  485. {
  486. Control.OpenContextMenu(MenuEntry.FromCefModel(model), parameters.X, parameters.Y, callback);
  487. }
  488. void ICefBrowserHost.HandleCloseContextMenu()
  489. {
  490. Control.CloseContextMenu();
  491. }
  492. void ICefBrowserHost.HandleException(Exception exception)
  493. {
  494. HandleException("Unknown", exception);
  495. }
  496. #endregion
  497. }
  498. }