Skip to content

Commit

Permalink
feat(java): support JSII_NODE setting
Browse files Browse the repository at this point in the history
Users can customize the `node` runtime used by the jsii runtime for java
by providing the `JSII_NODE` environment variable. Additionally, this
corrects how the child process is spawned so that `JSII_NODE` and
`JSII_RUNTIME` can contain spaces (previously, this would result in a
spawn error). Added a test to verify the various scenarios work as
intended.

Fixes #4009
  • Loading branch information
RomainMuller committed Mar 22, 2023
1 parent 6b2fd18 commit 32d31de
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 7 deletions.
1 change: 1 addition & 0 deletions packages/@jsii/java-runtime-test/project/.mvn/maven.config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--settings=user.xml
1 change: 1 addition & 0 deletions packages/@jsii/java-runtime/project/.mvn/maven.config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--settings=user.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
import software.amazon.jsii.api.Callback;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
Expand All @@ -14,6 +15,7 @@
import java.lang.reflect.InvocationTargetException;
import java.nio.channels.Channels;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -71,6 +73,24 @@ public final class JsiiRuntime {
*/
private Thread shutdownHook;

/** The value of the JSII_RUNTIME environment variable */
@Nullable
private final String customRuntime;

/** The value of the JSII_NODE environment variable */
@Nullable
private final String customNode;

public JsiiRuntime() {
this(System.getenv("JSII_RUNTIME"), System.getenv("JSII_NODE"));
}

@VisibleForTesting
JsiiRuntime(@Nullable final String customRuntime, @Nullable final String customNode) {
this.customRuntime = customRuntime;
this.customNode = customNode;
}

/**
* The main API of this class. Sends a JSON request to jsii-runtime and returns the JSON response.
*
Expand Down Expand Up @@ -273,13 +293,7 @@ private synchronized void startRuntimeIfNeeded() {
&& !jsiiDebug.equalsIgnoreCase("false")
&& !jsiiDebug.equalsIgnoreCase("0");

// If JSII_RUNTIME is set, use it to find the jsii-server executable
// otherwise, we default to "jsii-runtime" from PATH.
final String jsiiRuntimeEnv = System.getenv("JSII_RUNTIME");
final List<String> jsiiRuntimeCommand = jsiiRuntimeEnv == null
? Arrays.asList("node", BundledRuntime.extract(getClass()))
: Collections.singletonList(jsiiRuntimeEnv);

final List<String> jsiiRuntimeCommand = jsiiRuntimeCommand();
if (traceEnabled) {
System.err.println("jsii-runtime: " + String.join(" ", jsiiRuntimeCommand));
}
Expand Down Expand Up @@ -314,6 +328,56 @@ private synchronized void startRuntimeIfNeeded() {
this.client = new JsiiClient(this);
}

/**
* Determines the correct command to execute in order to start the jsii runtime program. If custom runtimes are
* configured (either via `JSII_RUNTIME` or `JSII_NODE`), defer to `sh -c` in order to ensure platform-appropriate
* command parsing is performed, since {@link ProcessBuilder#command(String...)} won't do any of this by itself.
*
* @return The command to execute to start the jsii runtime program.
*/
private List<String> jsiiRuntimeCommand() {
if (this.customRuntime != null) {
if (this.customRuntime.matches(".*\\s.*")) {
// Shell out only if the custom runtime includes white space.
return shellOut(this.customRuntime);
}
return Collections.singletonList(this.customRuntime);
}

// We don't use a custom runtime, so extract the bundled one...
final String bundledRuntime = BundledRuntime.extract(JsiiRuntime.class);

if (this.customNode != null && this.customNode.matches(".*\\s.*")) {
// Shell out only if the custom node includes white space.
return shellOut(this.customNode, bundledRuntime);
}
return Arrays.asList(this.customNode != null ? this.customNode : "node", bundledRuntime);
}

/**
* Creates a command to sub-shell to the specified end-user command, that uses `/bin/sh` on *NIXes, and %COMSPEC%, or
* cmd.exe on Windows.
*
* <p>
* This is heavily inspired from <a href="https://github.com/nodejs/node/blob/434bdde97464cc04f79ed3c8398f2a50c71c39d1/lib/child_process.js#L617-L642">how Node.js does the same thing</a>.
* </p>
*
* @param command the end-user command to be run.
*
* @return a full sub-shell command that is platform-appropriate.
*/
private static List<String> shellOut(final String... command) {
final boolean isWindows = System.getProperty("os.name").startsWith("Windows");
if (isWindows) {
String cmd = System.getenv("COMSPEC");
if (cmd == null) {
cmd = "cmd.exe";
}
return Arrays.asList(cmd, "/d", "/s", "/c", String.join(" ", command));
}
return Arrays.asList("/bin/sh", "-c", String.join(" ", command));
}

/**
* Verifies the "hello" message and runtime version compatibility.
* In the meantime, we require full version compatibility, but we should use semver eventually.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package software.amazon.jsii;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;

public final class JsiiRuntimeTest {
@Test
public void withNoCustomization() {
final JsiiRuntime runtime = new JsiiRuntime(null, null);
runtime.getClient().createObject("Object", Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
runtime.terminate();
}

@Test
public void withCustomNode_Simple() {
final String customNode = resolveNodeFromPath();

final JsiiRuntime runtime = new JsiiRuntime(null, customNode);
runtime.getClient().createObject("Object", Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
runtime.terminate();
}

@Test
public void withCustomNode_WithSpace() {
final JsiiRuntime runtime = new JsiiRuntime(null, "node --max_old_space_size=1024");
runtime.getClient().createObject("Object", Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
runtime.terminate();
}

@Test
public void withCustomRuntime_Simple() throws Exception {
final Path launcher = Files.createTempFile("jsii-runtime", ".launcher.bat");
try (final Writer writer = new FileWriter(launcher.toFile())) {
writer.write("node ./src/main/resources/software/amazon/jsii/bin/jsii-runtime.js\n");
}
Assertions.assertTrue(launcher.toFile().setExecutable(true));

final JsiiRuntime runtime = new JsiiRuntime(launcher.toString(), null);
runtime.getClient().createObject("Object", Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
runtime.terminate();
}

@Test
public void withCustomRuntime_WithSpace() {
final JsiiRuntime runtime = new JsiiRuntime("node ./src/main/resources/software/amazon/jsii/bin/jsii-runtime.js", null);
runtime.getClient().createObject("Object", Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
runtime.terminate();
}

private static String resolveNodeFromPath() {
try {
final String[] command = System.getProperty("os.name").startsWith("Windows")
? new String[]{"cmd.exe", "/d", "/s", "/c", "where node"}
: new String[]{"sh", "-c", "command -v node"};
final Process process = Runtime.getRuntime().exec(command);
try {
final BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
return br.readLine();
} finally {
process.waitFor();
}
} catch (final IOException ioe) {
throw new UncheckedIOException(ioe);
} catch (final InterruptedException ie) {
throw new RuntimeException(ie);
}
}
}

0 comments on commit 32d31de

Please sign in to comment.