Skip to content

Commit 644c8e0

Browse files
ggeurtskblok
authored andcommitted
Improve target destruction reliability (#949)
* Unit tests and fix for issue #948 * Added WithTimeout overloads that with TimeSpan arguments * TaskHelper.WithTimeout methods now prefer TimeSpan above int
1 parent 44956cc commit 644c8e0

File tree

4 files changed

+85
-37
lines changed

4 files changed

+85
-37
lines changed

lib/PuppeteerSharp.Tests/PageTests/CloseTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,12 @@ public async Task ShouldSetThePageCloseState()
6666
await Page.CloseAsync();
6767
Assert.True(Page.IsClosed);
6868
}
69+
70+
[Fact(Timeout = 10000)]
71+
public async Task ShouldCloseWhenConnectionBreaksPrematurely()
72+
{
73+
Browser.Connection.Dispose();
74+
await Page.CloseAsync();
75+
}
6976
}
7077
}

lib/PuppeteerSharp/Browser.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,13 @@ private async Task CloseCoreAsync()
355355
}
356356
}
357357

358+
// Ensure that remaining targets are always marked closed, so that asynchronous page close
359+
// operations on any associated pages don't get blocked.
360+
foreach (var target in TargetsMap.Values)
361+
{
362+
target.CloseTaskWrapper.TrySetResult(false);
363+
}
364+
358365
Closed?.Invoke(this, new EventArgs());
359366
}
360367

lib/PuppeteerSharp/ChromiumProcess.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public async Task<bool> WaitForExitAsync(TimeSpan? timeout)
193193
await _exitCompletionSource.Task.WithTimeout(() =>
194194
{
195195
taskCompleted = false;
196-
}, timeout.Value.Milliseconds).ConfigureAwait(false);
196+
}, timeout.Value).ConfigureAwait(false);
197197
return taskCompleted;
198198
}
199199

@@ -616,7 +616,7 @@ await waitForExitTask.WithTimeout(async () =>
616616
{
617617
await Killing.EnterFromAsync(p, this).ConfigureAwait(false);
618618
await waitForExitTask.ConfigureAwait(false);
619-
}, timeout.Minutes).ConfigureAwait(false);
619+
}, timeout).ConfigureAwait(false);
620620
}
621621

622622
public override Task KillAsync(ChromiumProcess p) => Killing.EnterFromAsync(p, this);

lib/PuppeteerSharp/Helpers/TaskHelper.cs

Lines changed: 69 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,32 @@ namespace PuppeteerSharp.Helpers
99
/// </summary>
1010
public static class TaskHelper
1111
{
12+
private static readonly Func<TimeSpan, Exception> DefaultExceptionFactory =
13+
timeout => new TimeoutException($"Timeout Exceeded: {timeout.TotalMilliseconds}ms exceeded");
14+
1215
//Recipe from https://blogs.msdn.microsoft.com/pfxteam/2012/10/05/how-do-i-cancel-non-cancelable-async-operations/
1316
/// <summary>
1417
/// Cancels the <paramref name="task"/> after <paramref name="milliseconds"/> milliseconds
1518
/// </summary>
1619
/// <returns>The task result.</returns>
1720
/// <param name="task">Task to wait for.</param>
1821
/// <param name="milliseconds">Milliseconds timeout.</param>
19-
/// <param name="exceptionToThrow">Optional exception to be thrown.</param>
20-
public static Task WithTimeout(
21-
this Task task,
22-
int milliseconds = 1_000,
23-
Exception exceptionToThrow = null)
22+
/// <param name="exceptionFactory">Optional timeout exception factory.</param>
23+
public static Task WithTimeout(this Task task, int milliseconds = 1_000, Func<TimeSpan, Exception> exceptionFactory = null)
24+
=> WithTimeout(task, TimeSpan.FromMilliseconds(milliseconds), exceptionFactory);
25+
26+
//Recipe from https://blogs.msdn.microsoft.com/pfxteam/2012/10/05/how-do-i-cancel-non-cancelable-async-operations/
27+
/// <summary>
28+
/// Cancels the <paramref name="task"/> after a given <paramref name="timeout"/> period
29+
/// </summary>
30+
/// <returns>The task result.</returns>
31+
/// <param name="task">Task to wait for.</param>
32+
/// <param name="timeout">The timeout period.</param>
33+
/// <param name="exceptionFactory">Optional timeout exception factory.</param>
34+
public static Task WithTimeout(this Task task, TimeSpan timeout, Func<TimeSpan, Exception> exceptionFactory = null)
2435
=> task.WithTimeout(
25-
() => throw exceptionToThrow ?? new TimeoutException($"Timeout Exceeded: {milliseconds}ms exceeded"),
26-
milliseconds);
36+
() => throw (exceptionFactory ?? DefaultExceptionFactory)(timeout),
37+
timeout);
2738

2839
//Recipe from https://blogs.msdn.microsoft.com/pfxteam/2012/10/05/how-do-i-cancel-non-cancelable-async-operations/
2940
/// <summary>
@@ -33,16 +44,23 @@ public static Task WithTimeout(
3344
/// <param name="task">Task to wait for.</param>
3445
/// <param name="timeoutAction">Action to be executed on Timeout.</param>
3546
/// <param name="milliseconds">Milliseconds timeout.</param>
36-
public static async Task WithTimeout(
37-
this Task task,
38-
Func<Task> timeoutAction,
39-
int milliseconds = 1_000)
47+
public static Task WithTimeout(this Task task, Func<Task> timeoutAction, int milliseconds = 1_000)
48+
=> WithTimeout(task, timeoutAction, TimeSpan.FromMilliseconds(milliseconds));
49+
50+
//Recipe from https://blogs.msdn.microsoft.com/pfxteam/2012/10/05/how-do-i-cancel-non-cancelable-async-operations/
51+
/// <summary>
52+
/// Cancels the <paramref name="task"/> after a given <paramref name="timeout"/> period
53+
/// </summary>
54+
/// <returns>The task result.</returns>
55+
/// <param name="task">Task to wait for.</param>
56+
/// <param name="timeoutAction">Action to be executed on Timeout.</param>
57+
/// <param name="timeout">The timeout period.</param>
58+
public static async Task WithTimeout(this Task task, Func<Task> timeoutAction, TimeSpan timeout)
4059
{
41-
if (await TimeoutTask(task, milliseconds))
60+
if (await TimeoutTask(task, timeout))
4261
{
4362
await timeoutAction();
4463
}
45-
4664
await task;
4765
}
4866

@@ -54,12 +72,20 @@ public static async Task WithTimeout(
5472
/// <param name="task">Task to wait for.</param>
5573
/// <param name="timeoutAction">Action to be executed on Timeout.</param>
5674
/// <param name="milliseconds">Milliseconds timeout.</param>
57-
public static async Task<T> WithTimeout<T>(
58-
this Task<T> task,
59-
Action timeoutAction,
60-
int milliseconds = 1_000)
75+
public static Task<T> WithTimeout<T>(this Task<T> task, Action timeoutAction, int milliseconds = 1_000)
76+
=> WithTimeout(task, timeoutAction, TimeSpan.FromMilliseconds(milliseconds));
77+
78+
//Recipe from https://blogs.msdn.microsoft.com/pfxteam/2012/10/05/how-do-i-cancel-non-cancelable-async-operations/
79+
/// <summary>
80+
/// Cancels the <paramref name="task"/> after a given <paramref name="timeout"/> period
81+
/// </summary>
82+
/// <returns>The task result.</returns>
83+
/// <param name="task">Task to wait for.</param>
84+
/// <param name="timeoutAction">Action to be executed on Timeout.</param>
85+
/// <param name="timeout">The timeout period.</param>
86+
public static async Task<T> WithTimeout<T>(this Task<T> task, Action timeoutAction, TimeSpan timeout)
6187
{
62-
if (await TimeoutTask(task, milliseconds))
88+
if (await TimeoutTask(task, timeout))
6389
{
6490
timeoutAction();
6591
return default;
@@ -75,38 +101,46 @@ public static async Task<T> WithTimeout<T>(
75101
/// <returns>The task result.</returns>
76102
/// <param name="task">Task to wait for.</param>
77103
/// <param name="milliseconds">Milliseconds timeout.</param>
78-
/// <param name="exceptionToThrow">Optional exception to be thrown.</param>
104+
/// <param name="exceptionFactory">Optional timeout exception factory.</param>
105+
/// <typeparam name="T">Task return type.</typeparam>
106+
public static Task<T> WithTimeout<T>(this Task<T> task, int milliseconds = 1_000, Func<TimeSpan, Exception> exceptionFactory = null)
107+
=> WithTimeout(task, TimeSpan.FromMilliseconds(milliseconds), exceptionFactory);
108+
109+
//Recipe from https://blogs.msdn.microsoft.com/pfxteam/2012/10/05/how-do-i-cancel-non-cancelable-async-operations/
110+
/// <summary>
111+
/// Cancels the <paramref name="task"/> after a given <paramref name="timeout"/> period
112+
/// </summary>
113+
/// <returns>The task result.</returns>
114+
/// <param name="task">Task to wait for.</param>
115+
/// <param name="timeout">The timeout period.</param>
116+
/// <param name="exceptionFactory">Optional timeout exception factory.</param>
79117
/// <typeparam name="T">Task return type.</typeparam>
80-
public static async Task<T> WithTimeout<T>(
81-
this Task<T> task,
82-
int milliseconds = 1_000,
83-
Exception exceptionToThrow = null)
118+
public static async Task<T> WithTimeout<T>(this Task<T> task, TimeSpan timeout, Func<TimeSpan, Exception> exceptionFactory = null)
84119
{
85-
if (await TimeoutTask(task, milliseconds))
120+
if (await TimeoutTask(task, timeout))
86121
{
87-
throw exceptionToThrow ?? new TimeoutException($"Timeout Exceeded: {milliseconds}ms exceeded");
122+
throw (exceptionFactory ?? DefaultExceptionFactory)(timeout);
88123
}
89124

90125
return await task;
91126
}
92127

93-
private static async Task<bool> TimeoutTask(Task task, int milliseconds)
128+
private static async Task<bool> TimeoutTask(Task task, TimeSpan timeout)
94129
{
130+
if (timeout <= TimeSpan.Zero)
131+
{
132+
await task;
133+
return false;
134+
}
135+
95136
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
96137
using (var cancellationToken = new CancellationTokenSource())
97138
{
98-
if (milliseconds > 0)
99-
{
100-
cancellationToken.CancelAfter(milliseconds);
101-
}
139+
cancellationToken.CancelAfter(timeout);
102140
using (cancellationToken.Token.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
103141
{
104-
if (task != await Task.WhenAny(task, tcs.Task))
105-
{
106-
return true;
107-
}
142+
return tcs.Task == await Task.WhenAny(task, tcs.Task);
108143
}
109-
return false;
110144
}
111145
}
112146
}

0 commit comments

Comments
 (0)