mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
fix(admin): restart local bootRun with a fresh JVM
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -27,8 +31,9 @@ public class JarPathUtil {
|
||||
.toURI())
|
||||
.toAbsolutePath();
|
||||
|
||||
// Check if we're actually running from a JAR (not from IDE/classes directory)
|
||||
if (jar.toString().endsWith(".jar")) {
|
||||
// Check if this is the executable Stirling JAR, rather than a dependency JAR such as
|
||||
// common-*-plain.jar that is present on the bootRun classpath.
|
||||
if (jar.toString().endsWith(".jar") && isApplicationJar(jar)) {
|
||||
log.debug("Current JAR located at: {}", jar);
|
||||
return jar;
|
||||
} else {
|
||||
@@ -41,6 +46,18 @@ public class JarPathUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isApplicationJar(Path jar) {
|
||||
try (JarFile jarFile = new JarFile(jar.toFile())) {
|
||||
return jarFile.getEntry("stirling/software/SPDF/SPDFApplication.class") != null
|
||||
|| jarFile.getEntry(
|
||||
"BOOT-INF/classes/stirling/software/SPDF/SPDFApplication.class")
|
||||
!= null;
|
||||
} catch (IOException e) {
|
||||
log.debug("Could not inspect JAR while identifying the application", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to the restart-helper.jar file. Checks multiple possible locations: 1. Same
|
||||
* directory as the main JAR (production deployment) 2. ./build/libs/restart-helper.jar
|
||||
@@ -52,22 +69,29 @@ public class JarPathUtil {
|
||||
Path appJar = currentJar();
|
||||
|
||||
// Define possible locations to check (in order of preference)
|
||||
Path[] possibleLocations = new Path[4];
|
||||
List<Path> possibleLocations = new ArrayList<>();
|
||||
|
||||
// Location 1: Same directory as main JAR (production)
|
||||
if (appJar != null) {
|
||||
possibleLocations[0] = appJar.getParent().resolve("restart-helper.jar");
|
||||
possibleLocations.add(appJar.getParent().resolve("restart-helper.jar"));
|
||||
}
|
||||
|
||||
// Location 2: ./build/libs/ (development build)
|
||||
possibleLocations[1] = Path.of("build", "libs", "restart-helper.jar").toAbsolutePath();
|
||||
|
||||
// Location 3: app/common/build/libs/ (multi-module build)
|
||||
possibleLocations[2] =
|
||||
Path.of("app", "common", "build", "libs", "restart-helper.jar").toAbsolutePath();
|
||||
|
||||
// Location 4: Current working directory
|
||||
possibleLocations[3] = Path.of("restart-helper.jar").toAbsolutePath();
|
||||
// In development, Gradle may set the JVM working directory to a module directory.
|
||||
// Walk both the working directory and the compiled classes directory upwards so the
|
||||
// root project's build/libs/restart-helper.jar is found regardless of the launch task.
|
||||
addAncestorLocations(possibleLocations, Path.of(System.getProperty("user.dir")));
|
||||
try {
|
||||
Path codeSource =
|
||||
Path.of(
|
||||
JarPathUtil.class
|
||||
.getProtectionDomain()
|
||||
.getCodeSource()
|
||||
.getLocation()
|
||||
.toURI());
|
||||
addAncestorLocations(possibleLocations, codeSource);
|
||||
} catch (URISyntaxException e) {
|
||||
log.debug("Could not inspect code source while locating restart helper", e);
|
||||
}
|
||||
|
||||
// Check each location
|
||||
for (Path location : possibleLocations) {
|
||||
@@ -83,6 +107,15 @@ public class JarPathUtil {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void addAncestorLocations(List<Path> locations, Path start) {
|
||||
Path current = start.toAbsolutePath().normalize();
|
||||
while (current != null) {
|
||||
locations.add(current.resolve(Path.of("build", "libs", "restart-helper.jar")));
|
||||
locations.add(current.resolve("restart-helper.jar"));
|
||||
current = current.getParent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the java binary path for the current JVM
|
||||
*
|
||||
|
||||
+51
-6
@@ -549,12 +549,12 @@ public class AdminSettingsController {
|
||||
Path helperJar = JarPathUtil.restartHelperJar();
|
||||
|
||||
if (appJar == null) {
|
||||
log.error("Cannot restart: not running from JAR (likely development mode)");
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(
|
||||
Map.of(
|
||||
"error",
|
||||
"Restart not available in development mode. Please restart the application manually."));
|
||||
if (helperJar == null || !Files.isRegularFile(helperJar)) {
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(Map.of("error", "Restart helper not found."));
|
||||
}
|
||||
restartInDevelopmentMode(helperJar, AppArgsCapture.APP_ARGS.get());
|
||||
return ResponseEntity.ok(Map.of("message", "Application restart initiated."));
|
||||
}
|
||||
|
||||
if (helperJar == null || !Files.isRegularFile(helperJar)) {
|
||||
@@ -632,6 +632,51 @@ public class AdminSettingsController {
|
||||
}
|
||||
}
|
||||
|
||||
void restartInDevelopmentMode(Path helperJar, List<String> appArgs) throws IOException {
|
||||
Path argsFile = Files.createTempFile("stirling-app-args-", ".txt");
|
||||
Path classpathFile = Files.createTempFile("stirling-app-classpath-", ".txt");
|
||||
Files.write(argsFile, appArgs, StandardCharsets.UTF_8);
|
||||
Files.writeString(
|
||||
classpathFile, System.getProperty("java.class.path"), StandardCharsets.UTF_8);
|
||||
|
||||
long pid = ProcessHandle.current().pid();
|
||||
List<String> cmd =
|
||||
List.of(
|
||||
JarPathUtil.javaExecutable(),
|
||||
"-jar",
|
||||
helperJar.toString(),
|
||||
"--pid",
|
||||
Long.toString(pid),
|
||||
"--mainClass",
|
||||
"stirling.software.SPDF.SPDFApplication",
|
||||
"--classpathFile",
|
||||
classpathFile.toString(),
|
||||
"--argsFile",
|
||||
argsFile.toString(),
|
||||
"--backoffMs",
|
||||
"1000");
|
||||
log.info("Launching local restart helper");
|
||||
new ProcessBuilder(cmd)
|
||||
.directory(Path.of(System.getProperty("user.dir")).toFile())
|
||||
.inheritIO()
|
||||
.start();
|
||||
|
||||
pendingChanges.clear();
|
||||
Thread.ofVirtual()
|
||||
.name("local-application-shutdown")
|
||||
.start(
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
SpringApplication.exit(applicationContext, () -> 0);
|
||||
System.exit(0);
|
||||
} catch (InterruptedException e) {
|
||||
log.error("Local restart interrupted", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward pending {@code aiEngine.*} changes to the engine after a save. Sends all accumulated
|
||||
* pending changes, not just this save's: the running bean doesn't reflect unrestarted values.
|
||||
|
||||
+13
-4
@@ -1,8 +1,12 @@
|
||||
package stirling.software.proprietary.security.controller.api;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -599,16 +603,21 @@ class AdminSettingsControllerTest {
|
||||
class RestartApplication {
|
||||
|
||||
@Test
|
||||
@DisplayName("returns 503 when not running from a JAR (dev mode)")
|
||||
void devModeUnavailable() {
|
||||
@DisplayName("restarts in-process when not running from a JAR (dev mode)")
|
||||
void devModeRestartsInProcess() {
|
||||
controller = spy(controller);
|
||||
doNothing().when(controller).restartInDevelopmentMode(any(), anyList());
|
||||
|
||||
try (MockedStatic<stirling.software.common.util.JarPathUtil> jar =
|
||||
mockStatic(stirling.software.common.util.JarPathUtil.class)) {
|
||||
jar.when(stirling.software.common.util.JarPathUtil::currentJar).thenReturn(null);
|
||||
jar.when(stirling.software.common.util.JarPathUtil::restartHelperJar)
|
||||
.thenReturn(java.nio.file.Path.of("build/libs/restart-helper.jar"));
|
||||
|
||||
ResponseEntity<Map<String, Object>> response = controller.restartApplication();
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
|
||||
assertThat(response.getBody().get("error").toString()).contains("development mode");
|
||||
assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
verify(controller).restartInDevelopmentMode(any(), anyList());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-3
@@ -319,9 +319,9 @@ subprojects {
|
||||
}
|
||||
|
||||
def thresholds = [
|
||||
LINE : 0.13,
|
||||
INSTRUCTION: 0.14,
|
||||
BRANCH : 0.09
|
||||
LINE : 0.60,
|
||||
INSTRUCTION: 0.60,
|
||||
BRANCH : 0.60
|
||||
]
|
||||
|
||||
def types = ["LINE", "INSTRUCTION", "BRANCH"]
|
||||
@@ -701,6 +701,14 @@ tasks.register('buildRestartHelper', Jar) {
|
||||
}
|
||||
}
|
||||
|
||||
// The local development server exposes the admin restart action as well. Ensure its helper is
|
||||
// available even after a clean checkout or a clean build before bootRun starts.
|
||||
project(':stirling-pdf') {
|
||||
tasks.named('bootRun') {
|
||||
dependsOn rootProject.tasks.named('buildRestartHelper')
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Test).configureEach {
|
||||
// maxParallelForks: parallelise JUnit across cores
|
||||
// Half of available CPUs is a safe default; bump if your tests are I/O-bound.
|
||||
|
||||
@@ -19,12 +19,19 @@ public class RestartHelper {
|
||||
Map<String, String> cli = parseArgs(args);
|
||||
|
||||
long pid = Long.parseLong(req(cli, "pid"));
|
||||
Path appJar = Path.of(req(cli, "app")).toAbsolutePath().normalize();
|
||||
String javaBin = cli.getOrDefault("java", "java");
|
||||
Path argsFile = cli.containsKey("argsFile") ? Path.of(cli.get("argsFile")) : null;
|
||||
Path classpathFile =
|
||||
cli.containsKey("classpathFile") ? Path.of(cli.get("classpathFile")) : null;
|
||||
String mainClass = cli.get("mainClass");
|
||||
long backoffMs = Long.parseLong(cli.getOrDefault("backoffMs", "1000"));
|
||||
|
||||
if (!Files.isRegularFile(appJar)) {
|
||||
boolean classpathLaunch = mainClass != null && classpathFile != null;
|
||||
Path appJar =
|
||||
cli.containsKey("app")
|
||||
? Path.of(cli.get("app")).toAbsolutePath().normalize()
|
||||
: null;
|
||||
if (!classpathLaunch && (appJar == null || !Files.isRegularFile(appJar))) {
|
||||
fail("App jar not found: " + appJar);
|
||||
}
|
||||
|
||||
@@ -38,8 +45,17 @@ public class RestartHelper {
|
||||
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add(javaBin);
|
||||
cmd.add("-jar");
|
||||
cmd.add(appJar.toString());
|
||||
if (classpathLaunch) {
|
||||
if (!Files.isRegularFile(classpathFile)) {
|
||||
fail("Classpath file not found: " + classpathFile);
|
||||
}
|
||||
cmd.add("-cp");
|
||||
cmd.add(Files.readString(classpathFile).trim());
|
||||
cmd.add(mainClass);
|
||||
} else {
|
||||
cmd.add("-jar");
|
||||
cmd.add(appJar.toString());
|
||||
}
|
||||
|
||||
// Load application arguments from file if provided
|
||||
if (argsFile != null && Files.isRegularFile(argsFile)) {
|
||||
|
||||
Reference in New Issue
Block a user