1221 lines
40 KiB
C#
1221 lines
40 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Windows.Forms;
|
|
using Lskj.Util;
|
|
using Xilium.CefGlue;
|
|
|
|
namespace Lskj.Control
|
|
{
|
|
/// <summary>
|
|
/// Keeps an application-owned root HWND alive for the complete CEF
|
|
/// lifecycle. The root is created independently of recreatable WinForms
|
|
/// control handles, then embedded after CEF reports OnAfterCreated.
|
|
/// </summary>
|
|
internal sealed class CefBrowserLifetimeCoordinator
|
|
{
|
|
// CEF 生命周期诊断默认关闭,避免正常创建和销毁浏览器时持续写入日志。
|
|
internal static readonly bool LifecycleTracingEnabled = false;
|
|
private static readonly CefBrowserSettings _sharedBrowserSettings =
|
|
new CefBrowserSettings();
|
|
private readonly object _hostWindowSyncRoot = new object();
|
|
private CefBrowser _browser;
|
|
private CefBrowserHostWindow _hostWindow;
|
|
private int _browserCreationPending;
|
|
private int _browserCloseRequested;
|
|
private int _browserCloseApproved;
|
|
private int _browserBeforeCloseReceived;
|
|
private int _disposeRequested;
|
|
private int _ownerHandleDestroying;
|
|
|
|
public CefBrowser Browser
|
|
{
|
|
get { return Interlocked.CompareExchange(ref _browser, null, null); }
|
|
}
|
|
|
|
public static CefBrowserSettings SharedBrowserSettings
|
|
{
|
|
get { return _sharedBrowserSettings; }
|
|
}
|
|
|
|
public bool IsDisposing
|
|
{
|
|
get { return Thread.VolatileRead(ref _disposeRequested) != 0; }
|
|
}
|
|
|
|
public bool IsUnavailable
|
|
{
|
|
get
|
|
{
|
|
return IsDisposing ||
|
|
Thread.VolatileRead(ref _ownerHandleDestroying) != 0;
|
|
}
|
|
}
|
|
|
|
public bool HasBrowserOrPendingCreation
|
|
{
|
|
get
|
|
{
|
|
return Browser != null ||
|
|
Thread.VolatileRead(ref _browserCreationPending) != 0;
|
|
}
|
|
}
|
|
|
|
public IntPtr GetCreationParentHandle(
|
|
IntPtr ownerHandle,
|
|
int width,
|
|
int height)
|
|
{
|
|
if (ownerHandle == IntPtr.Zero ||
|
|
!CefBrowserNativeMethods.IsWindow(ownerHandle))
|
|
throw new ArgumentException(
|
|
"A valid WinForms owner handle is required.",
|
|
"ownerHandle");
|
|
|
|
CefBrowserHostWindow hostWindow;
|
|
lock (_hostWindowSyncRoot)
|
|
{
|
|
if (_hostWindow == null || _hostWindow.IsDestroyed)
|
|
{
|
|
_hostWindow = new CefBrowserHostWindow(
|
|
ownerHandle,
|
|
width,
|
|
height,
|
|
HostWindowCloseRequested,
|
|
HostWindowDestroyed);
|
|
}
|
|
|
|
hostWindow = _hostWindow;
|
|
}
|
|
|
|
IntPtr hostHandle = hostWindow.Handle;
|
|
bool hostValid = hostHandle != IntPtr.Zero &&
|
|
CefBrowserNativeMethods.IsWindow(hostHandle);
|
|
bool ownerValid = CefBrowserNativeMethods.IsWindow(ownerHandle);
|
|
TraceClose(
|
|
"creation-parent hostHwnd=" + hostHandle +
|
|
" hostValid=" + hostValid +
|
|
" hostParent=" + CefBrowserNativeMethods.GetParent(hostHandle) +
|
|
" ownerHwnd=" + ownerHandle +
|
|
" ownerValid=" + ownerValid +
|
|
" hostThread=" + hostWindow.OwnerThreadId +
|
|
" ownerThread=" + CefBrowserNativeMethods.GetWindowThread(ownerHandle),
|
|
null,
|
|
null);
|
|
|
|
if (!hostValid || !ownerValid)
|
|
throw new InvalidOperationException(
|
|
"CEF browser creation requires live host and owner HWNDs.");
|
|
|
|
return hostHandle;
|
|
}
|
|
|
|
public bool TryBeginBrowserCreation()
|
|
{
|
|
if (IsUnavailable || Browser != null)
|
|
return false;
|
|
|
|
if (Interlocked.CompareExchange(ref _browserCreationPending, 1, 0) != 0)
|
|
return false;
|
|
|
|
if (!CefBrowserRuntimeLifetime.TryRegister(this))
|
|
{
|
|
Interlocked.Exchange(ref _browserCreationPending, 0);
|
|
return false;
|
|
}
|
|
|
|
Interlocked.Exchange(ref _browserCloseApproved, 0);
|
|
Interlocked.Exchange(ref _browserBeforeCloseReceived, 0);
|
|
if (!IsUnavailable)
|
|
return true;
|
|
|
|
BrowserCreationFailed();
|
|
return false;
|
|
}
|
|
|
|
public void BrowserCreationFailed()
|
|
{
|
|
Interlocked.Exchange(ref _browserCreationPending, 0);
|
|
RequestHostWindowDestroy("creation-failed");
|
|
TryReleaseHostWindow();
|
|
CefBrowserRuntimeLifetime.Unregister(this);
|
|
}
|
|
|
|
public bool BrowserCreated(CefBrowser browser)
|
|
{
|
|
if (browser == null)
|
|
throw new ArgumentNullException("browser");
|
|
|
|
Interlocked.Exchange(ref _browserCreationPending, 0);
|
|
var currentBrowser = Interlocked.CompareExchange(
|
|
ref _browser,
|
|
browser,
|
|
null);
|
|
TraceClose("created", browser, currentBrowser);
|
|
|
|
if (currentBrowser != null && !IsSameBrowser(currentBrowser, browser))
|
|
{
|
|
CloseBrowser(browser, true);
|
|
return false;
|
|
}
|
|
|
|
if (IsUnavailable)
|
|
{
|
|
RequestClose(IsDisposing);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool BrowserClosed(CefBrowser browser, Action clearOwnerBrowser)
|
|
{
|
|
var currentBrowser = Browser;
|
|
bool sameBrowser = currentBrowser != null &&
|
|
IsSameBrowser(currentBrowser, browser);
|
|
TraceClose(
|
|
"on-before-close same=" + sameBrowser,
|
|
browser,
|
|
currentBrowser);
|
|
if (!sameBrowser)
|
|
return false;
|
|
|
|
if (clearOwnerBrowser != null)
|
|
clearOwnerBrowser();
|
|
|
|
if (currentBrowser != null)
|
|
Interlocked.CompareExchange(ref _browser, null, currentBrowser);
|
|
|
|
Interlocked.Exchange(ref _browserBeforeCloseReceived, 1);
|
|
Interlocked.Exchange(ref _browserCloseRequested, 0);
|
|
RequestHostWindowDestroy("on-before-close");
|
|
TryReleaseHostWindow();
|
|
DisposeBrowserReference(currentBrowser);
|
|
CefBrowserRuntimeLifetime.Unregister(this);
|
|
return true;
|
|
}
|
|
|
|
public bool BrowserClosing(CefBrowser browser)
|
|
{
|
|
var currentBrowser = Browser;
|
|
bool sameBrowser = currentBrowser != null &&
|
|
IsSameBrowser(currentBrowser, browser);
|
|
TraceClose(
|
|
"do-close same=" + sameBrowser,
|
|
browser,
|
|
currentBrowser);
|
|
if (sameBrowser)
|
|
{
|
|
// DoClose runs after onbeforeunload has been accepted. Make the
|
|
// stable host top-level before returning so CEF's WM_CLOSE is
|
|
// never sent to the WPF root window.
|
|
bool parked = ParkBrowser(browser, "do-close");
|
|
if (!parked)
|
|
{
|
|
// Suppress CEF's OS close event when the stable host could
|
|
// not be detached. Destroying the application-owned host
|
|
// is the non-standard-window equivalent of cefclient's
|
|
// DestroyRender path and avoids targeting the WPF root.
|
|
RequestHostWindowDestroy("do-close-park-failed");
|
|
return true;
|
|
}
|
|
|
|
Interlocked.Exchange(ref _browserCloseApproved, 1);
|
|
}
|
|
|
|
// Match CEF's documented non-standard close flow. Returning false
|
|
// makes CEF send WM_CLOSE after this callback returns. The parked
|
|
// application-owned host handles that message and destroys itself.
|
|
return false;
|
|
}
|
|
|
|
public void RequestClose()
|
|
{
|
|
RequestClose(false);
|
|
}
|
|
|
|
private void RequestClose(bool forceClose)
|
|
{
|
|
var browser = Browser;
|
|
if (browser == null)
|
|
return;
|
|
|
|
// Suppress only duplicate dispatches. The flag is reset as soon as
|
|
// CloseBrowser is delivered so a canceled onbeforeunload can be
|
|
// retried. A forced dispose is always allowed to upgrade a pending
|
|
// non-forced close.
|
|
if (!forceClose &&
|
|
Interlocked.Exchange(ref _browserCloseRequested, 1) != 0)
|
|
return;
|
|
|
|
Interlocked.Exchange(ref _browserCloseRequested, 1);
|
|
if (forceClose)
|
|
ParkBrowser(browser, "forced-close-request");
|
|
|
|
CloseBrowser(browser, forceClose);
|
|
}
|
|
|
|
public void BeginDispose()
|
|
{
|
|
if (Interlocked.Exchange(ref _disposeRequested, 1) != 0)
|
|
return;
|
|
|
|
Interlocked.Exchange(ref _ownerHandleDestroying, 1);
|
|
TraceOwnerAction("owner-dispose");
|
|
RequestClose(true);
|
|
TryReleaseHostWindow();
|
|
}
|
|
|
|
public void BeginOwnerHandleDestruction()
|
|
{
|
|
if (IsDisposing ||
|
|
Interlocked.Exchange(ref _ownerHandleDestroying, 1) != 0)
|
|
return;
|
|
|
|
// WinForms can recreate a control HWND for DPI, parenting or style
|
|
// changes. Preserve the browser and only detach its stable host.
|
|
TraceOwnerAction("owner-handle-destroy");
|
|
ParkBrowser(Browser, "owner-handle-destroy");
|
|
}
|
|
|
|
public void OwnerHandleCreated(
|
|
IntPtr ownerHandle,
|
|
int width,
|
|
int height)
|
|
{
|
|
if (IsDisposing || ownerHandle == IntPtr.Zero)
|
|
return;
|
|
|
|
Interlocked.Exchange(ref _ownerHandleDestroying, 0);
|
|
var hostWindow = GetHostWindow();
|
|
var browser = Browser;
|
|
if (hostWindow != null && !hostWindow.IsDestroyed && browser != null)
|
|
hostWindow.AttachTo(ownerHandle, width, height, true);
|
|
|
|
TraceClose(
|
|
"owner-handle-created ownerHwnd=" + ownerHandle +
|
|
" ownerThread=" + CefBrowserNativeMethods.GetWindowThread(ownerHandle),
|
|
Browser,
|
|
null);
|
|
}
|
|
|
|
public void AttachBrowserToOwner(
|
|
CefBrowser browser,
|
|
IntPtr ownerHandle,
|
|
int width,
|
|
int height)
|
|
{
|
|
if (browser == null || ownerHandle == IntPtr.Zero ||
|
|
!CefBrowserNativeMethods.IsWindow(ownerHandle) ||
|
|
IsUnavailable)
|
|
return;
|
|
|
|
var currentBrowser = Browser;
|
|
if (currentBrowser == null || !IsSameBrowser(currentBrowser, browser))
|
|
return;
|
|
|
|
try
|
|
{
|
|
var hostWindow = GetHostWindow();
|
|
if (hostWindow == null || hostWindow.IsDestroyed)
|
|
return;
|
|
|
|
IntPtr browserHandle;
|
|
using (var browserHost = browser.GetHost())
|
|
{
|
|
browserHandle = browserHost.GetWindowHandle();
|
|
}
|
|
IntPtr hostHandle = hostWindow.Handle;
|
|
if (browserHandle == IntPtr.Zero || hostHandle == IntPtr.Zero)
|
|
return;
|
|
|
|
IntPtr browserParent = CefBrowserNativeMethods.GetParent(browserHandle);
|
|
if (browserParent != hostHandle)
|
|
{
|
|
TraceClose(
|
|
"attach-invalid-parent browserHwnd=" + browserHandle +
|
|
" browserParent=" + browserParent +
|
|
" hostHwnd=" + hostHandle,
|
|
browser,
|
|
currentBrowser);
|
|
return;
|
|
}
|
|
|
|
hostWindow.AttachTo(ownerHandle, width, height, true);
|
|
ResizeBrowserWindow(browserHandle, width, height, true);
|
|
TraceClose(
|
|
"attach browserHwnd=" + browserHandle +
|
|
" hostHwnd=" + hostHandle +
|
|
" hostParent=" + CefBrowserNativeMethods.GetParent(hostHandle) +
|
|
" hostThread=" + hostWindow.OwnerThreadId +
|
|
" ownerThread=" + CefBrowserNativeMethods.GetWindowThread(ownerHandle),
|
|
browser,
|
|
currentBrowser);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
TraceClose(
|
|
"attach-failed " + ex.GetType().FullName,
|
|
browser,
|
|
currentBrowser);
|
|
}
|
|
}
|
|
|
|
public void ResizeBrowser(
|
|
IntPtr browserHandle,
|
|
int width,
|
|
int height)
|
|
{
|
|
if (browserHandle == IntPtr.Zero || IsUnavailable)
|
|
return;
|
|
|
|
try
|
|
{
|
|
var hostWindow = GetHostWindow();
|
|
if (hostWindow == null || hostWindow.IsDestroyed)
|
|
return;
|
|
|
|
hostWindow.Resize(width, height);
|
|
ResizeBrowserWindow(browserHandle, width, height, false);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
private bool ParkBrowser(CefBrowser browser, string reason)
|
|
{
|
|
var hostWindow = GetHostWindow();
|
|
if (hostWindow == null || hostWindow.IsDestroyed)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
bool parked = hostWindow.Park();
|
|
TraceClose(
|
|
"park reason=" + reason +
|
|
" parked=" + parked +
|
|
" hostHwnd=" + hostWindow.Handle +
|
|
" hostParent=" +
|
|
CefBrowserNativeMethods.GetParent(hostWindow.Handle),
|
|
browser,
|
|
Browser);
|
|
return parked;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
TraceClose(
|
|
"park-failed " + ex.GetType().FullName,
|
|
browser,
|
|
Browser);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static void ResizeBrowserWindow(
|
|
IntPtr browserHandle,
|
|
int width,
|
|
int height,
|
|
bool show)
|
|
{
|
|
uint flags = CefBrowserNativeMethods.SwpNoZOrder |
|
|
CefBrowserNativeMethods.SwpNoActivate;
|
|
if (show)
|
|
flags |= CefBrowserNativeMethods.SwpShowWindow;
|
|
|
|
CefBrowserNativeMethods.SetWindowPos(
|
|
browserHandle,
|
|
IntPtr.Zero,
|
|
0,
|
|
0,
|
|
Math.Max(1, width),
|
|
Math.Max(1, height),
|
|
flags);
|
|
}
|
|
|
|
private CefBrowserHostWindow GetHostWindow()
|
|
{
|
|
lock (_hostWindowSyncRoot)
|
|
{
|
|
return _hostWindow;
|
|
}
|
|
}
|
|
|
|
private bool HostWindowCloseRequested()
|
|
{
|
|
bool approved =
|
|
Thread.VolatileRead(ref _browserCloseApproved) != 0;
|
|
TraceClose(
|
|
"host-wm-close approved=" + approved,
|
|
Browser,
|
|
null);
|
|
if (!approved)
|
|
RequestClose(IsDisposing);
|
|
return approved;
|
|
}
|
|
|
|
private void HostWindowDestroyed(CefBrowserHostWindow hostWindow)
|
|
{
|
|
TraceClose(
|
|
"host-destroyed hostHwnd=" + hostWindow.LastHandle,
|
|
Browser,
|
|
null);
|
|
TryReleaseHostWindow();
|
|
}
|
|
|
|
private void RequestHostWindowDestroy(string reason)
|
|
{
|
|
var hostWindow = GetHostWindow();
|
|
if (hostWindow == null || hostWindow.IsDestroyed)
|
|
return;
|
|
|
|
TraceClose(
|
|
"host-destroy-request reason=" + reason +
|
|
" hostHwnd=" + hostWindow.Handle +
|
|
" valid=" + CefBrowserNativeMethods.IsWindow(hostWindow.Handle),
|
|
Browser,
|
|
null);
|
|
hostWindow.RequestDestroy();
|
|
}
|
|
|
|
private void TryReleaseHostWindow()
|
|
{
|
|
var hostWindow = GetHostWindow();
|
|
if (hostWindow == null)
|
|
return;
|
|
|
|
bool browserFinished = Browser == null &&
|
|
Thread.VolatileRead(ref _browserCreationPending) == 0;
|
|
if (!browserFinished)
|
|
return;
|
|
|
|
bool closeCompleted =
|
|
Thread.VolatileRead(ref _browserBeforeCloseReceived) != 0;
|
|
if (!hostWindow.IsDestroyed)
|
|
{
|
|
if (IsDisposing || closeCompleted)
|
|
RequestHostWindowDestroy("release-ready");
|
|
return;
|
|
}
|
|
|
|
lock (_hostWindowSyncRoot)
|
|
{
|
|
if (ReferenceEquals(_hostWindow, hostWindow) &&
|
|
Browser == null &&
|
|
Thread.VolatileRead(ref _browserCreationPending) == 0)
|
|
{
|
|
_hostWindow = null;
|
|
}
|
|
}
|
|
|
|
TraceClose(
|
|
"release-complete hostHwnd=" + hostWindow.LastHandle +
|
|
" browserClosed=" + closeCompleted,
|
|
null,
|
|
null);
|
|
}
|
|
|
|
private static bool IsSameBrowser(CefBrowser left, CefBrowser right)
|
|
{
|
|
if (ReferenceEquals(left, right))
|
|
return true;
|
|
if (left == null || right == null)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
return left.IsSame(right);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
try
|
|
{
|
|
return left.Identifier == right.Identifier;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CloseBrowser(CefBrowser browser, bool forceClose)
|
|
{
|
|
try
|
|
{
|
|
TraceClose(
|
|
"close-request force=" + forceClose,
|
|
browser,
|
|
null);
|
|
if (CefRuntime.CurrentlyOn(CefThreadId.UI))
|
|
{
|
|
ExecuteCloseBrowser(browser, forceClose);
|
|
return;
|
|
}
|
|
|
|
bool posted = CefRuntime.PostTask(
|
|
CefThreadId.UI,
|
|
new CefActionTask(delegate
|
|
{
|
|
ExecuteCloseBrowser(browser, forceClose);
|
|
}));
|
|
TraceClose("close-post-ui accepted=" + posted, browser, null);
|
|
if (!posted)
|
|
ExecuteCloseBrowser(browser, forceClose);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
BrowserCloseFailed(browser, ex);
|
|
}
|
|
}
|
|
|
|
private void ExecuteCloseBrowser(CefBrowser browser, bool forceClose)
|
|
{
|
|
try
|
|
{
|
|
TraceClose(
|
|
"close-execute-ui force=" + forceClose,
|
|
browser,
|
|
null);
|
|
using (var browserHost = browser.GetHost())
|
|
{
|
|
browserHost.CloseBrowser(forceClose);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
BrowserCloseFailed(browser, ex);
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Exchange(ref _browserCloseRequested, 0);
|
|
}
|
|
}
|
|
|
|
private static void DisposeBrowserReference(CefBrowser browser)
|
|
{
|
|
if (browser == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
browser.Dispose();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
TraceClose(
|
|
"browser-reference-dispose-failed " + ex.GetType().FullName,
|
|
null,
|
|
null);
|
|
}
|
|
}
|
|
|
|
private void BrowserCloseFailed(CefBrowser browser, Exception exception)
|
|
{
|
|
TraceClose(
|
|
"close-failed " + exception.GetType().FullName,
|
|
browser,
|
|
Browser);
|
|
Interlocked.Exchange(ref _browserCloseRequested, 0);
|
|
if (IsDisposing)
|
|
RequestHostWindowDestroy("close-failed");
|
|
TryReleaseHostWindow();
|
|
}
|
|
|
|
private void TraceOwnerAction(string action)
|
|
{
|
|
if (!LifecycleTracingEnabled)
|
|
return;
|
|
|
|
string stack = Environment.StackTrace
|
|
.Replace("\r", string.Empty)
|
|
.Replace("\n", " | ");
|
|
if (stack.Length > 1800)
|
|
stack = stack.Substring(0, 1800);
|
|
|
|
TraceClose(action + " stack=" + stack, Browser, null);
|
|
}
|
|
|
|
private static void TraceClose(
|
|
string action,
|
|
CefBrowser browser,
|
|
CefBrowser currentBrowser)
|
|
{
|
|
if (!LifecycleTracingEnabled)
|
|
return;
|
|
|
|
try
|
|
{
|
|
LogHelper.Instance.WriteLog(
|
|
"[CEF_LIFECYCLE] " + action +
|
|
"; browser=" + GetBrowserIdentifier(browser) +
|
|
"; current=" + GetBrowserIdentifier(currentBrowser) +
|
|
"; thread=" + Thread.CurrentThread.ManagedThreadId +
|
|
"; cefUi=" + CefRuntime.CurrentlyOn(CefThreadId.UI));
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
private static string GetBrowserIdentifier(CefBrowser browser)
|
|
{
|
|
if (browser == null)
|
|
return "null";
|
|
|
|
try
|
|
{
|
|
return browser.Identifier.ToString();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return "unavailable";
|
|
}
|
|
}
|
|
|
|
private sealed class CefBrowserHostWindow : NativeWindow
|
|
{
|
|
private const int WmClose = 0x0010;
|
|
private const int WmReleaseHandle = 0x8001;
|
|
private const int WmPark = 0x8002;
|
|
private const int WmNcDestroy = 0x0082;
|
|
|
|
private readonly uint _ownerThreadId;
|
|
private Func<bool> _onCloseRequested;
|
|
private Action<CefBrowserHostWindow> _onDestroyed;
|
|
private IntPtr _lastHandle;
|
|
private int _destroyRequested;
|
|
private int _destroyed;
|
|
private int _parked;
|
|
private int _parking;
|
|
|
|
public uint OwnerThreadId
|
|
{
|
|
get { return _ownerThreadId; }
|
|
}
|
|
|
|
public bool IsDestroyed
|
|
{
|
|
get { return Thread.VolatileRead(ref _destroyed) != 0; }
|
|
}
|
|
|
|
public bool IsParked
|
|
{
|
|
get { return Thread.VolatileRead(ref _parked) != 0; }
|
|
}
|
|
|
|
public IntPtr LastHandle
|
|
{
|
|
get { return _lastHandle; }
|
|
}
|
|
|
|
public CefBrowserHostWindow(
|
|
IntPtr ownerHandle,
|
|
int width,
|
|
int height,
|
|
Func<bool> onCloseRequested,
|
|
Action<CefBrowserHostWindow> onDestroyed)
|
|
{
|
|
_onCloseRequested = onCloseRequested;
|
|
_onDestroyed = onDestroyed;
|
|
_ownerThreadId = CefBrowserNativeMethods.GetCurrentThreadId();
|
|
|
|
uint ownerThread =
|
|
CefBrowserNativeMethods.GetWindowThread(ownerHandle);
|
|
if (ownerThread != _ownerThreadId)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"CEF host HWND must be created on the WinForms owner thread.");
|
|
}
|
|
|
|
CreateHandle(new CreateParams
|
|
{
|
|
Caption = "Lskj.CefBrowserHostWindow",
|
|
Parent = IntPtr.Zero,
|
|
X = -32000,
|
|
Y = -32000,
|
|
Width = 1,
|
|
Height = 1,
|
|
Style = CefBrowserNativeMethods.WsPopup |
|
|
CefBrowserNativeMethods.WsClipChildren |
|
|
CefBrowserNativeMethods.WsClipSiblings
|
|
});
|
|
_lastHandle = Handle;
|
|
Interlocked.Exchange(ref _parked, 1);
|
|
|
|
if (_lastHandle == IntPtr.Zero ||
|
|
!CefBrowserNativeMethods.IsWindow(_lastHandle))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Failed to create the stable CEF root HWND.");
|
|
}
|
|
}
|
|
|
|
public void AttachTo(
|
|
IntPtr ownerHandle,
|
|
int width,
|
|
int height,
|
|
bool show)
|
|
{
|
|
EnsureOwnerThread(ownerHandle);
|
|
if (IsDestroyed || Handle == IntPtr.Zero ||
|
|
!CefBrowserNativeMethods.IsWindow(Handle))
|
|
return;
|
|
|
|
int style = CefBrowserNativeMethods.GetWindowLong(
|
|
Handle,
|
|
CefBrowserNativeMethods.GwlStyle);
|
|
CefBrowserNativeMethods.SetWindowLong(
|
|
Handle,
|
|
CefBrowserNativeMethods.GwlStyle,
|
|
(style & ~CefBrowserNativeMethods.WsPopup) |
|
|
CefBrowserNativeMethods.WsChild |
|
|
CefBrowserNativeMethods.WsClipChildren |
|
|
CefBrowserNativeMethods.WsClipSiblings);
|
|
CefBrowserNativeMethods.SetParent(Handle, ownerHandle);
|
|
if (!CefBrowserNativeMethods.IsWindow(Handle) ||
|
|
CefBrowserNativeMethods.GetParent(Handle) != ownerHandle)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Failed to attach the CEF host HWND to its owner.");
|
|
}
|
|
|
|
uint flags = CefBrowserNativeMethods.SwpNoZOrder |
|
|
CefBrowserNativeMethods.SwpNoActivate |
|
|
CefBrowserNativeMethods.SwpFrameChanged;
|
|
if (show)
|
|
flags |= CefBrowserNativeMethods.SwpShowWindow;
|
|
CefBrowserNativeMethods.SetWindowPos(
|
|
Handle,
|
|
IntPtr.Zero,
|
|
0,
|
|
0,
|
|
Math.Max(1, width),
|
|
Math.Max(1, height),
|
|
flags);
|
|
Interlocked.Exchange(ref _parked, 0);
|
|
}
|
|
|
|
public void Resize(int width, int height)
|
|
{
|
|
if (IsDestroyed || Handle == IntPtr.Zero ||
|
|
!CefBrowserNativeMethods.IsWindow(Handle))
|
|
return;
|
|
|
|
CefBrowserNativeMethods.SetWindowPos(
|
|
Handle,
|
|
IntPtr.Zero,
|
|
0,
|
|
0,
|
|
Math.Max(1, width),
|
|
Math.Max(1, height),
|
|
CefBrowserNativeMethods.SwpNoZOrder |
|
|
CefBrowserNativeMethods.SwpNoActivate);
|
|
}
|
|
|
|
public bool Park()
|
|
{
|
|
if (IsDestroyed || Handle == IntPtr.Zero ||
|
|
!CefBrowserNativeMethods.IsWindow(Handle))
|
|
return false;
|
|
|
|
if (IsParked)
|
|
return true;
|
|
|
|
if (CefBrowserNativeMethods.GetCurrentThreadId() !=
|
|
_ownerThreadId)
|
|
{
|
|
CefBrowserNativeMethods.SendMessage(
|
|
Handle,
|
|
WmPark,
|
|
IntPtr.Zero,
|
|
IntPtr.Zero);
|
|
return IsParked &&
|
|
CefBrowserNativeMethods.IsWindow(Handle) &&
|
|
CefBrowserNativeMethods.GetParent(Handle) == IntPtr.Zero;
|
|
}
|
|
|
|
return ParkNow();
|
|
}
|
|
|
|
private bool ParkNow()
|
|
{
|
|
if (IsParked)
|
|
return true;
|
|
if (Interlocked.Exchange(ref _parking, 1) != 0)
|
|
return false;
|
|
|
|
try
|
|
{
|
|
IntPtr handle = Handle;
|
|
if (IsDestroyed || handle == IntPtr.Zero ||
|
|
!CefBrowserNativeMethods.IsWindow(handle))
|
|
return false;
|
|
|
|
CefBrowserNativeMethods.ShowWindow(
|
|
handle,
|
|
CefBrowserNativeMethods.SwHide);
|
|
int style = CefBrowserNativeMethods.GetWindowLong(
|
|
handle,
|
|
CefBrowserNativeMethods.GwlStyle);
|
|
// Win32 requires WS_CHILD to be cleared before setting a
|
|
// child window's parent to NULL. Reversing this order can
|
|
// leave the host attached to the WPF root while appearing
|
|
// parked in managed state.
|
|
CefBrowserNativeMethods.SetWindowLong(
|
|
handle,
|
|
CefBrowserNativeMethods.GwlStyle,
|
|
(style & ~CefBrowserNativeMethods.WsChild) |
|
|
CefBrowserNativeMethods.WsPopup |
|
|
CefBrowserNativeMethods.WsClipChildren |
|
|
CefBrowserNativeMethods.WsClipSiblings);
|
|
CefBrowserNativeMethods.SetParent(handle, IntPtr.Zero);
|
|
if (!CefBrowserNativeMethods.IsWindow(handle) ||
|
|
CefBrowserNativeMethods.GetParent(handle) != IntPtr.Zero)
|
|
return false;
|
|
|
|
CefBrowserNativeMethods.SetWindowPos(
|
|
handle,
|
|
IntPtr.Zero,
|
|
-32000,
|
|
-32000,
|
|
1,
|
|
1,
|
|
CefBrowserNativeMethods.SwpNoZOrder |
|
|
CefBrowserNativeMethods.SwpNoActivate |
|
|
CefBrowserNativeMethods.SwpFrameChanged);
|
|
Interlocked.Exchange(ref _parked, 1);
|
|
return true;
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Exchange(ref _parking, 0);
|
|
}
|
|
}
|
|
|
|
public void RequestDestroy()
|
|
{
|
|
if (Interlocked.Exchange(ref _destroyRequested, 1) != 0)
|
|
return;
|
|
|
|
IntPtr handle = Handle;
|
|
if (handle == IntPtr.Zero ||
|
|
!CefBrowserNativeMethods.IsWindow(handle))
|
|
{
|
|
NotifyDestroyed();
|
|
return;
|
|
}
|
|
|
|
if (CefBrowserNativeMethods.GetCurrentThreadId() ==
|
|
_ownerThreadId)
|
|
{
|
|
DestroyNow();
|
|
return;
|
|
}
|
|
|
|
if (!CefBrowserNativeMethods.PostMessage(
|
|
handle,
|
|
WmReleaseHandle,
|
|
IntPtr.Zero,
|
|
IntPtr.Zero))
|
|
{
|
|
Interlocked.Exchange(ref _destroyRequested, 0);
|
|
TraceClose(
|
|
"host-destroy-post-failed hostHwnd=" + handle,
|
|
null,
|
|
null);
|
|
}
|
|
}
|
|
|
|
protected override void WndProc(ref Message message)
|
|
{
|
|
if (message.Msg == WmClose)
|
|
{
|
|
var onCloseRequested = _onCloseRequested;
|
|
bool allowClose = onCloseRequested == null ||
|
|
onCloseRequested();
|
|
if (allowClose)
|
|
base.WndProc(ref message);
|
|
return;
|
|
}
|
|
|
|
if (message.Msg == WmReleaseHandle)
|
|
{
|
|
DestroyNow();
|
|
return;
|
|
}
|
|
|
|
if (message.Msg == WmPark)
|
|
{
|
|
ParkNow();
|
|
return;
|
|
}
|
|
|
|
if (message.Msg == WmNcDestroy)
|
|
{
|
|
_lastHandle = message.HWnd;
|
|
base.WndProc(ref message);
|
|
NotifyDestroyed();
|
|
return;
|
|
}
|
|
|
|
base.WndProc(ref message);
|
|
}
|
|
|
|
private void DestroyNow()
|
|
{
|
|
IntPtr handle = Handle;
|
|
if (handle == IntPtr.Zero ||
|
|
!CefBrowserNativeMethods.IsWindow(handle))
|
|
{
|
|
NotifyDestroyed();
|
|
return;
|
|
}
|
|
|
|
TraceClose(
|
|
"host-destroying hostHwnd=" + handle,
|
|
null,
|
|
null);
|
|
DestroyHandle();
|
|
}
|
|
|
|
private void NotifyDestroyed()
|
|
{
|
|
if (Interlocked.Exchange(ref _destroyed, 1) != 0)
|
|
return;
|
|
|
|
_onCloseRequested = null;
|
|
var onDestroyed = Interlocked.Exchange(
|
|
ref _onDestroyed,
|
|
null);
|
|
if (onDestroyed != null)
|
|
onDestroyed(this);
|
|
}
|
|
|
|
private void EnsureOwnerThread(IntPtr ownerHandle)
|
|
{
|
|
EnsureCurrentThread();
|
|
if (CefBrowserNativeMethods.GetWindowThread(ownerHandle) !=
|
|
_ownerThreadId)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"CEF host and owner HWND must belong to the same thread.");
|
|
}
|
|
}
|
|
|
|
private void EnsureCurrentThread()
|
|
{
|
|
if (CefBrowserNativeMethods.GetCurrentThreadId() !=
|
|
_ownerThreadId)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"CEF host HWND may only be changed on its owner thread.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors the browser list used by CEF's cefsimple application. CEF may
|
|
/// only be shut down after every asynchronous OnBeforeClose callback has
|
|
/// completed.
|
|
/// </summary>
|
|
public static class CefBrowserRuntimeLifetime
|
|
{
|
|
private static readonly object _syncRoot = new object();
|
|
private static readonly HashSet<CefBrowserLifetimeCoordinator>
|
|
_activeBrowsers = new HashSet<CefBrowserLifetimeCoordinator>();
|
|
private static readonly ManualResetEvent _allBrowsersClosed =
|
|
new ManualResetEvent(true);
|
|
private static bool _shutdownRequested;
|
|
|
|
internal static bool TryRegister(
|
|
CefBrowserLifetimeCoordinator coordinator)
|
|
{
|
|
if (coordinator == null)
|
|
throw new ArgumentNullException("coordinator");
|
|
|
|
lock (_syncRoot)
|
|
{
|
|
if (_shutdownRequested)
|
|
return false;
|
|
|
|
if (_activeBrowsers.Add(coordinator))
|
|
_allBrowsersClosed.Reset();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
internal static void Unregister(
|
|
CefBrowserLifetimeCoordinator coordinator)
|
|
{
|
|
if (coordinator == null)
|
|
return;
|
|
|
|
lock (_syncRoot)
|
|
{
|
|
_activeBrowsers.Remove(coordinator);
|
|
if (_activeBrowsers.Count == 0)
|
|
_allBrowsersClosed.Set();
|
|
}
|
|
}
|
|
|
|
public static bool CloseAllBrowsersAndWait(int timeoutMilliseconds)
|
|
{
|
|
CefBrowserLifetimeCoordinator[] browsers;
|
|
lock (_syncRoot)
|
|
{
|
|
_shutdownRequested = true;
|
|
browsers = new CefBrowserLifetimeCoordinator[
|
|
_activeBrowsers.Count];
|
|
_activeBrowsers.CopyTo(browsers);
|
|
if (browsers.Length == 0)
|
|
return true;
|
|
}
|
|
|
|
foreach (var browser in browsers)
|
|
browser.BeginDispose();
|
|
|
|
bool closed = _allBrowsersClosed.WaitOne(
|
|
Math.Max(0, timeoutMilliseconds));
|
|
TraceShutdown(closed, timeoutMilliseconds);
|
|
return closed;
|
|
}
|
|
|
|
private static void TraceShutdown(
|
|
bool closed,
|
|
int timeoutMilliseconds)
|
|
{
|
|
if (!CefBrowserLifetimeCoordinator.LifecycleTracingEnabled)
|
|
return;
|
|
|
|
try
|
|
{
|
|
int remaining;
|
|
lock (_syncRoot)
|
|
{
|
|
remaining = _activeBrowsers.Count;
|
|
}
|
|
|
|
LogHelper.Instance.WriteLog(
|
|
"[CEF_LIFECYCLE] shutdown-wait closed=" + closed +
|
|
"; remaining=" + remaining +
|
|
"; timeoutMs=" + timeoutMilliseconds);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static class CefBrowserNativeMethods
|
|
{
|
|
internal const int GwlStyle = -16;
|
|
internal const int SwHide = 0;
|
|
internal const int WsChild = 0x40000000;
|
|
internal const int WsClipChildren = 0x02000000;
|
|
internal const int WsClipSiblings = 0x04000000;
|
|
internal const int WsPopup = unchecked((int)0x80000000);
|
|
internal const uint SwpNoZOrder = 0x0004;
|
|
internal const uint SwpNoActivate = 0x0010;
|
|
internal const uint SwpFrameChanged = 0x0020;
|
|
internal const uint SwpShowWindow = 0x0040;
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
internal static extern IntPtr SetParent(
|
|
IntPtr childWindow,
|
|
IntPtr newParentWindow);
|
|
|
|
[DllImport("user32.dll")]
|
|
internal static extern IntPtr GetParent(IntPtr window);
|
|
|
|
[DllImport("user32.dll")]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
internal static extern bool IsWindow(IntPtr window);
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
internal static extern int GetWindowLong(IntPtr window, int index);
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
internal static extern int SetWindowLong(
|
|
IntPtr window,
|
|
int index,
|
|
int newValue);
|
|
|
|
[DllImport("user32.dll")]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
internal static extern bool ShowWindow(IntPtr window, int command);
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
internal static extern bool SetWindowPos(
|
|
IntPtr window,
|
|
IntPtr insertAfter,
|
|
int x,
|
|
int y,
|
|
int width,
|
|
int height,
|
|
uint flags);
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
internal static extern bool PostMessage(
|
|
IntPtr window,
|
|
int message,
|
|
IntPtr wParam,
|
|
IntPtr lParam);
|
|
|
|
[DllImport("user32.dll")]
|
|
internal static extern IntPtr SendMessage(
|
|
IntPtr window,
|
|
int message,
|
|
IntPtr wParam,
|
|
IntPtr lParam);
|
|
|
|
[DllImport("user32.dll")]
|
|
internal static extern uint GetWindowThreadProcessId(
|
|
IntPtr window,
|
|
IntPtr processId);
|
|
|
|
[DllImport("kernel32.dll")]
|
|
internal static extern uint GetCurrentThreadId();
|
|
|
|
internal static uint GetWindowThread(IntPtr window)
|
|
{
|
|
return window == IntPtr.Zero
|
|
? 0
|
|
: GetWindowThreadProcessId(window, IntPtr.Zero);
|
|
}
|
|
}
|
|
|
|
internal sealed class CefActionTask : CefTask
|
|
{
|
|
private Action _action;
|
|
|
|
public CefActionTask(Action action)
|
|
{
|
|
if (action == null)
|
|
throw new ArgumentNullException("action");
|
|
|
|
_action = action;
|
|
}
|
|
|
|
protected override void Execute()
|
|
{
|
|
var action = Interlocked.Exchange(ref _action, null);
|
|
if (action == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
action();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Never propagate managed exceptions into a native CEF task.
|
|
}
|
|
}
|
|
}
|
|
}
|