001package co.codewizards.cloudstore.updater; 002 003import static co.codewizards.cloudstore.core.io.StreamUtil.*; 004import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; 005import static co.codewizards.cloudstore.core.util.AssertUtil.*; 006import static co.codewizards.cloudstore.core.util.Util.*; 007 008import java.io.FileFilter; 009import java.io.IOException; 010import java.io.InputStream; 011import java.io.OutputStream; 012import java.lang.reflect.Constructor; 013import java.lang.reflect.InvocationTargetException; 014import java.net.URL; 015import java.util.Collection; 016import java.util.HashSet; 017import java.util.Properties; 018import java.util.Set; 019 020import org.kohsuke.args4j.CmdLineException; 021import org.kohsuke.args4j.CmdLineParser; 022import org.kohsuke.args4j.Option; 023import org.slf4j.Logger; 024import org.slf4j.LoggerFactory; 025 026import ch.qos.logback.classic.LoggerContext; 027import ch.qos.logback.classic.joran.JoranConfigurator; 028import ch.qos.logback.core.joran.spi.JoranException; 029import ch.qos.logback.core.util.StatusPrinter; 030import co.codewizards.cloudstore.core.appid.AppId; 031import co.codewizards.cloudstore.core.appid.AppIdRegistry; 032import co.codewizards.cloudstore.core.config.ConfigDir; 033import co.codewizards.cloudstore.core.io.LockFile; 034import co.codewizards.cloudstore.core.io.LockFileFactory; 035import co.codewizards.cloudstore.core.io.TimeoutException; 036import co.codewizards.cloudstore.core.oio.File; 037import co.codewizards.cloudstore.core.updater.CloudStoreUpdaterCore; 038import co.codewizards.cloudstore.core.util.AssertUtil; 039import co.codewizards.cloudstore.core.util.IOUtil; 040 041public class CloudStoreUpdater extends CloudStoreUpdaterCore { 042 private static final Logger logger = LoggerFactory.getLogger(CloudStoreUpdater.class); 043 private static final AppId appId = AppIdRegistry.getInstance().getAppIdOrFail(); 044 045 private static Class<? extends CloudStoreUpdater> cloudStoreUpdaterClass = CloudStoreUpdater.class; 046 047 private final String[] args; 048 private boolean throwException = true; 049 050 @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.") 051 private String installationDir; 052 private File installationDirFile; 053 054 private Properties remoteUpdateProperties; 055 private File tempDownloadDir; 056 057 private File localServerRunningFile; 058 private LockFile localServerRunningLockFile; 059 private File localServerStopFile; 060 061 public static void main(final String[] args) throws Exception { 062 initLogging(); 063 try { 064 final int programExitStatus = createCloudStoreUpdater(args).throwException(false).execute(); 065 System.exit(programExitStatus); 066 } catch (final Throwable x) { 067 logger.error(x.toString(), x); 068 System.exit(999); 069 } 070 } 071 072 protected static Constructor<? extends CloudStoreUpdater> getCloudStoreUpdaterConstructor() throws NoSuchMethodException, SecurityException { 073 final Class<? extends CloudStoreUpdater> clazz = getCloudStoreUpdaterClass(); 074 final Constructor<? extends CloudStoreUpdater> constructor = clazz.getConstructor(String[].class); 075 return constructor; 076 } 077 078 protected static CloudStoreUpdater createCloudStoreUpdater(final String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { 079 final Constructor<? extends CloudStoreUpdater> constructor = getCloudStoreUpdaterConstructor(); 080 final CloudStoreUpdater cloudStoreUpdater = constructor.newInstance(new Object[] { args }); 081 return cloudStoreUpdater; 082 } 083 084 protected static Class<? extends CloudStoreUpdater> getCloudStoreUpdaterClass() { 085 return cloudStoreUpdaterClass; 086 } 087 protected static void setCloudStoreUpdaterClass(final Class<? extends CloudStoreUpdater> cloudStoreUpdaterClass) { 088 assertNotNull(cloudStoreUpdaterClass, "cloudStoreUpdaterClass"); 089 CloudStoreUpdater.cloudStoreUpdaterClass = cloudStoreUpdaterClass; 090 } 091 092 public CloudStoreUpdater(final String[] args) { 093 this.args = args; 094 } 095 096 public boolean isThrowException() { 097 return throwException; 098 } 099 public void setThrowException(final boolean throwException) { 100 this.throwException = throwException; 101 } 102 public CloudStoreUpdater throwException(final boolean throwException) { 103 setThrowException(throwException); 104 return this; 105 } 106 107 public int execute() throws Exception { 108 int programExitStatus = 1; 109 final CmdLineParser parser = new CmdLineParser(this); 110 try { 111 parser.parseArgument(args); 112 this.run(); 113 programExitStatus = 0; 114 } catch (final CmdLineException e) { 115 // handling of wrong arguments 116 programExitStatus = 2; 117 System.err.println("Error: " + e.getMessage()); 118 System.err.println(); 119 if (throwException) 120 throw e; 121 } catch (final Exception x) { 122 programExitStatus = 3; 123 logger.error(x.toString(), x); 124 if (throwException) 125 throw x; 126 } 127 return programExitStatus; 128 } 129 130 private static void initLogging() throws IOException, JoranException { 131 ConfigDir.getInstance().getLogDir(); 132 133 final String logbackXmlName = "logback.updater.xml"; 134 final File logbackXmlFile = createFile(ConfigDir.getInstance().getFile(), logbackXmlName); 135 if (!logbackXmlFile.exists()) { 136 AppIdRegistry.getInstance().copyResourceResolvingAppId( 137 CloudStoreUpdater.class, logbackXmlName, logbackXmlFile); 138 } 139 140 final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); 141 try { 142 final JoranConfigurator configurator = new JoranConfigurator(); 143 configurator.setContext(context); 144 // Call context.reset() to clear any previous configuration, e.g. default 145 // configuration. For multi-step configuration, omit calling context.reset(). 146 context.reset(); 147 configurator.doConfigure(logbackXmlFile.getIoFile()); 148 } catch (final JoranException je) { 149 // StatusPrinter will handle this 150 doNothing(); 151 } 152 StatusPrinter.printInCaseOfErrorsOrWarnings(context); 153 } 154 155 private void run() throws Exception { 156 System.out.println(String.format("%s updater started. Downloading meta-data.", appId.getName())); 157 158 boolean restoreRenamedFiles = false; 159 try { 160 stopLocalServer(); 161 final long localServerStoppedTimestamp = System.currentTimeMillis(); 162 163 final File downloadFile = downloadURLViaRemoteUpdateProperties("artifact[co.codewizards.cloudstore.aggregator].downloadURL"); 164 final File signatureFile = downloadURLViaRemoteUpdateProperties("artifact[co.codewizards.cloudstore.aggregator].signatureURL"); 165 166 System.out.println("Verifying PGP signature."); 167 new PGPVerifier().verify(downloadFile, signatureFile); 168 169 final long durationAfterLocalServerStop = System.currentTimeMillis() - localServerStoppedTimestamp; 170 final long additionalWaitTime = 10_000L - durationAfterLocalServerStop; 171 if (additionalWaitTime > 0L) { 172 // We make sure, at least 10 seconds passed after the LocalServer stopped in order to make sure 173 // the Java process really finished (this is *after* the lock is released by the running process). 174 // In Windows, we might otherwise run into some lingering file locks. 175 Thread.sleep(additionalWaitTime); 176 } 177 178 checkAvailableDiskSpace(getInstallationDir(), downloadFile.length() * 5); 179 180 final File backupDir = getBackupDir(); 181 backupDir.mkdirs(); 182 final File backupTarGzFile = createFile(backupDir, resolve(String.format("co.codewizards.cloudstore.aggregator-${localVersion}.backup-%s.tar.gz", Long.toString(System.currentTimeMillis(), 36)))); 183 System.out.println("Creating backup: " + backupTarGzFile); 184 185 new TarGzFile(backupTarGzFile) 186 .fileFilter(fileFilterIgnoringBackupAndUpdaterDir) 187 .compress(getInstallationDir()); 188 189 // Because of f***ing Windows and its insane file-locking, we first try to move all 190 // files out of the way by renaming them. If this fails, we restore the previous 191 // state. This way, we increase the probability that we leave a consistent state. 192 // If a file is locked, this should fail already now, rather than later after we extracted 193 // half of the tarball. 194 System.out.println("Renaming files in installation directory: " + getInstallationDir()); 195 restoreRenamedFiles = true; 196 renameFiles(getInstallationDir(), fileFilterIgnoringBackupAndUpdaterDir); 197 198 System.out.println("Overwriting installation directory: " + getInstallationDir()); 199 final Set<File> keepFiles = new HashSet<>(); 200 keepFiles.add(getInstallationDir()); 201 populateFilesRecursively(getBackupDir(), keepFiles); 202 populateFilesRecursively(getUpdaterDir(), keepFiles); 203 204 new TarGzFile(downloadFile) 205 .tarGzEntryNameConverter(new ExtractTarGzEntryNameConverter()) 206 .fileFilter(new FileFilterTrackingExtractedFiles(keepFiles)) 207 .extract(getInstallationDir()); 208 209 restoreRenamedFiles = false; 210 211 System.out.println("Deleting old files from installation directory: " + getInstallationDir()); 212 deleteAllExcept(getInstallationDir(), keepFiles); 213 } finally { 214 if (restoreRenamedFiles) 215 restoreRenamedFiles(getInstallationDir()); 216 217 if (tempDownloadDir != null) { 218 System.out.println("Deleting temporary download-directory."); 219 IOUtil.deleteDirectoryRecursively(tempDownloadDir); 220 } 221 222 if (localServerRunningLockFile != null) { 223 localServerRunningLockFile.release(); 224 localServerRunningLockFile = null; 225 } 226 } 227 System.out.println("Update successfully done. Exiting."); 228 } 229 230 private void stopLocalServer() { 231 try { 232 boolean localServerRunning = ! tryAcquireLocalServerRunningLockFile(); 233 if (localServerRunning) { 234 System.out.println("LocalServer is running. Stopping it..."); 235 final File localServerStopFile = getLocalServerStopFile(); 236 237 if (localServerStopFile.exists()) { 238 localServerStopFile.delete(); 239 if (localServerStopFile.exists()) 240 logger.warn("Failed to delete file: {}", localServerStopFile); 241 else 242 System.out.println("File successfully deleted: " + localServerStopFile); 243 } 244 else { 245 System.out.println("WARNING: File does not exist (could thus not delete it): " + localServerStopFile); 246 logger.warn("File does not exist: {}", localServerStopFile); 247 } 248 249 System.out.println("Waiting for LocalServer to stop..."); 250 final long waitStartTimestamp = System.currentTimeMillis(); 251 do { 252 if (System.currentTimeMillis() - waitStartTimestamp > 120_000L) 253 throw new TimeoutException("LocalServer did not stop within timeout!"); 254 255 localServerRunning = ! tryAcquireLocalServerRunningLockFile(); 256 } while (localServerRunning); 257 258 System.out.println("LocalServer stopped."); 259 } 260 } catch (Exception x) { 261 logger.error("stopLocalServer: " + x, x); 262 x.printStackTrace(); 263 } 264 } 265 266 private File getLocalServerRunningFile() { 267 if (localServerRunningFile == null) { 268 localServerRunningFile = createFile(ConfigDir.getInstance().getFile(), "localServerRunning.lock"); 269 try { 270 localServerRunningFile = localServerRunningFile.getCanonicalFile(); 271 } catch (IOException x) { 272 logger.warn("getLocalServerRunningFile: " + x, x); 273 } 274 } 275 return localServerRunningFile; 276 } 277 278 private File getLocalServerStopFile() { 279 if (localServerStopFile == null) 280 localServerStopFile = createFile(ConfigDir.getInstance().getFile(), "localServerRunning.deleteToStop"); 281 282 return localServerStopFile; 283 } 284 285 private boolean tryAcquireLocalServerRunningLockFile() { 286 if (localServerRunningLockFile != null) { 287 logger.warn("tryAcquireLocalServerRunningLockFile: Already acquired before!!! Skipping!"); 288 return true; 289 } 290 291 try { 292 localServerRunningLockFile = LockFileFactory.getInstance().acquire(getLocalServerRunningFile(), 1000); 293 return true; 294 } catch (TimeoutException x) { 295 return false; 296 } 297 } 298 299 private void checkAvailableDiskSpace(final File dir, final long expectedRequiredSpace) throws IOException { 300 final long usableSpace = dir.getUsableSpace(); 301 logger.debug("checkAvailableDiskSpace: dir='{}' dir.usableSpace='{} MiB' expectedRequiredSpace='{} MiB'", 302 dir, usableSpace / 1024 / 1024, expectedRequiredSpace / 1024 / 1024); 303 304 if (usableSpace < expectedRequiredSpace) { 305 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!", 306 dir, usableSpace / 1024 / 1024, usableSpace, expectedRequiredSpace / 1024 / 1024, expectedRequiredSpace); 307 logger.error("checkAvailableDiskSpace: " + msg); 308 throw new IOException(msg); 309 } 310 } 311 312 private static final String RENAMED_FILE_SUFFIX = ".csupdbak"; 313 314 private void renameFiles(final File dir, final FileFilter fileFilter) throws IOException { 315 final File[] children = dir.listFiles(fileFilter); 316 if (children != null) { 317 for (final File child : children) { 318 if (child.isDirectory()) 319 renameFiles(child, fileFilter); 320 else { 321 final File newChild = createFile(dir, child.getName() + RENAMED_FILE_SUFFIX); 322 logger.debug("renameFiles: file='{}', newName='{}'", child, newChild.getName()); 323 if (!child.renameTo(newChild)) { 324 final String msg = String.format("Failed to rename the file '%s' to '%s' (in the same directory)!", child, newChild.getName()); 325 logger.error("renameFiles: {}", msg); 326 throw new IOException(msg); 327 } 328 } 329 } 330 } 331 } 332 333 private void restoreRenamedFiles(final File dir) { 334 final File[] children = dir.listFiles(); 335 if (children != null) { 336 for (final File child : children) { 337 if (child.isDirectory()) 338 restoreRenamedFiles(child); 339 else if (child.getName().endsWith(RENAMED_FILE_SUFFIX)) { 340 final File newChild = createFile(dir, child.getName().substring(0, child.getName().length() - RENAMED_FILE_SUFFIX.length())); 341 logger.debug("restoreRenamedFiles: file='{}', newName='{}'", child, newChild.getName()); 342 newChild.delete(); 343 if (!child.renameTo(newChild)) 344 logger.warn("restoreRenamedFiles: Failed to rename the file '{}' back to its original name '{}' (in the same directory)!", child, newChild.getName()); 345 } 346 } 347 } 348 } 349 350 private static class FileFilterTrackingExtractedFiles implements FileFilter { 351 private final Collection<File> files; 352 353 public FileFilterTrackingExtractedFiles(final Collection<File> files) { 354 this.files = assertNotNull(files, "files"); 355 } 356 357 @Override 358 public boolean accept(final java.io.File file) { 359 files.add(createFile(file)); 360 files.add(createFile(file.getParentFile())); // just in case the parent didn't have its own entry and was created implicitly 361 return true; 362 } 363 } 364 365 private static class ExtractTarGzEntryNameConverter implements TarGzEntryNameConverter { 366 @Override 367 public String getEntryName(final File rootDir, final File file) { throw new UnsupportedOperationException(); } 368 369 @Override 370 public File getFile(final File rootDir, String entryName) { 371 final String prefix1 = appId.getSimpleId() + "/"; 372 final String prefix2 = appId.getSimpleId() + "-"; // needed by subshare! it uses "subshare-server" in its server-installation 373 374 if (entryName.startsWith(prefix1)) 375 entryName = entryName.substring(prefix1.length()); 376 else if (entryName.startsWith(prefix2)) { 377 final int slashIndex = entryName.indexOf('/', prefix2.length()); 378 if (slashIndex >= 0) 379 entryName = entryName.substring(slashIndex + 1); 380 } 381 382 return entryName.isEmpty() ? rootDir : createFile(rootDir, entryName); 383 } 384 } 385 386 private void populateFilesRecursively(final File fileOrDir, final Set<File> files) { 387 AssertUtil.assertNotNull(fileOrDir, "fileOrDir"); 388 AssertUtil.assertNotNull(files, "files"); 389 files.add(fileOrDir); 390 final File[] children = fileOrDir.listFiles(); 391 if (children != null) { 392 for (final File child : children) 393 populateFilesRecursively(child, files); 394 } 395 } 396 397 private void deleteAllExcept(final File fileOrDir, final Set<File> keepFiles) { 398 AssertUtil.assertNotNull(fileOrDir, "fileOrDir"); 399 AssertUtil.assertNotNull(keepFiles, "keepFiles"); 400 if (keepFiles.contains(fileOrDir)) { 401 logger.debug("deleteAllExcept: Keeping: {}", fileOrDir); 402 final File[] children = fileOrDir.listFiles(); 403 if (children != null) { 404 for (final File child : children) 405 deleteAllExcept(child, keepFiles); 406 } 407 } 408 else { 409 logger.debug("deleteAllExcept: Deleting: {}", fileOrDir); 410 IOUtil.deleteDirectoryRecursively(fileOrDir); 411 } 412 } 413 414 private File downloadURLViaRemoteUpdateProperties(final String remoteUpdatePropertiesKey) { 415 logger.debug("downloadURLViaRemoteUpdateProperties: remoteUpdatePropertiesKey='{}'", remoteUpdatePropertiesKey); 416 final String resolvedKey = resolve(remoteUpdatePropertiesKey); 417 final String urlStr = getRemoteUpdateProperties().getProperty(resolvedKey); 418 if (urlStr == null || urlStr.trim().isEmpty()) 419 throw new IllegalStateException("No value for key in remoteUpdateProperties: " + resolvedKey); 420 421 final String resolvedURLStr = resolve(urlStr); 422 logger.debug("downloadURLViaRemoteUpdateProperties: resolvedURLStr='{}'", resolvedURLStr); 423 424 final File tempDownloadDir = getTempDownloadDir(); 425 426 try { 427 System.out.println("Downloading: " + resolvedURLStr); 428 final URL url = new URL(resolvedURLStr); 429 final long contentLength = url.openConnection().getContentLengthLong(); 430 if (contentLength < 0) 431 logger.warn("downloadURLViaRemoteUpdateProperties: contentLength unknown! url='{}'", url); 432 else { 433 logger.debug("downloadURLViaRemoteUpdateProperties: contentLength={} url='{}'", contentLength, url); 434 checkAvailableDiskSpace(tempDownloadDir, Math.max(1024 * 1024, contentLength * 3 / 2)); 435 } 436 int logLastPercentage = -100; // We start with this negative value, because we want the '0%' to be printed ;-) 437 final int logStepPercentageDiff = 5; 438 long downloadedLength = 0; 439 440 final String path = url.getPath(); 441 final int lastSlashIndex = path.lastIndexOf('/'); 442 if (lastSlashIndex < 0) 443 throw new IllegalStateException("No '/' found in URL?!"); 444 445 final String fileName = path.substring(lastSlashIndex + 1); 446 final File downloadFile = createFile(tempDownloadDir, fileName); 447 448 boolean successful = false; 449 final InputStream in = url.openStream(); 450 try { 451 final OutputStream out = castStream(downloadFile.createOutputStream()); 452 try { 453 454 final byte[] buf = new byte[65535]; 455 int bytesRead; 456 while ((bytesRead = in.read(buf)) >= 0) { 457 out.write(buf, 0, bytesRead); 458 downloadedLength += bytesRead; 459 460 if (contentLength > 0) { 461 int percentage = (int) (downloadedLength * 100 / contentLength); 462 if (logStepPercentageDiff <= percentage - logLastPercentage) { 463 logLastPercentage = percentage; 464 System.out.printf(" ... %d%%", percentage); 465 } 466 } 467 } 468 469 } finally { 470 out.close(); 471 System.out.println(); 472 } 473 successful = true; 474 } finally { 475 in.close(); 476 477 if (!successful) 478 downloadFile.delete(); 479 } 480 481 return downloadFile; 482 } catch (final IOException e) { 483 throw new RuntimeException(e); 484 } 485 } 486 487 private File getTempDownloadDir() { 488 if (tempDownloadDir == null) { 489 try { 490 tempDownloadDir = IOUtil.createUniqueRandomFolder(IOUtil.getTempDir(), "cloudstore-update-"); 491 } catch (final IOException e) { 492 throw new RuntimeException(e); 493 } 494 } 495 return tempDownloadDir; 496 } 497 498 /** 499 * Gets the installation directory that was passed as command line parameter. 500 */ 501 @Override 502 protected File getInstallationDir() { 503 if (installationDirFile == null) { 504 final String path = IOUtil.simplifyPath(createFile(AssertUtil.assertNotNull(installationDir, "installationDir"))); 505 final File f = createFile(path); 506 if (!f.exists()) 507 throw new IllegalArgumentException(String.format("installationDir '%s' (specified as '%s') does not exist!", f, installationDir)); 508 509 if (!f.isDirectory()) 510 throw new IllegalArgumentException(String.format("installationDir '%s' (specified as '%s') is not a directory!", f, installationDir)); 511 512 installationDirFile = f; 513 } 514 return installationDirFile; 515 } 516 517 private Properties getRemoteUpdateProperties() { 518 if (remoteUpdateProperties == null) { 519 final String resolvedRemoteUpdatePropertiesURL = resolve(remoteUpdatePropertiesURL); 520 final Properties properties = new Properties(); 521 try { 522 final URL url = new URL(resolvedRemoteUpdatePropertiesURL); 523 final InputStream in = url.openStream(); 524 try { 525 properties.load(in); 526 } finally { 527 in.close(); 528 } 529 } catch (final IOException e) { 530 throw new RuntimeException(e); 531 } 532 remoteUpdateProperties = properties; 533 } 534 return remoteUpdateProperties; 535 } 536}