2

I'm trying to launch a new process on a separate JVM via code following the method illustrated here: Executing a Java application in a separate process

The code I'm using is the following (taken from the question above):

public static int exec(Class klass) throws IOException, InterruptedException {
        String javaHome = System.getProperty("java.home");
        String javaBin = javaHome +
                File.separator + "bin" +
                File.separator + "java";
        String classpath = System.getProperty("java.class.path");
        String className = klass.getName();

        ProcessBuilder builder = new ProcessBuilder(javaBin,"-cp",classpath,className);
        Process process = builder.inheritIO().start();
        process.waitFor();
        return process.exitValue();
    }

...in which klass is the class I want to launch. This would work for a normal Java process, but the problem is that I'm trying to launch a JavaFX application, and the code above generates the following error:

Error: JavaFX runtime components are missing, and are required to run this application

So, to add the JavaFX modules, I tried including the --module-path and --add-modules commands in the declaration of builder, I even attempted copying and pasting the entire execution command, and I kept getting this other error:

Unrecognized option: (command string with modules)
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

How could I solve this?

Let me know if details are needed.

1 Answers1

3

Since the advent of modules, there are at least three different ways a JavaFX application can be configured:

  1. Put everything on the module-path, including the JavaFX modules and your module.

    • This is the ideal situation but not always possible/viable (e.g. because of incompatible dependencies).
  2. Put the JavaFX modules on the module-path and your own code on the class-path.

    • This configuration requires the use of --add-modules.
  3. Put everything on the class-path, including the JavaFX modules and your own code.

    • With this configuration your main class cannot be a subtype of Application. Otherwise you get the error you mentioned in your question: "Error: JavaFX runtime components are missing, and are required to run this application".
    • This configuration allows for easy use of so-called fat/uber JARs.
    • Warning: This approach is explicitly unsupported.

The command line used with ProcessBuilder will depend on which configuration your application uses. You also have to take into account any other options passed the command line, such as the default encoding or locale. Unfortunately, your question doesn't provide enough information to tell what exactly is going wrong. The error you mention makes me think you're using the third configuration, but I can't be sure.

That said, I'll give some examples of launching the same application from within the application; you should be able to modify things to fit your needs. Note I used Java/JavaFX 13.0.1 when testing the below code.


Configuration #1

Put everything on the module-path.

module-info.java:

module app {
  requires javafx.controls;

  exports com.example.app to
      javafx.graphics;
}

Main.java:

package com.example.app;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

import static java.lang.System.getProperty;

public class Main extends Application {

  private static void launchProcess() {
    try {
      new ProcessBuilder(
              Path.of(getProperty("java.home"), "bin", "java").toString(),
              "--module-path",
              getProperty("jdk.module.path"),
              "--module",
              getProperty("jdk.module.main") + "/" + getProperty("jdk.module.main.class"))
          .inheritIO()
          .start();
    } catch (IOException ex) {
      throw new UncheckedIOException(ex);
    }
  }

  @Override
  public void start(Stage primaryStage) {
    Button launchBtn = new Button("Launch process");
    launchBtn.setOnAction(
        event -> {
          event.consume();
          launchProcess();
        });
    primaryStage.setScene(new Scene(new StackPane(launchBtn), 500, 300));
    primaryStage.setTitle("Multi-Process Example");
    primaryStage.show();
  }
}

Command line:

java --module-path <PATH> --module app/com.example.app.Main

Replace "<PATH>" with a path containing both the JavaFX modules and the above module.


Configuration #2

Put JavaFX modules on the module-path and your code on the class-path.

Main.java:

package com.example.app;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

import static java.lang.System.getProperty;

public class Main extends Application {

  private static void launchProcess() {
    try {
      new ProcessBuilder(
              Path.of(getProperty("java.home"), "bin", "java").toString(),
              "--module-path",
              getProperty("jdk.module.path"),
              "--add-modules",
              "javafx.controls",
              "--class-path",
              getProperty("java.class.path"),
              Main.class.getName())
          .inheritIO()
          .start();
    } catch (IOException ex) {
      throw new UncheckedIOException(ex);
    }
  }

  @Override
  public void start(Stage primaryStage) {
    Button launchBtn = new Button("Launch process");
    launchBtn.setOnAction(
        event -> {
          event.consume();
          launchProcess();
        });
    primaryStage.setScene(new Scene(new StackPane(launchBtn), 500, 300));
    primaryStage.setTitle("Multi-Process Example");
    primaryStage.show();
  }
}

Command line:

java --module-path <M_PATH> --add-modules javafx.controls --class-path <C_PATH> com.example.app.Main

Replace "<M_PATH>" with a path containing the JavaFX modules and replace "<C_PATH>" with a path containing the above code.


Configuration #3

Put everything on the class-path. Note the main class (now Launcher) is not a subclass of Application.

Launcher.java:

package com.example.app;

import javafx.application.Application;

public class Launcher {

  public static void main(String[] args) {
    Application.launch(Main.class, args);
  }
}

Main.java:

package com.example.app;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

import static java.lang.System.getProperty;

public class Main extends Application {

  private static void launchProcess() {
    try {
      new ProcessBuilder(
              Path.of(getProperty("java.home"), "bin", "java").toString(),
              "--class-path",
              getProperty("java.class.path"),
              Launcher.class.getName())
          .inheritIO()
          .start();
    } catch (IOException ex) {
      throw new UncheckedIOException(ex);
    }
  }

  @Override
  public void start(Stage primaryStage) {
    Button launchBtn = new Button("Launch process");
    launchBtn.setOnAction(
        event -> {
          event.consume();
          launchProcess();
        });
    primaryStage.setScene(new Scene(new StackPane(launchBtn), 500, 300));
    primaryStage.setTitle("Multi-Process Example");
    primaryStage.show();
  }
}

Command line:

java --class-path <PATH> com.example.app.Launcher

Replace "<PATH>" with a path containing the JavaFX JARs and the above code.

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • Hi! Thanks for answering, I used Configuration 2 and managed to execute a new process, however, I am unsure that it's running on a new JVM. I'm using IntelliJ IDEA 2019.2.3 and whatever console output I print with the new process is printed in the same window as the calling process, not a new window. – DannyTheGamer Jan 06 '20 at 14:31
  • That's because I used [`ProcessBuilder#inheritIO()`](https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/ProcessBuilder.html#inheritIO()), which makes the new process use the same input/output/error streams as the current process; however, it's still a new process. You have to consume the output/error streams of the new process lest the buffers fill up and block said new process. You could do this by consuming each stream in it's own thread, but using `#inheritIO()` was simpler for the examples. – Slaw Jan 06 '20 at 15:06
  • By the way, this is documented by [`Process`](https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/Process.html): "_Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the process may cause the process to block, or even deadlock_". And if you want to confirm it's a new process you can query [`ProcessHandle#pid()`](https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/ProcessHandle.html#pid()) (Java 9+). – Slaw Jan 06 '20 at 16:38
  • Out of curiostiy, how would the Thread-based approach differ from the previous approach? I was intending on having each process run on its own thread anyway. Meanwhile, I'm upvoting and marking your answer as solution for the main issue. Thanks, man! – DannyTheGamer Jan 07 '20 at 00:02
  • Well the process is already being executed in its own thread by virtue of it being a different process; once started, the call to `#start()` returns and the two processes are now running concurrently. What I meant is that you'd consume the [output](https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/Process.html#getInputStream()) and [error](https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/Process.html#getInputStream()) streams in different threads. I've been trying to find an example Q&A but haven't found one I felt was clear enough. – Slaw Jan 07 '20 at 01:57
  • I wasn't aware this was added in Java 9, but if you don't care about the output/error and you don't want the child process to inherit the parent's streams, then you can use [`ProcessBuilder.Redirect.DISCARD`](https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/ProcessBuilder.Redirect.html#DISCARD) with [`ProcessBuilder#redirectOutput(ProcessBuilder.Redirect)`](https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/lang/ProcessBuilder.html#redirectOutput(java.lang.ProcessBuilder.Redirect)) (same with the error stream). – Slaw Jan 07 '20 at 02:08
  • Ok, got it. I'm now going to try to have the processes' streams be consumed in different threads, as they're supposed to communicate through BufferedReaders and BufferedWriters anyway. Thanks for helping me out! – DannyTheGamer Jan 07 '20 at 16:10