From 2f4d6897222bdf5e4cba8c73a3aaaf15d294b405 Mon Sep 17 00:00:00 2001 From: asofold Date: Mon, 17 Nov 2014 23:54:33 +0100 Subject: [PATCH] [PLACEBO] Raw sketch of the upcoming logging framework, doing nothing. --- .../nocheatplus/logging/LoggerID.java | 25 + .../nocheatplus/logging/StreamID.java | 25 + .../nocheatplus/logging/Streams.java | 30 ++ .../logging/details/AbstractLogManager.java | 487 ++++++++++++++++++ .../details/AbstractLogNodeDispatcher.java | 232 +++++++++ .../logging/details/ContentLogger.java | 16 + .../logging/details/ContentStream.java | 30 ++ .../logging/details/DefaultContentStream.java | 51 ++ .../logging/details/FileLogger.java | 143 +++++ .../logging/details/FileLoggerAdapter.java | 26 + .../nocheatplus/logging/details/LogNode.java | 18 + .../logging/details/LogNodeDispatcher.java | 33 ++ .../logging/details/LogOptions.java | 51 ++ .../logging/details/LogRecord.java | 28 + .../logging/details/LoggerAdapter.java | 19 + .../utilities/ds/corw/QueueRORA.java | 96 ++++ .../nocheatplus/config/DefaultConfig.java | 1 + .../nocheatplus/logging/LogManager.java | 154 ++++++ .../details/BukkitLogNodeDispatcher.java | 69 +++ 19 files changed, 1534 insertions(+) create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/LoggerID.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/StreamID.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/Streams.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogManager.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogNodeDispatcher.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentLogger.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentStream.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/DefaultContentStream.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLogger.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLoggerAdapter.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNode.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNodeDispatcher.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogOptions.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogRecord.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LoggerAdapter.java create mode 100644 NCPCommons/src/main/java/fr/neatmonster/nocheatplus/utilities/ds/corw/QueueRORA.java create mode 100644 NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/LogManager.java create mode 100644 NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/details/BukkitLogNodeDispatcher.java diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/LoggerID.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/LoggerID.java new file mode 100644 index 00000000..c10ef992 --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/LoggerID.java @@ -0,0 +1,25 @@ +package fr.neatmonster.nocheatplus.logging; + +/** + * Restrictions for registration: + *
  • Unique instances.
  • + *
  • Unique names.
  • + *
  • Custom registrations can not start with the default prefix (see AbstractLogManager).
  • + * + * @author dev1mc + * + */ +public class LoggerID { + + public final String name; + + public LoggerID(String name) { + this.name = name; + } + + @Override + public String toString() { + return ""; + } + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/StreamID.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/StreamID.java new file mode 100644 index 00000000..130a76c4 --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/StreamID.java @@ -0,0 +1,25 @@ +package fr.neatmonster.nocheatplus.logging; + +/** + * Restrictions for registration: + *
  • Unique instances.
  • + *
  • Unique names.
  • + *
  • Custom registrations can not start with the default prefix (see AbstractLogManager).
  • + * + * @author dev1mc + * + */ +public class StreamID { + + public final String name; + + public StreamID(String name) { + this.name = name; + } + + @Override + public String toString() { + return ""; + } + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/Streams.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/Streams.java new file mode 100644 index 00000000..645a3d41 --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/Streams.java @@ -0,0 +1,30 @@ +package fr.neatmonster.nocheatplus.logging; + +/** + * Default StreamIDs. + * @author dev1mc + * + */ +public class Streams { + + // Maybe temporary: StremID instances for default usage (custom registrations can not start with "default."). + /** + * Default prefix, should not be used with custom registration (LoggerID, StreamID). + */ + public static final String defaultPrefix = "default."; + + // More or less raw default streams. + + /** For initialization and shutdown, not more than primary thread only is guaranteed here. Always available (fall-back). */ + public static final StreamID INIT = new StreamID(defaultPrefix + "init"); + /** Might not allow asynchronous access. */ + public static final StreamID SERVER_LOGGER = new StreamID(defaultPrefix + "logger.server"); + /** Might not allow asynchronous access. */ + public static final StreamID PLUGIN_LOGGER = new StreamID(defaultPrefix + "logger.plugin"); + public static final StreamID NOTIFY_INGAME = new StreamID(defaultPrefix + "chat.notify"); + public static final StreamID DEFAULT_FILE = new StreamID(defaultPrefix + "file"); + public static final StreamID TRACE_FILE = new StreamID(defaultPrefix + "file.trace"); + + // TODO: More abstract streams (init, trace, violations?), have loggers/files be LoggerID ? + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogManager.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogManager.java new file mode 100644 index 00000000..7c345ffc --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogManager.java @@ -0,0 +1,487 @@ +package fr.neatmonster.nocheatplus.logging.details; + +import java.io.File; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import fr.neatmonster.nocheatplus.logging.LoggerID; +import fr.neatmonster.nocheatplus.logging.StreamID; +import fr.neatmonster.nocheatplus.utilities.StringUtil; + +/** + * Central access point for logging. Abstract class providing basic registration functionality. + * @author dev1mc + * + */ +public abstract class AbstractLogManager { + + // TODO: Visibility of methods. + // TODO: Add option for per stream prefixes. + // TODO: Concept for adding in the time at the point of call/scheduling. + + // TODO: Add a void stream (no loggers, allow logging unknown ids to void). + // TODO: Allow route log with non existing stream to init or void (flag). + // TODO: Attaching loggers with different options to different streams is necessary (e.g. normal -> async logging, but any thread with init). + // TODO: Re-register with other options: Add methods for LoggerID + StreamID + options. + // TODO: Hierarchical LogNode relations, to ensure other log nodes with the same logger are removed too [necessary to allow removing individual loggers]. + + // TODO: Temporary streams, e.g. for players, unregistering with command and/or logout. + // TODO: Mechanics of removing temporary streams: 1. remove + + // TODO: Consider generalizing the (internal) implementation right away (sub registry by content class). + // TODO: Consider adding a global cache (good for re-mapping, contra: reload is not intended to happen on a regular basis). + + private final LogNodeDispatcher dispatcher; + private final String defaultPrefix; + + /** + * Fast streamID access map (runtime). Copy on write with registryLock. + */ + private Map> idStreamMap = new IdentityHashMap>(); + + /** + * Map name to Stream. Copy on write with registryLock. + */ + private Map> nameStreamMap = new HashMap>(); + + /** + * Lower-case name to StreamID. + */ + private Map nameStreamIDMap = new HashMap(); + + /** + * LogNode registry by LoggerID. Copy on write with registryLock. + */ + private Map> idNodeMap = new IdentityHashMap>(); + + /** + * LogNode registry by lower-case name. Copy on write with registryLock. + */ + private Map> nameNodeMap = new HashMap>(); + + /** + * Lower-case name to LoggerID. + */ + private Map nameLoggerIDMap = new HashMap(); + + /** Registry changes have to be done under this lock (copy on write) */ + protected final Object registryCOWLock = new Object(); + // TODO: Future: Only an init string stream or (later) always "the init stream" for all content types. + private final StreamID initStreamID; + private final StreamID voidStreamID = new StreamID("void"); + /** + * Fall-back StreamID, for the case of logging to a non-existing StreamID. + * By default set to void, but can be altered. Set to null to have calls + * fail. + */ + private StreamID fallBackStreamID = voidStreamID; + + /** + * Wrapping logging to the init stream. + */ + protected final ContentLogger initLogger = new ContentLogger() { + @Override + public void log(final Level level, final String content) { + AbstractLogManager.this.log(getInitStreamID(), level, content); + } + }; + + /** + * + * @param dispatcher + * @param defaultPrefix + * @param initStreamID This id is stored, the stream is created, but no loggers will be attached to it within this constructor. + */ + public AbstractLogManager(LogNodeDispatcher dispatcher, String defaultPrefix, StreamID initStreamID) { + this.dispatcher = dispatcher; + this.defaultPrefix = defaultPrefix; + this.initStreamID = initStreamID; + createInitStream(); + registerInitLogger(); + dispatcher.setInitLogger(initLogger); + } + + /** + * Create stream if it does not exist. + */ + protected void createInitStream() { + synchronized (registryCOWLock) { + if (!hasStream(initStreamID)) { + createStringStream(initStreamID); + } + } + } + + /** + * Create the minimal init logger(s). Synchronize over registryCOWLock. It's preferable not to duplicate loggers. Prefer LoggerID("init..."). + */ + protected abstract void registerInitLogger(); + + protected LogNodeDispatcher getLogNodeDispatcher() { + return dispatcher; + } + + /** + * Don't use this prefix for custom registrations with StreamID and LoggerID. + * @return + */ + public String getDefaultPrefix() { + return defaultPrefix; + } + + /** + * This should be a fail-safe direct String-logger, that has the highest probability of working + * within the default context and rather skips messages instead of failing or scheduling tasks, + * typically the main application primary thread. + * + * @return + */ + public StreamID getInitStreamID() { + return initStreamID; + } + + /** + * A stream that skips all messages. It's not registered officially. + * @return + */ + public StreamID getVoidStreamID() { + return voidStreamID; + } + + /** + * Case-insensitive lookup. + * @param name + * @return Returns the registered StreamID or null, if not present. + */ + public StreamID getStreamID(String name) { + return nameStreamIDMap.get(name.toLowerCase()); + } + + /** + * Case-insensitive lookup. + * @param name + * @return Returns the registered StreamID or null, if not present. + */ + public LoggerID getLoggerID(String name) { + return nameLoggerIDMap.get(name.toLowerCase()); + } + + public void debug(final StreamID streamID, final String message) { + log(streamID, Level.FINE, message); // TODO: Not sure what happens with FINE and provided Logger instances. + } + + public void info(final StreamID streamID, final String message) { + log(streamID, Level.INFO, message); + } + + public void warning(final StreamID streamID, final String message) { + log(streamID, Level.WARNING, message); + } + public void severe(final StreamID streamID, final String message) { + log(streamID, Level.SEVERE, message); + } + + public void log(final StreamID streamID, final Level level, final String message) { + if (streamID != voidStreamID) { + final ContentStream stream = idStreamMap.get(streamID); + if (stream != null) { + stream.log(level, message); + } else { + handleFallBack(streamID, level, message); + } + } + } + + private void handleFallBack(final StreamID streamID, final Level level, final String message) { + if (fallBackStreamID != null && streamID != fallBackStreamID) { + log(fallBackStreamID, level, message); + } else { + throw new RuntimeException("Stream not registered: " + streamID); + } + } + + public void debug(final StreamID streamID, final Throwable t) { + log(streamID, Level.FINE, t); // TODO: Not sure what happens with FINE and provided Logger instances. + } + + public void info(final StreamID streamID, final Throwable t) { + log(streamID, Level.INFO, t); + } + + public void warning(final StreamID streamID, final Throwable t) { + log(streamID, Level.WARNING, t); + } + + public void severe(final StreamID streamID, final Throwable t) { + log(streamID, Level.SEVERE, t); + } + + public void log(final StreamID streamID, final Level level, final Throwable t) { + // Not sure adding streams for Throwable would be better. + log(streamID, level, StringUtil.throwableToString(t)); + } + + /** + * A newly created id can be used here. For logging use existing ids always. + * @param streamID + * @return + */ + public boolean hasStream(final StreamID streamID) { + return this.idStreamMap.containsKey(streamID) || this.nameStreamMap.containsKey(streamID.name.toLowerCase()); + } + + public boolean hasStream(String name) { + return getStreamID(name) != null; + } + + /** + * Call under lock. + * @param streamID + */ + private void testRegisterStream(final StreamID streamID) { + if (streamID == null) { + throw new NullPointerException("StreamID must not be null."); + } + else if (streamID.name == null) { + throw new NullPointerException("StreamID.name must not be null."); + } + else if (streamID.name.equalsIgnoreCase(voidStreamID.name)) { + throw new RuntimeException("Can not overrite void StreamID."); + } + else if (hasStream(streamID)) { + throw new IllegalArgumentException("Stream already registered: " + streamID.name.toLowerCase()); + } + } + + protected ContentStream createStringStream(final StreamID streamID) { + ContentStream stream; + synchronized (registryCOWLock) { + testRegisterStream(streamID); + Map> idStreamMap = new IdentityHashMap>(this.idStreamMap); + Map> nameStreamMap = new HashMap>(this.nameStreamMap); + Map nameStreamIDMap = new HashMap(this.nameStreamIDMap); + stream = new DefaultContentStream(dispatcher); + idStreamMap.put(streamID, stream); + nameStreamMap.put(streamID.name.toLowerCase(), stream); + nameStreamIDMap.put(streamID.name.toLowerCase(), streamID); + this.idStreamMap = idStreamMap; + this.nameStreamMap = nameStreamMap; + this.nameStreamIDMap = nameStreamIDMap; + + } + return stream; + } + + /** + * A newly created id can be used here. For logging use existing ids always. + * @param loggerID + * @return + */ + public boolean hasLogger(final LoggerID loggerID) { + return this.idNodeMap.containsKey(loggerID) || this.nameNodeMap.containsKey(loggerID.name.toLowerCase()); + } + + public boolean hasLogger(String name) { + return getLoggerID(name) != null; + } + + /** + * Call under lock. + * @param loggerID + */ + private void testRegisterLogger(final LoggerID loggerID) { + if (loggerID == null) { + throw new NullPointerException("LoggerID must not be null."); + } + else if (loggerID.name == null) { + throw new NullPointerException("LoggerID.name must not be null."); + } + else if (hasLogger(loggerID)) { + throw new IllegalArgumentException("Logger already registered: " + loggerID.name.toLowerCase()); + } + } + + /** + * Convenience method. + * @param logger + * @param options + * @return + */ + protected LoggerID registerStringLogger(final ContentLogger logger, final LogOptions options) { + LoggerID loggerID = new LoggerID(options.name); + registerStringLogger(loggerID, logger, options); + return loggerID; + } + + /** + * Convenience method. + * @param logger + * @param options + * @return + */ + protected LoggerID registerStringLogger(final Logger logger, final LogOptions options) { + LoggerID loggerID = new LoggerID(options.name); + registerStringLogger(loggerID, logger, options); + return loggerID; + } + + /** + * Convenience method. + * @param logger + * @param options + * @return + */ + protected LoggerID registerStringLogger(final File file, final LogOptions options) { + LoggerID loggerID = new LoggerID(options.name); + registerStringLogger(loggerID, file, options); + return loggerID; + } + + protected LogNode registerStringLogger(final LoggerID loggerID, final ContentLogger logger, final LogOptions options) { + LogNode node; + synchronized (registryCOWLock) { + testRegisterLogger(loggerID); + Map> idNodeMap = new IdentityHashMap>(this.idNodeMap); + Map> nameNodeMap = new HashMap>(this.nameNodeMap); + Map nameLoggerIDMap = new HashMap(this.nameLoggerIDMap); + node = new LogNode(loggerID, logger, options); + idNodeMap.put(loggerID, node); + nameNodeMap.put(loggerID.name.toLowerCase(), node); + nameLoggerIDMap.put(loggerID.name.toLowerCase(), loggerID); + this.idNodeMap = idNodeMap; + this.nameNodeMap = nameNodeMap; + this.nameLoggerIDMap = nameLoggerIDMap; + } + return node; + } + + protected LogNode registerStringLogger(LoggerID loggerID, Logger logger, LogOptions options) { + LogNode node; + synchronized (registryCOWLock) { + LoggerAdapter adapter = new LoggerAdapter(logger); // Low cost. + // TODO: Store loggers too to prevent redundant registration. + node = registerStringLogger(loggerID, adapter, options); + } + return node; + } + + protected LogNode registerStringLogger(LoggerID loggerID, File file, LogOptions options) { + LogNode node; + synchronized (registryCOWLock) { + testRegisterLogger(loggerID); // Redundant checking, because file loggers are expensive. + // TODO: Detect duplicate loggers (register same logger with given id and options). + FileLoggerAdapter adapter = new FileLoggerAdapter(file); + if (adapter.isInoperable()) { + adapter.detachLogger(); + throw new RuntimeException("Failed to set up file logger for id '" + loggerID + "': " + file); + } + try { + node = registerStringLogger(loggerID, adapter, options); + } catch (Exception ex) { + // TODO: Exception is still bad. + adapter.detachLogger(); + throw new RuntimeException(ex); + } + } + return node; + } + + /** + * Attach a logger to a stream. Redundant attaching will mean no changes. + * @param loggerID Must exist. + * @param streamID Must exist. + */ + protected void attachStringLogger(final LoggerID loggerID, final StreamID streamID) { + // TODO: More light-weight locking concept (thinking of dynamically changing per player streams)? + synchronized (registryCOWLock) { + if (!hasLogger(loggerID)) { + throw new RuntimeException("Logger is not registered: " + loggerID); + } + if (!hasStream(streamID)) { + // Note: This should also ensure the voidStreamID can't be used, because that one can't be registered. + throw new RuntimeException("Stream is not registered: " + streamID); + } + final LogNode node = idNodeMap.get(loggerID); + if (streamID == initStreamID) { + // TODO: Not sure about restrictions here. Could allow attaching other streams temporarily if other stuff is wanted. + switch(node.options.callContext) { + case PRIMARY_THREAD_ONLY: + case ANY_THREAD_DIRECT: + break; + default: + throw new RuntimeException("Unsupported call context for init stream " + streamID + ": " + node.options.callContext); + } + } + idStreamMap.get(streamID).addNode(node); + } + } + + // TODO: Methods to replace options for loggers (+ loggers themselves) + + // TODO: Later: attach streams to streams ? [few loggers: attach loggers rather] + + // TODO: logger/stream: allow id lookup logger, file, etc. ? + + /** + * Remove all loggers and streams including init, resulting in roughly the + * same state as is after calling the AbstractLogger constructor. Call from + * the primary thread (policy pending). If fallBackStreamID is set, it will be replaced by the init stream (if available) or by the void stream. + * + * @param msWaitFlush + */ + protected void clear(final long msWaitFlush, final boolean recreateInitLogger) { + // TODO: enum (remove_all, recreate init, remap init, remap all to init) + synchronized (registryCOWLock) { + // Remove loggers from string streams. + for (ContentStream stream : idStreamMap.values()) { + stream.clear(); + } + // Flush queues. + dispatcher.flush(msWaitFlush); + // Close/flush/shutdown string loggers, where possible, remove all from registry. + for (final LogNode node : idNodeMap.values()) { + if (node.logger instanceof FileLoggerAdapter) { + FileLoggerAdapter logger = (FileLoggerAdapter) node.logger; + logger.flush(); + logger.detachLogger(); + } + } + idNodeMap = new IdentityHashMap>(); + nameNodeMap = new HashMap>(); + nameLoggerIDMap = new HashMap(); + // Remove string streams. + idStreamMap = new IdentityHashMap>(); + nameStreamMap = new HashMap>(); + nameStreamIDMap = new HashMap(); + if (recreateInitLogger) { + createInitStream(); + registerInitLogger(); + if (fallBackStreamID != null && fallBackStreamID != voidStreamID) { + fallBackStreamID = initStreamID; + } + } + else if (fallBackStreamID != null) { + fallBackStreamID = voidStreamID; + } + } + } + +// /** +// * Remove all registered streams and loggers, recreates init logger (and stream). +// */ +// public void clear(final long msWaitFlush) { +// clear(msWaitFlush, true); +// } + + /** + * Rather a graceful shutdown, including waiting for the asynchronous task, if necessary. Clear the registry. Also removes the init logger [subject to change]. + * Call from the primary thread (policy pending). + */ + public void shutdown() { + clear(500, false); // TODO: Policy / making sense. + } + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogNodeDispatcher.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogNodeDispatcher.java new file mode 100644 index 00000000..77e9f4e7 --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogNodeDispatcher.java @@ -0,0 +1,232 @@ +package fr.neatmonster.nocheatplus.logging.details; + +import java.util.List; +import java.util.logging.Level; + +import fr.neatmonster.nocheatplus.utilities.ds.corw.QueueRORA; + +/** + * Basic functionality, with int task ids, assuming a primary thread exists. + * @author dev1mc + * + */ +public abstract class AbstractLogNodeDispatcher implements LogNodeDispatcher { // TODO: Name + + // TODO: Queues might need a drop policy with thresholds. + // TODO: Allow multiple tasks for logging, e.g. per file, also thinking of SQL logging. Could pool + round-robin. + + /** + * This queue has to be processed in a task within the primary thread with calling runLogsPrimary. + */ + protected final QueueRORA> queuePrimary = new QueueRORA>(); + protected final QueueRORA> queueAsynchronous = new QueueRORA>(); + + /** Once a queue reaches this size, it will be reduced (loss of content). */ + protected int maxQueueSize = 5000; + + /** + * Task id, -1 means the asynchronous task is not running. Synchronize over + * queueAsynchronous. Must be maintained. + */ + protected int taskAsynchronousID = -1; + /** + * Optional implementation for an asynchronous task, using the + * taskAsynchronousID, synchronized over queueAsynchronous. + */ + protected final Runnable taskAsynchronous = new Runnable() { + + @Override + public void run() { + // TODO: A more sophisticated System to allow "wake up on burst"? + int i = 0; + while (i < 3) { + if (runLogsAsynchronous()) { + i = 0; + if (taskAsynchronousID == -1) { + // Shutdown, hard return; + return; + } + Thread.yield(); + } else { + i ++; + try { + Thread.sleep(25); + } catch (InterruptedException e) { + synchronized (queueAsynchronous) { + // Ensure re-scheduling can happen. + taskAsynchronousID = -1; + } + // TODO: throw? + return; + } + } + synchronized (queueAsynchronous) { + if (queueAsynchronous.isEmpty()) { + if (i >= 3) { + // Ensure re-scheduling can happen. + taskAsynchronousID = -1; + } + } else { + i = 0; + } + } + } + } + + }; + + /** + * Optional init logger to log errors. Should log to the init stream, no queuing. + */ + protected ContentLogger initLogger = null; + + public void dispatch(LogNode node, Level level, C content) { + // TODO: Try/catch ? + if (isWithinContext(node)) { + node.logger.log(level, content); + } else { + scheduleLog(node, level, content); + } + } + + protected boolean runLogsPrimary() { + return runLogs(queuePrimary); + } + + protected boolean runLogsAsynchronous() { + return runLogs(queueAsynchronous); + } + + private boolean runLogs(final QueueRORA> queue) { + // TODO: Consider allowYield + msYield, calling yield after 5 ms if async. + final List> records = queue.removeAll(); + if (records.isEmpty()) { + return false; + } + for (final LogRecord record : records) { + record.run(); + } + return true; + } + + @Override + public void flush(long ms) { + if (!isPrimaryThread()) { + // TODO: Could also switch policy to emptying the primary-thread queue if not called from within the primary thread. + throw new IllegalStateException("Must only be called from within the primary thread."); + } + // TODO: Note that all streams should be emptied here, except the fallback logger. + + // Cancel task. + synchronized (queueAsynchronous) { + if (taskAsynchronousID != -1) { + // TODO: Allow queues to set to "no more input" ? + cancelTask(taskAsynchronousID); + taskAsynchronousID = -1; + } else { + // No need to wait. + ms = 0L; + } + } + if (ms > 0) { + try { + // TODO: Replace by a better concept. + Thread.sleep(ms); + } catch (InterruptedException e) { + // Ignore. + } + } + + // Log the rest (from here logging should be done via the appropriate direct-only stream). + runLogsPrimary(); + runLogsAsynchronous(); + } + + protected boolean isWithinContext(LogNode node) { + switch (node.options.callContext) { + case PRIMARY_THREAD_DIRECT: + case PRIMARY_THREAD_ONLY: + return isPrimaryThread(); + case ANY_THREAD_DIRECT: + return true; + case ASYNCHRONOUS_ONLY: + case ASYNCHRONOUS_DIRECT: + return !isPrimaryThread(); + case PRIMARY_THREAD_TASK: + case ASYNCHRONOUS_TASK: + // TODO: Each: Consider detecting if within that task (should rather be in case of re-scheduling?). + return false; // Always schedule (!). + default: + return false; // Force scheduling. + } + } + + protected void scheduleLog(LogNode node, Level level, C content) { + final LogRecord record = new LogRecord(node, level, content); // TODO: parameters. + switch (node.options.callContext) { + case ASYNCHRONOUS_TASK: + case ASYNCHRONOUS_DIRECT: + if (queueAsynchronous.add(record) > maxQueueSize) { + reduceQueue(queueAsynchronous); + } + if (taskAsynchronousID == -1) { // Works, due to add being synchronized (not sure it's really better than full sync). + scheduleAsynchronous(); + } + break; + case PRIMARY_THREAD_TASK: + case PRIMARY_THREAD_DIRECT: + queuePrimary.add(record); + // TODO: Consider re-scheduling policy ? + break; + case ASYNCHRONOUS_ONLY: + case PRIMARY_THREAD_ONLY: + // SKIP LOGGING. + break; + default: + // (ANY_THREAD_DIRECT never gets scheduled.) + throw new IllegalArgumentException("Bad CallContext: " + node.options.callContext); + } + } + + /** + * Hard reduce the queue (heavy locking!). + * @param queue + */ + private void reduceQueue(final QueueRORA> queue) { + // TODO: Different dropping strategies (drop first, last, alternate). + final int dropped; + synchronized (queue) { + final int size = queue.size(); + if (size < maxQueueSize) { + // Never mind :). + return; + } + logINIT(Level.WARNING, "Dropping log entries from the " + (queue == queueAsynchronous ? "asynchronous" : "primary thread") + " queue to reduce memory consumption..."); + dropped = queue.reduce(maxQueueSize / 2); + } + logINIT(Level.WARNING, "Dropped " + dropped + " log entries from the " + (queue == queueAsynchronous ? "asynchronous" : "primary thread") + " queue."); + } + + @Override + public void setMaxQueueSize(int maxQueueSize) { + this.maxQueueSize = maxQueueSize; + } + + @Override + public void setInitLogger(ContentLogger logger) { + this.initLogger = logger; + } + + protected void logINIT(final Level level, final String message) { + if (initLogger != null) { + initLogger.log(level, message); + } + } + + protected abstract boolean isPrimaryThread(); + + protected abstract void scheduleAsynchronous(); + + protected abstract void cancelTask(int taskId); + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentLogger.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentLogger.java new file mode 100644 index 00000000..e5667751 --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentLogger.java @@ -0,0 +1,16 @@ +package fr.neatmonster.nocheatplus.logging.details; + +import java.util.logging.Level; + +/** + * Minimal interface for logging content to. + * @author dev1mc + * + * @param + */ +public interface ContentLogger { + + // TODO: Not sure about generics. + public void log(Level level, C content); + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentStream.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentStream.java new file mode 100644 index 00000000..83b6cb68 --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentStream.java @@ -0,0 +1,30 @@ +package fr.neatmonster.nocheatplus.logging.details; + + +/** + * Contracts: + *
  • Intended usage: register=seldom, log=often
  • + *
  • Asynchronous access must be possible and fast, without locks (rather copy on write).
  • + * @author dev1mc + * + * @param + */ +public interface ContentStream extends ContentLogger { + + // TODO: Maybe also an abstract class. + + // Maybe not: addFilter (filter away some stuff, e.g. by regex from config). + + // TODO: Consider extra arguments for efficient registratioon with COWs. + public void addNode(LogNode node); + + // addAdapter(ContentAdapter adapter) ? ID etc., attach to another stream. + + // Removal and look up methods. + + /** + * Remove all attached loggers and other. + */ + public void clear(); + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/DefaultContentStream.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/DefaultContentStream.java new file mode 100644 index 00000000..fb97206f --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/DefaultContentStream.java @@ -0,0 +1,51 @@ +package fr.neatmonster.nocheatplus.logging.details; + +import java.util.ArrayList; +import java.util.logging.Level; + +/** + * Locking is done with the internal node list, log does not block (extra to whatever loggers would do). + * @author dev1mc + * + * @param + */ +public class DefaultContentStream implements ContentStream { + + private ArrayList> nodes = new ArrayList>(); + + private final LogNodeDispatcher dispatcher; + + public DefaultContentStream(LogNodeDispatcher dispatcher) { + this.dispatcher = dispatcher; + } + + @Override + public void log(final Level level, final C content) { + final ArrayList> nodes = this.nodes; + for (int i = 0; i < nodes.size(); i++) { + dispatcher.dispatch(nodes.get(i), level, content); + } + } + + @Override + public void addNode(LogNode node) { + // TODO: Consider throwing things at callers, in case of duplicate logger entries? + synchronized (nodes) { + if (this.nodes.contains(node)) { + // TODO: Consider throwing something. + return; + } + ArrayList> nodes = new ArrayList>(this.nodes); + nodes.add(node); + this.nodes = nodes; + } + } + + @Override + public void clear() { + synchronized (nodes) { + nodes.clear(); + } + } + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLogger.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLogger.java new file mode 100644 index 00000000..20957106 --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLogger.java @@ -0,0 +1,143 @@ +package fr.neatmonster.nocheatplus.logging.details; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.logging.FileHandler; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import fr.neatmonster.nocheatplus.utilities.StringUtil; + +/** + * Wrap a file logger, adding convenience setup methods. + * @author dev1mc + * + */ +public class FileLogger { + + protected static class FileLogFormatter extends Formatter { + + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yy-MM-dd HH:mm:ss"); + + // TODO: Consider storing a custom line break (needs adding to StringUtil.throwableToString). + + @Override + public String format(final LogRecord record) { + final Throwable throwable = record.getThrown(); + final String message = record.getMessage(); + int roughLength = 64 + message.length(); + final String throwableMessage; + if (throwable != null) { + throwableMessage = StringUtil.throwableToString(throwable); + roughLength += throwableMessage.length(); + } else { + throwableMessage = null; + } + final StringBuilder builder = new StringBuilder(roughLength); + builder.append(dateFormat.format(record.getMillis())); + builder.append(" ["); + builder.append(record.getLevel().getLocalizedName().toUpperCase()); + builder.append("] "); + builder.append(message); + builder.append('\n'); + if (throwableMessage != null) { + builder.append(throwableMessage); + } + return builder.toString(); + } + + } + + public final Logger logger; + protected FileHandler fileHandler = null; + + protected boolean inoperable = false; + + /** + * Initialize with an anonymous logger and the given log-file path. + * + * @param file Path to log file or an existing directory. + */ + public FileLogger(File file) { + // TODO: Should re-add a file-name prefix (allow null). + // TODO: File encoding + line endings. + // TODO: Add options to switch file with file size, rolling files, etc. + // [could also keep track of rough size of written data, to switch file "on the fly", problem: which log? -> replace logger copy on write :p]. + logger = Logger.getAnonymousLogger(); + detachLogger(); + final File container; + if (file.isDirectory()) { + container = file; + // Find a file name. + SimpleDateFormat dateFormat = new SimpleDateFormat("yy-MM-dd"); + String prefix = dateFormat.format(System.currentTimeMillis()); + int n = 1; + while (true) { + file = new File(container, prefix + "-" + n + ".log"); + if (!file.exists()) { + break; + } + n ++; + } + } else { + container = file.getParentFile(); + } + // Ensure the container exists. + if (!container.exists()) { + container.mkdirs(); + } + // Create file and initialize logger. + if (!file.exists()) { + try { + file.createNewFile(); + } catch (IOException e) { + file = null; + } + } + if (file != null) { + try { + initLogger(file); + logger.log(Level.INFO, "Logger started."); + inoperable = false; + } catch (SecurityException e) { + } catch (IOException e) { + } + } + + } + + protected void initLogger(File file) throws SecurityException, IOException { + logger.setUseParentHandlers(false); + logger.setLevel(Level.ALL); + fileHandler = new FileHandler(file.getCanonicalPath(), true); + fileHandler.setLevel(Level.ALL); + fileHandler.setFormatter(new FileLogFormatter()); + logger.addHandler(fileHandler); + } + + public void flush() { + if (fileHandler != null) { + fileHandler.flush(); + } + } + + public void detachLogger() { + if (fileHandler != null) { + fileHandler.close(); + fileHandler = null; + } + for (Handler handler : logger.getHandlers()) { + logger.removeHandler(handler); + } + } + + public boolean isInoperable () { + return inoperable; + } + + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLoggerAdapter.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLoggerAdapter.java new file mode 100644 index 00000000..7b4eb63d --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLoggerAdapter.java @@ -0,0 +1,26 @@ +package fr.neatmonster.nocheatplus.logging.details; + +import java.io.File; +import java.util.logging.Level; + +public class FileLoggerAdapter extends FileLogger implements ContentLogger { + + // TODO: Store path/file either here or on FileLogger. + + // TODO: Do store the string path (directory / direct file path), to reference correctly. + + /** + * See FileLogger(File). + * @param file Path to log file or existing directory. + */ + public FileLoggerAdapter(File file) { + super(file); + } + + @Override + public void log(Level level, String content) { + // TODO: Check loggerisInoperable() ? + logger.log(level, content); + } + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNode.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNode.java new file mode 100644 index 00000000..1470314d --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNode.java @@ -0,0 +1,18 @@ +package fr.neatmonster.nocheatplus.logging.details; + +import fr.neatmonster.nocheatplus.logging.LoggerID; + + +public class LogNode { + + public final LoggerID loggerID; + public final ContentLogger logger; + public final LogOptions options; + + public LogNode(LoggerID loggerID, ContentLogger logger, LogOptions options) { + this.loggerID = loggerID; + this.logger = logger; + this.options = new LogOptions(options); + } + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNodeDispatcher.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNodeDispatcher.java new file mode 100644 index 00000000..d606558c --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNodeDispatcher.java @@ -0,0 +1,33 @@ +package fr.neatmonster.nocheatplus.logging.details; + +import java.util.logging.Level; + +/** + * Handle logging on LogNode instances (log directly, skip, schedule to log within tasks). + * @author dev1mc + * + */ +public interface LogNodeDispatcher { // TODO: Name. + + public void dispatch(LogNode node, Level level, C content); + + /** + * Cancel asynchronous tasks and remove all logs based on policy (log all or clear), default is to log + * all. Should be called from the primary thread, if it exists. + * @param ms Milliseconds to wait in case there is something being processed by asynchronous tasks. + */ + public abstract void flush(long ms); + + /** + * Allow to add a logger, for logging errors to the init stream. Must not use the queues. Can be null (no logging). + * @param logger + */ + void setInitLogger(ContentLogger logger); + + /** + * Set maximum queue size. After reaching that size queues will be reduced by dropping elements (asynchronous and primaray thread individually). + * @param maxQueueSize + */ + void setMaxQueueSize(int maxQueueSize); + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogOptions.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogOptions.java new file mode 100644 index 00000000..dff5e86b --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogOptions.java @@ -0,0 +1,51 @@ +package fr.neatmonster.nocheatplus.logging.details; + +public class LogOptions { + + /** + * Describe the context in which the log method may be used.
    + * Policies: + *
  • ..._DIRECT: Will log directly if within context, otherwise schedule to a task.
  • + *
  • ..._TASK: Always schedule to a task.
  • + * + * @author dev1mc + * + */ + public static enum CallContext { + // TODO: Consider making this a general enum for call contexts (API wrappers, player data etc. too?). + // Primary as long as it exists... + /** Only execute directly in the primary thread, for other threads use a task to execute within the primary thread. */ + PRIMARY_THREAD_DIRECT, + /** Always schedule to execute within the primary thread. */ + PRIMARY_THREAD_TASK, + /** Only log if within the primary thread. */ + PRIMARY_THREAD_ONLY, + /** Execute directly independently of thread. */ + ANY_THREAD_DIRECT, + /** Ensure asynchronous logging, but avoid scheduling. */ + ASYNCHRONOUS_DIRECT, + /** Always schedule to execute within a (more or less) dedicated asynchronous task. */ + ASYNCHRONOUS_TASK, + /** Ensure it's logged asynchronously. */ + ASYNCHRONOUS_ONLY, + + // CUSTOM_THREAD_DIRECT|TASK // Needs a variable (Thread, methods to sync into a specific thread would have to be registered in LogManager). + ; + + // TODO: Can distinguish further: Call from where, log from where directly, schedule from where (allow to skip certain contexts). + } + + public final String name; // TODO: Name necessary ? + public final CallContext callContext; + + public LogOptions(LogOptions options) { + this(options.name, options.callContext); + } + + public LogOptions(String name, CallContext callContext) { + this.name = name; + this.callContext = callContext; + // TODO: shutdown policy (clear, log), rather with per-world threads. + } + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogRecord.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogRecord.java new file mode 100644 index 00000000..76a3c213 --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogRecord.java @@ -0,0 +1,28 @@ +package fr.neatmonster.nocheatplus.logging.details; + +import java.util.logging.Level; + +/** + * A log message to be executed within a task from a queue, hiding the generics. + * @author dev1mc + * + */ +public class LogRecord implements Runnable { + + private final LogNode node; + private final Level level; + private final C content; + + public LogRecord(LogNode node, Level level, C content) { + this.node = node; + this.level = level; + this.content = content; + } + + @Override + public void run() { + // TODO: Checks / try-catch where? + node.logger.log(level, content); + } + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LoggerAdapter.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LoggerAdapter.java new file mode 100644 index 00000000..d67abfc7 --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LoggerAdapter.java @@ -0,0 +1,19 @@ +package fr.neatmonster.nocheatplus.logging.details; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class LoggerAdapter implements ContentLogger { + + protected final Logger logger; + + public LoggerAdapter(Logger logger) { + this.logger = logger; + } + + @Override + public void log(Level level, String content) { + logger.log(level, content); + } + +} diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/utilities/ds/corw/QueueRORA.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/utilities/ds/corw/QueueRORA.java new file mode 100644 index 00000000..43dd847f --- /dev/null +++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/utilities/ds/corw/QueueRORA.java @@ -0,0 +1,96 @@ +package fr.neatmonster.nocheatplus.utilities.ds.corw; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + + +/** + * A replace-on-read-all queue-thing, exchanging the internal list under lock by + * a new empty one, for a small locking time, so it is not really a typical + * copy-on-read. All methods use locking, the QueueRORA instance is used for + * locking. + * + * @author dev1mc + * + */ +public class QueueRORA { + + private LinkedList elements = new LinkedList(); + + /** + * Add to list (synchronized). + * @param element + * @return Size of queue after adding. + */ + public int add(final E element) { + final int size; + synchronized (this) { + elements.add(element); + size = elements.size(); + } + return size; + } + + /** + * + * @return An ordinary (linked) List containing all elements. + */ + public List removeAll() { + final List result; + synchronized (this) { + result = elements; + elements = new LinkedList(); + } + return result; + } + + /** + * Remove oldest entries until maxSize is reached. + * @param maxSize + * @return + */ + public int reduce(final int maxSize) { + int dropped = 0; + synchronized (this) { + final int size = elements.size(); + if (size <= maxSize) { + return dropped; + } + while (dropped < size - maxSize) { + elements.removeFirst(); + dropped ++; + } + } + return dropped; + } + + public void clear() { + removeAll(); + } + + /** + * + * @return + */ + public boolean isEmpty() { + final boolean isEmpty; + synchronized (this) { + isEmpty = elements.isEmpty(); + } + return isEmpty; + } + + /** + * + * @return + */ + public int size() { + final int size; + synchronized (this) { + size = elements.size(); + } + return size; + } + +} diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/config/DefaultConfig.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/config/DefaultConfig.java index b2faf641..1f64c398 100644 --- a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/config/DefaultConfig.java +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/config/DefaultConfig.java @@ -36,6 +36,7 @@ public class DefaultConfig extends ConfigFile { // not set(ConfPaths.CONFIGVERSION_SAVED, -1); set(ConfPaths.LOGGING_ACTIVE, true); set(ConfPaths.LOGGING_DEBUG, false); + set(ConfPaths.LOGGING_MAXQUEUESIZE, 5000); set(ConfPaths.LOGGING_BACKEND_CONSOLE_ACTIVE, true); set(ConfPaths.LOGGING_BACKEND_CONSOLE_PREFIX, "[NoCheatPlus] "); set(ConfPaths.LOGGING_BACKEND_FILE_ACTIVE, true); diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/LogManager.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/LogManager.java new file mode 100644 index 00000000..845168af --- /dev/null +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/LogManager.java @@ -0,0 +1,154 @@ +package fr.neatmonster.nocheatplus.logging; + +import java.io.File; +import java.util.logging.Level; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +import fr.neatmonster.nocheatplus.NCPAPIProvider; +import fr.neatmonster.nocheatplus.config.ConfPaths; +import fr.neatmonster.nocheatplus.config.ConfigFile; +import fr.neatmonster.nocheatplus.config.ConfigManager; +import fr.neatmonster.nocheatplus.logging.details.AbstractLogManager; +import fr.neatmonster.nocheatplus.logging.details.BukkitLogNodeDispatcher; +import fr.neatmonster.nocheatplus.logging.details.ContentLogger; +import fr.neatmonster.nocheatplus.logging.details.FileLoggerAdapter; +import fr.neatmonster.nocheatplus.logging.details.LogOptions; +import fr.neatmonster.nocheatplus.logging.details.LogOptions.CallContext; + + +/** + * Central access point for logging. The default loggers use the stream names, + * at least as prefixes).
    + * Note that logging to the init/plugin/server with debug/fine or finer, might + * result in the server loggers suppressing those. As long as default file is + * activated, logging to init will log all levels to the file. + * + * @author dev1mc + * + */ +public class LogManager extends AbstractLogManager { + + // TODO: Make LogManager an interface <- AbstractLogManager <- BukkitLogManager (hide some / instanceof). + + // TODO: ingame logging [ingame needs api to keep track of players who receive notifications.]. + // TODO: Later: Custom loggers (file, other), per-player-streams (debug per player), custom ingame loggers (one or more players). + + protected final Plugin plugin; + + /** + * This will create all default loggers as well. + * @param plugin + */ + public LogManager(Plugin plugin) { + super(new BukkitLogNodeDispatcher(plugin), Streams.defaultPrefix, Streams.INIT); + this.plugin = plugin; + ConfigFile config = ConfigManager.getConfigFile(); + createDefaultLoggers(config); + getLogNodeDispatcher().setMaxQueueSize(config.getInt(ConfPaths.LOGGING_MAXQUEUESIZE)); + } + + @Override + protected void registerInitLogger() { + synchronized (registryCOWLock) { + if (!hasStream(Streams.INIT)) { + createInitStream(); + } + else if (hasLogger("init")) { + // Shallow check. + return; + } + // Attach a new restrictive init logger. + // TODO: If thread-safe use ANY_THREAD_DIRECT (should then allow to interrupt). + LoggerID initLoggerID = registerStringLogger(new ContentLogger() { + + @Override + public void log(Level level, String content) { + try { + Bukkit.getLogger().log(level, content); + } catch (Throwable t) {} + } + + }, new LogOptions(Streams.INIT.name, CallContext.PRIMARY_THREAD_ONLY)); + attachStringLogger(initLoggerID, Streams.INIT); + } + } + + /** + * Create default loggers and streams. + */ + protected void createDefaultLoggers(ConfigFile config) { + for (StreamID streamID : new StreamID[] { + Streams.SERVER_LOGGER, Streams.PLUGIN_LOGGER, + Streams.NOTIFY_INGAME, + Streams.DEFAULT_FILE, Streams.TRACE_FILE, + + }) { + createStringStream(streamID); + } + // TODO: Consult configuration and/or detect what options can or want to be used here. + CallContext bukkitLoggerContext = CallContext.PRIMARY_THREAD_TASK; // TODO: Config + individually. + LoggerID tempID; + + // Server logger. + tempID = registerStringLogger(new ContentLogger() { + + @Override + public void log(Level level, String content) { + Bukkit.getLogger().log(level, content); + } + + }, new LogOptions(Streams.SERVER_LOGGER.name, bukkitLoggerContext)); + + // Plugin logger. + tempID = registerStringLogger(plugin.getLogger(), new LogOptions(Streams.PLUGIN_LOGGER.name, bukkitLoggerContext)); + attachStringLogger(tempID, Streams.PLUGIN_LOGGER); + + // Ingame logger (assume not thread-safe at first). + tempID = registerStringLogger(new ContentLogger() { + + @Override + public void log(Level level, String content) { + // Ignore level for now. + NCPAPIProvider.getNoCheatPlusAPI().sendAdminNotifyMessage(content); + } + + }, new LogOptions(Streams.NOTIFY_INGAME.name, CallContext.PRIMARY_THREAD_DIRECT)); + attachStringLogger(tempID, Streams.NOTIFY_INGAME); + + File file; + // TODO: Allow absolute paths ? + // Default file logger. + file = new File(plugin.getDataFolder(), config.getString(ConfPaths.LOGGING_BACKEND_FILE_FILENAME)); + ContentLogger defaultFileLogger = new FileLoggerAdapter(file); + tempID = registerStringLogger(defaultFileLogger, new LogOptions(Streams.DEFAULT_FILE.name, CallContext.ASYNCHRONOUS_DIRECT)); + attachStringLogger(tempID, Streams.DEFAULT_FILE); + // Attach default file logger to init too, to log something, even if asynchronous, direct from any thread. + tempID = registerStringLogger(defaultFileLogger, new LogOptions(Streams.DEFAULT_FILE.name +".init", CallContext.ANY_THREAD_DIRECT)); + attachStringLogger(tempID, Streams.INIT); + + // Trace file logger. + // TODO: Create a real if "needed", dedicated file. + attachStringLogger(getLoggerID(Streams.DEFAULT_FILE.name), Streams.TRACE_FILE); // Direct to default file for now. + + } + + /** + * Not "official". TODO: Hide. + */ + public void onReload() { + // Hard clear and re-do loggers. Might result in loss of content. + clear(0L, true); // Can not afford to wait. + createDefaultLoggers(ConfigManager.getConfigFile()); + } + + /** + * Necessary logging to a primary thread task (TickTask). + */ + public void startTasks() { + // TODO: Schedule / hide. + ((BukkitLogNodeDispatcher) getLogNodeDispatcher()).startTasks(); + } + +} diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/details/BukkitLogNodeDispatcher.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/details/BukkitLogNodeDispatcher.java new file mode 100644 index 00000000..f041a1c6 --- /dev/null +++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/details/BukkitLogNodeDispatcher.java @@ -0,0 +1,69 @@ +package fr.neatmonster.nocheatplus.logging.details; + +import java.util.logging.Level; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; + +import fr.neatmonster.nocheatplus.components.TickListener; +import fr.neatmonster.nocheatplus.utilities.TickTask; + +public class BukkitLogNodeDispatcher extends AbstractLogNodeDispatcher { // TODO: Name. + + + /** + * Permanent TickListener for logging [TODO: on-demand scheduling, but thread-safe. With extra lock.] + */ + private final TickListener taskPrimary = new TickListener() { + + @Override + public void onTick(final int tick, final long timeLast) { + if (runLogsPrimary()) { + // TODO: Here or within runLogsPrimary, handle rescheduling. + } + } + + }; + + /** + * Needed for scheduling. + */ + private final Plugin plugin; + + public BukkitLogNodeDispatcher(Plugin plugin) { + this.plugin = plugin; + } + + public void startTasks() { + // TODO: This is a temporary solution. Needs on-demand scheduling [or a wrapper task]. + TickTask.addTickListener(taskPrimary); + } + + @Override + protected void logINIT(Level level, String message) { + // TODO: This is cheating ! + + } + + @Override + protected void scheduleAsynchronous() { + synchronized (queueAsynchronous) { + if (taskAsynchronousID == -1) { + // Deadlocking should not be possible. + taskAsynchronousID = Bukkit.getScheduler().runTaskAsynchronously(plugin, taskAsynchronous).getTaskId(); + // TODO: Re-check task id here. + } + } + } + + @Override + protected final boolean isPrimaryThread() { + return Bukkit.isPrimaryThread(); + } + + @Override + protected void cancelTask(int taskId) { + Bukkit.getScheduler().cancelTask(taskId); + } + +}