931 lines
25 KiB
C#
931 lines
25 KiB
C#
using System;
|
||
using System.IO;
|
||
using System.Diagnostics;
|
||
using System.Collections;
|
||
using System.Runtime.CompilerServices;
|
||
using System.Collections.Generic;
|
||
using System.Collections.Immutable;
|
||
using System.Reflection;
|
||
using System.Collections.Concurrent;
|
||
using System.Linq;
|
||
using System.Threading;
|
||
using lib.Net;
|
||
using System.Text;
|
||
//using System.Threading.Tasks;
|
||
|
||
#nullable enable
|
||
|
||
/*
|
||
|
||
T O D O :
|
||
x) Hook the C# prints from glue.
|
||
x) Fix
|
||
x) Refactor various logs in order to do automagic structured logging
|
||
ref: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/interpolated-string-handler
|
||
|
||
|
||
D O N E:
|
||
|
||
|
||
N O T D O I N G :
|
||
|
||
|
||
*/
|
||
|
||
public record struct Value<T>( T _val, string _exp = "" )
|
||
{
|
||
public static T Default = default!;
|
||
|
||
public static implicit operator T( Value<T> v )
|
||
{
|
||
return v._val;
|
||
}
|
||
|
||
static public Value<U> Get<U>( U v,
|
||
[CallerArgumentExpression("v")]
|
||
string dbgExp = ""
|
||
)
|
||
{
|
||
return new( v, dbgExp );
|
||
}
|
||
}
|
||
|
||
public struct SourceLoc
|
||
{
|
||
readonly string _reason = "";
|
||
readonly string _dbgName = "";
|
||
readonly string _dbgPath = "";
|
||
readonly string _dbgFile = "";
|
||
readonly int _dbgLine = -1;
|
||
|
||
public SourceLoc( string reason, string dbgName, string dbgPath, int dbgLine )
|
||
{
|
||
_reason = reason;
|
||
_dbgName = dbgName;
|
||
_dbgPath = dbgPath;
|
||
_dbgLine = dbgLine;
|
||
|
||
_dbgFile = log.whatFile( dbgPath );
|
||
}
|
||
|
||
public string ToLogString() => $"{_dbgFile}.{_dbgName}";
|
||
|
||
public string Log => ToLogString();
|
||
|
||
static public SourceLoc Record(
|
||
string reason = "",
|
||
[CallerMemberName] string dbgName = "",
|
||
[CallerFilePath] string dbgPath = "",
|
||
[CallerLineNumber] int dbgLine = 0
|
||
)
|
||
{
|
||
return new SourceLoc( reason, dbgName, dbgPath, dbgLine );
|
||
}
|
||
|
||
|
||
}
|
||
|
||
|
||
static public class log
|
||
{
|
||
|
||
//static
|
||
|
||
#region CLR Logging
|
||
|
||
|
||
static log()
|
||
{
|
||
log.high( $"Starting tracers" );
|
||
|
||
/*
|
||
{
|
||
var start = new ThreadStart( StartGCWatcher );
|
||
|
||
var thread = new Thread( start );
|
||
thread.Priority = ThreadPriority.BelowNormal;
|
||
thread.Name = $"Logging";
|
||
thread.Start();
|
||
}
|
||
// */
|
||
|
||
//log.info( $"Tracing.Program.CreateTracingSession" );
|
||
//var task = log.call( Tracing.Program.CreateTracingSession( false, true, 64 ) );
|
||
|
||
/*
|
||
log.info( $"Tracing.MemoryPipe" );
|
||
|
||
var memoryPipe = log.call( () => new Tracing.MemoryPipe() );
|
||
|
||
//var pipeTask = log.call( () => memoryPipe.StartAsync( true ) );
|
||
//*/
|
||
|
||
/*
|
||
{
|
||
var start = new ThreadStart( StartTracing );
|
||
|
||
var thread = new Thread( start );
|
||
thread.Priority = ThreadPriority.BelowNormal;
|
||
thread.Name = $"Logging";
|
||
thread.Start();
|
||
}
|
||
// */
|
||
|
||
|
||
}
|
||
|
||
static void StartGCWatcher()
|
||
{
|
||
// This wait is no longer needed with BlockingCollection, but kept for this component's logic
|
||
// It would be better to use a ManualResetEvent here if waiting is truly needed.
|
||
Thread.Sleep( 500 ); // Simple wait
|
||
var processId = Process.GetCurrentProcess().Id;
|
||
LogGC.PrintRuntimeGCEvents( processId );
|
||
}
|
||
|
||
static void StartTracing()
|
||
{
|
||
// See above comment
|
||
Thread.Sleep( 500 );
|
||
Tracing.TraceLogMonitor.Run();
|
||
}
|
||
|
||
#endregion // CLR Logging
|
||
|
||
static public Value<T> Value<T>( T val,
|
||
[CallerArgumentExpression("val")]
|
||
string dbgExp = ""
|
||
)
|
||
{
|
||
return new( val, dbgExp );
|
||
}
|
||
|
||
[Flags]
|
||
public enum LogType
|
||
{
|
||
Invalid = 0,
|
||
Trace = 1,
|
||
Debug = 2,
|
||
Info = 3,
|
||
High = 4,
|
||
Warn = 5,
|
||
Error = 6,
|
||
Fatal = 7,
|
||
|
||
Time = 64,
|
||
Raw = 65,
|
||
}
|
||
|
||
[Flags]
|
||
public enum Endpoints
|
||
{
|
||
None = 0,
|
||
|
||
File = 1 << 0,
|
||
Console = 1 << 1,
|
||
|
||
All = File | Console,
|
||
}
|
||
|
||
|
||
public struct LogEvent
|
||
{
|
||
public DateTime Time;
|
||
public LogType LogType;
|
||
public string Msg;
|
||
public string Path;
|
||
public int Line;
|
||
public string Member;
|
||
public string Exp;
|
||
|
||
public string Cat;
|
||
public object? Obj;
|
||
|
||
|
||
static ImmutableDictionary<int, string> s_shortname = ImmutableDictionary<int, string>.Empty;
|
||
|
||
public LogEvent( LogType logType, string msg, string dbgPath, int dbgLine, string dbgMethod, string cat, string exp, object? obj )
|
||
{
|
||
|
||
//Cache the automatic category names
|
||
// R A R E and S L O W and S A F E
|
||
if( string.IsNullOrEmpty( cat ) )
|
||
{
|
||
var pathHash = dbgPath.GetHashCode();
|
||
if( s_shortname.TryGetValue( pathHash, out var autoCat ) )
|
||
{
|
||
cat = autoCat;
|
||
}
|
||
else
|
||
{
|
||
var pathPieces = dbgPath.Split( '\\' );
|
||
|
||
if( pathPieces.Length < 2 )
|
||
{
|
||
pathPieces = dbgPath.Split( '/' );
|
||
}
|
||
|
||
var lastDir = pathPieces[pathPieces.Length - 2];
|
||
|
||
ImmutableInterlocked.AddOrUpdate( ref s_shortname, pathHash, lastDir, ( key, value ) => { return lastDir; } );
|
||
|
||
cat = lastDir;
|
||
}
|
||
}
|
||
|
||
Time = DateTime.Now;
|
||
LogType = logType;
|
||
Msg = msg;
|
||
Path = dbgPath;
|
||
Line = dbgLine;
|
||
Member = dbgMethod;
|
||
Cat = cat;
|
||
Exp = exp;
|
||
Obj = obj;
|
||
}
|
||
}
|
||
|
||
static LogEvent CreateLogEvent( LogType logType, string msg, string cat, object? obj, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", string exp = "" )
|
||
{
|
||
var logEvent = new LogEvent( logType, msg, dbgPath, dbgLine, dbgMethod, cat, exp, obj );
|
||
|
||
return logEvent;
|
||
}
|
||
|
||
public delegate void Log_delegate( LogEvent evt );
|
||
|
||
static ImmutableDictionary<string, Endpoints> s_logEPforCat = ImmutableDictionary<string, Endpoints>.Empty;
|
||
|
||
static public void endpointForCat( string cat, Endpoints ep )
|
||
{
|
||
ImmutableInterlocked.AddOrUpdate( ref s_logEPforCat, cat, ep, ( k, v ) => ep );
|
||
}
|
||
|
||
|
||
|
||
static public void shutdown()
|
||
{
|
||
string msg = "==============================================================================\nLogfile shutdown at " + DateTime.Now.ToString();
|
||
var evt = CreateLogEvent( LogType.Info, msg, "System", null );
|
||
s_events.Add( evt ); // Use Add instead of Enqueue
|
||
|
||
stop();
|
||
}
|
||
|
||
// MODIFIED: Replaced ConcurrentQueue with BlockingCollection for a more robust producer-consumer pattern.
|
||
static readonly BlockingCollection<LogEvent> s_events = new BlockingCollection<LogEvent>( new ConcurrentQueue<LogEvent>() );
|
||
|
||
static private Thread? s_thread;
|
||
|
||
static string s_cwd = Directory.GetCurrentDirectory();
|
||
static int s_cwdLength = s_cwd.Length;
|
||
static ImmutableDictionary<int, string> s_files = ImmutableDictionary<int, string>.Empty;
|
||
|
||
#region Util
|
||
static public SourceLoc loc( [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1 )
|
||
=> new SourceLoc( "", dbgName, dbgPath, dbgLine );
|
||
|
||
|
||
static public string whatFile( string path )
|
||
{
|
||
var file = "";
|
||
|
||
var pathHash = path.GetHashCode();
|
||
if( s_files.TryGetValue( pathHash, out var autoCat ) )
|
||
{
|
||
file = autoCat;
|
||
}
|
||
else
|
||
{
|
||
var pathPieces = path.Split( '\\' );
|
||
|
||
if( pathPieces.Length < 2 )
|
||
{
|
||
pathPieces = path.Split( '/' );
|
||
}
|
||
|
||
var lastDir = pathPieces[pathPieces.Length - 1];
|
||
|
||
ImmutableInterlocked.AddOrUpdate( ref s_files, pathHash, lastDir, ( key, value ) => { return lastDir; } );
|
||
|
||
file = lastDir;
|
||
}
|
||
|
||
return file;
|
||
}
|
||
|
||
static public string relativePath( string fullPath )
|
||
{
|
||
if( fullPath.Length < s_cwdLength )
|
||
return fullPath;
|
||
|
||
var rel = fullPath.Substring( s_cwdLength + 1 );
|
||
|
||
return rel;
|
||
}
|
||
|
||
static public string thisFilePath( [CallerFilePath] string dbgPath = "" )
|
||
{
|
||
return relativePath( dbgPath );
|
||
}
|
||
#endregion // Util
|
||
|
||
|
||
#region Forwards
|
||
static public T call<T>( Func<T> func, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "func" )] string dbgExp = "" )
|
||
{
|
||
log.info( $"Fn {dbgExp}", "", null, dbgPath, dbgLine, dbgName, dbgExp );
|
||
var val = func();
|
||
log.info( $"| Got {val}", "", null, dbgPath, dbgLine, dbgName, dbgExp );
|
||
return val;
|
||
}
|
||
|
||
/*
|
||
static public void info( string msg, string cat = "", object? obj = null,
|
||
[CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
|
||
*/
|
||
|
||
static public T var<T>( T val, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "val" )] string dbgExp = "" )
|
||
{
|
||
log.info( $"{dbgExp} = {val}", "", null, dbgPath, dbgLine, dbgName, dbgExp );
|
||
return val;
|
||
}
|
||
|
||
static public void call( Action func, [CallerMemberName] string dbgName = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "func" )] string dbgExp = "" )
|
||
{
|
||
log.info( $"{dbgExp}", "", null, dbgPath, dbgLine, dbgName, dbgExp );
|
||
func();
|
||
log.info( $"| Done", "", null, dbgPath, dbgLine, dbgName, dbgExp );
|
||
}
|
||
|
||
static public void fatal( string msg, string cat = "", object? obj = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
|
||
{
|
||
logBase( msg, LogType.Fatal, dbgPath, dbgLine, dbgMethod, cat, dbgExp, obj );
|
||
}
|
||
|
||
[StackTraceHidden]
|
||
static public void error( string msg, string cat = "", object? obj = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
|
||
{
|
||
logBase( $"{dbgMethod}: {msg}", LogType.Error, dbgPath, dbgLine, dbgMethod, cat, dbgExp, obj );
|
||
}
|
||
|
||
[StackTraceHidden]
|
||
static public void warn( string msg, string cat = "", object? obj = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
|
||
{
|
||
logBase( msg, LogType.Warn, dbgPath, dbgLine, dbgMethod, cat, dbgExp, obj );
|
||
}
|
||
|
||
static public void high( string msg, string cat = "", object? obj = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
|
||
{
|
||
logBase( msg, LogType.High, dbgPath, dbgLine, dbgMethod, cat, dbgExp, obj );
|
||
}
|
||
|
||
static public void info( string msg, string cat = "", object? obj = null,
|
||
[CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
|
||
{
|
||
logBase( msg, LogType.Info, dbgPath, dbgLine, dbgMethod, cat, dbgExp, obj );
|
||
}
|
||
|
||
static public void debug( string msg, string cat = "", object? obj = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
|
||
{
|
||
logBase( msg, LogType.Debug, dbgPath, dbgLine, dbgMethod, cat, dbgExp, obj );
|
||
}
|
||
|
||
static public void trace( string msg, string cat = "", object? obj = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" )
|
||
{
|
||
logBase( msg, LogType.Trace, dbgPath, dbgLine, dbgMethod, cat, dbgExp, obj );
|
||
}
|
||
|
||
|
||
|
||
|
||
#endregion
|
||
|
||
#region Helpers
|
||
static public void logProps( object obj, string header, LogType type = LogType.Debug, string cat = "", string prefix = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "obj" )] string dbgExpObj = "" )
|
||
{
|
||
var list = refl.GetAllProperties( obj.GetType() );
|
||
|
||
lock( s_lock )
|
||
{
|
||
var evt = new LogEvent( type, header, dbgPath, dbgLine, dbgMethod, cat, dbgExpObj, obj );
|
||
{
|
||
// Use Add instead of Enqueue
|
||
s_events.Add( evt );
|
||
|
||
foreach( var pi in list )
|
||
{
|
||
try
|
||
{
|
||
var v = pi.GetValue( obj );
|
||
|
||
logBase( $"{prefix}{pi.Name} = {v}", type, dbgPath, dbgLine, dbgMethod, dbgExpObj, cat );
|
||
}
|
||
catch( Exception ex )
|
||
{
|
||
logBase( $"Exception processing {pi.Name} {ex.Message}", LogType.Error, dbgPath, dbgLine, dbgMethod, cat, dbgExpObj, obj );
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
//This might seem a little odd, but the intent is that usually you wont need to set notExpectedValue.
|
||
static public void expected<T>( T value, string falseString, string trueString = "", T? notExpectedValue = default( T ) )
|
||
{
|
||
if( !object.Equals( value, notExpectedValue ) )
|
||
{
|
||
log.debug( $"Properly got {value}{trueString}" );
|
||
}
|
||
else
|
||
{
|
||
log.warn( $"Got {notExpectedValue} instead of {value}{falseString}" );
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
|
||
static object s_lock = new object();
|
||
static object s_lockTypeCallback = new object();
|
||
static ImmutableDictionary<LogType, Action<LogEvent>> s_callbacks = ImmutableDictionary<LogType, Action<LogEvent>>.Empty;
|
||
|
||
[StackTraceHidden]
|
||
static public void addDirectCallback( LogType logType, Action<LogEvent> callback )
|
||
{
|
||
//ImmutableInterlocked.Add( ref s_callbacks, logType, callback );
|
||
var added = II.TryAdd( ref s_callbacks, logType, callback );
|
||
if( !added )
|
||
{
|
||
log.warn( $"Failed to add callback for {logType}" );
|
||
}
|
||
}
|
||
|
||
|
||
static public LogEvent logCreateEvent( string msg, LogType type = LogType.Debug, string dbgPath = "", int dbgLine = -1, string dbgMethod = "", string cat = "unk", string exp = "", object? obj = null )
|
||
{
|
||
LogEvent evt = new LogEvent( type, msg, dbgPath, dbgLine, dbgMethod, cat, exp, obj );
|
||
return evt;
|
||
}
|
||
|
||
[StackTraceHidden]
|
||
static public void logBase( string msg, LogType type = LogType.Debug, string dbgPath = "", int dbgLine = -1, string dbgMethod = "", string cat = "unk", string exp = "", object? obj = null )
|
||
{
|
||
var evt = logCreateEvent( msg, type, dbgPath, dbgLine, dbgMethod, cat, exp );
|
||
|
||
s_callbacks.TryGetValue( type, out var callback );
|
||
|
||
if( callback != null )
|
||
{
|
||
lock( s_lockTypeCallback )
|
||
{
|
||
callback( evt );
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// MODIFIED: Use Add instead of Enqueue for the BlockingCollection.
|
||
s_events.Add( evt );
|
||
}
|
||
|
||
|
||
}
|
||
|
||
|
||
static Endpoints s_endpoints = Endpoints.Console;
|
||
static int s_catWidth = 14;
|
||
|
||
static public bool IsLogging => s_thread != null && s_thread.IsAlive;
|
||
|
||
static public void startup( string filename, Endpoints endpoints )
|
||
{
|
||
if( string.IsNullOrWhiteSpace( filename ) )
|
||
{
|
||
return;
|
||
}
|
||
|
||
lock( s_lock )
|
||
{
|
||
//We're already running if the thread is alive
|
||
if( s_thread != null && s_thread.IsAlive )
|
||
{
|
||
log.info( $"Already running, so this is a NOP" );
|
||
return;
|
||
}
|
||
|
||
s_startTime = DateTime.Now;
|
||
|
||
s_cwd = Directory.GetCurrentDirectory();
|
||
s_cwdLength = s_cwd.Length;
|
||
|
||
s_endpoints = endpoints;
|
||
|
||
var dir = Path.GetDirectoryName( filename );
|
||
|
||
if( dir?.Length > 0 )
|
||
{
|
||
Directory.CreateDirectory( dir );
|
||
}
|
||
|
||
|
||
s_stream = new FileStream( filename, FileMode.Append, FileAccess.Write );
|
||
s_writer = new StreamWriter( s_stream, Encoding.UTF8, 128, true );
|
||
|
||
//s_errorStream = new FileStream( filename + ".error", FileMode.Append, FileAccess.Write );
|
||
//s_errorWriter = new StreamWriter( s_errorStream );
|
||
|
||
{
|
||
var time = DateTime.Now;
|
||
// Header for this run
|
||
var blankLine = new LogEvent( LogType.Raw, $"", "", 0, "", "lib.time", "", null );
|
||
var beginLine = new LogEvent( LogType.Raw, $"Begin B E G I N ******************************************************************************************************************", "", 0, "", "lib.time", "", null );
|
||
var timeLine = new LogEvent( LogType.Raw, $"D A T E {time.Year}/{time.Month.ToString( "00" )}/{time.Day.ToString( "00" )} T I M E {time.Hour.ToString( "00" )}:{time.Minute.ToString( "00" )}:{time.Second.ToString( "00" )}.{time.Millisecond.ToString( "000" )}{time.Microsecond.ToString( "000" )}", "", 0, "", "lib.time", "", null );
|
||
|
||
// MODIFIED: All writes are now safely enqueued to be processed by the logger thread.
|
||
// This prevents the StreamWriter buffer corruption that caused null bytes.
|
||
s_events.Add( blankLine );
|
||
s_events.Add( blankLine );
|
||
s_events.Add( beginLine );
|
||
s_events.Add( blankLine );
|
||
s_events.Add( timeLine );
|
||
s_events.Add( blankLine );
|
||
}
|
||
|
||
LogEvent msgStartupBegin = new LogEvent( LogType.Info, $"startup BEGIN", "", 0, "", "log.startup", "", null );
|
||
s_events.Add( msgStartupBegin );
|
||
|
||
LogEvent msgFilename = new LogEvent( LogType.Info, $"Logging in {filename}", "", 0, "", "log.startup", "", null );
|
||
s_events.Add( msgFilename );
|
||
|
||
var optionsLine = new LogEvent( LogType.Info, $"Endpoints: {endpoints}", "", 0, "", "log.startup", "", null );
|
||
s_events.Add( optionsLine );
|
||
|
||
StartThread();
|
||
|
||
LogGC.RegisterObjectId( s_lock );
|
||
|
||
info( $"startup END", cat: "log.startup" );
|
||
}
|
||
|
||
}
|
||
|
||
private static void StartThread()
|
||
{
|
||
var start = new ThreadStart( threadLoop );
|
||
|
||
s_thread = new Thread( start );
|
||
s_thread.Priority = ThreadPriority.BelowNormal;
|
||
s_thread.Name = $"Logging";
|
||
s_thread.IsBackground = true; // Mark as background thread
|
||
s_thread.Start();
|
||
}
|
||
|
||
// REMOVED: ThreadState enum, StopThread, pauseThread, and unpauseThread methods.
|
||
// They are replaced by the simpler, safer lifecycle management of BlockingCollection.
|
||
|
||
// MODIFIED: Complete rewrite of the thread loop.
|
||
// This is now an efficient, blocking loop that consumes zero CPU while waiting for messages.
|
||
// It is also free of race conditions.
|
||
static void threadLoop()
|
||
{
|
||
Console.WriteLine( $"**********************************************************\n" );
|
||
Console.WriteLine( $"Logger thread started" );
|
||
|
||
try
|
||
{
|
||
// This loop will block when the collection is empty and will
|
||
// automatically finish when s_events.CompleteAdding() is called from another thread.
|
||
foreach( var evt in s_events.GetConsumingEnumerable() )
|
||
{
|
||
writeToAll( evt );
|
||
}
|
||
}
|
||
catch( Exception ex )
|
||
{
|
||
Console.WriteLine( $"[CRITICAL] Logger thread crashed: {ex}" );
|
||
}
|
||
|
||
Console.WriteLine( $"Logger thread has finished processing and is shutting down." );
|
||
}
|
||
|
||
|
||
public static void stop()
|
||
{
|
||
log.info( "Logger shutdown requested.", "log.system" );
|
||
|
||
// MODIFIED: This safely signals the consumer thread to finish.
|
||
s_events.CompleteAdding();
|
||
|
||
while( s_events.Count > 0 )
|
||
{
|
||
// Wait for the queue to be fully processed.
|
||
Thread.Sleep( 10 );
|
||
}
|
||
|
||
// Wait for the thread to finish processing all remaining messages in the queue.
|
||
s_thread?.Join( 1000 );
|
||
|
||
s_thread = null;
|
||
|
||
s_writer?.Close();
|
||
s_stream?.Close();
|
||
|
||
//s_errorWriter?.Close();
|
||
//s_errorStream?.Close();
|
||
}
|
||
|
||
static public void addDelegate( Log_delegate cb )
|
||
{
|
||
s_delegates.Add( cb );
|
||
}
|
||
|
||
public static char getSymbol( LogType type )
|
||
{
|
||
switch( type )
|
||
{
|
||
case LogType.Trace:
|
||
return ' ';
|
||
case LogType.Debug:
|
||
return ' ';
|
||
case LogType.Info:
|
||
return ' ';
|
||
case LogType.High:
|
||
return '+';
|
||
case LogType.Warn:
|
||
return '+';
|
||
case LogType.Error:
|
||
return '*';
|
||
case LogType.Fatal:
|
||
return '*';
|
||
default:
|
||
return '?';
|
||
}
|
||
}
|
||
|
||
private static void setConsoleColor( log.LogEvent evt )
|
||
{
|
||
switch( evt.LogType )
|
||
{
|
||
case log.LogType.Trace:
|
||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||
break;
|
||
case log.LogType.Debug:
|
||
Console.ForegroundColor = ConsoleColor.Gray;
|
||
break;
|
||
case log.LogType.Info:
|
||
Console.ForegroundColor = ConsoleColor.DarkGreen;
|
||
break;
|
||
case log.LogType.High:
|
||
Console.ForegroundColor = ConsoleColor.Cyan;
|
||
break;
|
||
case log.LogType.Warn:
|
||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||
break;
|
||
case log.LogType.Error:
|
||
Console.ForegroundColor = ConsoleColor.DarkRed;
|
||
Console.BackgroundColor = ConsoleColor.DarkGray;
|
||
break;
|
||
case log.LogType.Fatal:
|
||
Console.ForegroundColor = ConsoleColor.Red;
|
||
Console.BackgroundColor = ConsoleColor.DarkGray;
|
||
break;
|
||
|
||
case log.LogType.Invalid:
|
||
case log.LogType.Time:
|
||
case log.LogType.Raw:
|
||
Console.ForegroundColor = ConsoleColor.Red;
|
||
break;
|
||
|
||
}
|
||
}
|
||
|
||
static private DateTime s_startTime = DateTime.MinValue;
|
||
|
||
static private int s_lastDisplaySeconds = -1;
|
||
static private int s_lastSecond = -1;
|
||
static private string s_timeHeader = " ";
|
||
|
||
|
||
static public string headerPrint( LogEvent evt )
|
||
{
|
||
if( evt.LogType != LogType.Raw )
|
||
{
|
||
var span = evt.Time - s_startTime;
|
||
|
||
char sym = getSymbol( evt.LogType );
|
||
|
||
var truncatedCat = evt.Cat.Substring( 0, Math.Min( s_catWidth, evt.Cat.Length ) );
|
||
|
||
var timeHdr = $"{s_timeHeader}{( (int)span.TotalMinutes ).ToString( "000" )}:{span.Seconds.ToString( "D2" )}.{span.Milliseconds.ToString( "000" )}";
|
||
|
||
var msgHdr = string.Format( $"{timeHdr} | {{0,-{s_catWidth}}}{{1}}| ", truncatedCat, sym );
|
||
|
||
return msgHdr;
|
||
}
|
||
else
|
||
{
|
||
return "";
|
||
}
|
||
|
||
}
|
||
|
||
static public string headerFile( LogEvent evt )
|
||
{
|
||
if( evt.LogType != LogType.Raw )
|
||
{
|
||
try
|
||
{
|
||
var span = evt.Time - s_startTime;
|
||
|
||
char sym = getSymbol( evt.LogType );
|
||
|
||
var truncatedCat = evt.Cat.Substring( 0, Math.Min( s_catWidth, evt.Cat.Length ) );
|
||
|
||
if( string.IsNullOrWhiteSpace( truncatedCat ) )
|
||
truncatedCat = $"B R O K E N truncatedCat";
|
||
|
||
//Dont really need the year-month-day frankly.
|
||
//var timeHdr = $"{evt.Time.Year}-{evt.Time.Month.ToString("00")}-{evt.Time.Day.ToString("00")} {evt.Time.Hour.ToString("00")}:{evt.Time.Minute.ToString("00")}:{evt.Time.Second.ToString("00")}.{evt.Time.Millisecond.ToString("000")}{evt.Time.Microsecond.ToString("000")}";
|
||
var timeHdr = $"{evt.Time.Hour.ToString( "00" )}:{evt.Time.Minute.ToString( "00" )}:{evt.Time.Second.ToString( "00" )}.{evt.Time.Millisecond.ToString( "000" )}{evt.Time.Microsecond.ToString( "000" )}";
|
||
|
||
if( string.IsNullOrWhiteSpace( timeHdr ) )
|
||
timeHdr = $"B R O K E N timeHdr";
|
||
|
||
var msgHdr = string.Format( $"{timeHdr} | {{0,-{s_catWidth}}}{{1}}| ", truncatedCat, sym );
|
||
|
||
if( string.IsNullOrWhiteSpace( msgHdr ) )
|
||
msgHdr = $"B R O K E N msgHdr";
|
||
|
||
|
||
return msgHdr;
|
||
}
|
||
catch( Exception ex )
|
||
{
|
||
return $"Ex {ex.Message} processing msg";
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Empty for RAW
|
||
return "";
|
||
}
|
||
}
|
||
|
||
|
||
static public string msgPrint( LogEvent evt )
|
||
{
|
||
var msgHdr = headerPrint( evt );
|
||
|
||
string finalLine = $"{msgHdr}{evt.Msg}";
|
||
|
||
return finalLine;
|
||
}
|
||
|
||
static public string msgFile( LogEvent evt )
|
||
{
|
||
var msgHdr = headerFile( evt );
|
||
|
||
var msg = evt.Msg;
|
||
|
||
if( ( string.IsNullOrWhiteSpace( msg ) && evt.LogType != LogType.Raw ) || msg.Contains( (char)0 ) )
|
||
{
|
||
msg = "B R O K E N msg";
|
||
}
|
||
|
||
string finalLine = $"{msgHdr}{msg}";
|
||
|
||
return finalLine;
|
||
}
|
||
|
||
static private void writeSpecialEvent( LogEvent evt )
|
||
{
|
||
if( ( s_endpoints & Endpoints.Console ) == Endpoints.Console )
|
||
{
|
||
//setConsoleColor( evt );
|
||
//Console.WriteLine( $"{evt.Time.ToShortTimeString()}" );
|
||
//Console.ResetColor();
|
||
}
|
||
|
||
foreach( Log_delegate cb in s_delegates )
|
||
{
|
||
cb( evt );
|
||
}
|
||
}
|
||
|
||
static private void writeToAll( LogEvent evt )
|
||
{
|
||
try
|
||
{
|
||
#region Time
|
||
var span = evt.Time - s_startTime;
|
||
|
||
var curSeconds = (int)span.TotalSeconds;
|
||
|
||
if( curSeconds - s_lastDisplaySeconds > 10 )
|
||
{
|
||
s_lastDisplaySeconds = curSeconds;
|
||
|
||
var minuteEvt = new LogEvent( LogType.Raw, $"T I M E ==> {evt.Time.Hour.ToString( "00" )}:{evt.Time.Minute.ToString( "00" )}:{evt.Time.Second.ToString( "00" )}.{evt.Time.Millisecond.ToString( "000" )} : {evt.Time.ToShortDateString()}", "", 0, "", "lib.time", "", null );
|
||
minuteEvt.Time = evt.Time;
|
||
writeSpecialEvent( minuteEvt );
|
||
}
|
||
|
||
if( evt.LogType != LogType.Raw )
|
||
{
|
||
var curSecond = (int)span.TotalSeconds;
|
||
|
||
if( s_lastSecond == curSecond )
|
||
{
|
||
s_timeHeader = " ";
|
||
}
|
||
else
|
||
{
|
||
s_timeHeader = "*";
|
||
s_lastSecond = curSecond;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
|
||
#region Write
|
||
|
||
var finalEndpoints = s_logEPforCat.TryGetValue( evt.Cat, out var ep ) ? ep & s_endpoints : s_endpoints;
|
||
|
||
//Console.WriteLine( $"Final endpoints for {evt.Cat} = {finalEndpoints}" );
|
||
|
||
if( ( finalEndpoints & Endpoints.File ) == Endpoints.File )
|
||
{
|
||
var line = msgFile( evt );
|
||
if( !string.IsNullOrWhiteSpace( line ) )
|
||
{
|
||
var trimmedLine = line.Trim( (char)0 );
|
||
s_writer?.WriteLine( trimmedLine );
|
||
}
|
||
}
|
||
|
||
if( ( finalEndpoints & Endpoints.Console ) == Endpoints.Console )
|
||
{
|
||
var line = msgPrint( evt );
|
||
if( !string.IsNullOrWhiteSpace( line ) )
|
||
{
|
||
setConsoleColor( evt );
|
||
Console.WriteLine( line );
|
||
Console.ResetColor();
|
||
}
|
||
}
|
||
|
||
|
||
//Debug.WriteLine( finalLine );
|
||
|
||
s_writer?.Flush();
|
||
|
||
foreach( Log_delegate cb in s_delegates )
|
||
{
|
||
cb( evt );
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
catch( Exception ex )
|
||
{
|
||
#region Catch
|
||
Console.WriteLine( "EXCEPTION DURING LOGGING" );
|
||
Console.WriteLine( "EXCEPTION DURING LOGGING" );
|
||
Console.WriteLine( "EXCEPTION DURING LOGGING" );
|
||
Console.WriteLine( "EXCEPTION DURING LOGGING" );
|
||
Console.WriteLine( "EXCEPTION DURING LOGGING" );
|
||
Console.WriteLine( $"Exception {ex}" );
|
||
|
||
Debug.WriteLine( "EXCEPTION DURING LOGGING" );
|
||
Debug.WriteLine( "EXCEPTION DURING LOGGING" );
|
||
Debug.WriteLine( "EXCEPTION DURING LOGGING" );
|
||
Debug.WriteLine( "EXCEPTION DURING LOGGING" );
|
||
Debug.WriteLine( "EXCEPTION DURING LOGGING" );
|
||
Debug.WriteLine( $"Exception {ex}" );
|
||
#endregion
|
||
}
|
||
|
||
}
|
||
|
||
public static void WriteToConsole( LogEvent evt )
|
||
{
|
||
char sym = getSymbol( evt.LogType );
|
||
|
||
var truncatedCat = evt.Cat.Substring( 0, Math.Min( 8, evt.Cat.Length ) );
|
||
|
||
string finalLine = string.Format( "{0,-8}{1}| {2}", truncatedCat, sym, evt.Msg );
|
||
|
||
Console.WriteLine( finalLine );
|
||
}
|
||
|
||
|
||
private static Stream? s_stream;
|
||
private static StreamWriter? s_writer;
|
||
|
||
//private static Stream? s_errorStream;
|
||
//private static StreamWriter? s_errorWriter;
|
||
|
||
private static ArrayList s_delegates = new ArrayList();
|
||
|
||
} |