mirror of
https://github.com/NoCheatPlus/NoCheatPlus.git
synced 2025-02-15 11:11:35 +01:00
[PLACEBO] Raw sketch of the upcoming logging framework, doing nothing.
This commit is contained in:
parent
f49c64e89d
commit
2f4d689722
@ -0,0 +1,25 @@
|
||||
package fr.neatmonster.nocheatplus.logging;
|
||||
|
||||
/**
|
||||
* Restrictions for registration:
|
||||
* <li>Unique instances.</li>
|
||||
* <li>Unique names.</li>
|
||||
* <li>Custom registrations can not start with the default prefix (see AbstractLogManager).</li>
|
||||
*
|
||||
* @author dev1mc
|
||||
*
|
||||
*/
|
||||
public class LoggerID {
|
||||
|
||||
public final String name;
|
||||
|
||||
public LoggerID(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "<LoggerID " + name + ">";
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package fr.neatmonster.nocheatplus.logging;
|
||||
|
||||
/**
|
||||
* Restrictions for registration:
|
||||
* <li>Unique instances.</li>
|
||||
* <li>Unique names.</li>
|
||||
* <li>Custom registrations can not start with the default prefix (see AbstractLogManager).</li>
|
||||
*
|
||||
* @author dev1mc
|
||||
*
|
||||
*/
|
||||
public class StreamID {
|
||||
|
||||
public final String name;
|
||||
|
||||
public StreamID(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "<StreamID " + name + ">";
|
||||
}
|
||||
|
||||
}
|
@ -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 ?
|
||||
|
||||
}
|
@ -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<StreamID, ContentStream<String>> idStreamMap = new IdentityHashMap<StreamID, ContentStream<String>>();
|
||||
|
||||
/**
|
||||
* Map name to Stream. Copy on write with registryLock.
|
||||
*/
|
||||
private Map<String, ContentStream<String>> nameStreamMap = new HashMap<String, ContentStream<String>>();
|
||||
|
||||
/**
|
||||
* Lower-case name to StreamID.
|
||||
*/
|
||||
private Map<String, StreamID> nameStreamIDMap = new HashMap<String, StreamID>();
|
||||
|
||||
/**
|
||||
* LogNode registry by LoggerID. Copy on write with registryLock.
|
||||
*/
|
||||
private Map<LoggerID, LogNode<String>> idNodeMap = new IdentityHashMap<LoggerID, LogNode<String>>();
|
||||
|
||||
/**
|
||||
* LogNode registry by lower-case name. Copy on write with registryLock.
|
||||
*/
|
||||
private Map<String, LogNode<String>> nameNodeMap = new HashMap<String, LogNode<String>>();
|
||||
|
||||
/**
|
||||
* Lower-case name to LoggerID.
|
||||
*/
|
||||
private Map<String, LoggerID> nameLoggerIDMap = new HashMap<String, LoggerID>();
|
||||
|
||||
/** 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<String> initLogger = new ContentLogger<String>() {
|
||||
@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<String> 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<String> createStringStream(final StreamID streamID) {
|
||||
ContentStream<String> stream;
|
||||
synchronized (registryCOWLock) {
|
||||
testRegisterStream(streamID);
|
||||
Map<StreamID, ContentStream<String>> idStreamMap = new IdentityHashMap<StreamID, ContentStream<String>>(this.idStreamMap);
|
||||
Map<String, ContentStream<String>> nameStreamMap = new HashMap<String, ContentStream<String>>(this.nameStreamMap);
|
||||
Map<String, StreamID> nameStreamIDMap = new HashMap<String, StreamID>(this.nameStreamIDMap);
|
||||
stream = new DefaultContentStream<String>(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<String> 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<String> registerStringLogger(final LoggerID loggerID, final ContentLogger<String> logger, final LogOptions options) {
|
||||
LogNode<String> node;
|
||||
synchronized (registryCOWLock) {
|
||||
testRegisterLogger(loggerID);
|
||||
Map<LoggerID, LogNode<String>> idNodeMap = new IdentityHashMap<LoggerID, LogNode<String>>(this.idNodeMap);
|
||||
Map<String, LogNode<String>> nameNodeMap = new HashMap<String, LogNode<String>>(this.nameNodeMap);
|
||||
Map<String, LoggerID> nameLoggerIDMap = new HashMap<String, LoggerID>(this.nameLoggerIDMap);
|
||||
node = new LogNode<String>(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<String> registerStringLogger(LoggerID loggerID, Logger logger, LogOptions options) {
|
||||
LogNode<String> 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<String> registerStringLogger(LoggerID loggerID, File file, LogOptions options) {
|
||||
LogNode<String> 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<String> 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<String> stream : idStreamMap.values()) {
|
||||
stream.clear();
|
||||
}
|
||||
// Flush queues.
|
||||
dispatcher.flush(msWaitFlush);
|
||||
// Close/flush/shutdown string loggers, where possible, remove all from registry.
|
||||
for (final LogNode<String> node : idNodeMap.values()) {
|
||||
if (node.logger instanceof FileLoggerAdapter) {
|
||||
FileLoggerAdapter logger = (FileLoggerAdapter) node.logger;
|
||||
logger.flush();
|
||||
logger.detachLogger();
|
||||
}
|
||||
}
|
||||
idNodeMap = new IdentityHashMap<LoggerID, LogNode<String>>();
|
||||
nameNodeMap = new HashMap<String, LogNode<String>>();
|
||||
nameLoggerIDMap = new HashMap<String, LoggerID>();
|
||||
// Remove string streams.
|
||||
idStreamMap = new IdentityHashMap<StreamID, ContentStream<String>>();
|
||||
nameStreamMap = new HashMap<String, ContentStream<String>>();
|
||||
nameStreamIDMap = new HashMap<String, StreamID>();
|
||||
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.
|
||||
}
|
||||
|
||||
}
|
@ -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<LogRecord<?>> queuePrimary = new QueueRORA<LogRecord<?>>();
|
||||
protected final QueueRORA<LogRecord<?>> queueAsynchronous = new QueueRORA<LogRecord<?>>();
|
||||
|
||||
/** 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<String> initLogger = null;
|
||||
|
||||
public <C> void dispatch(LogNode<C> 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<LogRecord<?>> queue) {
|
||||
// TODO: Consider allowYield + msYield, calling yield after 5 ms if async.
|
||||
final List<LogRecord<?>> 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 <C> boolean isWithinContext(LogNode<C> 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 <C> void scheduleLog(LogNode<C> node, Level level, C content) {
|
||||
final LogRecord<C> record = new LogRecord<C>(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<LogRecord<?>> 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<String> 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);
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package fr.neatmonster.nocheatplus.logging.details;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Minimal interface for logging content to.
|
||||
* @author dev1mc
|
||||
*
|
||||
* @param <C>
|
||||
*/
|
||||
public interface ContentLogger <C> {
|
||||
|
||||
// TODO: Not sure about generics.
|
||||
public void log(Level level, C content);
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package fr.neatmonster.nocheatplus.logging.details;
|
||||
|
||||
|
||||
/**
|
||||
* Contracts:
|
||||
* <li>Intended usage: register=seldom, log=often</li>
|
||||
* <li>Asynchronous access must be possible and fast, without locks (rather copy on write). </li>
|
||||
* @author dev1mc
|
||||
*
|
||||
* @param <C>
|
||||
*/
|
||||
public interface ContentStream <C> extends ContentLogger<C> {
|
||||
|
||||
// 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<C> node);
|
||||
|
||||
// addAdapter(ContentAdapter<C, ?> adapter) ? ID etc., attach to another stream.
|
||||
|
||||
// Removal and look up methods.
|
||||
|
||||
/**
|
||||
* Remove all attached loggers and other.
|
||||
*/
|
||||
public void clear();
|
||||
|
||||
}
|
@ -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 <C>
|
||||
*/
|
||||
public class DefaultContentStream<C> implements ContentStream<C> {
|
||||
|
||||
private ArrayList<LogNode<C>> nodes = new ArrayList<LogNode<C>>();
|
||||
|
||||
private final LogNodeDispatcher dispatcher;
|
||||
|
||||
public DefaultContentStream(LogNodeDispatcher dispatcher) {
|
||||
this.dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(final Level level, final C content) {
|
||||
final ArrayList<LogNode<C>> nodes = this.nodes;
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
dispatcher.dispatch(nodes.get(i), level, content);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addNode(LogNode<C> 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<LogNode<C>> nodes = new ArrayList<LogNode<C>>(this.nodes);
|
||||
nodes.add(node);
|
||||
this.nodes = nodes;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
synchronized (nodes) {
|
||||
nodes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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<String> {
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package fr.neatmonster.nocheatplus.logging.details;
|
||||
|
||||
import fr.neatmonster.nocheatplus.logging.LoggerID;
|
||||
|
||||
|
||||
public class LogNode<C> {
|
||||
|
||||
public final LoggerID loggerID;
|
||||
public final ContentLogger<C> logger;
|
||||
public final LogOptions options;
|
||||
|
||||
public LogNode(LoggerID loggerID, ContentLogger<C> logger, LogOptions options) {
|
||||
this.loggerID = loggerID;
|
||||
this.logger = logger;
|
||||
this.options = new LogOptions(options);
|
||||
}
|
||||
|
||||
}
|
@ -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 <C> void dispatch(LogNode<C> 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<String> 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);
|
||||
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package fr.neatmonster.nocheatplus.logging.details;
|
||||
|
||||
public class LogOptions {
|
||||
|
||||
/**
|
||||
* Describe the context in which the log method may be used.<br>
|
||||
* Policies:
|
||||
* <li>..._DIRECT: Will log directly if within context, otherwise schedule to a task.</li>
|
||||
* <li>..._TASK: Always schedule to a task.</li>
|
||||
*
|
||||
* @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.
|
||||
}
|
||||
|
||||
}
|
@ -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<C> implements Runnable {
|
||||
|
||||
private final LogNode<C> node;
|
||||
private final Level level;
|
||||
private final C content;
|
||||
|
||||
public LogRecord(LogNode<C> 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);
|
||||
}
|
||||
|
||||
}
|
@ -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<String> {
|
||||
|
||||
protected final Logger logger;
|
||||
|
||||
public LoggerAdapter(Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String content) {
|
||||
logger.log(level, content);
|
||||
}
|
||||
|
||||
}
|
@ -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<E> {
|
||||
|
||||
private LinkedList<E> elements = new LinkedList<E>();
|
||||
|
||||
/**
|
||||
* 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<E> removeAll() {
|
||||
final List<E> result;
|
||||
synchronized (this) {
|
||||
result = elements;
|
||||
elements = new LinkedList<E>();
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
@ -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).<br>
|
||||
* 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<String>() {
|
||||
|
||||
@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<String>() {
|
||||
|
||||
@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<String>() {
|
||||
|
||||
@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<String> 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();
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user