Compare commits
93
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae787622d2 | ||
|
|
d248630ddc | ||
|
|
b15d0a0a08 | ||
|
|
683ca25aaf | ||
|
|
d3cc50b6e8 | ||
|
|
8c3c866eff | ||
|
|
78907de455 | ||
|
|
500adfa789 | ||
|
|
9658fc28bd | ||
|
|
42ad98ab1f | ||
|
|
bd0a876b5b | ||
|
|
806cf0d670 | ||
|
|
82a51293b9 | ||
|
|
4cd7867e04 | ||
|
|
6560278568 | ||
|
|
08a233b2c5 | ||
|
|
7aa92d4806 | ||
|
|
468bcf786e | ||
|
|
21324985db | ||
|
|
4529602d97 | ||
|
|
bce5af56e0 | ||
|
|
f8643c4de1 | ||
|
|
161b20fea7 | ||
|
|
882e894fba | ||
|
|
79c87bedfd | ||
|
|
b1324c567c | ||
|
|
d2627be55b | ||
|
|
2ba6092385 | ||
|
|
fee0651595 | ||
|
|
58fc16d70b | ||
|
|
ddf8996f01 | ||
|
|
0c2cc0dad2 | ||
|
|
1e0c65f213 | ||
|
|
c53a2d9078 | ||
|
|
6223bc925f | ||
|
|
aca1d79023 | ||
|
|
45d12dea6f | ||
|
|
2cee6cb0f2 | ||
|
|
e9cda221bb | ||
|
|
c6181a76ec | ||
|
|
cd24373c80 | ||
|
|
a464b1fc21 | ||
|
|
b8f6af7112 | ||
|
|
1c8d01562f | ||
|
|
2608ee4de6 | ||
|
|
e63f706f41 | ||
|
|
94cd07cd3c | ||
|
|
967ca817ba | ||
|
|
979637cc07 | ||
|
|
5c7ff08a78 | ||
|
|
a64d1a82ed | ||
|
|
bf1766f90a | ||
|
|
1b789ad5d5 | ||
|
|
a7404d0e24 | ||
|
|
a64327ac36 | ||
|
|
c886303458 | ||
|
|
38003592df | ||
|
|
82c2b17a1f | ||
|
|
2660c564a6 | ||
|
|
7e2b4ed908 | ||
|
|
c1960f2150 | ||
|
|
4124443f36 | ||
|
|
6bab62a062 | ||
|
|
c426df990c | ||
|
|
20614fb9e6 | ||
|
|
302431b64c | ||
|
|
db34c579d0 | ||
|
|
83afddc918 | ||
|
|
cf77faf972 | ||
|
|
684513ee59 | ||
|
|
45a6d59b5e | ||
|
|
73c8fecdf2 | ||
|
|
8c77df207a | ||
|
|
1c6f9af2bb | ||
|
|
0cfde7f06b | ||
|
|
d3c361b544 | ||
|
|
b35b7d92f9 | ||
|
|
eb4f7ad971 | ||
|
|
8677e49680 | ||
|
|
29faef86e3 | ||
|
|
6ef42fcc21 | ||
|
|
d2d41df155 | ||
|
|
6d2477be4a | ||
|
|
57152ae749 | ||
|
|
41408b30da | ||
|
|
136be96e1b | ||
|
|
0258e54b4e | ||
|
|
b8e54e1fc9 | ||
|
|
8d42232589 | ||
|
|
e4d4d75642 | ||
|
|
2b5a0ffe14 | ||
|
|
8157a752bd | ||
|
|
4dd805bb1c |
+64
-1
@@ -1,3 +1,10 @@
|
||||
# Generated build outputs for all projects, including Debug/Release archives.
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
# Generated test results and hardware diagnostic captures.
|
||||
[Tt]est[Rr]esults/
|
||||
|
||||
AppDiagnostic/bin/
|
||||
AppDiagnostic/obj/
|
||||
Common/bin/
|
||||
@@ -93,7 +100,8 @@ Users/obj/
|
||||
TBF.sln.DotSettings.user
|
||||
/ExternalProjects
|
||||
/GenesisCordonelTester
|
||||
/packages
|
||||
/packages/*
|
||||
!/packages/Common/
|
||||
/GenesisCordonelInterface/bin/Debug/GenesisCordonelInterfaceLogger.log
|
||||
/GenesisCordonelInterface/bin/Debug/GenesisCordonelTesterLogger.log
|
||||
/GenesisCordonelInterface/bin/Debug/SirtConfig.json
|
||||
@@ -101,3 +109,58 @@ TBF.sln.DotSettings.user
|
||||
/GenesisCordonelInterface/obj/Debug
|
||||
/GenesisCordonelInterface/obj/Release
|
||||
/ExternalProjects
|
||||
/AppDiagnostic.Logging/bin/Debug/net472/AppDiagnostic.Logging.dll
|
||||
/AppDiagnostic.Logging/bin/Debug/net472/AppDiagnostic.Logging.pdb
|
||||
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.AssemblyInfo.cs
|
||||
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.AssemblyInfoInputs.cache
|
||||
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.csproj.AssemblyReference.cache
|
||||
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.dll
|
||||
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Metrology.Measurements.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Metrology.Measurements.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Ui.CordonelPreadjustmentUi.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Ui.CordonelPreadjustmentUi.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Ui.GenesisToolBox.exe
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Ui.GenesisToolBox.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/XylemCommonUiLegacyGenCtl.dll
|
||||
/GenesisCordonelInterface/bin/Debug/XylemCommonUiLegacyGenCtl.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/de/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisFile.resources.dll
|
||||
/GenesisCordonelInterface/bin/Debug/GenesisCordonelInterface.exe
|
||||
/GenesisCordonelInterface/bin/Debug/GenesisCordonelInterface.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/TempFlansh.exe
|
||||
/GenesisCordonelInterface/bin/Debug/TempFlansh.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.Interfaces.Protocols.ProtocolCore.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.Interfaces.Protocols.ProtocolCore.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.Interfaces.Protocols.TransmitProtocol.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.Interfaces.Protocols.TransmitProtocol.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.eRegister.eRegisterCore.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.eRegister.eRegisterCore.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisFile.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisFile.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisPwd.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisPwd.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.RequestProtocol.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.RequestProtocol.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.StreamingProtocol.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.StreamingProtocol.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.MagFlux.MagFluxConfig.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.MagFlux.MagFluxConfig.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.MagFlux.MagFluxCore.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.MagFlux.MagFluxCore.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Logic.RelatePcb.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Logic.RelatePcb.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Logic.ServiceCore.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Logic.ServiceCore.pdb
|
||||
/GenesisCordonelInterface/RuntimePackage/Package
|
||||
/SharedComponents/obj/SharedComponents.csproj.nuget.g.props
|
||||
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.AssemblyInfo.cs
|
||||
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.AssemblyInfoInputs.cache
|
||||
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.csproj.AssemblyReference.cache
|
||||
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.dll
|
||||
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.pdb
|
||||
/AppDiagnostic.Logging/bin/Release/net472/AppDiagnostic.Logging.dll
|
||||
/AppDiagnostic.Logging/bin/Release/net472/AppDiagnostic.Logging.pdb
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<RootNamespace>AppDiagnostic.Logging</RootNamespace>
|
||||
<AssemblyName>AppDiagnostic.Logging</AssemblyName>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<ProjectGuid>{A7AF8ED2-362E-4DBB-B607-EBA2820719EC}</ProjectGuid>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NLog" Version="5.2.2" />
|
||||
<Reference Include="System.Runtime.Serialization" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,41 @@
|
||||
using NLog;
|
||||
using NLog.Config;
|
||||
using NLog.Layouts;
|
||||
|
||||
namespace AppDiagnostic.Logging
|
||||
{
|
||||
public static class AppDiagnosticConnector
|
||||
{
|
||||
private static readonly object syncRoot = new object();
|
||||
|
||||
public static void Register()
|
||||
{
|
||||
lock (syncRoot)
|
||||
{
|
||||
LoggingConfiguration configuration = LogManager.Configuration ?? new LoggingConfiguration();
|
||||
NLog.Targets.Target existingTarget = configuration.FindTargetByName("appDiagnostic");
|
||||
|
||||
if (existingTarget != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var target = new AppDiagnosticTarget
|
||||
{
|
||||
Name = "appDiagnostic",
|
||||
PipeName = DiagnosticProtocol.DefaultPipeName,
|
||||
QueueLimit = DiagnosticProtocol.DefaultQueueLimit,
|
||||
ReconnectDelayMilliseconds = DiagnosticProtocol.DefaultReconnectDelayMilliseconds,
|
||||
ConnectTimeoutMilliseconds = DiagnosticProtocol.DefaultConnectTimeoutMilliseconds,
|
||||
Layout = new SimpleLayout("${message}")
|
||||
};
|
||||
|
||||
configuration.AddTarget(target.Name, target);
|
||||
configuration.AddRule(LogLevel.Trace, LogLevel.Fatal, target, "*");
|
||||
|
||||
LogManager.Configuration = configuration;
|
||||
LogManager.ReconfigExistingLoggers();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace AppDiagnostic.Logging
|
||||
{
|
||||
[DataContract]
|
||||
public sealed class DiagnosticLogEntry
|
||||
{
|
||||
[DataMember(Order = 1)]
|
||||
public DateTime TimestampUtc { get; set; }
|
||||
|
||||
[DataMember(Order = 2)]
|
||||
public string MachineName { get; set; }
|
||||
|
||||
[DataMember(Order = 3)]
|
||||
public string ApplicationName { get; set; }
|
||||
|
||||
[DataMember(Order = 4)]
|
||||
public int ProcessId { get; set; }
|
||||
|
||||
[DataMember(Order = 5)]
|
||||
public string LoggerName { get; set; }
|
||||
|
||||
[DataMember(Order = 6)]
|
||||
public string Level { get; set; }
|
||||
|
||||
[DataMember(Order = 7)]
|
||||
public string Message { get; set; }
|
||||
|
||||
[DataMember(Order = 8)]
|
||||
public string Exception { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization.Json;
|
||||
|
||||
namespace AppDiagnostic.Logging
|
||||
{
|
||||
public static class DiagnosticMessageSerializer
|
||||
{
|
||||
private static readonly DataContractJsonSerializer Serializer =
|
||||
new DataContractJsonSerializer(typeof(DiagnosticLogEntry));
|
||||
|
||||
private static readonly object SerializerLock = new object();
|
||||
|
||||
public static byte[] Serialize(DiagnosticLogEntry entry)
|
||||
{
|
||||
if (entry == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(entry));
|
||||
}
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
lock (SerializerLock)
|
||||
{
|
||||
Serializer.WriteObject(stream, entry);
|
||||
}
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static DiagnosticLogEntry Deserialize(byte[] payload)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(payload));
|
||||
}
|
||||
|
||||
if (payload.Length == 0 ||
|
||||
payload.Length > DiagnosticProtocol.MaximumMessageSize)
|
||||
{
|
||||
throw new InvalidDataException("Invalid diagnostic message size.");
|
||||
}
|
||||
|
||||
using (var stream = new MemoryStream(payload, false))
|
||||
{
|
||||
lock (SerializerLock)
|
||||
{
|
||||
return (DiagnosticLogEntry)Serializer.ReadObject(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteFramed(Stream stream, DiagnosticLogEntry entry)
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
}
|
||||
|
||||
byte[] payload = Serialize(entry);
|
||||
|
||||
if (payload.Length > DiagnosticProtocol.MaximumMessageSize)
|
||||
{
|
||||
throw new InvalidDataException("Diagnostic message is too large.");
|
||||
}
|
||||
|
||||
byte[] length = BitConverter.GetBytes(payload.Length);
|
||||
stream.Write(length, 0, length.Length);
|
||||
stream.Write(payload, 0, payload.Length);
|
||||
stream.Flush();
|
||||
}
|
||||
|
||||
public static DiagnosticLogEntry ReadFramed(Stream stream)
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
}
|
||||
|
||||
byte[] lengthBuffer = ReadExactly(stream, sizeof(int));
|
||||
int payloadLength = BitConverter.ToInt32(lengthBuffer, 0);
|
||||
|
||||
if (payloadLength <= 0 ||
|
||||
payloadLength > DiagnosticProtocol.MaximumMessageSize)
|
||||
{
|
||||
throw new InvalidDataException("Invalid diagnostic message size.");
|
||||
}
|
||||
|
||||
return Deserialize(ReadExactly(stream, payloadLength));
|
||||
}
|
||||
|
||||
private static byte[] ReadExactly(Stream stream, int count)
|
||||
{
|
||||
byte[] buffer = new byte[count];
|
||||
int offset = 0;
|
||||
|
||||
while (offset < count)
|
||||
{
|
||||
int bytesRead = stream.Read(buffer, offset, count - offset);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
throw new EndOfStreamException();
|
||||
}
|
||||
|
||||
offset += bytesRead;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace AppDiagnostic.Logging
|
||||
{
|
||||
public static class DiagnosticProtocol
|
||||
{
|
||||
public const string DefaultPipeName = "Sensus.AppDiagnostic.LogPipe";
|
||||
public const int MaximumMessageSize = 1024 * 1024;
|
||||
public const int DefaultQueueLimit = 10000;
|
||||
public const int DefaultReconnectDelayMilliseconds = 500;
|
||||
public const int DefaultConnectTimeoutMilliseconds = 250;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# AppDiagnostic.Logging
|
||||
|
||||
Reusable NLog target for sending structured diagnostic events from multiple
|
||||
.NET Framework processes to `AppDiagnostic.exe` through a local named pipe.
|
||||
|
||||
## Producer integration
|
||||
|
||||
Reference this project from the producer application and add the extension and
|
||||
target to its NLog configuration:
|
||||
|
||||
```xml
|
||||
<extensions>
|
||||
<add assembly="AppDiagnostic.Logging" />
|
||||
</extensions>
|
||||
|
||||
<targets>
|
||||
<target name="appDiagnostic"
|
||||
xsi:type="AppDiagnostic"
|
||||
layout="${message}" />
|
||||
</targets>
|
||||
|
||||
<rules>
|
||||
<logger name="*" minlevel="Trace" writeTo="appDiagnostic" />
|
||||
</rules>
|
||||
```
|
||||
|
||||
The target writes to a bounded background queue. Producer threads are not
|
||||
blocked while the diagnostic viewer is unavailable. Queued messages are sent
|
||||
after the viewer becomes available; new messages are discarded when the queue
|
||||
limit is reached.
|
||||
|
||||
The receiver can read messages with:
|
||||
|
||||
```csharp
|
||||
DiagnosticLogEntry entry =
|
||||
DiagnosticMessageSerializer.ReadFramed(namedPipeServerStream);
|
||||
```
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"format": 1,
|
||||
"restore": {
|
||||
"C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
|
||||
"projectName": "AppDiagnostic.Logging",
|
||||
"projectPath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
|
||||
"packagesPath": "C:\\Users\\MFRNIAK\\.nuget\\packages\\",
|
||||
"outputPath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"fallbackFolders": [
|
||||
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
|
||||
],
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\MFRNIAK\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net472"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net472": {
|
||||
"framework": "net472",
|
||||
"targetAlias": "net472",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.300"
|
||||
},
|
||||
"frameworks": {
|
||||
"net472": {
|
||||
"framework": "net472",
|
||||
"targetAlias": "net472",
|
||||
"dependencies": {
|
||||
"NLog": {
|
||||
"target": "Package",
|
||||
"version": "[5.2.2, )"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.303\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -5,11 +5,12 @@
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\micha\.nuget\packages\</NuGetPackageFolders>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\MFRNIAK\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.9.2</NuGetToolVersion>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="C:\Users\micha\.nuget\packages\" />
|
||||
<SourceRoot Include="C:\Users\MFRNIAK\.nuget\packages\" />
|
||||
<SourceRoot Include="C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
is_global = true
|
||||
build_property.RootNamespace = AppDiagnostic.Logging
|
||||
build_property.ProjectDir = C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||
build_property.EffectiveAnalysisLevelStyle =
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
551ba94934db30a277a1a9cec2dacbe03f8393107e5d0b2436695f9bd62bd569
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Debug\net472\AppDiagnostic.Logging.dll
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Debug\net472\AppDiagnostic.Logging.pdb
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Debug\net472\NLog.dll
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.csproj.AssemblyReference.cache
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.GeneratedMSBuildEditorConfig.editorconfig
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.AssemblyInfoInputs.cache
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.AssemblyInfo.cs
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.csproj.CoreCompileInputs.cache
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagn.CFEA7AC0.Up2Date
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.dll
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.pdb
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
is_global = true
|
||||
build_property.RootNamespace = AppDiagnostic.Logging
|
||||
build_property.ProjectDir = C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||
build_property.EffectiveAnalysisLevelStyle =
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
a53d59482cad49b656138b1af5cf3e55edf2108969aedc574c362be6c671436b
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Release\net472\AppDiagnostic.Logging.dll
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Release\net472\AppDiagnostic.Logging.pdb
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Release\net472\NLog.dll
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.csproj.AssemblyReference.cache
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.GeneratedMSBuildEditorConfig.editorconfig
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.AssemblyInfoInputs.cache
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.AssemblyInfo.cs
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.csproj.CoreCompileInputs.cache
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagn.CFEA7AC0.Up2Date
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.dll
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.pdb
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"version": 4,
|
||||
"targets": {
|
||||
"net472": {
|
||||
"NLog/5.2.2": {
|
||||
"type": "package",
|
||||
"frameworkAssemblies": [
|
||||
"System",
|
||||
"System.Configuration",
|
||||
"System.Core",
|
||||
"System.IO.Compression",
|
||||
"System.Xml"
|
||||
],
|
||||
"compile": {
|
||||
"lib/net46/NLog.dll": {
|
||||
"related": ".xml"
|
||||
}
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net46/NLog.dll": {
|
||||
"related": ".xml"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"NLog/5.2.2": {
|
||||
"sha512": "r6Q9740g29gTwmTWlsgdIFm0mhNsfNZmbvWKX/Fxmi8X89ZrpUowHM2T2X1lP7RVpND+ef+XnfKL5g6Q1iNGXA==",
|
||||
"type": "package",
|
||||
"path": "nlog/5.2.2",
|
||||
"files": [
|
||||
".nupkg.metadata",
|
||||
".signature.p7s",
|
||||
"N.png",
|
||||
"lib/net35/NLog.dll",
|
||||
"lib/net35/NLog.xml",
|
||||
"lib/net45/NLog.dll",
|
||||
"lib/net45/NLog.xml",
|
||||
"lib/net46/NLog.dll",
|
||||
"lib/net46/NLog.xml",
|
||||
"lib/netstandard1.3/NLog.dll",
|
||||
"lib/netstandard1.3/NLog.xml",
|
||||
"lib/netstandard1.5/NLog.dll",
|
||||
"lib/netstandard1.5/NLog.xml",
|
||||
"lib/netstandard2.0/NLog.dll",
|
||||
"lib/netstandard2.0/NLog.xml",
|
||||
"nlog.5.2.2.nupkg.sha512",
|
||||
"nlog.nuspec"
|
||||
]
|
||||
}
|
||||
},
|
||||
"projectFileDependencyGroups": {
|
||||
"net472": [
|
||||
"NLog >= 5.2.2"
|
||||
]
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\MFRNIAK\\.nuget\\packages\\": {},
|
||||
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {}
|
||||
},
|
||||
"project": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
|
||||
"projectName": "AppDiagnostic.Logging",
|
||||
"projectPath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
|
||||
"packagesPath": "C:\\Users\\MFRNIAK\\.nuget\\packages\\",
|
||||
"outputPath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"fallbackFolders": [
|
||||
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
|
||||
],
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\MFRNIAK\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net472"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net472": {
|
||||
"framework": "net472",
|
||||
"targetAlias": "net472",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.300"
|
||||
},
|
||||
"frameworks": {
|
||||
"net472": {
|
||||
"framework": "net472",
|
||||
"targetAlias": "net472",
|
||||
"dependencies": {
|
||||
"NLog": {
|
||||
"target": "Package",
|
||||
"version": "[5.2.2, )"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.303\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "ezwKOPZAAXU=",
|
||||
"success": true,
|
||||
"projectFilePath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
|
||||
"expectedPackageFiles": [
|
||||
"C:\\Users\\MFRNIAK\\.nuget\\packages\\nlog\\5.2.2\\nlog.5.2.2.nupkg.sha512"
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
@@ -3,17 +3,12 @@
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
||||
</startup>
|
||||
<log4net>
|
||||
<appender name="LogCacheAppender" type="Namespace.LogCacheAppender, AssemblyName">
|
||||
<param name="Cache" value="YourCacheInstance" />
|
||||
<layout type="log4net.Layout.PatternLayout">
|
||||
<param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<root>
|
||||
<level value="DEBUG" />
|
||||
<appender-ref ref="LogCacheAppender" />
|
||||
</root>
|
||||
</log4net>
|
||||
</configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
|
||||
@@ -33,9 +33,6 @@
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="log4net, Version=3.0.3.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\log4net.3.0.3\lib\net462\log4net.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Core" />
|
||||
@@ -51,12 +48,15 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="BufferedListView.cs">
|
||||
<SubType>Component</SubType>
|
||||
<Compile Include="ColorRuleEditorForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="DiagApi.cs" />
|
||||
<Compile Include="LogAdapter.cs" />
|
||||
<Compile Include="LogFilter.cs" />
|
||||
<Compile Include="DiagnosticLogFormatter.cs" />
|
||||
<Compile Include="DiagnosticLogStore.cs" />
|
||||
<Compile Include="DiagnosticPipeServer.cs" />
|
||||
<Compile Include="LogColorizer.cs" />
|
||||
<Compile Include="LogColorRule.cs" />
|
||||
<Compile Include="LogColorRuleRepository.cs" />
|
||||
<Compile Include="MainForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
@@ -78,7 +78,6 @@
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
</Compile>
|
||||
<None Include="packages.config" />
|
||||
<None Include="Properties\Settings.settings">
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
@@ -93,9 +92,9 @@
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharedComponents\SharedComponents.csproj">
|
||||
<Project>{8f942729-f454-4c99-ba6c-746962065ae3}</Project>
|
||||
<Name>SharedComponents</Name>
|
||||
<ProjectReference Include="..\AppDiagnostic.Logging\AppDiagnostic.Logging.csproj">
|
||||
<Project>{A7AF8ED2-362E-4DBB-B607-EBA2820719EC}</Project>
|
||||
<Name>AppDiagnostic.Logging</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
public sealed class ColorRuleEditorForm : Form
|
||||
{
|
||||
private readonly List<LogColorRule> rules;
|
||||
private readonly ListBox ruleList = new ListBox();
|
||||
private readonly TextBox patternTextBox = new TextBox();
|
||||
private readonly CheckBox enabledCheckBox = new CheckBox();
|
||||
private readonly CheckBox boldCheckBox = new CheckBox();
|
||||
private readonly CheckBox backgroundCheckBox = new CheckBox();
|
||||
private readonly Button foregroundButton = new Button();
|
||||
private readonly Button backgroundButton = new Button();
|
||||
private readonly RichTextBox previewBox = new RichTextBox();
|
||||
private readonly Font previewBoldFont;
|
||||
private bool loadingRule;
|
||||
|
||||
public ColorRuleEditorForm(IEnumerable<LogColorRule> sourceRules)
|
||||
{
|
||||
rules = sourceRules.Select(rule => rule.Clone()).ToList();
|
||||
InitializeWindow();
|
||||
previewBoldFont = new Font(previewBox.Font, FontStyle.Bold);
|
||||
RefreshRuleList(0);
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
public List<LogColorRule> Rules => rules.Select(rule => rule.Clone()).ToList();
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
previewBoldFont.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void InitializeWindow()
|
||||
{
|
||||
Text = "Log color rules";
|
||||
Icon = Properties.Resources.TBF_icon;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
MinimumSize = new Size(760, 500);
|
||||
ClientSize = new Size(860, 560);
|
||||
|
||||
var root = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 3, Padding = new Padding(10) };
|
||||
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 145F));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 42F));
|
||||
|
||||
var split = new SplitContainer { Dock = DockStyle.Fill, SplitterDistance = 280 };
|
||||
split.Panel1.Padding = new Padding(0, 0, 8, 0);
|
||||
split.Panel2.Padding = new Padding(8, 0, 0, 0);
|
||||
|
||||
ruleList.Dock = DockStyle.Fill;
|
||||
ruleList.IntegralHeight = false;
|
||||
ruleList.SelectedIndexChanged += RuleList_SelectedIndexChanged;
|
||||
|
||||
var listButtons = new FlowLayoutPanel { Dock = DockStyle.Bottom, Height = 36, FlowDirection = FlowDirection.LeftToRight };
|
||||
listButtons.Controls.Add(CreateButton("Add", AddRule_Click));
|
||||
listButtons.Controls.Add(CreateButton("Delete", DeleteRule_Click));
|
||||
listButtons.Controls.Add(CreateButton("Up", MoveUp_Click));
|
||||
listButtons.Controls.Add(CreateButton("Down", MoveDown_Click));
|
||||
split.Panel1.Controls.Add(ruleList);
|
||||
split.Panel1.Controls.Add(listButtons);
|
||||
|
||||
var editor = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, ColumnCount = 2, Padding = new Padding(4) };
|
||||
editor.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
|
||||
editor.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
|
||||
enabledCheckBox.Text = "Rule enabled";
|
||||
enabledCheckBox.AutoSize = true;
|
||||
enabledCheckBox.CheckedChanged += EditorValueChanged;
|
||||
AddEditorRow(editor, "State", enabledCheckBox);
|
||||
|
||||
patternTextBox.Dock = DockStyle.Fill;
|
||||
patternTextBox.TextChanged += EditorValueChanged;
|
||||
AddEditorRow(editor, "Substring", patternTextBox);
|
||||
|
||||
foregroundButton.Text = "Choose text color...";
|
||||
foregroundButton.AutoSize = true;
|
||||
foregroundButton.Click += ForegroundButton_Click;
|
||||
AddEditorRow(editor, "Text color", foregroundButton);
|
||||
|
||||
var backgroundPanel = new FlowLayoutPanel { Dock = DockStyle.Fill, AutoSize = true };
|
||||
backgroundCheckBox.Text = "Use background";
|
||||
backgroundCheckBox.AutoSize = true;
|
||||
backgroundCheckBox.CheckedChanged += EditorValueChanged;
|
||||
backgroundButton.Text = "Choose...";
|
||||
backgroundButton.AutoSize = true;
|
||||
backgroundButton.Click += BackgroundButton_Click;
|
||||
backgroundPanel.Controls.Add(backgroundCheckBox);
|
||||
backgroundPanel.Controls.Add(backgroundButton);
|
||||
AddEditorRow(editor, "Background", backgroundPanel);
|
||||
|
||||
boldCheckBox.Text = "Bold text";
|
||||
boldCheckBox.AutoSize = true;
|
||||
boldCheckBox.CheckedChanged += EditorValueChanged;
|
||||
AddEditorRow(editor, "Font", boldCheckBox);
|
||||
|
||||
var resetButton = CreateButton("Restore default rules", RestoreDefaults_Click);
|
||||
resetButton.AutoSize = true;
|
||||
AddEditorRow(editor, string.Empty, resetButton);
|
||||
split.Panel2.Controls.Add(editor);
|
||||
|
||||
previewBox.Dock = DockStyle.Fill;
|
||||
previewBox.ReadOnly = true;
|
||||
previewBox.WordWrap = true;
|
||||
previewBox.Font = new Font("Consolas", 9F);
|
||||
previewBox.Text = "2026-09-10 14:25:31.042|DEBUG|TBF[14280]|TBF.UI.MainWnd|Device configuration loaded.\n2026-09-10 14:25:31.384|INFO |TBF[14280]|TBF.Rig.StateMachine|Test bench started.\n2026-09-10 14:25:32.018|WARN |TBF[14280]|TBF.Device|Pressure is approaching the limit.\n2026-09-10 14:25:32.611|ERROR|TBF[14280]|TBF.Device|Communication failed.";
|
||||
|
||||
var previewGroup = new GroupBox { Text = "Live preview", Dock = DockStyle.Fill, Padding = new Padding(8) };
|
||||
previewGroup.Controls.Add(previewBox);
|
||||
|
||||
var footer = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft, Padding = new Padding(0, 8, 0, 0) };
|
||||
var okButton = new Button { Text = "OK", DialogResult = DialogResult.OK, AutoSize = true };
|
||||
var cancelButton = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, AutoSize = true };
|
||||
footer.Controls.Add(okButton);
|
||||
footer.Controls.Add(cancelButton);
|
||||
|
||||
AcceptButton = okButton;
|
||||
CancelButton = cancelButton;
|
||||
root.Controls.Add(split, 0, 0);
|
||||
root.Controls.Add(previewGroup, 0, 1);
|
||||
root.Controls.Add(footer, 0, 2);
|
||||
Controls.Add(root);
|
||||
}
|
||||
|
||||
private static Button CreateButton(string text, EventHandler clickHandler)
|
||||
{
|
||||
var button = new Button { Text = text, AutoSize = true };
|
||||
button.Click += clickHandler;
|
||||
return button;
|
||||
}
|
||||
|
||||
private static void AddEditorRow(TableLayoutPanel panel, string labelText, Control control)
|
||||
{
|
||||
int row = panel.RowCount++;
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
var label = new Label { Text = labelText, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(3, 8, 3, 3) };
|
||||
control.Margin = new Padding(3, 5, 3, 5);
|
||||
panel.Controls.Add(label, 0, row);
|
||||
panel.Controls.Add(control, 1, row);
|
||||
}
|
||||
|
||||
private void RefreshRuleList(int selectedIndex)
|
||||
{
|
||||
ruleList.BeginUpdate();
|
||||
ruleList.Items.Clear();
|
||||
foreach (LogColorRule rule in rules)
|
||||
{
|
||||
ruleList.Items.Add(rule);
|
||||
}
|
||||
ruleList.EndUpdate();
|
||||
|
||||
if (rules.Count > 0)
|
||||
{
|
||||
ruleList.SelectedIndex = Math.Max(0, Math.Min(selectedIndex, rules.Count - 1));
|
||||
}
|
||||
}
|
||||
|
||||
private void RuleList_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
LogColorRule rule = ruleList.SelectedItem as LogColorRule;
|
||||
if (rule == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
loadingRule = true;
|
||||
enabledCheckBox.Checked = rule.Enabled;
|
||||
patternTextBox.Text = rule.Pattern;
|
||||
foregroundButton.BackColor = rule.ForeColor;
|
||||
backgroundCheckBox.Checked = rule.UseBackColor;
|
||||
backgroundButton.Enabled = rule.UseBackColor;
|
||||
backgroundButton.BackColor = rule.UseBackColor ? rule.BackColor : SystemColors.Control;
|
||||
boldCheckBox.Checked = rule.Bold;
|
||||
loadingRule = false;
|
||||
}
|
||||
|
||||
private void EditorValueChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (loadingRule)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LogColorRule rule = ruleList.SelectedItem as LogColorRule;
|
||||
if (rule == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rule.Enabled = enabledCheckBox.Checked;
|
||||
rule.Pattern = patternTextBox.Text;
|
||||
rule.UseBackColor = backgroundCheckBox.Checked;
|
||||
rule.Bold = boldCheckBox.Checked;
|
||||
backgroundButton.Enabled = rule.UseBackColor;
|
||||
int index = ruleList.SelectedIndex;
|
||||
RefreshRuleList(index);
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
private void ForegroundButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
LogColorRule rule = ruleList.SelectedItem as LogColorRule;
|
||||
if (rule == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (var dialog = new ColorDialog { Color = rule.ForeColor, FullOpen = true })
|
||||
{
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rule.ForeColorArgb = dialog.Color.ToArgb();
|
||||
foregroundButton.BackColor = dialog.Color;
|
||||
RefreshPreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void BackgroundButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
LogColorRule rule = ruleList.SelectedItem as LogColorRule;
|
||||
if (rule == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Color initialColor = rule.UseBackColor ? rule.BackColor : SystemColors.Window;
|
||||
using (var dialog = new ColorDialog { Color = initialColor, FullOpen = true })
|
||||
{
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rule.BackColorArgb = dialog.Color.ToArgb();
|
||||
rule.UseBackColor = true;
|
||||
backgroundCheckBox.Checked = true;
|
||||
backgroundButton.BackColor = dialog.Color;
|
||||
RefreshPreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRule_Click(object sender, EventArgs e)
|
||||
{
|
||||
rules.Add(new LogColorRule { Pattern = "NEW SUBSTRING", ForeColorArgb = Color.MediumPurple.ToArgb() });
|
||||
RefreshRuleList(rules.Count - 1);
|
||||
patternTextBox.Focus();
|
||||
patternTextBox.SelectAll();
|
||||
}
|
||||
|
||||
private void DeleteRule_Click(object sender, EventArgs e)
|
||||
{
|
||||
int index = ruleList.SelectedIndex;
|
||||
if (index < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rules.RemoveAt(index);
|
||||
RefreshRuleList(Math.Min(index, rules.Count - 1));
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
private void MoveUp_Click(object sender, EventArgs e)
|
||||
{
|
||||
MoveSelectedRule(-1);
|
||||
}
|
||||
|
||||
private void MoveDown_Click(object sender, EventArgs e)
|
||||
{
|
||||
MoveSelectedRule(1);
|
||||
}
|
||||
|
||||
private void MoveSelectedRule(int offset)
|
||||
{
|
||||
int index = ruleList.SelectedIndex;
|
||||
int newIndex = index + offset;
|
||||
if (index < 0 || newIndex < 0 || newIndex >= rules.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LogColorRule rule = rules[index];
|
||||
rules.RemoveAt(index);
|
||||
rules.Insert(newIndex, rule);
|
||||
RefreshRuleList(newIndex);
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
private void RestoreDefaults_Click(object sender, EventArgs e)
|
||||
{
|
||||
rules.Clear();
|
||||
rules.AddRange(LogColorRule.CreateDefaults());
|
||||
RefreshRuleList(0);
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
private void RefreshPreview()
|
||||
{
|
||||
int selectionStart = previewBox.SelectionStart;
|
||||
int selectionLength = previewBox.SelectionLength;
|
||||
string text = previewBox.Text;
|
||||
LogColorizer.Apply(previewBox, 0, text.Length, text, rules, previewBox.Font, previewBoldFont);
|
||||
previewBox.Select(Math.Min(selectionStart, previewBox.TextLength), Math.Min(selectionLength, Math.Max(0, previewBox.TextLength - selectionStart)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using AppDiagnostic.Logging;
|
||||
using System;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
public static class DiagnosticLogFormatter
|
||||
{
|
||||
public static string Format(DiagnosticLogEntry entry)
|
||||
{
|
||||
if (entry == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string level = (entry.Level ?? string.Empty).ToUpperInvariant().PadRight(5);
|
||||
string application = string.IsNullOrWhiteSpace(entry.ApplicationName) ? "Unknown" : entry.ApplicationName;
|
||||
string logger = entry.LoggerName ?? string.Empty;
|
||||
string message = entry.Message ?? string.Empty;
|
||||
string exception = string.IsNullOrWhiteSpace(entry.Exception) ? string.Empty : Environment.NewLine + entry.Exception;
|
||||
|
||||
return $"{entry.TimestampUtc.ToLocalTime():yyyy-MM-dd HH:mm:ss.fff}|{level}|{application}[{entry.ProcessId}]|{logger}|{message}{exception}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using AppDiagnostic.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
public sealed class DiagnosticLogStore
|
||||
{
|
||||
private const int MaximumLogCount = 10000;
|
||||
private static readonly DiagnosticLogStore instance = new DiagnosticLogStore();
|
||||
private readonly object syncRoot = new object();
|
||||
private readonly List<DiagnosticLogEntry> entries = new List<DiagnosticLogEntry>();
|
||||
private long totalReceived;
|
||||
private long version;
|
||||
|
||||
private DiagnosticLogStore()
|
||||
{
|
||||
}
|
||||
|
||||
public static DiagnosticLogStore Instance => instance;
|
||||
|
||||
public long TotalReceived
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (syncRoot)
|
||||
{
|
||||
return totalReceived;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long Version
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (syncRoot)
|
||||
{
|
||||
return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(DiagnosticLogEntry entry)
|
||||
{
|
||||
if (entry == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (syncRoot)
|
||||
{
|
||||
entries.Add(entry);
|
||||
totalReceived++;
|
||||
version++;
|
||||
|
||||
int excessCount = entries.Count - MaximumLogCount;
|
||||
if (excessCount > 0)
|
||||
{
|
||||
entries.RemoveRange(0, excessCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<DiagnosticLogEntry> GetSnapshot()
|
||||
{
|
||||
lock (syncRoot)
|
||||
{
|
||||
return entries.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (syncRoot)
|
||||
{
|
||||
entries.Clear();
|
||||
version++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
[Serializable]
|
||||
public sealed class LogColorRule
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public string Pattern { get; set; } = string.Empty;
|
||||
public int ForeColorArgb { get; set; } = Color.Black.ToArgb();
|
||||
public int BackColorArgb { get; set; }
|
||||
public bool UseBackColor { get; set; }
|
||||
public bool Bold { get; set; }
|
||||
|
||||
public Color ForeColor => Color.FromArgb(ForeColorArgb);
|
||||
public Color BackColor => Color.FromArgb(BackColorArgb);
|
||||
|
||||
public LogColorRule Clone()
|
||||
{
|
||||
return new LogColorRule
|
||||
{
|
||||
Enabled = Enabled,
|
||||
Pattern = Pattern,
|
||||
ForeColorArgb = ForeColorArgb,
|
||||
BackColorArgb = BackColorArgb,
|
||||
UseBackColor = UseBackColor,
|
||||
Bold = Bold
|
||||
};
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string state = Enabled ? string.Empty : "[disabled] ";
|
||||
return state + Pattern;
|
||||
}
|
||||
|
||||
public static List<LogColorRule> CreateDefaults()
|
||||
{
|
||||
return new List<LogColorRule>
|
||||
{
|
||||
Create("|TRACE|", Color.DimGray, false),
|
||||
Create("|DEBUG|", Color.RoyalBlue, false),
|
||||
Create("|INFO |", Color.ForestGreen, false),
|
||||
Create("|WARN |", Color.DarkOrange, true),
|
||||
Create("|ERROR|", Color.Crimson, true),
|
||||
Create("|FATAL|", Color.White, true, Color.DarkRed)
|
||||
};
|
||||
}
|
||||
|
||||
private static LogColorRule Create(string pattern, Color foreground, bool bold, Color? background = null)
|
||||
{
|
||||
return new LogColorRule
|
||||
{
|
||||
Enabled = true,
|
||||
Pattern = pattern,
|
||||
ForeColorArgb = foreground.ToArgb(),
|
||||
BackColorArgb = background?.ToArgb() ?? Color.Empty.ToArgb(),
|
||||
UseBackColor = background.HasValue,
|
||||
Bold = bold
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
public static class LogColorRuleRepository
|
||||
{
|
||||
private static readonly string settingsFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Sensus", "AppDiagnostic");
|
||||
private static readonly string settingsPath = Path.Combine(settingsFolder, "color-rules.xml");
|
||||
|
||||
public static List<LogColorRule> Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(settingsPath))
|
||||
{
|
||||
return LogColorRule.CreateDefaults();
|
||||
}
|
||||
|
||||
var serializer = new XmlSerializer(typeof(List<LogColorRule>));
|
||||
using (FileStream stream = File.OpenRead(settingsPath))
|
||||
{
|
||||
var rules = serializer.Deserialize(stream) as List<LogColorRule>;
|
||||
return rules == null || rules.Count == 0 ? LogColorRule.CreateDefaults() : rules;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LogColorRule.CreateDefaults();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Save(IEnumerable<LogColorRule> rules)
|
||||
{
|
||||
Directory.CreateDirectory(settingsFolder);
|
||||
|
||||
var serializer = new XmlSerializer(typeof(List<LogColorRule>));
|
||||
using (FileStream stream = File.Create(settingsPath))
|
||||
{
|
||||
serializer.Serialize(stream, new List<LogColorRule>(rules));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
public static class LogColorizer
|
||||
{
|
||||
public static void Apply(RichTextBox target, int startIndex, int length, string text, IEnumerable<LogColorRule> rules, Font regularFont, Font boldFont)
|
||||
{
|
||||
if (target == null || length <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
target.Select(startIndex, length);
|
||||
target.SelectionColor = target.ForeColor;
|
||||
target.SelectionBackColor = target.BackColor;
|
||||
target.SelectionFont = regularFont;
|
||||
|
||||
foreach (LogColorRule rule in rules)
|
||||
{
|
||||
if (rule == null || !rule.Enabled || string.IsNullOrEmpty(rule.Pattern))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int searchIndex = 0;
|
||||
while (searchIndex < text.Length)
|
||||
{
|
||||
int matchIndex = text.IndexOf(rule.Pattern, searchIndex, StringComparison.OrdinalIgnoreCase);
|
||||
if (matchIndex < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
target.Select(startIndex + matchIndex, rule.Pattern.Length);
|
||||
target.SelectionColor = rule.ForeColor;
|
||||
target.SelectionBackColor = rule.UseBackColor ? rule.BackColor : target.BackColor;
|
||||
target.SelectionFont = rule.Bold ? boldFont : regularFont;
|
||||
searchIndex = matchIndex + rule.Pattern.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+281
-250
@@ -1,278 +1,309 @@
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
partial class MainForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
private System.Windows.Forms.ToolStrip mainToolStrip;
|
||||
private System.Windows.Forms.ToolStripButton pauseButton;
|
||||
private System.Windows.Forms.ToolStripButton refreshButton;
|
||||
private System.Windows.Forms.ToolStripButton clearButton;
|
||||
private System.Windows.Forms.ToolStripButton saveButton;
|
||||
private System.Windows.Forms.ToolStripButton copyButton;
|
||||
private System.Windows.Forms.ToolStripButton copyAllButton;
|
||||
private System.Windows.Forms.ToolStripButton wrapButton;
|
||||
private System.Windows.Forms.ToolStripButton autoScrollButton;
|
||||
private System.Windows.Forms.ToolStripButton alwaysOnTopButton;
|
||||
private System.Windows.Forms.ToolStripButton colorRulesButton;
|
||||
private System.Windows.Forms.ToolStripButton closeButton;
|
||||
private System.Windows.Forms.TableLayoutPanel filterPanel;
|
||||
private System.Windows.Forms.Label filterLabel;
|
||||
private System.Windows.Forms.TextBox filterTextBox;
|
||||
private System.Windows.Forms.Label levelLabel;
|
||||
private System.Windows.Forms.ComboBox levelComboBox;
|
||||
private System.Windows.Forms.Label applicationLabel;
|
||||
private System.Windows.Forms.ComboBox applicationComboBox;
|
||||
private System.Windows.Forms.Button applyFilterButton;
|
||||
private System.Windows.Forms.Button clearFilterButton;
|
||||
private System.Windows.Forms.RichTextBox logView;
|
||||
private System.Windows.Forms.StatusStrip statusStrip;
|
||||
private System.Windows.Forms.ToolStripStatusLabel receiverStatusLabel;
|
||||
private System.Windows.Forms.ToolStripStatusLabel statusSpacer;
|
||||
private System.Windows.Forms.ToolStripStatusLabel countStatusLabel;
|
||||
private System.Windows.Forms.ContextMenuStrip logContextMenu;
|
||||
private System.Windows.Forms.ToolStripMenuItem copySelectionMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem copyLineMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem copyAllMenuItem;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
if (disposing && components != null)
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
//private BufferedListView memoListView;
|
||||
private System.Windows.Forms.ColumnHeader columnHeaderTime;
|
||||
private System.Windows.Forms.ColumnHeader columnHeaderMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.columnHeaderTime = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.columnHeaderMessage = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.panel1 = new System.Windows.Forms.Panel();
|
||||
this.refreshMemoButton = new System.Windows.Forms.Button();
|
||||
this.button6 = new System.Windows.Forms.Button();
|
||||
this.button7 = new System.Windows.Forms.Button();
|
||||
this.filterManagementPanel = new System.Windows.Forms.Panel();
|
||||
this.button5 = new System.Windows.Forms.Button();
|
||||
this.filterButton = new System.Windows.Forms.Button();
|
||||
this.components = new System.ComponentModel.Container();
|
||||
this.mainToolStrip = new System.Windows.Forms.ToolStrip();
|
||||
this.pauseButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.refreshButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.clearButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.saveButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.copyButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.copyAllButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.wrapButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.autoScrollButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.alwaysOnTopButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.colorRulesButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.closeButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.filterPanel = new System.Windows.Forms.TableLayoutPanel();
|
||||
this.filterLabel = new System.Windows.Forms.Label();
|
||||
this.filterTextBox = new System.Windows.Forms.TextBox();
|
||||
this.label2 = new System.Windows.Forms.Label();
|
||||
this.wndControlPanel = new System.Windows.Forms.Panel();
|
||||
this.button2 = new System.Windows.Forms.Button();
|
||||
this.flowControlsPanel = new System.Windows.Forms.Panel();
|
||||
this.runButton = new System.Windows.Forms.Button();
|
||||
this.stopButton = new System.Windows.Forms.Button();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.memoListView = new BufferedListView();
|
||||
this.panel1.SuspendLayout();
|
||||
this.filterManagementPanel.SuspendLayout();
|
||||
this.wndControlPanel.SuspendLayout();
|
||||
this.flowControlsPanel.SuspendLayout();
|
||||
this.levelLabel = new System.Windows.Forms.Label();
|
||||
this.levelComboBox = new System.Windows.Forms.ComboBox();
|
||||
this.applicationLabel = new System.Windows.Forms.Label();
|
||||
this.applicationComboBox = new System.Windows.Forms.ComboBox();
|
||||
this.applyFilterButton = new System.Windows.Forms.Button();
|
||||
this.clearFilterButton = new System.Windows.Forms.Button();
|
||||
this.logContextMenu = new System.Windows.Forms.ContextMenuStrip(this.components);
|
||||
this.copySelectionMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.copyLineMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.copyAllMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.logView = new System.Windows.Forms.RichTextBox();
|
||||
this.statusStrip = new System.Windows.Forms.StatusStrip();
|
||||
this.receiverStatusLabel = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.statusSpacer = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.countStatusLabel = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.mainToolStrip.SuspendLayout();
|
||||
this.filterPanel.SuspendLayout();
|
||||
this.logContextMenu.SuspendLayout();
|
||||
this.statusStrip.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// columnHeaderTime
|
||||
//
|
||||
this.columnHeaderTime.Text = "Time";
|
||||
this.columnHeaderTime.Width = 150;
|
||||
//
|
||||
// columnHeaderMessage
|
||||
//
|
||||
this.columnHeaderMessage.Text = "Message";
|
||||
this.columnHeaderMessage.Width = 350;
|
||||
//
|
||||
// panel1
|
||||
//
|
||||
this.panel1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.panel1.Controls.Add(this.refreshMemoButton);
|
||||
this.panel1.Controls.Add(this.button6);
|
||||
this.panel1.Controls.Add(this.button7);
|
||||
this.panel1.Location = new System.Drawing.Point(189, 356);
|
||||
this.panel1.Name = "panel1";
|
||||
this.panel1.Size = new System.Drawing.Size(283, 26);
|
||||
this.panel1.TabIndex = 16;
|
||||
//
|
||||
// refreshMemoButton
|
||||
//
|
||||
this.refreshMemoButton.Location = new System.Drawing.Point(108, 0);
|
||||
this.refreshMemoButton.Name = "refreshMemoButton";
|
||||
this.refreshMemoButton.Size = new System.Drawing.Size(84, 26);
|
||||
this.refreshMemoButton.TabIndex = 17;
|
||||
this.refreshMemoButton.Text = "Refresh memo";
|
||||
this.refreshMemoButton.UseVisualStyleBackColor = true;
|
||||
this.refreshMemoButton.Click += new System.EventHandler(this.refreshMemoButton_Click);
|
||||
//
|
||||
// button6
|
||||
//
|
||||
this.button6.Location = new System.Drawing.Point(0, 0);
|
||||
this.button6.Name = "button6";
|
||||
this.button6.Size = new System.Drawing.Size(102, 26);
|
||||
this.button6.TabIndex = 4;
|
||||
this.button6.Text = "Clean diag cache";
|
||||
this.button6.UseVisualStyleBackColor = true;
|
||||
this.button6.Click += new System.EventHandler(this.button6_Click);
|
||||
//
|
||||
// button7
|
||||
//
|
||||
this.button7.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.button7.Location = new System.Drawing.Point(198, 0);
|
||||
this.button7.Name = "button7";
|
||||
this.button7.Size = new System.Drawing.Size(72, 26);
|
||||
this.button7.TabIndex = 5;
|
||||
this.button7.Text = "Save logs";
|
||||
this.button7.UseVisualStyleBackColor = true;
|
||||
this.button7.Click += new System.EventHandler(this.button7_Click);
|
||||
//
|
||||
// filterManagementPanel
|
||||
//
|
||||
this.filterManagementPanel.Controls.Add(this.button5);
|
||||
this.filterManagementPanel.Controls.Add(this.filterButton);
|
||||
this.filterManagementPanel.Controls.Add(this.filterTextBox);
|
||||
this.filterManagementPanel.Controls.Add(this.label2);
|
||||
this.filterManagementPanel.Location = new System.Drawing.Point(127, 6);
|
||||
this.filterManagementPanel.Name = "filterManagementPanel";
|
||||
this.filterManagementPanel.Size = new System.Drawing.Size(393, 26);
|
||||
this.filterManagementPanel.TabIndex = 15;
|
||||
//
|
||||
// button5
|
||||
//
|
||||
this.button5.Enabled = false;
|
||||
this.button5.Location = new System.Drawing.Point(294, 4);
|
||||
this.button5.Name = "button5";
|
||||
this.button5.Size = new System.Drawing.Size(81, 20);
|
||||
this.button5.TabIndex = 11;
|
||||
this.button5.Text = "Add new filter";
|
||||
this.button5.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// filterButton
|
||||
//
|
||||
this.filterButton.Location = new System.Drawing.Point(225, 4);
|
||||
this.filterButton.Name = "filterButton";
|
||||
this.filterButton.Size = new System.Drawing.Size(64, 20);
|
||||
this.filterButton.TabIndex = 10;
|
||||
this.filterButton.Text = "Set filter";
|
||||
this.filterButton.UseVisualStyleBackColor = true;
|
||||
this.filterButton.Click += new System.EventHandler(this.filterButton_Click_1);
|
||||
//
|
||||
// filterTextBox
|
||||
//
|
||||
this.filterTextBox.Location = new System.Drawing.Point(33, 4);
|
||||
this.filterTextBox.Name = "filterTextBox";
|
||||
this.filterTextBox.Size = new System.Drawing.Size(187, 20);
|
||||
this.filterTextBox.TabIndex = 7;
|
||||
//
|
||||
// label2
|
||||
//
|
||||
this.label2.AutoSize = true;
|
||||
this.label2.Location = new System.Drawing.Point(3, 8);
|
||||
this.label2.Name = "label2";
|
||||
this.label2.Size = new System.Drawing.Size(29, 13);
|
||||
this.label2.TabIndex = 6;
|
||||
this.label2.Text = "Filter";
|
||||
//
|
||||
// wndControlPanel
|
||||
//
|
||||
this.wndControlPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.wndControlPanel.Controls.Add(this.button2);
|
||||
this.wndControlPanel.Location = new System.Drawing.Point(831, 356);
|
||||
this.wndControlPanel.Name = "wndControlPanel";
|
||||
this.wndControlPanel.Size = new System.Drawing.Size(70, 26);
|
||||
this.wndControlPanel.TabIndex = 14;
|
||||
//
|
||||
// button2
|
||||
//
|
||||
this.button2.Location = new System.Drawing.Point(3, 0);
|
||||
this.button2.Name = "button2";
|
||||
this.button2.Size = new System.Drawing.Size(64, 26);
|
||||
this.button2.TabIndex = 2;
|
||||
this.button2.Text = "Close";
|
||||
this.button2.UseVisualStyleBackColor = true;
|
||||
this.button2.Click += new System.EventHandler(this.button2_Click);
|
||||
//
|
||||
// flowControlsPanel
|
||||
//
|
||||
this.flowControlsPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.flowControlsPanel.Controls.Add(this.runButton);
|
||||
this.flowControlsPanel.Controls.Add(this.stopButton);
|
||||
this.flowControlsPanel.Location = new System.Drawing.Point(12, 356);
|
||||
this.flowControlsPanel.Name = "flowControlsPanel";
|
||||
this.flowControlsPanel.Size = new System.Drawing.Size(133, 26);
|
||||
this.flowControlsPanel.TabIndex = 13;
|
||||
//
|
||||
// runButton
|
||||
//
|
||||
this.runButton.Enabled = false;
|
||||
this.runButton.Location = new System.Drawing.Point(0, 0);
|
||||
this.runButton.Name = "runButton";
|
||||
this.runButton.Size = new System.Drawing.Size(64, 26);
|
||||
this.runButton.TabIndex = 4;
|
||||
this.runButton.Text = "Run";
|
||||
this.runButton.UseVisualStyleBackColor = true;
|
||||
this.runButton.Click += new System.EventHandler(this.runButton_Click);
|
||||
//
|
||||
// stopButton
|
||||
//
|
||||
this.stopButton.Location = new System.Drawing.Point(69, 0);
|
||||
this.stopButton.Name = "stopButton";
|
||||
this.stopButton.Size = new System.Drawing.Size(64, 26);
|
||||
this.stopButton.TabIndex = 5;
|
||||
this.stopButton.Text = "Stop";
|
||||
this.stopButton.UseVisualStyleBackColor = true;
|
||||
this.stopButton.Click += new System.EventHandler(this.stopButton_Click);
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 19);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(76, 13);
|
||||
this.label1.TabIndex = 12;
|
||||
this.label1.Text = "Flow of events";
|
||||
//
|
||||
// memoListView
|
||||
//
|
||||
this.memoListView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.memoListView.FullRowSelect = true;
|
||||
this.memoListView.GridLines = true;
|
||||
this.memoListView.HideSelection = false;
|
||||
this.memoListView.Location = new System.Drawing.Point(12, 38);
|
||||
this.memoListView.Name = "memoListView";
|
||||
this.memoListView.Size = new System.Drawing.Size(889, 312);
|
||||
this.memoListView.TabIndex = 0;
|
||||
this.memoListView.UseCompatibleStateImageBehavior = false;
|
||||
this.memoListView.View = System.Windows.Forms.View.Details;
|
||||
|
||||
this.memoListView.DoubleClick += new System.EventHandler(this.MemoListView_DoubleClick);
|
||||
//
|
||||
//
|
||||
// mainToolStrip
|
||||
//
|
||||
this.mainToolStrip.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
|
||||
this.mainToolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.pauseButton,
|
||||
this.refreshButton,
|
||||
this.clearButton,
|
||||
this.saveButton,
|
||||
new System.Windows.Forms.ToolStripSeparator(),
|
||||
this.copyButton,
|
||||
this.copyAllButton,
|
||||
new System.Windows.Forms.ToolStripSeparator(),
|
||||
this.wrapButton,
|
||||
this.autoScrollButton,
|
||||
this.alwaysOnTopButton,
|
||||
this.colorRulesButton,
|
||||
this.closeButton});
|
||||
this.mainToolStrip.Location = new System.Drawing.Point(0, 0);
|
||||
this.mainToolStrip.Name = "mainToolStrip";
|
||||
this.mainToolStrip.Padding = new System.Windows.Forms.Padding(6, 3, 6, 3);
|
||||
this.mainToolStrip.Size = new System.Drawing.Size(1184, 31);
|
||||
this.mainToolStrip.TabIndex = 0;
|
||||
//
|
||||
// pauseButton
|
||||
//
|
||||
this.pauseButton.CheckOnClick = true;
|
||||
this.pauseButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.pauseButton.Text = "Pause";
|
||||
this.pauseButton.ToolTipText = "Pause screen updates while continuing to receive logs";
|
||||
this.pauseButton.Click += new System.EventHandler(this.pauseButton_Click);
|
||||
//
|
||||
// refreshButton
|
||||
//
|
||||
this.refreshButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.refreshButton.Text = "Refresh";
|
||||
this.refreshButton.Click += new System.EventHandler(this.refreshButton_Click);
|
||||
//
|
||||
// clearButton
|
||||
//
|
||||
this.clearButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.clearButton.Text = "Clear";
|
||||
this.clearButton.Click += new System.EventHandler(this.clearButton_Click);
|
||||
//
|
||||
// saveButton
|
||||
//
|
||||
this.saveButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.saveButton.Text = "Save...";
|
||||
this.saveButton.Click += new System.EventHandler(this.saveButton_Click);
|
||||
//
|
||||
// copyButton
|
||||
//
|
||||
this.copyButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.copyButton.Text = "Copy";
|
||||
this.copyButton.ToolTipText = "Copy selected text or the current log line";
|
||||
this.copyButton.Click += new System.EventHandler(this.copyButton_Click);
|
||||
//
|
||||
// copyAllButton
|
||||
//
|
||||
this.copyAllButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.copyAllButton.Text = "Copy all";
|
||||
this.copyAllButton.Click += new System.EventHandler(this.copyAllButton_Click);
|
||||
//
|
||||
// wrapButton
|
||||
//
|
||||
this.wrapButton.Checked = true;
|
||||
this.wrapButton.CheckOnClick = true;
|
||||
this.wrapButton.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||
this.wrapButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.wrapButton.Text = "Wrap lines";
|
||||
this.wrapButton.Click += new System.EventHandler(this.wrapButton_Click);
|
||||
//
|
||||
// autoScrollButton
|
||||
//
|
||||
this.autoScrollButton.Checked = true;
|
||||
this.autoScrollButton.CheckOnClick = true;
|
||||
this.autoScrollButton.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||
this.autoScrollButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.autoScrollButton.Text = "Auto-scroll";
|
||||
//
|
||||
// alwaysOnTopButton
|
||||
//
|
||||
this.alwaysOnTopButton.CheckOnClick = true;
|
||||
this.alwaysOnTopButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.alwaysOnTopButton.Text = "Always on top";
|
||||
this.alwaysOnTopButton.Click += new System.EventHandler(this.alwaysOnTopButton_Click);
|
||||
//
|
||||
// colorRulesButton
|
||||
//
|
||||
this.colorRulesButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.colorRulesButton.Text = "Color rules...";
|
||||
this.colorRulesButton.Click += new System.EventHandler(this.colorRulesButton_Click);
|
||||
//
|
||||
// closeButton
|
||||
//
|
||||
this.closeButton.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
|
||||
this.closeButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.closeButton.Text = "Close";
|
||||
this.closeButton.Click += new System.EventHandler(this.closeButton_Click);
|
||||
//
|
||||
// filterPanel
|
||||
//
|
||||
this.filterPanel.ColumnCount = 8;
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 42F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 42F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 105F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 72F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 180F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 66F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 66F));
|
||||
this.filterPanel.Controls.Add(this.filterLabel, 0, 0);
|
||||
this.filterPanel.Controls.Add(this.filterTextBox, 1, 0);
|
||||
this.filterPanel.Controls.Add(this.levelLabel, 2, 0);
|
||||
this.filterPanel.Controls.Add(this.levelComboBox, 3, 0);
|
||||
this.filterPanel.Controls.Add(this.applicationLabel, 4, 0);
|
||||
this.filterPanel.Controls.Add(this.applicationComboBox, 5, 0);
|
||||
this.filterPanel.Controls.Add(this.applyFilterButton, 6, 0);
|
||||
this.filterPanel.Controls.Add(this.clearFilterButton, 7, 0);
|
||||
this.filterPanel.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.filterPanel.Location = new System.Drawing.Point(0, 31);
|
||||
this.filterPanel.Name = "filterPanel";
|
||||
this.filterPanel.Padding = new System.Windows.Forms.Padding(6, 5, 6, 5);
|
||||
this.filterPanel.RowCount = 1;
|
||||
this.filterPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
this.filterPanel.Size = new System.Drawing.Size(1184, 38);
|
||||
this.filterPanel.TabIndex = 1;
|
||||
//
|
||||
// filter controls
|
||||
//
|
||||
this.filterLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.filterLabel.AutoSize = true;
|
||||
this.filterLabel.Text = "Filter";
|
||||
this.filterTextBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.filterTextBox.KeyDown += new System.Windows.Forms.KeyEventHandler(this.filterTextBox_KeyDown);
|
||||
this.levelLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.levelLabel.AutoSize = true;
|
||||
this.levelLabel.Text = "Level";
|
||||
this.levelComboBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.levelComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.levelComboBox.SelectedIndexChanged += new System.EventHandler(this.filterValueChanged);
|
||||
this.applicationLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.applicationLabel.AutoSize = true;
|
||||
this.applicationLabel.Text = "Application";
|
||||
this.applicationComboBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.applicationComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.applicationComboBox.SelectedIndexChanged += new System.EventHandler(this.filterValueChanged);
|
||||
this.applyFilterButton.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.applyFilterButton.Text = "Apply";
|
||||
this.applyFilterButton.Click += new System.EventHandler(this.applyFilterButton_Click);
|
||||
this.clearFilterButton.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.clearFilterButton.Text = "Reset";
|
||||
this.clearFilterButton.Click += new System.EventHandler(this.clearFilterButton_Click);
|
||||
//
|
||||
// logContextMenu
|
||||
//
|
||||
this.logContextMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.copySelectionMenuItem,
|
||||
this.copyLineMenuItem,
|
||||
this.copyAllMenuItem});
|
||||
this.copySelectionMenuItem.Text = "Copy selection";
|
||||
this.copySelectionMenuItem.Click += new System.EventHandler(this.copySelectionMenuItem_Click);
|
||||
this.copyLineMenuItem.Text = "Copy current line";
|
||||
this.copyLineMenuItem.Click += new System.EventHandler(this.copyLineMenuItem_Click);
|
||||
this.copyAllMenuItem.Text = "Copy all visible logs";
|
||||
this.copyAllMenuItem.Click += new System.EventHandler(this.copyAllButton_Click);
|
||||
//
|
||||
// logView
|
||||
//
|
||||
this.logView.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.logView.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
this.logView.ContextMenuStrip = this.logContextMenu;
|
||||
this.logView.DetectUrls = false;
|
||||
this.logView.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.logView.Font = new System.Drawing.Font("Consolas", 9F);
|
||||
this.logView.HideSelection = false;
|
||||
this.logView.Location = new System.Drawing.Point(0, 69);
|
||||
this.logView.Name = "logView";
|
||||
this.logView.ReadOnly = true;
|
||||
this.logView.Size = new System.Drawing.Size(1184, 554);
|
||||
this.logView.TabIndex = 2;
|
||||
this.logView.Text = "";
|
||||
this.logView.WordWrap = true;
|
||||
//
|
||||
// statusStrip
|
||||
//
|
||||
this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.receiverStatusLabel,
|
||||
this.statusSpacer,
|
||||
this.countStatusLabel});
|
||||
this.statusStrip.Location = new System.Drawing.Point(0, 623);
|
||||
this.statusStrip.Name = "statusStrip";
|
||||
this.statusStrip.Size = new System.Drawing.Size(1184, 22);
|
||||
this.statusStrip.TabIndex = 3;
|
||||
this.receiverStatusLabel.Text = "Listening";
|
||||
this.statusSpacer.Spring = true;
|
||||
this.countStatusLabel.Text = "Received: 0 | Visible: 0";
|
||||
//
|
||||
// MainForm
|
||||
//
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(913, 388);
|
||||
this.Controls.Add(this.memoListView);
|
||||
this.Controls.Add(this.panel1);
|
||||
this.Controls.Add(this.filterManagementPanel);
|
||||
this.Controls.Add(this.wndControlPanel);
|
||||
this.Controls.Add(this.flowControlsPanel);
|
||||
this.Controls.Add(this.label1);
|
||||
this.ClientSize = new System.Drawing.Size(1184, 645);
|
||||
this.Controls.Add(this.logView);
|
||||
this.Controls.Add(this.filterPanel);
|
||||
this.Controls.Add(this.mainToolStrip);
|
||||
this.Controls.Add(this.statusStrip);
|
||||
this.KeyPreview = true;
|
||||
this.MinimumSize = new System.Drawing.Size(860, 480);
|
||||
this.Name = "MainForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "Application live diagnostic";
|
||||
this.panel1.ResumeLayout(false);
|
||||
this.filterManagementPanel.ResumeLayout(false);
|
||||
this.filterManagementPanel.PerformLayout();
|
||||
this.wndControlPanel.ResumeLayout(false);
|
||||
this.flowControlsPanel.ResumeLayout(false);
|
||||
this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.MainForm_KeyDown);
|
||||
this.mainToolStrip.ResumeLayout(false);
|
||||
this.mainToolStrip.PerformLayout();
|
||||
this.filterPanel.ResumeLayout(false);
|
||||
this.filterPanel.PerformLayout();
|
||||
this.logContextMenu.ResumeLayout(false);
|
||||
this.statusStrip.ResumeLayout(false);
|
||||
this.statusStrip.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Panel panel1;
|
||||
private System.Windows.Forms.Button button6;
|
||||
private System.Windows.Forms.Button button7;
|
||||
private System.Windows.Forms.Panel filterManagementPanel;
|
||||
private System.Windows.Forms.Button button5;
|
||||
private System.Windows.Forms.Button filterButton;
|
||||
private System.Windows.Forms.TextBox filterTextBox;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.Panel wndControlPanel;
|
||||
private System.Windows.Forms.Button button2;
|
||||
private System.Windows.Forms.Panel flowControlsPanel;
|
||||
private System.Windows.Forms.Button runButton;
|
||||
private System.Windows.Forms.Button stopButton;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.Button refreshMemoButton;
|
||||
private BufferedListView memoListView;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+392
-214
@@ -1,267 +1,445 @@
|
||||
using log4net.Config;
|
||||
using log4net;
|
||||
using AppDiagnostic.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using log4net.Appender;
|
||||
using SharedComponents;
|
||||
using static System.Windows.Forms.VisualStyles.VisualStyleElement;
|
||||
using System.IO; // Pre File.WriteAllLines a prácu so súbormi
|
||||
using System.Linq;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
private bool stopUpdate = false; // Premenná na zastavenie aktualizácie
|
||||
private int lastDisplayedLogIndex = 0;
|
||||
private bool autoScrollEnabled = true; // Automatický posun, predvolene povolený
|
||||
private MainForm appDiagnosticForm;
|
||||
private Timer refreshTimer;
|
||||
private LogAdapter _logAdapter; // Pre LogAdapter
|
||||
private LogFilter _logFilter; // LogFilter objekt pre filtráciu
|
||||
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()
|
||||
public MainForm() : this(null, null)
|
||||
{
|
||||
this.Icon = Properties.Resources.TBF_icon;
|
||||
}
|
||||
|
||||
public MainForm(Func<int> connectedClientCountProvider, Func<bool> activationRequestProvider)
|
||||
{
|
||||
this.connectedClientCountProvider = connectedClientCountProvider;
|
||||
this.activationRequestProvider = activationRequestProvider;
|
||||
InitializeComponent();
|
||||
Icon = Properties.Resources.TBF_icon;
|
||||
|
||||
// Inicializujeme LogAdapter a priradíme ho k memoListView
|
||||
_logAdapter = new LogAdapter(memoListView);
|
||||
regularLogFont = new Font(logView.Font, FontStyle.Regular);
|
||||
boldLogFont = new Font(logView.Font, FontStyle.Bold);
|
||||
colorRules = LogColorRuleRepository.Load();
|
||||
|
||||
RefreshLogView();
|
||||
InitializeFilters();
|
||||
|
||||
// Pripojenie na udalosť pri pridaní nového logu
|
||||
// nakoľko sa memo refreshuje cyklicky, tak refresh na udalosť nepotrebujem
|
||||
//LiveLogCache.Instance.LogAdded += OnLogAdded;
|
||||
refreshTimer = new Timer { Interval = 250 };
|
||||
refreshTimer.Tick += refreshTimer_Tick;
|
||||
refreshTimer.Start();
|
||||
|
||||
// Vytvoríme LogFilter objekt
|
||||
//_logFilter = new LogFilter(memoListView);
|
||||
}
|
||||
|
||||
private void OnLogAdded(string log)
|
||||
{
|
||||
try
|
||||
{
|
||||
RefreshLogView();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Error in OnLogAdded: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshLogView()
|
||||
{
|
||||
// Skontrolujte, či voláme z iného vlákna
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke((Action)RefreshLogView);
|
||||
return;
|
||||
}
|
||||
|
||||
memoListView.BeginUpdate(); // Zabraňuje vizuálnym aktualizáciám počas vykresľovania
|
||||
|
||||
try
|
||||
{
|
||||
// Skontrolujte, či je užívateľ na poslednom viditeľnom zázname
|
||||
if (memoListView.Items.Count > 0)
|
||||
{
|
||||
var lastVisibleIndex = memoListView.TopItem.Index + memoListView.ClientRectangle.Height / memoListView.Items[0].Bounds.Height;
|
||||
autoScrollEnabled = lastVisibleIndex >= memoListView.Items.Count - 1;
|
||||
}
|
||||
|
||||
// Získajte nové logy od posledného indexu
|
||||
var logs = LiveLogCache.Instance.GetLogs(lastDisplayedLogIndex, LiveLogCache.Instance.Logs.Count - lastDisplayedLogIndex);
|
||||
|
||||
// Pridajte len nové logy
|
||||
foreach (var log in logs)
|
||||
{
|
||||
memoListView.Items.Add(new ListViewItem(log));
|
||||
}
|
||||
|
||||
// Aktualizujte posledný zobrazený index
|
||||
lastDisplayedLogIndex = LiveLogCache.Instance.Logs.Count;
|
||||
|
||||
// Ak je autoScrollEnabled, posuňte sa na posledný záznam
|
||||
if (autoScrollEnabled && memoListView.Items.Count > 0)
|
||||
{
|
||||
memoListView.EnsureVisible(memoListView.Items.Count - 1);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
memoListView.EndUpdate(); // Umožní vizuálne aktualizácie
|
||||
}
|
||||
}
|
||||
|
||||
// Táto metóda sa spustí pri scrollovaní v memoListView (tzn. ak sa používateľ dostane na spodok listu)
|
||||
private void memoListView_Scroll(object sender, EventArgs e)
|
||||
{
|
||||
// Skontrolujte, či používateľ dosiahol spodok ListView
|
||||
if (memoListView.Items.Count > 0 &&
|
||||
memoListView.Items[memoListView.Items.Count - 1].Bounds.Bottom <= memoListView.ClientSize.Height)
|
||||
{
|
||||
// Ak áno, načítame ďalšie logy
|
||||
RefreshLogView();
|
||||
}
|
||||
}
|
||||
|
||||
private void button6_Click(object sender, EventArgs e)
|
||||
{
|
||||
//LiveLogCache.Instance.AddLog("Nový log z hlavnej aplikácie");
|
||||
|
||||
LiveLogCache.Instance.ClearLogs();
|
||||
ClearListView();
|
||||
}
|
||||
|
||||
private void runButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (stopUpdate) // Ak je časovač zastavený, spustíme ho
|
||||
{
|
||||
stopUpdate = false; // Zastav aktualizáciu
|
||||
runButton.Enabled = false; // Zakážeme tlačidlo Start počas behu
|
||||
stopButton.Enabled = true; // Povolenie tlačidla Stop
|
||||
refreshTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (!stopUpdate) // Ak časovač beží, môžeme ho zastaviť
|
||||
{
|
||||
stopUpdate = true; // Spusti aktualizáciu
|
||||
runButton.Enabled = true; // Povolenie tlačidla Start
|
||||
stopButton.Enabled = false; // Zakážeme tlačidlo Stop
|
||||
refreshTimer.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearListView()
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(new Action(ClearListView));
|
||||
return;
|
||||
}
|
||||
|
||||
memoListView.Items.Clear();
|
||||
}
|
||||
|
||||
private void refreshMemoButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void button7_Click(object sender, EventArgs e)
|
||||
{
|
||||
SaveLogsToFile();
|
||||
//LiveLogCache.Instance.AddLog("Toto je nový log.");
|
||||
RefreshView(true);
|
||||
}
|
||||
|
||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||
{
|
||||
// Odpojenie udalosti, keď sa okno zatvára, inak môže po opätovnom spustení okna a pridaní nového itemu do listu zhhodiť program
|
||||
// nakoľko sa memo refreshuje cyklicky, tak refresh na udalosť nepotrebujem
|
||||
//LiveLogCache.Instance.LogAdded -= OnLogAdded;
|
||||
|
||||
_logAdapter = null;
|
||||
|
||||
refreshTimer.Stop();
|
||||
refreshTimer.Dispose();
|
||||
regularLogFont.Dispose();
|
||||
boldLogFont.Dispose();
|
||||
base.OnFormClosing(e);
|
||||
}
|
||||
|
||||
private void filterButton_Click_1(object sender, EventArgs e)
|
||||
private void InitializeFilters()
|
||||
{
|
||||
// Aplikujeme filter na základe textu z filterTextBox
|
||||
string filterText = filterTextBox.Text;
|
||||
//_logFilter.ApplyFilter(filterText); // Aplikovanie filtra
|
||||
_logAdapter.ApplyFilter(filterText);
|
||||
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 SubForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
|
||||
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
|
||||
{
|
||||
// Vaša logika pri uzatváraní formy
|
||||
// this.Hide(); // Podforma sa len skryje
|
||||
Clipboard.SetText(text);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Ošetrenie výnimky
|
||||
MessageBox.Show("Chyba pri zatváraní pod-aplikácie: " + ex.Message);
|
||||
MessageBox.Show(this, $"Unable to copy text: {exception.Message}", "Clipboard error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveLogsToFile()
|
||||
private void wrapButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
using (SaveFileDialog saveFileDialog = new SaveFileDialog())
|
||||
{
|
||||
saveFileDialog.Filter = "Text Files (*.txt)|*.txt|All Files (*.*)|*.*";
|
||||
saveFileDialog.Title = "Save TBF live logs";
|
||||
saveFileDialog.DefaultExt = "txt";
|
||||
saveFileDialog.FileName = "TBFLiveLogs.txt";
|
||||
logView.WordWrap = wrapButton.Checked;
|
||||
}
|
||||
|
||||
if (saveFileDialog.ShowDialog() == DialogResult.OK)
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Načítanie všetkých logov z LiveLogCache
|
||||
var logs = LiveLogCache.Instance.GetLogs(0, LiveLogCache.Instance.GetCount());
|
||||
|
||||
// Uloženie logov do vybraného súboru
|
||||
File.WriteAllLines(saveFileDialog.FileName, logs);
|
||||
|
||||
MessageBox.Show("Logs saved successfully.", "Save TBF live logs", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"An error occurred while saving TBF live logs: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
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 button2_Click(object sender, EventArgs e)
|
||||
private void applyFilterButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
this.Close();
|
||||
//this.Hide(); // Podforma sa len skryje
|
||||
RefreshView(true);
|
||||
}
|
||||
|
||||
private void MemoListView_DoubleClick(object sender, EventArgs e)
|
||||
private void clearFilterButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (memoListView.SelectedItems.Count > 0)
|
||||
loadingFilters = true;
|
||||
filterTextBox.Clear();
|
||||
levelComboBox.SelectedIndex = 0;
|
||||
applicationComboBox.SelectedIndex = 0;
|
||||
loadingFilters = false;
|
||||
RefreshView(true);
|
||||
}
|
||||
|
||||
private void filterValueChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (!loadingFilters)
|
||||
{
|
||||
var selectedItem = memoListView.SelectedItems[0];
|
||||
int selectedIndex = memoListView.Items.IndexOf(selectedItem);
|
||||
RefreshView(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedIndex >= 0)
|
||||
{
|
||||
// Zrušenie filtra
|
||||
_logAdapter._filterText = string.Empty;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Načítanie logov od indexu vybraného riadku
|
||||
var logs = LiveLogCache.Instance.GetLogs(selectedIndex, LiveLogCache.Instance.GetCount() - selectedIndex);
|
||||
private void closeButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
// Obnovenie ListView s novými údajmi
|
||||
memoListView.BeginUpdate();
|
||||
memoListView.Items.Clear();
|
||||
foreach (var log in logs)
|
||||
{
|
||||
var item = new ListViewItem(log)
|
||||
{
|
||||
BackColor = memoListView.Items.Count % 2 == 0 ? Color.White : Color.FromArgb(240, 240, 240)
|
||||
};
|
||||
memoListView.Items.Add(item);
|
||||
}
|
||||
memoListView.EndUpdate();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
@@ -14,9 +15,26 @@ namespace AppDiagnostic
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new MainForm());
|
||||
bool isFirstInstance;
|
||||
|
||||
using (var instanceMutex = new Mutex(true, "Sensus.AppDiagnostic.SingleInstance", out isFirstInstance))
|
||||
using (var activationEvent = new EventWaitHandle(false, EventResetMode.AutoReset, "Sensus.AppDiagnostic.Activate"))
|
||||
{
|
||||
if (!isFirstInstance)
|
||||
{
|
||||
activationEvent.Set();
|
||||
return;
|
||||
}
|
||||
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
using (var diagnosticServer = new DiagnosticPipeServer(DiagnosticLogStore.Instance.Add))
|
||||
{
|
||||
diagnosticServer.Start();
|
||||
Application.Run(new MainForm(() => diagnosticServer.ConnectedClientCount, () => activationEvent.WaitOne(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,9 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\Strings.cs.resx" />
|
||||
<EmbeddedResource Include="Resources\Strings.de.resx" />
|
||||
<EmbeddedResource Include="Resources\Strings.de.resx">
|
||||
<SubType>Designer</SubType>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\Strings.fr.resx" />
|
||||
<EmbeddedResource Include="Resources\Strings.it.resx" />
|
||||
<EmbeddedResource Include="Resources\Strings.pl.resx" />
|
||||
|
||||
@@ -5,7 +5,6 @@ using CordonelPreadjustmentUi.Processes.Itinerary;
|
||||
using GenesisCordonelInterface.Core.Threading;
|
||||
using GenesisCordonelInterface.UI.LaatzenAPI_CordonelPreadjustmentUI;
|
||||
using NLog;
|
||||
using NLog.Fluent;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
@@ -54,6 +53,8 @@ namespace GenesisCordonelInterface.API
|
||||
|
||||
private readonly ConcurrentDictionary<int, bool> _selectedSlots = new ConcurrentDictionary<int, bool>();
|
||||
|
||||
private readonly ConcurrentDictionary<int, IDisposable> _streamingLeases = new ConcurrentDictionary<int, IDisposable>();
|
||||
|
||||
public InterfaceGCIToLaatzen()
|
||||
{
|
||||
|
||||
@@ -282,7 +283,7 @@ namespace GenesisCordonelInterface.API
|
||||
PortConfig? str,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(() => InitSlot(slot, cfg, pwd, req, str), token);
|
||||
return GetWorker(slot).RunAsync(() => InitSlot(slot, cfg, pwd, req, str), token, nameof(InitSlot));
|
||||
}
|
||||
|
||||
public PublicModels.GciInitSlotResult InitSlot(
|
||||
@@ -334,7 +335,7 @@ namespace GenesisCordonelInterface.API
|
||||
PortConfig? str,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(() => UpdateSlot(slot, cfg, pwd, req, str), token);
|
||||
return GetWorker(slot).RunAsync(() => UpdateSlot(slot, cfg, pwd, req, str), token, nameof(UpdateSlot));
|
||||
}
|
||||
|
||||
public PublicModels.GciInitSlotResult UpdateSlot(
|
||||
@@ -491,7 +492,7 @@ namespace GenesisCordonelInterface.API
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(() => GetOneMeterInfo(slot), token);
|
||||
return GetWorker(slot).RunAsync(() => GetOneMeterInfo(slot), token, nameof(GetOneMeterInfo));
|
||||
}
|
||||
|
||||
public PublicModels.GciSlotInfo GetOneMeterInfo(int slot)
|
||||
@@ -702,7 +703,7 @@ namespace GenesisCordonelInterface.API
|
||||
int slot,
|
||||
CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
return GetWorker(slot).RunAsync(() => DetectStreamingPort(slot), token);
|
||||
return GetWorker(slot).RunAsync(() => DetectStreamingPort(slot), token, nameof(DetectStreamingPort));
|
||||
}
|
||||
|
||||
public PortDetectionResult DetectStreamingPort(int slot)
|
||||
@@ -757,7 +758,7 @@ namespace GenesisCordonelInterface.API
|
||||
int slot,
|
||||
CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
return GetWorker(slot).RunAsync(() => DetectRequestPort(slot), token);
|
||||
return GetWorker(slot).RunAsync(() => DetectRequestPort(slot), token, nameof(DetectRequestPort));
|
||||
}
|
||||
|
||||
public PortDetectionResult DetectRequestPort(int slot)
|
||||
@@ -807,7 +808,7 @@ namespace GenesisCordonelInterface.API
|
||||
#region ================================== LOGIN ==================================
|
||||
public Task<PublicModels.GciLoginResult> LoginOneSlotAsync(int slot, CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(() => LoginOneSlot(slot), token);
|
||||
return GetWorker(slot).RunAsync(() => LoginOneSlot(slot), token, nameof(LoginOneSlot));
|
||||
}
|
||||
|
||||
public PublicModels.GciLoginResult LoginOneSlot(int slot)
|
||||
@@ -879,9 +880,106 @@ namespace GenesisCordonelInterface.API
|
||||
|
||||
#region ================================== CONNECT ==================================
|
||||
|
||||
/// <summary>
|
||||
/// Borrows the streaming port for one externally controlled preadjustment phase.
|
||||
/// The caller must stop its reader first and dispose the lease after all phase tasks finish.
|
||||
/// </summary>
|
||||
public Task<IDisposable> AcquirePreadjustmentStreamingPortAsync(
|
||||
int slot, PortConfig streamingPort, CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(() =>
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var meter = GetMeterThreadSafe(slot) as ZeroFlowGenesisMeter;
|
||||
if (meter == null || !meter.requestPortConfig.HasValue)
|
||||
throw new InvalidOperationException($"Slot {slot} must be initialized before borrowing its streaming port.");
|
||||
if (string.IsNullOrWhiteSpace(streamingPort.PortName) ||
|
||||
string.Equals(streamingPort.PortName, "NA", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(streamingPort.PortName, meter.requestPortConfig.Value.PortName, StringComparison.OrdinalIgnoreCase))
|
||||
throw new ArgumentException("A separate, valid streaming COM port is required.", nameof(streamingPort));
|
||||
|
||||
var lease = new StreamingPortLease(() =>
|
||||
GetWorker(slot).RunAsync(() =>
|
||||
{
|
||||
ReleasePreadjustmentStreamingPort(slot, meter);
|
||||
return true;
|
||||
}, CancellationToken.None, "ReleasePreadjustmentStreamingPort").GetAwaiter().GetResult());
|
||||
if (!_streamingLeases.TryAdd(slot, lease))
|
||||
throw new InvalidOperationException($"Slot {slot} already has an active streaming-port lease.");
|
||||
|
||||
try
|
||||
{
|
||||
// Borrow only streaming resources. Keep the existing request protocol,
|
||||
// authentication and process settings throughout the handoff.
|
||||
meter.ConnectStreamingPort(streamingPort);
|
||||
if (meter.StreamingPort == null || !meter.StreamingPort.IsOpen())
|
||||
throw new InvalidOperationException($"GCI could not open {streamingPort.PortName} for slot {slot}.");
|
||||
token.ThrowIfCancellationRequested();
|
||||
// Preparation originally installed this handler for the entire sequence.
|
||||
// Each newly created protocol needs its own subscription and decoder mode.
|
||||
meter.StartRecordData();
|
||||
LogInfo("AcquirePreadjustmentStreamingPort", $"Slot={slot}, Port={streamingPort.PortName}, Owner=GCI, LoggedOn={meter.IsLoggedOn}, RecordingStarted=True");
|
||||
return (IDisposable)lease;
|
||||
}
|
||||
catch (Exception acquisitionError)
|
||||
{
|
||||
// Cleanup is streaming-only, including cancellation during acquisition.
|
||||
try
|
||||
{
|
||||
meter.DisconnectStreamingPort();
|
||||
}
|
||||
catch (Exception restoreError)
|
||||
{
|
||||
throw new AggregateException("Streaming acquisition and streaming cleanup failed.", acquisitionError, restoreError);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_streamingLeases.TryRemove(slot, out _);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}, token, nameof(AcquirePreadjustmentStreamingPortAsync));
|
||||
}
|
||||
|
||||
private void ReleasePreadjustmentStreamingPort(int slot, ZeroFlowGenesisMeter meter)
|
||||
{
|
||||
string portName = meter.StreamingPort == null ? "NA" : meter.StreamingPort.GetPortName();
|
||||
try
|
||||
{
|
||||
meter.StopRecordData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// A failed decoder stop must never prevent physical port release.
|
||||
LogError("StopRecordDataBeforeStreamingRelease", ex);
|
||||
}
|
||||
|
||||
meter.DisconnectStreamingPort();
|
||||
_streamingLeases.TryRemove(slot, out _);
|
||||
LogInfo("ReleasePreadjustmentStreamingPort", $"Slot={slot}, Port={portName}, Owner=Reader, RequestConnected={meter.IsConnected}");
|
||||
}
|
||||
|
||||
private sealed class StreamingPortLease : IDisposable
|
||||
{
|
||||
private readonly object gate = new object();
|
||||
private Action release;
|
||||
|
||||
public StreamingPortLease(Action releaseAction) { release = releaseAction; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
if (release == null) return;
|
||||
release();
|
||||
release = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<PublicModels.GciConnectResult> ConnectOneSlotAsync(int slot, CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(() => ConnectOneSlot(slot), token);
|
||||
return GetWorker(slot).RunAsync(() => ConnectOneSlot(slot), token, nameof(ConnectOneSlot));
|
||||
}
|
||||
|
||||
public PublicModels.GciConnectResult ConnectOneSlot(int slot)
|
||||
@@ -972,7 +1070,7 @@ namespace GenesisCordonelInterface.API
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(() => Disconnect(slot), token);
|
||||
return GetWorker(slot).RunAsync(() => Disconnect(slot), token, nameof(Disconnect));
|
||||
}
|
||||
|
||||
public PublicModels.GciDisconnectResult Disconnect(int slot)
|
||||
@@ -1059,7 +1157,7 @@ namespace GenesisCordonelInterface.API
|
||||
int slot,
|
||||
CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
return GetWorker(slot).RunAsync(() => GetPcbId(slot), token, "GetPcbId");
|
||||
return GetWorker(slot).RunAsync(() => GetPcbId(slot), token, nameof(GetPcbId));
|
||||
}
|
||||
|
||||
public PublicModels.GciGetPcbIdResult GetPcbId(int slot)
|
||||
@@ -1112,7 +1210,7 @@ namespace GenesisCordonelInterface.API
|
||||
|
||||
public Task<RegisterReadResult> ReadRegisterAsync(int slot, string name, CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(() => ReadRegister(slot, name), token);
|
||||
return GetWorker(slot).RunAsync(() => ReadRegister(slot, name), token, nameof(ReadRegister));
|
||||
}
|
||||
|
||||
public RegisterReadResult ReadRegister(int slot, string name)
|
||||
@@ -1166,7 +1264,7 @@ namespace GenesisCordonelInterface.API
|
||||
bool refreshSystemState = false,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(() => WriteRegister(slot, registerName, value, storeToDevice, refreshSystemState), token);
|
||||
return GetWorker(slot).RunAsync(() => WriteRegister(slot, registerName, value, storeToDevice, refreshSystemState), token, nameof(WriteRegister));
|
||||
}
|
||||
|
||||
public RegisterWriteResult WriteRegister(
|
||||
@@ -1279,7 +1377,7 @@ namespace GenesisCordonelInterface.API
|
||||
{
|
||||
return GetWorker(slot).RunAsync(() => SetMeterPassword(slot, password),
|
||||
token,
|
||||
"SetMeterPassword");
|
||||
nameof(SetMeterPassword));
|
||||
}
|
||||
|
||||
public PublicModels.GciSetPasswordResult SetMeterPassword(
|
||||
@@ -1547,15 +1645,38 @@ namespace GenesisCordonelInterface.API
|
||||
public PreAdjustmentInitializationResult Preadjustment_Initialization(
|
||||
ProcessProgress pp,
|
||||
List<MeterStateControl> mc)
|
||||
{
|
||||
return Preadjustment_Initialization(pp, mc, CancellationToken.None);
|
||||
}
|
||||
|
||||
public PreAdjustmentInitializationResult Preadjustment_Initialization(
|
||||
ProcessProgress pp,
|
||||
List<MeterStateControl> mc,
|
||||
CancellationToken token)
|
||||
{
|
||||
const string operation = nameof(Preadjustment_Initialization);
|
||||
VerifyPreadjustmentAttemptAccess();
|
||||
|
||||
try
|
||||
{
|
||||
LogInfo(operation, "Start.");
|
||||
|
||||
_progressProcess = pp;
|
||||
_meterControls = mc;
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
lock (preadjustmentProcessGate)
|
||||
{
|
||||
if (activePreadjustmentCancellation != null || (_progressProcess != null && _progressProcess.IsBusy))
|
||||
throw new InvalidOperationException("Cannot initialize while a preadjustment process is running.");
|
||||
_progressProcess = pp;
|
||||
_meterControls = mc;
|
||||
// A new attempt must not inherit the previous selection/failure flags.
|
||||
foreach (var control in mc)
|
||||
{
|
||||
control.Failed = false;
|
||||
control.Ok = false;
|
||||
SetSlotSelected(control.Slot, control.IsEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
var initializedSlots = _meterBatch.ListOfMeters
|
||||
.Select(m => m.Slot)
|
||||
@@ -1610,6 +1731,7 @@ namespace GenesisCordonelInterface.API
|
||||
CancellationToken token = default)
|
||||
{
|
||||
const string operation = nameof(Preadjustment_Detect);
|
||||
VerifyPreadjustmentAttemptAccess();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1665,20 +1787,39 @@ namespace GenesisCordonelInterface.API
|
||||
|
||||
SetUnknownStatus(allMeterControls);
|
||||
|
||||
CheckNormalMeters(_meterControls, token);
|
||||
|
||||
CheckTemperatureMeters(
|
||||
_progressProcess.Setting,
|
||||
_tempMeterControls,
|
||||
token);
|
||||
|
||||
List<MeterStateControl> failedMeters = _meterControls.Where(m => m.IsEnabled && !m.Ok).ToList();
|
||||
foreach (var failed in failedMeters)
|
||||
{
|
||||
failed.Failed = true;
|
||||
failed.IsEnabled = false;
|
||||
}
|
||||
|
||||
return new PreadjustmentDetectResult
|
||||
{
|
||||
Success = true,
|
||||
DetectedMeterCount = _meterControls.Count(m => m.IsEnabled),
|
||||
DetectedThermometerCount = _tempMeterControls.Count(t => t.IsEnabled)
|
||||
Success = failedMeters.Count == 0,
|
||||
DetectedMeterCount = _meterControls.Count(m => m.IsEnabled && m.Ok),
|
||||
DetectedThermometerCount = _tempMeterControls.Count(t => t.IsEnabled && t.Ok),
|
||||
ErrorMessage = failedMeters.Count == 0 ? null : $"Request or streaming port check failed for slots: {string.Join(",", failedMeters.Select(m => m.Slot))}."
|
||||
};
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
// A batch-level detection error cannot reuse an earlier successful status.
|
||||
if (_meterControls != null)
|
||||
foreach (var control in _meterControls)
|
||||
{
|
||||
control.Ok = false;
|
||||
control.Failed = true;
|
||||
control.IsEnabled = false;
|
||||
}
|
||||
return new PreadjustmentDetectResult
|
||||
{
|
||||
Success = false,
|
||||
@@ -1784,17 +1925,25 @@ namespace GenesisCordonelInterface.API
|
||||
List<MeterStateControl> meterControls,
|
||||
CancellationToken token)
|
||||
{
|
||||
foreach (var meterCtl in meterControls)
|
||||
var checks = meterControls.Where(m => m != null && m.IsEnabled).Select(meterCtl =>
|
||||
GetWorker(meterCtl.Slot).RunAsync(() =>
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (meterCtl != null && meterCtl.IsEnabled)
|
||||
try
|
||||
{
|
||||
meterCtl.Ok =
|
||||
meterCtl.Ok = meterCtl.Meter != null &&
|
||||
meterCtl.Meter.CheckRequestPort() &&
|
||||
meterCtl.Meter.CheckStreamingPort();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
meterCtl.Ok = false;
|
||||
LogError($"DetectSlot{meterCtl.Slot}", ex);
|
||||
}
|
||||
return meterCtl.Ok;
|
||||
}, token, "PreadjustmentDetectSlot")).ToArray();
|
||||
Task.WhenAll(checks).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private void CheckTemperatureMeters(
|
||||
@@ -1825,22 +1974,30 @@ namespace GenesisCordonelInterface.API
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(
|
||||
() => PreAdjustment_Preparation(slot),
|
||||
token);
|
||||
() => PreAdjustment_Preparation(slot, token),
|
||||
token,
|
||||
nameof(PreAdjustment_Preparation));
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_PreparationDirect()
|
||||
{
|
||||
return PreAdjustment_Preparation();
|
||||
return PreAdjustment_PreparationDirect(CancellationToken.None);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_PreparationDirect(CancellationToken token)
|
||||
{
|
||||
return PreAdjustment_Preparation(-1, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes standalone preparation process.
|
||||
/// </summary>
|
||||
public PreAdjustmentProcessResult PreAdjustment_Preparation(
|
||||
int slot = -1)
|
||||
int slot = -1,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
const string operation = nameof(PreAdjustment_Preparation);
|
||||
VerifyPreadjustmentAttemptAccess();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1849,7 +2006,7 @@ namespace GenesisCordonelInterface.API
|
||||
BaseProcess process = CreatePreparationProcess(_progressProcess);
|
||||
|
||||
bool success =
|
||||
ExecuteProcess(process, _progressProcess);
|
||||
ExecuteProcess(process, _progressProcess, token);
|
||||
|
||||
var result =
|
||||
new PreAdjustmentProcessResult
|
||||
@@ -1866,6 +2023,7 @@ namespace GenesisCordonelInterface.API
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError(operation, ex);
|
||||
@@ -1924,19 +2082,27 @@ namespace GenesisCordonelInterface.API
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(
|
||||
() => PreAdjustment_AmplitudeTest(slot),
|
||||
token);
|
||||
() => PreAdjustment_AmplitudeTest(slot, token),
|
||||
token,
|
||||
nameof(PreAdjustment_AmplitudeTest));
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_AmplitudeTestDirect()
|
||||
{
|
||||
return PreAdjustment_AmplitudeTest();
|
||||
return PreAdjustment_AmplitudeTestDirect(CancellationToken.None);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_AmplitudeTestDirect(CancellationToken token)
|
||||
{
|
||||
return PreAdjustment_AmplitudeTest(-1, token);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_AmplitudeTest(
|
||||
int slot = -1)
|
||||
int slot = -1,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
const string operation = nameof(PreAdjustment_AmplitudeTest);
|
||||
VerifyPreadjustmentAttemptAccess();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1954,7 +2120,7 @@ namespace GenesisCordonelInterface.API
|
||||
|
||||
BaseProcess process = CreateAmplitudeTestProcess(_progressProcess);
|
||||
|
||||
bool success = ExecuteProcess(process, _progressProcess);
|
||||
bool success = ExecuteProcess(process, _progressProcess, token);
|
||||
|
||||
LogInfo(operation, $"Finished. Slot={slot}, Success={success}");
|
||||
|
||||
@@ -1966,6 +2132,7 @@ namespace GenesisCordonelInterface.API
|
||||
ErrorMessage = success ? null : process.FailedMessage
|
||||
};
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError(operation, ex);
|
||||
@@ -2014,57 +2181,116 @@ namespace GenesisCordonelInterface.API
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(
|
||||
() => PreAdjustment_TemperatureCalibration(slot),
|
||||
token);
|
||||
() => PreAdjustment_TemperatureCalibration(slot, token),
|
||||
token,
|
||||
nameof(PreAdjustment_TemperatureCalibration));
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect()
|
||||
{
|
||||
return PreAdjustment_TemperatureCalibration();
|
||||
return PreAdjustment_TemperatureCalibrationDirect(CancellationToken.None);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect(CancellationToken token)
|
||||
{
|
||||
return PreAdjustment_TemperatureCalibration(-1, token);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect(
|
||||
CalibrationTemperatureInput temperature, CancellationToken token)
|
||||
{
|
||||
return PreAdjustment_TemperatureCalibration(-1, token, temperature);
|
||||
}
|
||||
|
||||
public Task<PreAdjustmentProcessResult> PreAdjustment_TemperatureCalibrationAsync(
|
||||
int slot, CalibrationTemperatureInput temperature, CancellationToken token)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(
|
||||
() => PreAdjustment_TemperatureCalibration(slot, token, temperature),
|
||||
token, nameof(PreAdjustment_TemperatureCalibration));
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibration(
|
||||
int slot = -1)
|
||||
int slot = -1, CancellationToken token = default)
|
||||
{
|
||||
return PreAdjustment_TemperatureCalibration(slot, token, null);
|
||||
}
|
||||
|
||||
private PreAdjustmentProcessResult PreAdjustment_TemperatureCalibration(
|
||||
int slot, CancellationToken token, CalibrationTemperatureInput temperature)
|
||||
{
|
||||
const string operation = nameof(PreAdjustment_TemperatureCalibration);
|
||||
|
||||
VerifyPreadjustmentAttemptAccess();
|
||||
BaseProcess process = null;
|
||||
int[] participatingSlots = new int[0];
|
||||
try
|
||||
{
|
||||
LogInfo(operation, $"Start.");
|
||||
|
||||
BaseProcess process = CreateTemperatureCalibrationProcess(_progressProcess);
|
||||
|
||||
bool success = ExecuteProcess(process, _progressProcess);
|
||||
|
||||
var result =
|
||||
new PreAdjustmentProcessResult
|
||||
{
|
||||
Success = success,
|
||||
Slot = slot,
|
||||
ProcessName = process.ProcessName,
|
||||
ErrorMessage = success
|
||||
? null
|
||||
: process.FailedMessage
|
||||
};
|
||||
|
||||
LogInfo(operation, "Start.");
|
||||
participatingSlots = _meterControls.Where(m => m.IsEnabled).Select(m => m.Slot).ToArray();
|
||||
process = CreateTemperatureCalibrationProcess(_progressProcess, temperature);
|
||||
bool success = ExecuteProcess(process, _progressProcess, token);
|
||||
var result = BuildTemperatureCalibrationResult(process, participatingSlots, slot,
|
||||
success, success ? null : process.FailedMessage);
|
||||
LogInfo(operation, result.ToString());
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError(operation, ex);
|
||||
|
||||
return new PreAdjustmentProcessResult
|
||||
{
|
||||
Success = false,
|
||||
Slot = slot,
|
||||
ProcessName = "Temperature Calibration",
|
||||
ErrorMessage = ex.Message
|
||||
};
|
||||
return BuildTemperatureCalibrationResult(process, participatingSlots, slot, false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private TemperatureCalibrationResult BuildTemperatureCalibrationResult(
|
||||
BaseProcess process, int[] participatingSlots, int slot, bool success, string error)
|
||||
{
|
||||
var source = process as ITemperatureCalibrationMeasurements;
|
||||
var measurements = source == null
|
||||
? new Dictionary<int, CalibrationTemperatureMeasurement>()
|
||||
: source.GetTemperatureMeasurements().ToDictionary(m => m.Slot);
|
||||
var result = new TemperatureCalibrationResult
|
||||
{
|
||||
Success = success, Slot = slot, ProcessName = process?.ProcessName ?? "Temperature Calibration",
|
||||
ErrorMessage = error
|
||||
};
|
||||
foreach (int meterSlot in participatingSlots)
|
||||
{
|
||||
CalibrationTemperatureMeasurement measurement;
|
||||
measurements.TryGetValue(meterSlot, out measurement);
|
||||
var control = _meterControls.FirstOrDefault(m => m.Slot == meterSlot);
|
||||
bool completed = measurement != null && measurement.CalibrationCompleted &&
|
||||
control != null && !control.Failed;
|
||||
result.Slots.Add(new TemperatureCalibrationSlotResult
|
||||
{
|
||||
Slot = meterSlot, ProcessName = result.ProcessName, Success = completed,
|
||||
ReferenceTemperatureCelsius = measurement?.ReferenceTemperatureCelsius,
|
||||
MeterTemperatureCelsius = measurement?.MeterTemperatureCelsius,
|
||||
AbsoluteDifferenceCelsius = measurement?.AbsoluteDifferenceCelsius,
|
||||
DeviationLimitCelsius = measurement?.DeviationLimitCelsius,
|
||||
SampleCount = measurement?.SampleCount ?? 0,
|
||||
TemperatureCheckPassed = measurement?.TemperatureCheckPassed,
|
||||
ErrorMessage = completed ? null :
|
||||
(measurement?.ErrorMessage ?? error ?? "Temperature calibration did not complete.")
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public TemperatureCalibrationResult PreAdjustment_TemperatureCalibrationDetailedDirect(
|
||||
CalibrationTemperatureInput temperature, CancellationToken token)
|
||||
{
|
||||
return TemperatureCalibrationResult.FromProcessResult(
|
||||
PreAdjustment_TemperatureCalibration(-1, token, temperature));
|
||||
}
|
||||
|
||||
public async Task<TemperatureCalibrationResult> PreAdjustment_TemperatureCalibrationDetailedAsync(
|
||||
int slot, CalibrationTemperatureInput temperature, CancellationToken token)
|
||||
{
|
||||
var result = await PreAdjustment_TemperatureCalibrationAsync(slot, temperature, token).ConfigureAwait(false);
|
||||
return TemperatureCalibrationResult.FromProcessResult(result);
|
||||
}
|
||||
|
||||
public bool PreAdjustment_PushTemperature(double temperature)
|
||||
{
|
||||
if (_progressProcess != null)
|
||||
@@ -2082,7 +2308,7 @@ namespace GenesisCordonelInterface.API
|
||||
/// </summary>
|
||||
/// <param name="pp">Current process progress context</param>
|
||||
/// <returns>Configured temperature calibration process</returns>
|
||||
private BaseProcess CreateTemperatureCalibrationProcess(ProcessProgress pp)
|
||||
private BaseProcess CreateTemperatureCalibrationProcess(ProcessProgress pp, CalibrationTemperatureInput temperature)
|
||||
{
|
||||
if (pp.Setting.NumberOfPaths == 1)
|
||||
{
|
||||
@@ -2091,7 +2317,7 @@ namespace GenesisCordonelInterface.API
|
||||
PreAdjustmentControl.StatusPanelItems.TempCal,
|
||||
PreAdjustmentControl.PredefinedMessages.WaitUntilTemperatureCalibrationFinished(pp.Setting.Culture),
|
||||
PreAdjustmentControl.PredefinedMessages.TempCalFailed(pp.Setting.Culture),
|
||||
2 * 60);
|
||||
2 * 60) { DirectTemperature = temperature };
|
||||
}
|
||||
|
||||
return new TemperatureCalibrationProcess(
|
||||
@@ -2099,7 +2325,7 @@ namespace GenesisCordonelInterface.API
|
||||
PreAdjustmentControl.StatusPanelItems.TempCal,
|
||||
PreAdjustmentControl.PredefinedMessages.WaitUntilTemperatureCalibrationFinished(pp.Setting.Culture),
|
||||
PreAdjustmentControl.PredefinedMessages.TempCalFailed(pp.Setting.Culture),
|
||||
2 * 60);
|
||||
2 * 60) { DirectTemperature = temperature };
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -2111,19 +2337,27 @@ namespace GenesisCordonelInterface.API
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(
|
||||
() => PreAdjustment_OffsetTest(slot),
|
||||
token);
|
||||
() => PreAdjustment_OffsetTest(slot, token),
|
||||
token,
|
||||
nameof(PreAdjustment_OffsetTest));
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_OffsetTestDirect()
|
||||
{
|
||||
return PreAdjustment_OffsetTest();
|
||||
return PreAdjustment_OffsetTestDirect(CancellationToken.None);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_OffsetTestDirect(CancellationToken token)
|
||||
{
|
||||
return PreAdjustment_OffsetTest(-1, token);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_OffsetTest(
|
||||
int slot = -1)
|
||||
int slot = -1,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
const string operation = nameof(PreAdjustment_OffsetTest);
|
||||
VerifyPreadjustmentAttemptAccess();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -2141,7 +2375,7 @@ namespace GenesisCordonelInterface.API
|
||||
|
||||
BaseProcess process = CreateOffsetTestProcess(_progressProcess);
|
||||
|
||||
bool success = ExecuteProcess(process, _progressProcess);
|
||||
bool success = ExecuteProcess(process, _progressProcess, token);
|
||||
|
||||
var result =
|
||||
new PreAdjustmentProcessResult
|
||||
@@ -2158,6 +2392,7 @@ namespace GenesisCordonelInterface.API
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError(operation, ex);
|
||||
@@ -2206,19 +2441,27 @@ namespace GenesisCordonelInterface.API
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetWorker(slot).RunAsync(
|
||||
() => PreAdjustment_Completion(slot),
|
||||
token);
|
||||
() => PreAdjustment_Completion(slot, token),
|
||||
token,
|
||||
nameof(PreAdjustment_Completion));
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_CompletionDirect()
|
||||
{
|
||||
return PreAdjustment_Completion();
|
||||
return PreAdjustment_CompletionDirect(CancellationToken.None);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_CompletionDirect(CancellationToken token)
|
||||
{
|
||||
return PreAdjustment_Completion(-1, token);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_Completion(
|
||||
int slot = -1)
|
||||
int slot = -1,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
const string operation = nameof(PreAdjustment_Completion);
|
||||
VerifyPreadjustmentAttemptAccess();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -2226,10 +2469,10 @@ namespace GenesisCordonelInterface.API
|
||||
|
||||
BaseProcess process = CreateCompletionProcess(_progressProcess);
|
||||
|
||||
bool success = ExecuteProcess(process, _progressProcess);
|
||||
bool success = ExecuteProcess(process, _progressProcess, token);
|
||||
|
||||
var result =
|
||||
new PreAdjustmentProcessResult
|
||||
new CompletionProcessResult
|
||||
{
|
||||
Success = success,
|
||||
Slot = slot,
|
||||
@@ -2239,10 +2482,12 @@ namespace GenesisCordonelInterface.API
|
||||
: process.FailedMessage
|
||||
};
|
||||
|
||||
CaptureCompletionDatabaseRecords(process, result);
|
||||
LogInfo(operation, result.ToString());
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogError(operation, ex);
|
||||
@@ -2257,6 +2502,54 @@ namespace GenesisCordonelInterface.API
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureCompletionDatabaseRecords(BaseProcess process, CompletionProcessResult result)
|
||||
{
|
||||
var completion = process as CompletionProcess;
|
||||
if (completion == null)
|
||||
{
|
||||
// Single-path Completion has a different readout contract.
|
||||
foreach (var control in _meterControls.Where(m => m.IsEnabled || m.Failed))
|
||||
_progressProcess.DebugMessage("SQL Completion export is unavailable for this process type.", control.Slot, "DB");
|
||||
return;
|
||||
}
|
||||
var snapshots = completion.GetDatabaseSnapshots();
|
||||
foreach (var snapshot in snapshots)
|
||||
{
|
||||
int pcbId;
|
||||
int.TryParse(snapshot.PcbId, out pcbId);
|
||||
// The writer reports invalid identity without sending it to SQL.
|
||||
result.DatabaseRecords.Add(new PreadjustmentCompletionRecord
|
||||
{
|
||||
Slot = snapshot.Slot, PcbId = pcbId, MeterSize = snapshot.MeterSize,
|
||||
ZeroOffset1 = snapshot.ZeroOffset1, ZeroOffset2 = snapshot.ZeroOffset2,
|
||||
ZeroOffset3 = snapshot.ZeroOffset3,
|
||||
FirstHitUpdatePeriod = snapshot.FirstHitUpdatePeriod,
|
||||
FirstHitShift = snapshot.FirstHitShift,
|
||||
FirstHitPercent1 = snapshot.FirstHitPercent1,
|
||||
FirstHitPercent2 = snapshot.FirstHitPercent2,
|
||||
FirstHitPercent3 = snapshot.FirstHitPercent3,
|
||||
ToFTempOffset1 = snapshot.ToFTempOffset1,
|
||||
ToFTempOffset2 = snapshot.ToFTempOffset2,
|
||||
ToFTempOffset3 = snapshot.ToFTempOffset3,
|
||||
Date = snapshot.Date, Successful = snapshot.Successful
|
||||
});
|
||||
}
|
||||
foreach (var control in _meterControls.Where(m => m.IsEnabled || m.Failed))
|
||||
{
|
||||
if (snapshots.Any(s => s.Slot == control.Slot)) continue;
|
||||
_progressProcess.DebugMessage("SQL write skipped: Completion produced no verified register readout.", control.Slot, "DB");
|
||||
result.DatabaseWrites.Add(new CompletionDatabaseWriteResult
|
||||
{
|
||||
Slot = control.Slot,
|
||||
Result = new CalibrationWriteResult
|
||||
{
|
||||
Status = CalibrationWriteStatus.Failed,
|
||||
ErrorMessage = "Completion produced no verified register readout."
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates completion process instance based on current configuration.
|
||||
/// </summary>
|
||||
@@ -2285,36 +2578,184 @@ namespace GenesisCordonelInterface.API
|
||||
#endregion
|
||||
|
||||
#region ================================== PreAdjustmentUI ExecuteProcess ==================================
|
||||
private bool ExecuteProcess(
|
||||
BaseProcess process,
|
||||
ProcessProgress pp)
|
||||
private readonly AsyncLocal<object> preadjustmentAttemptContext = new AsyncLocal<object>();
|
||||
private object activePreadjustmentAttempt;
|
||||
|
||||
private void VerifyPreadjustmentAttemptAccess()
|
||||
{
|
||||
pp.IsBusy = true;
|
||||
|
||||
process.StartProcess(
|
||||
pp,
|
||||
_meterControls,
|
||||
_tempMeterControls);
|
||||
|
||||
while (pp.IsBusy && !pp.StopSequence)
|
||||
lock (preadjustmentProcessGate)
|
||||
{
|
||||
Thread.Sleep(50);
|
||||
if (activePreadjustmentAttempt != null &&
|
||||
!ReferenceEquals(activePreadjustmentAttempt, preadjustmentAttemptContext.Value))
|
||||
throw new InvalidOperationException("Another preadjustment attempt owns the slot selection.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes one synchronous phase attempt with a temporary list of participating slots.
|
||||
/// The callback must return only after all phase workers and streaming leases are drained.
|
||||
/// This changes process participation only; it does not connect, disconnect or log in.
|
||||
/// </summary>
|
||||
public T ExecutePreadjustmentAttempt<T>(IEnumerable<int> slots, bool retry,
|
||||
bool initialization, CancellationToken token, Func<T> action)
|
||||
{
|
||||
if (slots == null) throw new ArgumentNullException(nameof(slots));
|
||||
if (action == null) throw new ArgumentNullException(nameof(action));
|
||||
var selected = new HashSet<int>(slots);
|
||||
if (selected.Count == 0 || selected.Any(s => s <= 0))
|
||||
throw new ArgumentException("A phase attempt requires valid slots.", nameof(slots));
|
||||
|
||||
List<MeterStateControl> original;
|
||||
Dictionary<int, bool> originalSelection;
|
||||
object previousContext = preadjustmentAttemptContext.Value;
|
||||
var owner = new object();
|
||||
lock (preadjustmentProcessGate)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (activePreadjustmentAttempt != null || activePreadjustmentCancellation != null ||
|
||||
(_progressProcess != null && _progressProcess.IsBusy))
|
||||
throw new InvalidOperationException("A preadjustment process is still running.");
|
||||
original = (_meterControls ?? new List<MeterStateControl>()).ToList();
|
||||
if (!initialization && selected.Any(s => !original.Any(c => c.Slot == s)))
|
||||
throw new InvalidOperationException("Initialize preadjustment before retrying a phase.");
|
||||
originalSelection = original.ToDictionary(c => c.Slot, c => IsSlotSelected(c.Slot));
|
||||
activePreadjustmentAttempt = owner;
|
||||
preadjustmentAttemptContext.Value = owner;
|
||||
}
|
||||
|
||||
foreach (var meterCtrl in _meterControls)
|
||||
try
|
||||
{
|
||||
if (meterCtrl.IsEnabled && meterCtrl.Failed)
|
||||
if (!initialization)
|
||||
{
|
||||
pp.GenerateReturnNote(process.FailedMessage, meterCtrl);
|
||||
meterCtrl.IsEnabled = false;
|
||||
_meterControls = original.Where(c => selected.Contains(c.Slot)).ToList();
|
||||
foreach (var control in _meterControls)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (retry)
|
||||
{
|
||||
// Restore execution state only. Do not call SetChecked here:
|
||||
// its UI dispatch may block on the thread that created the control.
|
||||
// IsEnabled controls phase eligibility; keep meter calibration data.
|
||||
control.Failed = false;
|
||||
control.Ok = false;
|
||||
control.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
if (retry && _progressProcess != null) _progressProcess.Aborted = false;
|
||||
foreach (var control in original)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
SetSlotSelected(control.Slot, selected.Contains(control.Slot) && control.IsEnabled);
|
||||
}
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
return action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
// Detect can create replacement controls. Merge these back by slot so successful
|
||||
// meters from an earlier attempt remain available for subsequent phases.
|
||||
if (!initialization)
|
||||
{
|
||||
var updated = (_meterControls ?? new List<MeterStateControl>())
|
||||
.Where(c => selected.Contains(c.Slot)).ToDictionary(c => c.Slot);
|
||||
_meterControls = original.Select(c => updated.ContainsKey(c.Slot) ? updated[c.Slot] : c).ToList();
|
||||
foreach (var control in _meterControls)
|
||||
SetSlotSelected(control.Slot, selected.Contains(control.Slot)
|
||||
? control.IsEnabled && !control.Failed : originalSelection[control.Slot]);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (preadjustmentProcessGate)
|
||||
{
|
||||
activePreadjustmentAttempt = null;
|
||||
preadjustmentAttemptContext.Value = previousContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pp.StopSequence)
|
||||
return false;
|
||||
private readonly object preadjustmentProcessGate = new object();
|
||||
private CancellationTokenSource activePreadjustmentCancellation;
|
||||
|
||||
return _meterControls.Any(m => m.IsEnabled && !m.Failed);
|
||||
public void CancelPreadjustment()
|
||||
{
|
||||
CancellationTokenSource cancellation;
|
||||
lock (preadjustmentProcessGate)
|
||||
cancellation = activePreadjustmentCancellation;
|
||||
if (cancellation == null) return;
|
||||
try { cancellation.Cancel(); }
|
||||
catch (ObjectDisposedException) { /* The process has already completed. */ }
|
||||
}
|
||||
|
||||
private bool ExecuteProcess(BaseProcess process, ProcessProgress pp, CancellationToken token)
|
||||
{
|
||||
CancellationTokenSource previousSource;
|
||||
CancellationTokenSource runSource;
|
||||
lock (preadjustmentProcessGate)
|
||||
{
|
||||
VerifyPreadjustmentAttemptAccess();
|
||||
if (activePreadjustmentCancellation != null || pp.IsBusy)
|
||||
throw new InvalidOperationException("A preadjustment process is still running.");
|
||||
token.ThrowIfCancellationRequested();
|
||||
previousSource = pp.CancellationSource;
|
||||
// A cancelled, completed run must not poison the next explicit run.
|
||||
if (previousSource == null || previousSource.IsCancellationRequested)
|
||||
previousSource = new CancellationTokenSource();
|
||||
runSource = CancellationTokenSource.CreateLinkedTokenSource(token, previousSource.Token);
|
||||
activePreadjustmentCancellation = runSource;
|
||||
lock (pp) pp.CancellationSource = runSource;
|
||||
pp.StopSequence = false;
|
||||
pp.IsBusy = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_meterControls == null || !_meterControls.Any(m => m.IsEnabled && !m.Failed))
|
||||
throw new InvalidOperationException("No eligible meters remain; preadjustment process was not executed. Run Initialization and Detect for a new attempt.");
|
||||
foreach (var meterCtrl in _meterControls) meterCtrl.Ok = false;
|
||||
process.StartProcess(pp, _meterControls, _tempMeterControls);
|
||||
// Do not return when cancellation is requested: wait for every owned task.
|
||||
process.Completion.GetAwaiter().GetResult();
|
||||
runSource.Token.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (var meterCtrl in _meterControls)
|
||||
{
|
||||
// Set only after all phase tasks completed; failed/absent slots stay false.
|
||||
meterCtrl.Ok = meterCtrl.IsEnabled && !meterCtrl.Failed;
|
||||
if (meterCtrl.IsEnabled && meterCtrl.Failed)
|
||||
{
|
||||
runSource.Token.ThrowIfCancellationRequested();
|
||||
// This API runs in a background batch. Return-note generation can show
|
||||
// a modal MessageBox/PDF UI and would leave the batch waiting forever.
|
||||
// The interactive PreAdjustmentControl retains responsibility for reports.
|
||||
pp.DebugMessage(
|
||||
process.ProcessName + (activePreadjustmentAttempt != null
|
||||
? " failed in this attempt."
|
||||
: " failed; slot excluded from remaining preadjustment phases."),
|
||||
meterCtrl.Slot,
|
||||
"GCI",
|
||||
meterCtrl.Meter == null ? string.Empty : meterCtrl.Meter.PcbId);
|
||||
meterCtrl.IsEnabled = false;
|
||||
}
|
||||
}
|
||||
return _meterControls.Any(m => m.IsEnabled && !m.Failed);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Completion has drained workers before the shared progress can be reused.
|
||||
lock (preadjustmentProcessGate)
|
||||
{
|
||||
lock (pp) pp.CancellationSource = previousSource;
|
||||
pp.IsBusy = false;
|
||||
activePreadjustmentCancellation = null;
|
||||
}
|
||||
runSource.Dispose();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,82 @@ namespace GenesisCordonelInterface.API
|
||||
public event Action<List<MeterBatchDebugStatus>> MeterBatchStatusChanged;
|
||||
private readonly IMeterLoginPasswordReader loginPasswordsReader;
|
||||
private readonly IPreAdjustmentCalibrationParamsReader calibrationParamsReader;
|
||||
private readonly Core.DataStorage.Writing.CalibrationResultWriter calibrationResultWriter =
|
||||
new Core.DataStorage.Writing.CalibrationResultWriter(null);
|
||||
|
||||
/// <summary>Stores one detached completion record without accessing meter ports.</summary>
|
||||
public Task<CalibrationWriteResult> StorePreadjustmentCompletionResultAsync(
|
||||
PreadjustmentCompletionRecord record, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
if (record == null) throw new ArgumentNullException(nameof(record));
|
||||
// Capture the progress instance and slot for this write, even if a new
|
||||
// batch is initialized while the database operation is still finishing.
|
||||
var progress = _innerMeterAPI?._progressProcess;
|
||||
int? slot = record.Slot;
|
||||
Action<string> diagnostic = null;
|
||||
if (progress != null && slot.HasValue && slot.Value > 0)
|
||||
diagnostic = message => progress.DebugMessage(message, slot.Value, "DB");
|
||||
return calibrationResultWriter.StorePreadjustmentCompletionAsync(record, token, diagnostic);
|
||||
}
|
||||
|
||||
/// <summary>Creates a synthetic Completion record and exercises the real configured SQL writer.</summary>
|
||||
public async Task<CompletionProcessResult> StoreSimulatedCompletionResultAsync(
|
||||
int slot, int pcbId, bool phaseSuccess, string phaseError,
|
||||
CancellationToken token, Action<string> diagnostic)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (slot <= 0) throw new ArgumentOutOfRangeException(nameof(slot));
|
||||
// Synthetic fixture values belong to GCI, not to the hardware Completion process.
|
||||
var record = new PreadjustmentCompletionRecord
|
||||
{
|
||||
Slot = slot, PcbId = pcbId, MeterSize = 2,
|
||||
ZeroOffset1 = -20, ZeroOffset2 = -30, ZeroOffset3 = -67,
|
||||
FirstHitUpdatePeriod = 10, FirstHitShift = 4,
|
||||
FirstHitPercent1 = 17, FirstHitPercent2 = 13, FirstHitPercent3 = 17,
|
||||
ToFTempOffset1 = 22260, ToFTempOffset2 = -115410, ToFTempOffset3 = 14992,
|
||||
Date = DateTime.UtcNow, Successful = phaseSuccess
|
||||
};
|
||||
// Optional diagnostic subscribers must never change the storage outcome.
|
||||
try { diagnostic?.Invoke("Simulated calibration data; actual SQL write requested. Fixture MeterSize=2."); }
|
||||
catch (Exception) { }
|
||||
var write = await calibrationResultWriter.StorePreadjustmentCompletionAsync(
|
||||
record, token, diagnostic).ConfigureAwait(false);
|
||||
var result = new CompletionProcessResult
|
||||
{
|
||||
Slot = slot, Success = phaseSuccess, ProcessName = "PreAdjustment_Completion",
|
||||
ErrorMessage = phaseError
|
||||
};
|
||||
result.DatabaseRecords.Add(record);
|
||||
result.DatabaseWrites.Add(new CompletionDatabaseWriteResult { Slot = slot, Result = write });
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Stores Q3 values supplied by TBF; does not perform Q3 calibration.</summary>
|
||||
public Task<CalibrationWriteResult> StoreQ3CalibrationResultAsync(
|
||||
Q3CalibrationRecord record, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
return calibrationResultWriter.StoreQ3CalibrationAsync(record, token);
|
||||
}
|
||||
|
||||
public Task<CalibrationWriteResult> StoreQ3CalibrationResultAsync(
|
||||
Q3CalibrationRecord record, CancellationToken token, Action<string> diagnostic)
|
||||
{
|
||||
return calibrationResultWriter.StoreQ3CalibrationAsync(record, token, diagnostic);
|
||||
}
|
||||
|
||||
public Task<CalibrationWriteResult> StoreSimulatedQ3CalibrationResultAsync(
|
||||
int pcbId, CancellationToken token, Action<string> diagnostic)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
try { diagnostic?.Invoke("Simulated Q3 fixture values; actual SQL write requested."); }
|
||||
catch (Exception) { }
|
||||
return calibrationResultWriter.StoreQ3CalibrationAsync(new Q3CalibrationRecord
|
||||
{
|
||||
PcbId = pcbId, P1Offset = -0.3, P1Cal = 15260,
|
||||
P2Offset = -0.3, P2Cal = 15451, P3Offset = -0.3, P3Cal = 15204,
|
||||
Date = DateTime.UtcNow
|
||||
}, token, diagnostic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the public GCI facade.
|
||||
@@ -58,10 +134,20 @@ namespace GenesisCordonelInterface.API
|
||||
InterfaceGCIToLaatzen innerMeterApi,
|
||||
IMeterLoginPasswordReader passwordReader,
|
||||
IPreAdjustmentCalibrationParamsReader calibrationReader)
|
||||
: this(innerMeterApi, passwordReader, calibrationReader, null)
|
||||
{
|
||||
}
|
||||
|
||||
public InterfaceOutsideToGCI(
|
||||
InterfaceGCIToLaatzen innerMeterApi,
|
||||
IMeterLoginPasswordReader passwordReader,
|
||||
IPreAdjustmentCalibrationParamsReader calibrationReader,
|
||||
GciDataStorageConfig storageConfig)
|
||||
{
|
||||
_innerMeterAPI = innerMeterApi?? throw new ArgumentNullException(nameof(innerMeterApi));
|
||||
loginPasswordsReader = passwordReader ?? throw new ArgumentNullException(nameof(passwordReader));
|
||||
calibrationParamsReader = calibrationReader ?? throw new ArgumentNullException(nameof(calibrationReader));
|
||||
calibrationResultWriter = new Core.DataStorage.Writing.CalibrationResultWriter(storageConfig);
|
||||
|
||||
//init handlers
|
||||
//_innerMeterAPI._progressProcess.OnRequestedCalibrationParamsFromDb += RequestedCalibrationParamsFromDb;
|
||||
@@ -71,23 +157,36 @@ namespace GenesisCordonelInterface.API
|
||||
object sender,
|
||||
EventArgsProcessProgress e)
|
||||
{
|
||||
var progress = e.Value;
|
||||
var source = progress.CancellationSource;
|
||||
var token = source.Token;
|
||||
try
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
DataQuery query = new DataQuery();
|
||||
query.QueryParams.Add(((int)e.Value.Setting.MeterSize).ToString());
|
||||
|
||||
Dictionary<string, UInt32> calibrationParams = await ReadPreAdjustmentCalibrationParamsAsync(query).ConfigureAwait(false);
|
||||
Dictionary<string, UInt32> calibrationParams = await ReadPreAdjustmentCalibrationParamsAsync(query, token).ConfigureAwait(false);
|
||||
|
||||
e.Value.PushedCalibrationParams = calibrationParams;
|
||||
lock (progress)
|
||||
{
|
||||
if (!token.IsCancellationRequested && ReferenceEquals(progress.CancellationSource, source))
|
||||
progress.PushedCalibrationParams = calibrationParams;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
e.Value.DebugMessage(
|
||||
$"Calibration params reading failed: {ex.Message}");
|
||||
if (!token.IsCancellationRequested && ReferenceEquals(progress.CancellationSource, source))
|
||||
progress.DebugMessage($"Calibration params reading failed: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
e.Value.CalibrationParamsReadEvent.Set();
|
||||
lock (progress)
|
||||
{
|
||||
if (!token.IsCancellationRequested && ReferenceEquals(progress.CancellationSource, source))
|
||||
progress.CalibrationParamsReadEvent.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +230,16 @@ namespace GenesisCordonelInterface.API
|
||||
|
||||
#region ================================== INIT/UPDATE/GET slot ==================================
|
||||
|
||||
/// <summary>Acquires streaming reception until the returned lease is disposed.</summary>
|
||||
public Task<IDisposable> AcquirePreadjustmentStreamingPortAsync(
|
||||
int slot, GciPortConfig streamingPort, CancellationToken token = default)
|
||||
{
|
||||
if (streamingPort == null) throw new ArgumentNullException(nameof(streamingPort));
|
||||
var mappedPort = ModelsMapping.MapPort(streamingPort);
|
||||
if (!mappedPort.HasValue) throw new ArgumentException("A streaming port is required.", nameof(streamingPort));
|
||||
return _innerMeterAPI.AcquirePreadjustmentStreamingPortAsync(slot, mappedPort.Value, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a meter slot using the provided slot configuration.
|
||||
/// </summary>
|
||||
@@ -638,11 +747,27 @@ namespace GenesisCordonelInterface.API
|
||||
List<MeterStateControl> mc)
|
||||
{
|
||||
//init handlers
|
||||
pp.OnRequestedCalibrationParamsFromDb -= RequestedCalibrationParamsFromDb;
|
||||
pp.OnRequestedCalibrationParamsFromDb += RequestedCalibrationParamsFromDb;
|
||||
|
||||
return _innerMeterAPI?.Preadjustment_Initialization(pp, mc);
|
||||
}
|
||||
|
||||
public PreAdjustmentInitializationResult Preadjustment_Initialization(
|
||||
ProcessProgress pp,
|
||||
List<MeterStateControl> mc,
|
||||
CancellationToken token)
|
||||
{
|
||||
pp.OnRequestedCalibrationParamsFromDb -= RequestedCalibrationParamsFromDb;
|
||||
pp.OnRequestedCalibrationParamsFromDb += RequestedCalibrationParamsFromDb;
|
||||
return _innerMeterAPI?.Preadjustment_Initialization(pp, mc, token);
|
||||
}
|
||||
|
||||
public void CancelPreadjustment()
|
||||
{
|
||||
_innerMeterAPI?.CancelPreadjustment();
|
||||
}
|
||||
|
||||
public Task<PreadjustmentDetectResult> PreAdjustment_DetectAsync(
|
||||
IEnumerable<PublicModels.MeterBatchDebugStatus> selectedSlots,
|
||||
CancellationToken token = default)
|
||||
@@ -669,6 +794,11 @@ namespace GenesisCordonelInterface.API
|
||||
return _innerMeterAPI.PreAdjustment_PreparationDirect();
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_PreparationDirect(CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_PreparationDirect(token);
|
||||
}
|
||||
|
||||
public Task<PreAdjustmentProcessResult> PreAdjustment_AmplitudeTestAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
@@ -681,6 +811,11 @@ namespace GenesisCordonelInterface.API
|
||||
return _innerMeterAPI.PreAdjustment_AmplitudeTestDirect();
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_AmplitudeTestDirect(CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_AmplitudeTestDirect(token);
|
||||
}
|
||||
|
||||
public Task<PreAdjustmentProcessResult> PreAdjustment_TemperatureCalibrationAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
@@ -693,12 +828,49 @@ namespace GenesisCordonelInterface.API
|
||||
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDirect();
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect(CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDirect(token);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect(
|
||||
CalibrationTemperatureInput temperature, CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDirect(temperature, token);
|
||||
}
|
||||
|
||||
public Task<PreAdjustmentProcessResult> PreAdjustment_TemperatureCalibrationAsync(
|
||||
int slot, CalibrationTemperatureInput temperature, CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationAsync(slot, temperature, token);
|
||||
}
|
||||
|
||||
public TemperatureCalibrationResult PreAdjustment_TemperatureCalibrationDetailedDirect(
|
||||
CalibrationTemperatureInput temperature, CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDetailedDirect(temperature, token);
|
||||
}
|
||||
|
||||
public Task<TemperatureCalibrationResult> PreAdjustment_TemperatureCalibrationDetailedAsync(
|
||||
int slot, CalibrationTemperatureInput temperature, CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDetailedAsync(slot, temperature, token);
|
||||
}
|
||||
|
||||
public bool PreAdjustment_PushTemperature(
|
||||
double temperature)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_PushTemperature(temperature);
|
||||
}
|
||||
|
||||
public bool PreAdjustment_PushTemperature(
|
||||
double temperature,
|
||||
CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
return _innerMeterAPI.PreAdjustment_PushTemperature(temperature);
|
||||
}
|
||||
|
||||
public Task<PreAdjustmentProcessResult> PreAdjustment_OffsetTestAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
@@ -711,6 +883,11 @@ namespace GenesisCordonelInterface.API
|
||||
return _innerMeterAPI.PreAdjustment_OffsetTestDirect();
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_OffsetTestDirect(CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_OffsetTestDirect(token);
|
||||
}
|
||||
|
||||
public Task<PreAdjustmentProcessResult> PreAdjustment_CompletionAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
@@ -723,6 +900,11 @@ namespace GenesisCordonelInterface.API
|
||||
return _innerMeterAPI.PreAdjustment_CompletionDirect();
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_CompletionDirect(CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_CompletionDirect(token);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================================== UNI DATA STORAGE READER ==================================
|
||||
@@ -782,4 +964,4 @@ namespace GenesisCordonelInterface.API
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,87 @@ namespace GenesisCordonelInterface.API
|
||||
/// </summary>
|
||||
public class PublicModels
|
||||
{
|
||||
/// <summary>Persisted values for one meter and one preadjustment attempt.</summary>
|
||||
public sealed class PreadjustmentCompletionRecord
|
||||
{
|
||||
// Diagnostic routing only. Not a database column or SQL parameter.
|
||||
public int? Slot { get; set; }
|
||||
public int PcbId { get; set; }
|
||||
public int? MeterSize { get; set; }
|
||||
public int? ZeroOffset1 { get; set; }
|
||||
public int? ZeroOffset2 { get; set; }
|
||||
public int? ZeroOffset3 { get; set; }
|
||||
public int? FirstHitUpdatePeriod { get; set; }
|
||||
public int? FirstHitShift { get; set; }
|
||||
public int? FirstHitPercent1 { get; set; }
|
||||
public int? FirstHitPercent2 { get; set; }
|
||||
public int? FirstHitPercent3 { get; set; }
|
||||
public int? ToFTempOffset1 { get; set; }
|
||||
public int? ToFTempOffset2 { get; set; }
|
||||
public int? ToFTempOffset3 { get; set; }
|
||||
// Timestamp of the attempt, not the time of a possible later write retry.
|
||||
public DateTime Date { get; set; }
|
||||
public bool Successful { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Q3 values calculated and confirmed by the caller; no port access.</summary>
|
||||
public sealed class Q3CalibrationRecord
|
||||
{
|
||||
public int PcbId { get; set; }
|
||||
// Preserve the existing TBF meaning/unit of each offset (do not rescale here).
|
||||
public double? P1Offset { get; set; }
|
||||
public int? P1Cal { get; set; }
|
||||
public double? P2Offset { get; set; }
|
||||
public int? P2Cal { get; set; }
|
||||
public double? P3Offset { get; set; }
|
||||
public int? P3Cal { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Completion outcome plus detached SQL inputs and separate write outcomes.</summary>
|
||||
public sealed class CompletionProcessResult : PreAdjustmentProcessResult
|
||||
{
|
||||
public List<PreadjustmentCompletionRecord> DatabaseRecords { get; set; } =
|
||||
new List<PreadjustmentCompletionRecord>();
|
||||
public List<CompletionDatabaseWriteResult> DatabaseWrites { get; set; } =
|
||||
new List<CompletionDatabaseWriteResult>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return base.ToString() + ", SQL=[" + string.Join("; ",
|
||||
DatabaseWrites.Select(w => "Slot=" + w.Slot + ", " + w.Result.Status)) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CompletionDatabaseWriteResult
|
||||
{
|
||||
public int Slot { get; set; }
|
||||
public CalibrationWriteResult Result { get; set; }
|
||||
}
|
||||
|
||||
public enum CalibrationWriteStatus
|
||||
{
|
||||
Failed = 0, Stored = 1, Disabled = 2, Simulated = 3,
|
||||
// The connection failed during commit; verify the DB before retrying.
|
||||
OutcomeUnknown = 4
|
||||
}
|
||||
|
||||
/// <summary>Database outcome, independent of the physical calibration outcome.</summary>
|
||||
public sealed class CalibrationWriteResult
|
||||
{
|
||||
public CalibrationWriteStatus Status { get; set; }
|
||||
public bool Success { get { return Status == CalibrationWriteStatus.Stored; } }
|
||||
public int PcbId { get; set; }
|
||||
public long? RecordId { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("Status={0}, PcbId={1}, RecordId={2}, Error={3}",
|
||||
Status, PcbId, RecordId, ErrorMessage ?? "None");
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HideSensitiveValues { get; set; } = true;
|
||||
|
||||
private static string FormatPassword(string password)
|
||||
@@ -466,6 +547,57 @@ namespace GenesisCordonelInterface.API
|
||||
}
|
||||
}
|
||||
|
||||
public class TemperatureCalibrationSlotResult : PreAdjustmentProcessResult
|
||||
{
|
||||
public decimal? ReferenceTemperatureCelsius { get; set; }
|
||||
public decimal? MeterTemperatureCelsius { get; set; }
|
||||
public decimal? AbsoluteDifferenceCelsius { get; set; }
|
||||
// The effective existing process limit, reported for diagnostics only.
|
||||
public decimal? DeviationLimitCelsius { get; set; }
|
||||
public int SampleCount { get; set; }
|
||||
public bool? TemperatureCheckPassed { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var culture = System.Globalization.CultureInfo.InvariantCulture;
|
||||
return base.ToString() + string.Format(culture,
|
||||
", Reference={0} C, Meter={1} C, Difference={2} C, Limit={3} C, Samples={4}, Check={5}",
|
||||
ReferenceTemperatureCelsius?.ToString(culture) ?? "N/A",
|
||||
MeterTemperatureCelsius?.ToString(culture) ?? "N/A",
|
||||
AbsoluteDifferenceCelsius?.ToString(culture) ?? "N/A",
|
||||
DeviationLimitCelsius?.ToString(culture) ?? "N/A", SampleCount,
|
||||
TemperatureCheckPassed.HasValue ? (TemperatureCheckPassed.Value ? "PASSED" : "FAILED") : "NOT_EVALUATED");
|
||||
}
|
||||
}
|
||||
|
||||
public class TemperatureCalibrationResult : PreAdjustmentProcessResult
|
||||
{
|
||||
public List<TemperatureCalibrationSlotResult> Slots { get; set; } =
|
||||
new List<TemperatureCalibrationSlotResult>();
|
||||
public bool IsSimulated { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return base.ToString() + ", Simulated=" + IsSimulated +
|
||||
", TemperatureResults=[" + string.Join("; ", Slots.Select(s => s.ToString())) + "]";
|
||||
}
|
||||
|
||||
// Preserve status for failures before process creation and for simulation.
|
||||
// Missing measurements must never be represented as a measured zero.
|
||||
public static TemperatureCalibrationResult FromProcessResult(
|
||||
PreAdjustmentProcessResult result, bool simulated = false)
|
||||
{
|
||||
if (result == null) throw new ArgumentNullException(nameof(result));
|
||||
var detailed = result as TemperatureCalibrationResult;
|
||||
if (detailed != null) return detailed;
|
||||
return new TemperatureCalibrationResult
|
||||
{
|
||||
Success = result.Success, Slot = result.Slot, ProcessName = result.ProcessName,
|
||||
ErrorMessage = result.ErrorMessage, IsSimulated = simulated
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class PreadjustmentDetectResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
|
||||
@@ -31,6 +31,48 @@
|
||||
"Type": "LocalDatabase",
|
||||
"DataSource": "Server=(localdb)\\SensusLocalDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
|
||||
"QueryTemplate": "SELECT [ParameterName], [ParameterValue] FROM [dbo].[PreAdjustmentCalibrationParams] WHERE [MeterSize] = QUERYPARAM"
|
||||
},
|
||||
|
||||
"PreadjustmentCompletionWriter": {
|
||||
"Enabled": true,
|
||||
"DataSource": "Server=(localdb)\\MojaDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
|
||||
"CommandText": "INSERT INTO [dbo].[PreadjustmentCompletion] ([PcbId], [ZeroOffset1], [ZeroOffset2], [ZeroOffset3], [FirstHitUpdatePeriod], [FirstHitShift], [FirstHitPercent1], [FirstHitPercent2], [FirstHitPercent3], [ToFTempOffset1], [ToFTempOffset2], [ToFTempOffset3], [MeterSize], [Date], [Successful]) OUTPUT INSERTED.[Id] VALUES (@PcbId, @ZeroOffset1, @ZeroOffset2, @ZeroOffset3, @FirstHitUpdatePeriod, @FirstHitShift, @FirstHitPercent1, @FirstHitPercent2, @FirstHitPercent3, @ToFTempOffset1, @ToFTempOffset2, @ToFTempOffset3, @MeterSize, @Date, @Successful);",
|
||||
"UseStoredProcedure": false,
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"ParameterMappings": {
|
||||
"PcbId": "@PcbId",
|
||||
"ZeroOffset1": "@ZeroOffset1",
|
||||
"ZeroOffset2": "@ZeroOffset2",
|
||||
"ZeroOffset3": "@ZeroOffset3",
|
||||
"FirstHitUpdatePeriod": "@FirstHitUpdatePeriod",
|
||||
"FirstHitShift": "@FirstHitShift",
|
||||
"FirstHitPercent1": "@FirstHitPercent1",
|
||||
"FirstHitPercent2": "@FirstHitPercent2",
|
||||
"FirstHitPercent3": "@FirstHitPercent3",
|
||||
"ToFTempOffset1": "@ToFTempOffset1",
|
||||
"ToFTempOffset2": "@ToFTempOffset2",
|
||||
"ToFTempOffset3": "@ToFTempOffset3",
|
||||
"MeterSize": "@MeterSize",
|
||||
"Date": "@Date",
|
||||
"Successful": "@Successful"
|
||||
}
|
||||
},
|
||||
"Q3CalibrationWriter": {
|
||||
"Enabled": true,
|
||||
"DataSource": "Server=(localdb)\\MojaDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
|
||||
"CommandText": "INSERT INTO [dbo].[Q3Calibration] ([PcbId], [P1Offset], [P1Cal], [P2Offset], [P2Cal], [P3Offset], [P3Cal], [Date]) OUTPUT INSERTED.[Id] VALUES (@PcbId, @P1Offset, @P1Cal, @P2Offset, @P2Cal, @P3Offset, @P3Cal, @Date);",
|
||||
"UseStoredProcedure": false,
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"ParameterMappings": {
|
||||
"PcbId": "@PcbId",
|
||||
"P1Offset": "@P1Offset",
|
||||
"P1Cal": "@P1Cal",
|
||||
"P2Offset": "@P2Offset",
|
||||
"P2Cal": "@P2Cal",
|
||||
"P3Offset": "@P3Offset",
|
||||
"P3Cal": "@P3Cal",
|
||||
"Date": "@Date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ namespace GenesisCordonelInterface.Core.DataStorage.Config
|
||||
/// </summary>
|
||||
public class GciDataStorageConfig
|
||||
{
|
||||
public GciDatabaseWriterConfig PreadjustmentCompletionWriter { get; set; }
|
||||
public GciDatabaseWriterConfig Q3CalibrationWriter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for meter login password lookup.
|
||||
/// </summary>
|
||||
@@ -27,4 +30,4 @@ namespace GenesisCordonelInterface.Core.DataStorage.Config
|
||||
/// </summary>
|
||||
public DataStorageConfig PreAdjustmentCalibrationParams { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Config
|
||||
{
|
||||
/// <summary>SQL Server destination for one logical result type.</summary>
|
||||
public sealed class GciDatabaseWriterConfig
|
||||
{
|
||||
// Missing sections in existing configuration files leave writes disabled.
|
||||
public bool Enabled { get; set; }
|
||||
public string DataSource { get; set; }
|
||||
public string CommandText { get; set; }
|
||||
public bool UseStoredProcedure { get; set; }
|
||||
public int CommandTimeoutSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Optional complete mapping: DTO property name -> SQL parameter name.
|
||||
/// Null/empty uses the documented default parameter names.
|
||||
/// Commands must return the persisted positive identity as their first scalar.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> ParameterMappings { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Config;
|
||||
using static GenesisCordonelInterface.API.PublicModels;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Writing
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores detached result records. Each call owns its connection and transaction;
|
||||
/// slot workers, request sessions and streaming ownership are never involved.
|
||||
/// </summary>
|
||||
public sealed class CalibrationResultWriter
|
||||
{
|
||||
private readonly GciDatabaseWriterConfig completion;
|
||||
private readonly GciDatabaseWriterConfig q3;
|
||||
|
||||
public CalibrationResultWriter(GciDataStorageConfig config)
|
||||
{
|
||||
completion = Snapshot(config?.PreadjustmentCompletionWriter);
|
||||
q3 = Snapshot(config?.Q3CalibrationWriter);
|
||||
}
|
||||
|
||||
private static GciDatabaseWriterConfig Snapshot(GciDatabaseWriterConfig config)
|
||||
{
|
||||
if (config == null) return null;
|
||||
return new GciDatabaseWriterConfig
|
||||
{
|
||||
Enabled = config.Enabled, DataSource = config.DataSource,
|
||||
CommandText = config.CommandText, UseStoredProcedure = config.UseStoredProcedure,
|
||||
CommandTimeoutSeconds = config.CommandTimeoutSeconds,
|
||||
ParameterMappings = config.ParameterMappings == null ? null :
|
||||
new Dictionary<string, string>(config.ParameterMappings, StringComparer.Ordinal)
|
||||
};
|
||||
}
|
||||
|
||||
public Task<CalibrationWriteResult> StorePreadjustmentCompletionAsync(
|
||||
PreadjustmentCompletionRecord record, CancellationToken token)
|
||||
{
|
||||
return StorePreadjustmentCompletionAsync(record, token, null);
|
||||
}
|
||||
|
||||
public Task<CalibrationWriteResult> StorePreadjustmentCompletionAsync(
|
||||
PreadjustmentCompletionRecord record, CancellationToken token, Action<string> diagnostic)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (record == null) throw new ArgumentNullException(nameof(record));
|
||||
// Materialize all values before the first await; no mutable record is retained.
|
||||
var values = new Dictionary<string, object>
|
||||
{
|
||||
{ "PcbId", record.PcbId }, { "MeterSize", record.MeterSize },
|
||||
{ "ZeroOffset1", record.ZeroOffset1 }, { "ZeroOffset2", record.ZeroOffset2 },
|
||||
{ "ZeroOffset3", record.ZeroOffset3 },
|
||||
{ "FirstHitUpdatePeriod", record.FirstHitUpdatePeriod },
|
||||
{ "FirstHitShift", record.FirstHitShift },
|
||||
{ "FirstHitPercent1", record.FirstHitPercent1 },
|
||||
{ "FirstHitPercent2", record.FirstHitPercent2 },
|
||||
{ "FirstHitPercent3", record.FirstHitPercent3 },
|
||||
{ "ToFTempOffset1", record.ToFTempOffset1 },
|
||||
{ "ToFTempOffset2", record.ToFTempOffset2 },
|
||||
{ "ToFTempOffset3", record.ToFTempOffset3 },
|
||||
{ "Date", record.Date }, { "Successful", record.Successful }
|
||||
};
|
||||
string error = !record.MeterSize.HasValue || record.MeterSize.Value < 0
|
||||
? "MeterSize is required and must not be negative." : null;
|
||||
if (record.Successful && values.Any(v => v.Value == null))
|
||||
error = "Successful preadjustment requires all calibration values.";
|
||||
return WriteAsync(completion, record.PcbId, values, error, token, diagnostic);
|
||||
}
|
||||
|
||||
public Task<CalibrationWriteResult> StoreQ3CalibrationAsync(
|
||||
Q3CalibrationRecord record, CancellationToken token)
|
||||
{
|
||||
return StoreQ3CalibrationAsync(record, token, null);
|
||||
}
|
||||
|
||||
public Task<CalibrationWriteResult> StoreQ3CalibrationAsync(
|
||||
Q3CalibrationRecord record, CancellationToken token, Action<string> diagnostic)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (record == null) throw new ArgumentNullException(nameof(record));
|
||||
var values = new Dictionary<string, object>
|
||||
{
|
||||
{ "PcbId", record.PcbId }, { "P1Offset", record.P1Offset },
|
||||
{ "P1Cal", record.P1Cal }, { "P2Offset", record.P2Offset },
|
||||
{ "P2Cal", record.P2Cal }, { "P3Offset", record.P3Offset },
|
||||
{ "P3Cal", record.P3Cal }, { "Date", record.Date }
|
||||
};
|
||||
string error = values.Any(v => v.Value == null)
|
||||
? "Q3 requires the offset and confirmed calibration factor for all three paths."
|
||||
: null;
|
||||
if (values.Values.OfType<double>().Any(v => double.IsNaN(v) || double.IsInfinity(v)))
|
||||
error = "Q3 offsets must be finite numbers.";
|
||||
return WriteAsync(q3, record.PcbId, values, error, token, diagnostic);
|
||||
}
|
||||
|
||||
private static async Task<CalibrationWriteResult> WriteAsync(
|
||||
GciDatabaseWriterConfig config, int pcbId, IDictionary<string, object> values,
|
||||
string validationError, CancellationToken token, Action<string> diagnostic = null)
|
||||
{
|
||||
string operation = values.ContainsKey("Successful") ? "Completion" : "Q3 calibration";
|
||||
Report(diagnostic, operation + " database write: START.");
|
||||
var result = new CalibrationWriteResult { PcbId = pcbId };
|
||||
if (config == null || !config.Enabled)
|
||||
{
|
||||
result.Status = CalibrationWriteStatus.Disabled;
|
||||
result.ErrorMessage = "Database writer is not enabled.";
|
||||
Report(diagnostic, operation + " database write skipped: writer is disabled or not configured.");
|
||||
return result;
|
||||
}
|
||||
bool commitStarted = false;
|
||||
bool connectionOpened = false;
|
||||
string stage = "validation";
|
||||
try
|
||||
{
|
||||
if (validationError != null) throw new ArgumentException(validationError);
|
||||
if (pcbId <= 0) throw new ArgumentException("PcbId must be positive.");
|
||||
if ((DateTime)values["Date"] == default(DateTime))
|
||||
throw new ArgumentException("The calibration timestamp is required.");
|
||||
if (string.IsNullOrWhiteSpace(config.DataSource) || string.IsNullOrWhiteSpace(config.CommandText))
|
||||
throw new ArgumentException("Writer DataSource and CommandText are required.");
|
||||
if (config.CommandTimeoutSeconds < 1)
|
||||
throw new ArgumentException("CommandTimeoutSeconds must be positive.");
|
||||
|
||||
using (var connection = new SqlConnection(config.DataSource))
|
||||
using (var command = new SqlCommand(config.CommandText, connection))
|
||||
{
|
||||
command.CommandType = config.UseStoredProcedure ? CommandType.StoredProcedure : CommandType.Text;
|
||||
command.CommandTimeout = config.CommandTimeoutSeconds;
|
||||
AddParameters(command, config, values);
|
||||
stage = "server connection";
|
||||
// Only log the destination, never credentials or the full connection string.
|
||||
var destination = new SqlConnectionStringBuilder(config.DataSource);
|
||||
Report(diagnostic, "Connecting to server '" + destination.DataSource +
|
||||
"', database '" + destination.InitialCatalog + "'.");
|
||||
await connection.OpenAsync(token).ConfigureAwait(false);
|
||||
connectionOpened = true;
|
||||
Report(diagnostic, "Connection opened.");
|
||||
token.ThrowIfCancellationRequested();
|
||||
stage = "transaction start";
|
||||
using (var transaction = connection.BeginTransaction())
|
||||
{
|
||||
command.Transaction = transaction;
|
||||
stage = "data write";
|
||||
Report(diagnostic, "Writing " + operation + " data (" +
|
||||
(config.UseStoredProcedure ? "stored procedure" : "SQL command") + ").");
|
||||
// The command/procedure must return the stored Id (OUTPUT INSERTED.Id).
|
||||
object scalar = await command.ExecuteScalarAsync(token).ConfigureAwait(false);
|
||||
stage = "returned record Id validation";
|
||||
if (scalar == null || scalar == DBNull.Value)
|
||||
throw new InvalidOperationException("Writer did not return a record Id; transaction rolled back.");
|
||||
decimal id = Convert.ToDecimal(scalar, CultureInfo.InvariantCulture);
|
||||
if (id <= 0 || id != decimal.Truncate(id) || id > long.MaxValue)
|
||||
throw new InvalidOperationException("Writer returned an invalid record Id; transaction rolled back.");
|
||||
token.ThrowIfCancellationRequested();
|
||||
Report(diagnostic, "Write command completed; confirming transaction.");
|
||||
stage = "transaction commit";
|
||||
// Once commit starts, cancellation must not hide a committed record.
|
||||
commitStarted = true;
|
||||
transaction.Commit();
|
||||
result.RecordId = (long)id;
|
||||
result.Status = CalibrationWriteStatus.Stored;
|
||||
Report(diagnostic, "Transaction committed; RecordId=" + result.RecordId + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (!commitStarted)
|
||||
{
|
||||
Report(diagnostic, operation + " database write cancelled during " + stage + ".");
|
||||
// Disposing the uncommitted transaction rolls back its write.
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Status = commitStarted ? CalibrationWriteStatus.OutcomeUnknown : CalibrationWriteStatus.Failed;
|
||||
result.ErrorMessage = ex.Message;
|
||||
// Server messages can contain record values or login names. Keep
|
||||
// the slot log free of these; retain the original error in the result.
|
||||
var sqlError = ex as SqlException;
|
||||
string detail = sqlError == null ? ex.GetType().Name :
|
||||
"SQL error " + sqlError.Number + ", state " + sqlError.State;
|
||||
Report(diagnostic, operation + " database write failed during " + stage + " (" + detail + ").");
|
||||
if (commitStarted)
|
||||
Report(diagnostic, "Commit outcome is unknown. Verify the database before retrying.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The SQL using scopes have already disposed their resources here.
|
||||
if (connectionOpened) Report(diagnostic, "SQL connection released.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void Report(Action<string> diagnostic, string message)
|
||||
{
|
||||
if (diagnostic == null) return;
|
||||
try { diagnostic(message); }
|
||||
catch (Exception)
|
||||
{
|
||||
// A closed/failing diagnostic subscriber must not change the DB outcome.
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddParameters(SqlCommand command, GciDatabaseWriterConfig config,
|
||||
IDictionary<string, object> values)
|
||||
{
|
||||
var mappings = config.ParameterMappings;
|
||||
bool custom = mappings != null && mappings.Count > 0;
|
||||
if (custom && (mappings.Count != values.Count || mappings.Keys.Any(k => !values.ContainsKey(k))))
|
||||
throw new ArgumentException("ParameterMappings must map every documented record field exactly once.");
|
||||
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var value in values)
|
||||
{
|
||||
string name = custom ? mappings[value.Key] : "@" + value.Key;
|
||||
if (string.IsNullOrWhiteSpace(name) || name.Length < 2 || name.Length > 128 ||
|
||||
name[0] != '@' || !(char.IsLetter(name[1]) || name[1] == '_') ||
|
||||
name.Skip(2).Any(c => !char.IsLetterOrDigit(c) && c != '_') || !names.Add(name))
|
||||
throw new ArgumentException("Invalid or duplicate SQL parameter name for " + value.Key + ".");
|
||||
SqlDbType type = value.Key == "Date" ? SqlDbType.DateTime2 :
|
||||
value.Key == "Successful" ? SqlDbType.Bit :
|
||||
value.Key == "P1Offset" || value.Key == "P2Offset" || value.Key == "P3Offset"
|
||||
? SqlDbType.Float : SqlDbType.Int;
|
||||
command.Parameters.Add(name, type).Value = value.Value ?? DBNull.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,8 @@ namespace GenesisCordonelInterface.Core
|
||||
gciExternalInterface = new InterfaceOutsideToGCI(
|
||||
new InterfaceGCIToLaatzen(),
|
||||
loginPasswordsDataStorageReader,
|
||||
calibrationParamsStorageReader);
|
||||
calibrationParamsStorageReader,
|
||||
gciConfig.DataStorageSection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,8 @@ namespace GenesisCordonelInterface.Core.Threading
|
||||
|
||||
public sealed class ApiWorker : IDisposable
|
||||
{
|
||||
private static readonly NLog.ILogger Logger = NLog.LogManager.GetLogger("GenesisCordonelInterface-threading");
|
||||
private static readonly NLog.ILogger Logger = NLog.LogManager.GetLogger("GenesisCordonelInterface.SlotWorker");
|
||||
private static long nextRequestId;
|
||||
/// <summary>
|
||||
/// Thread-safe FIFO queue holding work items.
|
||||
/// </summary>
|
||||
@@ -232,6 +233,12 @@ namespace GenesisCordonelInterface.Core.Threading
|
||||
};
|
||||
|
||||
thread.Start();
|
||||
|
||||
Logger.Info(
|
||||
"[SLOT-WORKER] CREATED Worker={0}, WorkerThread={1}, CallerThread={2}",
|
||||
Name,
|
||||
thread.ManagedThreadId,
|
||||
Thread.CurrentThread.ManagedThreadId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -251,31 +258,93 @@ namespace GenesisCordonelInterface.Core.Threading
|
||||
var tcs = new TaskCompletionSource<T>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
long requestId = Interlocked.Increment(ref nextRequestId);
|
||||
string operation = string.IsNullOrWhiteSpace(operationName) ? action.Method.Name : operationName;
|
||||
int callerThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
Stopwatch queueWait = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"ENQUEUE {operationName} slot worker={Name} time={DateTime.Now:HH:mm:ss.fff}");
|
||||
Logger.Debug(
|
||||
"[SLOT-REQUEST] ENQUEUE RequestId={0}, Worker={1}, Operation={2}, CallerThread={3}, QueueLengthBefore={4}",
|
||||
requestId,
|
||||
Name,
|
||||
operation,
|
||||
callerThreadId,
|
||||
QueueLength);
|
||||
|
||||
queue.Add(() =>
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
queueWait.Stop();
|
||||
LastActivity = DateTime.Now;
|
||||
|
||||
Logger.Warn(
|
||||
"[SLOT-REQUEST] CANCELLED_BEFORE_START RequestId={0}, Worker={1}, Operation={2}, WorkerThread={3}, QueueWaitMs={4}, QueueLength={5}",
|
||||
requestId,
|
||||
Name,
|
||||
operation,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
queueWait.ElapsedMilliseconds,
|
||||
QueueLength);
|
||||
|
||||
tcs.TrySetCanceled();
|
||||
return;
|
||||
}
|
||||
|
||||
Stopwatch execution = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
IsBusy = true;
|
||||
CurrentOperation = operationName ?? action.Method.Name;
|
||||
CurrentOperation = operation;
|
||||
LastActivity = DateTime.Now;
|
||||
LastError = null;
|
||||
|
||||
queueWait.Stop();
|
||||
|
||||
Logger.Debug(
|
||||
"[SLOT-REQUEST] START RequestId={0}, Worker={1}, Operation={2}, WorkerThread={3}, CallerThread={4}, QueueWaitMs={5}, QueueLength={6}",
|
||||
requestId,
|
||||
Name,
|
||||
operation,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
callerThreadId,
|
||||
queueWait.ElapsedMilliseconds,
|
||||
QueueLength);
|
||||
|
||||
var result = action();
|
||||
|
||||
tcs.TrySetResult(result);
|
||||
|
||||
execution.Stop();
|
||||
|
||||
Logger.Debug(
|
||||
"[SLOT-REQUEST] COMPLETE RequestId={0}, Worker={1}, Operation={2}, WorkerThread={3}, DurationMs={4}, QueueLength={5}",
|
||||
requestId,
|
||||
Name,
|
||||
operation,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
execution.ElapsedMilliseconds,
|
||||
QueueLength);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
execution.Stop();
|
||||
LastError = ex.Message;
|
||||
|
||||
Logger.Error(
|
||||
ex,
|
||||
"[SLOT-REQUEST] FAILED RequestId={0}, Worker={1}, Operation={2}, WorkerThread={3}, DurationMs={4}, QueueLength={5}, Error={6}",
|
||||
requestId,
|
||||
Name,
|
||||
operation,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
execution.ElapsedMilliseconds,
|
||||
QueueLength,
|
||||
ex.Message);
|
||||
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
finally
|
||||
@@ -286,13 +355,47 @@ namespace GenesisCordonelInterface.Core.Threading
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
queueWait.Stop();
|
||||
|
||||
Logger.Warn(
|
||||
"[SLOT-REQUEST] ENQUEUE_CANCELLED RequestId={0}, Worker={1}, Operation={2}, CallerThread={3}, QueueWaitMs={4}",
|
||||
requestId,
|
||||
Name,
|
||||
operation,
|
||||
callerThreadId,
|
||||
queueWait.ElapsedMilliseconds);
|
||||
|
||||
tcs.TrySetCanceled();
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
queueWait.Stop();
|
||||
|
||||
Logger.Error(
|
||||
ex,
|
||||
"[SLOT-REQUEST] ENQUEUE_FAILED RequestId={0}, Worker={1}, Operation={2}, Reason=WorkerDisposed",
|
||||
requestId,
|
||||
Name,
|
||||
operation);
|
||||
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
tcs.TrySetException(new ObjectDisposedException(nameof(ApiWorker), ex));
|
||||
queueWait.Stop();
|
||||
|
||||
var disposedException = new ObjectDisposedException(nameof(ApiWorker), ex);
|
||||
|
||||
Logger.Error(
|
||||
disposedException,
|
||||
"[SLOT-REQUEST] ENQUEUE_FAILED RequestId={0}, Worker={1}, Operation={2}, Reason=QueueCompleted",
|
||||
requestId,
|
||||
Name,
|
||||
operation);
|
||||
|
||||
tcs.TrySetException(disposedException);
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
@@ -327,6 +430,11 @@ namespace GenesisCordonelInterface.Core.Threading
|
||||
/// </summary>
|
||||
private void WorkerLoop()
|
||||
{
|
||||
Logger.Info(
|
||||
"[SLOT-WORKER] THREAD_STARTED Worker={0}, WorkerThread={1}",
|
||||
Name,
|
||||
Thread.CurrentThread.ManagedThreadId);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var item in queue.GetConsumingEnumerable())
|
||||
@@ -339,7 +447,26 @@ namespace GenesisCordonelInterface.Core.Threading
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// worker is shutting down
|
||||
Logger.Debug(
|
||||
"[SLOT-WORKER] QUEUE_DISPOSED Worker={0}, WorkerThread={1}",
|
||||
Name,
|
||||
Thread.CurrentThread.ManagedThreadId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(
|
||||
ex,
|
||||
"[SLOT-WORKER] THREAD_FAILED Worker={0}, WorkerThread={1}, Error={2}",
|
||||
Name,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logger.Info(
|
||||
"[SLOT-WORKER] THREAD_STOPPED Worker={0}, WorkerThread={1}",
|
||||
Name,
|
||||
Thread.CurrentThread.ManagedThreadId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,14 +478,41 @@ namespace GenesisCordonelInterface.Core.Threading
|
||||
if (disposed)
|
||||
return;
|
||||
|
||||
Logger.Info(
|
||||
"[SLOT-WORKER] DISPOSE_BEGIN Worker={0}, CallerThread={1}, WorkerThread={2}, IsBusy={3}, CurrentOperation={4}, QueueLength={5}",
|
||||
Name,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
thread.ManagedThreadId,
|
||||
IsBusy,
|
||||
CurrentOperation ?? "<none>",
|
||||
QueueLength);
|
||||
|
||||
disposed = true;
|
||||
|
||||
queue.CompleteAdding();
|
||||
|
||||
if (Thread.CurrentThread != thread && thread.IsAlive)
|
||||
thread.Join(1000);
|
||||
{
|
||||
bool stopped = thread.Join(1000);
|
||||
if (!stopped)
|
||||
{
|
||||
Logger.Warn(
|
||||
"[SLOT-WORKER] DISPOSE_WAIT_TIMEOUT Worker={0}, WorkerThread={1}, CurrentOperation={2}, QueueLength={3}",
|
||||
Name,
|
||||
thread.ManagedThreadId,
|
||||
CurrentOperation ?? "<none>",
|
||||
QueueLength);
|
||||
}
|
||||
}
|
||||
|
||||
queue.Dispose();
|
||||
|
||||
Logger.Info(
|
||||
"[SLOT-WORKER] DISPOSE_END Worker={0}, CallerThread={1}, WorkerThread={2}, ThreadAlive={3}",
|
||||
Name,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
thread.ManagedThreadId,
|
||||
thread.IsAlive);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,8 @@
|
||||
<Compile Include="Core\Config\GciConfig.cs" />
|
||||
<Compile Include="Core\Config\GciConfigLoader.cs" />
|
||||
<Compile Include="Core\DataStorage\Config\GciDataStorageConfig.cs" />
|
||||
<Compile Include="Core\DataStorage\Config\GciDatabaseWriterConfig.cs" />
|
||||
<Compile Include="Core\DataStorage\Writing\CalibrationResultWriter.cs" />
|
||||
<Compile Include="API\Enums.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Providers\CsvDataStorageReader.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Providers\DatabaseDataStorageReader.cs" />
|
||||
@@ -317,10 +319,10 @@
|
||||
<WCFMetadata Include="Connected Services\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ExternalProjects\Laatzen\Genesis\Common\CordonelPreadjustmentUi\CordonelPreadjustmentUi.csproj">
|
||||
<Project>{D0C8D887-ED52-40AB-A069-90BCE0E801E2}</Project>
|
||||
<Name>CordonelPreadjustmentUi</Name>
|
||||
</ProjectReference>
|
||||
<Reference Include="Xylem.Common.Ui.CordonelPreadjustmentUi">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Ui.CordonelPreadjustmentUi.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<ProjectReference Include="..\ExternalProjects\Laatzen\Genesis\Common\Ui\GenesisToolBox\GenesisToolBox.csproj">
|
||||
<Project>{95A50087-99EE-4349-9A17-C547290DB42F}</Project>
|
||||
<Name>GenesisToolBox</Name>
|
||||
@@ -329,4 +331,4 @@
|
||||
<!-- Import custom GCI runtime packaging logic -->
|
||||
<Import Project="RuntimePackage\Build\Copy.targets.xml" />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
+15
-11
@@ -2,44 +2,48 @@
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
<section name="Xylem.Common.Ui.GenesisToolBox.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false"/>
|
||||
<section name="Xylem.Common.Ui.GenesisToolBox.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
<userSettings>
|
||||
<Xylem.Common.Ui.GenesisToolBox.Properties.Settings>
|
||||
<setting name="InitialBinaryPath" serializeAs="String">
|
||||
<value/>
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="InitialPackageFile" serializeAs="String">
|
||||
<value/>
|
||||
<value />
|
||||
</setting>
|
||||
</Xylem.Common.Ui.GenesisToolBox.Properties.Settings>
|
||||
</userSettings>
|
||||
<appSettings>
|
||||
<add key="AdminName" value="My Name"/>
|
||||
<add key="AdminEMail" value="MyEMailAddress"/>
|
||||
<add key="ClientSettingsProvider.ServiceUri" value=""/>
|
||||
<add key="AdminName" value="My Name" />
|
||||
<add key="AdminEMail" value="MyEMailAddress" />
|
||||
<add key="ClientSettingsProvider.ServiceUri" value="" />
|
||||
</appSettings>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0"/>
|
||||
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Xylem.Common.CommonCore" publicKeyToken="null" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.8.18.29406" newVersion="2.8.18.29406" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
<system.web>
|
||||
<membership defaultProvider="ClientAuthenticationMembershipProvider">
|
||||
<providers>
|
||||
<add name="ClientAuthenticationMembershipProvider" type="System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri=""/>
|
||||
<add name="ClientAuthenticationMembershipProvider" type="System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" />
|
||||
</providers>
|
||||
</membership>
|
||||
<roleManager defaultProvider="ClientRoleProvider" enabled="true">
|
||||
<providers>
|
||||
<add name="ClientRoleProvider" type="System.Web.ClientServices.Providers.ClientRoleProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" cacheTimeout="86400"/>
|
||||
<add name="ClientRoleProvider" type="System.Web.ClientServices.Providers.ClientRoleProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" cacheTimeout="86400" />
|
||||
</providers>
|
||||
</roleManager>
|
||||
</system.web>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -31,6 +31,48 @@
|
||||
"Type": "LocalDatabase",
|
||||
"DataSource": "Server=(localdb)\\SensusLocalDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
|
||||
"QueryTemplate": "SELECT [ParameterName], [ParameterValue] FROM [dbo].[PreAdjustmentCalibrationParams] WHERE [MeterSize] = QUERYPARAM"
|
||||
},
|
||||
|
||||
"PreadjustmentCompletionWriter": {
|
||||
"Enabled": true,
|
||||
"DataSource": "Server=(localdb)\\MojaDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
|
||||
"CommandText": "INSERT INTO [dbo].[PreadjustmentCompletion] ([PcbId], [ZeroOffset1], [ZeroOffset2], [ZeroOffset3], [FirstHitUpdatePeriod], [FirstHitShift], [FirstHitPercent1], [FirstHitPercent2], [FirstHitPercent3], [ToFTempOffset1], [ToFTempOffset2], [ToFTempOffset3], [MeterSize], [Date], [Successful]) OUTPUT INSERTED.[Id] VALUES (@PcbId, @ZeroOffset1, @ZeroOffset2, @ZeroOffset3, @FirstHitUpdatePeriod, @FirstHitShift, @FirstHitPercent1, @FirstHitPercent2, @FirstHitPercent3, @ToFTempOffset1, @ToFTempOffset2, @ToFTempOffset3, @MeterSize, @Date, @Successful);",
|
||||
"UseStoredProcedure": false,
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"ParameterMappings": {
|
||||
"PcbId": "@PcbId",
|
||||
"ZeroOffset1": "@ZeroOffset1",
|
||||
"ZeroOffset2": "@ZeroOffset2",
|
||||
"ZeroOffset3": "@ZeroOffset3",
|
||||
"FirstHitUpdatePeriod": "@FirstHitUpdatePeriod",
|
||||
"FirstHitShift": "@FirstHitShift",
|
||||
"FirstHitPercent1": "@FirstHitPercent1",
|
||||
"FirstHitPercent2": "@FirstHitPercent2",
|
||||
"FirstHitPercent3": "@FirstHitPercent3",
|
||||
"ToFTempOffset1": "@ToFTempOffset1",
|
||||
"ToFTempOffset2": "@ToFTempOffset2",
|
||||
"ToFTempOffset3": "@ToFTempOffset3",
|
||||
"MeterSize": "@MeterSize",
|
||||
"Date": "@Date",
|
||||
"Successful": "@Successful"
|
||||
}
|
||||
},
|
||||
"Q3CalibrationWriter": {
|
||||
"Enabled": true,
|
||||
"DataSource": "Server=(localdb)\\MojaDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
|
||||
"CommandText": "INSERT INTO [dbo].[Q3Calibration] ([PcbId], [P1Offset], [P1Cal], [P2Offset], [P2Cal], [P3Offset], [P3Cal], [Date]) OUTPUT INSERTED.[Id] VALUES (@PcbId, @P1Offset, @P1Cal, @P2Offset, @P2Cal, @P3Offset, @P3Cal, @Date);",
|
||||
"UseStoredProcedure": false,
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"ParameterMappings": {
|
||||
"PcbId": "@PcbId",
|
||||
"P1Offset": "@P1Offset",
|
||||
"P1Cal": "@P1Cal",
|
||||
"P2Offset": "@P2Offset",
|
||||
"P2Cal": "@P2Cal",
|
||||
"P3Offset": "@P3Offset",
|
||||
"P3Cal": "@P3Cal",
|
||||
"Date": "@Date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user