blob: 5d5498e07d9792e9a10a4f00478c6bf640e7085b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sling.launchpad.base.impl;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.felix.framework.Logger;
import org.apache.sling.launchpad.api.StartupHandler;
import org.apache.sling.launchpad.api.StartupListener;
import org.apache.sling.launchpad.api.StartupMode;
import org.apache.sling.launchpad.api.StartupService;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.ServiceReference;
import org.osgi.service.startlevel.StartLevel;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
/**
* The installation listener is listening for
* - framework events
* - events from other services
*
* It notifies listeners about the finish of the startup and
* about an approximate progress.
*
* @see StartupHandler
* @see StartupListener
* @since 2.4.0
*/
@SuppressWarnings("deprecation")
public class DefaultStartupHandler
implements StartupHandler, BundleListener, FrameworkListener, Runnable {
/** Logger. */
private final Logger logger;
/** Marker for finished. */
private final AtomicBoolean finished = new AtomicBoolean(false);
/** Marker if startup should wait */
private final AtomicInteger startupShouldWait = new AtomicInteger(0);
/** The queue for increasing the start level. */
private final BlockingQueue<Boolean> queue = new LinkedBlockingQueue<Boolean>();
/** The start level service. */
private final StartLevel startLevelService;
/** The target start level. */
private final long targetStartLevel;
/** The startup mode. */
private final StartupMode startupMode;
/** Service tracker for startup listeners. */
private final ServiceTracker<StartupListener, StartupListener> listenerTracker;
/** Expected bundle counts. */
private final int expectedBundlesCount;
/** Active bundle set. */
private final Set<String> activeBundles = new HashSet<String>();
/** Bundle Context. */
private final BundleContext bundleContext;
/** Use incremental start level handling. */
private final boolean useIncremental;
/** MBean startup listener. */
private final StartupListener mbeanStartupListener;
/** The started time. */
private final long startedAt;
private volatile Object[] logService;
/**
* Constructor.
* @param context Bundle context
* @param logger Logger
* @param manager The startup manager
*/
public DefaultStartupHandler(final BundleContext context,
final Logger logger,
final StartupManager manager,
final long startedAt) {
this.logger = logger;
this.bundleContext = context;
this.startedAt = startedAt;
this.startupMode = manager.getMode();
this.targetStartLevel = manager.getTargetStartLevel();
StartupListener listener = null;
try {
listener = new MBeanStartupListener();
} catch ( final Exception ignore ) {
// ignore
}
this.mbeanStartupListener = listener;
this.listenerTracker = new ServiceTracker<StartupListener, StartupListener>(context, StartupListener.class,
new ServiceTrackerCustomizer<StartupListener, StartupListener>() {
@Override
public void removedService(final ServiceReference<StartupListener> reference, final StartupListener service) {
context.ungetService(reference);
}
@Override
public void modifiedService(final ServiceReference<StartupListener> reference, final StartupListener service) {
// nothing to do
}
@Override
public StartupListener addingService(final ServiceReference<StartupListener> reference) {
final StartupListener listener = context.getService(reference);
if (listener != null) {
try {
listener.inform(startupMode, finished.get());
} catch (final Throwable t) {
logger.log(Logger.LOG_ERROR, "Error calling StartupListener " + listener, t);
}
}
return listener;
}
});
this.listenerTracker.open();
this.startLevelService = (StartLevel)context.getService(context.getServiceReference(StartLevel.class.getName()));
context.addFrameworkListener(this);
this.useIncremental = this.startupMode != StartupMode.RESTART && manager.isIncrementalStartupEnabled();
if ( !this.useIncremental ) {
final Bundle[] bundles = context.getBundles();
this.expectedBundlesCount = (bundles != null && bundles.length > 0 ? bundles.length : 10);
context.addBundleListener(this);
} else {
this.expectedBundlesCount = 10;
}
this.bundleContext.registerService(StartupHandler.class.getName(), this, null);
this.log(Logger.LOG_INFO, "Started startup handler with target start level="
+ String.valueOf(this.targetStartLevel) + ", and expected bundle count=" + String.valueOf(this.expectedBundlesCount));
final Thread t = new Thread(this);
t.start();
}
/**
* @see org.apache.sling.launchpad.api.StartupHandler#getMode()
*/
@Override
public StartupMode getMode() {
return this.startupMode;
}
/**
* @see org.apache.sling.launchpad.api.StartupHandler#isFinished()
*/
@Override
public boolean isFinished() {
return this.finished.get();
}
/**
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
while ( !this.finished.get() ) {
Boolean doInc = null;
try {
doInc = this.queue.take();
} catch (final InterruptedException e) {
// ignore
}
if ( doInc != null && doInc ) {
// if the installer is idle we first wait
// we have to do this to give the installer or plugins for the installer,
// time to start after the start level has changed
if ( this.startupShouldWait.get() == 0 ) {
this.sleep(2000L);
}
// now we wait until the installer is idle
while ( this.startupShouldWait.get() != 0 ) {
this.sleep(50L);
}
this.incStartLevel();
}
}
}
/**
* Increment the current start level by one
*/
private void incStartLevel() {
final int newLevel = this.startLevelService.getStartLevel() + 1;
this.log(Logger.LOG_DEBUG, "Increasing start level to " + String.valueOf(newLevel));
this.startLevelService.setStartLevel(newLevel);
}
/**
* @see org.apache.sling.launchpad.api.StartupHandler#waitWithStartup(boolean)
*/
@Override
public void waitWithStartup(final boolean flag) {
this.log(Logger.LOG_DEBUG, "Wait with startup " + flag);
if ( flag ) {
this.startupShouldWait.incrementAndGet();
} else {
this.startupShouldWait.decrementAndGet();
}
}
/**
* Sleep a little bit
* @param time Sleeping time
*/
private void sleep(final long time) {
try {
Thread.sleep(time);
} catch ( final InterruptedException e) {
// ignore
}
}
/**
* Put a task in the queue
* @param info Add new boolean information to queue
*/
private void enqueue(final boolean info) {
try {
this.queue.put(info);
} catch (final InterruptedException e) {
// ignore
}
}
/**
* @see org.osgi.framework.FrameworkListener#frameworkEvent(org.osgi.framework.FrameworkEvent)
*/
@Override
public void frameworkEvent(final FrameworkEvent event) {
if ( finished.get() ) {
return;
}
this.log(Logger.LOG_DEBUG, "Received framework event " + event);
if ( !this.useIncremental ) {
// restart
if ( event.getType() == FrameworkEvent.STARTED ) {
this.startupFinished();
}
} else {
// first startup or update
if ( event.getType() == FrameworkEvent.STARTED ) {
this.enqueue(true);
} else if ( event.getType() == FrameworkEvent.STARTLEVEL_CHANGED ) {
if ( this.startLevelService.getStartLevel() >= this.targetStartLevel ) {
this.startupFinished();
} else {
this.enqueue(true);
final int startLevel = this.startLevelService.getStartLevel();
this.log(Logger.LOG_DEBUG, "Startup progress " + String.valueOf(startLevel) + '/' + String.valueOf(targetStartLevel));
final float ratio = (float) startLevel / (float) targetStartLevel;
this.startupProgress(ratio);
}
}
}
}
private void log(final int level, final String msg) {
log(null, level, msg, null);
}
private void log(final int level, final String msg, final Throwable t) {
log(null, level, msg, t);
}
private void log(final ServiceReference<?> sRef, final int level, final String msg, final Throwable t) {
boolean loggedWithService = false;
if ( this.logService == null ) {
final ServiceReference<?> ref = this.bundleContext.getServiceReference("org.osgi.service.log.LogService");
if ( ref != null ) {
final Object ls = this.bundleContext.getService(ref);
if ( ls != null ) {
final Class<?>[] formalParams = {
ServiceReference.class,
Integer.TYPE,
String.class,
Throwable.class
};
try {
final Method logMethod = ls.getClass().getMethod("log", formalParams);
logMethod.setAccessible(true);
logService = new Object[] { ls, logMethod };
} catch (final NoSuchMethodException ex) {
// no need to log
}
}
}
}
if ( this.logService != null ) {
final Object[] params = {sRef, new Integer(level), msg, t};
try {
((Method) this.logService[1]).invoke(this.logService[0], params);
loggedWithService = true;
} catch (final InvocationTargetException ex) {
// no need to log
} catch (final IllegalAccessException ex) {
// no need to log
}
}
if ( !loggedWithService ) {
logger.log(level, msg);
}
}
/**
* Notify finished startup
*/
private void startupFinished() {
this.log(Logger.LOG_INFO, "Startup finished in " + String.valueOf(System.currentTimeMillis() - this.startedAt) + "ms");
this.finished.set(true);
for (final StartupListener listener : this.listenerTracker.getServices(new StartupListener[0])) {
try {
listener.startupFinished(this.startupMode);
} catch (Throwable t) {
this.log(Logger.LOG_ERROR, "Error calling StartupListener " + listener, t);
}
}
if ( this.mbeanStartupListener != null ) {
this.mbeanStartupListener.startupFinished(this.startupMode);
}
// stop the queue
this.enqueue(false);
// clear bundle set
this.activeBundles.clear();
// unregister listeners
if ( !this.useIncremental ) {
this.bundleContext.removeBundleListener(this);
}
this.bundleContext.removeFrameworkListener(this);
// register startup service
final Dictionary<String, Object> serviceProps = new Hashtable<String, Object>();
serviceProps.put(StartupMode.class.getName(), this.startupMode.name());
serviceProps.put(Constants.SERVICE_DESCRIPTION, "Apache Sling Startup Service");
serviceProps.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
this.bundleContext.registerService(StartupService.class, new StartupService() {
@Override
public StartupMode getStartupMode() {
return startupMode;
}
}, serviceProps);
}
/**
* Notify startup progress
* @param ratio ratio
*/
private void startupProgress(final float ratio) {
for (final StartupListener listener : this.listenerTracker.getServices(new StartupListener[0])) {
try {
listener.startupProgress(ratio);
} catch (final Throwable t) {
this.log(Logger.LOG_ERROR, "Error calling StartupListener " + listener, t);
}
}
if ( this.mbeanStartupListener != null ) {
this.mbeanStartupListener.startupProgress(ratio);
}
}
/**
* @see org.osgi.framework.BundleListener#bundleChanged(org.osgi.framework.BundleEvent)
*/
@Override
public void bundleChanged(final BundleEvent event) {
if (!finished.get()) {
this.log(Logger.LOG_DEBUG, "Received bundle event " + event);
if (event.getType() == BundleEvent.RESOLVED || event.getType() == BundleEvent.STARTED) {
// Add (if not existing) bundle to active bundles and refresh progress bar
activeBundles.add(event.getBundle().getSymbolicName());
this.log(Logger.LOG_DEBUG, "Startup progress " + String.valueOf(activeBundles.size()) + '/' + String.valueOf(expectedBundlesCount));
final float ratio = (float) activeBundles.size() / (float) expectedBundlesCount;
this.startupProgress(ratio);
} else if (event.getType() == BundleEvent.STOPPED) {
// Only remove bundle from active bundles,
// but do not refresh progress bar, to prevent progress bar from going back
activeBundles.remove(event.getBundle().getSymbolicName());
}
}
}
}