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 9b71b2cbc..517650dea 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 @@ -32,6 +32,7 @@ public abstract class Driver { protected final Map env = new LinkedHashMap<>(System.getenv()); public static final String PLAYWRIGHT_NODEJS_PATH = "PLAYWRIGHT_NODEJS_PATH"; + public static final String PLAYWRIGHT_DRIVER_DIR = "PLAYWRIGHT_DRIVER_DIR"; private static Driver instance; @@ -108,9 +109,12 @@ public static Driver createAndInstall(Map env, Boolean installBr } private static Driver newInstance() throws Exception { - String pathFromProperty = System.getProperty("playwright.cli.dir"); - if (pathFromProperty != null) { - return new PreinstalledDriver(Paths.get(pathFromProperty)); + String driverDir = System.getProperty("playwright.cli.dir"); + if (driverDir == null) { + driverDir = System.getenv(PLAYWRIGHT_DRIVER_DIR); + } + if (driverDir != null) { + return new PreinstalledDriver(Paths.get(driverDir)); } String driverImpl = diff --git a/driver/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java b/driver/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java index 060bd9c2d..4e35d21c5 100644 --- a/driver/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java +++ b/driver/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java @@ -30,17 +30,11 @@ public class DriverJar extends Driver { private static final String PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD"; private static final String SELENIUM_REMOTE_URL = "SELENIUM_REMOTE_URL"; private final Path driverTempDir; + private final boolean deleteOnExit; private Path preinstalledNodePath; public DriverJar() throws IOException { - // Allow specifying custom path for the driver installation - // See https://github.com/microsoft/playwright-java/issues/728 - String alternativeTmpdir = System.getProperty("playwright.driver.tmpdir"); - String prefix = "playwright-java-"; - driverTempDir = alternativeTmpdir == null - ? Files.createTempDirectory(prefix) - : Files.createTempDirectory(Paths.get(alternativeTmpdir), prefix); - driverTempDir.toFile().deleteOnExit(); + this(createTempDriverDir(), true); String nodePath = System.getProperty("playwright.nodejs.path"); if (nodePath != null) { preinstalledNodePath = Paths.get(nodePath); @@ -51,6 +45,32 @@ public DriverJar() throws IOException { logMessage("created DriverJar: " + driverTempDir); } + private DriverJar(Path driverDir, boolean deleteOnExit) { + this.driverTempDir = driverDir; + this.deleteOnExit = deleteOnExit; + if (deleteOnExit) { + driverTempDir.toFile().deleteOnExit(); + } + } + + private static Path createTempDriverDir() throws IOException { + // Allow specifying custom path for the driver installation + // See https://github.com/microsoft/playwright-java/issues/728 + String alternativeTmpdir = System.getProperty("playwright.driver.tmpdir"); + String prefix = "playwright-java-"; + return alternativeTmpdir == null + ? Files.createTempDirectory(prefix) + : Files.createTempDirectory(Paths.get(alternativeTmpdir), prefix); + } + + // Extracts the driver (playwright-core package and the Node.js binary for the current platform) + // into the given directory, persistently. Point playwright.cli.dir / PLAYWRIGHT_DRIVER_DIR at it + // to run without extracting to a temp directory on every launch. See issue #1268. + public static void installDriverTo(Path driverDir) throws IOException, URISyntaxException { + Files.createDirectories(driverDir); + new DriverJar(driverDir, false).extractDriverToTempDir(); + } + @Override protected void initialize(Boolean installBrowsers) throws Exception { if (preinstalledNodePath == null && env.containsKey(PLAYWRIGHT_NODEJS_PATH)) { @@ -156,7 +176,9 @@ private void extractResourceToDir(String resourcePath, Path destDir) throws URIS toPath.toFile().setExecutable(true, true); } } - toPath.toFile().deleteOnExit(); + if (deleteOnExit) { + toPath.toFile().deleteOnExit(); + } } catch (IOException e) { throw new RuntimeException("Failed to extract driver from " + uri + ", full uri: " + originalUri, e); } @@ -179,7 +201,9 @@ private URI maybeExtractNestedJar(final URI uri) throws URISyntaxException { Path fromPath = Paths.get(jarUri); Path toPath = driverTempDir.resolve(fromPath.getFileName().toString()); Files.copy(fromPath, toPath); - toPath.toFile().deleteOnExit(); + if (deleteOnExit) { + toPath.toFile().deleteOnExit(); + } return new URI("jar:" + toPath.toUri() + JAR_URL_SEPARATOR + parts[2]); } catch (IOException e) { throw new RuntimeException("Failed to extract driver's nested .jar from " + jarUri + "; full uri: " + uri, e); diff --git a/playwright/src/main/java/com/microsoft/playwright/CLI.java b/playwright/src/main/java/com/microsoft/playwright/CLI.java index 21951bd04..b44febf0b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/CLI.java +++ b/playwright/src/main/java/com/microsoft/playwright/CLI.java @@ -17,9 +17,12 @@ package com.microsoft.playwright; import com.microsoft.playwright.impl.driver.Driver; +import com.microsoft.playwright.impl.driver.jar.DriverJar; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collections; import static java.util.Arrays.asList; @@ -28,7 +31,13 @@ * Use this class to launch playwright cli. */ public class CLI { - public static void main(String[] args) throws IOException, InterruptedException { + public static void main(String[] args) throws IOException, InterruptedException, URISyntaxException { + // Extract the driver into a fixed directory instead of running the playwright CLI. This is + // handled in Java because it must not require an already-extracted driver. See issue #1268. + if (args.length > 0 && "install-driver".equals(args[0])) { + installDriver(args); + return; + } Driver driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false); ProcessBuilder pb = driver.createProcessBuilder(); pb.command().addAll(asList(args)); @@ -40,4 +49,17 @@ public static void main(String[] args) throws IOException, InterruptedException Process process = pb.start(); System.exit(process.waitFor()); } + + private static void installDriver(String[] args) throws IOException, URISyntaxException { + String dir = args.length > 1 ? args[1] : System.getenv(Driver.PLAYWRIGHT_DRIVER_DIR); + if (dir == null) { + System.err.println("Usage: install-driver (or set the " + Driver.PLAYWRIGHT_DRIVER_DIR + + " environment variable)"); + System.exit(1); + return; + } + Path driverDir = Paths.get(dir); + DriverJar.installDriverTo(driverDir); + System.out.println("Installed Playwright driver into " + driverDir.toAbsolutePath()); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/impl/driver/jar/TestInstall.java b/playwright/src/test/java/com/microsoft/playwright/impl/driver/jar/TestInstall.java index 953549bf1..6ee296b47 100644 --- a/playwright/src/test/java/com/microsoft/playwright/impl/driver/jar/TestInstall.java +++ b/playwright/src/test/java/com/microsoft/playwright/impl/driver/jar/TestInstall.java @@ -132,6 +132,30 @@ void canSpecifyPreinstalledNodeJsAsEnv(@TempDir Path tmpDir) throws IOException, } + @Test + void canInstallDriverToDirectoryAndReuseIt(@TempDir Path tmpDir) throws Exception { + Path driverDir = tmpDir.resolve("driver"); + DriverJar.installDriverTo(driverDir); + // The directory is self-contained: the playwright-core package and the Node.js binary. + assertTrue(Files.exists(driverDir.resolve("package").resolve("cli.js"))); + assertTrue(Files.exists(driverDir.resolve(isWindows() ? "node.exe" : "node"))); + + // Pointing playwright.cli.dir at it must reuse it as-is, without extracting to a temp directory. + System.setProperty("playwright.cli.dir", driverDir.toString()); + Driver driver = Driver.createAndInstall(Collections.emptyMap(), false); + assertEquals(driverDir, driver.driverDir()); + + ProcessBuilder pb = driver.createProcessBuilder(); + pb.command().add("--version"); + pb.redirectError(ProcessBuilder.Redirect.INHERIT); + Path out = tmpDir.resolve("out.txt"); + pb.redirectOutput(out.toFile()); + Process p = pb.start(); + assertTrue(p.waitFor(1, TimeUnit.MINUTES), "Timed out waiting for version to be printed"); + String stdout = new String(Files.readAllBytes(out), StandardCharsets.UTF_8); + assertTrue(stdout.contains("Version "), stdout); + } + private static String extractNodeJsToTemp() throws URISyntaxException, IOException { DriverJar auxDriver = new DriverJar(); auxDriver.extractDriverToTempDir(); diff --git a/utils/docker/Dockerfile.jammy b/utils/docker/Dockerfile.jammy index 0f290946f..aa64ef493 100644 --- a/utils/docker/Dockerfile.jammy +++ b/utils/docker/Dockerfile.jammy @@ -38,6 +38,10 @@ ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH} ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright +# Extract the Playwright driver into the image once so the library reuses it instead of unpacking +# it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. +ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver + RUN mkdir /ms-playwright && \ mkdir /tmp/pw-java @@ -45,6 +49,8 @@ COPY . /tmp/pw-java RUN cd /tmp/pw-java && \ mvn install -D skipTests --no-transfer-progress && \ + mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ + -D exec.args="install-driver" -f playwright/pom.xml --no-transfer-progress && \ DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ -D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \ mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ @@ -61,4 +67,5 @@ RUN cd /tmp/pw-java && \ else \ rm /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstwebrtc.so; \ fi && \ - chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH + chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \ + chmod -R 777 $PLAYWRIGHT_DRIVER_DIR diff --git a/utils/docker/Dockerfile.noble b/utils/docker/Dockerfile.noble index 6b8b4bac8..eafb59ba4 100644 --- a/utils/docker/Dockerfile.noble +++ b/utils/docker/Dockerfile.noble @@ -38,6 +38,10 @@ ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH} ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright +# Extract the Playwright driver into the image once so the library reuses it instead of unpacking +# it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. +ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver + RUN mkdir /ms-playwright && \ mkdir /tmp/pw-java @@ -45,6 +49,8 @@ COPY . /tmp/pw-java RUN cd /tmp/pw-java && \ mvn install -D skipTests --no-transfer-progress && \ + mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ + -D exec.args="install-driver" -f playwright/pom.xml --no-transfer-progress && \ DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ -D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \ mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ @@ -52,4 +58,5 @@ RUN cd /tmp/pw-java && \ mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ -D exec.args="mark-docker-image '${DOCKER_IMAGE_NAME_TEMPLATE}'" -f playwright/pom.xml --no-transfer-progress && \ rm -rf /tmp/pw-java && \ - chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH + chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \ + chmod -R 777 $PLAYWRIGHT_DRIVER_DIR diff --git a/utils/docker/Dockerfile.resolute b/utils/docker/Dockerfile.resolute index a775c5ca4..64a68de4a 100644 --- a/utils/docker/Dockerfile.resolute +++ b/utils/docker/Dockerfile.resolute @@ -38,6 +38,10 @@ ENV JAVA_HOME=/usr/lib/jvm/java-25-openjdk-${PW_TARGET_ARCH} ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright +# Extract the Playwright driver into the image once so the library reuses it instead of unpacking +# it into /tmp on every launch. See https://github.com/microsoft/playwright-java/issues/1268. +ENV PLAYWRIGHT_DRIVER_DIR=/ms-playwright-driver + RUN mkdir /ms-playwright && \ mkdir /tmp/pw-java @@ -45,6 +49,8 @@ COPY . /tmp/pw-java RUN cd /tmp/pw-java && \ mvn install -D skipTests --no-transfer-progress && \ + mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ + -D exec.args="install-driver" -f playwright/pom.xml --no-transfer-progress && \ DEBIAN_FRONTEND=noninteractive mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ -D exec.args="install-deps" -f playwright/pom.xml --no-transfer-progress && \ mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ @@ -52,4 +58,5 @@ RUN cd /tmp/pw-java && \ mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \ -D exec.args="mark-docker-image '${DOCKER_IMAGE_NAME_TEMPLATE}'" -f playwright/pom.xml --no-transfer-progress && \ rm -rf /tmp/pw-java && \ - chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH + chmod -R 777 $PLAYWRIGHT_BROWSERS_PATH && \ + chmod -R 777 $PLAYWRIGHT_DRIVER_DIR