001/* 002 * (C) Copyright 2006-2017 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * bstefanescu 018 * Kevin Leturc <kleturc@nuxeo.com> 019 */ 020package org.nuxeo.runtime.reload; 021 022import java.io.File; 023import java.io.IOException; 024import java.net.MalformedURLException; 025import java.net.URL; 026import java.nio.file.Files; 027import java.nio.file.Path; 028import java.nio.file.StandardCopyOption; 029import java.util.ArrayList; 030import java.util.LinkedHashMap; 031import java.util.List; 032import java.util.Optional; 033import java.util.concurrent.TimeUnit; 034import java.util.jar.Manifest; 035import java.util.stream.Collectors; 036import java.util.stream.Stream; 037 038import javax.transaction.Transaction; 039 040import org.apache.commons.io.FileUtils; 041import org.apache.commons.logging.Log; 042import org.apache.commons.logging.LogFactory; 043import org.nuxeo.common.Environment; 044import org.nuxeo.common.utils.JarUtils; 045import org.nuxeo.common.utils.ZipUtils; 046import org.nuxeo.osgi.application.DevMutableClassLoader; 047import org.nuxeo.runtime.RuntimeServiceException; 048import org.nuxeo.runtime.api.Framework; 049import org.nuxeo.runtime.deployment.preprocessor.DeploymentPreprocessor; 050import org.nuxeo.runtime.model.ComponentContext; 051import org.nuxeo.runtime.model.ComponentManager; 052import org.nuxeo.runtime.model.DefaultComponent; 053import org.nuxeo.runtime.services.event.Event; 054import org.nuxeo.runtime.services.event.EventService; 055import org.nuxeo.runtime.transaction.TransactionHelper; 056import org.nuxeo.runtime.util.Watch; 057import org.osgi.framework.Bundle; 058import org.osgi.framework.BundleContext; 059import org.osgi.framework.BundleException; 060import org.osgi.framework.ServiceReference; 061import org.osgi.service.packageadmin.PackageAdmin; 062 063/** 064 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 065 */ 066public class ReloadComponent extends DefaultComponent implements ReloadService { 067 068 /** 069 * The reload strategy to adopt for hot reload. Default value is {@link #RELOAD_STRATEGY_VALUE_DEFAULT}. 070 * 071 * @since 9.3 072 */ 073 public static final String RELOAD_STRATEGY_PARAMETER = "org.nuxeo.runtime.reload_strategy"; 074 075 public static final String RELOAD_STRATEGY_VALUE_UNSTASH = "unstash"; 076 077 public static final String RELOAD_STRATEGY_VALUE_STANDBY = "standby"; 078 079 public static final String RELOAD_STRATEGY_VALUE_RESTART = "restart"; 080 081 public static final String RELOAD_STRATEGY_VALUE_DEFAULT = RELOAD_STRATEGY_VALUE_STANDBY; 082 083 private static final Log log = LogFactory.getLog(ReloadComponent.class); 084 085 protected static Bundle bundle; 086 087 protected Long lastFlushed; 088 089 public static BundleContext getBundleContext() { 090 return bundle.getBundleContext(); 091 } 092 093 public static Bundle getBundle() { 094 return bundle; 095 } 096 097 @Override 098 public void activate(ComponentContext context) { 099 super.activate(context); 100 bundle = context.getRuntimeContext().getBundle(); 101 } 102 103 @Override 104 public void deactivate(ComponentContext context) { 105 super.deactivate(context); 106 bundle = null; 107 } 108 109 /** 110 * @deprecated since 9.3, this method is only used in deployBundles and undeployBundles which are deprecated. Keep 111 * it for backward compatibility. 112 */ 113 @Deprecated 114 protected void refreshComponents() { 115 String reloadStrategy = Framework.getProperty(RELOAD_STRATEGY_PARAMETER, RELOAD_STRATEGY_VALUE_DEFAULT); 116 if (log.isInfoEnabled()) { 117 log.info("Refresh components. Strategy: " + reloadStrategy); 118 } 119 // reload components / contributions 120 ComponentManager mgr = Framework.getRuntime().getComponentManager(); 121 switch (reloadStrategy) { 122 case RELOAD_STRATEGY_VALUE_UNSTASH: 123 // compat mode 124 mgr.unstash(); 125 break; 126 case RELOAD_STRATEGY_VALUE_STANDBY: 127 // standby / resume 128 mgr.standby(); 129 mgr.unstash(); 130 mgr.resume(); 131 break; 132 case RELOAD_STRATEGY_VALUE_RESTART: 133 default: 134 // restart mode 135 mgr.refresh(false); 136 break; 137 } 138 } 139 140 @Override 141 public void reload() throws InterruptedException { 142 log.debug("Starting reload"); 143 144 try { 145 reloadProperties(); 146 } catch (IOException e) { 147 throw new RuntimeServiceException(e); 148 } 149 150 triggerReloadWithNewTransaction(RELOAD_EVENT_ID); 151 } 152 153 @Override 154 public void reloadProperties() throws IOException { 155 log.info("Before reload runtime properties"); 156 Framework.getRuntime().reloadProperties(); 157 log.info("After reload runtime properties"); 158 } 159 160 @Override 161 public void reloadSeamComponents() { 162 log.info("Reload Seam components"); 163 Framework.getService(EventService.class) 164 .sendEvent(new Event(RELOAD_TOPIC, RELOAD_SEAM_EVENT_ID, this, null)); 165 } 166 167 @Override 168 public void flush() { 169 log.info("Before flush caches"); 170 Framework.getService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, FLUSH_EVENT_ID, this, null)); 171 flushJaasCache(); 172 setFlushedNow(); 173 log.info("After flush caches"); 174 } 175 176 @Override 177 public void flushJaasCache() { 178 log.info("Before flush the JAAS cache"); 179 Framework.getService(EventService.class) 180 .sendEvent(new Event("usermanager", "user_changed", this, "Deployer")); 181 setFlushedNow(); 182 log.info("After flush the JAAS cache"); 183 } 184 185 @Override 186 public void flushSeamComponents() { 187 log.info("Flush Seam components"); 188 Framework.getService(EventService.class) 189 .sendEvent(new Event(RELOAD_TOPIC, FLUSH_SEAM_EVENT_ID, this, null)); 190 setFlushedNow(); 191 } 192 193 /** 194 * @deprecated since 9.3 use {@link #reloadBundles(ReloadContext)} instead. 195 */ 196 @Override 197 @Deprecated 198 public void deployBundles(List<File> files, boolean reloadResources) throws BundleException { 199 long begin = System.currentTimeMillis(); 200 List<String> missingNames = files.stream() 201 .filter(file -> getOSGIBundleName(file) == null) 202 .map(File::getAbsolutePath) 203 .collect(Collectors.toList()); 204 if (!missingNames.isEmpty()) { 205 missingNames.forEach( 206 name -> log.error(String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", name))); 207 // TODO investigate why we need to exit here, getBundleContext().installBundle(path) will throw an exception 208 // unless, maybe tests ? 209 return; 210 } 211 if (log.isInfoEnabled()) { 212 StringBuilder builder = new StringBuilder("Before deploy bundles\n"); 213 Framework.getRuntime().getStatusMessage(builder); 214 log.info(builder.toString()); 215 } 216 217 // Reload resources 218 if (reloadResources) { 219 List<URL> urls = files.stream().map(this::toURL).collect(Collectors.toList()); 220 Framework.reloadResourceLoader(urls, null); 221 } 222 223 // Deploy bundles 224 Transaction tx = TransactionHelper.suspendTransaction(); 225 try { 226 _deployBundles(files); 227 refreshComponents(); 228 } finally { 229 TransactionHelper.resumeTransaction(tx); 230 } 231 232 if (log.isInfoEnabled()) { 233 StringBuilder builder = new StringBuilder("After deploy bundles.\n"); 234 Framework.getRuntime().getStatusMessage(builder); 235 log.info(builder.toString()); 236 log.info(String.format("Hot deploy was done in %s ms.", System.currentTimeMillis() - begin)); 237 } 238 } 239 240 /** 241 * @deprecated since 9.3 use {@link #reloadBundles(ReloadContext)} instead. 242 */ 243 @Override 244 @Deprecated 245 public void undeployBundles(List<String> bundleNames, boolean reloadResources) throws BundleException { 246 long begin = System.currentTimeMillis(); 247 if (log.isInfoEnabled()) { 248 StringBuilder builder = new StringBuilder("Before undeploy bundles\n"); 249 Framework.getRuntime().getStatusMessage(builder); 250 log.info(builder.toString()); 251 } 252 253 // Undeploy bundles 254 Transaction tx = TransactionHelper.suspendTransaction(); 255 ReloadResult result = new ReloadResult(); 256 try { 257 result.merge(_undeployBundles(bundleNames)); 258 refreshComponents(); 259 } finally { 260 TransactionHelper.resumeTransaction(tx); 261 } 262 263 // Reload resources 264 if (reloadResources) { 265 List<URL> undeployedBundleURLs = result.undeployedBundles.stream() 266 .map(this::toURL) 267 .collect(Collectors.toList()); 268 Framework.reloadResourceLoader(null, undeployedBundleURLs); 269 } 270 271 if (log.isInfoEnabled()) { 272 StringBuilder builder = new StringBuilder("After undeploy bundles.\n"); 273 Framework.getRuntime().getStatusMessage(builder); 274 log.info(builder.toString()); 275 log.info(String.format("Hot undeploy was done in %s ms.", System.currentTimeMillis() - begin)); 276 } 277 } 278 279 @Override 280 public ReloadResult reloadBundles(ReloadContext context) throws BundleException { 281 ReloadResult result = new ReloadResult(); 282 List<String> bundlesNamesToUndeploy = context.bundlesNamesToUndeploy; 283 284 Watch watch = new Watch(new LinkedHashMap<>()).start(); 285 if (log.isInfoEnabled()) { 286 StringBuilder builder = new StringBuilder("Before updating Nuxeo server\n"); 287 Framework.getRuntime().getStatusMessage(builder); 288 log.info(builder.toString()); 289 } 290 // get class loader 291 Optional<DevMutableClassLoader> classLoader = Optional.of(getClass().getClassLoader()) 292 .filter(DevMutableClassLoader.class::isInstance) 293 .map(DevMutableClassLoader.class::cast); 294 295 watch.start("flush"); 296 flush(); 297 watch.stop("flush"); 298 299 // Suspend current transaction 300 Transaction tx = TransactionHelper.suspendTransaction(); 301 302 try { 303 // Stop or Standby the component manager 304 ComponentManager componentManager = Framework.getRuntime().getComponentManager(); 305 String reloadStrategy = Framework.getProperty(RELOAD_STRATEGY_PARAMETER, RELOAD_STRATEGY_VALUE_DEFAULT); 306 if (log.isInfoEnabled()) { 307 log.info("Component reload strategy=" + reloadStrategy); 308 } 309 310 watch.start("stop/standby"); 311 log.info("Before stop/standby component manager"); 312 if (RELOAD_STRATEGY_VALUE_RESTART.equals(reloadStrategy)) { 313 componentManager.stop(); 314 } else { 315 // standby strategy by default 316 componentManager.standby(); 317 } 318 log.info("After stop/standby component manager"); 319 watch.stop("stop/standby"); 320 321 // Undeploy bundles 322 if (!bundlesNamesToUndeploy.isEmpty()) { 323 watch.start("undeploy-bundles"); 324 log.info("Before undeploy bundles"); 325 logComponentManagerStatus(); 326 327 result.merge(_undeployBundles(bundlesNamesToUndeploy)); 328 componentManager.unstash(); 329 330 // Clear the class loader 331 classLoader.ifPresent(DevMutableClassLoader::clearPreviousClassLoader); 332 // TODO shall we do a GC here ? see DevFrameworkBootstrap#clearClassLoader 333 334 log.info("After undeploy bundles"); 335 logComponentManagerStatus(); 336 watch.stop("undeploy-bundles"); 337 } 338 339 watch.start("delete-copy"); 340 // Delete old bundles 341 log.info("Before delete-copy"); 342 List<URL> urlsToRemove = result.undeployedBundles.stream() 343 .map(Bundle::getLocation) 344 .map(File::new) 345 .peek(File::delete) 346 .map(this::toURL) 347 .collect(Collectors.toList()); 348 // Then copy new ones 349 List<File> bundlesToDeploy = copyBundlesToDeploy(context); 350 List<URL> urlsToAdd = bundlesToDeploy.stream().map(this::toURL).collect(Collectors.toList()); 351 log.info("After delete-copy"); 352 watch.stop("delete-copy"); 353 354 // Reload resources 355 watch.start("reload-resources"); 356 Framework.reloadResourceLoader(urlsToAdd, urlsToRemove); 357 watch.stop("reload-resources"); 358 359 // Deploy bundles 360 if (!bundlesToDeploy.isEmpty()) { 361 watch.start("deploy-bundles"); 362 log.info("Before deploy bundles"); 363 logComponentManagerStatus(); 364 365 // Fill the class loader 366 classLoader.ifPresent(cl -> cl.addClassLoader(urlsToAdd.toArray(new URL[0]))); 367 368 result.merge(_deployBundles(bundlesToDeploy)); 369 componentManager.unstash(); 370 371 log.info("After deploy bundles"); 372 logComponentManagerStatus(); 373 watch.stop("deploy-bundles"); 374 } 375 376 // Start or Resume the component manager 377 watch.start("start/resume"); 378 log.info("Before start/resume component manager"); 379 if (RELOAD_STRATEGY_VALUE_RESTART.equals(reloadStrategy)) { 380 componentManager.start(); 381 } else { 382 // standby strategy by default 383 componentManager.resume(); 384 } 385 log.info("After start/resume component manager"); 386 watch.stop("start/resume"); 387 388 try { 389 // run deployment preprocessor 390 watch.start("deployment-preprocessor"); 391 runDeploymentPreprocessor(); 392 watch.stop("deployment-preprocessor"); 393 } catch (IOException e) { 394 throw new BundleException("Unable to run deployment preprocessor", e); 395 } 396 397 try { 398 // reload 399 watch.start("reload-properties"); 400 reloadProperties(); 401 watch.stop("reload-properties"); 402 } catch (IOException e) { 403 throw new BundleException("Unable to reload properties", e); 404 } 405 } finally { 406 TransactionHelper.resumeTransaction(tx); 407 } 408 if (log.isInfoEnabled()) { 409 StringBuilder builder = new StringBuilder("After updating Nuxeo server\n"); 410 Framework.getRuntime().getStatusMessage(builder); 411 log.info(builder.toString()); 412 } 413 414 watch.stop(); 415 if (log.isInfoEnabled()) { 416 StringBuilder message = new StringBuilder(); 417 message.append("Hot reload was done in ") 418 .append(watch.getTotal().elapsed(TimeUnit.MILLISECONDS)) 419 .append(" ms, detailed steps:"); 420 Stream.of(watch.getIntervals()) 421 .forEach(i -> message.append("\n- ") 422 .append(i.getName()) 423 .append(": ") 424 .append(i.elapsed(TimeUnit.MILLISECONDS)) 425 .append(" ms")); 426 log.info(message.toString()); 427 } 428 return result; 429 } 430 431 protected List<File> copyBundlesToDeploy(ReloadContext context) throws BundleException { 432 List<File> bundlesToDeploy = new ArrayList<>(); 433 Path homePath = Framework.getRuntime().getHome().toPath(); 434 Path destinationPath = homePath.resolve(context.bundlesDestination); 435 try { 436 Files.createDirectories(destinationPath); 437 for (File bundle : context.bundlesToDeploy) { 438 Path bundlePath = bundle.toPath(); 439 // check if the bundle is located under the desired destination 440 // if not copy it to the desired destination 441 if (!bundlePath.startsWith(destinationPath)) { 442 if (Files.isDirectory(bundlePath)) { 443 // If it's a directory, assume that it's an exploded jar 444 bundlePath = JarUtils.zipDirectory(bundlePath, 445 destinationPath.resolve("hotreload-bundle-" + System.currentTimeMillis() + ".jar"), 446 StandardCopyOption.REPLACE_EXISTING); 447 } else { 448 bundlePath = destinationPath.resolve(bundle.getName()); 449 // JDK nio Files will replace the existing file (if destination already exists) which is an 450 // an issue on Windows cause you can't replace a file used by the JVM 451 // so use commons-io instead because it will override the content by using a FileInputStream 452 // instead of replacing the file 453 FileUtils.copyFile(bundle, bundlePath.toFile(), false); 454 } 455 } 456 bundlesToDeploy.add(bundlePath.toFile()); 457 } 458 return bundlesToDeploy; 459 } catch (IOException e) { 460 throw new BundleException("Unable to copy bundles to " + destinationPath, e); 461 } 462 } 463 464 /* 465 * TODO Change this method name when deployBundles will be removed. 466 */ 467 protected ReloadResult _deployBundles(List<File> bundlesToDeploy) throws BundleException { 468 ReloadResult result = new ReloadResult(); 469 BundleContext bundleContext = getBundleContext(); 470 for (File file : bundlesToDeploy) { 471 String path = file.getAbsolutePath(); 472 if (log.isInfoEnabled()) { 473 log.info(String.format("Before deploy bundle for file at '%s'", path)); 474 } 475 Bundle bundle = bundleContext.installBundle(path); 476 if (bundle == null) { 477 // TODO check why this is necessary, our implementation always return sth 478 throw new IllegalArgumentException("Could not find a valid bundle at path: " + path); 479 } 480 bundle.start(); 481 result.deployedBundles.add(bundle); 482 if (log.isInfoEnabled()) { 483 log.info(String.format("Deploy done for bundle with name '%s'", bundle.getSymbolicName())); 484 } 485 } 486 return result; 487 } 488 489 /* 490 * TODO Change this method name when undeployBundles will be removed. 491 */ 492 protected ReloadResult _undeployBundles(List<String> bundleNames) throws BundleException { 493 ReloadResult result = new ReloadResult(); 494 BundleContext ctx = getBundleContext(); 495 ServiceReference ref = ctx.getServiceReference(PackageAdmin.class.getName()); 496 PackageAdmin srv = (PackageAdmin) ctx.getService(ref); 497 try { 498 for (String bundleName : bundleNames) { 499 for (Bundle bundle : srv.getBundles(bundleName, null)) { 500 if (bundle != null && bundle.getState() == Bundle.ACTIVE) { 501 if (log.isInfoEnabled()) { 502 log.info(String.format("Before undeploy bundle with name '%s'.", bundleName)); 503 } 504 bundle.stop(); 505 bundle.uninstall(); 506 result.undeployedBundles.add(bundle); 507 if (log.isInfoEnabled()) { 508 log.info(String.format("After undeploy bundle with name '%s'.", bundleName)); 509 } 510 } 511 } 512 } 513 } finally { 514 ctx.ungetService(ref); 515 } 516 return result; 517 } 518 519 /** 520 * This method needs to be called before bundle uninstallation, otherwise {@link Bundle#getLocation()} throw a NPE. 521 */ 522 protected URL toURL(Bundle bundle) { 523 String location = bundle.getLocation(); 524 File file = new File(location); 525 return toURL(file); 526 } 527 528 protected URL toURL(File file) { 529 try { 530 return file.toURI().toURL(); 531 } catch (MalformedURLException e) { 532 throw new RuntimeServiceException(e); 533 } 534 } 535 536 /** 537 * Logs the {@link ComponentManager} status. 538 */ 539 protected void logComponentManagerStatus() { 540 if (log.isDebugEnabled()) { 541 StringBuilder builder = new StringBuilder("ComponentManager status:\n"); 542 Framework.getRuntime().getStatusMessage(builder); 543 log.debug(builder.toString()); 544 } 545 } 546 547 @Override 548 public Long lastFlushed() { 549 return lastFlushed; 550 } 551 552 /** 553 * Sets the last date date to current date timestamp 554 * 555 * @since 5.6 556 */ 557 protected void setFlushedNow() { 558 lastFlushed = Long.valueOf(System.currentTimeMillis()); 559 } 560 561 /** 562 * @deprecated since 5.6, use {@link #runDeploymentPreprocessor()} instead. Keep it as compatibility code until 563 * NXP-9642 is done. 564 */ 565 @Override 566 @Deprecated 567 public void installWebResources(File file) throws IOException { 568 log.info("Install web resources"); 569 if (file.isDirectory()) { 570 File war = new File(file, "web"); 571 war = new File(war, "nuxeo.war"); 572 if (war.isDirectory()) { 573 org.nuxeo.common.utils.FileUtils.copyTree(war, getAppDir()); 574 } else { 575 // compatibility mode with studio 1.5 - see NXP-6186 576 war = new File(file, "nuxeo.war"); 577 if (war.isDirectory()) { 578 org.nuxeo.common.utils.FileUtils.copyTree(war, getAppDir()); 579 } 580 } 581 } else if (file.isFile()) { // a jar 582 File war = getWarDir(); 583 ZipUtils.unzip("web/nuxeo.war", file, war); 584 // compatibility mode with studio 1.5 - see NXP-6186 585 ZipUtils.unzip("nuxeo.war", file, war); 586 } 587 } 588 589 @Override 590 public void runDeploymentPreprocessor() throws IOException { 591 log.info("Start running deployment preprocessor"); 592 String rootPath = Environment.getDefault().getRuntimeHome().getAbsolutePath(); 593 File root = new File(rootPath); 594 DeploymentPreprocessor processor = new DeploymentPreprocessor(root); 595 // initialize 596 processor.init(); 597 // and predeploy 598 processor.predeploy(); 599 log.info("Deployment preprocessing done"); 600 } 601 602 protected static File getAppDir() { 603 return Environment.getDefault().getConfig().getParentFile(); 604 } 605 606 protected static File getWarDir() { 607 return new File(getAppDir(), "nuxeo.war"); 608 } 609 610 @Override 611 public String getOSGIBundleName(File file) { 612 Manifest mf = JarUtils.getManifest(file); 613 if (mf == null) { 614 return null; 615 } 616 String bundleName = mf.getMainAttributes().getValue("Bundle-SymbolicName"); 617 if (bundleName == null) { 618 return null; 619 } 620 int index = bundleName.indexOf(';'); 621 if (index > -1) { 622 bundleName = bundleName.substring(0, index); 623 } 624 return bundleName; 625 } 626 627 /** 628 * @deprecated since 9.3 should not be needed anymore 629 */ 630 @Deprecated 631 protected void triggerReloadWithNewTransaction(String eventId) throws InterruptedException { 632 if (TransactionHelper.isTransactionMarkedRollback()) { 633 throw new AssertionError("The calling transaction is marked rollback"); 634 } 635 // we need to commit or rollback transaction because suspending it leads to a lock/errors when acquiring a new 636 // connection during the datasource reload 637 boolean hasTransaction = TransactionHelper.isTransactionActiveOrMarkedRollback(); 638 if (hasTransaction) { 639 TransactionHelper.commitOrRollbackTransaction(); 640 } 641 try { 642 TransactionHelper.runInTransaction(() -> triggerReload(eventId)); 643 } finally { 644 // start a new transaction only if one already existed 645 // this is because there's no user transaction when coming from SDK 646 if (hasTransaction) { 647 TransactionHelper.startTransaction(); 648 } 649 } 650 } 651 652 /** 653 * @deprecated since 9.3 should not be needed anymore 654 */ 655 @Deprecated 656 protected void triggerReload(String eventId) { 657 log.info("About to send reload event for id: " + eventId); 658 Framework.getService(EventService.class) 659 .sendEvent(new Event(RELOAD_TOPIC, BEFORE_RELOAD_EVENT_ID, this, null)); 660 try { 661 Framework.getService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, eventId, this, null)); 662 } finally { 663 Framework.getService(EventService.class) 664 .sendEvent(new Event(RELOAD_TOPIC, AFTER_RELOAD_EVENT_ID, this, null)); 665 log.info("Returning from reload for event id: " + eventId); 666 } 667 } 668}