diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java index 83a7e17bd..b7c0b0144 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; import static com.microsoft.playwright.impl.Serialization.gson; import static java.lang.System.currentTimeMillis; @@ -69,6 +70,7 @@ public class Connection { private final Map> callbacks = new HashMap<>(); private String title; private boolean titleReported = false; + private static final long DRIVER_RESPONSE_GRACE_MS = 10_000; private static final boolean isLogging; static { String debug = System.getenv("DEBUG"); @@ -145,7 +147,19 @@ public JsonElement sendMessage(String guid, String method, JsonObject params) { } public JsonElement sendMessage(String guid, String method, JsonObject params, Double timeout) { - return root.runUntil(() -> {}, internalSendMessage(guid, method, params, timeout, true, true)); + WaitableResult result = internalSendMessage(guid, method, params, timeout, true, true); + if (timeout == null || timeout == 0) { + return root.runUntil(() -> {}, result); + } + // The driver enforces the timeout, but the driver itself may hang. + long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout.longValue() + DRIVER_RESPONSE_GRACE_MS); + while (!result.isDone()) { + if (!processOneMessage() && System.nanoTime() - deadline > 0) { + result.completeExceptionally(new TimeoutError("Timeout " + timeout.longValue() + "ms exceeded while waiting for the driver to respond to " + + method + ". The driver is not responding.")); + } + } + return result.get(); } public WaitableResult sendMessageAsync(String guid, String method, JsonObject params) { @@ -232,14 +246,15 @@ void unregisterObject(String guid) { objects.remove(guid); } - void processOneMessage() { + boolean processOneMessage() { JsonObject message = transport.poll(Duration.ofMillis(10)); if (message == null) { - return; + return false; } Gson gson = gson(); Message messageObj = gson.fromJson(message, Message.class); dispatch(messageObj); + return true; } private static String formatCallLog(JsonArray log) { diff --git a/playwright/src/test/java/com/microsoft/playwright/TestUnresponsiveDriver.java b/playwright/src/test/java/com/microsoft/playwright/TestUnresponsiveDriver.java new file mode 100644 index 000000000..e211a648c --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestUnresponsiveDriver.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.condition.OS; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestUnresponsiveDriver { + @Test + @DisabledOnOs(OS.WINDOWS) + @EnabledForJreRange(min = JRE.JAVA_9) + void shouldThrowTimeoutErrorWhenDriverStopsResponding() throws Exception { + try (Playwright playwright = Playwright.create()) { + Browser browser = Utils.getBrowserTypeFromEnv(playwright).launch(TestBase.createLaunchOptions()); + Page page = browser.newPage(); + long pid = driverPid(playwright); + signal("STOP", pid); + try { + long start = System.nanoTime(); + TimeoutError e = assertThrows(TimeoutError.class, + () -> page.waitForFunction("() => false", null, new Page.WaitForFunctionOptions().setTimeout(100))); + long elapsedMs = (System.nanoTime() - start) / 1_000_000; + assertTrue(e.getMessage().contains("Timeout 100ms exceeded while waiting for the driver to respond to waitForFunction"), e.getMessage()); + assertTrue(elapsedMs < 20_000, "elapsed: " + elapsedMs); + } finally { + signal("CONT", pid); + } + // The driver's late reply to the abandoned call must not break the connection. + assertEquals(2, page.evaluate("1 + 1")); + } + } + + private static long driverPid(Playwright playwright) throws Exception { + Field field = playwright.getClass().getDeclaredField("driverProcess"); + field.setAccessible(true); + // Called via reflection as the tests are compiled with JDK 8 on CI. + return (long) Process.class.getMethod("pid").invoke(field.get(playwright)); + } + + private static void signal(String signal, long pid) throws Exception { + Process kill = new ProcessBuilder("kill", "-" + signal, Long.toString(pid)).inheritIO().start(); + assertEquals(0, kill.waitFor()); + } +}