diff --git a/src/main/java/org/qortal/api/restricted/resource/AdminResource.java b/src/main/java/org/qortal/api/restricted/resource/AdminResource.java index f0e045ed3..35bf91089 100644 --- a/src/main/java/org/qortal/api/restricted/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/restricted/resource/AdminResource.java @@ -257,6 +257,37 @@ public String restart(@HeaderParam(Security.API_KEY_HEADER) String apiKey) { return "true"; } + @GET + @Path("/bootstrap") + @Operation( + summary = "Bootstrap", + description = "Delete and download new database archive", + responses = { + @ApiResponse( + description = "\"true\"", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + @SecurityRequirement(name = "apiKey") + public String bootstrap(@HeaderParam(Security.API_KEY_HEADER) String apiKey) { + Security.checkApiCallAllowed(request); + + new Thread(() -> { + // Short sleep to allow HTTP response body to be emitted + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // Not important + } + + AutoUpdate.attemptBootstrap(); + + }).start(); + + return "true"; + } + @GET @Path("/summary") @Operation( diff --git a/src/main/java/org/qortal/controller/AutoUpdate.java b/src/main/java/org/qortal/controller/AutoUpdate.java index bc232e1bf..0bdcfd488 100644 --- a/src/main/java/org/qortal/controller/AutoUpdate.java +++ b/src/main/java/org/qortal/controller/AutoUpdate.java @@ -23,9 +23,12 @@ import java.io.OutputStream; import java.lang.management.ManagementFactory; import java.nio.ByteBuffer; +import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -365,4 +368,97 @@ public static boolean attemptRestart() { } } + public static boolean attemptBootstrap() { + LOGGER.info(String.format("Bootstrapping node...")); + + // Give repository a chance to backup in case things go badly wrong (if enabled) + if (Settings.getInstance().getRepositoryBackupInterval() > 0) { + try { + // Timeout if the database isn't ready for backing up after 60 seconds + long timeout = 60 * 1000L; + RepositoryManager.backup(true, "backup", timeout); + + } catch (TimeoutException e) { + LOGGER.info("Attempt to backup repository failed due to timeout: {}", e.getMessage()); + // Continue with the bootstrap anyway... + } + } + + // Get the repository path from settings + String repositoryPath = Settings.getInstance().getRepositoryPath(); + LOGGER.debug(String.format("Repository path: %s", repositoryPath)); + + // Call ApplyUpdate to end this process (unlocking current JAR so it can be replaced) + String javaHome = System.getProperty("java.home"); + LOGGER.debug(String.format("Java home: %s", javaHome)); + + Path javaBinary = Paths.get(javaHome, "bin", "java"); + LOGGER.debug(String.format("Java binary: %s", javaBinary)); + + try { + Path directory = Paths.get(repositoryPath); + Files.walkFileTree(directory, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + + List javaCmd = new ArrayList<>(); + + // Java runtime binary itself + javaCmd.add(javaBinary.toString()); + + // JVM arguments + javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments()); + + // Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port + javaCmd = javaCmd.stream() + .map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG)) + .collect(Collectors.toList()); + + // Remove JNI options as they won't be supported by command-line 'java' + // These are typically added by the AdvancedInstaller Java launcher EXE + javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf")); + + // Call ApplyUpdate using JAR + javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyUpdate.class.getCanonicalName())); + + // Add command-line args saved from start-up + String[] savedArgs = Controller.getInstance().getSavedArgs(); + if (savedArgs != null) + javaCmd.addAll(Arrays.asList(savedArgs)); + + LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd))); + + SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "AUTO_UPDATE"), //TODO + Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"), //TODO + MessageType.INFO); + + ProcessBuilder processBuilder = new ProcessBuilder(javaCmd); + + // New process will inherit our stdout and stderr + processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT); + processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT); + + Process process = processBuilder.start(); + + // Nothing to pipe to new process, so close output stream (process's stdin) + process.getOutputStream().close(); + + return true; // restarting node OK + } catch (Exception e) { + LOGGER.error(String.format("Failed to restart node: %s", e.getMessage())); + + return true; // repo was okay, even if applying update failed + } + } + }