Cause: - Live diagnostics required logging from multiple applications without blocking TBF. - Shared nlog.config files caused configuration conflicts. - The original viewer lacked configurable colors and practical log controls. - Inconsistent diagnostic deployment caused log transfer failures and locked shared DLLs during builds. Solution: 1. Added universal diagnostic transport - Added AppDiagnostic.Logging with an NLog target and named-pipe communication. - Added background buffering and automatic reconnection. - Registered the diagnostic connector programmatically. 2. Separated diagnostic window lifecycle - Run AppDiagnostic as an independent process. - Activate the existing window on repeated launches. - Keep TBF running when the diagnostic window closes. 3. Corrected diagnostic deployment - Copy the complete diagnostic output into a dedicated AppDiagnostic subdirectory during build. - Overwrite existing deployment files to prevent inconsistent versions. - Validate and launch the executable from that subdirectory with an explicit working directory. 4. Improved the log viewer - Added configurable substring colors with a palette and live preview. - Added text, level and application filters. - Added line wrapping, clipboard actions and log export. - Added Pause/Resume, Refresh, Clear, auto-scroll and Always on top. - Added connection and log counters. - Persist color rules between sessions. 5. Removed legacy dependencies - Replaced LiveLogCache with structured diagnostic storage. - Removed AppDiagnostic dependencies on log4net and SharedComponents. Validation: - Confirmed successful log transfer when launching AppDiagnostic from TBF.
165 lines
4.6 KiB
C#
165 lines
4.6 KiB
C#
using AppDiagnostic.Logging;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.IO.Pipes;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace AppDiagnostic
|
|
{
|
|
public sealed class DiagnosticPipeServer : IDisposable
|
|
{
|
|
private readonly Action<DiagnosticLogEntry> messageReceived;
|
|
private readonly CancellationTokenSource cancellation = new CancellationTokenSource();
|
|
private readonly object syncRoot = new object();
|
|
private readonly List<NamedPipeServerStream> connectedClients = new List<NamedPipeServerStream>();
|
|
private NamedPipeServerStream waitingServer;
|
|
private Task serverTask;
|
|
|
|
public DiagnosticPipeServer(Action<DiagnosticLogEntry> messageReceived)
|
|
{
|
|
this.messageReceived = messageReceived ?? throw new ArgumentNullException(nameof(messageReceived));
|
|
}
|
|
|
|
public int ConnectedClientCount
|
|
{
|
|
get
|
|
{
|
|
lock (syncRoot)
|
|
{
|
|
return connectedClients.Count;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Start()
|
|
{
|
|
if (serverTask != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
serverTask = Task.Run((Action)AcceptConnections);
|
|
}
|
|
|
|
private void AcceptConnections()
|
|
{
|
|
while (!cancellation.IsCancellationRequested)
|
|
{
|
|
NamedPipeServerStream server = null;
|
|
|
|
try
|
|
{
|
|
server = new NamedPipeServerStream(DiagnosticProtocol.DefaultPipeName, PipeDirection.In, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
|
|
|
|
lock (syncRoot)
|
|
{
|
|
waitingServer = server;
|
|
}
|
|
|
|
server.WaitForConnection();
|
|
|
|
lock (syncRoot)
|
|
{
|
|
waitingServer = null;
|
|
connectedClients.Add(server);
|
|
}
|
|
|
|
NamedPipeServerStream connectedServer = server;
|
|
server = null;
|
|
Task.Run(() => ReadClient(connectedServer));
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
if (!cancellation.IsCancellationRequested)
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
catch (IOException)
|
|
{
|
|
if (!cancellation.IsCancellationRequested)
|
|
{
|
|
Thread.Sleep(250);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
lock (syncRoot)
|
|
{
|
|
if (ReferenceEquals(waitingServer, server))
|
|
{
|
|
waitingServer = null;
|
|
}
|
|
}
|
|
|
|
server?.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ReadClient(NamedPipeServerStream client)
|
|
{
|
|
try
|
|
{
|
|
while (client.IsConnected && !cancellation.IsCancellationRequested)
|
|
{
|
|
DiagnosticLogEntry entry = DiagnosticMessageSerializer.ReadFramed(client);
|
|
messageReceived(entry);
|
|
}
|
|
}
|
|
catch (EndOfStreamException)
|
|
{
|
|
// The producer application was closed.
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// The producer application disconnected.
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
// The diagnostic application is shutting down.
|
|
}
|
|
finally
|
|
{
|
|
lock (syncRoot)
|
|
{
|
|
connectedClients.Remove(client);
|
|
}
|
|
|
|
client.Dispose();
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
cancellation.Cancel();
|
|
|
|
lock (syncRoot)
|
|
{
|
|
waitingServer?.Dispose();
|
|
waitingServer = null;
|
|
|
|
foreach (NamedPipeServerStream client in connectedClients.ToArray())
|
|
{
|
|
client.Dispose();
|
|
}
|
|
|
|
connectedClients.Clear();
|
|
}
|
|
|
|
try
|
|
{
|
|
serverTask?.Wait(2000);
|
|
}
|
|
catch (AggregateException)
|
|
{
|
|
// Cancellation during shutdown is expected.
|
|
}
|
|
|
|
cancellation.Dispose();
|
|
}
|
|
}
|
|
}
|