Files
8damon-Blackbird-Platform/Client/analysis/Services/DebugConsoleService.cs
T
2026-04-03 14:23:15 +10:00

284 lines
8.7 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BlackbirdInterface
{
internal sealed class DebugConsoleEntry
{
public DateTime TimestampLocal { get; init; }
public int ProcessId { get; init; }
public string Source { get; init; } = string.Empty;
public string Message { get; init; } = string.Empty;
public string FilterText => $"{TimestampLocal:O} {ProcessId} {Source} {Message}";
}
internal static class DebugConsoleService
{
private const uint AttachParentProcess = 0xFFFFFFFF;
private const int MaxEntries = 4096;
private static readonly object s_gate = new();
private static readonly Queue<DebugConsoleEntry> s_entries = new();
private static bool s_started;
private static bool s_consoleAttached;
private static bool s_outputCaptureSubscribed;
private static CancellationTokenSource? s_listenerCts;
private static Task? s_listenerTask;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AttachConsole(uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FreeConsole();
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool SetConsoleTitleW(string lpConsoleTitle);
public static event Action<DebugConsoleEntry>? EntryReceived;
public static void Start(bool attachConsole)
{
lock (s_gate)
{
if (s_started)
{
if (attachConsole && !s_consoleAttached)
{
AttachDebugConsole();
}
return;
}
if (attachConsole)
{
AttachDebugConsole();
}
s_started = true;
WriteLocal("debug console online");
if (!s_outputCaptureSubscribed)
{
OutputCapture.LineReceived += OnOutputCaptureLineReceived;
s_outputCaptureSubscribed = true;
}
s_listenerCts = new CancellationTokenSource();
s_listenerTask = Task.Run(() => RunOutputDebugStringListener(s_listenerCts.Token));
}
}
public static void Stop()
{
lock (s_gate)
{
if (!s_started)
{
return;
}
if (s_listenerCts != null)
{
s_listenerCts.Cancel();
}
}
try
{
s_listenerTask?.Wait(1000);
}
catch
{
}
lock (s_gate)
{
if (s_outputCaptureSubscribed)
{
OutputCapture.LineReceived -= OnOutputCaptureLineReceived;
s_outputCaptureSubscribed = false;
}
s_listenerTask = null;
s_listenerCts?.Dispose();
s_listenerCts = null;
s_started = false;
if (s_consoleAttached)
{
_ = FreeConsole();
s_consoleAttached = false;
}
}
}
public static IReadOnlyList<DebugConsoleEntry> Snapshot()
{
lock (s_gate)
{
return s_entries.ToArray();
}
}
public static void WriteLocal(string message)
{
WriteEntry("INTERFACE", Environment.ProcessId, message);
}
private static void OnOutputCaptureLineReceived(string line)
{
WriteEntry("INTERFACE", Environment.ProcessId, line);
}
private static void AttachDebugConsole()
{
if (!AttachConsole(AttachParentProcess))
{
_ = AllocConsole();
}
Console.OutputEncoding = Encoding.UTF8;
Console.InputEncoding = Encoding.UTF8;
_ = SetConsoleTitleW("Blackbird Interface Debug Console");
s_consoleAttached = true;
}
private static void WriteEntry(string source, int processId, string? message)
{
if (!s_started || string.IsNullOrWhiteSpace(message))
{
return;
}
string trimmed = message.TrimEnd('\r', '\n');
var entry = new DebugConsoleEntry
{
TimestampLocal = DateTime.Now,
ProcessId = processId,
Source = source ?? string.Empty,
Message = trimmed
};
lock (s_gate)
{
if (s_entries.Count >= MaxEntries)
{
s_entries.Dequeue();
}
s_entries.Enqueue(entry);
}
try
{
if (s_consoleAttached)
{
Console.WriteLine($"[{entry.TimestampLocal:HH:mm:ss.fff}] [{source}] {trimmed}");
}
}
catch
{
}
EntryReceived?.Invoke(entry);
}
private static void RunOutputDebugStringListener(CancellationToken cancellationToken)
{
const int bufferSize = 4096;
EventWaitHandle? bufferReady = null;
EventWaitHandle? dataReady = null;
try
{
bufferReady = new EventWaitHandle(false, EventResetMode.AutoReset, "DBWIN_BUFFER_READY");
dataReady = new EventWaitHandle(false, EventResetMode.AutoReset, "DBWIN_DATA_READY");
using MemoryMappedFile mapping = MemoryMappedFile.CreateOrOpen("DBWIN_BUFFER", bufferSize, MemoryMappedFileAccess.ReadWrite);
using MemoryMappedViewAccessor accessor = mapping.CreateViewAccessor(0, bufferSize, MemoryMappedFileAccess.Read);
WriteLocal("OutputDebugString listener online");
while (!cancellationToken.IsCancellationRequested)
{
bufferReady.Set();
int signaled = WaitHandle.WaitAny(new WaitHandle[] { dataReady, cancellationToken.WaitHandle }, 250);
if (signaled != 0)
{
continue;
}
int pid = accessor.ReadInt32(0);
byte[] bytes = new byte[bufferSize - sizeof(int)];
accessor.ReadArray(sizeof(int), bytes, 0, bytes.Length);
int terminator = Array.IndexOf(bytes, (byte)0);
if (terminator < 0)
{
terminator = bytes.Length;
}
string text = Encoding.Default.GetString(bytes, 0, terminator).TrimEnd('\r', '\n', '\0');
if (string.IsNullOrWhiteSpace(text))
{
continue;
}
if (pid == Environment.ProcessId)
{
continue;
}
string source = ResolveSourceLabel(pid);
WriteEntry(source, pid, text);
}
}
catch (Exception ex)
{
WriteLocal($"OutputDebugString listener degraded: {ex.Message}");
}
finally
{
bufferReady?.Dispose();
dataReady?.Dispose();
}
}
private static string ResolveSourceLabel(int pid)
{
try
{
using var process = System.Diagnostics.Process.GetProcessById(pid);
string name = process.ProcessName;
if (name.IndexOf("BlackbirdController", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "CONTROLLER";
}
if (name.IndexOf("BlackbirdInterface", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "INTERFACE";
}
return $"{name}:{pid}";
}
catch (Win32Exception)
{
return $"PID:{pid}";
}
catch (InvalidOperationException)
{
return $"PID:{pid}";
}
}
}
}