blob: 6f7db25707603ae1893b70456a6b7d93f68bdb35 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2003, 2016 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.osgi.internal.debug;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.eclipse.osgi.internal.framework.EquinoxConfiguration;
import org.eclipse.osgi.internal.location.LocationHelper;
import org.eclipse.osgi.service.debug.DebugOptions;
import org.eclipse.osgi.service.debug.DebugOptionsListener;
import org.eclipse.osgi.service.debug.DebugTrace;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
/**
* The DebugOptions implementation class that allows accessing the list of debug options specified
* for the application as well as creating {@link DebugTrace} objects for the purpose of having
* dynamic enablement of debug tracing.
*
* @since 3.1
*/
public class FrameworkDebugOptions implements DebugOptions, ServiceTrackerCustomizer<DebugOptionsListener, DebugOptionsListener> {
private static final String OSGI_DEBUG = "osgi.debug"; //$NON-NLS-1$
private static final String OSGI_DEBUG_VERBOSE = "osgi.debug.verbose"; //$NON-NLS-1$
public static final String PROP_TRACEFILE = "osgi.tracefile"; //$NON-NLS-1$
/** The default name of the .options file if loading when the -debug command-line argument is used */
private static final String OPTIONS = ".options"; //$NON-NLS-1$
/** A lock object used to synchronize access to the trace file */
private final static Object writeLock = new Object();
/** monitor used to lock the options maps */
private final Object lock = new Object();
/** A current map of all the options with values set */
private Properties options = null;
/** A map of all the disabled options with values set at the time debug was disabled */
private Properties disabledOptions = null;
/** A cache of all of the bundles <code>DebugTrace</code> in the format <key,value> --> <bundle name, DebugTrace> */
protected final Map<String, DebugTrace> debugTraceCache = new HashMap<>();
/** The File object to store messages. This value may be null. */
protected File outFile = null;
/** Is verbose debugging enabled? Changing this value causes a new tracing session to start. */
protected boolean verboseDebug = true;
/** A flag to determine if the message being written is done to a new file (i.e. should the header information be written) */
private boolean newSession = true;
private final EquinoxConfiguration environmentInfo;
private volatile BundleContext context;
private volatile ServiceTracker<DebugOptionsListener, DebugOptionsListener> listenerTracker;
public FrameworkDebugOptions(EquinoxConfiguration environmentInfo) {
this.environmentInfo = environmentInfo;
// check if verbose debugging was set during initialization. This needs to be set even if debugging is disabled
this.verboseDebug = Boolean.valueOf(environmentInfo.getConfiguration(OSGI_DEBUG_VERBOSE, Boolean.TRUE.toString())).booleanValue();
// if no debug option was specified, don't even bother to try.
// Must ensure that the options slot is null as this is the signal to the
// platform that debugging is not enabled.
String debugOptionsFilename = environmentInfo.getConfiguration(OSGI_DEBUG);
if (debugOptionsFilename == null)
return;
options = new Properties();
URL optionsFile;
if (debugOptionsFilename.length() == 0) {
// default options location is user.dir (install location may be r/o so
// is not a good candidate for a trace options that need to be updatable by
// by the user)
String userDir = System.getProperty("user.dir").replace(File.separatorChar, '/'); //$NON-NLS-1$
if (!userDir.endsWith("/")) //$NON-NLS-1$
userDir += "/"; //$NON-NLS-1$
debugOptionsFilename = new File(userDir, OPTIONS).toString();
}
optionsFile = LocationHelper.buildURL(debugOptionsFilename, false);
if (optionsFile == null) {
System.out.println("Unable to construct URL for options file: " + debugOptionsFilename); //$NON-NLS-1$
return;
}
System.out.print("Debug options:\n " + optionsFile.toExternalForm()); //$NON-NLS-1$
try {
InputStream input = LocationHelper.getStream(optionsFile);
try {
options.load(input);
System.out.println(" loaded"); //$NON-NLS-1$
} finally {
input.close();
}
} catch (FileNotFoundException e) {
System.out.println(" not found"); //$NON-NLS-1$
} catch (IOException e) {
System.out.println(" did not parse"); //$NON-NLS-1$
e.printStackTrace(System.out);
}
// trim off all the blanks since properties files don't do that.
for (Object key : options.keySet()) {
options.put(key, ((String) options.get(key)).trim());
}
}
public void start(BundleContext bc) {
this.context = bc;
listenerTracker = new ServiceTracker<>(bc, DebugOptionsListener.class.getName(), this);
listenerTracker.open();
}
public void stop(BundleContext bc) {
listenerTracker.close();
listenerTracker = null;
this.context = null;
}
/**
* @see DebugOptions#getBooleanOption(String, boolean)
*/
@Override
public boolean getBooleanOption(String option, boolean defaultValue) {
String optionValue = getOption(option);
return optionValue != null ? optionValue.equalsIgnoreCase("true") : defaultValue; //$NON-NLS-1$
}
/**
* @see DebugOptions#getOption(String)
*/
@Override
public String getOption(String option) {
return getOption(option, null);
}
/**
* @see DebugOptions#getOption(String, String)
*/
@Override
public String getOption(String option, String defaultValue) {
synchronized (lock) {
if (options != null) {
return options.getProperty(option, defaultValue);
}
}
return defaultValue;
}
/**
* @see DebugOptions#getIntegerOption(String, int)
*/
@Override
public int getIntegerOption(String option, int defaultValue) {
String value = getOption(option);
try {
return value == null ? defaultValue : Integer.parseInt(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public Map<String, String> getOptions() {
Map<String, String> snapShot = new HashMap<>();
synchronized (lock) {
if (options != null)
snapShot.putAll((Map) options);
else if (disabledOptions != null)
snapShot.putAll((Map) disabledOptions);
}
return snapShot;
}
/*
* (non-Javadoc)
* @see org.eclipse.osgi.service.debug.DebugOptions#getAllOptions()
*/
String[] getAllOptions() {
String[] optionsArray = null;
synchronized (lock) {
if (options != null) {
optionsArray = new String[options.size()];
final Iterator<Map.Entry<Object, Object>> entrySetIterator = options.entrySet().iterator();
int i = 0;
while (entrySetIterator.hasNext()) {
Map.Entry<Object, Object> entry = entrySetIterator.next();
optionsArray[i] = ((String) entry.getKey()) + "=" + ((String) entry.getValue()); //$NON-NLS-1$
i++;
}
}
}
if (optionsArray == null) {
optionsArray = new String[1]; // TODO this is strange; null is the only element so we can print null in writeSession
}
return optionsArray;
}
/*
* (non-Javadoc)
* @see org.eclipse.osgi.service.debug.DebugOptions#removeOption(java.lang.String)
*/
@Override
public void removeOption(String option) {
if (option == null)
return;
String fireChangedEvent = null;
synchronized (lock) {
if (options != null && options.remove(option) != null) {
fireChangedEvent = getSymbolicName(option);
}
}
// Send the options change event outside the sync block
if (fireChangedEvent != null) {
optionsChanged(fireChangedEvent);
}
}
/*
* (non-Javadoc)
* @see org.eclipse.osgi.service.debug.DebugOptions#setOption(java.lang.String, java.lang.String)
*/
@Override
public void setOption(String option, String value) {
if (option == null || value == null) {
throw new IllegalArgumentException("The option and value must not be null."); //$NON-NLS-1$
}
String fireChangedEvent = null;
value = value != null ? value.trim() : null;
synchronized (lock) {
if (options != null) {
// get the current value
String currentValue = options.getProperty(option);
if (currentValue != null) {
if (!currentValue.equals(value)) {
fireChangedEvent = getSymbolicName(option);
}
} else {
if (value != null) {
fireChangedEvent = getSymbolicName(option);
}
}
if (fireChangedEvent != null) {
options.put(option, value);
}
}
}
// Send the options change event outside the sync block
if (fireChangedEvent != null) {
optionsChanged(fireChangedEvent);
}
}
private String getSymbolicName(String option) {
int firstSlashIndex = option.indexOf('/');
if (firstSlashIndex > 0)
return option.substring(0, firstSlashIndex);
return null;
}
@SuppressWarnings("cast")
@Override
public void setOptions(Map<String, String> ops) {
if (ops == null)
throw new IllegalArgumentException("The options must not be null."); //$NON-NLS-1$
Properties newOptions = new Properties();
for (Iterator<Map.Entry<String, String>> entries = ops.entrySet().iterator(); entries.hasNext();) {
Map.Entry<String, String> entry = entries.next();
if (!(entry.getKey() instanceof String) || !(entry.getValue() instanceof String))
throw new IllegalArgumentException("Option keys and values must be of type String: " + entry.getKey() + "=" + entry.getValue()); //$NON-NLS-1$ //$NON-NLS-2$
newOptions.put(entry.getKey(), entry.getValue().trim());
}
Set<String> fireChangesTo = null;
synchronized (lock) {
if (options == null) {
disabledOptions = newOptions;
// no events to fire
return;
}
fireChangesTo = new HashSet<>();
// first check for removals
for (Iterator<Object> keys = options.keySet().iterator(); keys.hasNext();) {
String key = (String) keys.next();
if (!newOptions.containsKey(key)) {
String symbolicName = getSymbolicName(key);
if (symbolicName != null)
fireChangesTo.add(symbolicName);
}
}
// now check for changes to existing values
for (Iterator<Map.Entry<Object, Object>> newEntries = newOptions.entrySet().iterator(); newEntries.hasNext();) {
Map.Entry<Object, Object> entry = newEntries.next();
String existingValue = (String) options.get(entry.getKey());
if (!entry.getValue().equals(existingValue)) {
String symbolicName = getSymbolicName((String) entry.getKey());
if (symbolicName != null)
fireChangesTo.add(symbolicName);
}
}
// finally set the actual options
options = newOptions;
}
if (fireChangesTo != null)
for (Iterator<String> iChanges = fireChangesTo.iterator(); iChanges.hasNext();)
optionsChanged(iChanges.next());
}
/*
* (non-Javadoc)
* @see org.eclipse.osgi.service.debug.DebugOptions#isDebugEnabled()
*/
@Override
public boolean isDebugEnabled() {
synchronized (lock) {
return options != null;
}
}
/*
* (non-Javadoc)
* @see org.eclipse.osgi.service.debug.DebugOptions#setDebugEnabled()
*/
@Override
public void setDebugEnabled(boolean enabled) {
boolean fireChangedEvent = false;
synchronized (lock) {
if (enabled) {
if (options != null)
return;
// notify the trace that a new session is started
this.newSession = true;
// enable platform debugging - there is no .options file
environmentInfo.setConfiguration(OSGI_DEBUG, ""); //$NON-NLS-1$
if (disabledOptions != null) {
options = disabledOptions;
disabledOptions = null;
// fire changed event to indicate some options were re-enabled
fireChangedEvent = true;
} else {
options = new Properties();
}
} else {
if (options == null)
return;
// disable platform debugging.
environmentInfo.clearConfiguration(OSGI_DEBUG);
if (options.size() > 0) {
// Save the current options off in case debug is re-enabled
disabledOptions = options;
// fire changed event to indicate some options were disabled
fireChangedEvent = true;
}
options = null;
}
}
if (fireChangedEvent) {
// (Bug 300911) need to fire event to listeners that options have been disabled
optionsChanged("*"); //$NON-NLS-1$
}
}
/*
* (non-Javadoc)
* @see org.eclipse.osgi.service.debug.DebugOptions#createTrace(java.lang.String)
*/
@Override
public final DebugTrace newDebugTrace(String bundleSymbolicName) {
return this.newDebugTrace(bundleSymbolicName, null);
}
/*
* (non-Javadoc)
* @see org.eclipse.osgi.service.debug.DebugOptions#createTrace(java.lang.String, java.lang.Class)
*/
@Override
public final DebugTrace newDebugTrace(String bundleSymbolicName, Class<?> traceEntryClass) {
DebugTrace debugTrace = null;
synchronized (debugTraceCache) {
debugTrace = debugTraceCache.get(bundleSymbolicName);
if (debugTrace == null) {
debugTrace = new EclipseDebugTrace(bundleSymbolicName, this, traceEntryClass);
debugTraceCache.put(bundleSymbolicName, debugTrace);
}
}
return debugTrace;
}
/*
* (non-Javadoc)
* @see org.eclipse.osgi.service.debug.DebugOptions#getFile()
*/
@Override
public final File getFile() {
return this.outFile;
}
/*
* (non-Javadoc)
* @see org.eclipse.osgi.service.debug.DebugOptions#setFile(java.io.File)
*/
@Override
public void setFile(final File traceFile) {
synchronized (lock) {
this.outFile = traceFile;
if (this.outFile != null)
environmentInfo.setConfiguration(PROP_TRACEFILE, this.outFile.getAbsolutePath());
else
environmentInfo.clearConfiguration(PROP_TRACEFILE);
// the file changed so start a new session
this.newSession = true;
}
}
boolean newSession() {
synchronized (lock) {
if (newSession) {
this.newSession = false;
return true;
}
return false;
}
}
Object getWriteLock() {
return writeLock;
}
/*
* (non-Javadoc)
* @see org.eclipse.osgi.service.debug.DebugOptions#getVerbose()
*/
boolean isVerbose() {
return this.verboseDebug;
}
EquinoxConfiguration getConfiguration() {
return this.environmentInfo;
}
/*
* (non-Javadoc)
* @see org.eclipse.osgi.service.debug.DebugOptions#setVerbose(boolean)
*/
public void setVerbose(final boolean verbose) {
synchronized (lock) {
this.verboseDebug = verbose;
// the verbose flag changed so start a new session
this.newSession = true;
}
}
/**
* Notifies the trace listener for the specified bundle that its option-path has changed.
* @param bundleSymbolicName The bundle of the owning trace listener to notify.
*/
private void optionsChanged(String bundleSymbolicName) {
// use osgi services to get the listeners
BundleContext bc = context;
if (bc == null)
return;
// do not use the service tracker because that is only used to call all listeners initially when they are registered
// here we only want the services with the specified name.
ServiceReference<?>[] listenerRefs = null;
try {
listenerRefs = bc.getServiceReferences(DebugOptionsListener.class.getName(), "(" + DebugOptions.LISTENER_SYMBOLICNAME + "=" + bundleSymbolicName + ")"); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
} catch (InvalidSyntaxException e) {
// consider logging; should not happen
}
if (listenerRefs == null)
return;
for (int i = 0; i < listenerRefs.length; i++) {
DebugOptionsListener service = (DebugOptionsListener) bc.getService(listenerRefs[i]);
if (service == null)
continue;
try {
service.optionsChanged(this);
} catch (Throwable t) {
// TODO consider logging
} finally {
bc.ungetService(listenerRefs[i]);
}
}
}
@Override
public DebugOptionsListener addingService(ServiceReference<DebugOptionsListener> reference) {
DebugOptionsListener listener = context.getService(reference);
listener.optionsChanged(this);
return listener;
}
@Override
public void modifiedService(ServiceReference<DebugOptionsListener> reference, DebugOptionsListener service) {
// nothing
}
@Override
public void removedService(ServiceReference<DebugOptionsListener> reference, DebugOptionsListener service) {
context.ungetService(reference);
}
}