From 7bb6b84e86dae04fe1ca1eebbd8385660d8a0ba0 Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Thu, 20 Apr 2023 16:23:57 -0400 Subject: [PATCH 1/3] Added API call for restarting node --- .../qortal/api/resource/AdminResource.java | 32 ++++++++ .../org/qortal/controller/AutoUpdate.java | 73 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/src/main/java/org/qortal/api/resource/AdminResource.java b/src/main/java/org/qortal/api/resource/AdminResource.java index 154f9159..1f516633 100644 --- a/src/main/java/org/qortal/api/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/resource/AdminResource.java @@ -42,6 +42,7 @@ import org.qortal.api.model.ActivitySummary; import org.qortal.api.model.NodeInfo; import org.qortal.api.model.NodeStatus; import org.qortal.block.BlockChain; +import org.qortal.controller.AutoUpdate; import org.qortal.controller.Controller; import org.qortal.controller.Synchronizer; import org.qortal.controller.Synchronizer.SynchronizationResult; @@ -199,6 +200,37 @@ public class AdminResource { return "true"; } + @GET + @Path("/restart") + @Operation( + summary = "Restart", + description = "Restart", + responses = { + @ApiResponse( + description = "\"true\"", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + @SecurityRequirement(name = "apiKey") + public String restart(@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.attemptRestart(); + + }).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 2ec7c94a..fde52fb1 100644 --- a/src/main/java/org/qortal/controller/AutoUpdate.java +++ b/src/main/java/org/qortal/controller/AutoUpdate.java @@ -293,4 +293,77 @@ public class AutoUpdate extends Thread { } } + public static boolean attemptRestart() { + LOGGER.info(String.format("Restarting 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 node restart anyway... + } + } + + // 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 { + 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 + } + } + } From 85980e4cfca3eee1801ba79046900807751fede8 Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Thu, 20 Apr 2023 16:41:47 -0400 Subject: [PATCH 2/3] Removed 3rd-party swagger server validation --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 083901a6..7ecb7b44 100644 --- a/pom.xml +++ b/pom.xml @@ -147,6 +147,7 @@ tagsSorter: "alpha", operationsSorter: "alpha", + validatorUrl: false, From 0993903aa0c97aca58b03c47f39df571624a3a2a Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 21 Apr 2023 11:03:24 +0100 Subject: [PATCH 3/3] Added `GET /settings/{setting}` endpoint Based on work by @QuickMythril, but modified to be generic. --- .../qortal/api/resource/AdminResource.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/main/java/org/qortal/api/resource/AdminResource.java b/src/main/java/org/qortal/api/resource/AdminResource.java index 1f516633..fa10c90d 100644 --- a/src/main/java/org/qortal/api/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/resource/AdminResource.java @@ -20,6 +20,7 @@ import java.time.LocalDate; import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -31,10 +32,13 @@ import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.appender.RollingFileAppender; +import org.json.JSONArray; +import org.json.JSONObject; import org.qortal.account.Account; import org.qortal.account.PrivateKeyAccount; import org.qortal.api.*; @@ -170,6 +174,37 @@ public class AdminResource { return nodeSettings; } + @GET + @Path("/settings/{setting}") + @Operation( + summary = "Fetch a single node setting", + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + public Object setting(@PathParam("setting") String setting) { + try { + Object settingValue = FieldUtils.readField(Settings.getInstance(), setting, true); + if (settingValue == null) { + return "null"; + } + else if (settingValue instanceof String[]) { + JSONArray array = new JSONArray(settingValue); + return array.toString(4); + } + else if (settingValue instanceof List) { + JSONArray array = new JSONArray((List) settingValue); + return array.toString(4); + } + return settingValue; + + } catch (IllegalAccessException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e); + } + } + @GET @Path("/stop") @Operation(