// Copyright © 2019 The CefSharp Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace CefSharp.Internals
{
///
/// ConcurrentMethodRunnerQueue - Async Javascript Binding methods are run
/// on the ThreadPool in parallel, when a method returns a Task
/// the we use ContinueWith to be notified of completion then
/// raise the MethodInvocationComplete event
///
public sealed class ConcurrentMethodRunnerQueue : IMethodRunnerQueue
{
private static readonly Type VoidTaskResultType = Type.GetType("System.Threading.Tasks.VoidTaskResult");
private readonly IJavascriptObjectRepositoryInternal repository;
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
///
public event EventHandler MethodInvocationComplete;
///
/// Default constructor
///
/// javascript object repository
public ConcurrentMethodRunnerQueue(IJavascriptObjectRepositoryInternal repository)
{
this.repository = repository;
}
///
public void Dispose()
{
MethodInvocationComplete = null;
cancellationTokenSource.Cancel();
}
///
public void Enqueue(MethodInvocation methodInvocation)
{
if (cancellationTokenSource.IsCancellationRequested)
{
return;
}
//Enqueue on ThreadPool
Task.Run(async () =>
{
if (cancellationTokenSource.IsCancellationRequested)
{
return;
}
var result = await ExecuteMethodInvocation(methodInvocation).ConfigureAwait(false);
if (cancellationTokenSource.IsCancellationRequested)
{
return;
}
//If the call failed or returned null then we'll fire the event immediately
if (!result.Success || result.Result == null)
{
OnMethodInvocationComplete(result, cancellationTokenSource.Token);
}
else
{
var resultType = result.Result.GetType();
//If the returned type is Task then we'll ContinueWith and perform the processing then.
if (typeof(Task).IsAssignableFrom(resultType))
{
var resultTask = (Task)result.Result;
if (resultType.IsGenericType)
{
//Discard the continuation as we rely on
//OnMethodInvocationComplete to send the response
//to the render process.
_ = resultTask.ContinueWith((t) =>
{
if (t.Status == TaskStatus.RanToCompletion)
{
//We use some reflection to get the Result
//If someone has a better way of doing this then please submit a PR
result.Result = resultType.GetProperty("Result").GetValue(resultTask);
if (result.Result != null && result.Result.GetType() == VoidTaskResultType)
{
result.Result = null;
}
}
else
{
result.Success = false;
result.Result = null;
var aggregateException = t.Exception;
//TODO: Add support for passing a more complex message
// to better represent the Exception
if (aggregateException.InnerExceptions.Count == 1)
{
result.Message = aggregateException.InnerExceptions[0].ToString();
}
else
{
result.Message = t.Exception.ToString();
}
}
OnMethodInvocationComplete(result, cancellationTokenSource.Token);
},
cancellationTokenSource.Token, TaskContinuationOptions.None, TaskScheduler.Default);
}
else
{
//If it's not a generic Task then it doesn't have a return object
//So we'll just set the result to null and continue on
result.Result = null;
OnMethodInvocationComplete(result, cancellationTokenSource.Token);
}
}
else
{
OnMethodInvocationComplete(result, cancellationTokenSource.Token);
}
}
}, cancellationTokenSource.Token);
}
private async Task ExecuteMethodInvocation(MethodInvocation methodInvocation)
{
object returnValue = null;
string exception;
var success = false;
var nameConverter = repository.NameConverter;
//make sure we don't throw exceptions in the executor task
try
{
var result = await repository.TryCallMethodAsync(methodInvocation.ObjectId, methodInvocation.MethodName, methodInvocation.Parameters.ToArray()).ConfigureAwait(false);
success = result.Success;
returnValue = result.ReturnValue;
exception = result.Exception;
}
catch (Exception e)
{
exception = e.Message;
}
return new MethodInvocationResult
{
BrowserId = methodInvocation.BrowserId,
CallbackId = methodInvocation.CallbackId,
FrameId = methodInvocation.FrameId,
Message = exception,
Result = returnValue,
Success = success,
NameConverter = nameConverter
};
}
private void OnMethodInvocationComplete(MethodInvocationResult e, CancellationToken token)
{
//If cancellation has been requested we don't need to continue.
if (!token.IsCancellationRequested)
{
MethodInvocationComplete?.Invoke(this, new MethodInvocationCompleteArgs(e));
}
}
}
}