diff --git a/src/main/java/org/qortal/ApplyBootstrap.java b/src/main/java/org/qortal/ApplyBootstrap.java new file mode 100644 index 00000000..547d8558 --- /dev/null +++ b/src/main/java/org/qortal/ApplyBootstrap.java @@ -0,0 +1,227 @@ +package org.qortal; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.qortal.api.ApiKey; +import org.qortal.api.ApiRequest; +import org.qortal.controller.BootstrapNode; +import org.qortal.settings.Settings; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +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.Security; +import java.util.*; +import java.util.stream.Collectors; + +import static org.qortal.controller.BootstrapNode.AGENTLIB_JVM_HOLDER_ARG; + +public class ApplyBootstrap { + + static { + // This static block will be called before others if using ApplyBootstrap.main() + + // Log into different files for bootstrap - this has to be before LogManger.getLogger() calls + System.setProperty("log4j2.filenameTemplate", "log-apply-bootstrap.txt"); + + // This must go before any calls to LogManager/Logger + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + } + + private static final Logger LOGGER = LogManager.getLogger(ApplyBootstrap.class); + private static final String JAR_FILENAME = BootstrapNode.JAR_FILENAME; + private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe"; + private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS"; + private static final String JAVA_TOOL_OPTIONS_VALUE = ""; + + private static final long CHECK_INTERVAL = 15 * 1000L; // ms + private static final int MAX_ATTEMPTS = 20; + + public static void main(String[] args) { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); + + // Load/check settings, which potentially sets up blockchain config, etc. + if (args.length > 0) + Settings.fileInstance(args[0]); + else + Settings.getInstance(); + + LOGGER.info("Applying bootstrap..."); + + // Shutdown node using API + if (!shutdownNode()) + return; + + // Delete db + deleteDB(); + + // Restart node + restartNode(args); + + LOGGER.info("Bootstrapping..."); + } + + private static boolean shutdownNode() { + String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/"; + LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri)); + + // The /admin/stop endpoint requires an API key, which may or may not be already generated + boolean apiKeyNewlyGenerated = false; + ApiKey apiKey = null; + try { + apiKey = new ApiKey(); + if (!apiKey.generated()) { + apiKey.generate(); + apiKeyNewlyGenerated = true; + LOGGER.info("Generated API key"); + } + } catch (IOException e) { + LOGGER.info("Error loading API key: {}", e.getMessage()); + } + + // Create GET params + Map params = new HashMap<>(); + if (apiKey != null) { + params.put("apiKey", apiKey.toString()); + } + + // Attempt to stop the node + int attempt; + for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) { + final int attemptForLogging = attempt; + LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS)); + String response = ApiRequest.perform(baseUri + "admin/stop", params); + if (response == null) { + // No response - consider node shut down + if (apiKeyNewlyGenerated) { + // API key was newly generated for bootstrapping node, so we need to remove it + ApplyBootstrap.removeGeneratedApiKey(); + } + return true; + } + + LOGGER.info(() -> String.format("Response from API: %s", response)); + + try { + Thread.sleep(CHECK_INTERVAL); + } catch (InterruptedException e) { + // We still need to check... + break; + } + } + + if (apiKeyNewlyGenerated) { + // API key was newly generated for bootstrapping node, so we need to remove it + ApplyBootstrap.removeGeneratedApiKey(); + } + + if (attempt == MAX_ATTEMPTS) { + LOGGER.error("Failed to shutdown node - giving up"); + return false; + } + + return true; + } + + private static void removeGeneratedApiKey() { + try { + LOGGER.info("Removing newly generated API key..."); + + // Delete the API key since it was only generated for bootstrapping node + ApiKey apiKey = new ApiKey(); + apiKey.delete(); + + } catch (IOException e) { + LOGGER.info("Error loading or deleting API key: {}", e.getMessage()); + } + } + + private static void deleteDB() { + // Get the repository path from settings + String repositoryPath = Settings.getInstance().getRepositoryPath(); + LOGGER.info(String.format("Repository path: %s", repositoryPath)); + + 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; + } + }); + } catch (IOException e) { + LOGGER.info("Error deleting DB: {}", e.getMessage()); + } + } + + private static void restartNode(String[] args) { + String javaHome = System.getProperty("java.home"); + LOGGER.info(() -> String.format("Java home: %s", javaHome)); + + Path javaBinary = Paths.get(javaHome, "bin", "java"); + LOGGER.info(() -> String.format("Java binary: %s", javaBinary)); + + Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER); + LOGGER.info(() -> String.format("Windows EXE launcher: %s", exeLauncher)); + + List javaCmd; + if (Files.exists(exeLauncher)) { + javaCmd = Arrays.asList(exeLauncher.toString()); + } else { + javaCmd = new ArrayList<>(); + // Java runtime binary itself + javaCmd.add(javaBinary.toString()); + + // JVM arguments + javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments()); + + // Reapply any retained, but disabled, -agentlib JVM arg + javaCmd = javaCmd.stream() + .map(arg -> arg.replace(AGENTLIB_JVM_HOLDER_ARG, "-agentlib")) + .collect(Collectors.toList()); + + // Call mainClass in JAR + javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME)); + + // Add saved command-line args + javaCmd.addAll(Arrays.asList(args)); + } + + try { + LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd))); + + ProcessBuilder processBuilder = new ProcessBuilder(javaCmd); + + if (Files.exists(exeLauncher)) { + LOGGER.info(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE)); + processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE); + } + + // 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(); + } catch (Exception e) { + LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage())); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/ApplyRestart.java b/src/main/java/org/qortal/ApplyRestart.java new file mode 100644 index 00000000..4a7ad473 --- /dev/null +++ b/src/main/java/org/qortal/ApplyRestart.java @@ -0,0 +1,196 @@ +package org.qortal; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.qortal.api.ApiKey; +import org.qortal.api.ApiRequest; +import org.qortal.controller.RestartNode; +import org.qortal.settings.Settings; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.Security; +import java.util.*; +import java.util.stream.Collectors; + +import static org.qortal.controller.RestartNode.AGENTLIB_JVM_HOLDER_ARG; + +public class ApplyRestart { + + static { + // This static block will be called before others if using ApplyRestart.main() + + // Log into different files for restart node - this has to be before LogManger.getLogger() calls + System.setProperty("log4j2.filenameTemplate", "log-apply-restart.txt"); + + // This must go before any calls to LogManager/Logger + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + } + + private static final Logger LOGGER = LogManager.getLogger(ApplyRestart.class); + private static final String JAR_FILENAME = RestartNode.JAR_FILENAME; + private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe"; + private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS"; + private static final String JAVA_TOOL_OPTIONS_VALUE = ""; + + private static final long CHECK_INTERVAL = 10 * 1000L; // ms + private static final int MAX_ATTEMPTS = 12; + + public static void main(String[] args) { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); + + // Load/check settings, which potentially sets up blockchain config, etc. + if (args.length > 0) + Settings.fileInstance(args[0]); + else + Settings.getInstance(); + + LOGGER.info("Applying restart..."); + + // Shutdown node using API + if (!shutdownNode()) + return; + + // Restart node + restartNode(args); + + LOGGER.info("Restarting..."); + } + + private static boolean shutdownNode() { + String baseUri = "http://localhost:" + Settings.getInstance().getApiPort() + "/"; + LOGGER.info(() -> String.format("Shutting down node using API via %s", baseUri)); + + // The /admin/stop endpoint requires an API key, which may or may not be already generated + boolean apiKeyNewlyGenerated = false; + ApiKey apiKey = null; + try { + apiKey = new ApiKey(); + if (!apiKey.generated()) { + apiKey.generate(); + apiKeyNewlyGenerated = true; + LOGGER.info("Generated API key"); + } + } catch (IOException e) { + LOGGER.info("Error loading API key: {}", e.getMessage()); + } + + // Create GET params + Map params = new HashMap<>(); + if (apiKey != null) { + params.put("apiKey", apiKey.toString()); + } + + // Attempt to stop the node + int attempt; + for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) { + final int attemptForLogging = attempt; + LOGGER.info(() -> String.format("Attempt #%d out of %d to shutdown node", attemptForLogging + 1, MAX_ATTEMPTS)); + String response = ApiRequest.perform(baseUri + "admin/stop", params); + if (response == null) { + // No response - consider node shut down + if (apiKeyNewlyGenerated) { + // API key was newly generated for restarting node, so we need to remove it + ApplyRestart.removeGeneratedApiKey(); + } + return true; + } + + LOGGER.info(() -> String.format("Response from API: %s", response)); + + try { + Thread.sleep(CHECK_INTERVAL); + } catch (InterruptedException e) { + // We still need to check... + break; + } + } + + if (apiKeyNewlyGenerated) { + // API key was newly generated for restarting node, so we need to remove it + ApplyRestart.removeGeneratedApiKey(); + } + + if (attempt == MAX_ATTEMPTS) { + LOGGER.error("Failed to shutdown node - giving up"); + return false; + } + + return true; + } + + private static void removeGeneratedApiKey() { + try { + LOGGER.info("Removing newly generated API key..."); + + // Delete the API key since it was only generated for restarting node + ApiKey apiKey = new ApiKey(); + apiKey.delete(); + + } catch (IOException e) { + LOGGER.info("Error loading or deleting API key: {}", e.getMessage()); + } + } + + private static void restartNode(String[] args) { + String javaHome = System.getProperty("java.home"); + LOGGER.info(() -> String.format("Java home: %s", javaHome)); + + Path javaBinary = Paths.get(javaHome, "bin", "java"); + LOGGER.info(() -> String.format("Java binary: %s", javaBinary)); + + Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER); + LOGGER.info(() -> String.format("Windows EXE launcher: %s", exeLauncher)); + + List javaCmd; + if (Files.exists(exeLauncher)) { + javaCmd = Arrays.asList(exeLauncher.toString()); + } else { + javaCmd = new ArrayList<>(); + // Java runtime binary itself + javaCmd.add(javaBinary.toString()); + + // JVM arguments + javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments()); + + // Reapply any retained, but disabled, -agentlib JVM arg + javaCmd = javaCmd.stream() + .map(arg -> arg.replace(AGENTLIB_JVM_HOLDER_ARG, "-agentlib")) + .collect(Collectors.toList()); + + // Call mainClass in JAR + javaCmd.addAll(Arrays.asList("-jar", JAR_FILENAME)); + + // Add saved command-line args + javaCmd.addAll(Arrays.asList(args)); + } + + try { + LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd))); + + ProcessBuilder processBuilder = new ProcessBuilder(javaCmd); + + if (Files.exists(exeLauncher)) { + LOGGER.info(() -> String.format("Setting env %s to %s", JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE)); + processBuilder.environment().put(JAVA_TOOL_OPTIONS_NAME, JAVA_TOOL_OPTIONS_VALUE); + } + + // 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(); + } catch (Exception e) { + LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage())); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/ApplyUpdate.java b/src/main/java/org/qortal/ApplyUpdate.java index f1f9d22f..768b1854 100644 --- a/src/main/java/org/qortal/ApplyUpdate.java +++ b/src/main/java/org/qortal/ApplyUpdate.java @@ -38,7 +38,7 @@ public class ApplyUpdate { private static final String NEW_JAR_FILENAME = AutoUpdate.NEW_JAR_FILENAME; private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe"; private static final String JAVA_TOOL_OPTIONS_NAME = "JAVA_TOOL_OPTIONS"; - private static final String JAVA_TOOL_OPTIONS_VALUE = "-XX:MaxRAMFraction=4"; + private static final String JAVA_TOOL_OPTIONS_VALUE = ""; private static final long CHECK_INTERVAL = 30 * 1000L; // ms private static final int MAX_ATTEMPTS = 12; 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 35bf9108..301e6562 100644 --- a/src/main/java/org/qortal/api/restricted/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/restricted/resource/AdminResource.java @@ -25,7 +25,9 @@ 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.BootstrapNode; import org.qortal.controller.Controller; +import org.qortal.controller.RestartNode; import org.qortal.controller.Synchronizer; import org.qortal.controller.Synchronizer.SynchronizationResult; import org.qortal.controller.repository.BlockArchiveRebuilder; @@ -250,7 +252,7 @@ public class AdminResource { // Not important } - AutoUpdate.attemptRestart(); + RestartNode.attemptToRestart(); }).start(); @@ -281,7 +283,7 @@ public class AdminResource { // Not important } - AutoUpdate.attemptBootstrap(); + BootstrapNode.attemptToBootstrap(); }).start(); diff --git a/src/main/java/org/qortal/controller/AutoUpdate.java b/src/main/java/org/qortal/controller/AutoUpdate.java index 0bdcfd48..4b315651 100644 --- a/src/main/java/org/qortal/controller/AutoUpdate.java +++ b/src/main/java/org/qortal/controller/AutoUpdate.java @@ -23,12 +23,9 @@ import java.io.InputStream; 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; @@ -294,171 +291,4 @@ public class AutoUpdate extends Thread { return true; // repo was okay, even if applying update failed } } - - 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 - } - } - - 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 - } - } - } diff --git a/src/main/java/org/qortal/controller/BootstrapNode.java b/src/main/java/org/qortal/controller/BootstrapNode.java new file mode 100644 index 00000000..9d0f8b36 --- /dev/null +++ b/src/main/java/org/qortal/controller/BootstrapNode.java @@ -0,0 +1,103 @@ +package org.qortal.controller; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.ApplyBootstrap; +import org.qortal.globalization.Translator; +import org.qortal.gui.SysTray; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; + +import java.awt.TrayIcon.MessageType; +import java.lang.management.ManagementFactory; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +/* NOTE: It is CRITICAL that we use OpenJDK and not Java SE because our uber jar repacks BouncyCastle which, in turn, unsigns BC causing it to be rejected as a security provider by Java SE. */ + +public class BootstrapNode { + + public static final String JAR_FILENAME = "qortal.jar"; + public static final String AGENTLIB_JVM_HOLDER_ARG = "-DQORTAL_agentlib="; + + private static final Logger LOGGER = LogManager.getLogger(BootstrapNode.class); + + public static boolean attemptToBootstrap() { + 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... + } + } + + // Call ApplyBootstrap to end this process + 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 ApplyBootstrap using JAR + javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyBootstrap.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", "BOOTSTRAP_NODE"), + Translator.INSTANCE.translate("SysTray", "APPLYING_BOOTSTRAP_AND_RESTARTING"), + 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 bootstrap failed + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/controller/RestartNode.java b/src/main/java/org/qortal/controller/RestartNode.java new file mode 100644 index 00000000..4a64a87f --- /dev/null +++ b/src/main/java/org/qortal/controller/RestartNode.java @@ -0,0 +1,102 @@ +package org.qortal.controller; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.ApplyRestart;; +import org.qortal.globalization.Translator; +import org.qortal.gui.SysTray; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; + +import java.awt.TrayIcon.MessageType; +import java.lang.management.ManagementFactory; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +/* NOTE: It is CRITICAL that we use OpenJDK and not Java SE because our uber jar repacks BouncyCastle which, in turn, unsigns BC causing it to be rejected as a security provider by Java SE. */ + +public class RestartNode { + + public static final String JAR_FILENAME = "qortal.jar"; + public static final String AGENTLIB_JVM_HOLDER_ARG = "-DQORTAL_agentlib="; + + private static final Logger LOGGER = LogManager.getLogger(RestartNode.class); + + public static boolean attemptToRestart() { + 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 ApplyRestart to end this process + 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 ApplyRestart using JAR + javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyRestart.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", "RESTARTING_NODE"), + Translator.INSTANCE.translate("SysTray", "APPLYING_RESTARTING_NODE"), + 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 restart failed + } + } +} \ No newline at end of file