feat(delmia-notifier): Program wiring, YES/NO reporter, diagnostics log
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.DelmiaNotifier;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Best-effort diagnostics: writes UTC-timestamped lines to stderr and, when a log path is configured,
|
||||||
|
/// appends them to a file (relative to the exe). Never throws — diagnostics must not break the YES/NO path.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class DiagLog(string? logPath)
|
||||||
|
{
|
||||||
|
private readonly string? _logPath = string.IsNullOrWhiteSpace(logPath) ? null : logPath;
|
||||||
|
|
||||||
|
public void Write(string message)
|
||||||
|
{
|
||||||
|
var line = string.Create(CultureInfo.InvariantCulture, $"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ} {message}");
|
||||||
|
Console.Error.WriteLine(line);
|
||||||
|
|
||||||
|
if (_logPath is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fullPath = Path.IsPathRooted(_logPath) ? _logPath : Path.Combine(AppContext.BaseDirectory, _logPath);
|
||||||
|
var dir = Path.GetDirectoryName(fullPath);
|
||||||
|
if (!string.IsNullOrEmpty(dir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
File.AppendAllText(fullPath, line + Environment.NewLine);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// diagnostics are best-effort; swallow file errors so the YES/NO contract is unaffected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,5 +2,72 @@ namespace ZB.MOM.WW.ScadaBridge.DelmiaNotifier;
|
|||||||
|
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
public static int Main(string[] args) => 0; // replaced in Task 7
|
public static async Task<int> Main(string[] args)
|
||||||
|
{
|
||||||
|
var stdout = Console.Out;
|
||||||
|
|
||||||
|
// Config first, so the diagnostic log can honour the configured LogPath even for early failures.
|
||||||
|
NotifierConfig cfg;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cfg = ConfigLoader.LoadFromDefaultFile();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
new DiagLog(null).Write("config load failed: " + ex.Message);
|
||||||
|
return Reporter.Report(false, "config load failed: " + ex.Message, stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
var log = new DiagLog(cfg.ScadaBridge.LogPath);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parse = ArgParser.Parse(args);
|
||||||
|
if (!parse.Ok)
|
||||||
|
{
|
||||||
|
log.Write("arg error: " + parse.Error);
|
||||||
|
return Reporter.Report(false, parse.Error!, stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseUrls = ConfigLoader.SplitBaseUrls(cfg.ScadaBridge.BaseUrls);
|
||||||
|
if (baseUrls.Length == 0)
|
||||||
|
{
|
||||||
|
log.Write("no BaseUrls configured");
|
||||||
|
return Reporter.Report(false, "no BaseUrls configured", stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiKey = ConfigLoader.ResolveApiKey(Environment.GetEnvironmentVariable);
|
||||||
|
if (apiKey is null)
|
||||||
|
{
|
||||||
|
log.Write("API key not configured (SCADABRIDGE_API_KEY)");
|
||||||
|
return Reporter.Report(false, "API key not configured (SCADABRIDGE_API_KEY)", stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(cfg.ScadaBridge.TimeoutSeconds) };
|
||||||
|
var sender = new LoggingRecipeSender(new HttpRecipeSender(http, apiKey), log);
|
||||||
|
|
||||||
|
log.Write($"notifying {baseUrls.Length} URL(s) for machine {parse.Payload!.MachineCode}");
|
||||||
|
var result = await Notifier.RunAsync(baseUrls, parse.Payload, sender, CancellationToken.None);
|
||||||
|
log.Write($"result: ok={result.Ok} reason={result.Reason}");
|
||||||
|
return Reporter.Report(result.Ok, result.Reason, stdout);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Write("unexpected error: " + ex);
|
||||||
|
return Reporter.Report(false, "unexpected error: " + ex.Message, stdout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Decorates a sender to emit a per-attempt diagnostic line; keeps stdout reserved for the YES/NO contract.</summary>
|
||||||
|
private sealed class LoggingRecipeSender(IRecipeSender inner, DiagLog log) : IRecipeSender
|
||||||
|
{
|
||||||
|
public async Task<AttemptOutcome> SendAsync(string baseUrl, RecipeDownload payload, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var outcome = await inner.SendAsync(baseUrl, payload, ct);
|
||||||
|
log.Write(outcome.Kind == AttemptKind.ConnectFailed
|
||||||
|
? $"attempt {baseUrl}: connect failed: {outcome.Error}"
|
||||||
|
: $"attempt {baseUrl}: HTTP {outcome.StatusCode}");
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace ZB.MOM.WW.ScadaBridge.DelmiaNotifier;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps an outcome to the legacy WWNotifier stdout contract: <c>YES</c> + exit 0 on success,
|
||||||
|
/// <c>NO</c> followed by a reason line + exit -1 on failure. The writer is injected so stdout is the
|
||||||
|
/// only thing Delmia ever parses (LF-terminated, deterministic across platforms).
|
||||||
|
/// </summary>
|
||||||
|
internal static class Reporter
|
||||||
|
{
|
||||||
|
public static int Report(bool ok, string reason, TextWriter stdout)
|
||||||
|
{
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
stdout.Write("YES\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.Write("NO\n");
|
||||||
|
stdout.Write(reason);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using ZB.MOM.WW.ScadaBridge.DelmiaNotifier;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.DelmiaNotifier.Tests;
|
||||||
|
|
||||||
|
public class ReporterTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Success_writes_YES_and_returns_0()
|
||||||
|
{
|
||||||
|
using var sw = new StringWriter();
|
||||||
|
var code = Reporter.Report(true, "", sw);
|
||||||
|
Assert.Equal("YES\n", sw.ToString());
|
||||||
|
Assert.Equal(0, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Failure_writes_NO_then_reason_and_returns_minus1()
|
||||||
|
{
|
||||||
|
using var sw = new StringWriter();
|
||||||
|
var code = Reporter.Report(false, "boom", sw);
|
||||||
|
Assert.Equal("NO\nboom", sw.ToString());
|
||||||
|
Assert.Equal(-1, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user