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 <[email protected]> 019 */ 020package org.nuxeo.runtime.tomcat.dev; 021 022import java.io.BufferedWriter; 023import java.io.File; 024import java.io.FileInputStream; 025import java.io.IOException; 026import java.lang.management.ManagementFactory; 027import java.lang.reflect.Method; 028import java.net.URI; 029import java.net.URL; 030import java.nio.file.CopyOption; 031import java.nio.file.FileSystem; 032import java.nio.file.FileSystems; 033import java.nio.file.FileVisitResult; 034import java.nio.file.Files; 035import java.nio.file.Path; 036import java.nio.file.SimpleFileVisitor; 037import java.nio.file.attribute.BasicFileAttributes; 038import java.util.ArrayList; 039import java.util.Arrays; 040import java.util.Collections; 041import java.util.HashMap; 042import java.util.Iterator; 043import java.util.List; 044import java.util.Map; 045import java.util.Timer; 046import java.util.TimerTask; 047 048import javax.management.JMException; 049import javax.management.MBeanServer; 050import javax.management.ObjectName; 051 052import org.apache.commons.logging.Log; 053import org.apache.commons.logging.LogFactory; 054import org.nuxeo.osgi.application.FrameworkBootstrap; 055import org.nuxeo.osgi.application.MutableClassLoader; 056 057/** 058 * @author <a href="mailto:[email protected]">Bogdan Stefanescu</a> 059 */ 060public class DevFrameworkBootstrap extends FrameworkBootstrap implements DevBundlesManager { 061 062 public static final String DEV_BUNDLES_NAME = "org.nuxeo:type=sdk,name=dev-bundles"; 063 064 public static final String WEB_RESOURCES_NAME = "org.nuxeo:type=sdk,name=web-resources"; 065 066 public static final String USE_COMPAT_HOT_RELOAD = "nuxeo.hotreload.compat.mechanism"; 067 068 protected static final String DEV_BUNDLES_CP = "dev-bundles/*"; 069 070 protected final Log log = LogFactory.getLog(DevFrameworkBootstrap.class); 071 072 protected DevBundle[] devBundles; 073 074 protected Timer bundlesCheck; 075 076 protected long lastModified = 0; 077 078 protected ReloadServiceInvoker reloadServiceInvoker; 079 080 protected File devBundlesFile; 081 082 protected final File seamdev; 083 084 protected final File webclasses; 085 086 protected boolean compatHotReload; 087 088 public DevFrameworkBootstrap(MutableClassLoader cl, File home) throws IOException { 089 super(cl, home); 090 devBundlesFile = new File(home, "dev.bundles"); 091 seamdev = new File(home, "nuxeo.war/WEB-INF/dev"); 092 webclasses = new File(home, "nuxeo.war/WEB-INF/classes"); 093 devBundles = new DevBundle[0]; 094 } 095 096 @Override 097 public void start(MutableClassLoader cl) throws ReflectiveOperationException, IOException, JMException { 098 // check if we have dev. bundles or libs to deploy and add them to the 099 // classpath 100 preloadDevBundles(); 101 // start the framework 102 super.start(cl); 103 ClassLoader loader = (ClassLoader) this.loader; 104 reloadServiceInvoker = new ReloadServiceInvoker(loader); 105 compatHotReload = new FrameworkInvoker(loader).isBooleanPropertyTrue(USE_COMPAT_HOT_RELOAD); 106 writeComponentIndex(); 107 postloadDevBundles(); // start dev bundles if any 108 String installReloadTimerOption = (String) env.get(INSTALL_RELOAD_TIMER); 109 if (installReloadTimerOption != null && Boolean.parseBoolean(installReloadTimerOption)) { 110 toggleTimer(); 111 } 112 MBeanServer server = ManagementFactory.getPlatformMBeanServer(); 113 server.registerMBean(this, new ObjectName(DEV_BUNDLES_NAME)); 114 server.registerMBean(cl, new ObjectName(WEB_RESOURCES_NAME)); 115 } 116 117 @Override 118 protected void initializeEnvironment() throws IOException { 119 super.initializeEnvironment(); 120 // add the dev-bundles to classpath 121 env.computeIfPresent(BUNDLES, (k, v) -> v + ":" + DEV_BUNDLES_CP); 122 } 123 124 @Override 125 public void toggleTimer() { 126 // start reload timer 127 if (isTimerRunning()) { 128 bundlesCheck.cancel(); 129 bundlesCheck = null; 130 } else { 131 bundlesCheck = new Timer("Dev Bundles Loader"); 132 bundlesCheck.scheduleAtFixedRate(new TimerTask() { 133 @Override 134 public void run() { 135 try { 136 loadDevBundles(); 137 } catch (RuntimeException e) { 138 log.error("Failed to reload dev bundles", e); 139 } 140 } 141 }, 2000, 2000); 142 } 143 } 144 145 @Override 146 public boolean isTimerRunning() { 147 return bundlesCheck != null; 148 } 149 150 @Override 151 public void stop(MutableClassLoader cl) throws ReflectiveOperationException, JMException { 152 if (bundlesCheck != null) { 153 bundlesCheck.cancel(); 154 bundlesCheck = null; 155 } 156 try { 157 MBeanServer server = ManagementFactory.getPlatformMBeanServer(); 158 server.unregisterMBean(new ObjectName(DEV_BUNDLES_NAME)); 159 server.unregisterMBean(new ObjectName(WEB_RESOURCES_NAME)); 160 } finally { 161 super.stop(cl); 162 } 163 } 164 165 @Override 166 public String getDevBundlesLocation() { 167 return devBundlesFile.getAbsolutePath(); 168 } 169 170 /** 171 * Load the development bundles and libs if any in the classpath before starting the framework. 172 * 173 * @deprecated since 9.3, we now have a new mechanism to hot reload bundles from {@link #devBundlesFile}. The new 174 * mechanism copies bundles to nxserver/bundles, so it's now useless to preload dev bundles as they're 175 * deployed as a regular bundle. 176 */ 177 @Deprecated 178 protected void preloadDevBundles() throws IOException { 179 if (!compatHotReload) { 180 return; 181 } 182 if (!devBundlesFile.isFile()) { 183 return; 184 } 185 lastModified = devBundlesFile.lastModified(); 186 devBundles = DevBundle.parseDevBundleLines(new FileInputStream(devBundlesFile)); 187 if (devBundles.length > 0) { 188 installNewClassLoader(devBundles); 189 } 190 } 191 192 /** 193 * @deprecated since 9.3, we now have a new mechanism to hot reload bundles from {@link #devBundlesFile}. The new 194 * mechanism copies bundles to nxserver/bundles, so it's now useless to postload dev bundles as they're 195 * deployed as a regular bundle. 196 */ 197 @Deprecated 198 protected void postloadDevBundles() throws ReflectiveOperationException { 199 if (!compatHotReload) { 200 return; 201 } 202 if (devBundles.length > 0) { 203 reloadServiceInvoker.hotDeployBundles(devBundles); 204 } 205 } 206 207 @Override 208 public void loadDevBundles() { 209 long tm = devBundlesFile.lastModified(); 210 if (lastModified >= tm) { 211 return; 212 } 213 lastModified = tm; 214 try { 215 reloadDevBundles(DevBundle.parseDevBundleLines(new FileInputStream(devBundlesFile))); 216 } catch (ReflectiveOperationException | IOException e) { 217 throw new RuntimeException("Failed to reload dev bundles", e); 218 } 219 } 220 221 @Override 222 public void resetDevBundles(String path) { 223 try { 224 devBundlesFile = new File(path); 225 lastModified = 0; 226 loadDevBundles(); 227 } catch (RuntimeException e) { 228 log.error("Unable to reset dev bundles", e); 229 } 230 } 231 232 @Override 233 public DevBundle[] getDevBundles() { 234 return devBundles; 235 } 236 237 protected synchronized void reloadDevBundles(DevBundle[] bundles) throws ReflectiveOperationException, IOException { 238 long begin = System.currentTimeMillis(); 239 240 if (compatHotReload) { 241 if (devBundles.length > 0) { // clear last context 242 try { 243 reloadServiceInvoker.hotUndeployBundles(devBundles); 244 clearClassLoader(); 245 } finally { 246 devBundles = new DevBundle[0]; 247 } 248 } 249 250 if (bundles.length > 0) { // create new context 251 try { 252 installNewClassLoader(bundles); 253 reloadServiceInvoker.hotDeployBundles(bundles); 254 } finally { 255 devBundles = bundles; 256 } 257 } 258 } else { 259 // symbolicName of bundlesToDeploy will be filled by hotReloadBundles before hot reload 260 // -> this allows server to be hot reloaded again in case of errors 261 // if everything goes fine, bundlesToDeploy will be replaced by result of hot reload containing symbolic 262 // name and the new bundle path 263 DevBundle[] bundlesToDeploy = bundles; 264 try { 265 bundlesToDeploy = reloadServiceInvoker.hotReloadBundles(devBundles, bundlesToDeploy); 266 267 // write the new dev bundles location to the file 268 writeDevBundles(bundlesToDeploy); 269 } finally { 270 devBundles = bundlesToDeploy; 271 } 272 } 273 if (log.isInfoEnabled()) { 274 log.info(String.format("Hot reload has been run in %s ms", System.currentTimeMillis() - begin)); 275 } 276 } 277 278 /** 279 * Writes to the {@link #devBundlesFile} the input {@code devBundles} by replacing the former file. 280 * <p /> 281 * This method will {@link #toggleTimer() toggle} the file update check timer if needed. 282 * 283 * @since 9.3 284 */ 285 protected void writeDevBundles(DevBundle[] devBundles) throws IOException { 286 boolean timerExists = isTimerRunning(); 287 if (timerExists) { 288 // timer is running, we need to stop it before editing the file 289 toggleTimer(); 290 } 291 // for nuxeo-cli needs, we need to keep comments 292 List<String> lines = Files.readAllLines(devBundlesFile.toPath()); 293 // newBufferedWriter without OpenOption will create/truncate if exist the target file 294 try (BufferedWriter writer = Files.newBufferedWriter(devBundlesFile.toPath())) { 295 Iterator<DevBundle> devBundlesIt = Arrays.asList(devBundles).iterator(); 296 for (String line : lines) { 297 if (line.startsWith("#")) { 298 writer.write(line); 299 } else if (devBundlesIt.hasNext()) { 300 writer.write(devBundlesIt.next().toString()); 301 } else { 302 // there's a sync problem between dev.bundles file and nuxeo runtime 303 // comment this bundle to not break further attempt 304 writer.write("# "); 305 writer.write(line); 306 } 307 writer.write(System.lineSeparator()); 308 } 309 } finally { 310 if (timerExists) { 311 // restore the time status 312 lastModified = System.currentTimeMillis(); 313 toggleTimer(); 314 } 315 } 316 } 317 318 /** 319 * Zips recursively the content of {@code source} to the {@code target} zip file. 320 * 321 * @since 9.3 322 */ 323 protected Path zipDirectory(Path source, Path target, CopyOption... options) throws IOException { 324 if (!source.toFile().isDirectory()) { 325 throw new IllegalArgumentException("Source argument must be a directory to zip"); 326 } 327 // locate file system by using the syntax defined in java.net.JarURLConnection 328 URI uri = URI.create("jar:file:" + target.toString()); 329 330 try (FileSystem zipfs = FileSystems.newFileSystem(uri, Collections.singletonMap("create", "true"))) { 331 Files.walkFileTree(source, new SimpleFileVisitor<Path>() { 332 333 @Override 334 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { 335 if (source.equals(dir)) { 336 // don't process root element 337 return FileVisitResult.CONTINUE; 338 } 339 return visitFile(dir, attrs); 340 } 341 342 @Override 343 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 344 // retrieve the destination path in zip 345 Path relativePath = source.relativize(file); 346 Path pathInZipFile = zipfs.getPath(relativePath.toString()); 347 // copy a file into the zip file 348 Files.copy(file, pathInZipFile, options); 349 return FileVisitResult.CONTINUE; 350 } 351 352 }); 353 } 354 return target; 355 } 356 357 /** 358 * @deprecated since 9.3 not needed anymore, here for backward compatibility, see {@link #compatHotReload} 359 */ 360 @Deprecated 361 protected void clearClassLoader() { 362 NuxeoDevWebappClassLoader devLoader = (NuxeoDevWebappClassLoader) loader; 363 devLoader.clear(); 364 } 365 366 /** 367 * @deprecated since 9.3 not needed anymore, here for backward compatibility, see {@link #compatHotReload} 368 */ 369 @Deprecated 370 protected void installNewClassLoader(DevBundle[] bundles) { 371 List<URL> jarUrls = new ArrayList<>(); 372 List<File> seamDirs = new ArrayList<>(); 373 List<File> resourceBundleFragments = new ArrayList<>(); 374 // filter dev bundles types 375 for (DevBundle bundle : bundles) { 376 if (bundle.devBundleType.isJar) { 377 try { 378 jarUrls.add(bundle.url()); 379 } catch (IOException e) { 380 log.error("Cannot install " + bundle); 381 } 382 } else if (bundle.devBundleType == DevBundleType.Seam) { 383 seamDirs.add(bundle.file()); 384 } else if (bundle.devBundleType == DevBundleType.ResourceBundleFragment) { 385 resourceBundleFragments.add(bundle.file()); 386 } 387 } 388 389 // install class loader 390 NuxeoDevWebappClassLoader devLoader = (NuxeoDevWebappClassLoader) loader; 391 devLoader.createLocalClassLoader(jarUrls.toArray(new URL[jarUrls.size()])); 392 393 // install seam classes in hot sync folder 394 try { 395 installSeamClasses(seamDirs.toArray(new File[seamDirs.size()])); 396 } catch (IOException e) { 397 log.error("Cannot install seam classes in hotsync folder", e); 398 } 399 400 // install l10n resources 401 try { 402 installResourceBundleFragments(resourceBundleFragments); 403 } catch (IOException e) { 404 log.error("Cannot install l10n resources", e); 405 } 406 } 407 408 public void writeComponentIndex() { 409 File file = new File(home.getParentFile(), "sdk"); 410 file.mkdirs(); 411 file = new File(file, "components.index"); 412 try { 413 Method m = getClassLoader().loadClass("org.nuxeo.runtime.model.impl.ComponentRegistrySerializer") 414 .getMethod("writeIndex", File.class); 415 m.invoke(null, file); 416 } catch (ReflectiveOperationException t) { 417 // ignore 418 } 419 } 420 421 /** 422 * @deprecated since 9.3 not needed anymore, here for backward compatibility, see {@link #compatHotReload} 423 */ 424 @Deprecated 425 public void installSeamClasses(File[] dirs) throws IOException { 426 if (seamdev.exists()) { 427 IOUtils.deleteTree(seamdev); 428 } 429 seamdev.mkdirs(); 430 for (File dir : dirs) { 431 IOUtils.copyTree(dir, seamdev); 432 } 433 } 434 435 /** 436 * @deprecated since 9.3 not needed anymore, here for backward compatibility, see {@link #compatHotReload} 437 */ 438 @Deprecated 439 public void installResourceBundleFragments(List<File> files) throws IOException { 440 Map<String, List<File>> fragments = new HashMap<>(); 441 442 for (File file : files) { 443 String name = resourceBundleName(file); 444 if (!fragments.containsKey(name)) { 445 fragments.put(name, new ArrayList<>()); 446 } 447 fragments.get(name).add(file); 448 } 449 for (String name : fragments.keySet()) { 450 IOUtils.appendResourceBundleFragments(name, fragments.get(name), webclasses); 451 } 452 } 453 454 /** 455 * @deprecated since 9.3 not needed anymore, here for backward compatibility, see {@link #compatHotReload} 456 */ 457 @Deprecated 458 protected static String resourceBundleName(File file) { 459 String name = file.getName(); 460 return name.substring(name.lastIndexOf('-') + 1); 461 } 462 463}