diff --git a/AGENTS.md b/AGENTS.md index dddec09..e50c55e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,58 +1,69 @@ # SharpLib and general coding guidelines +## 0. PERFORMANCE. Title it 0. AXIOM: HIGH-PERFORMANCE / ZERO-ALLOCATION + +* **Zero Allocation Default:** Assume all code runs in a 60hz+ game loop or high-throughput pipeline unless marked `[ColdPath]`. +* **NO LINQ:** `using System.Linq;` is strictly forbidden in core logic. Use `for` loops, arrays, or `Span`/`ReadOnlySpan`. +* **NO Hidden Closures:** Do not use lambdas or delegates that capture local variables. If a lambda is necessary, make it `static` and pass state explicitly. +* **Struct Semantics:** + * Pass readonly structs using `in` or `ref readonly`. + * Do not pass `struct` to interfaces unless constrained by generics (`where T : struct`), to prevent boxing. +* **String Allocation:** NO string interpolation (`$""`) on the hot path. Use static strings, `string.Create`, or format into pooled buffers. +* **Async/Task Overhead:** Default to synchronous execution for immediate state changes. If async is mandatory on a hot path, use `ValueTask`, never `Task`. +* **Collections:** No `List.Add()` inside tight loops without pre-sizing the capacity. Prefer array pooling (`ArrayPool.Shared`). + ## 1. CORE PHILOSOPHY -* **Leaky Abstractions:** All abstractions are always leaky. Do not hide complexity. Expose failure modes, costs, and "why" parameters. -* **Visual Cost:** "Expensive things must look expensive." No hidden RPCs. Use explicit Message structs and `Send` methods. -* **Fast & Introspectable:** Code must be highly performant (hot-path aware) but deeply debuggable (`log`, `DebugOld`, `reason` strings). +* **Leaky Abstractions:** All abstractions are always leaky. Do not hide complexity. Expose failure modes, costs, and "reason" parameters. +* **Visual Cost:** "Expensive things must look expensive." No hidden RPCs. Use explicit Message structs and `Send` methods instead of Rpcs +* **Fast & Introspectable:** Code must be highly performant (hot-path aware) but deeply debuggable (`log`, `DebugOld`, `reason` strings). These are there to make debugging + from console or log file possible ## 2. NAMING & SEMANTICS -* **Leaf Libraries:** Core libs (`io`, `imm`, `net`, `log`) are 2-5 chars. **NEVER** use `using` for them. Always type the prefix (e.g., `imm.Process`). -* **No Redundancy:** +* **Leaf Libraries:** Core libs (`io`, `imm`, `net`, `log`) are 2-5 chars. **NEVER** include using {namespace}; for core libs or any short namespace +* **No Redundancy:In Names** * **No "Get":** `Player()` not `GetPlayer()`. * **No Echo:** `Send(msg)` not `SendMsg(msg)`. * **No "I" Prefix:** Interfaces are `Renderer`, not `IRenderer`. -* **Architecture:** Slow/Pausable Realtime. Entity Component System (Pub/Sub). ## 3. LOGGING (`log` static class) * **Constraint:** **NO** `Console.WriteLine`, `Debug.WriteLine`, or `ILogger`. * **Mechanism:** The logging system is very fast. It only queues the log on the main thread. It collects the caller debug info, and uses the directory name of the file for a default category (this can be overridden) -* **Functional Tracing:** - * `var x = log.info( $"Got {log.var(Calc())} from the calculation" );` // This both logs the info and prints what happened * **Standard:** `log.info`, `log.debug`, `log.warn`, `log.error`. * **Finer grained:** `log.trace`, `log.high` * Use log.exception( ex, $"[what was trying to be done]" ); -* **Introspection:** `log.logProps(obj, "Header")` and `log.exception(ex, "Context")`. -* put important info as far to the left as possible, even at the cost of poor wording +* **Introspection:** `log.props(obj, "Header")` and `log.exception(ex, "Context")`. +* **Information** put important info as far to the left as possible, even at the cost of poor wording ## 4. IMMUTABLE STATE (`io`, `imm`) * **Rule:** Objects inheriting `Versioned`, `Recorded`, `Timed` are **IMMUTABLE**. -* **Change Pipeline:** MUST use `Process`. +* **Change Pipeline:** MUST use `imm.Process`. * **Ref Helper (Preferred):** `imm.Process(ref _state, s => s with { X = 1 }, "Reason");` * **Instance:** `_state = _state.Process(s => s with { X = 1 }, "Reason");` -* **The "Hole Punch":** Always propagate a `string reason` parameter in your methods to feed the `Process` log. -* **History:** `obj.DebugOld` is for **DEBUG ONLY**. Do not base game logic on it. +* **The "Hole Punch":** Always propagate a `string reason` parameter in your methods to feed the `imm.Process` log. +* **History:** `obj.DebugOld` is for **DEBUG ONLY**. Do not base program logic on it. ## 5. Visual Cost -* Unless something is a function call, it should look like a function call. For example RPCs are bad, sending a message is good. +* Network/IO/Expensive operations MUST NOT masquerade as local function calls. Require explicit structural allocation. * **No RPCs:** Do not make network calls look like functions. * **Pattern:** Construct Struct -> Send. * *Bad:* `proxy.Move(x,y)` * *Good:* `Net.Send(new MoveMsg(x,y))` -## FEW-SHOT EXAMPLES (Strictly Imitate) +## 6. FEW-SHOT EXAMPLES (Strictly Imitate) + ```csharp -// 1. NAMING & LOGGING +// ** NAMING & LOGGING // Explicit 'log' usage, no 'Get', no 'Console' public void Init() { - log.startup( "log/current_project.log" ); + log.startup( "log/current_project.log", log.Endpoints.All ); var waitTime = 1.0f; @@ -60,19 +71,20 @@ ## FEW-SHOT EXAMPLES (Strictly Imitate) try { - obj.SomeOperation(); + obj?.SomeOperation(); } catch( Exception ex ) { - log.exception( ex, $"" ); + //Logs the exception name, message, the reason, the stack + log.exception( ex, $"SomeOperation failed" ); } - // 2. IMMUTABLE PROCESS + // ** IMMUTABLE PROCESS // Reason "Init" passed down. Ref pattern used. imm.Process(ref _state, s => s with { Ready = true }, "Init"); } -// 3. LOGIC FLOW +// ** LOGIC FLOW public void Update(float dt) { // 'Player()' not 'GetPlayer()' @@ -85,9 +97,21 @@ ## FEW-SHOT EXAMPLES (Strictly Imitate) } } -// 5. INTROSPECTION API -// "Punching a hole" with the reason parameter -public void Equip(Item i, string reason = "Equip") + +// ** "Punching a hole" with the reason parameter +public void Equip(Item i, string reason /*No default so a reason will be passed in*/ ) { imm.Process( ref inv, s => s.Add(i), reason); } + +. . . + +// Somewhere else in project +public void PlayerSwappedWeapons( Player pl ) +{ + Equip( _item, $"Player {pl.Name} swapped weapons (PlayerSwappedWeapons)" ); +} + +``` + + diff --git a/logging/Log.cs b/logging/Log.cs deleted file mode 100644 index 9612641..0000000 --- a/logging/Log.cs +++ /dev/null @@ -1,1246 +0,0 @@ -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 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 : - - -*/ - -#region Helpers -public record struct Value( T _val, string _exp = "" ) -{ - public static T Default = default!; - - public static implicit operator T( Value v ) - { - return v._val; - } - - static public Value Get( U v, - [CallerArgumentExpression("v")] - string dbgExp = "" -) - { - return new( v, dbgExp ); - } -} - -public struct SourceLoc -{ - readonly string _reason = ""; - readonly string _dbgMethod = ""; - readonly string _dbgPath = ""; - readonly string _dbgFile = ""; - readonly int _dbgLine = -1; - - public SourceLoc( string reason, string dbgMethod, string dbgPath, int dbgLine ) - { - _reason = reason; - _dbgMethod = dbgMethod; - _dbgPath = dbgPath; - _dbgLine = dbgLine; - - _dbgFile = log.whatFile( dbgPath ); - } - - public string ToLogString() => $"{_dbgFile}.{_dbgMethod}"; - - public string Log => ToLogString(); - - public string File => _dbgFile; - public string Method => _dbgMethod; - - static public SourceLoc Record( - string reason = "", - [CallerMemberName] string dbgMethod = "", - [CallerFilePath] string dbgPath = "", - [CallerLineNumber] int dbgLine = 0 - ) - { - return new SourceLoc( reason, dbgMethod, dbgPath, dbgLine ); - } - - -} -#endregion // Helpers - -static public class log -{ - #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 - - [Flags] - public enum Levels - { - 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, - } - - - #region LogEvent - - public struct LogEvent - { - public DateTime Time; - public Levels Levels; - public string Msg; - public string Path; - public int Line; - public string Member; - public string Exp; - - public string Cat; - public SourceLoc? Loc; - - - static ImmutableDictionary s_shortname = ImmutableDictionary.Empty; - - public LogEvent( Levels level, string msg, string dbgPath, int dbgLine, string dbgMethod, string cat, string exp, SourceLoc? loc ) - { - - //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; - Levels = level; - Msg = msg; - Path = dbgPath; - Line = dbgLine; - Member = dbgMethod; - Cat = cat; - Exp = exp; - Loc = loc; - } - } - - static LogEvent CreateLogEvent( Levels level, string msg, string cat, SourceLoc? loc, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", string exp = "" ) - { - var logEvent = new LogEvent( level, msg, dbgPath, dbgLine, dbgMethod, cat, exp, loc ); - - return logEvent; - } - - public delegate void Log_delegate( LogEvent evt ); - - static ImmutableDictionary s_logEPforCat = ImmutableDictionary.Empty; - - static public void endpointForCat( string cat, Endpoints ep ) - { - ImmutableInterlocked.AddOrUpdate( ref s_logEPforCat, cat, ep, ( k, v ) => ep ); - } - - #endregion // LogEvent - - static public void shutdown() - { - string msg = "==============================================================================\nLogfile shutdown at " + DateTime.Now.ToString(); - var evt = CreateLogEvent( Levels.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 s_events = new BlockingCollection( new ConcurrentQueue() ); - - static private Thread? s_thread; - - static string s_cwd = Directory.GetCurrentDirectory(); - static int s_cwdLength = s_cwd.Length; - static ImmutableDictionary s_files = ImmutableDictionary.Empty; - - #region Util - static public SourceLoc loc( [CallerMemberName] string dbgMethod = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1 ) - => new SourceLoc( "", dbgMethod, dbgPath, dbgLine ); - - - static public string whatFile( string path, bool incExtension = false ) - { - 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 lastPathPiece = pathPieces[pathPieces.Length - 1]; - - ImmutableInterlocked.AddOrUpdate( ref s_files, pathHash, lastPathPiece, ( key, value ) => { return lastPathPiece; } ); - - if( !incExtension ) - { - var periodIndex = lastPathPiece.IndexOf( '.' ); - if( periodIndex > 0 ) - { - lastPathPiece = lastPathPiece.Substring( 0, periodIndex ); - } - } - - file = lastPathPiece; - } - - 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( Func func, [CallerMemberName] string dbgMethod = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "func" )] string dbgExp = "" ) - { - log.info( $"Fn {dbgExp}", "", null, dbgPath, dbgLine, dbgMethod, dbgExp ); - var val = func(); - log.info( $"| Got {val}", "", null, dbgPath, dbgLine, dbgMethod, dbgExp ); - return val; - } - - /* - static public void info( string msg, string cat = "",SourceLoc? loc = null, - [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) - */ - - static public T var( T val, [CallerMemberName] string dbgMethod = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "val" )] string dbgExp = "" ) - { - log.info( $"{dbgExp} = {val}", "", null, dbgPath, dbgLine, dbgMethod, dbgExp ); - return val; - } - - static public void operations( Action val, [CallerMemberName] string dbgMethod = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "val" )] string dbgExp = "" ) - { - log.info( $"{dbgExp} = {val}", "", null, dbgPath, dbgLine, dbgMethod, dbgExp ); - - val(); - } - - static public T operations( Func val, [CallerMemberName] string dbgMethod = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "val" )] string dbgExp = "" ) - { - log.info( $"{dbgExp} = {val}", "", null, dbgPath, dbgLine, dbgMethod, dbgExp ); - - var v = val(); - - return v; - } - - static public void operations( string prefix, Action val, [CallerMemberName] string dbgMethod = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "val" )] string dbgExp = "" ) - { - log.info( $"{prefix} {dbgExp} = {val}", "", null, dbgPath, dbgLine, dbgMethod, dbgExp ); - - val(); - } - - static public T operations( string prefix, Func val, [CallerMemberName] string dbgMethod = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "val" )] string dbgExp = "" ) - { - log.info( $"{prefix} {dbgExp} = {val}", "", null, dbgPath, dbgLine, dbgMethod, dbgExp ); - - var v = val(); - - return v; - } - - [StackTraceHidden] - static public void call( Action func, [CallerMemberName] string dbgMethod = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerArgumentExpression( "func" )] string dbgExp = "" ) - { - log.info( $"{dbgExp}", "", null, dbgPath, dbgLine, dbgMethod, dbgExp ); - func(); - log.info( $"| Done", "", null, dbgPath, dbgLine, dbgMethod, dbgExp ); - } - - [StackTraceHidden] - static public void info( string msg, string cat = "", SourceLoc? loc = null, - [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) - { - logBase( msg, Levels.Info, dbgPath, dbgLine, dbgMethod, cat, dbgExp, loc ); - } - - [StackTraceHidden] - static public void debug( string msg, string cat = "", SourceLoc? loc = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) - { - logBase( msg, Levels.Debug, dbgPath, dbgLine, dbgMethod, cat, dbgExp, loc ); - } - - [StackTraceHidden] - static public void trace( string msg, string cat = "", SourceLoc? loc = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) - { - logBase( msg, Levels.Trace, dbgPath, dbgLine, dbgMethod, cat, dbgExp, loc ); - } - - [StackTraceHidden] - static public void warn( string msg, string cat = "", SourceLoc? loc = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) - { - logBase( msg, Levels.Warn, dbgPath, dbgLine, dbgMethod, cat, dbgExp, loc ); - } - - [StackTraceHidden] - static public void high( string msg, string cat = "", SourceLoc? loc = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) - { - logBase( msg, Levels.High, dbgPath, dbgLine, dbgMethod, cat, dbgExp, loc ); - } - - [StackTraceHidden] - static public void error( string msg, string cat = "", SourceLoc? loc = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) - { - logBase( $"{dbgMethod}: {msg}", Levels.Error, dbgPath, dbgLine, dbgMethod, cat, dbgExp, loc ); - } - - - [StackTraceHidden] - // VERY SPECIALIZED ONLY USE IF YOU KNOW WHAT YOU ARE DOING - static public void row_internal( string msg, string cat = "", SourceLoc? loc = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) - { - logBase( msg, Levels.Raw, dbgPath, dbgLine, dbgMethod, cat, dbgExp, loc ); - } - - - [StackTraceHidden] - static public void exception( Exception ex, string msg, string cat = "", SourceLoc? loc = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) - { - - logBase( $"*******************************************************************************", - Levels.Raw, dbgPath, dbgLine, dbgMethod, cat, dbgExp, loc ); - logBase( $"{dbgMethod}: {msg} | Caught At:", Levels.Error, dbgPath, dbgLine, dbgMethod, cat, dbgExp, loc ); - logBase( $"{dbgPath}({dbgLine}): {dbgMethod}: ---------------", - Levels.Raw, dbgPath, dbgLine, dbgMethod, cat, dbgExp, loc ); - LogStackTrace( ex ); - } - - - - [StackTraceHidden] - static public void fatal( string msg, string cat = "", SourceLoc? loc = null, [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "msg" )] string dbgExp = "" ) - { - logBase( msg, Levels.Fatal, dbgPath, dbgLine, dbgMethod, cat, dbgExp, loc ); - } - - //new LogEvent( Levels.Raw, $"", "", 0, "", "lib.time", "", null ) - - #endregion - - static private bool hasMethod( string name, Type t ) - { - var mi = t.GetMethod( name ); - - return mi != null; - } - - static private bool hasMethod( string name ) => hasMethod( name, typeof( T ) ); - - - #region Helpers - - static public void environment( string header = "", string footer = "", Levels level = Levels.Info, string cat = "", string prefix = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "obj" )] string dbgExpObj = "", SourceLoc? loc = null ) - { - log.var( System.Environment.ProcessId, dbgMethod, dbgPath, dbgLine ); - - log.var( System.Environment.CommandLine, dbgMethod, dbgPath, dbgLine ); - - log.var( System.Environment.CurrentDirectory, dbgMethod, dbgPath, dbgLine ); - log.var( System.Environment.SystemDirectory, dbgMethod, dbgPath, dbgLine ); - - log.var( System.Environment.GetEnvironmentVariable( "PATH" ), dbgMethod, dbgPath, dbgLine ); - log.var( System.Environment.GetEnvironmentVariable( "USER" ), dbgMethod, dbgPath, dbgLine ); - log.var( System.Environment.GetEnvironmentVariable( "HOME" ), dbgMethod, dbgPath, dbgLine ); - - log.var( System.Environment.ProcessorCount, dbgMethod, dbgPath, dbgLine ); - log.var( System.Environment.Is64BitOperatingSystem, dbgMethod, dbgPath, dbgLine ); - log.var( System.Environment.Is64BitProcess, dbgMethod, dbgPath, dbgLine ); - - log.var( System.Environment.MachineName, dbgMethod, dbgPath, dbgLine ); - - log.var( System.Environment.Version, dbgMethod, dbgPath, dbgLine ); - log.var( System.Environment.OSVersion, dbgMethod, dbgPath, dbgLine ); - - } - - static public void props_orig( object obj, string header, int depth = 0, Levels type = Levels.Debug, string cat = "", string prefix = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "obj" )] string dbgExpObj = "", SourceLoc? loc = null ) - { - var list = refl.GetAllProperties( obj.GetType() ); - - lock( s_lock ) - { - var evt = new LogEvent( type, header, dbgPath, dbgLine, dbgMethod, cat, dbgExpObj, loc ); - { - // Use Add instead of Enqueue - s_events.Add( evt ); - - foreach( var pi in list ) - { - try - { - var v = pi.GetValue( obj ); - var isNull = v != null; - - if( isNull ) - { - logBase( $"{prefix}{pi.Name} = null", type, dbgPath, dbgLine, dbgMethod, dbgExpObj, cat ); - continue; - } - - //var isPOD = - - - logBase( $"{prefix}{pi.Name} = {v}", type, dbgPath, dbgLine, dbgMethod, dbgExpObj, cat ); - } - catch( Exception ex ) - { - logBase( $"{prefix}{pi.Name} = {ex.Message}", type, dbgPath, dbgLine, dbgMethod, dbgExpObj, cat ); - //logBase( $"Exception processing {pi.Name} {ex.Message}", Levels.Error, dbgPath, dbgLine, dbgMethod, cat, dbgExpObj, loc ); - } - } - - } - } - } - - - private static readonly HashSet s_podTypes = new() - { - typeof(string), typeof(bool), typeof(byte), typeof(sbyte), - typeof(char), typeof(decimal), typeof(double), typeof(float), - typeof(int), typeof(uint), typeof(long), typeof(ulong), - typeof(short), typeof(ushort), typeof(DateTime), typeof(DateTimeOffset), - typeof(TimeSpan), typeof(Guid) - }; - - static public void props( object obj, string header, int maxDepth = 0, Levels level = Levels.Debug, string cat = "", string prefix = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "obj" )] string dbgExpObj = "", SourceLoc? loc = null ) - { - // May not need this - //var visited = new HashSet( ReferenceEqualityComparer.Instance ); - - props_r( obj, header, maxDepth, level, cat, prefix, dbgPath, dbgLine, dbgMethod, dbgExpObj, loc ); - } - - static private void props_r( object obj, string header, int maxDepth = 0, Levels level = Levels.Debug, string cat = "", string prefix = "", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "", [CallerArgumentExpression( "obj" )] string dbgExpObj = "", SourceLoc? loc = null ) - { - // 1. Handle Null - if( obj is null ) - { - return; - } - - Type type = obj.GetType(); - - var tc = Type.GetTypeCode( type ); - - - //logBase( string msg, Levels type = Levels.Debug, string dbgPath = "", int dbgLine = -1, string dbgMethod = "", string cat = "unk", string exp = "", SourceLoc? loc = null ) - - - // 2. Handle POD (Plain Old Data) & Strings - if( tc != TypeCode.Object ) - { - logBase( $"{header}{dbgExpObj} = {obj}", level, dbgPath, dbgLine, dbgMethod, cat, dbgExpObj ); - return; - } - - // 3. Handle Overridden ToString() (User Rule) - // Check if the type overrides ToString() directly (not just inheriting object.ToString) - var toStringMethod = type.GetMethod( "ToString", Type.EmptyTypes ); - if( toStringMethod != null && toStringMethod.DeclaringType != typeof( object ) ) - { - logBase( $"{header}{dbgExpObj} = {obj.ToString()}", level, dbgPath, dbgLine, dbgMethod, cat, dbgExpObj ); - return; - } - - // 4. Cycle Detection - /* - if( !visited.Add( obj ) ) - { - sb.Append( $"{{$Cyclic:{type.Name}}}" ); - return; - } - //*/ - - // 5. Handle Collections (IEnumerable) - if( obj is IEnumerable collection ) - { - logBase( $"{header}{dbgExpObj}[{type.Name}] [[[", level, dbgPath, dbgLine, dbgMethod, cat, dbgExpObj ); - - if( maxDepth == 0 ) - { - } - else - { - var collectionHader = $"{header}{header}"; - var count = 0; - foreach( var item in collection ) - { - //if( count != 0 ) - props_r( item, collectionHader, --maxDepth, level, cat, prefix, dbgPath, dbgLine, dbgMethod, $"{count}" ); - } - } - logBase( $"{header}{dbgExpObj}[{type.Name}] ]]]", level, dbgPath, dbgLine, dbgMethod, cat, dbgExpObj ); - return; - } - - - logBase( $"{header}{dbgExpObj}[{type.Name}] ->", level, dbgPath, dbgLine, dbgMethod, cat, dbgExpObj ); - - // 6. Handle Complex Objects (Properties) - if( maxDepth < 0 ) - { - return; - } - - List members = new(); - refl.GetAllMembers( type, members ); - - // Get all readable properties - //var props = type.GetProperties( BindingFlags.Public | BindingFlags.Private | BindingFlags.Instance ) - // .Where( p => p.CanRead && p.GetIndexParameters().Length == 0 ); - - var props = members.Where( ( mi) => true ); - - var nextHeader = $"{header}{header}"; - - //bool firstProp = true; - foreach( var mi in members ) - { - var is_pi = mi is PropertyInfo pi; - var is_fi = mi is FieldInfo fi; - - var name = mi.Name; - var piType = mi.MemberType; - try - { - object? value = GetValue( obj, mi ); - props_r( value, nextHeader, --maxDepth, level, cat, prefix, dbgPath, dbgLine, dbgMethod, name ); - } - catch( Exception ex ) - { - logBase( $"{nextHeader}{name}[{mi.Name}] ex {ex.Message}", level, dbgPath, dbgLine, dbgMethod, cat, name ); - } - } - } - - static object GetValue( object obj, MemberInfo mi ) - { - if( mi is PropertyInfo pi ) - { - return pi.GetValue( obj ); - } - if( mi is FieldInfo fi ) - { - return fi.GetValue( obj ); - } - - return null; - - } - - //This might seem a little odd, but the intent is that usually you wont need to set notExpectedValue. - static public void expected( 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}" ); - } - } - - static void LogStackTrace( Exception ex, string prefixMsg = "" ) - { - var stackTrace = new StackTrace( ex, true ); - var formattedTrace = "Stack Trace:\n"; - - if( ex.InnerException != null ) - { - var prefix = $"{prefixMsg}Inner->"; - LogStackTrace( ex.InnerException, prefix ); - } - - log.error( $"{prefixMsg} Exception: {ex.Message}" ); - - foreach( var fr in stackTrace.GetFrames() ) - { - logBase( $"{fr.Log}", Levels.Raw ); - } - } - - #endregion - - public delegate bool FilterFn( LogEvent evt ); - - - - static object s_lock = new object(); - static object s_lockTypeCallback = new object(); - static ImmutableDictionary> s_callbacks = ImmutableDictionary>.Empty; - - static ConcurrentDictionary> s_filters = new(); - - static public void addFilter( string name, FilterFn filter ) - { - //* - s_filters.AddOrUpdate( name, - ( k ) => new List { filter }, - ( k, v ) => - { - v.Add( filter ); - return v; - } ); - //*/ - } - - - [StackTraceHidden] - static public void addDirectCallback( Levels level, Action callback ) - { - //ImmutableInterlocked.Add( ref s_callbacks, level, callback ); - var added = II.TryAdd( ref s_callbacks, level, callback ); - if( !added ) - { - log.warn( $"Failed to add callback for {level}" ); - } - } - - - static public LogEvent logCreateEvent( string msg, Levels type = Levels.Debug, string dbgPath = "", int dbgLine = -1, string dbgMethod = "", string cat = "unk", string exp = "", SourceLoc? loc = null ) - { - LogEvent evt = new LogEvent( type, msg, dbgPath, dbgLine, dbgMethod, cat, exp, loc ); - return evt; - } - - [StackTraceHidden] - static public void logBase( string msg, Levels type = Levels.Debug, string dbgPath = "", int dbgLine = -1, string dbgMethod = "", string cat = "unk", string exp = "", SourceLoc? loc = 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( Levels.Raw, $"", "", 0, "", "lib.time", "", null ); - var beginLine = new LogEvent( Levels.Raw, $"Begin B E G I N ******************************************************************************************************************", "", 0, "", "lib.time", "", null ); - var timeLine = new LogEvent( Levels.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( Levels.Info, $"startup BEGIN", "", 0, "", "log.startup", "", null ); - s_events.Add( msgStartupBegin ); - - LogEvent msgFilename = new LogEvent( Levels.Info, $"Logging in {filename}", "", 0, "", "log.startup", "", null ); - s_events.Add( msgFilename ); - - var optionsLine = new LogEvent( Levels.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() ) - { - //* - // Apply filters - var cat = evt.Cat; - var blocked = false; - if( s_filters.TryGetValue( cat, out var filterList ) ) - { - foreach( var filter in filterList ) - { - if( !filter( evt ) ) - { - blocked = true; - break; - } - } - - if( blocked ) - { - continue; // Skip this event - } - } - //*/ - 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( Levels type ) - { - switch( type ) - { - case Levels.Trace: - return ' '; - case Levels.Debug: - return ' '; - case Levels.Info: - return ' '; - case Levels.High: - return '+'; - case Levels.Warn: - return '+'; - case Levels.Error: - return '*'; - case Levels.Fatal: - return '*'; - default: - return '?'; - } - } - - private static void setConsoleColor( log.LogEvent evt ) - { - switch( evt.Levels ) - { - case log.Levels.Trace: - Console.ForegroundColor = ConsoleColor.DarkGray; - break; - case log.Levels.Debug: - Console.ForegroundColor = ConsoleColor.Gray; - break; - case log.Levels.Info: - Console.ForegroundColor = ConsoleColor.DarkGreen; - break; - case log.Levels.High: - Console.ForegroundColor = ConsoleColor.Cyan; - break; - case log.Levels.Warn: - Console.ForegroundColor = ConsoleColor.Yellow; - break; - case log.Levels.Error: - Console.ForegroundColor = ConsoleColor.DarkRed; - Console.BackgroundColor = ConsoleColor.DarkGray; - break; - case log.Levels.Fatal: - Console.ForegroundColor = ConsoleColor.Red; - Console.BackgroundColor = ConsoleColor.DarkGray; - break; - - case log.Levels.Invalid: - case log.Levels.Time: - case log.Levels.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.Levels != Levels.Raw ) - { - var span = evt.Time - s_startTime; - - char sym = getSymbol( evt.Levels ); - - 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.Levels != Levels.Raw ) - { - try - { - var span = evt.Time - s_startTime; - - char sym = getSymbol( evt.Levels ); - - 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.Levels != Levels.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( Levels.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.Levels != Levels.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 ); - s_writer?.Flush(); - } - } - - 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.Levels ); - - 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(); - - static public Value Value( T val, - [CallerArgumentExpression("val")] - string dbgExp = "" -) - { - return new( val, dbgExp ); - } - - -} // end static class log