Skip to content

Commit 4d278c3

Browse files
authored
feat: allow using preinstalled node.js (microsoft#1030)
1 parent 5c47cfb commit 4d278c3

File tree

7 files changed

+139
-58
lines changed

7 files changed

+139
-58
lines changed

driver-bundle/src/main/java/com/microsoft/playwright/impl/driver/jar/DriverJar.java

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
public class DriverJar extends Driver {
3030
private static final String PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD";
3131
private static final String SELENIUM_REMOTE_URL = "SELENIUM_REMOTE_URL";
32+
static final String PLAYWRIGHT_NODEJS_PATH = "PLAYWRIGHT_NODEJS_PATH";
3233
private final Path driverTempDir;
34+
private Path preinstalledNodePath;
3335

3436
public DriverJar() throws IOException {
3537
// Allow specifying custom path for the driver installation
@@ -40,11 +42,27 @@ public DriverJar() throws IOException {
4042
? Files.createTempDirectory(prefix)
4143
: Files.createTempDirectory(Paths.get(alternativeTmpdir), prefix);
4244
driverTempDir.toFile().deleteOnExit();
45+
String nodePath = System.getProperty("playwright.nodejs.path");
46+
if (nodePath != null) {
47+
preinstalledNodePath = Paths.get(nodePath);
48+
if (!Files.exists(preinstalledNodePath)) {
49+
throw new RuntimeException("Invalid Node.js path specified: " + nodePath);
50+
}
51+
}
4352
logMessage("created DriverJar: " + driverTempDir);
4453
}
4554

4655
@Override
47-
protected void initialize(Map<String, String> env, Boolean installBrowsers) throws Exception {
56+
protected void initialize(Boolean installBrowsers) throws Exception {
57+
if (preinstalledNodePath == null && env.containsKey(PLAYWRIGHT_NODEJS_PATH)) {
58+
preinstalledNodePath = Paths.get(env.get(PLAYWRIGHT_NODEJS_PATH));
59+
if (!Files.exists(preinstalledNodePath)) {
60+
throw new RuntimeException("Invalid Node.js path specified: " + preinstalledNodePath);
61+
}
62+
} else if (preinstalledNodePath != null) {
63+
// Pass the env variable to the driver process.
64+
env.put(PLAYWRIGHT_NODEJS_PATH, preinstalledNodePath.toString());
65+
}
4866
extractDriverToTempDir();
4967
logMessage("extracted driver from jar to " + driverPath());
5068
if (installBrowsers)
@@ -68,9 +86,8 @@ private void installBrowsers(Map<String, String> env) throws IOException, Interr
6886
if (!Files.exists(driver)) {
6987
throw new RuntimeException("Failed to find driver: " + driver);
7088
}
71-
ProcessBuilder pb = new ProcessBuilder(driver.toString(), "install");
72-
pb.environment().putAll(env);
73-
setRequiredEnvironmentVariables(pb);
89+
ProcessBuilder pb = createProcessBuilder();
90+
pb.command().add("install");
7491
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
7592
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
7693
Process p = pb.start();
@@ -89,9 +106,10 @@ private static boolean isExecutable(Path filePath) {
89106
return name.endsWith(".sh") || name.endsWith(".exe") || !name.contains(".");
90107
}
91108

92-
private void extractDriverToTempDir() throws URISyntaxException, IOException {
109+
void extractDriverToTempDir() throws URISyntaxException, IOException {
93110
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
94-
URI originalUri = classloader.getResource("driver/" + platformDir()).toURI();
111+
URI originalUri = classloader.getResource(
112+
"driver/" + platformDir()).toURI();
95113
URI uri = maybeExtractNestedJar(originalUri);
96114

97115
// Create zip filesystem if loading from jar.
@@ -103,6 +121,12 @@ private void extractDriverToTempDir() throws URISyntaxException, IOException {
103121
// See https://github.com/microsoft/playwright-java/issues/306
104122
Path srcRootDefaultFs = Paths.get(srcRoot.toString());
105123
Files.walk(srcRoot).forEach(fromPath -> {
124+
if (preinstalledNodePath != null) {
125+
String fileName = fromPath.getFileName().toString();
126+
if ("node.exe".equals(fileName) || "node".equals(fileName)) {
127+
return;
128+
}
129+
}
106130
Path relative = srcRootDefaultFs.relativize(Paths.get(fromPath.toString()));
107131
Path toPath = driverTempDir.resolve(relative.toString());
108132
try {

driver-bundle/src/test/java/com/microsoft/playwright/TestInstall.java renamed to driver-bundle/src/test/java/com/microsoft/playwright/impl/driver/jar/TestInstall.java

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,28 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.microsoft.playwright;
17+
package com.microsoft.playwright.impl.driver.jar;
1818

1919
import com.microsoft.playwright.impl.driver.Driver;
20-
import com.microsoft.playwright.impl.driver.jar.DriverJar;
20+
import org.junit.jupiter.api.AfterEach;
2121
import org.junit.jupiter.api.BeforeEach;
2222
import org.junit.jupiter.api.Test;
2323
import org.junit.jupiter.api.io.TempDir;
2424

2525
import java.io.IOException;
2626
import java.lang.reflect.Field;
2727
import java.net.ServerSocket;
28+
import java.net.URISyntaxException;
29+
import java.nio.charset.StandardCharsets;
2830
import java.nio.file.Files;
2931
import java.nio.file.Path;
3032
import java.util.Collections;
3133
import java.util.HashMap;
3234
import java.util.Map;
3335
import java.util.concurrent.TimeUnit;
3436

37+
import static com.microsoft.playwright.impl.driver.jar.DriverJar.PLAYWRIGHT_NODEJS_PATH;
38+
import static java.util.Collections.singletonMap;
3539
import static org.junit.jupiter.api.Assertions.*;
3640

3741
public class TestInstall {
@@ -57,6 +61,7 @@ void clearSystemProperties() {
5761
// Clear system property to ensure that the driver is loaded from jar.
5862
System.clearProperty("playwright.cli.dir");
5963
System.clearProperty("playwright.driver.tmpdir");
64+
System.clearProperty("playwright.nodejs.path");
6065
// Clear system property to ensure that the default driver is loaded.
6166
System.clearProperty("playwright.driver.impl");
6267
}
@@ -72,27 +77,20 @@ void shouldThrowWhenBrowserPathIsInvalid(@TempDir Path tmpDir) throws NoSuchFiel
7277
env.put("PLAYWRIGHT_BROWSERS_PATH", tmpDir.toString());
7378
env.put("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD", "false");
7479

75-
// Reset instance field value to null for the test.
76-
Field field = Driver.class.getDeclaredField("instance");
77-
field.setAccessible(true);
78-
Object value = field.get(Driver.class);
79-
field.set(Driver.class, null);
80-
8180
for (int i = 0; i < 2; i++){
8281
RuntimeException exception = assertThrows(RuntimeException.class, () -> Driver.ensureDriverInstalled(env, true));
8382
String message = exception.getMessage();
8483
assertTrue(message.contains("Failed to create driver"), message);
8584
}
86-
87-
field.set(Driver.class, value);
8885
}
8986

9087
@Test
9188
void playwrightCliInstalled() throws Exception {
92-
Path cli = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
93-
assertTrue(Files.exists(cli));
89+
Driver driver = Driver.createAndInstall(Collections.emptyMap(), false);
90+
assertTrue(Files.exists(driver.driverPath()));
9491

95-
ProcessBuilder pb = new ProcessBuilder(cli.toString(), "install");
92+
ProcessBuilder pb = driver.createProcessBuilder();
93+
pb.command().add("install");
9694
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
9795
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
9896
Process p = pb.start();
@@ -109,24 +107,62 @@ void playwrightDriverInAlternativeTmpdir(@TempDir Path tmpdir) throws Exception
109107

110108
@Test
111109
void playwrightDriverDefaultImpl() {
112-
assertDoesNotThrow(() -> Driver.ensureDriverInstalled(Collections.emptyMap(), false));
110+
assertDoesNotThrow(() -> Driver.createAndInstall(Collections.emptyMap(), false));
113111
}
114112

115113
@Test
116114
void playwrightDriverAlternativeImpl() throws NoSuchFieldException, IllegalAccessException {
117-
// Reset instance field value to null for the test.
118-
Field field = Driver.class.getDeclaredField("instance");
119-
field.setAccessible(true);
120-
Object value = field.get(Driver.class);
121-
field.set(Driver.class, null);
122-
123115
System.setProperty("playwright.driver.impl", "com.microsoft.playwright.impl.AlternativeDriver");
124116
RuntimeException thrown =
125117
assertThrows(
126118
RuntimeException.class,
127119
() -> Driver.ensureDriverInstalled(Collections.emptyMap(), false));
128120
assertEquals("Failed to create driver", thrown.getMessage());
121+
}
122+
123+
@Test
124+
void canPassPreinstalledNodeJsAsSystemProperty(@TempDir Path tmpDir) throws IOException, URISyntaxException, InterruptedException {
125+
String nodePath = extractNodeJsToTemp();
126+
System.setProperty("playwright.nodejs.path", nodePath);
127+
Driver driver = Driver.createAndInstall(Collections.emptyMap(), false);
128+
canSpecifyPreinstalledNodeJsShared(driver, tmpDir);
129+
}
130+
131+
@Test
132+
void canSpecifyPreinstalledNodeJsAsEnv(@TempDir Path tmpDir) throws IOException, URISyntaxException, InterruptedException {
133+
String nodePath = extractNodeJsToTemp();
134+
Driver driver = Driver.createAndInstall(singletonMap(PLAYWRIGHT_NODEJS_PATH, nodePath), false);
135+
canSpecifyPreinstalledNodeJsShared(driver, tmpDir);
136+
}
129137

130-
field.set(Driver.class, value);
138+
139+
private static String extractNodeJsToTemp() throws URISyntaxException, IOException {
140+
DriverJar auxDriver = new DriverJar();
141+
auxDriver.extractDriverToTempDir();
142+
String nodePath = auxDriver.driverPath().getParent().resolve(isWindows() ? "node.exe" : "node").toString();
143+
return nodePath;
144+
}
145+
146+
private static boolean isWindows() {
147+
String name = System.getProperty("os.name").toLowerCase();
148+
return name.contains("win");
149+
}
150+
151+
private static void canSpecifyPreinstalledNodeJsShared(Driver driver, Path tmpDir) throws IOException, URISyntaxException, InterruptedException {
152+
Path builtinNode = driver.driverPath().getParent().resolve("node");
153+
assertFalse(Files.exists(builtinNode), builtinNode.toString());
154+
Path builtinNodeExe = driver.driverPath().getParent().resolve("node.exe");
155+
assertFalse(Files.exists(builtinNodeExe), builtinNodeExe.toString());
156+
157+
ProcessBuilder pb = driver.createProcessBuilder();
158+
pb.command().add("--version");
159+
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
160+
Path out = tmpDir.resolve("out.txt");
161+
pb.redirectOutput(out.toFile());
162+
Process p = pb.start();
163+
boolean result = p.waitFor(1, TimeUnit.MINUTES);
164+
assertTrue(result, "Timed out waiting for version to be printed");
165+
String stdout = new String(Files.readAllBytes(out), StandardCharsets.UTF_8);
166+
assertTrue(stdout.contains("Version "), stdout);
131167
}
132168
}

driver/src/main/java/com/microsoft/playwright/impl/driver/Driver.java

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import java.nio.file.Path;
2020
import java.nio.file.Paths;
21+
import java.util.HashMap;
22+
import java.util.LinkedHashMap;
2123
import java.util.Map;
2224

2325
import static com.microsoft.playwright.impl.driver.DriverLogging.logWithTimestamp;
@@ -28,6 +30,8 @@
2830
* loaded from the driver-bundle module if that module is in the classpath.
2931
*/
3032
public abstract class Driver {
33+
protected final Map<String, String> env = new LinkedHashMap<>();
34+
3135
private static Driver instance;
3236

3337
private static class PreinstalledDriver extends Driver {
@@ -38,7 +42,7 @@ private static class PreinstalledDriver extends Driver {
3842
}
3943

4044
@Override
41-
protected void initialize(Map<String, String> env, Boolean installBrowsers) {
45+
protected void initialize(Boolean installBrowsers) {
4246
// no-op
4347
}
4448

@@ -48,36 +52,35 @@ protected Path driverDir() {
4852
}
4953
}
5054

51-
public static synchronized Path ensureDriverInstalled(Map<String, String> env, Boolean installBrowsers) {
55+
public static synchronized Driver ensureDriverInstalled(Map<String, String> env, Boolean installBrowsers) {
5256
if (instance == null) {
53-
try {
54-
instance = createDriver();
55-
logMessage("initializing driver");
56-
instance.initialize(env, installBrowsers);
57-
logMessage("driver initialized.");
58-
} catch (Exception exception) {
59-
instance = null;
60-
throw new RuntimeException("Failed to create driver", exception);
61-
}
57+
instance = createAndInstall(env, installBrowsers);
6258
}
63-
return instance.driverPath();
59+
return instance;
6460
}
6561

66-
protected abstract void initialize(Map<String, String> env, Boolean installBrowsers) throws Exception;
62+
private void initialize(Map<String, String> env, Boolean installBrowsers) throws Exception {
63+
this.env.putAll(env);
64+
initialize(installBrowsers);
65+
}
66+
protected abstract void initialize(Boolean installBrowsers) throws Exception;
6767

6868
public Path driverPath() {
6969
String cliFileName = System.getProperty("os.name").toLowerCase().contains("windows") ?
7070
"playwright.cmd" : "playwright.sh";
7171
return driverDir().resolve(cliFileName);
7272
}
7373

74-
public static void setRequiredEnvironmentVariables(ProcessBuilder pb) {
74+
public ProcessBuilder createProcessBuilder() {
75+
ProcessBuilder pb = new ProcessBuilder(driverPath().toString());
76+
pb.environment().putAll(env);
7577
pb.environment().put("PW_LANG_NAME", "java");
7678
pb.environment().put("PW_LANG_NAME_VERSION", getMajorJavaVersion());
7779
String version = Driver.class.getPackage().getImplementationVersion();
7880
if (version != null) {
7981
pb.environment().put("PW_CLI_DISPLAY_VERSION", version);
8082
}
83+
return pb;
8184
}
8285

8386
private static String getMajorJavaVersion() {
@@ -91,8 +94,19 @@ private static String getMajorJavaVersion() {
9194
}
9295
return version;
9396
}
97+
public static Driver createAndInstall(Map<String, String> env, Boolean installBrowsers) {
98+
try {
99+
Driver instance = newInstance();
100+
logMessage("initializing driver");
101+
instance.initialize(env, installBrowsers);
102+
logMessage("driver initialized.");
103+
return instance;
104+
} catch (Exception exception) {
105+
throw new RuntimeException("Failed to create driver", exception);
106+
}
107+
}
94108

95-
private static Driver createDriver() throws Exception {
109+
private static Driver newInstance() throws Exception {
96110
String pathFromProperty = System.getProperty("playwright.cli.dir");
97111
if (pathFromProperty != null) {
98112
return new PreinstalledDriver(Paths.get(pathFromProperty));

playwright/src/main/java/com/microsoft/playwright/CLI.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,9 @@
2929
*/
3030
public class CLI {
3131
public static void main(String[] args) throws IOException, InterruptedException {
32-
Path driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
33-
ProcessBuilder pb = new ProcessBuilder(driver.toString());
32+
Driver driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
33+
ProcessBuilder pb = driver.createProcessBuilder();
3434
pb.command().addAll(asList(args));
35-
Driver.setRequiredEnvironmentVariables(pb);
3635
String version = Playwright.class.getPackage().getImplementationVersion();
3736
if (version != null) {
3837
pb.environment().put("PW_CLI_DISPLAY_VERSION", version);

playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import com.microsoft.playwright.impl.driver.Driver;
2525

2626
import java.io.IOException;
27-
import java.nio.file.Path;
2827
import java.util.Collections;
2928
import java.util.Map;
3029
import java.util.concurrent.TimeUnit;
@@ -33,16 +32,21 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright {
3332
private Process driverProcess;
3433

3534
public static PlaywrightImpl create(CreateOptions options) {
35+
return createImpl(options, false);
36+
}
37+
38+
public static PlaywrightImpl createImpl(CreateOptions options, boolean forceNewDriverInstanceForTests) {
39+
Map<String, String> env = Collections.emptyMap();
40+
if (options != null && options.env != null) {
41+
env = options.env;
42+
}
43+
Driver driver = forceNewDriverInstanceForTests ?
44+
Driver.createAndInstall(env, true) :
45+
Driver.ensureDriverInstalled(env, true);
3646
try {
37-
Map<String, String> env = Collections.emptyMap();
38-
if (options != null && options.env != null) {
39-
env = options.env;
40-
}
41-
Path driver = Driver.ensureDriverInstalled(env, true);
42-
ProcessBuilder pb = new ProcessBuilder(driver.toString(), "run-driver");
47+
ProcessBuilder pb = driver.createProcessBuilder();
48+
pb.command().add("run-driver");
4349
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
44-
pb.environment().putAll(env);
45-
Driver.setRequiredEnvironmentVariables(pb);
4650
Process p = pb.start();
4751
Connection connection = new Connection(new PipeTransport(p.getInputStream(), p.getOutputStream()), env);
4852
PlaywrightImpl result = connection.initializePlaywright();

playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ void kill() throws InterruptedException {
6060

6161
private static BrowserServer launchBrowserServer(BrowserType browserType) {
6262
try {
63-
Path driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
64-
Path dir = driver.getParent();
63+
Driver driver = Driver.ensureDriverInstalled(Collections.emptyMap(), false);
64+
Path dir = driver.driverPath().getParent();
6565
String node = dir.resolve(isWindows ? "node.exe" : "node").toString();
6666
String cliJs = dir.resolve("package/lib/cli/cli.js").toString();
6767
// We launch node process directly instead of using playwright.sh script as killing the script

0 commit comments

Comments
 (0)