001/* 002 * (C) Copyright 2006-2010 Nuxeo SA (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 */ 019package org.nuxeo.runtime.reload; 020 021import java.io.File; 022import java.io.IOException; 023import java.net.MalformedURLException; 024import java.net.URL; 025import java.time.Duration; 026import java.util.Arrays; 027import java.util.jar.Manifest; 028 029import javax.transaction.Transaction; 030 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033import org.nuxeo.common.Environment; 034import org.nuxeo.common.utils.FileUtils; 035import org.nuxeo.common.utils.JarUtils; 036import org.nuxeo.common.utils.ZipUtils; 037import org.nuxeo.runtime.RuntimeService; 038import org.nuxeo.runtime.RuntimeServiceException; 039import org.nuxeo.runtime.api.Framework; 040import org.nuxeo.runtime.api.ServicePassivator; 041import org.nuxeo.runtime.deployment.preprocessor.DeploymentPreprocessor; 042import org.nuxeo.runtime.model.ComponentContext; 043import org.nuxeo.runtime.model.DefaultComponent; 044import org.nuxeo.runtime.services.event.Event; 045import org.nuxeo.runtime.services.event.EventService; 046import org.nuxeo.runtime.transaction.TransactionHelper; 047import org.osgi.framework.Bundle; 048import org.osgi.framework.BundleContext; 049import org.osgi.framework.BundleException; 050import org.osgi.framework.ServiceReference; 051import org.osgi.service.packageadmin.PackageAdmin; 052 053/** 054 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 055 */ 056public class ReloadComponent extends DefaultComponent implements ReloadService { 057 058 private static final Log log = LogFactory.getLog(ReloadComponent.class); 059 060 protected static Bundle bundle; 061 062 protected Long lastFlushed; 063 064 public static BundleContext getBundleContext() { 065 return bundle.getBundleContext(); 066 } 067 068 public static Bundle getBundle() { 069 return bundle; 070 } 071 072 @Override 073 public void activate(ComponentContext context) { 074 super.activate(context); 075 bundle = context.getRuntimeContext().getBundle(); 076 } 077 078 @Override 079 public void deactivate(ComponentContext context) { 080 super.deactivate(context); 081 bundle = null; 082 } 083 084 @Override 085 public void reload() { 086 if (log.isDebugEnabled()) { 087 log.debug("Starting reload"); 088 } 089 try { 090 reloadProperties(); 091 } catch (IOException e) { 092 throw new RuntimeServiceException(e); 093 } 094 triggerReloadWithNewTransaction(RELOAD_EVENT_ID); 095 } 096 097 @Override 098 public void reloadProperties() throws IOException { 099 log.info("Reload runtime properties"); 100 Framework.getRuntime().reloadProperties(); 101 } 102 103 @Override 104 public void reloadRepository() { 105 log.info("Reload repository"); 106 triggerReloadWithNewTransaction(RELOAD_REPOSITORIES_ID); 107 } 108 109 @Override 110 public void reloadSeamComponents() { 111 log.info("Reload Seam components"); 112 Framework.getLocalService(EventService.class) 113 .sendEvent(new Event(RELOAD_TOPIC, RELOAD_SEAM_EVENT_ID, this, null)); 114 } 115 116 @Override 117 public void flush() { 118 log.info("Flush caches"); 119 Framework.getLocalService(EventService.class) 120 .sendEvent(new Event(RELOAD_TOPIC, FLUSH_EVENT_ID, this, null)); 121 flushJaasCache(); 122 setFlushedNow(); 123 } 124 125 @Override 126 public void flushJaasCache() { 127 log.info("Flush the JAAS cache"); 128 Framework.getLocalService(EventService.class) 129 .sendEvent(new Event("usermanager", "user_changed", this, "Deployer")); 130 setFlushedNow(); 131 } 132 133 @Override 134 public void flushSeamComponents() { 135 log.info("Flush Seam components"); 136 Framework.getLocalService(EventService.class) 137 .sendEvent(new Event(RELOAD_TOPIC, FLUSH_SEAM_EVENT_ID, this, null)); 138 setFlushedNow(); 139 } 140 141 @Override 142 public String deployBundle(File file) throws BundleException { 143 return deployBundle(file, false); 144 } 145 146 @Override 147 public String deployBundle(File file, boolean reloadResourceClasspath) throws BundleException { 148 String name = getOSGIBundleName(file); 149 if (name == null) { 150 log.error( 151 String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", file.getAbsolutePath())); 152 return null; 153 } 154 155 String path = file.getAbsolutePath(); 156 157 log.info(String.format("Before deploy bundle for file at '%s'\n" + "%s", path, getRuntimeStatus())); 158 159 if (reloadResourceClasspath) { 160 URL url; 161 try { 162 url = new File(path).toURI().toURL(); 163 } catch (MalformedURLException e) { 164 throw new RuntimeException(e); 165 } 166 Framework.reloadResourceLoader(Arrays.asList(url), null); 167 } 168 169 // check if this is a bundle first 170 Bundle newBundle = getBundleContext().installBundle(path); 171 if (newBundle == null) { 172 throw new IllegalArgumentException("Could not find a valid bundle at path: " + path); 173 } 174 Transaction tx = TransactionHelper.suspendTransaction(); 175 try { 176 newBundle.start(); 177 } finally { 178 TransactionHelper.resumeTransaction(tx); 179 } 180 181 log.info(String.format("Deploy done for bundle with name '%s'.\n" + "%s", newBundle.getSymbolicName(), 182 getRuntimeStatus())); 183 184 return newBundle.getSymbolicName(); 185 } 186 187 @Override 188 public void undeployBundle(File file, boolean reloadResources) throws BundleException { 189 String name = getOSGIBundleName(file); 190 String path = file.getAbsolutePath(); 191 if (name == null) { 192 log.error(String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", path)); 193 return; 194 } 195 196 undeployBundle(name); 197 198 if (reloadResources) { 199 URL url; 200 try { 201 url = new File(path).toURI().toURL(); 202 } catch (MalformedURLException e) { 203 throw new RuntimeException(e); 204 } 205 Framework.reloadResourceLoader(null, Arrays.asList(url)); 206 } 207 } 208 209 @Override 210 public void undeployBundle(String bundleName) throws BundleException { 211 if (bundleName == null) { 212 // ignore 213 return; 214 } 215 log.info(String.format("Before undeploy bundle with name '%s'.\n" + "%s", bundleName, getRuntimeStatus())); 216 BundleContext ctx = getBundleContext(); 217 ServiceReference ref = ctx.getServiceReference(PackageAdmin.class.getName()); 218 PackageAdmin srv = (PackageAdmin) ctx.getService(ref); 219 try { 220 for (Bundle b : srv.getBundles(bundleName, null)) { 221 if (b != null && b.getState() == Bundle.ACTIVE) { 222 Transaction tx = TransactionHelper.suspendTransaction(); 223 try { 224 b.stop(); 225 b.uninstall(); 226 } finally { 227 TransactionHelper.resumeTransaction(tx); 228 } 229 } 230 } 231 } finally { 232 ctx.ungetService(ref); 233 } 234 log.info(String.format("Undeploy done.\n" + "%s", getRuntimeStatus())); 235 } 236 237 @Override 238 public Long lastFlushed() { 239 return lastFlushed; 240 } 241 242 /** 243 * Sets the last date date to current date timestamp 244 * 245 * @since 5.6 246 */ 247 protected void setFlushedNow() { 248 lastFlushed = Long.valueOf(System.currentTimeMillis()); 249 } 250 251 /** 252 * @deprecated since 5.6, use {@link #runDeploymentPreprocessor()} instead 253 */ 254 @Override 255 @Deprecated 256 public void installWebResources(File file) throws IOException { 257 log.info("Install web resources"); 258 if (file.isDirectory()) { 259 File war = new File(file, "web"); 260 war = new File(war, "nuxeo.war"); 261 if (war.isDirectory()) { 262 FileUtils.copyTree(war, getAppDir()); 263 } else { 264 // compatibility mode with studio 1.5 - see NXP-6186 265 war = new File(file, "nuxeo.war"); 266 if (war.isDirectory()) { 267 FileUtils.copyTree(war, getAppDir()); 268 } 269 } 270 } else if (file.isFile()) { // a jar 271 File war = getWarDir(); 272 ZipUtils.unzip("web/nuxeo.war", file, war); 273 // compatibility mode with studio 1.5 - see NXP-6186 274 ZipUtils.unzip("nuxeo.war", file, war); 275 } 276 } 277 278 @Override 279 public void runDeploymentPreprocessor() throws IOException { 280 if (log.isDebugEnabled()) { 281 log.debug("Start running deployment preprocessor"); 282 } 283 String rootPath = Environment.getDefault().getRuntimeHome().getAbsolutePath(); 284 File root = new File(rootPath); 285 DeploymentPreprocessor processor = new DeploymentPreprocessor(root); 286 // initialize 287 processor.init(); 288 // and predeploy 289 processor.predeploy(); 290 if (log.isDebugEnabled()) { 291 log.debug("Deployment preprocessing done"); 292 } 293 } 294 295 protected static File getAppDir() { 296 return Environment.getDefault().getConfig().getParentFile(); 297 } 298 299 protected static File getWarDir() { 300 return new File(getAppDir(), "nuxeo.war"); 301 } 302 303 @Override 304 public String getOSGIBundleName(File file) { 305 Manifest mf = JarUtils.getManifest(file); 306 if (mf == null) { 307 return null; 308 } 309 String bundleName = mf.getMainAttributes().getValue("Bundle-SymbolicName"); 310 if (bundleName == null) { 311 return null; 312 } 313 int index = bundleName.indexOf(';'); 314 if (index > -1) { 315 bundleName = bundleName.substring(0, index); 316 } 317 return bundleName; 318 } 319 320 protected String getRuntimeStatus() { 321 StringBuilder msg = new StringBuilder(); 322 RuntimeService runtime = Framework.getRuntime(); 323 runtime.getStatusMessage(msg); 324 return msg.toString(); 325 } 326 327 328 protected void triggerReloadWithNewTransaction(String id) { 329 if (TransactionHelper.isTransactionMarkedRollback()) { 330 throw new AssertionError("The calling transaction is marked rollback"); 331 } 332 Transaction tx = TransactionHelper.suspendTransaction(); 333 TransactionHelper.startTransaction(); 334 try { 335 try { 336 triggerReloadWithPassivate(id); 337 } catch (RuntimeException cause) { 338 TransactionHelper.setTransactionRollbackOnly(); 339 throw cause; 340 } 341 } finally { 342 boolean wasRollbacked = TransactionHelper.isTransactionMarkedRollback(); 343 TransactionHelper.commitOrRollbackTransaction(); 344 TransactionHelper.resumeTransaction(tx); 345 if (TransactionHelper.isTransactionActive()) { 346 if (wasRollbacked) { 347 TransactionHelper.setTransactionRollbackOnly(); 348 } 349 TransactionHelper.commitOrRollbackTransaction(); // should flush 350 TransactionHelper.startTransaction(); 351 } 352 } 353 } 354 355 protected void triggerReloadWithPassivate(String id) { 356 log.info("about to passivate for " + id); 357 Framework.getLocalService(EventService.class) 358 .sendEvent(new Event(RELOAD_TOPIC, BEFORE_RELOAD_EVENT_ID, this, null)); 359 try { 360 ServicePassivator.proceed(Duration.ofSeconds(5), Duration.ofSeconds(30), true, () -> { 361 log.info("about to send " + id); 362 Framework.getLocalService(EventService.class) 363 .sendEvent(new Event(RELOAD_TOPIC, id, this, null)); 364 }) 365 .onFailure(snapshot -> { 366 throw new UnsupportedOperationException( 367 "Detected access, should initiate a reboot " + snapshot.toString()); 368 }); 369 } finally { 370 Framework.getLocalService(EventService.class) 371 .sendEvent(new Event(RELOAD_TOPIC, AFTER_RELOAD_EVENT_ID, this, null)); 372 log.info("returning from " + id); 373 } 374 } 375}