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: + *
+ * 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());