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.Security.Cryptography.X509Certificates; using lib.Net; //using System.Threading.Tasks; static public class log { [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 Cat; public object Obj; static ImmutableDictionary s_shortname = ImmutableDictionary.Empty; public LogEvent( LogType logType, string msg, string path, int line, string member, string cat, 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 = path.GetHashCode(); if( s_shortname.TryGetValue( pathHash, out var autoCat ) ) { cat = autoCat; } else { var pathPieces = path.Split( '\\' ); if( pathPieces.Length < 2 ) { pathPieces = path.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 = path; Line = line; Member = member; Cat = cat; Obj = obj; } } static LogEvent CreateLogEvent( LogType logType, string msg, string cat, object obj, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "" ) { var logEvent = new LogEvent( logType, msg, path, line, member, cat, obj ); return logEvent; } public delegate void Log_delegate( LogEvent evt ); static public void create( string filename, Endpoints endpoints ) { startup( filename, endpoints ); } static public void destroy() { string msg = "==============================================================================\nLogfile shutdown at " + DateTime.Now.ToString(); var evt = CreateLogEvent( LogType.Info, msg, "System", null ); s_events.Enqueue( evt ); stop(); } static internal ConcurrentQueue s_events = new ConcurrentQueue(); static private Thread s_thread; /* static public Log log { get { return s_log; } } */ static string s_cwd = Directory.GetCurrentDirectory(); static int s_cwdLength = s_cwd.Length; static ImmutableDictionary s_files = ImmutableDictionary.Empty; #region Util 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 path = "" ) { return relativePath( path ); } #endregion // Util #region Forwards static public void fatal( string msg, string cat = "", object obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "" ) { logBase( msg, LogType.Fatal, path, line, member, cat, obj ); } static public void error( string msg, string cat = "", object obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "" ) { logBase( msg, LogType.Error, path, line, member, cat, obj ); } static public void warn( string msg, string cat = "", object obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "" ) { logBase( msg, LogType.Warn, path, line, member, cat, obj ); } static public void high( string msg, string cat = "", object obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "" ) { logBase( msg, LogType.High, path, line, member, cat, obj ); } static public void info( string msg, string cat = "", object obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "" ) { logBase( msg, LogType.Info, path, line, member, cat, obj ); } static public void debug( string msg, string cat = "", object obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "" ) { logBase( msg, LogType.Debug, path, line, member, cat, obj ); } static public void trace( string msg, string cat = "", object obj = null, [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "" ) { logBase( msg, LogType.Trace, path, line, member, cat, obj ); } #endregion #region Helpers static public void logProps( object obj, string header, LogType type = LogType.Debug, string cat = "", string prefix = "", [CallerFilePath] string path = "", [CallerLineNumber] int line = -1, [CallerMemberName] string member = "" ) { var list = refl.GetAllProperties( obj.GetType() ); lock( s_lock ) { var evt = new LogEvent( type, header, path, line, member, cat, obj ); { s_events.Enqueue( evt ); foreach( var pi in list ) { try { var v = pi.GetValue( obj ); logBase( $"{prefix}{pi.Name} = {v}", type, path, line, member, cat ); } catch( Exception ex ) { logBase( $"Exception processing {pi.Name} {ex.Message}", LogType.Error, "log" ); } } } } } //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( !value.Equals( notExpectedValue ) ) { log.debug( $"Properly got {value}{trueString}" ); } else { log.warn( $"Got {notExpectedValue} instead of {value}{falseString}" ); } } #endregion static object s_lock = new object(); static public LogEvent logCreateEvent( string msg, LogType type = LogType.Debug, string path = "", int line = -1, string member = "", string cat = "unk", object obj = null ) { LogEvent evt = new LogEvent( type, msg, path, line, member, cat, obj ); return evt; } static public void logBase( string msg, LogType type = LogType.Debug, string path = "", int line = -1, string member = "", string cat = "unk", object obj = null ) { var evt = logCreateEvent( msg, type, path, line, member, cat ); s_events.Enqueue( evt ); } static Endpoints s_endpoints = Endpoints.Console; static int s_catWidth = 14; static int s_timeWidth = 6; static void startup( string filename, Endpoints endpoints ) { s_startTime = DateTime.Now; s_cwd = Directory.GetCurrentDirectory(); s_cwdLength = s_cwd.Length; s_endpoints = endpoints; var start = new ThreadStart( run ); s_thread = new Thread( start ); s_thread.Priority = ThreadPriority.BelowNormal; s_thread.Name = $"Logging"; s_thread.Start(); //TODO: Fix this so itll work without a directory. Directory.CreateDirectory( Path.GetDirectoryName( filename ) ); string 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 ); s_errorStream = new FileStream( filename + ".error", FileMode.Append, FileAccess.Write ); s_errorWriter = new StreamWriter( s_errorStream ); info( $"Logging in {filename}" ); //Debug.Listeners.Add( this ); //var evt = CreateLogEvent( LogType.Info, $"startup", "System", null ); //s_events.Enqueue( evt ); /* if( (endpoints & Endpoints.Console) == Endpoints.Console ) { addDelegate(WriteToConsole); } */ info( $"startup" ); } static bool s_running = true; static void run() { while( s_running ) { while( s_events.TryDequeue( out var evt ) ) { writeToAll( evt ); } // TODO PERF Replace this with a semaphore/mutex Thread.Sleep( 0 ); } } static void stop() { s_running = false; 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_lastTime = DateTime.MinValue; 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 msgHeader( LogEvent evt ) { 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 time = span.Milliseconds; // humanTime( secondSpan ); var msgHdr = string.Format( $"{s_timeHeader}{{0,-{s_timeWidth}}} | {{1,-{s_catWidth}}}{{2}}| ", time, truncatedCat, sym ); return msgHdr; } static public string msgFrom( LogEvent evt ) { var msgHdr = msgHeader( evt ); string finalLine = $"{msgHdr}{evt.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("D2")}:{evt.Time.Minute.ToString("D2")}:{evt.Time.Second.ToString("D2")}.{evt.Time.Millisecond.ToString("D4")} | {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 finalLine = msgFrom( evt ); if( ( s_endpoints & Endpoints.File ) == Endpoints.File ) { s_writer.WriteLine( finalLine ); } if( ( s_endpoints & Endpoints.Console ) == Endpoints.Console ) { setConsoleColor( evt ); Console.WriteLine( finalLine ); 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(); }