init lserp cs 5.0

This commit is contained in:
cyf
2026-07-10 15:25:05 +08:00
commit 90f3fda86a
3799 changed files with 976868 additions and 0 deletions
@@ -0,0 +1,120 @@
namespace Cef3.Wrapper
{
using System;
using System.Collections.Generic;
using System.Text;
// Maps an arbitrary TId to an arbitrary TObject on a per-browser basis.
internal sealed class CefBrowserInfoMap<TKey, TValue>
{
public delegate bool Visitor(int browserId, TKey key, TValue value, ref bool remove);
private Dictionary<int, Dictionary<TKey, TValue>> _map = new Dictionary<int, Dictionary<TKey, TValue>>();
public CefBrowserInfoMap()
{
}
public bool IsEmpty
{
get
{
return Count() == 0;
}
}
public int Count()
{
var result = 0;
foreach (var v in _map.Values)
{
result += v.Count;
}
return result;
}
public int Count(int browserId)
{
Dictionary<TKey, TValue> v;
if (_map.TryGetValue(browserId, out v)) return v.Count;
else return 0;
}
public void Add(int browserId, TKey key, TValue value)
{
Dictionary<TKey, TValue> v;
if (!_map.TryGetValue(browserId, out v))
{
v = new Dictionary<TKey, TValue>();
_map.Add(browserId, v);
}
v.Add(key, value);
}
public TValue Find(int browserId, TKey key, Visitor visitor)
{
Dictionary<TKey, TValue> v;
if (!_map.TryGetValue(browserId, out v))
{
return default(TValue);
}
TValue x;
if (v.TryGetValue(key, out x))
{
bool remove = false;
visitor(browserId, key, x, ref remove);
if (remove)
{
v.Remove(key);
}
return x;
}
else return default(TValue);
}
public void FindAll(int browserId, Visitor visitor)
{
Dictionary<TKey, TValue> v;
if (!_map.TryGetValue(browserId, out v)) return;
bool hasRemoveKey = false;
TKey removeKey = default(TKey);
List<TKey> removeList = null;
foreach (var kv in v)
{
bool remove = false;
visitor(browserId, kv.Key, kv.Value, ref remove);
if (remove)
{
if (!hasRemoveKey)
{
removeKey = kv.Key;
hasRemoveKey = true;
}
else
{
if (removeList == null) removeList = new List<TKey>();
removeList.Add(kv.Key);
}
}
}
if (hasRemoveKey) v.Remove(removeKey);
if (removeList != null)
{
foreach (var k in removeList) v.Remove(k);
}
}
public void FindAll(Visitor visitor)
{
foreach (var k in _map.Keys)
{
FindAll(k, visitor);
}
}
}
}
@@ -0,0 +1,52 @@
namespace Cef3.Wrapper
{
using System;
using System.Collections.Generic;
using System.Text;
internal static class CefMessageRouter
{
// ID value reserved for internal use.
public const int ReservedId = 0;
// Appended to the JS function name for related IPC messages.
public const string MessageSuffix = "Msg";
// JS object member argument names for cefQuery.
public const string MemberRequest = "request";
public const string MemberOnSuccess = "onSuccess";
public const string MemberOnFailure = "onFailure";
public const string MemberPersistent = "persistent";
// Default error information when a query is canceled.
public const int CanceledErrorCode = -1;
public const string CanceledErrorMessage = "The query has been canceled";
public sealed class IdGeneratorInt32
{
private int _next_id;
public int GetNextId()
{
var id = ++_next_id;
if (id == CefMessageRouter.ReservedId)
id = ++_next_id;
return id;
}
}
public sealed class IdGeneratorInt64
{
private long _next_id;
public long GetNextId()
{
var id = ++_next_id;
if (id == CefMessageRouter.ReservedId)
id = ++_next_id;
return id;
}
}
}
}
@@ -0,0 +1,629 @@
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);
}
}
}
@@ -0,0 +1,44 @@
namespace Cef3.Wrapper
{
using System;
using System.Collections.Generic;
using System.Text;
/// <summary>
/// Used to configure the query router. The same values must be passed to both
/// CefMessageRouterBrowserSide and CefMessageRouterRendererSide. If using multiple
/// router pairs make sure to choose values that do not conflict.
/// </summary>
public sealed class CefMessageRouterConfig
{
public CefMessageRouterConfig()
{
JSQueryFunction = "cefQuery";
JSCancelFunction = "cefQueryCancel";
}
/// <summary>
/// Name of the JavaScript function that will be added to the 'window' object
/// for sending a query. The default value is "cefQuery".
/// </summary>
public string JSQueryFunction { get; set; }
/// <summary>
/// Name of the JavaScript function that will be added to the 'window' object
/// for canceling a pending query. The default value is "cefQueryCancel".
/// </summary>
public string JSCancelFunction { get; set; }
// Validate configuration settings.
internal bool Validate()
{
// Must specify function names.
if (string.IsNullOrEmpty(JSQueryFunction) || string.IsNullOrEmpty(JSCancelFunction))
{
return false;
}
return true;
}
}
}
@@ -0,0 +1,535 @@
namespace Cef3.Wrapper
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using BrowserRequestInfoMap = CefBrowserInfoMap<System.Collections.Generic.KeyValuePair<int, int>, Cef3.Wrapper.CefMessageRouterRendererSide.RequestInfo>;
/// <summary>
/// Implements the renderer side of query routing. The methods of this class must
/// be called on the render process main thread.
/// </summary>
public sealed class CefMessageRouterRendererSide
{
#region V8HandlerImpl
private sealed class V8HandlerImpl : CefV8Handler
{
private readonly CefMessageRouterRendererSide _router;
private readonly CefMessageRouterConfig _config;
private int _contextId;
public V8HandlerImpl(CefMessageRouterRendererSide router, CefMessageRouterConfig config)
{
_router = router;
_config = config;
_contextId = CefMessageRouter.ReservedId;
}
protected override bool Execute(string name, CefV8Value obj, CefV8Value[] arguments, out CefV8Value returnValue, out string exception)
{
if (name == _config.JSQueryFunction)
{
if (arguments.Length != 1 || !arguments[0].IsObject)
{
returnValue = null;
exception = "Invalid arguments; expecting a single object";
return true;
}
var arg = arguments[0];
var requestVal = arg.GetValue(CefMessageRouter.MemberRequest);
if (requestVal == null || !requestVal.IsString)
{
returnValue = null;
exception = "Invalid arguments; object member '" +
CefMessageRouter.MemberRequest + "' is required and must " +
"have type string";
return true;
}
CefV8Value successVal = null;
if (arg.HasValue(CefMessageRouter.MemberOnSuccess))
{
successVal = arg.GetValue(CefMessageRouter.MemberOnSuccess);
if (!successVal.IsFunction)
{
returnValue = null;
exception = "Invalid arguments; object member '" +
CefMessageRouter.MemberOnSuccess + "' must have type " +
"function";
return true;
}
}
CefV8Value failureVal = null;
if (arg.HasValue(CefMessageRouter.MemberOnFailure))
{
failureVal = arg.GetValue(CefMessageRouter.MemberOnFailure);
if (!failureVal.IsFunction)
{
returnValue = null;
exception = "Invalid arguments; object member '" +
CefMessageRouter.MemberOnFailure + "' must have type " +
"function";
return true;
}
}
CefV8Value persistentVal = null;
if (arg.HasValue(CefMessageRouter.MemberPersistent))
{
persistentVal = arg.GetValue(CefMessageRouter.MemberPersistent);
if (!persistentVal.IsBool)
{
returnValue = null;
exception = "Invalid arguments; object member '" +
CefMessageRouter.MemberPersistent + "' must have type " +
"boolean";
return true;
}
}
var context = CefV8Context.GetCurrentContext();
var contextId = GetIDForContext(context);
var frameId = context.GetFrame().Identifier;
var persistent = (persistentVal != null && persistentVal.GetBoolValue());
var requestId = _router.SendQuery(context.GetBrowser(), frameId, contextId,
requestVal.GetStringValue(), persistent, successVal, failureVal);
returnValue = CefV8Value.CreateInt(requestId);
exception = null;
return true;
}
else if (name == _config.JSCancelFunction)
{
if (arguments.Length != 1 || !arguments[0].IsInt)
{
returnValue = null;
exception = "Invalid arguments; expecting a single integer";
return true;
}
var result = false;
var requestId = arguments[0].GetIntValue();
if (requestId != CefMessageRouter.ReservedId)
{
CefV8Context context = CefV8Context.GetCurrentContext();
var contextId = GetIDForContext(context);
var frameId = context.GetFrame().Identifier;
result = _router.SendCancel(context.GetBrowser(), frameId, contextId, requestId);
}
returnValue = CefV8Value.CreateBool(result);
exception = null;
return true;
}
returnValue = null;
exception = null;
return false;
}
// Don't create the context ID until it's actually needed.
private int GetIDForContext(CefV8Context context)
{
if (_contextId == CefMessageRouter.ReservedId)
_contextId = _router.CreateIDForContext(context);
return _contextId;
}
};
#endregion
private readonly CefMessageRouterConfig _config;
private readonly string _queryMessageName;
private readonly string _cancelMessageName;
private readonly CefMessageRouter.IdGeneratorInt32 _contextIdGenerator = new CefMessageRouter.IdGeneratorInt32();
private readonly CefMessageRouter.IdGeneratorInt32 _requestIdGenerator = new CefMessageRouter.IdGeneratorInt32();
// Map of (request ID, context ID) to RequestInfo for pending queries. An
// entry is added when a request is initiated via the bound function and
// removed when either the request completes, is canceled via the bound
// function, or the associated context is released.
private readonly BrowserRequestInfoMap _browserRequestInfoMap = new BrowserRequestInfoMap();
// Map of context ID to CefV8Context for existing contexts. An entry is added
// when a bound function is executed for the first time in the context and
// removed when the context is released.
private readonly Dictionary<int, CefV8Context> _contextMap = new Dictionary<int, CefV8Context>();
/// <summary>
/// Create a new router with the specified configuration.
/// </summary>
public CefMessageRouterRendererSide(CefMessageRouterConfig config)
{
if (!config.Validate()) throw new ArgumentException("Invalid configuration.");
_config = config;
_queryMessageName = config.JSQueryFunction + CefMessageRouter.MessageSuffix;
_cancelMessageName = config.JSCancelFunction + CefMessageRouter.MessageSuffix;
}
/// <summary>
/// Create a new router with the specified configuration.
/// </summary>
public static CefMessageRouterRendererSide Create(CefMessageRouterConfig config)
{
return new CefMessageRouterRendererSide(config);
}
/// <summary>
/// Returns the number of queries currently pending for the specified |browser|
/// and/or |context|. Either or both values may be empty.
/// </summary>
public int GetPendingCount(CefBrowser browser = null, CefV8Context context = null)
{
Helpers.RequireRendererThread();
if (_browserRequestInfoMap.IsEmpty) return 0;
if (context != null)
{
var contextId = GetIDForContext(context, false);
if (contextId == CefMessageRouter.ReservedId)
return 0; // Nothing associated with the specified context.
int count = 0;
BrowserRequestInfoMap.Visitor visitor = (int browserId, KeyValuePair<int, int> infoId, RequestInfo info, ref bool remove) =>
{
if (infoId.Key == contextId) count++;
return true;
};
if (browser != null)
{
// Count requests associated with the specified browser.
_browserRequestInfoMap.FindAll(browser.Identifier, visitor);
}
else
{
// Count all requests for all browsers.
_browserRequestInfoMap.FindAll(visitor);
}
return count;
}
else if (browser != null)
{
return _browserRequestInfoMap.Count(browser.Identifier);
}
else
{
return _browserRequestInfoMap.Count();
}
return 0;
}
#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 CefRenderProcessHandler::OnContextCreated. Registers the
/// JavaScripts functions with the new context.
/// </summary>
public void OnContextCreated(CefBrowser browser, CefFrame frame, CefV8Context context)
{
Helpers.RequireRendererThread();
// Register function handlers with the 'window' object.
var window = context.GetGlobal();
var handler = new V8HandlerImpl(this, _config);
CefV8PropertyAttribute attributes = CefV8PropertyAttribute.ReadOnly | CefV8PropertyAttribute.DontEnum | CefV8PropertyAttribute.DontDelete;
// Add the query function.
var queryFunc = CefV8Value.CreateFunction(_config.JSQueryFunction, handler);
window.SetValue(_config.JSQueryFunction, queryFunc, attributes);
// Add the cancel function.
var cancelFunc = CefV8Value.CreateFunction(_config.JSCancelFunction, handler);
window.SetValue(_config.JSCancelFunction, cancelFunc, attributes);
}
/// <summary>
/// Call from CefRenderProcessHandler::OnContextReleased. Any pending queries
/// associated with the released context will be canceled and
/// Handler::OnQueryCanceled will be called in the browser process.
/// </summary>
public void OnContextReleased(CefBrowser browser, CefFrame frame, CefV8Context context)
{
Helpers.RequireRendererThread();
// Get the context ID and remove the context from the map.
var contextId = GetIDForContext(context, true);
if (contextId != CefMessageRouter.ReservedId)
{
// Cancel all pending requests for the context.
SendCancel(browser, frame.Identifier, contextId, CefMessageRouter.ReservedId);
}
}
/// <summary>
/// Call from CefRenderProcessHandler::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.RequireRendererThread();
var messageName = message.Name;
if (messageName == _queryMessageName)
{
var args = message.Arguments;
Debug.Assert(args.Count > 3);
var contextId = args.GetInt(0);
var requestId = args.GetInt(1);
var isSuccess = args.GetBool(2);
if (isSuccess)
{
Debug.Assert(args.Count == 4);
string response = args.GetString(3);
Helpers.PostTaskUncertainty(CefThreadId.Renderer,
Helpers.Apply(this.ExecuteSuccessCallback, browser.Identifier, contextId, requestId, response)
);
}
else
{
Debug.Assert(args.Count == 5);
var errorCode = args.GetInt(3);
string errorMessage = args.GetString(4);
Helpers.PostTaskUncertainty(CefThreadId.Renderer,
Helpers.Apply(this.ExecuteFailureCallback, browser.Identifier, contextId, requestId, errorCode, errorMessage)
);
}
return true;
}
return false;
}
#endregion
// Structure representing a pending request.
internal class RequestInfo
{
// True if the request is persistent.
public bool Persistent;
// Success callback function. May be NULL.
public CefV8Value SuccessCallback;
// Failure callback function. May be NULL.
public CefV8Value FailureCallback;
internal void Dispose()
{
}
};
// Retrieve a RequestInfo object from the map based on the renderer-side
// IDs. If |always_remove| is true then the RequestInfo object will always be
// removed from the map. Othewise, the RequestInfo 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 RequestInfo GetRequestInfo(int browserId, int requestId, int contextId, bool alwaysRemove, ref bool removed)
{
var removedTemp = false;
BrowserRequestInfoMap.Visitor visitor = (int vBrowserId, KeyValuePair<int, int> vInfoId, RequestInfo vInfo, ref bool vRemove) =>
{
vRemove = removedTemp = (alwaysRemove || !vInfo.Persistent);
return true;
};
var info = _browserRequestInfoMap.Find(browserId, new KeyValuePair<int, int>(requestId, contextId), visitor);
if (info != null) removed = removedTemp;
return info;
}
// Returns the new request ID.
private int SendQuery(CefBrowser browser, long frameId, int contextId, string request, bool persistent, CefV8Value successCallback, CefV8Value failureCallback)
{
Helpers.RequireRendererThread();
var requestId = _requestIdGenerator.GetNextId();
var info = new RequestInfo
{
Persistent = persistent,
SuccessCallback = successCallback,
FailureCallback = failureCallback,
};
_browserRequestInfoMap.Add(browser.Identifier, new KeyValuePair<int, int>(contextId, requestId), info);
var message = CefProcessMessage.Create(_queryMessageName);
var args = message.Arguments;
args.SetInt(0, Helpers.Int64GetLow(frameId));
args.SetInt(1, Helpers.Int64GetHigh(frameId));
args.SetInt(2, contextId);
args.SetInt(3, requestId);
args.SetString(4, request);
args.SetBool(5, persistent);
browser.SendProcessMessage(CefProcessId.Browser, message);
args.Dispose();
message.Dispose();
return requestId;
}
// If |requestId| is kReservedId all requests associated with |contextId|
// will be canceled, otherwise only the specified |requestId| will be
// canceled. Returns true if any request was canceled.
private bool SendCancel(CefBrowser browser, long frameId, int contextId, int requestId)
{
Helpers.RequireRendererThread();
var browserId = browser.Identifier;
int cancelCount = 0;
if (requestId != CefMessageRouter.ReservedId)
{
// Cancel a single request.
bool removed = false;
var info = GetRequestInfo(browserId, contextId, requestId, true, ref removed);
if (info != null)
{
Debug.Assert(removed);
info.Dispose();
cancelCount = 1;
}
}
else
{
// Cancel all requests with the specified context ID.
BrowserRequestInfoMap.Visitor visitor = (int vBrowserId, KeyValuePair<int, int> vInfoId, RequestInfo vInfo, ref bool vRemove) =>
{
if (vInfoId.Key == contextId)
{
vRemove = true;
vInfo.Dispose();
cancelCount++;
}
return true;
};
_browserRequestInfoMap.FindAll(browserId, visitor);
}
if (cancelCount > 0)
{
var message = CefProcessMessage.Create(_cancelMessageName);
var args = message.Arguments;
args.SetInt(0, contextId);
args.SetInt(1, requestId);
browser.SendProcessMessage(CefProcessId.Browser, message);
return true;
}
return false;
}
// Execute the onSuccess JavaScript callback.
private void ExecuteSuccessCallback(int browserId, int contextId, int requestId, string response)
{
Helpers.RequireRendererThread();
bool removed = false;
var info = GetRequestInfo(browserId, contextId, requestId, false, ref removed);
if (info == null) return;
var context = GetContextByID(contextId);
if (context != null && info.SuccessCallback != null)
{
var args = new[] { CefV8Value.CreateString(response) };
info.SuccessCallback.ExecuteFunctionWithContext(context, null, args);
}
if (removed) info.Dispose();
}
// Execute the onFailure JavaScript callback.
void ExecuteFailureCallback(int browserId, int contextId, int requestId,
int errorCode, string errorMessage)
{
Helpers.RequireRendererThread();
bool removed = false;
var info = GetRequestInfo(browserId, contextId, requestId, true, ref removed);
if (info == null) return;
var context = GetContextByID(contextId);
if (context != null && info.FailureCallback != null)
{
var args = new[] {
CefV8Value.CreateInt(errorCode),
CefV8Value.CreateString(errorMessage)
};
info.FailureCallback.ExecuteFunctionWithContext(context, null, args);
}
Debug.Assert(removed);
info.Dispose();
}
private int CreateIDForContext(CefV8Context context)
{
Helpers.RequireRendererThread();
// The context should not already have an associated ID.
Debug.Assert(GetIDForContext(context, false) == CefMessageRouter.ReservedId);
var contextId = _contextIdGenerator.GetNextId();
_contextMap.Add(contextId, context);
return contextId;
}
// Retrieves the existing ID value associated with the specified |context|.
// If |remove| is true the context will also be removed from the map.
private int GetIDForContext(CefV8Context context, bool remove)
{
Helpers.RequireRendererThread();
int contextId = CefMessageRouter.ReservedId;
int? removeContextId = null;
foreach (var kv in _contextMap)
{
if (kv.Value.IsSame(context))
{
contextId = kv.Key;
if (remove)
{
removeContextId = contextId;
}
break;
}
}
if (removeContextId.HasValue) _contextMap.Remove(removeContextId.Value);
return contextId;
}
private CefV8Context GetContextByID(int contextId)
{
Helpers.RequireRendererThread();
CefV8Context context;
if (_contextMap.TryGetValue(contextId, out context)) return context;
else return null;
}
}
}
@@ -0,0 +1,101 @@
namespace Cef3.Wrapper
{
using System;
using System.Collections.Generic;
using System.Text;
internal static class Helpers
{
public static void PostTask(CefThreadId threadId, Action action)
{
CefRuntime.PostTask(threadId, new ActionTask(action));
}
public static void PostTaskUncertainty(CefThreadId threadId, Action action)
{
CefRuntime.PostTask(threadId, new ActionTask(action));
}
internal sealed class ActionTask : CefTask
{
public Action _action;
public ActionTask(Action action)
{
_action = action;
}
protected override void Execute()
{
_action();
_action = null;
}
}
internal static void RequireUIThread()
{
if (!CefRuntime.CurrentlyOn(CefThreadId.UI))
throw new InvalidOperationException("This method should be called on CEF UI thread.");
}
internal static void RequireRendererThread()
{
if (!CefRuntime.CurrentlyOn(CefThreadId.Renderer))
throw new InvalidOperationException("This method should be called on CEF renderer thread.");
}
internal delegate void Action();
internal delegate void Action<T1, T2>(T1 arg1, T2 arg2);
internal delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3);
internal delegate void Action<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
internal delegate void Action<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);
//internal delegate TResult Func<TResult>();
//internal delegate TResult Func<T, TResult>(T arg);
//internal delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
public static Action Apply<T>(Action<T> action, T arg)
{
return () => action(arg);
}
public static Action Apply<T1, T2>(Action<T1, T2> action, T1 arg1, T2 arg2)
{
return () => action(arg1, arg2);
}
public static Action Apply<T1, T2, T3>(Action<T1, T2, T3> action, T1 arg1, T2 arg2, T3 arg3)
{
return () => action(arg1, arg2, arg3);
}
public static Action Apply<T1, T2, T3, T4>(Action<T1, T2, T3, T4> action, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
{
return () => action(arg1, arg2, arg3, arg4);
}
public static Action Apply<T1, T2, T3, T4, T5>(Action<T1, T2, T3, T4, T5> action, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
{
return () => action(arg1, arg2, arg3, arg4, arg5);
}
public static long Int64Set(int low, int high)
{
return (uint)low | (long)high << 32;
}
public static int Int64GetLow(long value)
{
return (int)value;
}
public static int Int64GetHigh(long value)
{
return (int)(value >> 32);
}
}
}
@@ -0,0 +1,147 @@
// The below classes implement support for routing aynchronous messages between
// JavaScript running in the renderer process and C++ running in the browser
// process. An application interacts with the router by passing it data from
// standard CEF C++ callbacks (OnBeforeBrowse, OnProcessMessageRecieved,
// OnContextCreated, etc). The renderer-side router supports generic JavaScript
// callback registration and execution while the browser-side router supports
// application-specific logic via one or more application-provided Handler
// instances.
//
// The renderer-side router implementation exposes a query function and a cancel
// function via the JavaScript 'window' object:
//
// // Create and send a new query.
// var request_id = window.cefQuery({
// request: 'my_request',
// persistent: false,
// onSuccess: function(response) {},
// onFailure: function(error_code, error_message) {}
// });
//
// // Optionally cancel the query.
// window.cefQueryCancel(request_id);
//
// When |window.cefQuery| is executed the request is sent asynchronously to one
// or more C++ Handler objects registered in the browser process. Each C++
// Handler can choose to either handle or ignore the query in the
// Handler::OnQuery callback. If a Handler chooses to handle the query then it
// should execute Callback::Success when a response is available or
// Callback::Failure if an error occurs. This will result in asynchronous
// execution of the associated JavaScript callback in the renderer process. Any
// queries unhandled by C++ code in the browser process will be automatically
// canceled and the associated JavaScript onFailure callback will be executed
// with an error code of -1.
//
// Queries can be either persistent or non-persistent. If the query is
// persistent than the callbacks will remain registered until one of the
// following conditions are met:
//
// A. The query is canceled in JavaScript using the |window.cefQueryCancel|
// function.
// B. The query is canceled in C++ code using the Callback::Failure function.
// C. The context associated with the query is released due to browser
// destruction, navigation or renderer process termination.
//
// If the query is non-persistent then the registration will be removed after
// the JavaScript callback is executed a single time. If a query is canceled for
// a reason other than Callback::Failure being executed then the associated
// Handler's OnQueryCanceled method will be called.
//
// Some possible usage patterns include:
//
// One-time Request. Use a non-persistent query to send a JavaScript request.
// The Handler evaluates the request and returns the response. The query is
// then discarded.
//
// Broadcast. Use a persistent query to register as a JavaScript broadcast
// receiver. The Handler keeps track of all registered Callbacks and executes
// them sequentially to deliver the broadcast message.
//
// Subscription. Use a persistent query to register as a JavaScript subscription
// receiver. The Handler initiates the subscription feed on the first request
// and delivers responses to all registered subscribers as they become
// available. The Handler cancels the subscription feed when there are no
// longer any registered JavaScript receivers.
//
// Message routing occurs on a per-browser and per-context basis. Consequently,
// additional application logic can be applied by restricting which browser or
// context instances are passed into the router. If you choose to use this
// approach do so cautiously. In order for the router to function correctly any
// browser or context instance passed into a single router callback must then
// be passed into all router callbacks.
//
// There is generally no need to have multiple renderer-side routers unless you
// wish to have multiple bindings with different JavaScript function names. It
// can be useful to have multiple browser-side routers with different client-
// provided Handler instances when implementing different behaviors on a per-
// browser basis.
//
// This implementation places no formatting restrictions on payload content.
// An application may choose to exchange anything from simple formatted
// strings to serialized XML or JSON data.
//
//
// EXAMPLE USAGE
//
// 1. Define the router configuration. You can optionally specify settings
// like the JavaScript function names. The configuration must be the same in
// both the browser and renderer processes. If using multiple routers in the
// same application make sure to specify unique function names for each
// router configuration.
//
// // Example config object showing the default values.
// CefMessageRouterConfig config;
// config.js_query_function = "cefQuery";
// config.js_cancel_function = "cefQueryCancel";
//
// 2. Create an instance of CefMessageRouterBrowserSide in the browser process.
// You might choose to make it a member of your CefClient implementation,
// for example.
//
// browser_side_router_ = CefMessageRouterBrowserSide::Create(config);
//
// 3. Register one or more Handlers. The Handler instances must either outlive
// the router or be removed from the router before they're deleted.
//
// browser_side_router_->AddHandler(my_handler);
//
// 4. Call all required CefMessageRouterBrowserSide methods from other callbacks
// in your CefClient implementation (OnBeforeClose, etc). See the
// CefMessageRouterBrowserSide class documentation for the complete list of
// methods.
//
// 5. Create an instance of CefMessageRouterRendererSide in the renderer process.
// You might choose to make it a member of your CefApp implementation, for
// example.
//
// renderer_side_router_ = CefMessageRouterRendererSide::Create(config);
//
// 6. Call all required CefMessageRouterRendererSide methods from other
// callbacks in your CefRenderProcessHandler implementation
// (OnContextCreated, etc). See the CefMessageRouterRendererSide class
// documentation for the complete list of methods.
//
// 7. Execute the query function from JavaScript code.
//
// window.cefQuery({request: 'my_request',
// persistent: false,
// onSuccess: function(response) { print(response); },
// onFailure: function(error_code, error_message) {} });
//
// 8. Handle the query in your Handler::OnQuery implementation and execute the
// appropriate callback either immediately or asynchronously.
//
// void MyHandler::OnQuery(int64 query_id,
// CefRefPtr<CefBrowser> browser,
// CefRefPtr<CefFrame> frame,
// const CefString& request,
// bool persistent,
// CefRefPtr<Callback> callback) {
// if (request == "my_request") {
// callback->Continue("my_response");
// return true;
// }
// return false; // Not handled.
// }
//
// 9. Notice that the onSuccess callback is executed in JavaScript.