单元测试去掉net462,xunit对它的支持有些问题,导致测试结束时卡死
石头 authored at 2024-11-24 23:18:00
13.05 KiB
X
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

#if __WIN__

using System.Windows.Forms;

#endif

using NewLife.Reflection;
using NewLife.Threading;
using NewLife.Windows;

#nullable enable

namespace NewLife.Log;

/// <summary>日志类,包含跟踪调试功能</summary>
/// <remarks>
/// 文档 https://newlifex.com/core/log
///
/// 该静态类包括写日志、写调用栈和Dump进程内存等调试功能。
///
/// 默认写日志到文本文件,可通过修改<see cref="Log"/>属性来增加日志输出方式。
/// 对于控制台工程,可以直接通过UseConsole方法,把日志输出重定向为控制台输出,并且可以为不同线程使用不同颜色。
/// </remarks>
public static class XTrace
{
    #region 写日志

    /// <summary>文本文件日志</summary>
    private static ILog _Log = Logger.Null;

    /// <summary>日志提供者,默认使用文本文件日志</summary>
    public static ILog Log { get { InitLog(); return _Log; } set { _Log = value; } }

    /// <summary>输出日志</summary>
    /// <param name="msg">信息</param>
    public static void WriteLine(String msg)
    {
        if (!InitLog()) return;

        WriteVersion();

        Log.Info(msg);
    }

    /// <summary>写日志</summary>
    /// <param name="format"></param>
    /// <param name="args"></param>
    public static void WriteLine(String format, params Object?[] args)
    {
        if (!InitLog()) return;

        WriteVersion();

        Log.Info(format, args);
    }

    ///// <summary>异步写日志</summary>
    ///// <param name="format"></param>
    ///// <param name="args"></param>
    //public static void WriteLineAsync(String format, params Object[] args)
    //{
    //    ThreadPool.QueueUserWorkItem(s => WriteLine(format, args));
    //}

    /// <summary>输出异常日志</summary>
    /// <param name="ex">异常信息</param>
    public static void WriteException(Exception ex)
    {
        if (!InitLog()) return;

        WriteVersion();

        Log.Error("{0}", ex);
    }

    #endregion 写日志

    #region 构造

    static XTrace()
    {
        AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
        TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
        AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
#if NETCOREAPP
        System.Runtime.Loader.AssemblyLoadContext.Default.Unloading += ctx => OnProcessExit(null, EventArgs.Empty);
#endif

        ThreadPoolX.Init();

        try
        {
            var set = Setting.Current;
            Debug = set.Debug;
            LogPath = set.LogPath;
        }
        catch { }
    }

    private static void CurrentDomain_UnhandledException(Object sender, UnhandledExceptionEventArgs e)
    {
        if (e.ExceptionObject is Exception ex)
        {
            // 全局异常埋点
            DefaultTracer.Instance?.NewError(ex.GetType().Name, ex);
            WriteException(ex);
        }
        if (e.IsTerminating)
        {
            Log.Fatal("异常退出!");

            OnProcessExit(null, EventArgs.Empty);
        }
    }

    private static void TaskScheduler_UnobservedTaskException(Object? sender, UnobservedTaskExceptionEventArgs e)
    {
        if (!e.Observed && e.Exception != null)
        {
            //WriteException(e.Exception);
            foreach (var ex in e.Exception.Flatten().InnerExceptions)
            {
                // 全局异常埋点
                DefaultTracer.Instance?.NewError(ex.GetType().Name, ex);
                WriteException(ex);
            }
            e.SetObserved();
        }
    }

    private static void OnProcessExit(Object? sender, EventArgs e)
    {
        if (Log is CompositeLog compositeLog)
        {
            var log = compositeLog.Get<TextFileLog>();
            log.TryDispose();
        }
        else
        {
            Log.TryDispose();
        }
    }

    private static readonly Object _lock = new();
    private static Int32 _initing = 0;

    /// <summary>
    /// 2012.11.05 修正初次调用的时候,由于同步BUG,导致Log为空的问题。
    /// </summary>
    private static Boolean InitLog()
    {
        /*
         * 日志初始化可能会触发配置模块,其内部又写日志导致死循环。
         * 1,外部写日志引发初始化
         * 2,标识日志初始化正在进行中
         * 3,初始化日志提供者
         * 4,此时如果再次引发写入日志,发现正在进行中,放弃写入的日志
         * 5,标识日志初始化已完成
         * 6,正常写入日志
         */

        if (_Log != null && _Log != Logger.Null) return true;
        if (_initing > 0 && _initing == Thread.CurrentThread.ManagedThreadId) return false;

        lock (_lock)
        {
            if (_Log != null && _Log != Logger.Null) return true;

            _initing = Thread.CurrentThread.ManagedThreadId;

            var set = Setting.Current;
            if (LogPath.IsNullOrEmpty() || LogPath == "Log") LogPath = set.LogPath;
            if (set.LogFileFormat.Contains("{1}"))
                _Log = new LevelLog(LogPath, set.LogFileFormat);
            else
                _Log = TextFileLog.Create(LogPath);

            if (!set.NetworkLog.IsNullOrEmpty())
            {
                var nlog = new NetworkLog(set.NetworkLog);
                _Log = new CompositeLog(_Log, nlog);
            }

            _initing = 0;
        }

        //WriteVersion();

        return true;
    }

    #endregion 构造

    #region 使用控制台输出

    private static Boolean _useConsole;

    /// <summary>使用控制台输出日志,只能调用一次</summary>
    /// <param name="useColor">是否使用颜色,默认使用</param>
    /// <param name="useFileLog">是否同时使用文件日志,默认使用</param>
    public static void UseConsole(Boolean useColor = true, Boolean useFileLog = true)
    {
        if (_useConsole) return;
        _useConsole = true;

        //if (!Runtime.IsConsole) return;
        Runtime.IsConsole = true;

        // 适当加大控制台窗口
        try
        {
#if !NETFRAMEWORK
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                if (Console.WindowWidth <= 80) Console.WindowWidth = Console.WindowWidth * 3 / 2;
                if (Console.WindowHeight <= 25) Console.WindowHeight = Console.WindowHeight * 3 / 2;
            }
#else
            if (Console.WindowWidth <= 80) Console.WindowWidth = Console.WindowWidth * 3 / 2;
            if (Console.WindowHeight <= 25) Console.WindowHeight = Console.WindowHeight * 3 / 2;
#endif
        }
        catch { }

        var clg = new ConsoleLog { UseColor = useColor };
        if (useFileLog)
            _Log = new CompositeLog(clg, Log);
        else
            _Log = clg;
    }

    #endregion 使用控制台输出

    #region 控制台禁用快捷编辑

    /// <summary>
    /// 禁用控制台快捷编辑,在UseConsole方法之后调用
    /// </summary>
    public static void DisableConsoleEdit()
    {
        if (!_useConsole) return;
        try
        {
            if (Runtime.Windows)
            {
                ConsoleHelper.DisableQuickEditMode();
            }
        }
        catch { }
    }

    /// <summary>
    /// 禁用控制台关闭按钮
    /// </summary>
    /// <param name="consoleTitle">控制台程序名称,可使用Console.Title动态设置的值</param>
    public static void DisableConsoleCloseButton(String consoleTitle)
    {
        try
        {
            if (Runtime.Windows)
            {
                ConsoleHelper.DisableCloseButton(consoleTitle);
            }
        }
        catch { }
    }

    #endregion 控制台禁用关闭按钮

    #region 拦截WinForm异常

#if __WIN__
    private static Int32 initWF = 0;
    private static Boolean _ShowErrorMessage;
    //private static String _Title;

    /// <summary>拦截WinForm异常并记录日志,可指定是否用<see cref="MessageBox"/>显示。</summary>
    /// <param name="showErrorMessage">发为捕获异常时,是否显示提示,默认显示</param>
    public static void UseWinForm(Boolean showErrorMessage = true)
    {
        Runtime.IsConsole = false;

        _ShowErrorMessage = showErrorMessage;

        if (initWF > 0 || Interlocked.CompareExchange(ref initWF, 1, 0) != 0) return;
        //if (!Application.MessageLoop) return;

        AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException2;
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
        Application.ThreadException += Application_ThreadException;
    }

    private static void CurrentDomain_UnhandledException2(Object sender, UnhandledExceptionEventArgs e)
    {
        var show = _ShowErrorMessage && Application.MessageLoop;
        var ex = e.ExceptionObject as Exception;
        var title = e.IsTerminating ? "异常退出" : "出错";
        if (show) MessageBox.Show(ex?.Message, title, MessageBoxButtons.OK, MessageBoxIcon.Error);
    }

    private static void Application_ThreadException(Object sender, ThreadExceptionEventArgs e)
    {
        WriteException(e.Exception);

        var show = _ShowErrorMessage && Application.MessageLoop;
        if (show) MessageBox.Show(e.Exception == null ? "" : e.Exception.Message, "出错", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }

    /// <summary>在WinForm控件上输出日志,主要考虑非UI线程操作</summary>
    /// <remarks>不是常用功能,为了避免干扰常用功能,保持UseWinForm开头</remarks>
    /// <param name="control">要绑定日志输出的WinForm控件</param>
    /// <param name="useFileLog">是否同时使用文件日志,默认使用</param>
    /// <param name="maxLines">最大行数</param>
    public static void UseWinFormControl(this Control control, Boolean useFileLog = true, Int32 maxLines = 1000)
    {
        var clg = _Log as TextControlLog;
        var ftl = _Log as TextFileLog;
        if (_Log is CompositeLog cmp)
        {
            ftl = cmp.Get<TextFileLog>();
            clg = cmp.Get<TextControlLog>();
        }

        // 控制控制台日志
        clg ??= new TextControlLog();
        clg.Control = control;
        clg.MaxLines = maxLines;

        if (!useFileLog)
        {
            Log = clg;
            ftl?.Dispose();
        }
        else
        {
            ftl ??= TextFileLog.Create(String.Empty);
            Log = new CompositeLog(clg, ftl);
        }
    }

    /// <summary>控件绑定到日志,生成混合日志</summary>
    /// <param name="control"></param>
    /// <param name="log"></param>
    /// <param name="maxLines"></param>
    /// <returns></returns>
    public static ILog Combine(this Control control, ILog log, Int32 maxLines = 1000)
    {
        //if (control == null || log == null) return log;

        var clg = new TextControlLog
        {
            Control = control,
            MaxLines = maxLines
        };

        return new CompositeLog(log, clg);
    }

#endif

    #endregion 拦截WinForm异常

    #region 属性

    /// <summary>是否调试。</summary>
    public static Boolean Debug { get; set; }

    /// <summary>文本日志目录</summary>
    public static String? LogPath { get; set; }

    ///// <summary>临时目录</summary>
    //public static String TempPath { get; set; } = Setting.Current.TempPath;

    #endregion 属性

    #region 版本信息

    private static Int32 _writeVersion;

    /// <summary>输出核心库和启动程序的版本号</summary>
    public static void WriteVersion()
    {
        if (_writeVersion > 0 || Interlocked.CompareExchange(ref _writeVersion, 1, 0) != 0) return;

        var asm = Assembly.GetExecutingAssembly();
        WriteVersion(asm);

        var asm2 = Assembly.GetEntryAssembly();
        if (asm2 != null && asm2 != asm) WriteVersion(asm2);
    }

    /// <summary>输出程序集版本</summary>
    /// <param name="asm"></param>
    public static void WriteVersion(this Assembly asm)
    {
        if (asm == null) return;

        var asmx = AssemblyX.Create(asm);
        if (asmx != null)
        {
            var ver = "";
            var tar = asm.GetCustomAttribute<TargetFrameworkAttribute>();
            if (tar != null)
            {
                ver = tar.FrameworkDisplayName;
                if (ver.IsNullOrEmpty()) ver = tar.FrameworkName;
            }

            WriteLine("{0} v{1} Build {2:yyyy-MM-dd HH:mm:ss} {3}", asmx.Name, asmx.FileVersion, asmx.Compile, ver);
            var att = asmx.Asm.GetCustomAttribute<AssemblyCopyrightAttribute>();
            WriteLine("{0} {1}", asmx.Title, att?.Copyright);
        }
    }

    #endregion 版本信息
}

#nullable restore