| /******************************************************************************* |
| * Copyright (c) 2006, 2021 Cognos Incorporated, 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 |
| ******************************************************************************/ |
| package org.eclipse.osgi.internal.log; |
| |
| import java.io.PrintStream; |
| import java.security.AccessController; |
| import java.security.PrivilegedAction; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Enumeration; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Queue; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.ThreadFactory; |
| import java.util.concurrent.ThreadPoolExecutor; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.concurrent.locks.ReentrantReadWriteLock; |
| import org.eclipse.equinox.log.LogFilter; |
| import org.eclipse.equinox.log.SynchronousLogListener; |
| import org.eclipse.osgi.framework.util.ArrayMap; |
| import org.eclipse.osgi.internal.framework.EquinoxContainer; |
| import org.eclipse.osgi.internal.log.OrderedExecutor.OrderedTaskQueue; |
| import org.osgi.framework.Bundle; |
| import org.osgi.framework.ServiceFactory; |
| import org.osgi.framework.ServiceReference; |
| import org.osgi.framework.ServiceRegistration; |
| import org.osgi.service.log.LogEntry; |
| import org.osgi.service.log.LogLevel; |
| import org.osgi.service.log.LogListener; |
| |
| public class ExtendedLogReaderServiceFactory implements ServiceFactory<ExtendedLogReaderServiceImpl> { |
| |
| static final int MAX_RECURSIONS = 50; |
| |
| static final class LogTask implements Runnable { |
| private final LogEntry logEntry; |
| private final LogListener listener; |
| |
| LogTask(LogEntry logEntry, LogListener listener) { |
| this.logEntry = logEntry; |
| this.listener = listener; |
| } |
| |
| @Override |
| public void run() { |
| safeLogged(listener, logEntry); |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static final Enumeration<LogEntry> EMPTY_ENUMERATION = Collections.enumeration(Collections.EMPTY_LIST); |
| |
| static final LogFilter NULL_LOGGER_FILTER = (b, loggerName, logLevel) -> true; |
| |
| private static final LogFilter[] ALWAYS_LOG = new LogFilter[0]; |
| |
| private static PrintStream errorStream; |
| |
| private final ReentrantReadWriteLock listenersLock = new ReentrantReadWriteLock(); |
| private ArrayMap<LogListener, Object[]> listeners = new ArrayMap<>(5); |
| private LogFilter[] filters = null; |
| private final ThreadLocal<int[]> nestedCallCount = new ThreadLocal<>(); |
| private final LinkedList<LogEntry> history; |
| private final int maxHistory; |
| private final LogLevel defaultLevel; |
| |
| private OrderedExecutor executor; |
| |
| static boolean safeIsLoggable(LogFilter filter, Bundle bundle, String name, int level) { |
| try { |
| return filter.isLoggable(bundle, name, level); |
| } catch (RuntimeException | LinkageError e) { |
| // "listener.logged" calls user code and might throw an unchecked exception |
| // we catch the error here to gather information on where the problem occurred. |
| // Catch linkage errors as these are generally recoverable but let other Errors propagate (see bug 222001) |
| getErrorStream().println("LogFilter.isLoggable threw a non-fatal unchecked exception as follows:"); //$NON-NLS-1$ |
| e.printStackTrace(getErrorStream()); |
| } |
| return false; |
| } |
| |
| private static synchronized PrintStream getErrorStream() { |
| if (errorStream == null) |
| return System.err; |
| |
| return errorStream; |
| } |
| |
| public static synchronized void setErrorStream(PrintStream ps) { |
| errorStream = ps; |
| } |
| |
| static void safeLogged(LogListener listener, LogEntry logEntry) { |
| try { |
| listener.logged(logEntry); |
| } catch (RuntimeException | LinkageError e) { |
| // "listener.logged" calls user code and might throw an unchecked exception |
| // we catch the error here to gather information on where the problem occurred. |
| // Catch linkage errors as these are generally recoverable but let other Errors propagate (see bug 222001) |
| getErrorStream().println("LogListener.logged threw a non-fatal unchecked exception as follows:"); //$NON-NLS-1$ |
| e.printStackTrace(getErrorStream()); |
| } |
| } |
| |
| public ExtendedLogReaderServiceFactory(int maxHistory, LogLevel defaultLevel) { |
| this.defaultLevel = defaultLevel; |
| this.maxHistory = maxHistory; |
| if (maxHistory > 0) { |
| history = new LinkedList<>(); |
| } else { |
| history = null; |
| } |
| } |
| |
| public void start(EquinoxContainer equinoxContainer) { |
| executor = new OrderedExecutor(equinoxContainer); |
| } |
| |
| public void stop() { |
| executor.shutdown(); |
| } |
| |
| public LogLevel getDefaultLogLevel() { |
| return defaultLevel; |
| } |
| |
| @Override |
| public ExtendedLogReaderServiceImpl getService(Bundle bundle, ServiceRegistration<ExtendedLogReaderServiceImpl> registration) { |
| return new ExtendedLogReaderServiceImpl(this); |
| } |
| |
| @Override |
| public void ungetService(Bundle bundle, ServiceRegistration<ExtendedLogReaderServiceImpl> registration, ExtendedLogReaderServiceImpl service) { |
| service.shutdown(); |
| } |
| |
| boolean isLoggable(final Bundle bundle, final String name, final int level) { |
| if (System.getSecurityManager() != null) { |
| return AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> isLoggablePrivileged(bundle, name, level)); |
| } |
| return isLoggablePrivileged(bundle, name, level); |
| } |
| |
| boolean isLoggablePrivileged(Bundle bundle, String name, int level) { |
| LogFilter[] filtersCopy; |
| listenersLock.readLock().lock(); |
| try { |
| filtersCopy = filters; |
| } finally { |
| listenersLock.readLock().unlock(); |
| } |
| try { |
| if (incrementNestedCount() == MAX_RECURSIONS) |
| return false; |
| if (filtersCopy == null) |
| return false; |
| |
| if (filtersCopy == ALWAYS_LOG) |
| return true; |
| |
| int filtersLength = filtersCopy.length; |
| for (int i = 0; i < filtersLength; i++) { |
| LogFilter filter = filtersCopy[i]; |
| if (safeIsLoggable(filter, bundle, name, level)) |
| return true; |
| } |
| } finally { |
| decrementNestedCount(); |
| } |
| return false; |
| } |
| |
| private int incrementNestedCount() { |
| int[] count = getCount(); |
| count[0] = count[0] + 1; |
| return count[0]; |
| } |
| |
| private void decrementNestedCount() { |
| int[] count = getCount(); |
| if (count[0] == 0) |
| return; |
| count[0] = count[0] - 1; |
| } |
| |
| private int[] getCount() { |
| int[] count = nestedCallCount.get(); |
| if (count == null) { |
| count = new int[] {0}; |
| nestedCallCount.set(count); |
| } |
| return count; |
| } |
| |
| void log(final Bundle bundle, final String name, final StackTraceElement stackTraceElement, final Object context, final LogLevel logLevelEnum, final int level, final String message, final ServiceReference<?> ref, final Throwable exception) { |
| if (System.getSecurityManager() != null) { |
| AccessController.doPrivileged((PrivilegedAction<Void>) () -> { |
| logPrivileged(bundle, name, stackTraceElement, context, logLevelEnum, level, message, ref, exception); |
| return null; |
| }); |
| } else { |
| logPrivileged(bundle, name, stackTraceElement, context, logLevelEnum, level, message, ref, exception); |
| } |
| } |
| |
| void logPrivileged(Bundle bundle, String name, StackTraceElement stackTraceElement, Object context, LogLevel logLevelEnum, int level, String message, ServiceReference<?> ref, Throwable exception) { |
| LogEntry logEntry = new ExtendedLogEntryImpl(bundle, name, stackTraceElement, context, logLevelEnum, level, message, ref, exception); |
| storeEntry(logEntry); |
| ArrayMap<LogListener, Object[]> listenersCopy; |
| listenersLock.readLock().lock(); |
| try { |
| listenersCopy = listeners; |
| } finally { |
| listenersLock.readLock().unlock(); |
| } |
| try { |
| if (incrementNestedCount() >= MAX_RECURSIONS) |
| return; |
| int size = listenersCopy.size(); |
| for (int i = 0; i < size; i++) { |
| Object[] listenerObjects = listenersCopy.getValue(i); |
| LogFilter filter = (LogFilter) listenerObjects[0]; |
| if (safeIsLoggable(filter, bundle, name, level)) { |
| LogListener listener = listenersCopy.getKey(i); |
| OrderedTaskQueue orderedTaskQueue = (OrderedTaskQueue) listenerObjects[1]; |
| if (orderedTaskQueue != null) { |
| orderedTaskQueue.execute(new LogTask(logEntry, listener), size); |
| } else { |
| // log synchronously |
| safeLogged(listener, logEntry); |
| } |
| } |
| } |
| } finally { |
| decrementNestedCount(); |
| } |
| } |
| |
| private void storeEntry(LogEntry logEntry) { |
| if (history != null) { |
| synchronized (history) { |
| if (history.size() == maxHistory) { |
| history.removeLast(); |
| } |
| history.addFirst(logEntry); |
| } |
| } |
| } |
| |
| void addLogListener(LogListener listener, LogFilter filter) { |
| listenersLock.writeLock().lock(); |
| try { |
| ArrayMap<LogListener, Object[]> listenersCopy = new ArrayMap<>(listeners.getKeys(), listeners.getValues()); |
| Object[] listenerObjects = listenersCopy.get(listener); |
| if (listenerObjects == null) { |
| // Only create a task queue for non-SynchronousLogListeners |
| OrderedTaskQueue taskQueue = (listener instanceof SynchronousLogListener) ? null : executor.createQueue(); |
| listenerObjects = new Object[] {filter, taskQueue}; |
| } else if (filter != listenerObjects[0]) { |
| // update the filter |
| listenerObjects[0] = filter; |
| } |
| listenersCopy.put(listener, listenerObjects); |
| recalculateFilters(listenersCopy); |
| listeners = listenersCopy; |
| } finally { |
| listenersLock.writeLock().unlock(); |
| } |
| } |
| |
| private void recalculateFilters(ArrayMap<LogListener, Object[]> listenersCopy) { |
| List<LogFilter> filtersList = new ArrayList<>(); |
| int size = listenersCopy.size(); |
| for (int i = 0; i < size; i++) { |
| Object[] listenerObjects = listenersCopy.getValue(i); |
| LogFilter filter = (LogFilter) listenerObjects[0]; |
| if (filter == NULL_LOGGER_FILTER) { |
| filters = ALWAYS_LOG; |
| return; |
| } |
| filtersList.add(filter); |
| } |
| |
| if (filtersList.isEmpty()) |
| filters = null; |
| |
| filters = filtersList.toArray(new LogFilter[filtersList.size()]); |
| } |
| |
| void removeLogListener(LogListener listener) { |
| listenersLock.writeLock().lock(); |
| try { |
| ArrayMap<LogListener, Object[]> listenersCopy = new ArrayMap<>(listeners.getKeys(), listeners.getValues()); |
| listenersCopy.remove(listener); |
| recalculateFilters(listenersCopy); |
| listeners = listenersCopy; |
| } finally { |
| listenersLock.writeLock().unlock(); |
| } |
| } |
| |
| Enumeration<LogEntry> getLog() { |
| if (history == null) { |
| return EMPTY_ENUMERATION; |
| } |
| synchronized (history) { |
| return Collections.enumeration(new ArrayList<>(history)); |
| } |
| } |
| |
| } |
| |
| /** |
| * This Executor uses OrderedTaskQueue to execute tasks in a FIFO order. |
| */ |
| class OrderedExecutor implements ThreadFactory { |
| private final int nThreads = Math.min(Runtime.getRuntime().availableProcessors(), 10); |
| private final String logThreadName; |
| private final ThreadPoolExecutor delegate; |
| private final LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(); |
| private int coreSize = 0; |
| |
| public OrderedExecutor(final EquinoxContainer equinoxContainer) { |
| this.logThreadName = "Equinox Log Thread - " + equinoxContainer.toString(); //$NON-NLS-1$ |
| this.delegate = new ThreadPoolExecutor(0, nThreads, 10L, TimeUnit.SECONDS, queue, this); |
| } |
| |
| @Override |
| public Thread newThread(Runnable r) { |
| Thread t = new Thread(r, logThreadName); |
| t.setDaemon(true); |
| return t; |
| } |
| |
| void executeOrderedTask(Runnable task, OrderedTaskQueue dependencyQueue, int numListeners) { |
| OrderedTaskQueue.OrderedTask firstOrderedTask; |
| synchronized (this) { |
| // This will either queue the task to wait for existing ones to finish |
| // or it will return the first ordered task so it can kick off the execution |
| // for ordered task queue |
| firstOrderedTask = dependencyQueue.addTaskAndReturnIfFirst(task); |
| if (firstOrderedTask != null) { |
| // Check that we are at the optimal target for core pool size |
| int targetSize = Math.min(nThreads, numListeners); |
| if (coreSize < targetSize) { |
| coreSize = targetSize; |
| delegate.setCorePoolSize(coreSize); |
| } |
| } |
| } |
| |
| // execute the first ordered task now |
| if (firstOrderedTask != null) { |
| delegate.execute(firstOrderedTask); |
| } |
| } |
| |
| OrderedTaskQueue createQueue() { |
| return new OrderedTaskQueue(); |
| } |
| |
| void shutdown() { |
| delegate.shutdown(); |
| } |
| |
| void executeNextTask(OrderedTaskQueue taskQueue) { |
| OrderedTaskQueue.OrderedTask nextTask; |
| synchronized (this) { |
| nextTask = taskQueue.getNextTask(); |
| if (nextTask == null && queue.isEmpty()) { |
| // The event storm has ended, let the threads be reclaimed |
| delegate.setCorePoolSize(0); |
| coreSize = 0; |
| } |
| } |
| if (nextTask != null) { |
| delegate.execute(nextTask); |
| } |
| } |
| |
| /** |
| * Keeps an list of ordered tasks and guarantees the tasks are run in the order |
| * they are queued. Tasks executed with this queue will always be run |
| * in FIFO order and will never run in parallel to guarantee events are |
| * received in the proper order by the listener. Each log listener |
| * has its own ordered task queue. |
| * <p> |
| * Note that only the execute method is thread safe. All other methods |
| * must be guarded by the OrderedExecutor monitor. |
| */ |
| class OrderedTaskQueue { |
| private final Queue<OrderedTask> dependencyQueue = new LinkedList<>(); |
| private AtomicReference<OrderedTask> firstTask = new AtomicReference<>(); |
| |
| void execute(Runnable task, int numListeners) { |
| executeOrderedTask(task, this, numListeners); |
| } |
| |
| OrderedTask addTaskAndReturnIfFirst(Runnable task) { |
| OrderedTask orderedTask = new OrderedTask(task); |
| if (firstTask.compareAndSet(null, orderedTask)) { |
| return orderedTask; |
| } |
| dependencyQueue.add(orderedTask); |
| return null; |
| } |
| |
| OrderedTask getNextTask() { |
| OrderedTask nextTask = dependencyQueue.poll(); |
| if (nextTask == null) { |
| // The queue has been drained reset the first task. |
| // the next task for this ordered task queue will become the first again |
| firstTask.set(null); |
| } |
| return nextTask; |
| } |
| |
| class OrderedTask implements Runnable { |
| private final Runnable task; |
| |
| public OrderedTask(Runnable task) { |
| this.task = task; |
| } |
| |
| @Override |
| public void run() { |
| try { |
| task.run(); |
| } finally { |
| executeNextTask(OrderedTaskQueue.this); |
| } |
| } |
| } |
| } |
| } |