KnowledgeServiceRunner.java
package network.ike.plugin;
import network.ike.knowledge.spi.IkeServiceBootstrap;
import org.apache.maven.api.plugin.Log;
import org.apache.maven.api.plugin.MojoException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* Runs a knowledge-pipeline service across the execution seam: writes the request
* properties file, launches {@code IkeServiceBootstrap} over the <em>project's</em>
* runtime classpath, and reads back the result properties. The plugin stays free of any
* store engine — the implementation arrives as a use-site (or parent-default) dependency
* and is resolved by ServiceLoader inside the seam.
*
* <p>Forked by default: the engine's store lifecycle is JVM-global (a second start in
* one JVM fails), reasoner runs are heap-heavy, and a fork survives anything the
* implementation does short of misbehaving hardware. Child output streams live; any
* non-zero exit fails the build. The in-process mode exists for debugging — same
* classpath, same bootstrap, no process boundary.
*
* <p>The child inherits {@code --enable-preview} when the Maven JVM runs with it, so
* preview-compiled engine classes load in the fork exactly as they do in the build.
*/
final class KnowledgeServiceRunner {
private final Log log;
/**
* Creates a runner logging through the given mojo log.
*
* @param log the mojo's log
*/
KnowledgeServiceRunner(Log log) {
this.log = log;
}
/**
* Executes one service invocation across the seam.
*
* @param serviceInterfaceName the SPI service interface's fully qualified name
* @param request the request properties (the record's wire form)
* @param classpath the seam classpath: the project's runtime dependency
* paths plus its classes directory
* @param workDirectory where the request and result files are written
* @param fork fork a child JVM (the default posture) or run
* in-process for debugging
* @param jvmArguments extra child-JVM arguments (fork only)
* @return the result properties
* @throws MojoException if the seam cannot be launched, the child exits non-zero,
* or the result cannot be read
*/
Properties run(String serviceInterfaceName, Properties request, List<Path> classpath,
Path workDirectory, boolean fork, List<String> jvmArguments) {
try {
Files.createDirectories(workDirectory);
String stem = serviceInterfaceName.substring(serviceInterfaceName.lastIndexOf('.') + 1);
Path requestFile = workDirectory.resolve(stem + "-request.properties");
Path resultFile = workDirectory.resolve(stem + "-result.properties");
try (var out = Files.newOutputStream(requestFile)) {
request.store(out, serviceInterfaceName + " request");
}
Files.deleteIfExists(resultFile);
if (fork) {
runForked(serviceInterfaceName, classpath, requestFile, resultFile, jvmArguments);
} else {
runInProcess(serviceInterfaceName, classpath, requestFile, resultFile);
}
Properties result = new Properties();
try (InputStream in = Files.newInputStream(resultFile)) {
result.load(in);
}
return result;
} catch (IOException e) {
throw new MojoException("Knowledge service seam I/O failed", e);
}
}
private void runForked(String serviceInterfaceName, List<Path> classpath,
Path requestFile, Path resultFile, List<String> jvmArguments) {
List<String> command = new ArrayList<>();
command.add(Path.of(System.getProperty("java.home"), "bin", "java").toString());
if (ManagementFactory.getRuntimeMXBean().getInputArguments().contains("--enable-preview")) {
command.add("--enable-preview");
}
command.addAll(jvmArguments);
command.add("-cp");
command.add(String.join(java.io.File.pathSeparator,
classpath.stream().map(Path::toString).toList()));
command.add(IkeServiceBootstrap.class.getName());
command.add(serviceInterfaceName);
command.add(requestFile.toString());
command.add(resultFile.toString());
try {
Process child = new ProcessBuilder(command).redirectErrorStream(true).start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(child.getInputStream(), StandardCharsets.UTF_8))) {
String line = reader.readLine();
while (line != null) {
log.info("[seam] " + line);
line = reader.readLine();
}
}
int exit = child.waitFor();
if (exit != 0) {
throw new MojoException("Knowledge service " + serviceInterfaceName
+ " failed in the forked seam (exit " + exit + ") — see the [seam] log"
+ " above; if the service was not found, declare the implementation"
+ " dependency (ike-knowledge-provider) at the use site or rely on the"
+ " parent default");
}
} catch (IOException e) {
throw new MojoException("Cannot launch the knowledge service fork", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MojoException("Interrupted waiting for the knowledge service fork", e);
}
}
private void runInProcess(String serviceInterfaceName, List<Path> classpath,
Path requestFile, Path resultFile) {
List<URL> urls = new ArrayList<>();
try {
for (Path path : classpath) {
urls.add(path.toUri().toURL());
}
} catch (Exception e) {
throw new MojoException("Cannot assemble the in-process seam classpath", e);
}
ClassLoader originalContext = Thread.currentThread().getContextClassLoader();
try (URLClassLoader loader = new URLClassLoader("ike-knowledge-seam",
urls.toArray(new URL[0]), ClassLoader.getPlatformClassLoader())) {
Thread.currentThread().setContextClassLoader(loader);
Class<?> bootstrap = Class.forName(IkeServiceBootstrap.class.getName(), true, loader);
Method main = bootstrap.getMethod("main", String[].class);
main.invoke(null, (Object) new String[]{serviceInterfaceName,
requestFile.toString(), resultFile.toString()});
} catch (java.lang.reflect.InvocationTargetException e) {
throw new MojoException("Knowledge service " + serviceInterfaceName
+ " failed in-process: " + e.getCause().getMessage(), e.getCause());
} catch (MojoException e) {
throw e;
} catch (Exception e) {
throw new MojoException("Cannot run the knowledge service in-process", e);
} finally {
Thread.currentThread().setContextClassLoader(originalContext);
}
}
}