001/* 002 * (C) Copyright 2006-2016 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.Collections; 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).sendEvent( 113 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).sendEvent(new Event(RELOAD_TOPIC, FLUSH_EVENT_ID, this, null)); 120 flushJaasCache(); 121 setFlushedNow(); 122 } 123 124 @Override 125 public void flushJaasCache() { 126 log.info("Flush the JAAS cache"); 127 Framework.getLocalService(EventService.class).sendEvent( 128 new Event("usermanager", "user_changed", this, "Deployer")); 129 setFlushedNow(); 130 } 131 132 @Override 133 public void flushSeamComponents() { 134 log.info("Flush Seam components"); 135 Framework.getLocalService(EventService.class).sendEvent( 136 new Event(RELOAD_TOPIC, FLUSH_SEAM_EVENT_ID, this, null)); 137 setFlushedNow(); 138 } 139 140 @Override 141 public String deployBundle(File file) throws BundleException { 142 return deployBundle(file, false); 143 } 144 145 @Override 146 public String deployBundle(File file, boolean reloadResourceClasspath) throws BundleException { 147 String name = getOSGIBundleName(file); 148 if (name == null) { 149 log.error(String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", file.getAbsolutePath())); 150 return null; 151 } 152 153 String path = file.getAbsolutePath(); 154 155 log.info(String.format("Before deploy bundle for file at '%s'\n" + "%s", path, getRuntimeStatus())); 156 157 if (reloadResourceClasspath) { 158 URL url; 159 try { 160 url = new File(path).toURI().toURL(); 161 } catch (MalformedURLException e) { 162 throw new RuntimeException(e); 163 } 164 Framework.reloadResourceLoader(Collections.singletonList(url), null); 165 } 166 167 // check if this is a bundle first 168 Bundle newBundle = getBundleContext().installBundle(path); 169 if (newBundle == null) { 170 throw new IllegalArgumentException("Could not find a valid bundle at path: " + path); 171 } 172 Transaction tx = TransactionHelper.suspendTransaction(); 173 try { 174 newBundle.start(); 175 } finally { 176 TransactionHelper.resumeTransaction(tx); 177 } 178 179 log.info(String.format("Deploy done for bundle with name '%s'.\n" + "%s", newBundle.getSymbolicName(), 180 getRuntimeStatus())); 181 182 return newBundle.getSymbolicName(); 183 } 184 185 @Override 186 public void undeployBundle(File file, boolean reloadResources) throws BundleException { 187 String name = getOSGIBundleName(file); 188 String path = file.getAbsolutePath(); 189 if (name == null) { 190 log.error(String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", path)); 191 return; 192 } 193 194 undeployBundle(name); 195 196 if (reloadResources) { 197 URL url; 198 try { 199 url = new File(path).toURI().toURL(); 200 } catch (MalformedURLException e) { 201 throw new RuntimeException(e); 202 } 203 Framework.reloadResourceLoader(null, Collections.singletonList(url)); 204 } 205 } 206 207 @Override 208 public void undeployBundle(String bundleName) throws BundleException { 209 if (bundleName == null) { 210 // ignore 211 return; 212 } 213 log.info(String.format("Before undeploy bundle with name '%s'.\n" + "%s", bundleName, getRuntimeStatus())); 214 BundleContext ctx = getBundleContext(); 215 ServiceReference ref = ctx.getServiceReference(PackageAdmin.class.getName()); 216 PackageAdmin srv = (PackageAdmin) ctx.getService(ref); 217 try { 218 for (Bundle b : srv.getBundles(bundleName, null)) { 219 if (b != null && b.getState() == Bundle.ACTIVE) { 220 Transaction tx = TransactionHelper.suspendTransaction(); 221 try { 222 b.stop(); 223 b.uninstall(); 224 } finally { 225 TransactionHelper.resumeTransaction(tx); 226 } 227 } 228 } 229 } finally { 230 ctx.ungetService(ref); 231 } 232 log.info(String.format("Undeploy done.\n" + "%s", getRuntimeStatus())); 233 } 234 235 @Override 236 public Long lastFlushed() { 237 return lastFlushed; 238 } 239 240 /** 241 * Sets the last date date to current date timestamp 242 * 243 * @since 5.6 244 */ 245 protected void setFlushedNow() { 246 lastFlushed = System.currentTimeMillis(); 247 } 248 249 /** 250 * @deprecated since 5.6, use {@link #runDeploymentPreprocessor()} instead 251 */ 252 @Override 253 @Deprecated 254 public void installWebResources(File file) throws IOException { 255 log.info("Install web resources"); 256 if (file.isDirectory()) { 257 File war = new File(file, "web"); 258 war = new File(war, "nuxeo.war"); 259 if (war.isDirectory()) { 260 FileUtils.copyTree(war, getAppDir()); 261 } else { 262 // compatibility mode with studio 1.5 - see NXP-6186 263 war = new File(file, "nuxeo.war"); 264 if (war.isDirectory()) { 265 FileUtils.copyTree(war, getAppDir()); 266 } 267 } 268 } else if (file.isFile()) { // a jar 269 File war = getWarDir(); 270 ZipUtils.unzip("web/nuxeo.war", file, war); 271 // compatibility mode with studio 1.5 - see NXP-6186 272 ZipUtils.unzip("nuxeo.war", file, war); 273 } 274 } 275 276 @Override 277 public void runDeploymentPreprocessor() throws IOException { 278 if (log.isDebugEnabled()) { 279 log.debug("Start running deployment preprocessor"); 280 } 281 String rootPath = Environment.getDefault().getRuntimeHome().getAbsolutePath(); 282 File root = new File(rootPath); 283 DeploymentPreprocessor processor = new DeploymentPreprocessor(root); 284 // initialize 285 processor.init(); 286 // and predeploy 287 processor.predeploy(); 288 if (log.isDebugEnabled()) { 289 log.debug("Deployment preprocessing done"); 290 } 291 } 292 293 protected static File getAppDir() { 294 return Environment.getDefault().getConfig().getParentFile(); 295 } 296 297 protected static File getWarDir() { 298 return new File(getAppDir(), "nuxeo.war"); 299 } 300 301 @Override 302 public String getOSGIBundleName(File file) { 303 Manifest mf = JarUtils.getManifest(file); 304 if (mf == null) { 305 return null; 306 } 307 String bundleName = mf.getMainAttributes().getValue("Bundle-SymbolicName"); 308 if (bundleName == null) { 309 return null; 310 } 311 int index = bundleName.indexOf(';'); 312 if (index > -1) { 313 bundleName = bundleName.substring(0, index); 314 } 315 return bundleName; 316 } 317 318 protected String getRuntimeStatus() { 319 StringBuilder msg = new StringBuilder(); 320 RuntimeService runtime = Framework.getRuntime(); 321 runtime.getStatusMessage(msg); 322 return msg.toString(); 323 } 324 325 protected void triggerReloadWithNewTransaction(String id) { 326 if (TransactionHelper.isTransactionMarkedRollback()) { 327 throw new AssertionError("The calling transaction is marked rollback"); 328 } 329 // we need to commit or rollback transaction because suspending it leads to a lock/errors when acquiring a new 330 // connection during the datasource reload 331 TransactionHelper.commitOrRollbackTransaction(); 332 TransactionHelper.startTransaction(); 333 try { 334 try { 335 triggerReloadWithPassivate(id); 336 } catch (RuntimeException cause) { 337 TransactionHelper.setTransactionRollbackOnly(); 338 throw cause; 339 } finally { 340 TransactionHelper.commitOrRollbackTransaction(); 341 } 342 } finally { 343 TransactionHelper.startTransaction(); 344 } 345 } 346 347 protected void triggerReloadWithPassivate(String id) { 348 log.info("about to passivate for " + id); 349 Framework.getLocalService(EventService.class).sendEvent( 350 new Event(RELOAD_TOPIC, BEFORE_RELOAD_EVENT_ID, this, null)); 351 try { 352 ServicePassivator.proceed(Duration.ofSeconds(5), Duration.ofSeconds(30), true, () -> { 353 log.info("about to send " + id); 354 Framework.getLocalService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, id, this, null)); 355 }).onFailure( 356 snapshot -> { 357 throw new UnsupportedOperationException("Detected access, should initiate a reboot " 358 + snapshot.toString()); 359 }); 360 } finally { 361 Framework.getLocalService(EventService.class).sendEvent( 362 new Event(RELOAD_TOPIC, AFTER_RELOAD_EVENT_ID, this, null)); 363 log.info("returning from " + id); 364 } 365 } 366}