blob: 7e2cc2dba22d0fa717e68a23f7c5d0c025a46495 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007 The Eclipse Foundation.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* The Eclipse Foundation - initial API and implementation
*******************************************************************************/
package org.eclipse.epp.usagedata.internal.gathering.services;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.adaptor.EclipseStarter;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.epp.usagedata.internal.gathering.UsageDataCaptureActivator;
import org.eclipse.epp.usagedata.internal.gathering.events.UsageDataEvent;
import org.eclipse.epp.usagedata.internal.gathering.events.UsageDataEventListener;
import org.eclipse.epp.usagedata.internal.gathering.monitors.UsageMonitor;
/**
* The {@link UsageDataService} class is registered as an OSGi service by the
* bundle activator on startup. It is responsible for installing monitors
* registered via the {@value #MONITORS_EXTENSION_POINT} extension point. These
* monitors all feed information back into the instance which is responsible for
* dispatching those events to event listeners registered via the
* {@link #addUsageDataEventListener(UsageDataEventListener)} method.
* <p>
* The instance starts monitoring activities immediately after it is started,
* but does not dispatch the resulting events until after the workbench is
* running (as reported by the {@link EclipseStarter#isRunning()} method.
* </p>
* <p>
* Efforts have been taken to try and keep the impact on the user
* experience&mdash;performance in particular&mdash;to a minimum. Any decision
* balancing absolute correctness of data capture and user experience is made in
* favour of preserving positive user experience and reducing any negative
* impact on performance. In that regard, for example, cancel really means
* cancel to the {@link #eventConsumerJob} and may leave some events
* undispatched to the listeners.
*
* @author Wayne Beaton
*/
@SuppressWarnings("restriction")
public class UsageDataService {
private static final String MONITORS_EXTENSION_POINT = UsageDataCaptureActivator.PLUGIN_ID + ".monitors"; //$NON-NLS-1$
private boolean monitoring = false;
/**
* The list of monitors hooked into various parts of the system listening to
* what the user is up to. The objects in this list are of type
* {@link UsageMonitor}. Strictly speaking this is not a list of
* "listeners", but {@link ListenerList} provides some convenient management
* functionality.
*/
private ListenerList monitors = new ListenerList();
/**
* The list of objects of type {@link UsageDataEventListener} listening to events
* generated by this service.
*/
private ListenerList eventListeners = new ListenerList();
/**
* The thread that figures out what to do with events provided by the
* various monitors. This functionality is separated into a separate thread
* in anticipation of performance issues (see {@link #startEventConsumerJob()}
* for discussion.
*/
Job eventConsumerJob;
/**
* A temporary home for events as they are generated. As they are created,
* events are dropped into the queue by the source thread. Events are consumed
* from the queue by the {@link #eventConsumerJob}.
* @see #startEventConsumerJob()
*/
protected LinkedBlockingQueue<UsageDataEvent> events = new LinkedBlockingQueue<UsageDataEvent>();
/**
* This field maps the symbolic name of bundles to the last loaded version.
* This information is handy for filling in missing bundle version information
* for singleton bundles.
* @see #registerBundleVersion(UsageDataEvent)
*/
private Map<String, String> bundleVersionMap = new HashMap<String, String>();
/**
* This method starts the monitoring process. If the service has already been
* "started" when this method is called, nothing happens (i.e. multiple calls
* to this method are tolerated).
*/
public void startMonitoring() {
if (isMonitoring())
return;
startMonitors();
startEventConsumerJob();
monitoring = true;
}
/**
* This method stops the monitoring process. If the service is already stopped
* when this method is called, nothing happens (i.e. multiple calls
* to this method are tolerated).
*/
public synchronized void stopMonitoring() {
if (!isMonitoring())
return;
stopMonitors();
stopEventConsumerJob();
monitoring = false;
}
public boolean isMonitoring() {
return monitoring;
}
/**
* Start the {@link #eventConsumerJob}. Various monitors add events to the
* {@link #events} queue. In order to avoid degrading system performance any
* more than necessary, events are added to the queue by the monitors. The
* {@link #eventConsumerJob} then consumes the events from the queue and
* dispatches them to the various {@link UsageDataEventListener}s. Since
* the event listeners will do expensive things like open and write to
* files, it is anticipated that this architecture will allow the necessary
* activities to happen without significantly impacting the user's
* experience.
*/
protected void startEventConsumerJob() {
// TODO Decide if the job is more trouble than it's worth.
if (eventConsumerJob != null) return;
eventConsumerJob = new Job("Usage Data Event consumer") { //$NON-NLS-1$
public IStatus run(IProgressMonitor monitor) {
waitForWorkbenchToFinishStarting();
while (!monitor.isCanceled()) {
UsageDataEvent event = getQueuedEvent();
dispatchEvent(event);
}
return Status.OK_STATUS;
}
};
eventConsumerJob.setSystem(true);
eventConsumerJob.setPriority(Job.LONG);
eventConsumerJob.schedule(1000); // Wait a few minutes before scheduling the job.
}
/**
* This method pauses the current thread until the workbench has
* finished starting. This should provide enough time for bundles
* that are installing usage data event listeners to complete before
* events are dispatched.
*/
protected void waitForWorkbenchToFinishStarting() {
/*
* We want the job to pause until after all the bundles that are
* loaded at startup have finished loading. This will give
* bundles that listen to usage data events time to load and
* install listeners before events are fired off (which should
* mean that events won't get lost).
*
* I had originally tried using Display.syncExec(Runnable) (with
* an "do nothing" Runnable, but this caused some weird classloading
* issues similar to those referenced in Bug 88109.
*/
while (!EclipseStarter.isRunning()) {
try {
// It probably doesn't matter too much if we wait too long here.
// TODO Is 1 second too long?
Thread.sleep(1000);
} catch (InterruptedException e) {
// Ignore and loop again!
}
}
}
protected void stopEventConsumerJob() {
eventConsumerJob.cancel();
// Interrupt the thread to make sure that the
// job gets the chance to terminate gracefully. Then join
// the thread to make sure that it gets enough time to
// properly shutdown. See Bug 306449.
Thread thread = eventConsumerJob.getThread();
if (thread != null)
thread.interrupt();
try {
eventConsumerJob.join();
} catch (InterruptedException e) {
// Oh well, we tried...
}
eventConsumerJob = null;
}
/**
* This method returns the next available event. If no event is available,
* the current thread is suspended until an event is added. This method will
* return <code>null</null> if it is called with an empty event queue after
* monitoring is turned off or if the thread is interrupted.
*
* @return an instance of {@link UsageDataEvent} or <code>null</code>.
*/
private UsageDataEvent getQueuedEvent() {
try {
return events.take();
} catch (InterruptedException e) {
return null;
}
}
/**
* This method queues an event containing the given information for
* processing.
*
* @param what
* what happened? was it an activation, started, clicked, ... ?
* @param kind
* what kind of thing caused it? view, editor, bundle, ... ?
* @param description
* information about the event. e.g. name of the command, view,
* editor, ...
* @param bundleId
* symbolic name of the bundle that owns the thing that caused
* the event.
*/
public void recordEvent(String what, String kind, String description,
String bundleId) {
recordEvent(what, kind, description, bundleId, null);
}
/**
* <p>
* This method queues an event containing the given information for
* processing.
* </p>
*
* @param what
* what happened? was it an activation, started, clicked, ... ?
* @param kind
* what kind of thing caused it? view, editor, bundle, ... ?
* @param description
* information about the event. e.g. name of the command, view,
* editor, ...
* @param bundleId
* symbolic name of the bundle that owns the thing that caused
* the event.
* @param bundleVersion
* the version of the bundle that owns the thing that caused the
* event.
*/
public void recordEvent(String what, String kind, String description,
String bundleId, String bundleVersion) {
UsageDataEvent event = new UsageDataEvent(what, kind, description, bundleId,
bundleVersion, System.currentTimeMillis());
recordEvent(event);
}
private void recordEvent(UsageDataEvent event) {
/*
* Multiple thread access to #events is managed the LinkedBlockingQueue
* implementation.
*/
events.add(event);
}
/**
* This method dispatches <code>event</code> to the registered event
* listeners.
*
* @param event
* the {@link UsageDataEvent} to dispatch.
*/
private void dispatchEvent(UsageDataEvent event) {
if (event == null) return;
registerBundleVersion(event);
if (event.bundleVersion == null) event.bundleVersion = getBundleVersion(event.bundleId);
Object[] listeners = eventListeners.getListeners();
for (int index = 0; index < listeners.length; index++) {
UsageDataEventListener listener = (UsageDataEventListener) listeners[index];
dispatchEvent(event, listener);
}
}
/**
* This method does the actual dispatching of the event to a single listener. If
* an exception occurs in the execution of the listener, an exception is logged.
*
* @param event
* @param listener
*/
private void dispatchEvent(UsageDataEvent event, UsageDataEventListener listener) {
try {
listener.accept(event);
} catch (Throwable e) {
// TODO Add some logic to remove repeat offenders.
UsageDataCaptureActivator.getDefault().logException("The listener (" + listener.getClass() + ") threw an exception", e); //$NON-NLS-1$ //$NON-NLS-2$
}
}
/**
* If the event represents a bundle activation, record a mapping between the
* bundleId and bundleVersion. This information is used to fill in missing
* information when an event comes in with just a bundleId and no version
* information. This assumes that the bundle is a singleton. That is, there
* is no provision here for dealing with multiple versions of bundles. If
* the event is a result of something that may come from a non-singleton
* bundle, then it is the responsibility of the event source to determine
* the appropriate version.
*
* @param event
* instance of {@link UsageDataEvent}.
*/
private void registerBundleVersion(UsageDataEvent event) {
/*
* This is a bit of a hack since we're using inside knowledge about a
* particular type of event (that we're pretty well decoupled
* from--though this knowledge does constitute a relatively tight
* form of coupling). If the event tells us that a bundle has been
* started, we'll move on; otherwise, we bail out. We're not interested
* in other bundle events (like stops, etc.), since these
* events will be relatively rare for the kinds of bundles we actually
* care about.
*/
if (!("bundle".equals(event.kind))) return; //$NON-NLS-1$
if (!("started".equals(event.what))) return; //$NON-NLS-1$
synchronized (bundleVersionMap) {
bundleVersionMap.put(event.bundleId, event.bundleVersion);
}
}
/**
* This method returns the version of the bundle with id bundleId. This
* assumes that the bundle is a singleton. That is, there is no provision
* here for dealing with multiple versions of bundles. If the event is a
* result of something that may come from a non-singleton bundle, then it is
* the responsibility of the event source to determine the appropriate
* version.
*
* @param bundleId
* the symbolic name of a bundle.
*
* @return the id of the last bundle started with the given id.
*/
private String getBundleVersion(String bundleId) {
if (bundleId == null) return null;
synchronized (bundleVersionMap) {
return bundleVersionMap.get(bundleId);
}
}
protected void startMonitors() {
IConfigurationElement[] elements = Platform.getExtensionRegistry()
.getConfigurationElementsFor(
MONITORS_EXTENSION_POINT);
for (IConfigurationElement element : elements) {
if ("monitor".equals(element.getName())) { //$NON-NLS-1$
try {
Object monitor = element.createExecutableExtension("class"); //$NON-NLS-1$
if (monitor instanceof UsageMonitor) {
startMonitor((UsageMonitor) monitor);
}
} catch (CoreException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
private void startMonitor(UsageMonitor monitor) {
monitor.startMonitoring(this);
monitors.add(monitor);
}
protected void stopMonitors() {
for (Object monitor : monitors.getListeners()) {
stopMonitor((UsageMonitor) monitor);
}
}
private void stopMonitor(UsageMonitor monitor) {
monitor.stopMonitoring();
monitors.remove(monitor);
}
public void addUsageDataEventListener(UsageDataEventListener listener) {
eventListeners.add(listener);
}
public void removeUsageDataEventListener(UsageDataEventListener listener) {
eventListeners.remove(listener);
}
}