Skip to content

Commit f780c7d

Browse files
committed
fix: time out on the client when the driver stops responding
Fixes: #1990 Fixes: #1810
1 parent 4217d84 commit f780c7d

5 files changed

Lines changed: 125 additions & 6 deletions

File tree

‎playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,15 +357,20 @@ public CDPSession newCDPSession(Page page) {
357357
JsonObject params = new JsonObject();
358358
params.add("page", ((PageImpl) page).toProtocolRef());
359359
JsonObject result = sendMessage("newCDPSession", params, NO_TIMEOUT).getAsJsonObject();
360-
return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
360+
CDPSessionImpl session = connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
361+
session.timeoutSettings = ((PageImpl) page).timeoutSettings;
362+
return session;
361363
}
362364

363365
@Override
364366
public CDPSession newCDPSession(Frame frame) {
365367
JsonObject params = new JsonObject();
366368
params.add("frame", ((FrameImpl) frame).toProtocolRef());
367369
JsonObject result = sendMessage("newCDPSession", params, NO_TIMEOUT).getAsJsonObject();
368-
return connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
370+
CDPSessionImpl session = connection.getExistingObject(result.getAsJsonObject("session").get("guid").getAsString());
371+
PageImpl page = ((FrameImpl) frame).page();
372+
session.timeoutSettings = page != null ? page.timeoutSettings : timeoutSettings;
373+
return session;
369374
}
370375

371376
@Override

‎playwright/src/main/java/com/microsoft/playwright/impl/CDPSessionImpl.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
public class CDPSessionImpl extends ChannelOwner implements CDPSession {
2727
private final ListenerCollection<String> listeners = new ListenerCollection<>(new HashMap<>(), this);
2828
private final ListenerCollection<EventType> typedListeners = new ListenerCollection<>(new HashMap<>(), this);
29+
// Set for page and frame sessions, browser sessions have no timeout.
30+
TimeoutSettings timeoutSettings;
2931

3032
enum EventType {
3133
CLOSE,
@@ -71,7 +73,7 @@ public JsonObject send(String method, JsonObject params) {
7173
args.add("params", params);
7274
}
7375
args.addProperty("method", method);
74-
JsonElement response = connection.sendMessage(guid, "send", args);
76+
JsonElement response = sendMessage("send", args, timeoutSettings == null ? NO_TIMEOUT : timeoutSettings.timeout(null));
7577
if (response == null) return null;
7678
else return response.getAsJsonObject().get("result").getAsJsonObject();
7779
}

‎playwright/src/main/java/com/microsoft/playwright/impl/Connection.java‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.List;
3030
import java.util.Map;
3131
import java.util.concurrent.ThreadLocalRandom;
32+
import java.util.concurrent.TimeUnit;
3233

3334
import static com.microsoft.playwright.impl.Serialization.gson;
3435
import static java.lang.System.currentTimeMillis;
@@ -69,6 +70,9 @@ public class Connection {
6970
private final Map<String, WaitableResult<JsonElement>> callbacks = new HashMap<>();
7071
private String title;
7172
private boolean titleReported = false;
73+
// How long past a call's timeout to wait for the driver's reply. The driver may spend up to 3s
74+
// in tracing instrumentation both before and after the call itself.
75+
private static final long DRIVER_RESPONSE_GRACE_MS = 10_000;
7276
private static final boolean isLogging;
7377
static {
7478
String debug = System.getenv("DEBUG");
@@ -145,7 +149,20 @@ public JsonElement sendMessage(String guid, String method, JsonObject params) {
145149
}
146150

147151
public JsonElement sendMessage(String guid, String method, JsonObject params, Double timeout) {
148-
return root.runUntil(() -> {}, internalSendMessage(guid, method, params, timeout, true, true));
152+
WaitableResult<JsonElement> result = internalSendMessage(guid, method, params, timeout, true, true);
153+
if (timeout == null || timeout == 0) {
154+
return root.runUntil(() -> {}, result);
155+
}
156+
// The timeout is enforced by the driver, but if the driver or the browser hangs no reply ever
157+
// comes. Give up once the driver is well past the timeout and has stopped sending messages.
158+
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout.longValue() + DRIVER_RESPONSE_GRACE_MS);
159+
while (!result.isDone()) {
160+
if (!processOneMessage() && System.nanoTime() - deadline > 0) {
161+
result.completeExceptionally(new TimeoutError("Timeout " + timeout.longValue() + "ms exceeded while waiting for the driver to respond to " +
162+
method + ". The driver or the browser process is not responding."));
163+
}
164+
}
165+
return result.get();
149166
}
150167

151168
public WaitableResult<JsonElement> sendMessageAsync(String guid, String method, JsonObject params) {
@@ -232,14 +249,16 @@ void unregisterObject(String guid) {
232249
objects.remove(guid);
233250
}
234251

235-
void processOneMessage() {
252+
// Returns false if no message arrived within the poll interval.
253+
boolean processOneMessage() {
236254
JsonObject message = transport.poll(Duration.ofMillis(10));
237255
if (message == null) {
238-
return;
256+
return false;
239257
}
240258
Gson gson = gson();
241259
Message messageObj = gson.fromJson(message, Message.class);
242260
dispatch(messageObj);
261+
return true;
243262
}
244263

245264
private static String formatCallLog(JsonArray log) {

‎playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCDPSession.java‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,29 @@ void shouldWork() {
4444
assertEquals("bar", foo);
4545
}
4646

47+
@Test
48+
void shouldRespectDefaultTimeout() {
49+
page.context().setDefaultTimeout(500);
50+
CDPSession cdpSession = page.context().newCDPSession(page);
51+
JsonObject params = new JsonObject();
52+
params.addProperty("expression", "new Promise(() => {})");
53+
params.addProperty("awaitPromise", true);
54+
TimeoutError e = assertThrows(TimeoutError.class, () -> cdpSession.send("Runtime.evaluate", params));
55+
assertTrue(e.getMessage().contains("Timeout 500ms exceeded"), e.getMessage());
56+
}
57+
58+
@Test
59+
void shouldNotTimeOutWhenDefaultTimeoutIsZero() {
60+
page.setDefaultTimeout(0);
61+
CDPSession cdpSession = page.context().newCDPSession(page);
62+
JsonObject params = new JsonObject();
63+
params.addProperty("expression", "new Promise(f => setTimeout(() => f(42), 100))");
64+
params.addProperty("awaitPromise", true);
65+
params.addProperty("returnByValue", true);
66+
JsonObject result = cdpSession.send("Runtime.evaluate", params);
67+
assertEquals(42, result.getAsJsonObject("result").get("value").getAsInt());
68+
}
69+
4770
@Test
4871
void shouldSendEvents() {
4972
CDPSession cdpSession = page.context().newCDPSession(page);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.microsoft.playwright;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.condition.DisabledOnOs;
21+
import org.junit.jupiter.api.condition.OS;
22+
23+
import java.lang.reflect.Field;
24+
25+
import static org.junit.jupiter.api.Assertions.*;
26+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
27+
28+
public class TestUnresponsiveDriver {
29+
@Test
30+
@DisabledOnOs(OS.WINDOWS)
31+
void shouldThrowTimeoutErrorWhenDriverStopsResponding() throws Exception {
32+
try (Playwright playwright = Playwright.create()) {
33+
Browser browser = Utils.getBrowserTypeFromEnv(playwright).launch(TestBase.createLaunchOptions());
34+
Page page = browser.newPage();
35+
long pid = driverPid(playwright);
36+
signal("STOP", pid);
37+
try {
38+
long start = System.nanoTime();
39+
TimeoutError e = assertThrows(TimeoutError.class,
40+
() -> page.waitForFunction("() => false", null, new Page.WaitForFunctionOptions().setTimeout(100)));
41+
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
42+
assertTrue(e.getMessage().contains("Timeout 100ms exceeded while waiting for the driver to respond to waitForFunction"), e.getMessage());
43+
assertTrue(elapsedMs < 20_000, "elapsed: " + elapsedMs);
44+
} finally {
45+
signal("CONT", pid);
46+
}
47+
// The driver's late reply to the abandoned call must not break the connection.
48+
assertEquals(2, page.evaluate("1 + 1"));
49+
}
50+
}
51+
52+
private static long driverPid(Playwright playwright) throws Exception {
53+
Field field = playwright.getClass().getDeclaredField("driverProcess");
54+
field.setAccessible(true);
55+
Process process = (Process) field.get(playwright);
56+
long pid = -1;
57+
try {
58+
// Process.pid() is only available since Java 9.
59+
pid = (long) Process.class.getMethod("pid").invoke(process);
60+
} catch (NoSuchMethodException e) {
61+
assumeTrue(false, "Process.pid() is not available");
62+
}
63+
return pid;
64+
}
65+
66+
private static void signal(String signal, long pid) throws Exception {
67+
Process kill = new ProcessBuilder("kill", "-" + signal, Long.toString(pid)).inheritIO().start();
68+
assertEquals(0, kill.waitFor());
69+
}
70+
}

0 commit comments

Comments
 (0)