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.
447 lines
14 KiB
C#
447 lines
14 KiB
C#
using AppDiagnostic.Logging;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Windows.Forms;
|
|
|
|
namespace AppDiagnostic
|
|
{
|
|
public partial class MainForm : Form
|
|
{
|
|
private readonly Func<int> connectedClientCountProvider;
|
|
private readonly Func<bool> activationRequestProvider;
|
|
private readonly Timer refreshTimer;
|
|
private readonly Font regularLogFont;
|
|
private readonly Font boldLogFont;
|
|
private List<LogColorRule> colorRules;
|
|
private long lastRenderedVersion = -1;
|
|
private int observedEntryCount;
|
|
private int visibleEntryCount;
|
|
private bool loadingFilters;
|
|
|
|
public MainForm() : this(null, null)
|
|
{
|
|
}
|
|
|
|
public MainForm(Func<int> connectedClientCountProvider, Func<bool> activationRequestProvider)
|
|
{
|
|
this.connectedClientCountProvider = connectedClientCountProvider;
|
|
this.activationRequestProvider = activationRequestProvider;
|
|
InitializeComponent();
|
|
Icon = Properties.Resources.TBF_icon;
|
|
|
|
regularLogFont = new Font(logView.Font, FontStyle.Regular);
|
|
boldLogFont = new Font(logView.Font, FontStyle.Bold);
|
|
colorRules = LogColorRuleRepository.Load();
|
|
|
|
InitializeFilters();
|
|
|
|
refreshTimer = new Timer { Interval = 250 };
|
|
refreshTimer.Tick += refreshTimer_Tick;
|
|
refreshTimer.Start();
|
|
|
|
RefreshView(true);
|
|
}
|
|
|
|
protected override void OnFormClosing(FormClosingEventArgs e)
|
|
{
|
|
refreshTimer.Stop();
|
|
refreshTimer.Dispose();
|
|
regularLogFont.Dispose();
|
|
boldLogFont.Dispose();
|
|
base.OnFormClosing(e);
|
|
}
|
|
|
|
private void InitializeFilters()
|
|
{
|
|
loadingFilters = true;
|
|
levelComboBox.Items.AddRange(new object[] { "All levels", "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL" });
|
|
levelComboBox.SelectedIndex = 0;
|
|
applicationComboBox.Items.Add("All applications");
|
|
applicationComboBox.SelectedIndex = 0;
|
|
loadingFilters = false;
|
|
}
|
|
|
|
private void refreshTimer_Tick(object sender, EventArgs e)
|
|
{
|
|
ProcessActivationRequest();
|
|
RefreshView(false);
|
|
}
|
|
|
|
private void ProcessActivationRequest()
|
|
{
|
|
if (activationRequestProvider == null || !activationRequestProvider())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (WindowState == FormWindowState.Minimized)
|
|
{
|
|
WindowState = FormWindowState.Normal;
|
|
}
|
|
|
|
Show();
|
|
BringToFront();
|
|
Activate();
|
|
}
|
|
|
|
private void RefreshView(bool forceFullRefresh)
|
|
{
|
|
List<DiagnosticLogEntry> snapshot = DiagnosticLogStore.Instance.GetSnapshot();
|
|
UpdateApplicationFilter(snapshot);
|
|
|
|
if (pauseButton.Checked)
|
|
{
|
|
UpdateStatus(snapshot.Count);
|
|
return;
|
|
}
|
|
|
|
long currentVersion = DiagnosticLogStore.Instance.Version;
|
|
if (!forceFullRefresh && currentVersion == lastRenderedVersion)
|
|
{
|
|
UpdateStatus(snapshot.Count);
|
|
return;
|
|
}
|
|
|
|
bool canAppend = !forceFullRefresh && snapshot.Count > observedEntryCount;
|
|
if (canAppend)
|
|
{
|
|
AppendEntries(snapshot.Skip(observedEntryCount));
|
|
}
|
|
else
|
|
{
|
|
RebuildView(snapshot);
|
|
}
|
|
|
|
observedEntryCount = snapshot.Count;
|
|
lastRenderedVersion = currentVersion;
|
|
UpdateStatus(snapshot.Count);
|
|
}
|
|
|
|
private void RebuildView(IEnumerable<DiagnosticLogEntry> entries)
|
|
{
|
|
logView.SuspendLayout();
|
|
logView.Clear();
|
|
visibleEntryCount = 0;
|
|
AppendEntries(entries, false);
|
|
logView.ResumeLayout();
|
|
|
|
if (autoScrollButton.Checked)
|
|
{
|
|
ScrollToEnd();
|
|
}
|
|
}
|
|
|
|
private void AppendEntries(IEnumerable<DiagnosticLogEntry> entries, bool scrollAfterAppend = true)
|
|
{
|
|
int originalSelectionStart = logView.SelectionStart;
|
|
int originalSelectionLength = logView.SelectionLength;
|
|
|
|
foreach (DiagnosticLogEntry entry in entries)
|
|
{
|
|
if (!MatchesFilter(entry))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AppendEntry(entry);
|
|
visibleEntryCount++;
|
|
}
|
|
|
|
if (scrollAfterAppend && autoScrollButton.Checked)
|
|
{
|
|
ScrollToEnd();
|
|
}
|
|
else if (!autoScrollButton.Checked)
|
|
{
|
|
int safeStart = Math.Min(originalSelectionStart, logView.TextLength);
|
|
int safeLength = Math.Min(originalSelectionLength, logView.TextLength - safeStart);
|
|
logView.Select(safeStart, safeLength);
|
|
}
|
|
}
|
|
|
|
private void AppendEntry(DiagnosticLogEntry entry)
|
|
{
|
|
string text = DiagnosticLogFormatter.Format(entry);
|
|
|
|
if (logView.TextLength > 0)
|
|
{
|
|
logView.AppendText(Environment.NewLine);
|
|
}
|
|
|
|
int startIndex = logView.TextLength;
|
|
logView.AppendText(text);
|
|
LogColorizer.Apply(logView, startIndex, text.Length, text, colorRules, regularLogFont, boldLogFont);
|
|
logView.Select(logView.TextLength, 0);
|
|
}
|
|
|
|
private bool MatchesFilter(DiagnosticLogEntry entry)
|
|
{
|
|
string selectedLevel = levelComboBox.SelectedItem as string;
|
|
if (!string.IsNullOrEmpty(selectedLevel) && selectedLevel != "All levels" && !string.Equals(entry.Level, selectedLevel, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string selectedApplication = applicationComboBox.SelectedItem as string;
|
|
if (!string.IsNullOrEmpty(selectedApplication) && selectedApplication != "All applications" && !string.Equals(entry.ApplicationName, selectedApplication, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string filter = filterTextBox.Text.Trim();
|
|
return filter.Length == 0 || DiagnosticLogFormatter.Format(entry).IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
|
|
}
|
|
|
|
private void UpdateApplicationFilter(IEnumerable<DiagnosticLogEntry> entries)
|
|
{
|
|
string selectedApplication = applicationComboBox.SelectedItem as string ?? "All applications";
|
|
List<string> applications = entries.Select(entry => entry.ApplicationName).Where(name => !string.IsNullOrWhiteSpace(name)).Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(name => name).ToList();
|
|
List<string> currentApplications = applicationComboBox.Items.Cast<string>().Skip(1).ToList();
|
|
|
|
if (applications.SequenceEqual(currentApplications, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
loadingFilters = true;
|
|
applicationComboBox.BeginUpdate();
|
|
applicationComboBox.Items.Clear();
|
|
applicationComboBox.Items.Add("All applications");
|
|
applicationComboBox.Items.AddRange(applications.Cast<object>().ToArray());
|
|
applicationComboBox.SelectedItem = applicationComboBox.Items.Cast<string>().FirstOrDefault(item => string.Equals(item, selectedApplication, StringComparison.OrdinalIgnoreCase)) ?? "All applications";
|
|
applicationComboBox.EndUpdate();
|
|
loadingFilters = false;
|
|
}
|
|
|
|
private void UpdateStatus(int storedCount)
|
|
{
|
|
int clients = connectedClientCountProvider?.Invoke() ?? 0;
|
|
receiverStatusLabel.Text = pauseButton.Checked ? $"Paused | Connected sources: {clients}" : $"Listening | Connected sources: {clients}";
|
|
countStatusLabel.Text = $"Received: {DiagnosticLogStore.Instance.TotalReceived:N0} | Stored: {storedCount:N0} | Visible: {visibleEntryCount:N0}";
|
|
}
|
|
|
|
private void ScrollToEnd()
|
|
{
|
|
logView.Select(logView.TextLength, 0);
|
|
logView.ScrollToCaret();
|
|
}
|
|
|
|
private void pauseButton_Click(object sender, EventArgs e)
|
|
{
|
|
pauseButton.Text = pauseButton.Checked ? "Resume" : "Pause";
|
|
|
|
if (!pauseButton.Checked)
|
|
{
|
|
RefreshView(true);
|
|
}
|
|
else
|
|
{
|
|
UpdateStatus(DiagnosticLogStore.Instance.GetSnapshot().Count);
|
|
}
|
|
}
|
|
|
|
private void refreshButton_Click(object sender, EventArgs e)
|
|
{
|
|
RefreshView(true);
|
|
}
|
|
|
|
private void clearButton_Click(object sender, EventArgs e)
|
|
{
|
|
DiagnosticLogStore.Instance.Clear();
|
|
observedEntryCount = 0;
|
|
visibleEntryCount = 0;
|
|
RefreshView(true);
|
|
}
|
|
|
|
private void saveButton_Click(object sender, EventArgs e)
|
|
{
|
|
using (var dialog = new SaveFileDialog())
|
|
{
|
|
dialog.Filter = "Text files (*.txt)|*.txt|Log files (*.log)|*.log|All files (*.*)|*.*";
|
|
dialog.Title = "Save visible diagnostic logs";
|
|
dialog.DefaultExt = "log";
|
|
dialog.FileName = $"AppDiagnostic_{DateTime.Now:yyyyMMdd_HHmmss}.log";
|
|
|
|
if (dialog.ShowDialog(this) != DialogResult.OK)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
File.WriteAllText(dialog.FileName, logView.Text);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
MessageBox.Show(this, $"Unable to save diagnostic logs: {exception.Message}", "Save error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void copyButton_Click(object sender, EventArgs e)
|
|
{
|
|
CopySelectionOrCurrentLine();
|
|
}
|
|
|
|
private void copySelectionMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
if (logView.SelectionLength > 0)
|
|
{
|
|
CopyText(logView.SelectedText);
|
|
}
|
|
}
|
|
|
|
private void copyLineMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
CopyCurrentLine();
|
|
}
|
|
|
|
private void copyAllButton_Click(object sender, EventArgs e)
|
|
{
|
|
CopyText(logView.Text);
|
|
}
|
|
|
|
private void CopySelectionOrCurrentLine()
|
|
{
|
|
if (logView.SelectionLength > 0)
|
|
{
|
|
CopyText(logView.SelectedText);
|
|
}
|
|
else
|
|
{
|
|
CopyCurrentLine();
|
|
}
|
|
}
|
|
|
|
private void CopyCurrentLine()
|
|
{
|
|
if (logView.Lines.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int lineIndex = logView.GetLineFromCharIndex(logView.SelectionStart);
|
|
if (lineIndex >= 0 && lineIndex < logView.Lines.Length)
|
|
{
|
|
CopyText(logView.Lines[lineIndex]);
|
|
}
|
|
}
|
|
|
|
private void CopyText(string text)
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
Clipboard.SetText(text);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
MessageBox.Show(this, $"Unable to copy text: {exception.Message}", "Clipboard error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
private void wrapButton_Click(object sender, EventArgs e)
|
|
{
|
|
logView.WordWrap = wrapButton.Checked;
|
|
}
|
|
|
|
private void alwaysOnTopButton_Click(object sender, EventArgs e)
|
|
{
|
|
TopMost = alwaysOnTopButton.Checked;
|
|
}
|
|
|
|
private void colorRulesButton_Click(object sender, EventArgs e)
|
|
{
|
|
using (var editor = new ColorRuleEditorForm(colorRules))
|
|
{
|
|
if (editor.ShowDialog(this) != DialogResult.OK)
|
|
{
|
|
return;
|
|
}
|
|
|
|
colorRules = editor.Rules;
|
|
|
|
try
|
|
{
|
|
LogColorRuleRepository.Save(colorRules);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
MessageBox.Show(this, $"Unable to save color rules: {exception.Message}", "Settings error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
}
|
|
|
|
RefreshView(true);
|
|
}
|
|
}
|
|
|
|
private void applyFilterButton_Click(object sender, EventArgs e)
|
|
{
|
|
RefreshView(true);
|
|
}
|
|
|
|
private void clearFilterButton_Click(object sender, EventArgs e)
|
|
{
|
|
loadingFilters = true;
|
|
filterTextBox.Clear();
|
|
levelComboBox.SelectedIndex = 0;
|
|
applicationComboBox.SelectedIndex = 0;
|
|
loadingFilters = false;
|
|
RefreshView(true);
|
|
}
|
|
|
|
private void filterValueChanged(object sender, EventArgs e)
|
|
{
|
|
if (!loadingFilters)
|
|
{
|
|
RefreshView(true);
|
|
}
|
|
}
|
|
|
|
private void filterTextBox_KeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
if (e.KeyCode == Keys.Enter)
|
|
{
|
|
RefreshView(true);
|
|
e.SuppressKeyPress = true;
|
|
}
|
|
else if (e.KeyCode == Keys.Escape)
|
|
{
|
|
clearFilterButton_Click(sender, EventArgs.Empty);
|
|
e.SuppressKeyPress = true;
|
|
}
|
|
}
|
|
|
|
private void closeButton_Click(object sender, EventArgs e)
|
|
{
|
|
Close();
|
|
}
|
|
|
|
private void MainForm_KeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
if (e.Control && e.KeyCode == Keys.F)
|
|
{
|
|
filterTextBox.Focus();
|
|
filterTextBox.SelectAll();
|
|
e.SuppressKeyPress = true;
|
|
}
|
|
else if (e.Control && e.KeyCode == Keys.S)
|
|
{
|
|
saveButton_Click(sender, EventArgs.Empty);
|
|
e.SuppressKeyPress = true;
|
|
}
|
|
else if (e.KeyCode == Keys.F5)
|
|
{
|
|
RefreshView(true);
|
|
e.SuppressKeyPress = true;
|
|
}
|
|
}
|
|
}
|
|
}
|