diff --git a/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy.csproj b/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy.csproj
index 7350d4985a5..560ba13e352 100644
--- a/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy.csproj
+++ b/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy.csproj
@@ -76,6 +76,9 @@
Services\Platform
+
+ Services\Mvvm
+
@@ -104,6 +107,7 @@
+
diff --git a/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Program.cs b/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Program.cs
index 6e5cd83f416..7e1d39267ba 100644
--- a/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Program.cs
+++ b/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Program.cs
@@ -81,6 +81,7 @@ static void ConfigureServices(IServiceCollection services)
#if DEBUG
l.AddConsole();
#endif
+ l.AddProvider(new LogConsoleService.Utf8StringLoggerProvider(moduleName));
});
// 设置仓储层数据库文件存放路径
diff --git a/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services.Implementation/LazyReverseProxyServiceImpl.cs b/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services.Implementation/LazyReverseProxyServiceImpl.cs
index fdd785a6169..2550649d406 100644
--- a/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services.Implementation/LazyReverseProxyServiceImpl.cs
+++ b/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services.Implementation/LazyReverseProxyServiceImpl.cs
@@ -23,6 +23,8 @@ public IReadOnlyCollection? Scripts
public byte[]? GetFlowStatistics_Bytes() => impl().GetFlowStatistics_Bytes();
+ public string? GetLogAllMessage() => impl().GetLogAllMessage();
+
public async Task StartProxyAsync(byte[] reverseProxySettings)
{
var result = await impl().StartProxyAsync(reverseProxySettings);
diff --git a/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services.Implementation/YarpReverseProxyServiceImpl.cs b/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services.Implementation/YarpReverseProxyServiceImpl.cs
index 7569f2c2bce..e9a96665706 100644
--- a/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services.Implementation/YarpReverseProxyServiceImpl.cs
+++ b/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services.Implementation/YarpReverseProxyServiceImpl.cs
@@ -65,6 +65,8 @@ StartProxyResult StartProxyCore()
WebRootPath = RootPath,
});
+ builder.Logging.AddProvider(new LogConsoleService.Utf8StringLoggerProvider(AssemblyInfo.Accelerator));
+
builder.Services.Configure(static o =>
{
o.AllowEmptyHosts = true;
@@ -167,6 +169,12 @@ public async Task StopProxyAsync()
return bytes;
}
+ public string? GetLogAllMessage()
+ {
+ var result = LogConsoleService.Builder;
+ return result.ToString();
+ }
+
// IDisposable
protected override void DisposeCore()
diff --git a/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services/IReverseProxyService.cs b/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services/IReverseProxyService.cs
index 92ca8bf7e15..f071c5e7a30 100644
--- a/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services/IReverseProxyService.cs
+++ b/src/BD.WTTS.Client.Plugins.Accelerator.ReverseProxy/Services/IReverseProxyService.cs
@@ -15,6 +15,12 @@ partial interface IReverseProxyService
///
///
byte[]? GetFlowStatistics_Bytes();
+
+ ///
+ /// 获取所有日志信息
+ ///
+ ///
+ string? GetLogAllMessage();
}
public static partial class ReverseProxyServiceExtensions
diff --git a/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/ProxyChartView.axaml.cs b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/ProxyChartView.axaml.cs
index 670b0c29100..34d863b48b2 100644
--- a/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/ProxyChartView.axaml.cs
+++ b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/ProxyChartView.axaml.cs
@@ -1,14 +1,10 @@
using Avalonia;
using Avalonia.Controls;
-using Avalonia.Data;
using Avalonia.Threading;
using Avalonia.VisualTree;
using LiveChartsCore;
-using LiveChartsCore.Motion;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Avalonia;
-using LiveChartsCore.SkiaSharpView.Drawing;
-using SkiaSharp;
namespace BD.WTTS.UI.Views.Controls;
diff --git a/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/ProxyLog.axaml b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/ProxyLog.axaml
new file mode 100644
index 00000000000..41a3103f18c
--- /dev/null
+++ b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/ProxyLog.axaml
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/ProxyLog.axaml.cs b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/ProxyLog.axaml.cs
new file mode 100644
index 00000000000..5b295ccb7b7
--- /dev/null
+++ b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/ProxyLog.axaml.cs
@@ -0,0 +1,92 @@
+using Avalonia.Controls;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+using AvaloniaEdit;
+using AvaloniaEdit.TextMate;
+using Org.BouncyCastle.Math;
+using TextMateSharp.Grammars;
+
+namespace BD.WTTS.UI.Views.Controls;
+
+public partial class ProxyLog : UserControl
+{
+ CancellationTokenSource cancellation = new();
+
+ public ProxyLog()
+ {
+ InitializeComponent();
+
+ var logTextbox = this.FindControl("LogTextbox")!;
+
+ //Here we initialize RegistryOptions with the theme we want to use.
+ var registryOptions = new TextMateSharp.Grammars.RegistryOptions(ThemeName.Dark);
+
+ //Initial setup of TextMate.
+ var textMateInstallation = logTextbox.InstallTextMate(registryOptions);
+
+ //Here we are getting the language by the extension and right after that we are initializing grammar with this language.
+ //And that's all 😀, you are ready to use AvaloniaEdit with syntax highlighting!
+ textMateInstallation.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions.GetLanguageByExtension(".log").Id));
+
+ //logTextbox.Text = LogText;
+ //logTextbox.ScrollToLine(logTextbox.Document.LineCount);
+
+ logTextbox.TextChanged += LogTextbox_TextChanged;
+
+ ProxyService.Current.WhenAnyValue(x => x.ProxyStatus)
+ .Subscribe(x =>
+ {
+ if (x)
+ {
+ cancellation = new CancellationTokenSource();
+ Task2.InBackground(FlushLogsAsync, true);
+ }
+ else
+ {
+ if (cancellation != null)
+ {
+ cancellation.Cancel();
+ cancellation.Dispose();
+ }
+ }
+ });
+ }
+
+ private void LogTextbox_TextChanged(object? sender, EventArgs e)
+ {
+ LogTextbox.ScrollToLine(LogTextbox.Document.LineCount);
+ }
+
+ void FlushLogsAsync()
+ {
+ while (!cancellation.IsCancellationRequested)
+ {
+ try
+ {
+ var isAttachedToVisualTree = this.IsAttachedToVisualTree();
+ if (isAttachedToVisualTree)
+ {
+ var logtext = IReverseProxyService.Constants.Instance.GetLogAllMessage();
+ if (string.IsNullOrEmpty(logtext))
+ {
+ Thread.Sleep(1000);
+ continue;
+ }
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (LogTextbox.Text.Length != logtext.Length)
+ LogTextbox.Text = logtext;
+ });
+ }
+ }
+ catch
+ {
+ }
+ finally
+ {
+ Thread.Sleep(1000);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Pages/AcceleratorPage2.axaml b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Pages/AcceleratorPage2.axaml
index decab56d4ab..38fd421f5b0 100644
--- a/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Pages/AcceleratorPage2.axaml
+++ b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Pages/AcceleratorPage2.axaml
@@ -449,6 +449,7 @@
-->
+
@@ -557,6 +558,8 @@
+
+
diff --git a/src/BD.WTTS.Client/App/IApplication.Log.cs b/src/BD.WTTS.Client/App/IApplication.Log.cs
index 206b1d90965..a6252f00715 100644
--- a/src/BD.WTTS.Client/App/IApplication.Log.cs
+++ b/src/BD.WTTS.Client/App/IApplication.Log.cs
@@ -86,6 +86,22 @@ public static Action ConfigureLogging(LogLevel minLevel = Defau
//#elif MACOS || MACCATALYST || (IOS && DEBUG)
// builder.AddProvider(Logging.OSLogLoggerProvider.Instance);
//#endif
+
+ //#if !ANDROID && !IOS
+ // var isBackend = false;
+ // try
+ // {
+ // isBackend = Startup.Instance.HasIPCRoot;
+ // }
+ // catch
+ // {
+ // }
+
+ // if (isBackend)
+ // {
+ // builder.AddProvider(new LogConsoleService.Utf8StringLoggerProvider(IPlatformService.IPCRoot.moduleName));
+ // }
+ //#endif
};
}
diff --git a/src/BD.WTTS.Client/BD.WTTS.Client.csproj b/src/BD.WTTS.Client/BD.WTTS.Client.csproj
index 7c4c31f2c50..aafb0a9ac8f 100644
--- a/src/BD.WTTS.Client/BD.WTTS.Client.csproj
+++ b/src/BD.WTTS.Client/BD.WTTS.Client.csproj
@@ -122,6 +122,7 @@
+
diff --git a/src/BD.WTTS.Client/Logging/ClientLogger.cs b/src/BD.WTTS.Client/Logging/ClientLogger.cs
index 0378a64afd9..a4c68cf154b 100644
--- a/src/BD.WTTS.Client/Logging/ClientLogger.cs
+++ b/src/BD.WTTS.Client/Logging/ClientLogger.cs
@@ -7,14 +7,8 @@ namespace BD.WTTS.Logging;
///
public abstract class ClientLogger : ILogger
{
- static readonly string _messagePadding;
- static readonly string _newLineWithMessagePadding;
-
- static ClientLogger()
- {
- _messagePadding = new string(' ', 6);
- _newLineWithMessagePadding = Environment.NewLine + _messagePadding;
- }
+ static readonly string _messagePadding = new string(' ', 6);
+ static readonly string _newLineWithMessagePadding = Environment.NewLine + _messagePadding;
protected readonly string name;
diff --git a/src/BD.WTTS.Client/Resources/Strings.Designer.cs b/src/BD.WTTS.Client/Resources/Strings.Designer.cs
index 03593c2cf9d..2def77249b0 100644
--- a/src/BD.WTTS.Client/Resources/Strings.Designer.cs
+++ b/src/BD.WTTS.Client/Resources/Strings.Designer.cs
@@ -1358,6 +1358,15 @@ public static string CommunityFix_OpenHostsDir {
}
}
+ ///
+ /// 查找类似 加速日志 的本地化字符串。
+ ///
+ public static string CommunityFix_ProxyLog {
+ get {
+ return ResourceManager.GetString("CommunityFix_ProxyLog", resourceCulture);
+ }
+ }
+
///
/// 查找类似 加速模式 的本地化字符串。
///
diff --git a/src/BD.WTTS.Client/Resources/Strings.resx b/src/BD.WTTS.Client/Resources/Strings.resx
index e6ffdd80b77..1d162ace81f 100644
--- a/src/BD.WTTS.Client/Resources/Strings.resx
+++ b/src/BD.WTTS.Client/Resources/Strings.resx
@@ -2611,6 +2611,9 @@
已加速
Comment=末尾带空格;
+
+
+ 加速日志
流量统计
diff --git a/src/BD.WTTS.Client/Services.Implementation/IPC/IPCMainProcessServiceImpl.cs b/src/BD.WTTS.Client/Services.Implementation/IPC/IPCMainProcessServiceImpl.cs
index 1debf99f2a3..d924b78fc78 100644
--- a/src/BD.WTTS.Client/Services.Implementation/IPC/IPCMainProcessServiceImpl.cs
+++ b/src/BD.WTTS.Client/Services.Implementation/IPC/IPCMainProcessServiceImpl.cs
@@ -450,6 +450,13 @@ public async Task ExitModules(IEnumerable moduleNames)
return result.All(static x => x.result);
}
+ public void WriteMessage(string? moduleName, byte[] bytes)
+ {
+#if !ANDROID && !IOS
+ LogConsoleService.Current.WriteMessage(moduleName, bytes);
+#endif
+ }
+
///
/// 配置服务
///
diff --git a/src/BD.WTTS.Client/Services/IPC/IPCMainProcessService.cs b/src/BD.WTTS.Client/Services/IPC/IPCMainProcessService.cs
index 38ccf093447..5568c19f205 100644
--- a/src/BD.WTTS.Client/Services/IPC/IPCMainProcessService.cs
+++ b/src/BD.WTTS.Client/Services/IPC/IPCMainProcessService.cs
@@ -67,4 +67,6 @@ public interface IPCMainProcessService : IAsyncDisposable
///
///
ValueTask GetServiceAsync(string moduleName) where T : class;
+
+ void WriteMessage(string? moduleName, byte[] bytes);
}
\ No newline at end of file
diff --git a/src/BD.WTTS.Client/Services/Mvvm/LogConsoleService.Logger.cs b/src/BD.WTTS.Client/Services/Mvvm/LogConsoleService.Logger.cs
new file mode 100644
index 00000000000..cc504205b19
--- /dev/null
+++ b/src/BD.WTTS.Client/Services/Mvvm/LogConsoleService.Logger.cs
@@ -0,0 +1,362 @@
+#if !ANDROID && !IOS
+using Utf8StringInterpolation;
+
+// ReSharper disable once CheckNamespace
+namespace BD.WTTS.Services;
+
+partial class LogConsoleService
+{
+ ///
+ /// 控制台显示最大字符数
+ ///
+ const int ConsoleMaxCharCount = 10_0000;
+
+ static readonly int ConsoleMaxByteCount = Encoding.UTF8.GetMaxByteCount(ConsoleMaxCharCount);
+
+ ///
+ /// 日志源
+ ///
+ [DebuggerDisplay("{DebuggerDisplay(),nq}")]
+ internal sealed partial class Source
+ {
+ List? chunk;
+ (string? str, long byteLength)? message = null;
+
+#if DEBUG
+ readonly string moduleName;
+
+ public Source(string moduleName)
+ {
+ this.moduleName = moduleName;
+ }
+
+ string DebuggerDisplay() =>
+$"""
+moduleName: {moduleName}
+ByteLength: {ByteLength}
+{ToString()}
+""";
+#endif
+
+ public long ByteLength { get; private set; }
+
+ public void BuilderAppend(byte[] value)
+ {
+ (chunk ??= new()).Add(value);
+ ByteLength += value.Length;
+ }
+
+ void BuilderClear()
+ {
+ if (chunk != null && chunk.Count > 1)
+ {
+ // 保留最后 21.3% 的日志
+ var keppLength = (int)MathF.Floor(chunk.Count * .213f);
+ if (keppLength <= 0 || keppLength >= chunk.Count)
+ {
+ keppLength = 1;
+ }
+ chunk = chunk.TakeLast(keppLength).ToList();
+ }
+
+ message = null;
+ ByteLength = 0;
+ }
+
+ ///
+ /// 追加收到的日志 UTF-8 字节
+ ///
+ public void Append(byte[]? value)
+ {
+ if (value != null && value.Length > 0)
+ {
+ if (ByteLength > ConsoleMaxByteCount)
+ {
+ BuilderClear();
+ }
+
+ // 追加收到的日志 UTF-8 字节
+ BuilderAppend(value);
+ }
+ }
+
+ public override string? ToString()
+ {
+ if (message != null)
+ {
+ if (ByteLength == message.Value.byteLength)
+ {
+ // 根据 len 判断是否使用缓存
+ return message.Value.str;
+ }
+ }
+
+ if (chunk == null)
+ {
+ return null;
+ }
+ else if (chunk.Count <= 0)
+ {
+ return string.Empty;
+ }
+
+ using var buffer = Utf8String.CreateWriter(out var writer);
+
+ foreach (var it in chunk)
+ {
+ // 拼接字符串
+ writer.AppendUtf8(it);
+ }
+
+ writer.Flush();
+
+ var u8_bytes = buffer.ToArray();
+
+ chunk = [u8_bytes];
+
+ var result = Encoding.UTF8.GetString(u8_bytes);
+
+ // 设置缓存值
+ message = new(result, ByteLength);
+ return result;
+ }
+ }
+
+ #region 服务进程部分
+
+ static readonly string _messagePadding = new string(' ', 6);
+ static readonly string _newLineWithMessagePadding = Environment.NewLine + _messagePadding;
+
+ static byte[] CreateDefaultLogMessage(string logName, int eventId, string? message, Exception? exception)
+ {
+ // https://github.com/dotnet/extensions/blob/v3.1.5/src/Logging/Logging.Console/src/ConsoleLogger.cs
+ // TODO: 对比 SimpleConsoleFormatter 实现带 Color 的版本
+ // https://github.com/dotnet/runtime/blob/v9.0.0-preview.7.24405.7/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs
+
+ if (!string.IsNullOrEmpty(message))
+ {
+ StringBuilder builder = new();
+ CreateDefaultLogMessage(builder, logName, eventId, message, exception);
+
+ var str = builder.ToString();
+ return Encoding.UTF8.GetBytes(str);
+ }
+ else
+ {
+ using var buffer = Utf8String.CreateWriter(out var logBuilder);
+
+ // Example:
+ // INFO: ConsoleApp.Program[10]
+ // Request received
+
+ // category and event id
+ logBuilder.Append(logName);
+ logBuilder.Append('[');
+ logBuilder.Append(eventId.ToString());
+ logBuilder.AppendLine("]");
+
+ // Example:
+ // System.InvalidOperationException
+ // at Namespace.Class.Function() in File:line X
+ if (exception != null)
+ // exception message
+ logBuilder.AppendLine(exception.ToString());
+
+ logBuilder.Flush();
+
+ var result = buffer.ToArray();
+ return result;
+ }
+ }
+
+ static void CreateDefaultLogMessage(StringBuilder logBuilder, string logName, int eventId, string? message, Exception? exception)
+ {
+ // Example:
+ // INFO: ConsoleApp.Program[10]
+ // Request received
+
+ // category and event id
+ logBuilder.Append(logName);
+ logBuilder.Append('[');
+ logBuilder.Append(eventId);
+ logBuilder.AppendLine("]");
+
+ if (!string.IsNullOrEmpty(message))
+ {
+ // message
+ logBuilder.Append(_messagePadding);
+
+ var len = logBuilder.Length;
+ logBuilder.AppendLine(message);
+ logBuilder.Replace(Environment.NewLine, _newLineWithMessagePadding, len, message.Length);
+ }
+
+ // Example:
+ // System.InvalidOperationException
+ // at Namespace.Class.Function() in File:line X
+ if (exception != null)
+ // exception message
+ logBuilder.AppendLine(exception.ToString());
+ }
+
+ static byte[] CreateDefaultLogMessageWithNLogFileLayout(LogLevel logLevel, string logName, int eventId, string? message, Exception? exception)
+ {
+ // Layout = "
+ // ${longdate}|
+ // ${level}|
+ // ${logger}|${message} |${all-event-properties} ${exception:format=tostring}",
+
+ using var buffer = Utf8String.CreateWriter(out var logBuilder);
+
+ logBuilder.Append(DateTime.Now.ToString(
+ "yyyy-MM-dd HH:mm:ss.fff",
+ CultureInfo.InvariantCulture));
+ logBuilder.AppendUtf8("|"u8);
+
+ switch (logLevel) // see NLog.LogLevel
+ {
+ case LogLevel.Trace:
+ logBuilder.AppendUtf8("Trace"u8);
+ break;
+ case LogLevel.Debug:
+ logBuilder.AppendUtf8("Debug"u8);
+ break;
+ case LogLevel.Information:
+ logBuilder.AppendUtf8("Info"u8);
+ break;
+ case LogLevel.Warning:
+ logBuilder.AppendUtf8("Warn"u8);
+ break;
+ case LogLevel.Error:
+ logBuilder.AppendUtf8("Error"u8);
+ break;
+ case LogLevel.Critical:
+ logBuilder.AppendUtf8("Fatal"u8);
+ break;
+ case LogLevel.None:
+ logBuilder.AppendUtf8("Off"u8);
+ break;
+ default:
+ logBuilder.Append(logLevel.ToString());
+ break;
+ }
+ logBuilder.AppendUtf8("|"u8);
+
+ logBuilder.Append(logName);
+ logBuilder.AppendUtf8("|"u8);
+
+ logBuilder.Append(message);
+ logBuilder.AppendUtf8("| "u8);
+
+ logBuilder.Append(exception?.ToString());
+ logBuilder.AppendLine();
+
+ logBuilder.Flush();
+
+ var result = buffer.ToArray();
+ return result;
+ }
+
+#if APP_REVERSE_PROXY
+ internal static readonly Source Builder = new(
+#if DEBUG
+ AssemblyInfo.Accelerator
+#endif
+ );
+#endif
+
+ internal sealed class Utf8StringLogger(string name) : ILogger
+ {
+ readonly string name = name;
+
+ public IDisposable? BeginScope(TState state) where TState : notnull
+ {
+ return NullScope.Instance;
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ // TODO: 可选加入设置项
+#if DEBUG
+ return true;
+#else
+ return logLevel >= LogLevel.Information;
+#endif
+ }
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel))
+ return;
+
+ if (formatter == null)
+ throw new ArgumentNullException(nameof(formatter));
+
+ var message = formatter(state, exception);
+
+ if (!string.IsNullOrEmpty(message) || exception != null)
+ {
+
+#if APP_REVERSE_PROXY
+ var logMessageU8 = CreateDefaultLogMessageWithNLogFileLayout(logLevel, name, eventId.Id, message, exception);
+ Builder.Append(logMessageU8);
+#else
+ try
+ {
+#if APP_REVERSE_PROXY
+ //var s = IReverseProxyService.Instance;
+ // TODO Ipc
+#else
+ var s = IPCMainProcessService.Instance;
+#endif
+ var logMessage = CreateDefaultLogMessage(name, eventId.Id, message, exception);
+ s.WriteMessage(Utf8StringLoggerProvider.ModuleName, logMessage);
+ }
+ catch
+ {
+ }
+#endif
+ }
+ }
+ }
+
+ ///
+ /// An empty scope without any logic
+ /// https://github.com/dotnet/extensions/blob/v3.1.5/src/Logging/shared/NullScope.cs
+ /// https://github.com/dotnet/runtime/blob/v5.0.0-rtm.20519.4/src/libraries/Common/src/Extensions/Logging/NullScope.cs
+ ///
+ sealed class NullScope : IDisposable
+ {
+ public static NullScope Instance { get; } = new();
+
+ NullScope()
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+
+ internal sealed class Utf8StringLoggerProvider : ILoggerProvider
+ {
+ internal static string? ModuleName { get; private set; }
+
+ public Utf8StringLoggerProvider(string moduleName)
+ {
+ ModuleName = moduleName;
+ }
+
+ public ILogger CreateLogger(string name)
+ {
+ return new Utf8StringLogger(name);
+ }
+
+ void IDisposable.Dispose()
+ {
+ }
+ }
+
+ #endregion
+}
+#endif
diff --git a/src/BD.WTTS.Client/Services/Mvvm/LogConsoleService.cs b/src/BD.WTTS.Client/Services/Mvvm/LogConsoleService.cs
new file mode 100644
index 00000000000..11f4c36329a
--- /dev/null
+++ b/src/BD.WTTS.Client/Services/Mvvm/LogConsoleService.cs
@@ -0,0 +1,98 @@
+#if !ANDROID && !IOS
+using Avalonia.Threading;
+using Utf8StringInterpolation;
+
+// ReSharper disable once CheckNamespace
+namespace BD.WTTS.Services;
+
+public sealed partial class LogConsoleService : ReactiveObject
+{
+ static readonly Lazy mCurrent = new(() => new(), LazyThreadSafetyMode.ExecutionAndPublication);
+
+ public static LogConsoleService Current => mCurrent.Value;
+
+ LogConsoleService()
+ {
+
+ }
+
+ #region UI 进程部分
+
+ readonly string?[] logMessages = new string?[2];
+
+ ///
+ /// 加速进程的日志
+ ///
+ public string? LogMessageAccelerator => logMessages[0];
+
+ ///
+ /// 后端进程的日志
+ ///
+ public string? LogMessageBackEnd => logMessages[1];
+
+ ///
+ /// 日志源
+ ///
+ readonly Dictionary sources = new();
+
+ partial class Source
+ {
+ ///
+ /// 追加收到的日志 UTF-8 字节并触发属性通知
+ ///
+ public void AppendWithPropertyChanged(IReactiveObject o, byte[]? value,
+ string propertyName, ref string? propertyValue)
+ {
+ if (value != null && value.Length > 0)
+ {
+ Append(value);
+
+ var logAllMessage = ToString();
+ propertyValue = logAllMessage;
+
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ o.RaisePropertyChanged(propertyName);
+ });
+
+ }
+ }
+ }
+
+ ///
+ /// 追加收到的日志 UTF-8 字节
+ ///
+ ///
+ ///
+ internal void WriteMessage(string? moduleName, byte[]? value)
+ {
+ if (!string.IsNullOrWhiteSpace(moduleName) &&
+ value != null && value.Length > 0)
+ {
+ if (!sources.TryGetValue(moduleName, out var source))
+ {
+ source = new(
+#if DEBUG
+ moduleName
+#endif
+ );
+ sources.Add(moduleName, source);
+ }
+
+ switch (moduleName)
+ {
+ case AssemblyInfo.Accelerator:
+ source.AppendWithPropertyChanged(this, value,
+ nameof(LogMessageAccelerator), ref logMessages[0]);
+ break;
+ case IPlatformService.IPCRoot.moduleName:
+ source.AppendWithPropertyChanged(this, value,
+ nameof(LogMessageBackEnd), ref logMessages[1]);
+ break;
+ }
+ }
+ }
+
+ #endregion
+}
+#endif
\ No newline at end of file