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