diff --git a/pom.xml b/pom.xml index bff266a4..2475c9b4 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,7 @@ 1.17.1 5.10.0 1.0.0 - 2.21.1 + 2.22.0 1.5.0-b01 3.12.1 3.3.0 @@ -46,7 +46,7 @@ 3.2.3 1.1.0 UTF-8 - 3.25.0 + 3.25.1 1.5.3 0.16 1.17 diff --git a/src/main/java/org/qortal/ApplyBootstrap.java b/src/main/java/org/qortal/ApplyBootstrap.java new file mode 100644 index 00000000..9b370f7a --- /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.debug(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.error("Error deleting DB: {}", e.getMessage()); + } + } + + private static void restartNode(String[] args) { + 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)); + + Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER); + LOGGER.debug(() -> 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.debug(() -> 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..70d07df5 --- /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.debug(() -> String.format("Java home: %s", javaHome)); + + Path javaBinary = Paths.get(javaHome, "bin", "java"); + LOGGER.debug(() -> String.format("Java binary: %s", javaBinary)); + + Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER); + LOGGER.debug(() -> 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.debug(String.format("Restarting node with: %s", String.join(" ", javaCmd))); + + ProcessBuilder processBuilder = new ProcessBuilder(javaCmd); + + if (Files.exists(exeLauncher)) { + LOGGER.debug(() -> 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..ba2e0860 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; @@ -139,7 +139,7 @@ public class ApplyUpdate { apiKey.delete(); } catch (IOException e) { - LOGGER.info("Error loading or deleting API key: {}", e.getMessage()); + LOGGER.error("Error loading or deleting API key: {}", e.getMessage()); } } @@ -181,13 +181,13 @@ public class ApplyUpdate { private static void restartNode(String[] args) { String javaHome = System.getProperty("java.home"); - LOGGER.info(() -> String.format("Java home: %s", javaHome)); + LOGGER.debug(() -> String.format("Java home: %s", javaHome)); Path javaBinary = Paths.get(javaHome, "bin", "java"); - LOGGER.info(() -> String.format("Java binary: %s", javaBinary)); + LOGGER.debug(() -> String.format("Java binary: %s", javaBinary)); Path exeLauncher = Paths.get(WINDOWS_EXE_LAUNCHER); - LOGGER.info(() -> String.format("Windows EXE launcher: %s", exeLauncher)); + LOGGER.debug(() -> String.format("Windows EXE launcher: %s", exeLauncher)); List javaCmd; if (Files.exists(exeLauncher)) { @@ -213,12 +213,12 @@ public class ApplyUpdate { } try { - LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd))); + LOGGER.debug(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)); + LOGGER.debug(() -> 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); } 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..837288e5 100644 --- a/src/main/java/org/qortal/api/restricted/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/restricted/resource/AdminResource.java @@ -24,8 +24,9 @@ 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.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 +251,7 @@ public class AdminResource { // Not important } - AutoUpdate.attemptRestart(); + RestartNode.attemptToRestart(); }).start(); @@ -281,7 +282,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..b8d81b85 --- /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.debug(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 diff --git a/src/main/resources/i18n/SysTray_de.properties b/src/main/resources/i18n/SysTray_de.properties index c92130f1..4f09a3a2 100644 --- a/src/main/resources/i18n/SysTray_de.properties +++ b/src/main/resources/i18n/SysTray_de.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = Synchronisiere Uhrzeit SYNCHRONIZING_BLOCKCHAIN = Synchronisiere SYNCHRONIZING_CLOCK = Uhrzeit wird synchronisiert + +RESTARTING_NODE = Knoten wird neu gestartet + +APPLYING_RESTARTING_NODE = Neustart des knotens wird angewendet. Bitte haben Sie Geduld... + +BOOTSTRAP_NODE = Bootstrapping-Knoten + +APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap anwenden und Knoten neu starten. Bitte haben Sie Geduld... diff --git a/src/main/resources/i18n/SysTray_en.properties b/src/main/resources/i18n/SysTray_en.properties index 39940be0..afc8210c 100644 --- a/src/main/resources/i18n/SysTray_en.properties +++ b/src/main/resources/i18n/SysTray_en.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = Synchronize clock SYNCHRONIZING_BLOCKCHAIN = Synchronizing SYNCHRONIZING_CLOCK = Synchronizing clock + +RESTARTING_NODE = Restarting Node + +APPLYING_RESTARTING_NODE = Applying restarting node. Please be patient... + +BOOTSTRAP_NODE = Bootstrapping Node + +APPLYING_BOOTSTRAP_AND_RESTARTING = Applying bootstrap and restarting node. Please be patient... diff --git a/src/main/resources/i18n/SysTray_es.properties b/src/main/resources/i18n/SysTray_es.properties index 36cbb22c..cc0ba7b2 100644 --- a/src/main/resources/i18n/SysTray_es.properties +++ b/src/main/resources/i18n/SysTray_es.properties @@ -44,3 +44,11 @@ SYNCHRONIZE_CLOCK = Sincronizar reloj SYNCHRONIZING_BLOCKCHAIN = Sincronizando SYNCHRONIZING_CLOCK = Sincronizando reloj + +RESTARTING_NODE = Reiniciando el nodo + +APPLYING_RESTARTING_NODE = Aplicando el nodo de reinicio. Por favor sea paciente... + +BOOTSTRAP_NODE = Nodo de arranque + +APPLYING_BOOTSTRAP_AND_RESTARTING = Aplicando bootstrap y reiniciando el nodo. Por favor sea paciente... diff --git a/src/main/resources/i18n/SysTray_fi.properties b/src/main/resources/i18n/SysTray_fi.properties index 4038d615..351bc921 100644 --- a/src/main/resources/i18n/SysTray_fi.properties +++ b/src/main/resources/i18n/SysTray_fi.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = Synkronisoi kello SYNCHRONIZING_BLOCKCHAIN = Synkronisoi SYNCHRONIZING_CLOCK = Synkronisoi kelloa + +RESTARTING_NODE = Käynnistetään uudelleen solmu + +APPLYING_RESTARTING_NODE = Käytetään uudelleenkäynnistyssolmua. Olkaa kärsivällisiä... + +BOOTSTRAP_NODE = Käynnistyssolmu + +APPLYING_BOOTSTRAP_AND_RESTARTING = Käynnistetään ja käynnistetään solmu uudelleen. Olkaa kärsivällisiä... diff --git a/src/main/resources/i18n/SysTray_fr.properties b/src/main/resources/i18n/SysTray_fr.properties index 2e376842..39ac44e3 100644 --- a/src/main/resources/i18n/SysTray_fr.properties +++ b/src/main/resources/i18n/SysTray_fr.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = Mettre l'heure à jour SYNCHRONIZING_BLOCKCHAIN = Synchronisation SYNCHRONIZING_CLOCK = Synchronisation de l'heure + +RESTARTING_NODE = Redémarrage du nœud + +APPLYING_RESTARTING_NODE = Application du redémarrage du nœud. S'il vous plaît, soyez patient... + +BOOTSTRAP_NODE = Nœud d'amorçage + +APPLYING_BOOTSTRAP_AND_RESTARTING = Application du bootstrap et redémarrage du nœud. S'il vous plaît, soyez patient... diff --git a/src/main/resources/i18n/SysTray_he.properties b/src/main/resources/i18n/SysTray_he.properties index 50ef8933..09a9f6dd 100644 --- a/src/main/resources/i18n/SysTray_he.properties +++ b/src/main/resources/i18n/SysTray_he.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = סנכרן שעון SYNCHRONIZING_BLOCKCHAIN ​​= מסנכרן SYNCHRONIZING_CLOCK = מסנכרן שעון + +RESTARTING_NODE = הפעלה מחדש של צומת + +APPLYING_RESTARTING_NODE = החלת צומת הפעלה מחדש. אנא התאזר בסבלנות... + +BOOTSTRAP_NODE = צומת אתחול + +APPLYING_BOOTSTRAP_AND_RESTARTING = החלת אתחול והפעלת צומת diff --git a/src/main/resources/i18n/SysTray_hu.properties b/src/main/resources/i18n/SysTray_hu.properties index 74ab21ac..3b1da2cc 100644 --- a/src/main/resources/i18n/SysTray_hu.properties +++ b/src/main/resources/i18n/SysTray_hu.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = Óra-szinkronizálás megkezdése SYNCHRONIZING_BLOCKCHAIN = Szinkronizálás SYNCHRONIZING_CLOCK = Óraszinkronizálás folyamatban + +RESTARTING_NODE = Csomópont újraindítása + +APPLYING_RESTARTING_NODE = Újraindító csomópont alkalmazása. Kérjük várjon... + +BOOTSTRAP_NODE = Rendszerindítási csomópont + +APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap alkalmazása és csomópont újraindítása. Kérjük várjon... diff --git a/src/main/resources/i18n/SysTray_it.properties b/src/main/resources/i18n/SysTray_it.properties index d966d825..55cd4c83 100644 --- a/src/main/resources/i18n/SysTray_it.properties +++ b/src/main/resources/i18n/SysTray_it.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = Sincronizzare l'orologio SYNCHRONIZING_BLOCKCHAIN = Sincronizzazione della blockchain SYNCHRONIZING_CLOCK = Sincronizzazione orologio + +RESTARTING_NODE = Riavvio del nodo + +APPLYING_RESTARTING_NODE = Applicazione del nodo di riavvio. Per favore sii paziente... + +BOOTSTRAP_NODE = Nodo di bootstrap + +APPLYING_BOOTSTRAP_AND_RESTARTING = Applicazione del bootstrap e riavvio del nodo. Per favore sii paziente... diff --git a/src/main/resources/i18n/SysTray_jp.properties b/src/main/resources/i18n/SysTray_jp.properties index c4cccb5b..036bf8f2 100644 --- a/src/main/resources/i18n/SysTray_jp.properties +++ b/src/main/resources/i18n/SysTray_jp.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = 時刻を同期 SYNCHRONIZING_BLOCKCHAIN = ブロックチェーンを同期中 SYNCHRONIZING_CLOCK = 時刻を同期中 + +RESTARTING_NODE = ノードを再起動しています + +APPLYING_RESTARTING_NODE = 再起動ノードを適用しています。 しばらくお待ちください... + +BOOTSTRAP_NODE = ブートストラップ ノード + +APPLYING_BOOTSTRAP_AND_RESTARTING = ブートストラップを適用し、ノードを再起動します。 しばらくお待ちください... diff --git a/src/main/resources/i18n/SysTray_ko.properties b/src/main/resources/i18n/SysTray_ko.properties index dc6cb69b..0db49c53 100644 --- a/src/main/resources/i18n/SysTray_ko.properties +++ b/src/main/resources/i18n/SysTray_ko.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = 시간 동기화 SYNCHRONIZING_BLOCKCHAIN = 동기화중 SYNCHRONIZING_CLOCK = 시간 동기화 + +RESTARTING_NODE = 노드 다시 시작 중 + +APPLYING_RESTARTING_NODE = 노드 재시작을 적용합니다. 기다려주십시오... + +BOOTSTRAP_NODE = 부트스트래핑 노드 + +APPLYING_BOOTSTRAP_AND_RESTARTING = 부트스트랩을 적용하고 노드를 다시 시작합니다. 기다려주십시오... diff --git a/src/main/resources/i18n/SysTray_nl.properties b/src/main/resources/i18n/SysTray_nl.properties index 3d7de024..776720d5 100644 --- a/src/main/resources/i18n/SysTray_nl.properties +++ b/src/main/resources/i18n/SysTray_nl.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = Synchronizeer klok SYNCHRONIZING_BLOCKCHAIN = Bezig met synchronizeren SYNCHRONIZING_CLOCK = Klok wordt gesynchronizeerd + +RESTARTING_NODE = Knooppunt opnieuw starten + +APPLYING_RESTARTING_NODE = Herstartknooppunt toepassen. Wees alstublieft geduldig... + +BOOTSTRAP_NODE = Opstartknooppunt + +APPLYING_BOOTSTRAP_AND_RESTARTING = Bootstrap toepassen en knooppunt opnieuw starten. Wees alstublieft geduldig... diff --git a/src/main/resources/i18n/SysTray_pl.properties b/src/main/resources/i18n/SysTray_pl.properties index 84740da0..8ee40751 100644 --- a/src/main/resources/i18n/SysTray_pl.properties +++ b/src/main/resources/i18n/SysTray_pl.properties @@ -44,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synchronizuj zegar SYNCHRONIZING_BLOCKCHAIN = Synchronizacja SYNCHRONIZING_CLOCK = Synchronizacja zegara + +RESTARTING_NODE = Ponowne uruchamianie węzła + +APPLYING_RESTARTING_NODE = Stosuję ponowne uruchomienie węzła. Proszę być cierpliwym... + +BOOTSTRAP_NODE = Węzeł ładowania początkowego + +APPLYING_BOOTSTRAP_AND_RESTARTING = Stosowanie ładowania początkowego i ponowne uruchamianie węzła. Proszę być cierpliwym... diff --git a/src/main/resources/i18n/SysTray_ro.properties b/src/main/resources/i18n/SysTray_ro.properties index 4130bbcb..f3cf76c9 100644 --- a/src/main/resources/i18n/SysTray_ro.properties +++ b/src/main/resources/i18n/SysTray_ro.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = Sincronizare ceas SYNCHRONIZING_BLOCKCHAIN = Sincronizare SYNCHRONIZING_CLOCK = Se sincronizeaza ceasul + +RESTARTING_NODE = Repornirea nodului + +APPLYING_RESTARTING_NODE = Se aplica nodul de repornire. Te rog fii rabdator... + +BOOTSTRAP_NODE = Nod de bootstrap + +APPLYING_BOOTSTRAP_AND_RESTARTING = Se aplica bootstrap si se reporneste nodul. Te rog fii rabdator... diff --git a/src/main/resources/i18n/SysTray_ru.properties b/src/main/resources/i18n/SysTray_ru.properties index c8615f73..ddf0c0a0 100644 --- a/src/main/resources/i18n/SysTray_ru.properties +++ b/src/main/resources/i18n/SysTray_ru.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = Синхронизировать время SYNCHRONIZING_BLOCKCHAIN = Синхронизация цепи SYNCHRONIZING_CLOCK = Проверка времени + +RESTARTING_NODE = Перезапуск узла + +APPLYING_RESTARTING_NODE = Применение перезапуска узла. Пожалуйста, будьте терпеливы... + +BOOTSTRAP_NODE = Узел начальной загрузки + +APPLYING_BOOTSTRAP_AND_RESTARTING = Применение начальной загрузки и перезапуск узла. Пожалуйста, будьте терпеливы... diff --git a/src/main/resources/i18n/SysTray_sv.properties b/src/main/resources/i18n/SysTray_sv.properties index 96f291b5..a38cca3a 100644 --- a/src/main/resources/i18n/SysTray_sv.properties +++ b/src/main/resources/i18n/SysTray_sv.properties @@ -44,3 +44,11 @@ SYNCHRONIZE_CLOCK = Synkronisera klockan SYNCHRONIZING_BLOCKCHAIN = Synkroniserar SYNCHRONIZING_CLOCK = Synkroniserar klockan + +RESTARTING_NODE = Mimitian deui Node + +APPLYING_RESTARTING_NODE = Nerapkeun titik ngamimitian deui. Mangga sing sabar... + +BOOTSTRAP_NODE = Bootstrapping Node + +APPLYING_BOOTSTRAP_AND_RESTARTING = Nerapkeun bootstrap sareng hurungkeun deui titik. Mangga sing sabar... diff --git a/src/main/resources/i18n/SysTray_zh_CN.properties b/src/main/resources/i18n/SysTray_zh_CN.properties index d6848a7c..7e84f3f2 100644 --- a/src/main/resources/i18n/SysTray_zh_CN.properties +++ b/src/main/resources/i18n/SysTray_zh_CN.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = 同步时钟 SYNCHRONIZING_BLOCKCHAIN = 正在同步区块链 SYNCHRONIZING_CLOCK = 正在同步时钟 + +RESTARTING_NODE = 重新启动节点 + +APPLYING_RESTARTING_NODE = 应用重新启动节点。 请耐心等待... + +BOOTSTRAP_NODE = 引导节点 + +APPLYING_BOOTSTRAP_AND_RESTARTING = 应用引导程序并重新启动节点。 请耐心等待... diff --git a/src/main/resources/i18n/SysTray_zh_TW.properties b/src/main/resources/i18n/SysTray_zh_TW.properties index eabdbb63..d774bc11 100644 --- a/src/main/resources/i18n/SysTray_zh_TW.properties +++ b/src/main/resources/i18n/SysTray_zh_TW.properties @@ -46,3 +46,11 @@ SYNCHRONIZE_CLOCK = 同步時鐘 SYNCHRONIZING_BLOCKCHAIN = 正在同步區塊鏈 SYNCHRONIZING_CLOCK = 正在同步時鐘 + +RESTARTING_NODE = 重新啟動節點 + +APPLYING_RESTARTING_NODE = 應用重新啟動節點。 請耐心等待... + +BOOTSTRAP_NODE = 引導節點 + +APPLYING_BOOTSTRAP_AND_RESTARTING = 應用引導程式並重新啟動節點。 請耐心等待...