forked from Qortal/qortal
Another attempt at auto-update
This commit is contained in:
parent
63a36073ec
commit
20aa49a1f1
116
src/main/java/org/qora/ApplyUpdate.java
Normal file
116
src/main/java/org/qora/ApplyUpdate.java
Normal file
@ -0,0 +1,116 @@
|
||||
package org.qora;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.security.Security;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
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.qora.api.ApiRequest;
|
||||
import org.qora.controller.AutoUpdate;
|
||||
import org.qora.settings.Settings;
|
||||
|
||||
public class ApplyUpdate {
|
||||
|
||||
static {
|
||||
// This static block will be called before others if using ApplyUpdate.main()
|
||||
|
||||
// Log into different files for auto-update - this has to be before LogManger.getLogger() calls
|
||||
System.setProperty("log4j2.filenameTemplate", "log-apply-update.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(ApplyUpdate.class);
|
||||
private static final String JAR_FILENAME = AutoUpdate.JAR_FILENAME;
|
||||
private static final String NEW_JAR_FILENAME = AutoUpdate.NEW_JAR_FILENAME;
|
||||
|
||||
private static final long CHECK_INTERVAL = 5 * 1000; // ms
|
||||
private static final int MAX_ATTEMPTS = 5;
|
||||
|
||||
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.
|
||||
Settings.getInstance();
|
||||
|
||||
LOGGER.info("Applying update...");
|
||||
|
||||
// Shutdown node using API
|
||||
if (!shutdownNode())
|
||||
return;
|
||||
|
||||
// Replace JAR
|
||||
replaceJar();
|
||||
|
||||
// Restart node
|
||||
restartNode();
|
||||
}
|
||||
|
||||
private static boolean shutdownNode() {
|
||||
String BASE_URI = "http://localhost:" + Settings.getInstance().getApiPort() + "/";
|
||||
LOGGER.info(String.format("Shutting down node using API via %s", BASE_URI));
|
||||
|
||||
int attempt;
|
||||
for (attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
|
||||
LOGGER.debug(String.format("Attempt #%d out of %d to shutdown node", attempt + 1, MAX_ATTEMPTS));
|
||||
String response = ApiRequest.perform(BASE_URI + "admin/stop", null);
|
||||
if (response != null)
|
||||
break;
|
||||
|
||||
try {
|
||||
Thread.sleep(CHECK_INTERVAL);
|
||||
} catch (InterruptedException e) {
|
||||
// We still need to check...
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (attempt == MAX_ATTEMPTS) {
|
||||
LOGGER.error("Failed to shutdown node - giving up");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void replaceJar() {
|
||||
// Assuming current working directory contains the JAR files
|
||||
Path realJar = Paths.get(JAR_FILENAME);
|
||||
Path newJar = Paths.get(NEW_JAR_FILENAME);
|
||||
|
||||
try {
|
||||
Files.copy(newJar, realJar, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn(String.format("Failed to copy %s to %s", newJar.toString(), realJar.toString()), e.getMessage());
|
||||
// Fall-through to restarting node...
|
||||
}
|
||||
}
|
||||
|
||||
private static void restartNode() {
|
||||
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<String> javaCmd = Arrays.asList(javaBinary.toString(), "-jar", JAR_FILENAME);
|
||||
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
||||
|
||||
new ProcessBuilder(javaCmd).start();
|
||||
} catch (IOException e) {
|
||||
LOGGER.error(String.format("Failed to restart node (BAD): %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
171
src/main/java/org/qora/controller/AutoUpdate.java
Normal file
171
src/main/java/org/qora/controller/AutoUpdate.java
Normal file
@ -0,0 +1,171 @@
|
||||
package org.qora.controller;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qora.ApplyUpdate;
|
||||
import org.qora.api.ApiRequest;
|
||||
import org.qora.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qora.data.transaction.ArbitraryTransactionData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.settings.Settings;
|
||||
import org.qora.transaction.ArbitraryTransaction;
|
||||
import org.qora.transaction.Transaction.TransactionType;
|
||||
import org.qora.utils.NTP;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class AutoUpdate extends Thread {
|
||||
|
||||
public static final String JAR_FILENAME = "qora-core.jar";
|
||||
public static final String NEW_JAR_FILENAME = "new-" + JAR_FILENAME;
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(AutoUpdate.class);
|
||||
private static final long CHECK_INTERVAL = 5 * 1000; // ms
|
||||
|
||||
private static final int DEV_GROUP_ID = 1;
|
||||
private static final int UPDATE_SERVICE = 1;
|
||||
private static final List<TransactionType> ARBITRARY_TX_TYPE = Arrays.asList(TransactionType.ARBITRARY);
|
||||
|
||||
private static AutoUpdate instance;
|
||||
|
||||
private boolean isStopping = false;
|
||||
|
||||
private AutoUpdate() {
|
||||
}
|
||||
|
||||
public static AutoUpdate getInstance() {
|
||||
if (instance == null)
|
||||
instance = new AutoUpdate();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
long buildTimestamp = Controller.getInstance().getBuildTimestamp() * 1000L;
|
||||
boolean attemptedUpdate = false;
|
||||
|
||||
while (!isStopping) {
|
||||
try {
|
||||
Thread.sleep(CHECK_INTERVAL);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to clean up any leftover downloads (but if we are/have attempted update then don't delete new JAR)
|
||||
if (!attemptedUpdate)
|
||||
try {
|
||||
Path newJar = Paths.get(NEW_JAR_FILENAME);
|
||||
Files.deleteIfExists(newJar);
|
||||
} catch (IOException de) {
|
||||
// Whatever
|
||||
}
|
||||
|
||||
// Look for "update" tx which is arbitrary tx in dev-group with service 1 and timestamp later than buildTimestamp
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, DEV_GROUP_ID, ARBITRARY_TX_TYPE, UPDATE_SERVICE, null, ConfirmationStatus.CONFIRMED, 1, null, true);
|
||||
if (signatures == null || signatures.isEmpty())
|
||||
continue;
|
||||
|
||||
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signatures.get(0));
|
||||
if (transactionData == null || !(transactionData instanceof ArbitraryTransactionData))
|
||||
continue;
|
||||
|
||||
// Transaction needs to be newer than this build
|
||||
if (transactionData.getTimestamp() <= buildTimestamp)
|
||||
continue;
|
||||
|
||||
ArbitraryTransaction arbitraryTransaction = new ArbitraryTransaction(repository, transactionData);
|
||||
if (!arbitraryTransaction.isDataLocal())
|
||||
continue; // We can't access data
|
||||
|
||||
// TODO: check arbitrary data length (pre-fetch) matches git commit length (20) + sha256 hash length (32) = 52 bytes
|
||||
|
||||
byte[] commitHash = arbitraryTransaction.fetchData();
|
||||
LOGGER.info(String.format("Update's git commit hash: %s", HashCode.fromBytes(commitHash).toString()));
|
||||
|
||||
String[] autoUpdateRepos = Settings.getInstance().getAutoUpdateRepos();
|
||||
for (String repo : autoUpdateRepos)
|
||||
if (attemptUpdate(commitHash, repo)) {
|
||||
// Consider ourselves updated so don't re-re-re-download
|
||||
buildTimestamp = NTP.getTime();
|
||||
attemptedUpdate = true;
|
||||
break;
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.warn("Repository issue to find updates", e);
|
||||
// Keep going I guess...
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info("Stopping auto-update service");
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
isStopping = true;
|
||||
}
|
||||
|
||||
private static boolean attemptUpdate(byte[] commitHash, String repoBaseUri) {
|
||||
LOGGER.info(String.format("Fetching update from %s", repoBaseUri));
|
||||
InputStream in = ApiRequest.fetchStream(repoBaseUri + "/raw/" + HashCode.fromBytes(commitHash).toString() + "/" + JAR_FILENAME);
|
||||
if (in == null) {
|
||||
LOGGER.warn(String.format("Failed to fetch update from %s", repoBaseUri));
|
||||
return false; // failed - try another repo
|
||||
}
|
||||
|
||||
Path newJar = Paths.get(NEW_JAR_FILENAME);
|
||||
try {
|
||||
// Save input stream into new JAR
|
||||
LOGGER.debug(String.format("Saving update from %s into %s", repoBaseUri, newJar.toString()));
|
||||
Files.copy(in, newJar, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn(String.format("Failed to save update from %s into %s", repoBaseUri, newJar.toString()));
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(newJar);
|
||||
} catch (IOException de) {
|
||||
LOGGER.warn(String.format("Failed to delete partial download: %s", de.getMessage()));
|
||||
}
|
||||
|
||||
return false; // failed - try another repo
|
||||
}
|
||||
|
||||
// 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<String> javaCmd = Arrays.asList(javaBinary.toString(), "-cp", NEW_JAR_FILENAME, ApplyUpdate.class.getCanonicalName());
|
||||
LOGGER.info(String.format("Applying update with: %s", String.join(" ", javaCmd)));
|
||||
|
||||
new ProcessBuilder(javaCmd).start();
|
||||
|
||||
return true; // applying update OK
|
||||
} catch (IOException e) {
|
||||
LOGGER.error(String.format("Failed to apply update: %s", e.getMessage()));
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(newJar);
|
||||
} catch (IOException de) {
|
||||
LOGGER.warn(String.format("Failed to delete update download: %s", de.getMessage()));
|
||||
}
|
||||
|
||||
return true; // repo was okay, even if applying update failed
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user