Browse Source

Another attempt at auto-update

pull/67/head
catbref 5 years ago
parent
commit
20aa49a1f1
  1. 116
      src/main/java/org/qora/ApplyUpdate.java
  2. 171
      src/main/java/org/qora/controller/AutoUpdate.java

116
src/main/java/org/qora/ApplyUpdate.java

@ -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

@ -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…
Cancel
Save