Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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:
* <ul>
* <li><b>preinstalled</b> in the host system and its path is passed as a system property</li>
* <li><b>classpath loaded:</b> the platform-independent driver code ships in the driver module and the Node.js binary in the optional driver-bundle module.</li>
* </ul>
*
* User can implement their own driver by implementing {@link Driver.ThirdPartyDriver}
* and one of the expecting driver interfaces:
* <ul>
* <li>{@link Driver.ExternalProcessDriver}</li>
* <li>{@link Driver.ByteChannelDriver}</li>
* </ul>
*/
public abstract class Driver {
protected final Map<String, String> env = new LinkedHashMap<>(System.getenv());
Expand All @@ -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) {
Expand Down
24 changes: 24 additions & 0 deletions playwright/src/main/java/com/microsoft/playwright/Playwright.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package com.microsoft.playwright;

import com.microsoft.playwright.impl.PlaywrightImpl;
import com.microsoft.playwright.impl.driver.Driver;

import java.util.*;

/**
Expand Down Expand Up @@ -96,6 +98,14 @@ public CreateOptions setEnv(Map<String, String> 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.
*
* <p>
* 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}.
* </p>
*
* <pre>{@code
* Playwright playwright = Playwright.create();
* Browser browser = playwright.webkit().launch();
Expand All @@ -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.
*
* <p>
* <b>Warning!</b> 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.
*</p>
* @since v1.62.0
*/
static Playwright create(CreateOptions options, Driver.ThirdPartyDriver driver) {
return PlaywrightImpl.createWithThirdPartyDriver(options, driver);
}

static Playwright create() {
return create(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> env = Collections.emptyMap();
if (options != null && options.env != null) {
env = options.env;
}
Map<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> getEnv(CreateOptions options) {
Map<String, String> 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;
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String, String> env = mapOf("PLAYWRIGHT_BROWSERS_PATH", browsersDir.toString());
Expand Down