diff --git a/CefSharp.Core.Runtime/Internals/CefFrameWrapper.cpp b/CefSharp.Core.Runtime/Internals/CefFrameWrapper.cpp index 423be8be6..e852a8381 100644 --- a/CefSharp.Core.Runtime/Internals/CefFrameWrapper.cpp +++ b/CefSharp.Core.Runtime/Internals/CefFrameWrapper.cpp @@ -237,7 +237,7 @@ Task^ CefFrameWrapper::EvaluateScriptAsync(String^ script, //If we're unable to get the underlying browser/browserhost then return null if (!browser.get() || !host.get()) { - return nullptr; + return Task::FromException(gcnew InvalidOperationException("Browser host not available")); } auto client = static_cast(host->GetClient().get()); @@ -245,7 +245,7 @@ Task^ CefFrameWrapper::EvaluateScriptAsync(String^ script, auto pendingTaskRepository = client->GetPendingTaskRepository(); //create a new taskcompletionsource - auto idAndComplectionSource = pendingTaskRepository->CreatePendingTask(timeout); + auto idAndComplectionSource = pendingTaskRepository->CreatePendingTask(Identifier, timeout); if (useImmediatelyInvokedFuncExpression) { diff --git a/CefSharp.Core.Runtime/Internals/ClientAdapter.cpp b/CefSharp.Core.Runtime/Internals/ClientAdapter.cpp index 6a48a4668..91808c0b4 100644 --- a/CefSharp.Core.Runtime/Internals/ClientAdapter.cpp +++ b/CefSharp.Core.Runtime/Internals/ClientAdapter.cpp @@ -701,6 +701,8 @@ namespace CefSharp void ClientAdapter::OnRenderProcessTerminated(CefRefPtr browser, TerminationStatus status, int errorCode, const CefString& errorString) { + _pendingTaskRepository->CancelPendingTasks(); + auto handler = _browserControl->RequestHandler; if (handler != nullptr) @@ -1382,9 +1384,13 @@ namespace CefSharp //we get here, only continue if we have a valid frame reference if (frame.get() && frame->IsValid()) { + auto frameId = StringUtils::ToClr(frame->GetIdentifier()); + + _pendingTaskRepository->CancelPendingTasks(frameId); + if (frame->IsMain()) { - _browserControl->SetCanExecuteJavascriptOnMainFrame(StringUtils::ToClr(frame->GetIdentifier()), false); + _browserControl->SetCanExecuteJavascriptOnMainFrame(frameId, false); } auto handler = _browserControl->RenderProcessMessageHandler; @@ -1475,14 +1481,16 @@ namespace CefSharp return true; } + auto frameId = StringUtils::ToClr(frame->GetIdentifier()); + auto callbackFactory = browserAdapter->JavascriptCallbackFactory; auto success = argList->GetBool(0); auto callbackId = GetInt64(argList, 1); auto pendingTask = name == kEvaluateJavascriptResponse ? - _pendingTaskRepository->RemovePendingTask(callbackId) : - _pendingTaskRepository->RemoveJavascriptCallbackPendingTask(callbackId); + _pendingTaskRepository->RemovePendingTask(frameId, callbackId) : + _pendingTaskRepository->RemoveJavascriptCallbackPendingTask(frameId, callbackId); if (pendingTask != nullptr) { diff --git a/CefSharp.Core.Runtime/Internals/ClientAdapter.h b/CefSharp.Core.Runtime/Internals/ClientAdapter.h index 0586cb004..eef96bc42 100644 --- a/CefSharp.Core.Runtime/Internals/ClientAdapter.h +++ b/CefSharp.Core.Runtime/Internals/ClientAdapter.h @@ -70,8 +70,7 @@ namespace CefSharp CloseAllPopups(true); - //this will dispose the repository and cancel all pending tasks - delete _pendingTaskRepository; + _pendingTaskRepository->CancelPendingTasks(); _browser = nullptr; _browserControl = nullptr; @@ -80,6 +79,7 @@ namespace CefSharp _tooltip = nullptr; _browserAdapter = nullptr; _popupBrowsers = nullptr; + _pendingTaskRepository = nullptr; } HWND GetBrowserHwnd() { return _browserHwnd; } diff --git a/CefSharp.Core.Runtime/Internals/JavascriptCallbackProxy.cpp b/CefSharp.Core.Runtime/Internals/JavascriptCallbackProxy.cpp index be8f4d786..1e96fb918 100644 --- a/CefSharp.Core.Runtime/Internals/JavascriptCallbackProxy.cpp +++ b/CefSharp.Core.Runtime/Internals/JavascriptCallbackProxy.cpp @@ -31,26 +31,27 @@ namespace CefSharp } auto browserWrapper = static_cast(browser); - auto javascriptNameConverter = GetJavascriptNameConverter(); - - auto doneCallback = _pendingTasks->CreateJavascriptCallbackPendingTask(_callback->Id, timeout); - - auto callbackMessage = CefProcessMessage::Create(kJavascriptCallbackRequest); - auto argList = callbackMessage->GetArgumentList(); - SetInt64(argList, 0, doneCallback.Key); - SetInt64(argList, 1, _callback->Id); - auto paramList = CefListValue::Create(); - for (int i = 0; i < parameters->Length; i++) - { - auto param = parameters[i]; - SerializeV8Object(paramList, i, param, javascriptNameConverter); - } - argList->SetList(2, paramList); auto frame = browserWrapper->Browser->GetFrameByIdentifier(StringUtils::ToNative(_callback->FrameId)); if (frame.get() && frame->IsValid()) { + auto javascriptNameConverter = GetJavascriptNameConverter(); + + auto doneCallback = _pendingTasks->CreateJavascriptCallbackPendingTask(_callback->FrameId, _callback->Id, timeout); + + auto callbackMessage = CefProcessMessage::Create(kJavascriptCallbackRequest); + auto argList = callbackMessage->GetArgumentList(); + SetInt64(argList, 0, doneCallback.Key); + SetInt64(argList, 1, _callback->Id); + auto paramList = CefListValue::Create(); + for (int i = 0; i < parameters->Length; i++) + { + auto param = parameters[i]; + SerializeV8Object(paramList, i, param, javascriptNameConverter); + } + argList->SetList(2, paramList); + frame->SendProcessMessage(CefProcessId::PID_RENDERER, callbackMessage); return doneCallback.Value->Task; diff --git a/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs b/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs index c4dd26c5c..3cc1bf181 100644 --- a/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs +++ b/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading.Tasks; using Bogus; +using CefSharp.Example; using Xunit; using Xunit.Abstractions; using Xunit.Repeat; @@ -26,6 +27,52 @@ public EvaluateScriptAsyncTests(ITestOutputHelper output, CefSharpFixture collec this.collectionFixture = collectionFixture; } + [Fact] + public async Task ShouldCancelAfterV8ContextChange() + { + Task evaluateCancelAfterDisposeTask; + using (var browser = new CefSharp.OffScreen.ChromiumWebBrowser(automaticallyCreateBrowser: false)) + { + await browser.CreateBrowserAsync(); + + // no V8 context + var withoutV8ContextException = await Assert.ThrowsAsync(() => browser.EvaluateScriptAsync("1+1")); + Assert.StartsWith("Unable to execute javascript at this time", withoutV8ContextException.Message); + + Task evaluateWithoutV8ContextTask; + using (var frame = browser.GetMainFrame()) + { + evaluateWithoutV8ContextTask = frame.EvaluateScriptAsync("1+2"); + } + + // V8 context + await browser.LoadUrlAsync(CefExample.HelloWorldUrl); + var evaluateWithoutV8ContextResponse = await evaluateWithoutV8ContextTask; + Assert.True(evaluateWithoutV8ContextResponse.Success); + Assert.Equal(3, evaluateWithoutV8ContextResponse.Result); + + var evaluateCancelAfterV8ContextChangeTask = browser.EvaluateScriptAsync("new Promise(resolve => setTimeout(resolve, 1000))"); + + // change V8 context + await browser.LoadUrlAsync(CefExample.HelloWorldUrl); + + await Assert.ThrowsAsync(() => evaluateCancelAfterV8ContextChangeTask); + + evaluateCancelAfterDisposeTask = browser.EvaluateScriptAsync("new Promise(resolve => setTimeout(resolve, 1000))"); + } + await Assert.ThrowsAsync(() => evaluateCancelAfterDisposeTask); + } + + [Fact] + public async Task ShouldCancelOnCrash() + { + AssertInitialLoadComplete(); + + var task = Browser.EvaluateScriptAsync("new Promise(resolve => setTimeout(resolve, 1000))"); + await Browser.LoadUrlAsync("chrome://crash"); + await Assert.ThrowsAsync(() => task); + } + [Theory] [InlineData(double.MaxValue, "Number.MAX_VALUE")] [InlineData(double.MaxValue / 2, "Number.MAX_VALUE / 2")] @@ -264,7 +311,7 @@ public async Task CanEvaluateScriptAsyncReturnArrayBuffer(int iteration) var randomizer = new Randomizer(); - var expected = randomizer.Utf16String(minLength: iteration, maxLength:iteration); + var expected = randomizer.Utf16String(minLength: iteration, maxLength: iteration); var expectedBytes = Encoding.UTF8.GetBytes(expected); var javascriptResponse = await Browser.EvaluateScriptAsync($"new TextEncoder().encode('{expected}').buffer"); diff --git a/CefSharp.Test/Javascript/JavascriptCallbackTests.cs b/CefSharp.Test/Javascript/JavascriptCallbackTests.cs index a945f7d9b..c548fcdb8 100644 --- a/CefSharp.Test/Javascript/JavascriptCallbackTests.cs +++ b/CefSharp.Test/Javascript/JavascriptCallbackTests.cs @@ -6,6 +6,7 @@ using System.Dynamic; using System.Globalization; using System.Threading.Tasks; +using CefSharp.Example; using Xunit; using Xunit.Abstractions; @@ -23,6 +24,74 @@ public JavascriptCallbackTests(ITestOutputHelper output, CefSharpFixture collect this.collectionFixture = collectionFixture; } + [Fact] + public async Task ShouldCancelAfterV8ContextChange() + { + IJavascriptCallback callbackExecuteCancelAfterDisposeCallback; + Task callbackExecuteCancelAfterDisposeTask; + using (var browser = new CefSharp.OffScreen.ChromiumWebBrowser(automaticallyCreateBrowser: false)) + { + await browser.CreateBrowserAsync(); + + // no V8 context + var withoutV8ContextException = await Assert.ThrowsAsync(() => browser.EvaluateScriptAsync("(function() { return 1+1; })")); + Assert.StartsWith("Unable to execute javascript at this time", withoutV8ContextException.Message); + + Task callbackExecuteWithoutV8ContextTask; + using (var frame = browser.GetMainFrame()) + { + callbackExecuteWithoutV8ContextTask = frame.EvaluateScriptAsync("(function() { return 1+2; })"); + } + + // V8 context + await browser.LoadUrlAsync(CefExample.HelloWorldUrl); + + var callbackExecuteWithoutV8ContextResponse = await callbackExecuteWithoutV8ContextTask; + Assert.True(callbackExecuteWithoutV8ContextResponse.Success); + var callbackExecuteWithoutV8ContextCallback = (IJavascriptCallback)callbackExecuteWithoutV8ContextResponse.Result; + var callbackExecuteWithoutV8ContextExecuteResponse = await callbackExecuteWithoutV8ContextCallback.ExecuteAsync(); + Assert.True(callbackExecuteWithoutV8ContextExecuteResponse.Success); + Assert.Equal(3, callbackExecuteWithoutV8ContextExecuteResponse.Result); + + var callbackExecuteCancelAfterV8ContextResponse = await browser.EvaluateScriptAsync("(function() { return new Promise(resolve => setTimeout(resolve, 1000)); })"); + Assert.True(callbackExecuteCancelAfterV8ContextResponse.Success); + var callbackExecuteCancelAfterV8ContextCallback = (IJavascriptCallback)callbackExecuteCancelAfterV8ContextResponse.Result; + var callbackExecuteCancelAfterV8ContextTask = callbackExecuteCancelAfterV8ContextCallback.ExecuteAsync(); + + // change V8 context + await browser.LoadUrlAsync(CefExample.HelloWorldUrl); + + await Assert.ThrowsAsync(() => callbackExecuteCancelAfterV8ContextTask); + var callbackExecuteCancelAfterV8ContextResult = await callbackExecuteCancelAfterV8ContextCallback.ExecuteAsync(); + Assert.False(callbackExecuteCancelAfterV8ContextResult.Success); + Assert.StartsWith("Unable to find JavascriptCallback with Id " + callbackExecuteCancelAfterV8ContextCallback.Id, callbackExecuteCancelAfterV8ContextResult.Message); + + var callbackExecuteCancelAfterDisposeResponse = await browser.EvaluateScriptAsync("(function() { return new Promise(resolve => setTimeout(resolve, 1000)); })"); + Assert.True(callbackExecuteCancelAfterDisposeResponse.Success); + callbackExecuteCancelAfterDisposeCallback = (IJavascriptCallback)callbackExecuteCancelAfterDisposeResponse.Result; + callbackExecuteCancelAfterDisposeTask = callbackExecuteCancelAfterDisposeCallback.ExecuteAsync(); + } + Assert.False(callbackExecuteCancelAfterDisposeCallback.CanExecute); + await Assert.ThrowsAsync(() => callbackExecuteCancelAfterDisposeTask); + await Assert.ThrowsAsync(() => callbackExecuteCancelAfterDisposeCallback.ExecuteAsync()); + } + + [Fact] + public async Task ShouldCancelOnCrash() + { + AssertInitialLoadComplete(); + + var javascriptResponse = await Browser.EvaluateScriptAsync("(function() { return new Promise(resolve => setTimeout(resolve, 1000)); })"); + Assert.True(javascriptResponse.Success); + + var callback = (IJavascriptCallback)javascriptResponse.Result; + + var task = callback.ExecuteAsync(); + + await Browser.LoadUrlAsync("chrome://crash"); + await Assert.ThrowsAsync(() => task); + } + [Theory] [InlineData("(function() { return Promise.resolve(53)})", 53)] [InlineData("(function() { return Promise.resolve('53')})", "53")] @@ -233,5 +302,23 @@ public async Task ShouldWorkWithExpandoObject() output.WriteLine("Expected {0} : Actual {1}", expectedDateTime, actualDateTime); } + + [Fact] + public async Task ShouldWorkWhenExecutedMultipleTimes() + { + AssertInitialLoadComplete(); + + var javascriptResponse = await Browser.EvaluateScriptAsync("(function() { return 42; })"); + Assert.True(javascriptResponse.Success); + + var callback = (IJavascriptCallback)javascriptResponse.Result; + + for (var i = 0; i < 3; i++) + { + var callbackResponse = await callback.ExecuteAsync(); + Assert.True(callbackResponse.Success); + Assert.Equal(42, callbackResponse.Result); + } + } } } diff --git a/CefSharp/Internals/FramePendingTaskRepository.cs b/CefSharp/Internals/FramePendingTaskRepository.cs new file mode 100644 index 000000000..25cf6cf0c --- /dev/null +++ b/CefSharp/Internals/FramePendingTaskRepository.cs @@ -0,0 +1,33 @@ +// Copyright © 2015 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.Collections.Concurrent; +using System.Threading.Tasks; + +namespace CefSharp.Internals +{ + internal sealed class FramePendingTaskRepository : IDisposable + { + public ConcurrentDictionary> PendingTasks { get; } = + new ConcurrentDictionary>(); + public ConcurrentDictionary> CallbackPendingTasks { get; } = + new ConcurrentDictionary>(); + + public void Dispose() + { + foreach (var tcs in PendingTasks.Values) + { + tcs.TrySetCanceled(); + } + PendingTasks.Clear(); + + foreach (var tcs in CallbackPendingTasks.Values) + { + tcs.TrySetCanceled(); + } + CallbackPendingTasks.Clear(); + } + } +} diff --git a/CefSharp/Internals/PendingTaskRepository.cs b/CefSharp/Internals/PendingTaskRepository.cs index 3a867c9a7..4b9716592 100644 --- a/CefSharp/Internals/PendingTaskRepository.cs +++ b/CefSharp/Internals/PendingTaskRepository.cs @@ -14,73 +14,106 @@ namespace CefSharp.Internals /// Class to store TaskCompletionSources indexed by a unique id. There are two distinct ConcurrentDictionary /// instances as we have some Tasks that are created from the browser process (EvaluateScriptAsync) calls, and /// some that are created for instances for which the Id's are created - /// in the render process. + /// in the render process. /// /// The type of the result produced by the tasks held. public sealed class PendingTaskRepository { - private readonly ConcurrentDictionary> pendingTasks = - new ConcurrentDictionary>(); - private readonly ConcurrentDictionary> callbackPendingTasks = - new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> framePendingTasks = + new ConcurrentDictionary>(); //should only be accessed by Interlocked.Increment private long lastId; /// /// Creates a new pending task with a timeout. /// + /// The frame id in which the task is created. /// The maximum running time of the task. /// The unique id of the newly created pending task and the newly created . - public KeyValuePair> CreatePendingTask(TimeSpan? timeout = null) + public KeyValuePair> CreatePendingTask(string frameId, TimeSpan? timeout = null) { var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var id = Interlocked.Increment(ref lastId); - pendingTasks.TryAdd(id, taskCompletionSource); + var result = new KeyValuePair>(id, taskCompletionSource); +#if NETCOREAPP + framePendingTasks.AddOrUpdate( + frameId, + (key, state) => { var value = new FramePendingTaskRepository(); value.PendingTasks.TryAdd(state.Key, state.Value); return value; }, + (key, value, state) => { value.PendingTasks.TryAdd(state.Key, state.Value); return value; }, + result + ); +#else + framePendingTasks.AddOrUpdate( + frameId, + (key) => { var value = new FramePendingTaskRepository(); value.PendingTasks.TryAdd(id, taskCompletionSource); return value; }, + (key, value) => { value.PendingTasks.TryAdd(id, taskCompletionSource); return value; } + ); +#endif if (timeout.HasValue) { - taskCompletionSource = taskCompletionSource.WithTimeout(timeout.Value, () => RemovePendingTask(id)); + taskCompletionSource.WithTimeout(timeout.Value, () => RemovePendingTask(frameId, id)); } - return new KeyValuePair>(id, taskCompletionSource); + return result; } /// /// Creates a new pending task with a timeout. /// + /// The frame id in which the task is created. /// Id passed in from the render process /// The maximum running time of the task. /// The unique id of the newly created pending task and the newly created . - public KeyValuePair> CreateJavascriptCallbackPendingTask(long id, TimeSpan? timeout = null) + public KeyValuePair> CreateJavascriptCallbackPendingTask(string frameId, long id, TimeSpan? timeout = null) { var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - callbackPendingTasks.TryAdd(id, taskCompletionSource); + var result = new KeyValuePair>(id, taskCompletionSource); +#if NETCOREAPP + framePendingTasks.AddOrUpdate( + frameId, + (key, state) => { var value = new FramePendingTaskRepository(); value.CallbackPendingTasks.TryAdd(state.Key, state.Value); return value; }, + (key, value, state) => { value.CallbackPendingTasks.TryAdd(state.Key, state.Value); return value; }, + result + ); +#else + framePendingTasks.AddOrUpdate( + frameId, + (key) => { var value = new FramePendingTaskRepository(); value.CallbackPendingTasks.TryAdd(id, taskCompletionSource); return value; }, + (key, value) => { value.CallbackPendingTasks.TryAdd(id, taskCompletionSource); return value; } + ); +#endif if (timeout.HasValue) { - taskCompletionSource = taskCompletionSource.WithTimeout(timeout.Value, () => RemoveJavascriptCallbackPendingTask(id)); + taskCompletionSource.WithTimeout(timeout.Value, () => RemoveJavascriptCallbackPendingTask(frameId, id)); } - return new KeyValuePair>(id, taskCompletionSource); + return result; } /// /// If a is found matching /// then it is removed from the ConcurrentDictionary and returned. /// + /// The frame id. /// Unique id of the pending task. /// /// The associated with the given id /// or null if no matching TaskComplectionSource found. /// - public TaskCompletionSource RemovePendingTask(long id) + public TaskCompletionSource RemovePendingTask(string frameId, long id) { - TaskCompletionSource result; - if (pendingTasks.TryRemove(id, out result)) + FramePendingTaskRepository repository; + if (framePendingTasks.TryGetValue(frameId, out repository)) { - return result; + TaskCompletionSource result; + if (repository.PendingTasks.TryRemove(id, out result)) + { + return result; + } } return null; @@ -90,21 +123,50 @@ public TaskCompletionSource RemovePendingTask(long id) /// If a is found matching /// then it is removed from the ConcurrentDictionary and returned. /// + /// The frame id. /// Unique id of the pending task. /// /// The associated with the given id /// or null if no matching TaskComplectionSource found. /// - public TaskCompletionSource RemoveJavascriptCallbackPendingTask(long id) + public TaskCompletionSource RemoveJavascriptCallbackPendingTask(string frameId, long id) { - TaskCompletionSource result; - - if (callbackPendingTasks.TryRemove(id, out result)) + FramePendingTaskRepository repository; + if (framePendingTasks.TryGetValue(frameId, out repository)) { - return result; + TaskCompletionSource result; + if (repository.CallbackPendingTasks.TryRemove(id, out result)) + { + return result; + } } return null; } + + /// + /// Cancels all pending tasks of a frame. + /// + /// The frame id. + public void CancelPendingTasks(string frameId) + { + FramePendingTaskRepository repository; + if (framePendingTasks.TryRemove(frameId, out repository)) + { + repository.Dispose(); + } + } + + /// + /// Cancels all pending tasks. + /// + public void CancelPendingTasks() + { + foreach (var repository in framePendingTasks.Values) + { + repository.Dispose(); + } + framePendingTasks.Clear(); + } } } diff --git a/CefSharp/Internals/TaskExtensions.cs b/CefSharp/Internals/TaskExtensions.cs index ef341fc44..c9c4828f0 100644 --- a/CefSharp/Internals/TaskExtensions.cs +++ b/CefSharp/Internals/TaskExtensions.cs @@ -20,9 +20,8 @@ public static TaskCompletionSource WithTimeout(this TaskComple var timer = new Timer(state => { ((Timer)state).Dispose(); - if (taskCompletionSource.Task.Status != TaskStatus.RanToCompletion) + if (taskCompletionSource.TrySetCanceled()) { - taskCompletionSource.TrySetCanceled(); if (cancelled != null) { cancelled();