Files
tbf/AppDiagnostic.Logging/AppDiagnosticTarget.cs
T
marekf 8c77df207a <Feat>: Add universal NLog diagnostics and improve AppDiagnostic UI
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.
2026-09-10 17:45:29 +02:00

234 lines
6.8 KiB
C#

using NLog;
using NLog.Targets;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
namespace AppDiagnostic.Logging
{
[Target("AppDiagnostic")]
public sealed class AppDiagnosticTarget : TargetWithLayout
{
private readonly object connectionLock = new object();
private readonly string applicationName;
private readonly int processId;
private BlockingCollection<DiagnosticLogEntry> queue;
private CancellationTokenSource cancellation;
private Task senderTask;
private NamedPipeClientStream pipeClient;
public AppDiagnosticTarget()
{
using (Process process = Process.GetCurrentProcess())
{
applicationName = process.ProcessName;
processId = process.Id;
}
PipeName = DiagnosticProtocol.DefaultPipeName;
QueueLimit = DiagnosticProtocol.DefaultQueueLimit;
ReconnectDelayMilliseconds =
DiagnosticProtocol.DefaultReconnectDelayMilliseconds;
ConnectTimeoutMilliseconds =
DiagnosticProtocol.DefaultConnectTimeoutMilliseconds;
}
public string PipeName { get; set; }
public int QueueLimit { get; set; }
public int ReconnectDelayMilliseconds { get; set; }
public int ConnectTimeoutMilliseconds { get; set; }
protected override void InitializeTarget()
{
base.InitializeTarget();
int queueLimit = QueueLimit > 0
? QueueLimit
: DiagnosticProtocol.DefaultQueueLimit;
queue = new BlockingCollection<DiagnosticLogEntry>(queueLimit);
cancellation = new CancellationTokenSource();
senderTask = Task.Factory.StartNew(
ProcessQueue,
cancellation.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
}
protected override void Write(LogEventInfo logEvent)
{
if (logEvent == null || queue == null || queue.IsAddingCompleted)
{
return;
}
var entry = new DiagnosticLogEntry
{
TimestampUtc = logEvent.TimeStamp.ToUniversalTime(),
MachineName = Environment.MachineName,
ApplicationName = applicationName,
ProcessId = processId,
LoggerName = logEvent.LoggerName ?? string.Empty,
Level = logEvent.Level?.Name ?? string.Empty,
Message = Layout != null
? Layout.Render(logEvent)
: logEvent.FormattedMessage,
Exception = logEvent.Exception?.ToString() ?? string.Empty
};
try
{
queue.TryAdd(entry);
}
catch (ObjectDisposedException)
{
// A concurrent NLog shutdown may dispose the queue.
}
catch (InvalidOperationException)
{
// A concurrent NLog shutdown may complete the queue.
}
}
protected override void CloseTarget()
{
BlockingCollection<DiagnosticLogEntry> currentQueue = queue;
CancellationTokenSource currentCancellation = cancellation;
if (currentQueue != null)
{
currentQueue.CompleteAdding();
}
if (currentCancellation != null)
{
currentCancellation.Cancel();
}
CloseConnection();
try
{
senderTask?.Wait(2000);
}
catch (AggregateException)
{
// Cancellation during application shutdown is expected.
}
currentQueue?.Dispose();
currentCancellation?.Dispose();
queue = null;
cancellation = null;
senderTask = null;
base.CloseTarget();
}
private void ProcessQueue()
{
CancellationToken token = cancellation.Token;
try
{
foreach (DiagnosticLogEntry entry in
queue.GetConsumingEnumerable(token))
{
SendWhenConnected(entry, token);
}
}
catch (OperationCanceledException)
{
// Target shutdown is expected to cancel the sender.
}
}
private void SendWhenConnected(
DiagnosticLogEntry entry,
CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
EnsureConnected();
DiagnosticMessageSerializer.WriteFramed(pipeClient, entry);
return;
}
catch (Exception exception) when (
exception is IOException ||
exception is TimeoutException ||
exception is InvalidOperationException)
{
CloseConnection();
int delay = ReconnectDelayMilliseconds > 0
? ReconnectDelayMilliseconds
: DiagnosticProtocol.DefaultReconnectDelayMilliseconds;
if (token.WaitHandle.WaitOne(delay))
{
return;
}
}
}
}
private void EnsureConnected()
{
lock (connectionLock)
{
if (pipeClient != null && pipeClient.IsConnected)
{
return;
}
CloseConnectionWithoutLock();
pipeClient = new NamedPipeClientStream(
".",
string.IsNullOrWhiteSpace(PipeName)
? DiagnosticProtocol.DefaultPipeName
: PipeName,
PipeDirection.Out,
PipeOptions.Asynchronous);
int timeout = ConnectTimeoutMilliseconds > 0
? ConnectTimeoutMilliseconds
: DiagnosticProtocol.DefaultConnectTimeoutMilliseconds;
pipeClient.Connect(timeout);
}
}
private void CloseConnection()
{
lock (connectionLock)
{
CloseConnectionWithoutLock();
}
}
private void CloseConnectionWithoutLock()
{
if (pipeClient == null)
{
return;
}
pipeClient.Dispose();
pipeClient = null;
}
}
}