From 3df749f9a3c227616c8881fad6396d280ca2161e Mon Sep 17 00:00:00 2001 From: campersau Date: Mon, 21 Jul 2025 14:11:00 +0200 Subject: [PATCH 1/8] Core - Cancel pending tasks when browser crashed or v8 context gets released --- .../Internals/CefFrameWrapper.cpp | 4 +- .../Internals/ClientAdapter.cpp | 14 +- .../Internals/ClientAdapter.h | 4 +- .../Internals/JavascriptCallbackProxy.cpp | 33 ++--- .../Javascript/EvaluateScriptAsyncTests.cs | 58 +++++++- CefSharp/Internals/PendingTaskRepository.cs | 129 +++++++++++++++--- CefSharp/Internals/TaskExtensions.cs | 3 +- 7 files changed, 197 insertions(+), 48 deletions(-) diff --git a/CefSharp.Core.Runtime/Internals/CefFrameWrapper.cpp b/CefSharp.Core.Runtime/Internals/CefFrameWrapper.cpp index 423be8be66..e852a83816 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 6a48a46688..91808c0b4f 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 0586cb0043..eef96bc426 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 be8f4d786b..12e59671ce 100644 --- a/CefSharp.Core.Runtime/Internals/JavascriptCallbackProxy.cpp +++ b/CefSharp.Core.Runtime/Internals/JavascriptCallbackProxy.cpp @@ -27,30 +27,31 @@ namespace CefSharp auto browser = GetBrowser(); if (browser == nullptr) { - throw gcnew InvalidOperationException("Browser instance is null. Check CanExecute before calling this method."); + return Task::FromException(gcnew InvalidOperationException("Browser instance is null. Check CanExecute before calling this method.")); } 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 c4dd26c5c4..b5db78bbe8 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,61 @@ public EvaluateScriptAsyncTests(ITestOutputHelper output, CefSharpFixture collec this.collectionFixture = collectionFixture; } + [Fact] + public async Task V8Context() + { + Task evaluateCancelAfterDisposeTask; + using (var browser = new CefSharp.OffScreen.ChromiumWebBrowser(automaticallyCreateBrowser: false)) + { + await browser.CreateBrowserAsync(); + + // no V8 context + await Assert.ThrowsAsync(() => browser.EvaluateScriptAsync("1+1")); + + Task evaluateWithoutV8ContextCancelTask; + Task evaluateWithoutV8ContextTask; + using (var frame = browser.GetMainFrame()) + { + evaluateWithoutV8ContextTask = frame.EvaluateScriptAsync("1+2"); + evaluateWithoutV8ContextCancelTask = frame.EvaluateScriptAsync("new Promise(resolve => setTimeout(resolve, 1000))"); + } + + // V8 context + await browser.LoadUrlAsync(CefExample.HelloWorldUrl); + var evaluateCancelAfterV8ContextChangeTask = browser.EvaluateScriptAsync("new Promise(resolve => setTimeout(resolve, 1000))"); + + Assert.Equal(3, await evaluateWithoutV8ContextTask); + Assert.Equal(4, await browser.EvaluateScriptAsync("1+3")); + + // change V8 context + await browser.LoadUrlAsync(CefExample.HelloWorldUrl); + evaluateCancelAfterDisposeTask = browser.EvaluateScriptAsync("new Promise(resolve => setTimeout(resolve, 1000))"); + + Assert.Equal(5, await browser.EvaluateScriptAsync("1+4")); + + await Assert.ThrowsAsync(() => evaluateCancelAfterV8ContextChangeTask); + await Assert.ThrowsAsync(() => evaluateWithoutV8ContextCancelTask); + } + await Assert.ThrowsAsync(() => evaluateCancelAfterDisposeTask); + } + + [Fact] + public async Task CancelEvaluateOnOOM() + { + await Assert.ThrowsAsync(() => Browser.EvaluateScriptAsync( + @" + let array1 = []; + for (let i = 0; i < 10000000; i++) { + let array2 = []; + for (let j = 0; j < 10000000; j++) { + array2.push('a'.repeat(100000000)); + } + array1.push(array2); + } + " + )); + } + [Theory] [InlineData(double.MaxValue, "Number.MAX_VALUE")] [InlineData(double.MaxValue / 2, "Number.MAX_VALUE / 2")] @@ -264,7 +320,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/Internals/PendingTaskRepository.cs b/CefSharp/Internals/PendingTaskRepository.cs index 3a867c9a7a..de31d005db 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,73 @@ 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(); + } + + private 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.SetCanceled(); + } + PendingTasks.Clear(); + + foreach (var tcs in CallbackPendingTasks.Values) + { + tcs.SetCanceled(); + } + CallbackPendingTasks.Clear(); + } + } } } diff --git a/CefSharp/Internals/TaskExtensions.cs b/CefSharp/Internals/TaskExtensions.cs index ef341fc445..c9c4828f0f 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(); From 6a155c13e1ee5bcf9d917949505e390d07c0cf2c Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 23 Jul 2025 08:34:41 +0200 Subject: [PATCH 2/8] Use TrySetCanceled to cancel pending tasks --- CefSharp/Internals/PendingTaskRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CefSharp/Internals/PendingTaskRepository.cs b/CefSharp/Internals/PendingTaskRepository.cs index de31d005db..cc432aaec1 100644 --- a/CefSharp/Internals/PendingTaskRepository.cs +++ b/CefSharp/Internals/PendingTaskRepository.cs @@ -180,13 +180,13 @@ public void Dispose() { foreach (var tcs in PendingTasks.Values) { - tcs.SetCanceled(); + tcs.TrySetCanceled(); } PendingTasks.Clear(); foreach (var tcs in CallbackPendingTasks.Values) { - tcs.SetCanceled(); + tcs.TrySetCanceled(); } CallbackPendingTasks.Clear(); } From 40c278ac894a8d58fa4715ca4b44863c751deadb Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 23 Jul 2025 08:35:26 +0200 Subject: [PATCH 3/8] Adjust test to use chrome://crash for simulating a crash --- .../Javascript/EvaluateScriptAsyncTests.cs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs b/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs index b5db78bbe8..070dc0b5a8 100644 --- a/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs +++ b/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs @@ -66,20 +66,11 @@ public async Task V8Context() } [Fact] - public async Task CancelEvaluateOnOOM() + public async Task CancelEvaluateOnCrash() { - await Assert.ThrowsAsync(() => Browser.EvaluateScriptAsync( - @" - let array1 = []; - for (let i = 0; i < 10000000; i++) { - let array2 = []; - for (let j = 0; j < 10000000; j++) { - array2.push('a'.repeat(100000000)); - } - array1.push(array2); - } - " - )); + var task = Browser.EvaluateScriptAsync("new Promise(resolve => setTimeout(resolve, 1000))"); + await Browser.LoadUrlAsync("chrome://crash"); + await Assert.ThrowsAsync(() => task); } [Theory] From 4a96ef5ebf0c0bc7af5a328aa4279a7988e46976 Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 23 Jul 2025 09:34:59 +0200 Subject: [PATCH 4/8] Add tests for javascript callbacks --- .../Javascript/EvaluateScriptAsyncTests.cs | 24 +++---- .../Javascript/JavascriptCallbackTests.cs | 63 +++++++++++++++++++ 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs b/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs index 070dc0b5a8..608a32074c 100644 --- a/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs +++ b/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs @@ -36,31 +36,29 @@ public async Task V8Context() await browser.CreateBrowserAsync(); // no V8 context - await Assert.ThrowsAsync(() => browser.EvaluateScriptAsync("1+1")); + var withoutV8ContextException = await Assert.ThrowsAsync(() => browser.EvaluateScriptAsync("1+1")); + Assert.StartsWith("Unable to execute javascript at this time", withoutV8ContextException.Message); - Task evaluateWithoutV8ContextCancelTask; - Task evaluateWithoutV8ContextTask; + Task evaluateWithoutV8ContextTask; using (var frame = browser.GetMainFrame()) { - evaluateWithoutV8ContextTask = frame.EvaluateScriptAsync("1+2"); - evaluateWithoutV8ContextCancelTask = frame.EvaluateScriptAsync("new Promise(resolve => setTimeout(resolve, 1000))"); + evaluateWithoutV8ContextTask = frame.EvaluateScriptAsync("1+2"); } // V8 context await browser.LoadUrlAsync(CefExample.HelloWorldUrl); - var evaluateCancelAfterV8ContextChangeTask = browser.EvaluateScriptAsync("new Promise(resolve => setTimeout(resolve, 1000))"); + var evaluateWithoutV8ContextResponse = await evaluateWithoutV8ContextTask; + Assert.True(evaluateWithoutV8ContextResponse.Success); + Assert.Equal(3, evaluateWithoutV8ContextResponse.Result); - Assert.Equal(3, await evaluateWithoutV8ContextTask); - Assert.Equal(4, await browser.EvaluateScriptAsync("1+3")); + var evaluateCancelAfterV8ContextChangeTask = browser.EvaluateScriptAsync("new Promise(resolve => setTimeout(resolve, 1000))"); // change V8 context await browser.LoadUrlAsync(CefExample.HelloWorldUrl); - evaluateCancelAfterDisposeTask = browser.EvaluateScriptAsync("new Promise(resolve => setTimeout(resolve, 1000))"); - - Assert.Equal(5, await browser.EvaluateScriptAsync("1+4")); await Assert.ThrowsAsync(() => evaluateCancelAfterV8ContextChangeTask); - await Assert.ThrowsAsync(() => evaluateWithoutV8ContextCancelTask); + + evaluateCancelAfterDisposeTask = browser.EvaluateScriptAsync("new Promise(resolve => setTimeout(resolve, 1000))"); } await Assert.ThrowsAsync(() => evaluateCancelAfterDisposeTask); } @@ -68,6 +66,8 @@ public async Task V8Context() [Fact] public async Task CancelEvaluateOnCrash() { + AssertInitialLoadComplete(); + var task = Browser.EvaluateScriptAsync("new Promise(resolve => setTimeout(resolve, 1000))"); await Browser.LoadUrlAsync("chrome://crash"); await Assert.ThrowsAsync(() => task); diff --git a/CefSharp.Test/Javascript/JavascriptCallbackTests.cs b/CefSharp.Test/Javascript/JavascriptCallbackTests.cs index a945f7d9b4..c7d468973f 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,68 @@ public JavascriptCallbackTests(ITestOutputHelper output, CefSharpFixture collect this.collectionFixture = collectionFixture; } + [Fact] + public async Task V8Context() + { + 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 evaluateCancelAfterV8ContextCallback = (IJavascriptCallback)callbackExecuteCancelAfterV8ContextResponse.Result; + var evaluateCancelAfterV8ContextTask = evaluateCancelAfterV8ContextCallback.ExecuteAsync(); + + // change V8 context + await browser.LoadUrlAsync(CefExample.HelloWorldUrl); + + await Assert.ThrowsAsync(() => evaluateCancelAfterV8ContextTask); + + var callbackExecuteCancelAfterDisposeResponse = await browser.EvaluateScriptAsync("(function() { return new Promise(resolve => setTimeout(resolve, 1000)); })"); + Assert.True(callbackExecuteCancelAfterDisposeResponse.Success); + var callbackExecuteCancelAfterDisposeCallback = (IJavascriptCallback)callbackExecuteCancelAfterDisposeResponse.Result; + callbackExecuteCancelAfterDisposeTask = callbackExecuteCancelAfterDisposeCallback.ExecuteAsync(); + } + await Assert.ThrowsAsync(() => callbackExecuteCancelAfterDisposeTask); + } + + [Fact] + public async Task CancelCallbackOnCrash() + { + 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")] From 73131d3cc490d145d3999785383e8d672789b4f9 Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 23 Jul 2025 12:41:28 +0200 Subject: [PATCH 5/8] Rename tests and add tests for calling Execute multiple times --- .../Internals/JavascriptCallbackProxy.cpp | 2 +- .../Javascript/EvaluateScriptAsyncTests.cs | 2 +- .../Javascript/JavascriptCallbackTests.cs | 34 ++++++++++++++++--- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/CefSharp.Core.Runtime/Internals/JavascriptCallbackProxy.cpp b/CefSharp.Core.Runtime/Internals/JavascriptCallbackProxy.cpp index 12e59671ce..1e96fb9181 100644 --- a/CefSharp.Core.Runtime/Internals/JavascriptCallbackProxy.cpp +++ b/CefSharp.Core.Runtime/Internals/JavascriptCallbackProxy.cpp @@ -27,7 +27,7 @@ namespace CefSharp auto browser = GetBrowser(); if (browser == nullptr) { - return Task::FromException(gcnew InvalidOperationException("Browser instance is null. Check CanExecute before calling this method.")); + throw gcnew InvalidOperationException("Browser instance is null. Check CanExecute before calling this method."); } auto browserWrapper = static_cast(browser); diff --git a/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs b/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs index 608a32074c..810ecd3232 100644 --- a/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs +++ b/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs @@ -64,7 +64,7 @@ public async Task V8Context() } [Fact] - public async Task CancelEvaluateOnCrash() + public async Task ShouldCancelOnCrash() { AssertInitialLoadComplete(); diff --git a/CefSharp.Test/Javascript/JavascriptCallbackTests.cs b/CefSharp.Test/Javascript/JavascriptCallbackTests.cs index c7d468973f..54327afc34 100644 --- a/CefSharp.Test/Javascript/JavascriptCallbackTests.cs +++ b/CefSharp.Test/Javascript/JavascriptCallbackTests.cs @@ -27,6 +27,7 @@ public JavascriptCallbackTests(ITestOutputHelper output, CefSharpFixture collect [Fact] public async Task V8Context() { + IJavascriptCallback callbackExecuteCancelAfterDisposeCallback; Task callbackExecuteCancelAfterDisposeTask; using (var browser = new CefSharp.OffScreen.ChromiumWebBrowser(automaticallyCreateBrowser: false)) { @@ -54,24 +55,29 @@ public async Task V8Context() var callbackExecuteCancelAfterV8ContextResponse = await browser.EvaluateScriptAsync("(function() { return new Promise(resolve => setTimeout(resolve, 1000)); })"); Assert.True(callbackExecuteCancelAfterV8ContextResponse.Success); - var evaluateCancelAfterV8ContextCallback = (IJavascriptCallback)callbackExecuteCancelAfterV8ContextResponse.Result; - var evaluateCancelAfterV8ContextTask = evaluateCancelAfterV8ContextCallback.ExecuteAsync(); + var callbackExecuteCancelAfterV8ContextCallback = (IJavascriptCallback)callbackExecuteCancelAfterV8ContextResponse.Result; + var callbackExecuteCancelAfterV8ContextTask = callbackExecuteCancelAfterV8ContextCallback.ExecuteAsync(); // change V8 context await browser.LoadUrlAsync(CefExample.HelloWorldUrl); - await Assert.ThrowsAsync(() => evaluateCancelAfterV8ContextTask); + 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); - var callbackExecuteCancelAfterDisposeCallback = (IJavascriptCallback)callbackExecuteCancelAfterDisposeResponse.Result; + callbackExecuteCancelAfterDisposeCallback = (IJavascriptCallback)callbackExecuteCancelAfterDisposeResponse.Result; callbackExecuteCancelAfterDisposeTask = callbackExecuteCancelAfterDisposeCallback.ExecuteAsync(); } + Assert.False(callbackExecuteCancelAfterDisposeCallback.CanExecute); await Assert.ThrowsAsync(() => callbackExecuteCancelAfterDisposeTask); + await Assert.ThrowsAsync(() => callbackExecuteCancelAfterDisposeCallback.ExecuteAsync()); } [Fact] - public async Task CancelCallbackOnCrash() + public async Task ShouldCancelOnCrash() { AssertInitialLoadComplete(); @@ -296,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); + } + } } } From a51cc30b3a7522e75b80e29129634c38ebba4b23 Mon Sep 17 00:00:00 2001 From: campersau Date: Sun, 3 Aug 2025 18:57:41 +0200 Subject: [PATCH 6/8] Move FramePendingTaskRepository to its own file --- .../Internals/FramePendingTaskRepository.cs | 33 +++++++++++++++++ CefSharp/Internals/PendingTaskRepository.cs | 37 ++++--------------- 2 files changed, 40 insertions(+), 30 deletions(-) create mode 100644 CefSharp/Internals/FramePendingTaskRepository.cs diff --git a/CefSharp/Internals/FramePendingTaskRepository.cs b/CefSharp/Internals/FramePendingTaskRepository.cs new file mode 100644 index 0000000000..25cf6cf0ca --- /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 cc432aaec1..ffd2c085db 100644 --- a/CefSharp/Internals/PendingTaskRepository.cs +++ b/CefSharp/Internals/PendingTaskRepository.cs @@ -19,8 +19,8 @@ namespace CefSharp.Internals /// The type of the result produced by the tasks held. public sealed class PendingTaskRepository { - private readonly ConcurrentDictionary framePendingTasks = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary> framePendingTasks = + new ConcurrentDictionary>(); //should only be accessed by Interlocked.Increment private long lastId; @@ -39,7 +39,7 @@ public KeyValuePair> CreatePendingTask(strin #if NETCOREAPP framePendingTasks.AddOrUpdate( frameId, - (key, state) => { var value = new FramePendingTaskRepository(); value.PendingTasks.TryAdd(state.Key, state.Value); return value; }, + (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 ); @@ -74,7 +74,7 @@ public KeyValuePair> CreateJavascriptCallbac #if NETCOREAPP framePendingTasks.AddOrUpdate( frameId, - (key, state) => { var value = new FramePendingTaskRepository(); value.CallbackPendingTasks.TryAdd(state.Key, state.Value); return value; }, + (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 ); @@ -106,7 +106,7 @@ public KeyValuePair> CreateJavascriptCallbac /// public TaskCompletionSource RemovePendingTask(string frameId, long id) { - FramePendingTaskRepository repository; + FramePendingTaskRepository repository; if (framePendingTasks.TryGetValue(frameId, out repository)) { TaskCompletionSource result; @@ -131,7 +131,7 @@ public TaskCompletionSource RemovePendingTask(string frameId, long id) /// public TaskCompletionSource RemoveJavascriptCallbackPendingTask(string frameId, long id) { - FramePendingTaskRepository repository; + FramePendingTaskRepository repository; if (framePendingTasks.TryGetValue(frameId, out repository)) { TaskCompletionSource result; @@ -150,7 +150,7 @@ public TaskCompletionSource RemoveJavascriptCallbackPendingTask(string /// The frame id. public void CancelPendingTasks(string frameId) { - FramePendingTaskRepository repository; + FramePendingTaskRepository repository; if (framePendingTasks.TryRemove(frameId, out repository)) { repository.Dispose(); @@ -168,28 +168,5 @@ public void CancelPendingTasks() } framePendingTasks.Clear(); } - - private 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(); - } - } } } From c6fa73d1e0988bfb44a11aca68d4b7c07e4a2473 Mon Sep 17 00:00:00 2001 From: campersau Date: Sun, 3 Aug 2025 19:01:28 +0200 Subject: [PATCH 7/8] Rename test to ShouldCancelAfterV8ContextChange --- CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs | 2 +- CefSharp.Test/Javascript/JavascriptCallbackTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs b/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs index 810ecd3232..3cc1bf1814 100644 --- a/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs +++ b/CefSharp.Test/Javascript/EvaluateScriptAsyncTests.cs @@ -28,7 +28,7 @@ public EvaluateScriptAsyncTests(ITestOutputHelper output, CefSharpFixture collec } [Fact] - public async Task V8Context() + public async Task ShouldCancelAfterV8ContextChange() { Task evaluateCancelAfterDisposeTask; using (var browser = new CefSharp.OffScreen.ChromiumWebBrowser(automaticallyCreateBrowser: false)) diff --git a/CefSharp.Test/Javascript/JavascriptCallbackTests.cs b/CefSharp.Test/Javascript/JavascriptCallbackTests.cs index 54327afc34..c548fcdb8b 100644 --- a/CefSharp.Test/Javascript/JavascriptCallbackTests.cs +++ b/CefSharp.Test/Javascript/JavascriptCallbackTests.cs @@ -25,7 +25,7 @@ public JavascriptCallbackTests(ITestOutputHelper output, CefSharpFixture collect } [Fact] - public async Task V8Context() + public async Task ShouldCancelAfterV8ContextChange() { IJavascriptCallback callbackExecuteCancelAfterDisposeCallback; Task callbackExecuteCancelAfterDisposeTask; From 1bf7e15b0b5bfd53873e8f468fca2a3db385f2e5 Mon Sep 17 00:00:00 2001 From: campersau Date: Sun, 3 Aug 2025 20:15:40 +0200 Subject: [PATCH 8/8] Fix compile error on .NET Framework --- CefSharp/Internals/PendingTaskRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CefSharp/Internals/PendingTaskRepository.cs b/CefSharp/Internals/PendingTaskRepository.cs index ffd2c085db..4b97165921 100644 --- a/CefSharp/Internals/PendingTaskRepository.cs +++ b/CefSharp/Internals/PendingTaskRepository.cs @@ -46,7 +46,7 @@ public KeyValuePair> CreatePendingTask(strin #else framePendingTasks.AddOrUpdate( frameId, - (key) => { var value = new FramePendingTaskRepository(); value.PendingTasks.TryAdd(id, taskCompletionSource); return value; }, + (key) => { var value = new FramePendingTaskRepository(); value.PendingTasks.TryAdd(id, taskCompletionSource); return value; }, (key, value) => { value.PendingTasks.TryAdd(id, taskCompletionSource); return value; } ); #endif @@ -81,7 +81,7 @@ public KeyValuePair> CreateJavascriptCallbac #else framePendingTasks.AddOrUpdate( frameId, - (key) => { var value = new FramePendingTaskRepository(); value.CallbackPendingTasks.TryAdd(id, taskCompletionSource); return value; }, + (key) => { var value = new FramePendingTaskRepository(); value.CallbackPendingTasks.TryAdd(id, taskCompletionSource); return value; }, (key, value) => { value.CallbackPendingTasks.TryAdd(id, taskCompletionSource); return value; } ); #endif