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());