From: Mathieu Baudier Date: Fri, 31 Dec 2021 10:25:40 +0000 (+0100) Subject: Make logging configurable X-Git-Tag: argeo-commons-2.3.5~113 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=1010bff4f1ffadbf3f7988fbd3eba70f5d672c88;p=lgpl%2Fargeo-commons.git Make logging configurable --- diff --git a/org.argeo.init/src/org/argeo/init/logging/ThinLogEntry.java b/org.argeo.init/src/org/argeo/init/logging/ThinLogEntry.java index c71fb6d27..51aadec32 100644 --- a/org.argeo.init/src/org/argeo/init/logging/ThinLogEntry.java +++ b/org.argeo.init/src/org/argeo/init/logging/ThinLogEntry.java @@ -9,6 +9,7 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; /** A log entry with equals semantics based on an incremental long sequence. */ +@Deprecated class ThinLogEntry implements Serializable { private static final long serialVersionUID = 5915553445193937270L; diff --git a/org.argeo.init/src/org/argeo/init/logging/ThinLoggerFinder.java b/org.argeo.init/src/org/argeo/init/logging/ThinLoggerFinder.java index 4147534dd..f443b2e66 100644 --- a/org.argeo.init/src/org/argeo/init/logging/ThinLoggerFinder.java +++ b/org.argeo.init/src/org/argeo/init/logging/ThinLoggerFinder.java @@ -1,10 +1,13 @@ package org.argeo.init.logging; +import java.io.Serializable; import java.lang.System.Logger; import java.lang.System.LoggerFinder; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.concurrent.Flow; +import java.util.function.Consumer; /** * Factory for Java system logging. As it has to be a public class in order to @@ -29,7 +32,7 @@ public class ThinLoggerFinder extends LoggerFinder { private static void init() { logging = new ThinLogging(); - Map configuration = new HashMap<>(); + Map configuration = new HashMap<>(); for (Object key : System.getProperties().keySet()) { Objects.requireNonNull(key); String property = key.toString(); @@ -37,11 +40,12 @@ public class ThinLoggerFinder extends LoggerFinder { || property.equals(ThinLogging.DEFAULT_LEVEL_PROPERTY)) configuration.put(property, System.getProperty(property)); } - logging.update(configuration); + logging.accept(configuration); } /** - * Falls back to java.util.logging if thin logging was not already initialised. + * Falls back to java.util.logging if thin logging was not already initialised + * by the {@link LoggerFinder} mechanism. */ public static void lazyInit() { if (logging != null) @@ -53,10 +57,20 @@ public class ThinLoggerFinder extends LoggerFinder { javaUtilLogging.readConfiguration(logging.getLevels()); } - public static void update(Map configuration) { + public static Consumer> getConfigurationConsumer() { + Objects.requireNonNull(logging); + return logging; + } + + public static Flow.Publisher> getLogEntryPublisher() { + Objects.requireNonNull(logging); + return logging.getLogEntryPublisher(); + } + + static void update(Map configuration) { if (logging == null) throw new IllegalStateException("Thin logging must be initialized first"); - logging.update(configuration); + logging.accept(configuration); if (javaUtilLogging != null) javaUtilLogging.readConfiguration(logging.getLevels()); } diff --git a/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java b/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java index 22c1c779b..9866a1f23 100644 --- a/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java +++ b/org.argeo.init/src/org/argeo/init/logging/ThinLogging.java @@ -1,11 +1,13 @@ package org.argeo.init.logging; import java.io.PrintStream; +import java.io.Serializable; import java.lang.System.Logger; import java.lang.System.Logger.Level; import java.text.MessageFormat; import java.time.Instant; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.NavigableMap; @@ -21,14 +23,24 @@ import java.util.concurrent.Flow; import java.util.concurrent.Flow.Subscription; import java.util.concurrent.SubmissionPublisher; import java.util.concurrent.TimeUnit; - -/** A thin logging system based on the {@link Logger} framework. */ -class ThinLogging { +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +/** + * A thin logging system based on the {@link Logger} framework. It is a + * {@link Consumer} of configuration, and can be registered as such. + */ +class ThinLogging implements Consumer> { final static String DEFAULT_LEVEL_NAME = ""; final static String DEFAULT_LEVEL_PROPERTY = "log"; final static String LEVEL_PROPERTY_PREFIX = DEFAULT_LEVEL_PROPERTY + "."; + final static String JOURNALD_PROPERTY = "argeo.logging.journald"; + final static String CALL_LOCATION_PROPERTY = "argeo.logging.callLocation"; + + private final static AtomicLong nextEntry = new AtomicLong(0l); + // we don't synchronize maps on purpose as it would be // too expensive during normal operation // updates to the config may be shortly inconsistent @@ -39,6 +51,9 @@ class ThinLogging { private final ExecutorService executor; private final LogEntryPublisher publisher; + private final boolean journald; + private final Level callLocationLevel; + ThinLogging() { executor = Executors.newCachedThreadPool((r) -> { Thread t = new Thread(r); @@ -54,9 +69,52 @@ class ThinLogging { // initial default level levels.put("", Level.WARNING); + + // Logging system config + // journald + +// Map env = new TreeMap<>(System.getenv()); +// for (String key : env.keySet()) { +// System.out.println(key + "=" + env.get(key)); +// } + + String journaldStr = System.getProperty(JOURNALD_PROPERTY, "auto"); + switch (journaldStr) { + case "auto": + String systemdInvocationId = System.getenv("INVOCATION_ID"); + if (systemdInvocationId != null) {// in systemd + // check whether we are indirectly in a desktop app (e.g. eclipse) + String desktopFilePid = System.getenv("GIO_LAUNCHED_DESKTOP_FILE_PID"); + if (desktopFilePid != null) { + Long javaPid = ProcessHandle.current().pid(); + if (!javaPid.toString().equals(desktopFilePid)) { + journald = false; + break; + } + } + journald = true; + break; + } + journald = false; + break; + case "true": + case "on": + journald = true; + break; + case "false": + case "off": + journald = false; + break; + default: + throw new IllegalArgumentException( + "Unsupported value '" + journaldStr + "' for property " + JOURNALD_PROPERTY); + } + + String callLocationStr = System.getProperty(CALL_LOCATION_PROPERTY, Level.WARNING.getName()); + callLocationLevel = Level.valueOf(callLocationStr); } - protected void close() { + private void close() { publisher.close(); try { // we ait a bit in order to make sure all messages are flushed @@ -67,35 +125,40 @@ class ThinLogging { } } - public boolean isLoggable(String name, Level level) { - Objects.requireNonNull(name); - Objects.requireNonNull(level); - - if (updatingConfiguration) { - synchronized (levels) { - try { - levels.wait(); - // TODO make exit more robust - } catch (InterruptedException e) { - throw new IllegalStateException(e); - } - } - } - - Map.Entry entry = levels.ceilingEntry(name); + private Level computeApplicableLevel(String name) { + Map.Entry entry = levels.floorEntry(name); assert entry != null; - return level.getSeverity() >= entry.getValue().getSeverity(); + return entry.getValue(); + } +// private boolean isLoggable(String name, Level level) { +// Objects.requireNonNull(name); +// Objects.requireNonNull(level); +// +// if (updatingConfiguration) { +// synchronized (levels) { +// try { +// levels.wait(); +// // TODO make exit more robust +// } catch (InterruptedException e) { +// throw new IllegalStateException(e); +// } +// } +// } +// +// return level.getSeverity() >= computeApplicableLevel(name).getSeverity(); +// } + public Logger getLogger(String name, Module module) { if (!loggers.containsKey(name)) { - ThinLogger logger = new ThinLogger(name); + ThinLogger logger = new ThinLogger(name, computeApplicableLevel(name)); loggers.put(name, logger); } return loggers.get(name); } - void update(Map configuration) { + public void accept(Map configuration) { synchronized (levels) { updatingConfiguration = true; @@ -106,7 +169,7 @@ class ThinLogging { properties: for (String property : configuration.keySet()) { if (!property.startsWith(LEVEL_PROPERTY_PREFIX)) continue properties; - String levelStr = configuration.get(property); + String levelStr = configuration.get(property).toString(); Level level = Level.valueOf(levelStr); levels.put(property.substring(LEVEL_PROPERTY_PREFIX.length()), level); } @@ -120,16 +183,17 @@ class ThinLogging { it.remove(); } } -// for (String name : levels.keySet()) { -// if (!configuration.containsKey(LEVEL_PROPERTY_PREFIX + name)) { -// levels.remove(name); -// } -// } - Level newDefaultLevel = Level.valueOf(configuration.get(DEFAULT_LEVEL_PROPERTY)); + Level newDefaultLevel = Level.valueOf(configuration.get(DEFAULT_LEVEL_PROPERTY).toString()); levels.put(DEFAULT_LEVEL_NAME, newDefaultLevel); // TODO notify everyone? } assert levels.containsKey(DEFAULT_LEVEL_NAME); + + // recompute all levels + for (String name : loggers.keySet()) { + ThinLogger logger = loggers.get(name); + logger.setLevel(computeApplicableLevel(name)); + } } catch (IllegalArgumentException e) { e.printStackTrace(); levels.clear(); @@ -141,17 +205,27 @@ class ThinLogging { } + Flow.Publisher> getLogEntryPublisher() { + return publisher; + } + Map getLevels() { return Collections.unmodifiableNavigableMap(levels); } - class ThinLogger implements System.Logger { + /* + * INTERNAL CLASSES + */ + + private class ThinLogger implements System.Logger { private final String name; - private boolean callLocationEnabled = true; - protected ThinLogger(String name) { + private Level level; + + protected ThinLogger(String name, Level level) { assert Objects.nonNull(name); this.name = name; + this.level = level; } @Override @@ -161,32 +235,59 @@ class ThinLogging { @Override public boolean isLoggable(Level level) { + return level.getSeverity() >= getLevel().getSeverity(); // TODO optimise by referencing the applicable level in this class? - return ThinLogging.this.isLoggable(name, level); +// return ThinLogging.this.isLoggable(name, level); + } + + private Level getLevel() { + if (updatingConfiguration) { + synchronized (levels) { + try { + levels.wait(); + // TODO make exit more robust + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + } + return level; } @Override public void log(Level level, ResourceBundle bundle, String msg, Throwable thrown) { + if (!isLoggable(level)) + return; // measure timestamp first Instant now = Instant.now(); - publisher.log(this, level, bundle, msg, thrown, now, findCallLocation()); + Thread thread = Thread.currentThread(); + publisher.log(this, level, bundle, msg, now, thread, thrown, findCallLocation(level, thread)); } @Override public void log(Level level, ResourceBundle bundle, String format, Object... params) { + if (!isLoggable(level)) + return; // measure timestamp first Instant now = Instant.now(); + Thread thread = Thread.currentThread(); // NOTE: this is the method called when logging a plain message without // exception, so it should be considered as a format only when args are not null String msg = params == null ? format : MessageFormat.format(format, params); - publisher.log(this, level, bundle, msg, null, now, findCallLocation()); + publisher.log(this, level, bundle, msg, now, thread, (Throwable) null, findCallLocation(level, thread)); + } + + private void setLevel(Level level) { + this.level = level; } - protected StackTraceElement findCallLocation() { + private StackTraceElement findCallLocation(Level level, Thread thread) { + assert level != null; + assert thread != null; StackTraceElement callLocation = null; - if (callLocationEnabled) { - StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + if (level.getSeverity() >= callLocationLevel.getSeverity()) { + StackTraceElement[] stack = thread.getStackTrace(); int lowestLoggerInterface = 0; stack: for (int i = 2; i < stack.length; i++) { String className = stack[i].getClassName(); @@ -212,27 +313,93 @@ class ThinLogging { } - class LogEntryPublisher extends SubmissionPublisher { + private final static String KEY_LOGGER = Logger.class.getName(); + private final static String KEY_LEVEL = Level.class.getName(); + private final static String KEY_MSG = String.class.getName(); + private final static String KEY_THROWABLE = Throwable.class.getName(); + private final static String KEY_INSTANT = Instant.class.getName(); + private final static String KEY_CALL_LOCATION = StackTraceElement.class.getName(); + private final static String KEY_THREAD = Thread.class.getName(); - protected LogEntryPublisher(Executor executor, int maxBufferCapacity) { + private class LogEntryPublisher extends SubmissionPublisher> { + + private LogEntryPublisher(Executor executor, int maxBufferCapacity) { super(executor, maxBufferCapacity); } - void log(ThinLogger logger, Level level, ResourceBundle bundle, String msg, Throwable thrown, Instant instant, - StackTraceElement callLocation) { - ThinLogEntry logEntry = new ThinLogEntry(logger, level, msg, instant, thrown, callLocation); - submit(logEntry); + private void log(ThinLogger logger, Level level, ResourceBundle bundle, String msg, Instant instant, + Thread thread, Throwable thrown, StackTraceElement callLocation) { + assert level != null; + assert logger != null; + assert msg != null; + assert instant != null; + assert thread != null; + + final long sequence = nextEntry.incrementAndGet(); + + Map logEntry = new LogEntryMap(sequence); + + // same object as key class name + logEntry.put(KEY_LEVEL, level); + logEntry.put(KEY_MSG, msg); + logEntry.put(KEY_INSTANT, instant); + if (thrown != null) + logEntry.put(KEY_THROWABLE, thrown); + if (callLocation != null) + logEntry.put(KEY_CALL_LOCATION, callLocation); + + // object is a string + logEntry.put(KEY_LOGGER, logger.getName()); + logEntry.put(KEY_THREAD, thread.getName()); + + // should be unmodifiable for security reasons + submit(Collections.unmodifiableMap(logEntry)); } } - class PrintStreamSubscriber implements Flow.Subscriber { + /** + * An internal optimisation for collections. It should not be referred to + * directly as a type. + */ + // TODO optimise memory with a custom map implementation? + // but access may be slower + private static class LogEntryMap extends HashMap { + private static final long serialVersionUID = 7361434381922521356L; + + private final long sequence; + + private LogEntryMap(long sequence) { + // maximum 7 fields, so using default load factor 0.75 + // an initial size of 10 should prevent rehashing (7 / 0.75 ~ 9.333) + // see HashMap class description for more details + super(10, 0.75f); + this.sequence = sequence; + } + + @Override + public boolean equals(Object o) { + if (o instanceof LogEntryMap) + return sequence == ((LogEntryMap) o).sequence; + else if (o instanceof Map) { + Map map = (Map) o; + return get(KEY_INSTANT).equals(map.get(KEY_INSTANT)) && get(KEY_THREAD).equals(map.get(KEY_THREAD)); + } else + return false; + } + + @Override + public int hashCode() { + return (int) sequence; + } + + } + + private class PrintStreamSubscriber implements Flow.Subscriber> { private PrintStream out; private PrintStream err; private int writeToErrLevel = Level.WARNING.getSeverity(); - private boolean journald = false; - protected PrintStreamSubscriber() { this(System.out, System.err); } @@ -242,18 +409,23 @@ class ThinLogging { this.err = err; } + private Level getLevel(Map logEntry) { + return (Level) logEntry.get(KEY_LEVEL); + } + @Override public void onSubscribe(Subscription subscription) { subscription.request(Long.MAX_VALUE); } @Override - public void onNext(ThinLogEntry item) { - if (item.getLevel().getSeverity() >= writeToErrLevel) { + public void onNext(Map item) { + if (getLevel(item).getSeverity() >= writeToErrLevel) { err.print(toPrint(item)); } else { out.print(toPrint(item)); } + // TODO flush for journald? } @Override @@ -267,22 +439,37 @@ class ThinLogging { err.flush(); } - protected boolean prefixOnEachLine() { - return journald; - } - - protected String firstLinePrefix(ThinLogEntry logEntry) { - return journald ? linePrefix(logEntry) - : logEntry.getLevel().toString() + "\t" + logEntry.getInstant() + " "; + protected String firstLinePrefix(Map logEntry) { + Level level = getLevel(logEntry); + String spaces; + switch (level) { + case ERROR: + case DEBUG: + case TRACE: + spaces = " "; + break; + case INFO: + spaces = " "; + break; + case WARNING: + spaces = " "; + break; + case ALL: + spaces = " "; + break; + default: + throw new IllegalArgumentException("Unsupported level " + level); + } + return journald ? linePrefix(logEntry) : logEntry.get(KEY_INSTANT) + " " + level + spaces; } - protected String firstLineSuffix(ThinLogEntry logEntry) { - return " - " + (logEntry.getCallLocation().isEmpty() ? logEntry.getLoggerName() - : logEntry.getCallLocation().get()); + protected String firstLineSuffix(Map logEntry) { + return " - " + (logEntry.containsKey(KEY_CALL_LOCATION) ? logEntry.get(KEY_CALL_LOCATION) + : logEntry.get(KEY_LOGGER)) + " [" + logEntry.get(KEY_THREAD) + "]"; } - protected String linePrefix(ThinLogEntry logEntry) { - return journald ? "<" + levelToJournald(logEntry.getLevel()) + ">" : ""; + protected String linePrefix(Map logEntry) { + return journald ? "<" + levelToJournald(getLevel(logEntry)) + ">" : ""; } protected int levelToJournald(Level level) { @@ -297,9 +484,9 @@ class ThinLogging { return 7; } - protected String toPrint(ThinLogEntry logEntry) { + protected String toPrint(Map logEntry) { StringBuilder sb = new StringBuilder(); - StringTokenizer st = new StringTokenizer(logEntry.getMessage(), "\r\n"); + StringTokenizer st = new StringTokenizer((String) logEntry.get(KEY_MSG), "\r\n"); assert st.hasMoreTokens(); // first line @@ -317,8 +504,8 @@ class ThinLogging { sb.append('\n'); } - if (!logEntry.getThrowable().isEmpty()) { - Throwable throwable = logEntry.getThrowable().get(); + if (logEntry.containsKey(KEY_THROWABLE)) { + Throwable throwable = (Throwable) logEntry.get(KEY_THROWABLE); sb.append(prefix); addThrowable(sb, prefix, throwable); } @@ -345,10 +532,12 @@ class ThinLogging { public static void main(String args[]) { Logger logger = System.getLogger(ThinLogging.class.getName()); - logger.log(Logger.Level.INFO, "Hello log!"); - logger.log(Logger.Level.ERROR, "Hello error!"); - logger.log(Logger.Level.DEBUG, "Hello multi\nline\ndebug!"); - logger.log(Logger.Level.WARNING, "Hello exception!", new Throwable()); + logger.log(Logger.Level.ALL, "Log all"); + logger.log(Logger.Level.TRACE, "Multi\nline\ntrace"); + logger.log(Logger.Level.DEBUG, "Log debug"); + logger.log(Logger.Level.INFO, "Log info"); + logger.log(Logger.Level.WARNING, "Log warning"); + logger.log(Logger.Level.ERROR, "Log exception", new Throwable()); } } diff --git a/org.argeo.init/src/org/argeo/init/osgi/Activator.java b/org.argeo.init/src/org/argeo/init/osgi/Activator.java index 3e0d8e117..a4f5a371d 100644 --- a/org.argeo.init/src/org/argeo/init/osgi/Activator.java +++ b/org.argeo.init/src/org/argeo/init/osgi/Activator.java @@ -2,6 +2,7 @@ package org.argeo.init.osgi; import java.lang.System.Logger; import java.lang.System.Logger.Level; +import java.util.Objects; import org.argeo.init.logging.ThinLoggerFinder; import org.osgi.framework.BundleActivator; @@ -17,28 +18,34 @@ public class Activator implements BundleActivator { // must be called first ThinLoggerFinder.lazyInit(); } - Logger logger = System.getLogger(Activator.class.getName()); + private Logger logger = System.getLogger(Activator.class.getName()); private Long checkpoint = null; + private OsgiRuntimeContext runtimeContext; public void start(final BundleContext bundleContext) throws Exception { + if (runtimeContext == null) { + runtimeContext = new OsgiRuntimeContext(bundleContext); + } logger.log(Level.DEBUG, () -> "Argeo init via OSGi activator"); // admin thread - Thread adminThread = new AdminThread(bundleContext); - adminThread.start(); +// Thread adminThread = new AdminThread(bundleContext); +// adminThread.start(); // bootstrap - OsgiBoot osgiBoot = new OsgiBoot(bundleContext); +// OsgiBoot osgiBoot = new OsgiBoot(bundleContext); if (checkpoint == null) { - osgiBoot.bootstrap(); +// osgiBoot.bootstrap(); checkpoint = System.currentTimeMillis(); } else { - osgiBoot.update(); + runtimeContext.update(); checkpoint = System.currentTimeMillis(); } } public void stop(BundleContext context) throws Exception { + Objects.requireNonNull(runtimeContext); + runtimeContext.stop(context); } } diff --git a/org.argeo.init/src/org/argeo/init/osgi/AdminThread.java b/org.argeo.init/src/org/argeo/init/osgi/AdminThread.java index fdb6fe923..e493e2c7e 100644 --- a/org.argeo.init/src/org/argeo/init/osgi/AdminThread.java +++ b/org.argeo.init/src/org/argeo/init/osgi/AdminThread.java @@ -6,6 +6,7 @@ import org.osgi.framework.BundleContext; import org.osgi.framework.launch.Framework; /** Monitors the runtime and can shut it down. */ +@Deprecated public class AdminThread extends Thread { public final static String PROP_ARGEO_OSGI_SHUTDOWN_FILE = "argeo.osgi.shutdownFile"; private File shutdownFile; diff --git a/org.argeo.init/src/org/argeo/init/osgi/OsgiRuntimeContext.java b/org.argeo.init/src/org/argeo/init/osgi/OsgiRuntimeContext.java index 91756e32b..7f44f6b27 100644 --- a/org.argeo.init/src/org/argeo/init/osgi/OsgiRuntimeContext.java +++ b/org.argeo.init/src/org/argeo/init/osgi/OsgiRuntimeContext.java @@ -1,24 +1,42 @@ package org.argeo.init.osgi; +import java.util.Collections; +import java.util.Hashtable; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.ServiceLoader; +import java.util.concurrent.Flow; +import java.util.function.Consumer; import org.argeo.init.RuntimeContext; +import org.argeo.init.logging.ThinLoggerFinder; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; import org.osgi.framework.launch.Framework; import org.osgi.framework.launch.FrameworkFactory; +/** An OSGi runtime context. */ public class OsgiRuntimeContext implements RuntimeContext { private Map config; private Framework framework; private OsgiBoot osgiBoot; + @SuppressWarnings("rawtypes") + private ServiceRegistration loggingConfigurationSr; + @SuppressWarnings("rawtypes") + private ServiceRegistration logEntryPublisherSr; + public OsgiRuntimeContext(Map config) { this.config = config; } + public OsgiRuntimeContext(BundleContext bundleContext) { + start(bundleContext); + } + @Override public void run() { ServiceLoader sl = ServiceLoader.load(FrameworkFactory.class); @@ -29,17 +47,44 @@ public class OsgiRuntimeContext implements RuntimeContext { try { framework.start(); BundleContext bundleContext = framework.getBundleContext(); - osgiBoot = new OsgiBoot(bundleContext); - osgiBoot.bootstrap(); + start(bundleContext); } catch (BundleException e) { throw new IllegalStateException("Cannot start OSGi framework", e); } } + public void start(BundleContext bundleContext) { + // logging + loggingConfigurationSr = bundleContext.registerService(Consumer.class, + ThinLoggerFinder.getConfigurationConsumer(), + new Hashtable<>(Collections.singletonMap(Constants.SERVICE_PID, "argeo.logging.configuration"))); + logEntryPublisherSr = bundleContext.registerService(Flow.Publisher.class, + ThinLoggerFinder.getLogEntryPublisher(), + new Hashtable<>(Collections.singletonMap(Constants.SERVICE_PID, "argeo.logging.publisher"))); + + osgiBoot = new OsgiBoot(bundleContext); + osgiBoot.bootstrap(); + + } + + public void update() { + Objects.requireNonNull(osgiBoot); + osgiBoot.update(); + } + + public void stop(BundleContext bundleContext) { + if (loggingConfigurationSr != null) + loggingConfigurationSr.unregister(); + if (logEntryPublisherSr != null) + logEntryPublisherSr.unregister(); + + } + @Override public void waitForStop(long timeout) throws InterruptedException { if (framework == null) throw new IllegalStateException("Framework is not initialised"); + stop(framework.getBundleContext()); framework.waitForStop(timeout); }