From 9e1b5bca6778e010b4210910bc17137359dbeca8 Mon Sep 17 00:00:00 2001 From: Anton Lakotka Date: Sun, 5 Jul 2026 22:48:46 +0200 Subject: [PATCH] feat(driver) Introduce experimental support for third-party Playwright drivers Introduce Driver.ThirdPartyDriver marker-interface and create separate factory method to be called with instance of the driver marked with this interface. This provides strong separation between default drivers and the ones that passed by user. These drivers can be specially handled when needed. Since, by definition, these drivers may not work as expected. Add two interfaces of driver variants: 1. External Process builder. 2. And more generic ByteChannel driver. Add two smoke tests to verify ability of PW to communicate with custom drivers --- .../playwright/impl/driver/Driver.java | 39 +++++++- .../com/microsoft/playwright/Playwright.java | 24 +++++ .../playwright/impl/PlaywrightImpl.java | 94 +++++++++++++++---- .../playwright/TestPlaywrightCreate.java | 91 ++++++++++++++++++ 4 files changed, 224 insertions(+), 24 deletions(-) diff --git a/driver/src/main/java/com/microsoft/playwright/impl/driver/Driver.java b/driver/src/main/java/com/microsoft/playwright/impl/driver/Driver.java index 517650dea..3fe75792e 100644 --- a/driver/src/main/java/com/microsoft/playwright/impl/driver/Driver.java +++ b/driver/src/main/java/com/microsoft/playwright/impl/driver/Driver.java @@ -16,6 +16,7 @@ package com.microsoft.playwright.impl.driver; +import java.nio.channels.ByteChannel; import java.nio.file.Path; import java.nio.file.Paths; import java.util.LinkedHashMap; @@ -24,10 +25,19 @@ import static com.microsoft.playwright.impl.driver.DriverLogging.logWithTimestamp; /** - * This class provides access to playwright-cli. It can be either preinstalled - * in the host system and its path is passed as a system property, or it can be - * loaded from the classpath: the platform-independent driver code ships in the - * driver module and the Node.js binary in the optional driver-bundle module. + * This class provides access to playwright-cli. + * Default implementations come in two variants: + * + * + * User can implement their own driver by implementing {@link Driver.ThirdPartyDriver} + * and one of the expecting driver interfaces: + * */ public abstract class Driver { protected final Map env = new LinkedHashMap<>(System.getenv()); @@ -36,6 +46,27 @@ public abstract class Driver { private static Driver instance; + /** + * Marker interface to identify third-party drivers. + * Playwright doesn't provide any guarantees about the stability of the third-party drivers. + * The most reliable communication is to proxy to the standard driver bundled to the current version of Playwright. + */ + public interface ThirdPartyDriver {} + + public interface ExternalProcessDriver { + /** + * Creates a process builder to launch the Playwright driver. + */ + ProcessBuilder createProcessBuilder(); + } + + public interface ByteChannelDriver { + /** + * Creates a new channel to communicate with the Playwright driver. + */ + ByteChannel createByteChannel(); + } + private static class PreinstalledDriver extends Driver { private final Path driverDir; PreinstalledDriver(Path driverDir) { diff --git a/playwright/src/main/java/com/microsoft/playwright/Playwright.java b/playwright/src/main/java/com/microsoft/playwright/Playwright.java index 2e69e5f5f..95e61d921 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Playwright.java +++ b/playwright/src/main/java/com/microsoft/playwright/Playwright.java @@ -17,6 +17,8 @@ package com.microsoft.playwright; import com.microsoft.playwright.impl.PlaywrightImpl; +import com.microsoft.playwright.impl.driver.Driver; + import java.util.*; /** @@ -96,6 +98,14 @@ public CreateOptions setEnv(Map env) { /** * Launches new Playwright driver process and connects to it. {@link com.microsoft.playwright.Playwright#close * Playwright.close()} should be called when the instance is no longer needed. + * + *

+ * Uses a default driver from the pre-installed location specified in + * system property 'playwright.cli.dir' or env variable 'PLAYWRIGHT_DRIVER_DIR' + * If none is provided, then a singleton instance will be created from the class + * provided in system property 'playwright.driver.impl' defaulting to {@link com.microsoft.playwright.impl.driver.jar.DriverJar DriverJar}. + *

+ * *
{@code
    * Playwright playwright = Playwright.create();
    * Browser browser = playwright.webkit().launch();
@@ -110,6 +120,20 @@ static Playwright create(CreateOptions options) {
     return PlaywrightImpl.create(options);
   }
 
+  /**
+   * Launches the new Playwright driver process using the provided 3rd party driver instance and connects to it.
+   * {@link com.microsoft.playwright.Playwright#close Playwright.close()} should be called when the instance is no longer needed.
+   *
+   * 

+ * Warning! Use of 3rd party drivers may not work as expected and can be a source of failures. + * Make sure you use a compatible driver with the current Playwright version. + *

+ * @since v1.62.0 + */ + static Playwright create(CreateOptions options, Driver.ThirdPartyDriver driver) { + return PlaywrightImpl.createWithThirdPartyDriver(options, driver); + } + static Playwright create() { return create(null); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java index 9cf96db39..5d3838f81 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java @@ -24,40 +24,101 @@ import com.microsoft.playwright.Selectors; import com.microsoft.playwright.impl.driver.Driver; -import java.io.IOException; +import java.io.*; +import java.nio.channels.ByteChannel; +import java.nio.channels.Channels; import java.util.Collections; import java.util.Map; import java.util.concurrent.TimeUnit; public class PlaywrightImpl extends ChannelOwner implements Playwright { - private Process driverProcess; + private Closeable driverCloseable; public static PlaywrightImpl create(CreateOptions options) { return createImpl(options, false); } public static PlaywrightImpl createImpl(CreateOptions options, boolean forceNewDriverInstanceForTests) { - Map env = Collections.emptyMap(); - if (options != null && options.env != null) { - env = options.env; - } + Map env = getEnv(options); + Driver driver = forceNewDriverInstanceForTests ? Driver.createAndInstall(env, true) : Driver.ensureDriverInstalled(env, true); + + ProcessBuilder pb = driver.createProcessBuilder(); + pb.command().add("run-driver"); + + return createFromProcessBuilder(env, pb); + } + + public static PlaywrightImpl createWithThirdPartyDriver(CreateOptions options, Driver.ThirdPartyDriver driver) { + Map env = getEnv(options); + + if (driver instanceof Driver.ByteChannelDriver) { + Driver.ByteChannelDriver byteChannelDriver = (Driver.ByteChannelDriver) driver; + return createFromByteChannel(env, byteChannelDriver); + } if (driver instanceof Driver.ExternalProcessDriver) { + Driver.ExternalProcessDriver externalProcessDriver = (Driver.ExternalProcessDriver) driver; + ProcessBuilder pb = externalProcessDriver.createProcessBuilder(); + return createFromProcessBuilder(env, pb); + } else { + throw new PlaywrightException("Unsupported 3rd party driver type: " + driver.getClass().getName()); + } + } + + private static PlaywrightImpl createFromByteChannel(Map env, Driver.ByteChannelDriver byteChannelDriver) { + ByteChannel channel = byteChannelDriver.createByteChannel(); + InputStream in = Channels.newInputStream(channel); + OutputStream out = Channels.newOutputStream(channel); + Closeable closeable = () -> { + in.close(); + out.close(); + channel.close(); + }; + return createWithStreams(env, in, out, closeable); + } + + private static PlaywrightImpl createWithStreams(Map env, + InputStream in, + OutputStream out, + Closeable driverCloseable) { + Connection connection = new Connection(new PipeTransport(in, out), env); + PlaywrightImpl result = connection.initializePlaywright(); + result.driverCloseable = driverCloseable; + return result; + } + + private static PlaywrightImpl createFromProcessBuilder(Map env, ProcessBuilder pb) { try { - ProcessBuilder pb = driver.createProcessBuilder(); - pb.command().add("run-driver"); pb.redirectError(ProcessBuilder.Redirect.INHERIT); + + //noinspection resource it is wrapped to closeable lambda Process p = pb.start(); - Connection connection = new Connection(new PipeTransport(p.getInputStream(), p.getOutputStream()), env); - PlaywrightImpl result = connection.initializePlaywright(); - result.driverProcess = p; - return result; + return createWithStreams(env, p.getInputStream(), p.getOutputStream(), () -> { + // playwright-cli will exit when its stdin is closed, we wait for that. + try { + boolean didClose = p.waitFor(30, TimeUnit.SECONDS); + if (!didClose) { + System.err.println("WARNING: Timed out while waiting for driver process to exit"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new PlaywrightException("Operation interrupted", e); + } + }); } catch (IOException e) { throw new PlaywrightException("Failed to launch driver", e); } } + private static Map getEnv(CreateOptions options) { + Map env = Collections.emptyMap(); + if (options != null && options.env != null) { + env = options.env; + } + return env; + } + private final BrowserTypeImpl chromium; private final BrowserTypeImpl firefox; private final BrowserTypeImpl webkit; @@ -115,16 +176,9 @@ public Selectors selectors() { public void close() { try { connection.close(); - // playwright-cli will exit when its stdin is closed, we wait for that. - boolean didClose = driverProcess.waitFor(30, TimeUnit.SECONDS); - if (!didClose) { - System.err.println("WARNING: Timed out while waiting for driver process to exit"); - } + if (driverCloseable != null) driverCloseable.close(); } catch (IOException e) { throw new PlaywrightException("Failed to terminate", e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new PlaywrightException("Operation interrupted", e); } } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPlaywrightCreate.java b/playwright/src/test/java/com/microsoft/playwright/TestPlaywrightCreate.java index 0085fa860..bcd758f12 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPlaywrightCreate.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPlaywrightCreate.java @@ -17,13 +17,17 @@ package com.microsoft.playwright; import com.microsoft.playwright.impl.PlaywrightImpl; +import com.microsoft.playwright.impl.driver.Driver; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.Map; import static com.microsoft.playwright.Utils.getBrowserTypeFromEnv; @@ -50,6 +54,93 @@ void shouldSupportEnvSkipBrowserDownload(@TempDir Path browsersDir) throws IOExc } } + private static class CustomProcessBuilderDriver implements Driver.ThirdPartyDriver, Driver.ExternalProcessDriver { + private final Driver defaultDriver = Driver.createAndInstall(Collections.emptyMap(), false); + + @Override + public ProcessBuilder createProcessBuilder() { + ProcessBuilder pb = defaultDriver.createProcessBuilder(); + pb.command().add("run-driver"); + return pb; + } + } + + @Test + void shouldAcceptThirdPartyExternalProcessDriver() { + Driver.ThirdPartyDriver customProxyDriver = new CustomProcessBuilderDriver(); + try (Playwright playwright = Playwright.create(null, customProxyDriver)) { + assertNotNull(playwright.chromium()); + } + } + + private static class CustomByteChannelDriver implements Driver.ThirdPartyDriver, Driver.ByteChannelDriver { + private final Driver defaultDriver = Driver.createAndInstall(Collections.emptyMap(), false); + + @Override + public ByteChannel createByteChannel() { + ProcessBuilder pb = defaultDriver.createProcessBuilder(); + pb.command().add("run-driver"); + pb.redirectError(ProcessBuilder.Redirect.INHERIT); + + try { + Process process = pb.start(); + return new ByteChannel() { + @Override + public int read(ByteBuffer dst) throws IOException { + int available = process.getInputStream().available(); + if (available == 0) { + int b = process.getInputStream().read(); + if (b == -1) { + return -1; + } + dst.put((byte) b); + return 1; + } + int toRead = Math.min(available, dst.remaining()); + byte[] buffer = new byte[toRead]; + int bytesRead = process.getInputStream().read(buffer); + if (bytesRead > 0) { + dst.put(buffer, 0, bytesRead); + } + return bytesRead; + } + + @Override + public int write(ByteBuffer src) throws IOException { + int bytesToWrite = src.remaining(); + byte[] buffer = new byte[bytesToWrite]; + src.get(buffer); + process.getOutputStream().write(buffer); + process.getOutputStream().flush(); + return bytesToWrite; + } + + @Override + public boolean isOpen() { + return process.isAlive(); + } + + @Override + public void close() throws IOException { + process.getOutputStream().close(); + process.getInputStream().close(); + process.destroy(); + } + }; + } catch (IOException e) { + throw new RuntimeException("Failed to start Playwright driver process", e); + } + } + } + + @Test + void shouldAcceptThirdPartyByteChannelDriver() { + Driver.ThirdPartyDriver customProxyDriver = new CustomByteChannelDriver(); + try (Playwright playwright = Playwright.create(null, customProxyDriver)) { + assertNotNull(playwright.chromium()); + } + } + // This test is too slow, so we don't run it. void shouldSupportEnvBrowsersPath(@TempDir Path browsersDir) throws IOException { Map env = mapOf("PLAYWRIGHT_BROWSERS_PATH", browsersDir.toString());