Files
2026-07-10 15:25:05 +08:00

630 lines
24 KiB
C#

namespace Cef3.Wrapper
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using BrowserInfoMap = CefBrowserInfoMap<long, Cef3.Wrapper.CefMessageRouterBrowserSide.QueryInfo>;
/// <summary>
/// Implements the browser side of query routing. The methods of this class may
/// be called on any browser process thread unless otherwise indicated.
/// </summary>
public sealed class CefMessageRouterBrowserSide
{
#region Callback
/// <summary>
/// Callback associated with a single pending asynchronous query. Execute the
/// Success or Failure method to send an asynchronous response to the
/// associated JavaScript handler. It is a runtime error to destroy a Callback
/// object associated with an uncanceled query without first executing one of
/// the callback methods. The methods of this class may be called on any
/// browser process thread.
/// </summary>
public sealed class Callback
{
private CefMessageRouterBrowserSide _router;
private readonly int _browserId;
private readonly long _queryId;
private readonly bool _persistent;
internal Callback(CefMessageRouterBrowserSide router, int browserId, long queryId, bool persistent)
{
_router = router;
_browserId = browserId;
_queryId = queryId;
_persistent = persistent;
}
~Callback()
{
Dispose();
}
internal void Dispose()
{
// Hitting this DCHECK means that you didn't call Success or Failure
// on the Callback after returning true from Handler::OnQuery. You must
// call Failure to terminate persistent queries.
Debug.Assert(_router == null);
// if (_router != null) throw new InvalidOperationException("You didn't call Success or Failure on the Callback after returning true from Handler::OnQuery.");
}
/// <summary>
/// Notify the associated JavaScript onSuccess callback that the query has
/// completed successfully with the specified |response|.
/// </summary>
public void Success(string response)
{
if (!CefRuntime.CurrentlyOn(CefThreadId.UI))
{
Helpers.PostTask(CefThreadId.UI,
Helpers.Apply(this.Success, response)
);
return;
}
if (_router != null)
{
Helpers.PostTaskUncertainty(CefThreadId.UI,
Helpers.Apply(_router.OnCallbackSuccess, _browserId, _queryId, response)
);
if (!_persistent)
{
// Non-persistent callbacks are only good for a single use.
_router = null;
}
}
}
/// <summary>
/// Notify the associated JavaScript onFailure callback that the query has
/// failed with the specified |error_code| and |error_message|.
/// </summary>
public void Failure(int errorCode, string errorMessage)
{
if (!CefRuntime.CurrentlyOn(CefThreadId.UI))
{
// Must execute on the UI thread to access member variables.
Helpers.PostTask(CefThreadId.UI,
Helpers.Apply(this.Failure, errorCode, errorMessage)
);
return;
}
if (_router != null)
{
Helpers.PostTaskUncertainty(CefThreadId.UI,
Helpers.Apply(_router.OnCallbackFailure, _browserId, _queryId, errorCode, errorMessage)
);
// Failure always invalidates the callback.
_router = null;
}
}
internal void Detach()
{
Helpers.RequireUIThread();
_router = null;
}
};
#endregion
#region Handler
/// <summary>
/// Implement this interface to handle queries. All methods will be executed on
/// the browser process UI thread.
/// </summary>
public class Handler
{
/// <summary>
/// Executed when a new query is received. |query_id| uniquely identifies the
/// query for the life span of the router. Return true to handle the query
/// or false to propagate the query to other registered handlers, if any. If
/// no handlers return true from this method then the query will be
/// automatically canceled with an error code of -1 delivered to the
/// JavaScript onFailure callback. If this method returns true then a
/// Callback method must be executed either in this method or asynchronously
/// to complete the query.
/// </summary>
public virtual bool OnQuery(CefBrowser browser, CefFrame frame, long queryId, string request, bool persistent, Callback callback)
{
return false;
}
/// <summary>
/// Executed when a query has been canceled either explicitly using the
/// JavaScript cancel function or implicitly due to browser destruction,
/// navigation or renderer process termination. It will only be called for
/// the single handler that returned true from OnQuery for the same
/// |query_id|. No references to the associated Callback object should be
/// kept after this method is called, nor should any Callback methods be
/// executed.
/// </summary>
public virtual void OnQueryCanceled(CefBrowser browser, CefFrame frame, long queryId)
{
}
};
#endregion
private readonly CefMessageRouterConfig _config;
private readonly string _queryMessageName;
private readonly string _cancelMessageName;
private readonly List<Handler> _handlerSet = new List<Handler>(4); // TODO: use a HashSet, for .NET 3.5+
private readonly BrowserInfoMap _browserQueryInfoMap = new BrowserInfoMap();
private readonly CefMessageRouter.IdGeneratorInt64 _queryIdGenerator = new CefMessageRouter.IdGeneratorInt64();
/// <summary>
/// Create a new router with the specified configuration.
/// </summary>
public CefMessageRouterBrowserSide(CefMessageRouterConfig config)
{
if (!config.Validate()) throw new ArgumentException("Invalid configuration.");
_config = config;
_queryMessageName = config.JSQueryFunction + CefMessageRouter.MessageSuffix;
_cancelMessageName = config.JSCancelFunction + CefMessageRouter.MessageSuffix;
}
~CefMessageRouterBrowserSide()
{
// There should be no pending queries when the router is deleted.
Debug.Assert(_browserQueryInfoMap.IsEmpty);
}
// TODO: Dispose method ?
/// <summary>
/// Create a new router with the specified configuration.
/// </summary>
public static CefMessageRouterBrowserSide Create(CefMessageRouterConfig config)
{
return new CefMessageRouterBrowserSide(config);
}
/// <summary>
/// Add a new query handler. If |first| is true it will be added as the first
/// handler, otherwise it will be added as the last handler. Returns true if
/// the handler is added successfully or false if the handler has already been
/// added. Must be called on the browser process UI thread. The Handler object
/// must either outlive the router or be removed before deletion.
/// </summary>
public bool AddHandler(Handler handler, bool first = false)
{
if (handler == null) throw new ArgumentNullException("handler");
Helpers.RequireUIThread();
if (_handlerSet.Contains(handler)) return false;
if (first) { _handlerSet.Insert(0, handler); }
else { _handlerSet.Add(handler); }
return true;
}
/// <summary>
/// Remove an existing query handler. Any pending queries associated with the
/// handler will be canceled. Handler::OnQueryCanceled will be called and the
/// associated JavaScript onFailure callback will be executed with an error
/// code of -1. Returns true if the handler is removed successfully or false
/// if the handler is not found. Must be called on the browser process UI
/// thread.
/// </summary>
public bool RemoveHandler(Handler handler)
{
if (handler == null) throw new ArgumentNullException("handler");
Helpers.RequireUIThread();
if (_handlerSet.Remove(handler))
{
CancelPendingFor(null, handler, true);
return true;
}
return false;
}
/// <summary>
/// Cancel all pending queries associated with either |browser| or |handler|.
/// If both |browser| and |handler| are NULL all pending queries will be
/// canceled. Handler::OnQueryCanceled will be called and the associated
/// JavaScript onFailure callback will be executed in all cases with an error
/// code of -1.
/// </summary>
public void CancelPending(CefBrowser browser = null, Handler handler = null)
{
CancelPendingFor(browser, handler, true);
}
/// <summary>
/// Returns the number of queries currently pending for the specified |browser|
/// and/or |handler|. Either or both values may be empty. Must be called on the
/// browser process UI thread.
/// </summary>
public int GetPendingCount(CefBrowser browser = null, Handler handler = null)
{
Helpers.RequireUIThread();
if (_browserQueryInfoMap.IsEmpty) return 0;
if (handler != null)
{
int count = 0;
BrowserInfoMap.Visitor visitor = (int browserId, long key, QueryInfo value, ref bool remove) =>
{
if (value.Handler == handler) count++;
return true;
};
if (browser != null)
{
// Count queries associated with the specified browser.
_browserQueryInfoMap.FindAll(browser.Identifier, visitor);
}
else
{
// Count all queries for all browsers.
_browserQueryInfoMap.FindAll(visitor);
}
return count;
}
else if (browser != null)
{
return _browserQueryInfoMap.Count(browser.Identifier);
}
else
{
return _browserQueryInfoMap.Count();
}
}
#region The below methods should be called from other CEF handlers. They must be called exactly as documented for the router to function correctly.
/// <summary>
/// Call from CefLifeSpanHandler::OnBeforeClose. Any pending queries associated
/// with |browser| will be canceled and Handler::OnQueryCanceled will be called.
/// No JavaScript callbacks will be executed since this indicates destruction
/// of the browser.
/// </summary>
public void OnBeforeClose(CefBrowser browser)
{
CancelPendingFor(browser, null, false);
}
/// <summary>
/// Call from CefRequestHandler::OnRenderProcessTerminated. Any pending queries
/// associated with |browser| will be canceled and Handler::OnQueryCanceled
/// will be called. No JavaScript callbacks will be executed since this
/// indicates destruction of the context.
/// </summary>
public void OnRenderProcessTerminated(CefBrowser browser)
{
CancelPendingFor(browser, null, false);
}
/// <summary>
/// Call from CefRequestHandler::OnBeforeBrowse only if the navigation is
/// allowed to proceed. If |frame| is the main frame then any pending queries
/// associated with |browser| will be canceled and Handler::OnQueryCanceled
/// will be called. No JavaScript callbacks will be executed since this
/// indicates destruction of the context.
/// </summary>
public void OnBeforeBrowse(CefBrowser browser, CefFrame frame)
{
if (frame.IsMain) CancelPendingFor(browser, null, false);
}
/// <summary>
/// Call from CefClient::OnProcessMessageReceived. Returns true if the message
/// is handled by this router or false otherwise.
/// </summary>
public bool OnProcessMessageReceived(CefBrowser browser, CefProcessId sourceProcess, CefProcessMessage message)
{
Helpers.RequireUIThread();
var messageName = message.Name;
if (messageName == _queryMessageName)
{
var args = message.Arguments;
Debug.Assert(args.Count == 6);
var frameId = Helpers.Int64Set(args.GetInt(0), args.GetInt(1));
var contextId = args.GetInt(2);
var requestId = args.GetInt(3);
var request = args.GetString(4);
var persistent = args.GetBool(5);
if (_handlerSet.Count == 0)
{
// No handlers so cancel the query.
CancelUnhandledQuery(browser, contextId, requestId);
return true;
}
var browserId = browser.Identifier;
var queryId = _queryIdGenerator.GetNextId();
var frame = browser.GetFrame(frameId);
var callback = new Callback(this, browserId, queryId, persistent);
// Make a copy of the handler list in case the user adds or removes a
// handler while we're iterating.
var handlers = _handlerSet.ToArray();
var handled = false;
Handler handler = null;
foreach (var x in handlers)
{
handled = x.OnQuery(browser, frame, queryId, request, persistent, callback);
if (handled)
{
handler = x;
break;
}
}
// If the query isn't handled nothing should be keeping a reference to
// the callback.
// DCHECK(handled || callback->GetRefCt() == 1);
// Debug.Assert(handled && handler != null);
// We don't need this assertion, in GC environment.
// There is client responsibility to do not reference callback, if request is not handled.
if (handled)
{
// Persist the query information until the callback executes.
// It's safe to do this here because the callback will execute
// asynchronously.
var info = new QueryInfo
{
Browser = browser,
FrameId = frameId,
ContextId = contextId,
RequestId = requestId,
Persistent = persistent,
Callback = callback,
Handler = handler,
};
_browserQueryInfoMap.Add(browserId, queryId, info);
}
else
{
// Invalidate the callback.
callback.Detach();
// No one chose to handle the query so cancel it.
CancelUnhandledQuery(browser, contextId, requestId);
}
return true;
}
else if (messageName == _cancelMessageName)
{
var args = message.Arguments;
Debug.Assert(args.Count == 2);
var browserId = browser.Identifier;
var contextId = args.GetInt(0);
var requestId = args.GetInt(1);
CancelPendingRequest(browserId, contextId, requestId);
return true;
}
return false;
}
#endregion
internal sealed class QueryInfo
{
// Browser and frame originated the query.
public CefBrowser Browser;
public long FrameId;
// IDs that uniquely identify the query in the renderer process. These
// values are opaque to the browser process but must be returned with the
// response.
public int ContextId;
public int RequestId;
// True if the query is persistent.
public bool Persistent;
// Callback associated with the query that must be detached when the query
// is canceled.
public Callback Callback;
// Handler that should be notified if the query is automatically canceled.
public Handler Handler;
public void Dispose()
{
Browser = null;
//if (Browser != null)
//{
// Browser.Dispose();
// Browser = null;
//}
}
};
// Retrieve a QueryInfo object from the map based on the browser-side query
// ID. If |always_remove| is true then the QueryInfo object will always be
// removed from the map. Othewise, the QueryInfo object will only be removed
// if the query is non-persistent. If |removed| is true the caller is
// responsible for deleting the returned QueryInfo object.
private QueryInfo GetQueryInfo(int browserId, long queryId, bool alwaysRemove, ref bool removed)
{
bool removedTemp = false;
BrowserInfoMap.Visitor visitor = (int browserId_, long key, QueryInfo value, ref bool remove) =>
{
remove = removedTemp = alwaysRemove || !value.Persistent;
return true;
};
var info = _browserQueryInfoMap.Find(browserId, queryId, visitor);
if (info != null) removed = removedTemp;
return info;
}
// Called by CallbackImpl on success.
private void OnCallbackSuccess(int browserId, long queryId, string response)
{
Helpers.RequireUIThread();
bool removed = false;
var info = GetQueryInfo(browserId, queryId, false, ref removed);
if (info != null)
{
SendQuerySuccess(info, response);
if (removed) info.Dispose();
}
}
// Called by CallbackImpl on failure.
private void OnCallbackFailure(int browserId, long queryId, int errorCode, string errorMessage)
{
Helpers.RequireUIThread();
bool removed = false;
var info = GetQueryInfo(browserId, queryId, true, ref removed);
if (info != null)
{
SendQueryFailure(info, errorCode, errorMessage);
Debug.Assert(removed);
info.Dispose();
}
}
private void SendQuerySuccess(QueryInfo info, string response)
{
SendQuerySuccess(info.Browser, info.ContextId, info.RequestId, response);
}
private void SendQuerySuccess(CefBrowser browser, int contextId, int requestId, string response)
{
var message = CefProcessMessage.Create(_queryMessageName);
var args = message.Arguments;
args.SetInt(0, contextId);
args.SetInt(1, requestId);
args.SetBool(2, true); // Indicates a success result.
args.SetString(3, response);
browser.SendProcessMessage(CefProcessId.Renderer, message);
args.Dispose();
message.Dispose();
}
private void SendQueryFailure(QueryInfo info, int errorCode, string errorMessage)
{
SendQueryFailure(info.Browser, info.ContextId, info.RequestId, errorCode, errorMessage);
}
private void SendQueryFailure(CefBrowser browser, int contextId, int requestId, int errorCode, string errorMessage)
{
var message = CefProcessMessage.Create(_queryMessageName);
var args = message.Arguments;
args.SetInt(0, contextId);
args.SetInt(1, requestId);
args.SetBool(2, false); // Indicates a failure result.
args.SetInt(3, errorCode);
args.SetString(4, errorMessage);
browser.SendProcessMessage(CefProcessId.Renderer, message);
args.Dispose();
message.Dispose();
}
// Cancel a query that has not been sent to a handler.
private void CancelUnhandledQuery(CefBrowser browser, int contextId, int requestId)
{
SendQueryFailure(browser, contextId, requestId, CefMessageRouter.CanceledErrorCode, CefMessageRouter.CanceledErrorMessage);
}
// Cancel a query that has already been sent to a handler.
private void CancelQuery(long queryId, QueryInfo info, bool notifyRenderer)
{
if (notifyRenderer)
SendQueryFailure(info, CefMessageRouter.CanceledErrorCode, CefMessageRouter.CanceledErrorMessage);
var frame = info.Browser.GetFrame(info.FrameId);
info.Handler.OnQueryCanceled(info.Browser, frame, queryId);
// Invalidate the callback.
info.Callback.Detach();
}
// Cancel all pending queries associated with either |browser| or |handler|.
// If both |browser| and |handler| are NULL all pending queries will be
// canceled. Set |notify_renderer| to true if the renderer should be notified.
private void CancelPendingFor(CefBrowser browser, Handler handler, bool notifyRenderer)
{
if (!CefRuntime.CurrentlyOn(CefThreadId.UI))
{
// Must execute on the UI thread.
Helpers.PostTask(CefThreadId.UI,
Helpers.Apply(this.CancelPendingFor, browser, handler, notifyRenderer)
);
return;
}
if (_browserQueryInfoMap.IsEmpty) return;
BrowserInfoMap.Visitor visitor = (int browserId, long queryId, QueryInfo info, ref bool remove) =>
{
if (handler == null || info.Handler == handler)
{
remove = true;
CancelQuery(queryId, info, notifyRenderer);
info.Dispose();
}
return true;
};
if (browser != null)
{
// Cancel all queries associated with the specified browser.
_browserQueryInfoMap.FindAll(browser.Identifier, visitor);
}
else
{
// Cancel all queries for all browsers.
_browserQueryInfoMap.FindAll(visitor);
}
}
// Cancel a query based on the renderer-side IDs. If |request_id| is
// kReservedId all requests associated with |context_id| will be canceled.
private void CancelPendingRequest(int browserId, int contextId, int requestId)
{
BrowserInfoMap.Visitor visitor = (int vBrowserId, long queryId, QueryInfo info, ref bool remove) =>
{
if (info.ContextId == contextId
&& (requestId == CefMessageRouter.ReservedId || info.RequestId == requestId))
{
remove = true;
CancelQuery(queryId, info, false);
info.Dispose();
// Stop iterating if only canceling a single request.
return requestId == CefMessageRouter.ReservedId;
}
return true;
};
_browserQueryInfoMap.FindAll(browserId, visitor);
}
}
}