diff --git a/CHANGELOG.md b/CHANGELOG.md index a0691b9..4b0f9de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 1.1.1 +- Fixed background thread leaks when worker construction is rejected - Fixed canonical DOM node identity and concrete prototypes through `DomSameObject` getters (#140) # 1.1.0 diff --git a/src/AngleSharp.Js.Tests/WorkerTests.cs b/src/AngleSharp.Js.Tests/WorkerTests.cs index c0da60e..63a9322 100644 --- a/src/AngleSharp.Js.Tests/WorkerTests.cs +++ b/src/AngleSharp.Js.Tests/WorkerTests.cs @@ -1,9 +1,11 @@ namespace AngleSharp.Js.Tests { + using AngleSharp.Dom; using AngleSharp.Io; using AngleSharp.Js.Tests.Mocks; using NUnit.Framework; using System; + using System.Diagnostics; using System.Threading.Tasks; [TestFixture] @@ -90,6 +92,34 @@ public async Task WorkerShouldRequireResourceLoading() Assert.AreEqual("true", result); } + [Test] + [NonParallelizable] + public async Task RejectedWorkersShouldNotLeaveRunningThreads() + { + using (var context = BrowsingContext.New(Configuration.Default.WithJs())) + { + var document = await context.OpenAsync(request => request.Content("")).ConfigureAwait(false); + Assert.Throws(() => new AngleSharp.Js.Dom.Worker(document.DefaultView, "/worker.js")); + var before = CountProcessThreads(); + + for (var i = 0; i < 32; i++) + { + Assert.Throws(() => new AngleSharp.Js.Dom.Worker(document.DefaultView, "/worker.js")); + } + + // Allow unrelated runtime threads without hiding one surviving loop per failure. + Assert.Less(CountProcessThreads() - before, 16); + } + } + + private static Int32 CountProcessThreads() + { + using (var process = Process.GetCurrentProcess()) + { + return process.Threads.Count; + } + } + [Test] public async Task WorkerDirectPostToOwnerShouldRaiseMessage() { @@ -133,4 +163,4 @@ private static async Task WaitForResultAsync(AngleSharp.Dom.IDocument document, } } } -} \ No newline at end of file +} diff --git a/src/AngleSharp.Js/Dom/Worker.cs b/src/AngleSharp.Js/Dom/Worker.cs index 1185dca..8a23c79 100644 --- a/src/AngleSharp.Js/Dom/Worker.cs +++ b/src/AngleSharp.Js/Dom/Worker.cs @@ -60,28 +60,45 @@ public Worker(IWindow window, String source) var parentContext = _window.Document.Context; _scripting = parentContext.GetService() ?? throw new DomException(DomError.NotSupported); - _workerLoop = new JsEventLoop(); - var workerConfig = Configuration.Default - .With(_scripting) - .WithOnly(_workerLoop); - _workerContext = BrowsingContext.New(workerConfig); - + EnsureLoaderAvailable(); + var workerUrl = ResolveUrl(source); _pendingMessages = new Queue(); - EnsureLoaderAvailable(); + // Check the parent's capabilities before starting a thread that a failed constructor + // would leave unreachable. Release it if context creation or scheduling fails too. + _workerLoop = new JsEventLoop(); + try + { + var workerConfig = Configuration.Default + .With(_scripting) + .WithOnly(_workerLoop); + _workerContext = BrowsingContext.New(workerConfig); - var workerUrl = ResolveUrl(source); - Enqueue(_workerLoop, TaskPriority.Critical, () => + Enqueue(_workerLoop, TaskPriority.Critical, () => + { + try + { + InitializeWorker(workerUrl); + } + catch (Exception ex) + { + _startupError = ex; + } + }); + } + catch { try { - InitializeWorker(workerUrl); + _workerContext?.Dispose(); } - catch (Exception ex) + finally { - _startupError = ex; + ((IDisposable)_workerLoop).Dispose(); } - }); + + throw; + } } #endregion @@ -289,4 +306,4 @@ private static void Enqueue(IEventLoop loop, TaskPriority priority, Action actio #endregion } -} \ No newline at end of file +}