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