Overview

This plugin contains utilities to programmatically execute and manipulate external programs. It also allows you to easily compile/package/install a Maven project located outside your application.

Usage

The utilities provided by this plugin are available through the SpincastProcessUtils interface, which you can inject wherever you want.

Running a goal on an external Maven project

The executeGoalOnExternalMavenProject(...) method allows you to programmatically compile, package or install a Maven project located outside of your application.

This feature can be useful for example to run tests that need to validate code from inside a .jar file.

The signature of this method is:

public File executeGoalOnExternalMavenProject(ResourceInfo projectRootInfo,
                                              MavenProjectGoal mavenGoal,
                                              Map<String,Object> pomParams);

Using this method you can:

  • Run the compile, package or install goal on a Maven project located:
    • On the file system
    • On the classpath (it will first be extracted to a temporary directory on the file system).
  • Specify parameters to be used to replace some placeholders in the project's pom.xml, before executing the Maven goal. The replacement is done using the Templating Engine.

Note that this method returns the root directory of the Maven project. If the project is extracted from the classpath, this directory is going to be created under the Temporary Directory. If the project is already on the file system, it will return its root directory, as is.

Let's look at an example where we run the package goal on a Maven project located on the classpath (the project would be provided by your application at "src/main/resources/myMavenProject"):

String spincastVersion = getSpincastUtils().getSpincastCurrentVersion();

File projectDir = getSpincastProcessUtils()
        .executeGoalOnExternalMavenProject(new ResourceInfo("/myMavenProject", true),
                                           MavenProjectGoal.PACKAGE,
                                           SpincastStatics.map("spincastVersion",
                                                               spincastVersion));

File jarFile = new File(projectDir, 
                        "target/project-artifact-name-" + spincastVersion + ".jar");
assertTrue(jarFile.isFile());

Explanation :

  • 1 : We get the current Spincast version. We will use replace a placeholder in the project's pom.xml file with this version.
  • 4 : We call the executeGoalOnExternalMavenProject() method. The first parameter is the path to the Maven project. Here, the project is on the classpath ("true").
  • 5 : We specify the Maven goal to run. In this example, we will call the "package" goal so the .jar associated with this project is generated.
  • 6-7 : The parameters to be used to replace placeholders in the project's pom.xml file (the Templating Engine is used to do so).
  • 9-11: A simple assertion to show that the project has been packaged successfully and that the associated .jar file has been successfully generated!

Have a look at this test for a real example of running a goal on an external Maven project.

Executing an external program, asynchronously

The executeAsync(...) method allows you to run an external program and to keep control over the created process.

Here is an example where we run an executable .jar file programmatically:

ProcessExecutionHandlerDefault handler = new ProcessExecutionHandlerDefault() {
    @Override
    public void onSystemOut(String line) {
        System.out.println("[jar out] - " + line);
    }
};

// Start an executable jar
getSpincastProcessUtils().executeAsync(handler,
                                       "java",
                                       "-jar", 
                                       "/some/path.jar", 
                                       "12345"); // argument for the .jar program
try {
    // Wait for the port 12345 to be open
    handler.waitForPortOpen("localhost", 12345, 5, 1000);

    // Make a request to the HTTP server started by the .jar program
    HttpResponse response = getHttpClient().GET("http://localhost:12345").send();
    assertEquals(HttpStatus.SC_OK, response.getStatus());
    // ...
} finally {
    // Kill the process when we are done with it!
    handler.killProcess();
}

Explanation :

  • 1-6 : The ProcessExecutionHandler handler that is going to receive information about the created process. Here, we base this handler on the default ProcessExecutionHandlerDefault implementation and we only override onSystemOut(...).
  • 9 : We call the executeAsync(...) method.
  • 10 : The program to run. Here, we will use java in order to start an executable .jar file. Note that you can (and sometimes must) specify an absolute path.
  • 11-13 : The program arguments. Here, the resulting command would be: java -jar /some/path.jar 12345. The "12345" argument would be passed to the executed jar (in our example this is the port to use for the jar to start an HTTP server!).
  • 16 : The default ProcessExecutionHandlerDefault handler provides a waitForPortOpen(...) method which allows you to wait for a port to be open (this method is synchronous!).
  • 19-20 : You use the started program. In our example, the executed .jar file started an HTTP server on port 12345. We make a request to it.
  • 22-25 : When we are done with the programmatically started program, we make sure its process is killed by calling killProcess(). Doing so is not always required but it is when the executed program does not exit by itself (as in our example, since the HTTP server started by the jar would listen for requests indefinitely).

On some environments, you may have to specify the full path to the program to be executed (it may depend on the PATH variable of your system).

Here are the methods available on the ProcessExecutionHandler handler and that need to be implemented (if you don't use the ProcessExecutionHandlerDefault default implementation, in which case you can override some):

  • void onExit(int exitCode)

    This method will be called when the process exits. If it exits without any error, exitCode will in general be "0".

    The default implementation, from ProcessExecutionHandlerDefault, will simply log the exit code.

    Note that if you execute a program that does not exit by itself (for example an HTTP server is started and wait for requests indefinitely), you have to explicitly kill the process.

    The onEnd() method is called after this one.

  • void onLaunchException(Exception ex)

    This method is going to be called if an exception occurs during the launch of the external program.

    The default implementation, from ProcessExecutionHandlerDefault, will simply log the exception.

    Note that when this method is called, onExit() will not be called: the program was never even started.

    The onEnd() method is called after this one.

  • void onTimeoutException()

    This method is going to be called if the program is launched properly but doesn't exit before the specified timeout is reached (if one is specified) .

    The default implementation, from ProcessExecutionHandlerDefault, will simply log the error.

    Note that when this method is called, onExit() will not be called.

    The process is killed automatically.

    The onEnd() method is called after this one.

  • void onEnd()

    Always called at the end, whether the process exited properly or an exception/timeout occurred.

  • void onSystemOut(String line)

    This method will be called when a line is printed to the standard output by the executed program.

    The default implementation, from ProcessExecutionHandlerDefault, will simply log the line using System.out.println().

  • void onSystemErr(String line)

    This method will be called when a line is printed to the standard errors output by the executed program.

    The default implementation, from ProcessExecutionHandlerDefault, will simply log the line using System.err.println().

  • void killProcess()

    Kills the created process.

    If the process can't be killed, for any reason, an exception is thrown.

    Only the first call to this method will be acknowledged, the following ones will simply be ignored.

  • boolean isProcessAlive()

    Return true if the created process is alive.

  • void waitForPortOpen(String host, int port, int nbrTry, int sleepMilliseconds)

    This method, provided by the ProcessExecutionHandlerDefault default implementation, allows you to wait for a port to be connectable. This is useful if the executed program starts an HTTP server and you need to wait for it to be ready.

    If the port is still not available after nbrTry * sleepMilliseconds milliseconds, a PortNotOpenException exception is thrown.

Remember that if you run a program that doesn't exit by itself (for example it starts an HTTP server that listens for requests indefinitely), you are responsible to kill the process when you are done with it, by calling killProcess()! This can be done using a try/finally block.

Executing an external program, synchronously

As we saw in the previous section, you can execute an external program asynchronously, so it doesn't block the current thread and can be manipulated. In simple cases, you can also execute it synchronously. Here is an example:

try {
    SyncExecutionResult result = 
        getSpincastProcessUtils().executeSync(3,
                                              TimeUnit.MINUTES,
                                              ExecutionOutputStrategy.BUFFER,
                                              "/my/program/to/execute",
                                              "arg1",
                                              "arg2");
    assertEquals(new Integer(0), result.getExitCode());
    
} catch (LaunchException ex) {
    // manage the exception
    
} catch (TimeoutException ex) {
    // manage the exception
}

Explanation :

  • 2 : A SyncExecutionResult object will be returned and will contain the result of the execution.
  • 3 : We call executeSync(...) to launch the program synchronously.
  • 3-4 : A timeout is always required when executing a program synchronously!
  • 5 : The strategy to use to deal with the output from the created process. Here, we ask for the outputs (Standard output and Standard errors) to be buffered so they are available through the SyncExecutionResult result object when the execution is done.
  • 6 : The program to execute.
  • 7-8 : arguments for the program.
  • 9 : as soon as the executeSync(...) method exits, we can get information from the SyncExecutionResult object. Here, we simply validate that the program exited with the code "0".
  • 11-12 : We manage the exception if one occurs during the launch of the program.
  • 14-16 : We manage the timeout exception, if any.

Two typed exceptions can be thrown when using executeSync():

  • LaunchException: if an error occurs during the launch of the program.
  • TimeoutException: if the program launched successfully but timed out. Note that the process will then be killed automatically.

The methods available on the SyncExecutionResult object are:

  • int getExitCode()

    The exit code of the executed program.

    Note that this code is undefined if the program timed out and the SyncExecutionResult instance has been taken from TimeoutException's getSyncExecutionResult()!

  • List<String> getSystemOutLines()

    The lines outputted to the standard output by the created process. Those will only be available if the ExecutionOutputStrategy parameter is explicitly set to BUFFER. Otherwise, an empty list is returned.

  • List<String> getSystemErrLines()

    The lines outputted to the standard errors by the created process. Those will only be available if the ExecutionOutputStrategy parameter is explicitly set to BUFFER. Otherwise, an empty list is returned.

The strategies that you can use to deal with the output of the created process are:

  • SYSTEM (default): the output will be redirected to System.out or System.err immediately, as it occurs.
  • BUFFER: the output will be buffered so it is available through the getSystemOutLines() and getSystemErrLines() methods of the SyncExecutionResult result object.
  • NONE: the output is ignored and discarded.

Beware that buffering the output (by using BUFFER as the strategy) may consume a lot of memory if the created process generates a lot of it! If this is the case for you, you may have to use the asynchronous method instead.

Installation

1. Add this Maven artifact to your project:

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-plugins-process-utils</artifactId>
    <version>1.14.1</version>
</dependency>

2. Add an instance of the SpincastProcessUtilsPlugin plugin to your Spincast Bootstrapper:


Spincast.configure()
        .plugin(new SpincastProcessUtilsPlugin())
        // ...