package co.codewizards.cloudstore.updater;
import static co.codewizards.cloudstore.core.io.StreamUtil.*;
import static co.codewizards.cloudstore.core.oio.OioFileFactory.*;
import static co.codewizards.cloudstore.core.util.AssertUtil.*;
import static co.codewizards.cloudstore.core.util.Util.*;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;
import co.codewizards.cloudstore.core.appid.AppId;
import co.codewizards.cloudstore.core.appid.AppIdRegistry;
import co.codewizards.cloudstore.core.config.ConfigDir;
import co.codewizards.cloudstore.core.io.LockFile;
import co.codewizards.cloudstore.core.io.LockFileFactory;
import co.codewizards.cloudstore.core.io.TimeoutException;
import co.codewizards.cloudstore.core.oio.File;
import co.codewizards.cloudstore.core.updater.CloudStoreUpdaterCore;
import co.codewizards.cloudstore.core.util.AssertUtil;
import co.codewizards.cloudstore.core.util.IOUtil;
public class CloudStoreUpdater extends CloudStoreUpdaterCore {
private static final Logger logger = LoggerFactory.getLogger(CloudStoreUpdater.class);
private static final AppId appId = AppIdRegistry.getInstance().getAppIdOrFail();
private static Class<? extends CloudStoreUpdater> cloudStoreUpdaterClass = CloudStoreUpdater.class;
private final String[] args;
private boolean throwException = true;
@Option(name="-installationDir", required=true, usage="Base-directory of the installation containing the 'bin' directory as well as the 'installation.properties' file - e.g. '/opt/cloudstore'. The installation in this directory will be updated.")
private String installationDir;
private File installationDirFile;
private Properties remoteUpdateProperties;
private File tempDownloadDir;
private File localServerRunningFile;
private LockFile localServerRunningLockFile;
private File localServerStopFile;
public static void main(final String[] args) throws Exception {
initLogging();
try {
final int programExitStatus = createCloudStoreUpdater(args).throwException(false).execute();
System.exit(programExitStatus);
} catch (final Throwable x) {
logger.error(x.toString(), x);
System.exit(999);
}
}
protected static Constructor<? extends CloudStoreUpdater> getCloudStoreUpdaterConstructor() throws NoSuchMethodException, SecurityException {
final Class<? extends CloudStoreUpdater> clazz = getCloudStoreUpdaterClass();
final Constructor<? extends CloudStoreUpdater> constructor = clazz.getConstructor(String[].class);
return constructor;
}
protected static CloudStoreUpdater createCloudStoreUpdater(final String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
final Constructor<? extends CloudStoreUpdater> constructor = getCloudStoreUpdaterConstructor();
final CloudStoreUpdater cloudStoreUpdater = constructor.newInstance(new Object[] { args });
return cloudStoreUpdater;
}
protected static Class<? extends CloudStoreUpdater> getCloudStoreUpdaterClass() {
return cloudStoreUpdaterClass;
}
protected static void setCloudStoreUpdaterClass(final Class<? extends CloudStoreUpdater> cloudStoreUpdaterClass) {
assertNotNull(cloudStoreUpdaterClass, "cloudStoreUpdaterClass");
CloudStoreUpdater.cloudStoreUpdaterClass = cloudStoreUpdaterClass;
}
public CloudStoreUpdater(final String[] args) {
this.args = args;
}
public boolean isThrowException() {
return throwException;
}
public void setThrowException(final boolean throwException) {
this.throwException = throwException;
}
public CloudStoreUpdater throwException(final boolean throwException) {
setThrowException(throwException);
return this;
}
public int execute() throws Exception {
int programExitStatus = 1;
final CmdLineParser parser = new CmdLineParser(this);
try {
parser.parseArgument(args);
this.run();
programExitStatus = 0;
} catch (final CmdLineException e) {
// handling of wrong arguments
programExitStatus = 2;
System.err.println("Error: " + e.getMessage());
System.err.println();
if (throwException)
throw e;
} catch (final Exception x) {
programExitStatus = 3;
logger.error(x.toString(), x);
if (throwException)
throw x;
}
return programExitStatus;
}
private static void initLogging() throws IOException, JoranException {
ConfigDir.getInstance().getLogDir();
final String logbackXmlName = "logback.updater.xml";
final File logbackXmlFile = createFile(ConfigDir.getInstance().getFile(), logbackXmlName);
if (!logbackXmlFile.exists()) {
AppIdRegistry.getInstance().copyResourceResolvingAppId(
CloudStoreUpdater.class, logbackXmlName, logbackXmlFile);
}
final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
try {
final JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
// Call context.reset() to clear any previous configuration, e.g. default
// configuration. For multi-step configuration, omit calling context.reset().
context.reset();
configurator.doConfigure(logbackXmlFile.getIoFile());
} catch (final JoranException je) {
// StatusPrinter will handle this
doNothing();
}
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
}
private void run() throws Exception {
System.out.println(String.format("%s updater started. Downloading meta-data.", appId.getName()));
boolean restoreRenamedFiles = false;
try {
stopLocalServer();
final long localServerStoppedTimestamp = System.currentTimeMillis();
final File downloadFile = downloadURLViaRemoteUpdateProperties("artifact[${artifactId}].downloadURL");
final File signatureFile = downloadURLViaRemoteUpdateProperties("artifact[${artifactId}].signatureURL");
System.out.println("Verifying PGP signature.");
new PGPVerifier().verify(downloadFile, signatureFile);
final long durationAfterLocalServerStop = System.currentTimeMillis() - localServerStoppedTimestamp;
final long additionalWaitTime = 10_000L - durationAfterLocalServerStop;
if (additionalWaitTime > 0L) {
// We make sure, at least 10 seconds passed after the LocalServer stopped in order to make sure
// the Java process really finished (this is *after* the lock is released by the running process).
// In Windows, we might otherwise run into some lingering file locks.
Thread.sleep(additionalWaitTime);
}
checkAvailableDiskSpace(getInstallationDir(), downloadFile.length() * 5);
final File backupDir = getBackupDir();
backupDir.mkdirs();
final File backupTarGzFile = createFile(backupDir, resolve(String.format("${artifactId}-${localVersion}.backup-%s.tar.gz", Long.toString(System.currentTimeMillis(), 36))));
System.out.println("Creating backup: " + backupTarGzFile);
new TarGzFile(backupTarGzFile)
.fileFilter(fileFilterIgnoringBackupAndUpdaterDir)
.compress(getInstallationDir());
// Because of f***ing Windows and its insane file-locking, we first try to move all
// files out of the way by renaming them. If this fails, we restore the previous
// state. This way, we increase the probability that we leave a consistent state.
// If a file is locked, this should fail already now, rather than later after we extracted
// half of the tarball.
System.out.println("Renaming files in installation directory: " + getInstallationDir());
restoreRenamedFiles = true;
renameFiles(getInstallationDir(), fileFilterIgnoringBackupAndUpdaterDir);
System.out.println("Overwriting installation directory: " + getInstallationDir());
final Set<File> keepFiles = new HashSet<>();
keepFiles.add(getInstallationDir());
populateFilesRecursively(getBackupDir(), keepFiles);
populateFilesRecursively(getUpdaterDir(), keepFiles);
new TarGzFile(downloadFile)
.tarGzEntryNameConverter(new ExtractTarGzEntryNameConverter())
.fileFilter(new FileFilterTrackingExtractedFiles(keepFiles))
.extract(getInstallationDir());
restoreRenamedFiles = false;
System.out.println("Deleting old files from installation directory: " + getInstallationDir());
deleteAllExcept(getInstallationDir(), keepFiles);
} finally {
if (restoreRenamedFiles)
restoreRenamedFiles(getInstallationDir());
if (tempDownloadDir != null) {
System.out.println("Deleting temporary download-directory.");
IOUtil.deleteDirectoryRecursively(tempDownloadDir);
}
if (localServerRunningLockFile != null) {
localServerRunningLockFile.release();
localServerRunningLockFile = null;
}
}
System.out.println("Update successfully done. Exiting.");
}
private void stopLocalServer() {
try {
boolean localServerRunning = ! tryAcquireLocalServerRunningLockFile();
if (localServerRunning) {
System.out.println("LocalServer is running. Stopping it...");
final File localServerStopFile = getLocalServerStopFile();
if (localServerStopFile.exists()) {
localServerStopFile.delete();
if (localServerStopFile.exists())
logger.warn("Failed to delete file: {}", localServerStopFile);
else
System.out.println("File successfully deleted: " + localServerStopFile);
}
else {
System.out.println("WARNING: File does not exist (could thus not delete it): " + localServerStopFile);
logger.warn("File does not exist: {}", localServerStopFile);
}
System.out.println("Waiting for LocalServer to stop...");
final long waitStartTimestamp = System.currentTimeMillis();
do {
if (System.currentTimeMillis() - waitStartTimestamp > 120_000L)
throw new TimeoutException("LocalServer did not stop within timeout!");
localServerRunning = ! tryAcquireLocalServerRunningLockFile();
} while (localServerRunning);
System.out.println("LocalServer stopped.");
}
} catch (Exception x) {
logger.error("stopLocalServer: " + x, x);
x.printStackTrace();
}
}
private File getLocalServerRunningFile() {
if (localServerRunningFile == null) {
localServerRunningFile = createFile(ConfigDir.getInstance().getFile(), "localServerRunning.lock");
try {
localServerRunningFile = localServerRunningFile.getCanonicalFile();
} catch (IOException x) {
logger.warn("getLocalServerRunningFile: " + x, x);
}
}
return localServerRunningFile;
}
private File getLocalServerStopFile() {
if (localServerStopFile == null)
localServerStopFile = createFile(ConfigDir.getInstance().getFile(), "localServerRunning.deleteToStop");
return localServerStopFile;
}
private boolean tryAcquireLocalServerRunningLockFile() {
if (localServerRunningLockFile != null) {
logger.warn("tryAcquireLocalServerRunningLockFile: Already acquired before!!! Skipping!");
return true;
}
try {
localServerRunningLockFile = LockFileFactory.getInstance().acquire(getLocalServerRunningFile(), 1000);
return true;
} catch (TimeoutException x) {
return false;
}
}
private void checkAvailableDiskSpace(final File dir, final long expectedRequiredSpace) throws IOException {
final long usableSpace = dir.getUsableSpace();
logger.debug("checkAvailableDiskSpace: dir='{}' dir.usableSpace='{} MiB' expectedRequiredSpace='{} MiB'",
dir, usableSpace / 1024 / 1024, expectedRequiredSpace / 1024 / 1024);
if (usableSpace < expectedRequiredSpace) {
final String msg = String.format("Insufficient disk space! The file system of the directory '%s' has %s MiB (%s B) available, but %s MiB (%s B) are required!",
dir, usableSpace / 1024 / 1024, usableSpace, expectedRequiredSpace / 1024 / 1024, expectedRequiredSpace);
logger.error("checkAvailableDiskSpace: " + msg);
throw new IOException(msg);
}
}
private static final String RENAMED_FILE_SUFFIX = ".csupdbak";
private void renameFiles(final File dir, final FileFilter fileFilter) throws IOException {
final File[] children = dir.listFiles(fileFilter);
if (children != null) {
for (final File child : children) {
if (child.isDirectory())
renameFiles(child, fileFilter);
else {
final File newChild = createFile(dir, child.getName() + RENAMED_FILE_SUFFIX);
logger.debug("renameFiles: file='{}', newName='{}'", child, newChild.getName());
if (!child.renameTo(newChild)) {
final String msg = String.format("Failed to rename the file '%s' to '%s' (in the same directory)!", child, newChild.getName());
logger.error("renameFiles: {}", msg);
throw new IOException(msg);
}
}
}
}
}
private void restoreRenamedFiles(final File dir) {
final File[] children = dir.listFiles();
if (children != null) {
for (final File child : children) {
if (child.isDirectory())
restoreRenamedFiles(child);
else if (child.getName().endsWith(RENAMED_FILE_SUFFIX)) {
final File newChild = createFile(dir, child.getName().substring(0, child.getName().length() - RENAMED_FILE_SUFFIX.length()));
logger.debug("restoreRenamedFiles: file='{}', newName='{}'", child, newChild.getName());
newChild.delete();
if (!child.renameTo(newChild))
logger.warn("restoreRenamedFiles: Failed to rename the file '{}' back to its original name '{}' (in the same directory)!", child, newChild.getName());
}
}
}
}
private static class FileFilterTrackingExtractedFiles implements FileFilter {
private final Collection<File> files;
public FileFilterTrackingExtractedFiles(final Collection<File> files) {
this.files = assertNotNull(files, "files");
}
@Override
public boolean accept(final java.io.File file) {
files.add(createFile(file));
files.add(createFile(file.getParentFile())); // just in case the parent didn't have its own entry and was created implicitly
return true;
}
}
private static class ExtractTarGzEntryNameConverter implements TarGzEntryNameConverter {
@Override
public String getEntryName(final File rootDir, final File file) { throw new UnsupportedOperationException(); }
@Override
public File getFile(final File rootDir, String entryName) {
final String prefix1 = appId.getSimpleId() + "/";
final String prefix2 = appId.getSimpleId() + "-"; // needed by subshare! it uses "subshare-server" in its server-installation
if (entryName.startsWith(prefix1))
entryName = entryName.substring(prefix1.length());
else if (entryName.startsWith(prefix2)) {
final int slashIndex = entryName.indexOf('/', prefix2.length());
if (slashIndex >= 0)
entryName = entryName.substring(slashIndex + 1);
}
return entryName.isEmpty() ? rootDir : createFile(rootDir, entryName);
}
}
private void populateFilesRecursively(final File fileOrDir, final Set<File> files) {
AssertUtil.assertNotNull(fileOrDir, "fileOrDir");
AssertUtil.assertNotNull(files, "files");
files.add(fileOrDir);
final File[] children = fileOrDir.listFiles();
if (children != null) {
for (final File child : children)
populateFilesRecursively(child, files);
}
}
private void deleteAllExcept(final File fileOrDir, final Set<File> keepFiles) {
AssertUtil.assertNotNull(fileOrDir, "fileOrDir");
AssertUtil.assertNotNull(keepFiles, "keepFiles");
if (keepFiles.contains(fileOrDir)) {
logger.debug("deleteAllExcept: Keeping: {}", fileOrDir);
final File[] children = fileOrDir.listFiles();
if (children != null) {
for (final File child : children)
deleteAllExcept(child, keepFiles);
}
}
else {
logger.debug("deleteAllExcept: Deleting: {}", fileOrDir);
IOUtil.deleteDirectoryRecursively(fileOrDir);
}
}
private File downloadURLViaRemoteUpdateProperties(final String remoteUpdatePropertiesKey) {
logger.debug("downloadURLViaRemoteUpdateProperties: remoteUpdatePropertiesKey='{}'", remoteUpdatePropertiesKey);
final String resolvedKey = resolve(remoteUpdatePropertiesKey);
final String urlStr = getRemoteUpdateProperties().getProperty(resolvedKey);
if (urlStr == null || urlStr.trim().isEmpty())
throw new IllegalStateException("No value for key in remoteUpdateProperties: " + resolvedKey);
final String resolvedURLStr = resolve(urlStr);
logger.debug("downloadURLViaRemoteUpdateProperties: resolvedURLStr='{}'", resolvedURLStr);
final File tempDownloadDir = getTempDownloadDir();
try {
System.out.println("Downloading: " + resolvedURLStr);
final URL url = new URL(resolvedURLStr);
final long contentLength = url.openConnection().getContentLengthLong();
if (contentLength < 0)
logger.warn("downloadURLViaRemoteUpdateProperties: contentLength unknown! url='{}'", url);
else {
logger.debug("downloadURLViaRemoteUpdateProperties: contentLength={} url='{}'", contentLength, url);
checkAvailableDiskSpace(tempDownloadDir, Math.max(1024 * 1024, contentLength * 3 / 2));
}
int logLastPercentage = -100; // We start with this negative value, because we want the '0%' to be printed ;-)
final int logStepPercentageDiff = 5;
long downloadedLength = 0;
final String path = url.getPath();
final int lastSlashIndex = path.lastIndexOf('/');
if (lastSlashIndex < 0)
throw new IllegalStateException("No '/' found in URL?!");
final String fileName = path.substring(lastSlashIndex + 1);
final File downloadFile = createFile(tempDownloadDir, fileName);
boolean successful = false;
final InputStream in = url.openStream();
try {
final OutputStream out = castStream(downloadFile.createOutputStream());
try {
final byte[] buf = new byte[65535];
int bytesRead;
while ((bytesRead = in.read(buf)) >= 0) {
out.write(buf, 0, bytesRead);
downloadedLength += bytesRead;
if (contentLength > 0) {
int percentage = (int) (downloadedLength * 100 / contentLength);
if (logStepPercentageDiff <= percentage - logLastPercentage) {
logLastPercentage = percentage;
System.out.printf(" ... %d%%", percentage);
}
}
}
} finally {
out.close();
System.out.println();
}
successful = true;
} finally {
in.close();
if (!successful)
downloadFile.delete();
}
return downloadFile;
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
private File getTempDownloadDir() {
if (tempDownloadDir == null) {
try {
tempDownloadDir = IOUtil.createUniqueRandomFolder(IOUtil.getTempDir(), "cloudstore-update-");
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
return tempDownloadDir;
}
/**
* Gets the installation directory that was passed as command line parameter.
*/
@Override
protected File getInstallationDir() {
if (installationDirFile == null) {
final String path = IOUtil.simplifyPath(createFile(AssertUtil.assertNotNull(installationDir, "installationDir")));
final File f = createFile(path);
if (!f.exists())
throw new IllegalArgumentException(String.format("installationDir '%s' (specified as '%s') does not exist!", f, installationDir));
if (!f.isDirectory())
throw new IllegalArgumentException(String.format("installationDir '%s' (specified as '%s') is not a directory!", f, installationDir));
installationDirFile = f;
}
return installationDirFile;
}
private Properties getRemoteUpdateProperties() {
if (remoteUpdateProperties == null) {
final String resolvedRemoteUpdatePropertiesURL = resolve(remoteUpdatePropertiesURL);
final Properties properties = new Properties();
try {
final URL url = new URL(resolvedRemoteUpdatePropertiesURL);
final InputStream in = url.openStream();
try {
properties.load(in);
} finally {
in.close();
}
} catch (final IOException e) {
throw new RuntimeException(e);
}
remoteUpdateProperties = properties;
}
return remoteUpdateProperties;
}
}