f4363e0b4b
Claude-Session: https://claude.ai/code/session_013L3XdmVii9i5sY4FChU8pB
83 lines
3.5 KiB
C#
83 lines
3.5 KiB
C#
namespace ZB.MOM.WW.ScadaBridge.DelmiaNotifier;
|
|
|
|
internal static class Program
|
|
{
|
|
/// <summary>Entry point: loads config, sends the recipe-download notification with failover across the configured base URLs, and reports the legacy YES/NO stdout contract.</summary>
|
|
/// <param name="args">Command-line arguments parsed into the recipe download payload.</param>
|
|
/// <returns>A task that resolves to the process exit code.</returns>
|
|
/// <summary>Stdout reason + log line when neither key source is set; one literal so the two cannot drift.</summary>
|
|
private const string ApiKeyNotConfigured = "API key not configured (SCADABRIDGE_API_KEY or ScadaBridge:ApiKey)";
|
|
|
|
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, cfg, out var apiKeySource);
|
|
if (apiKey is null)
|
|
{
|
|
log.Write(ApiKeyNotConfigured);
|
|
return Reporter.Report(false, ApiKeyNotConfigured, stdout);
|
|
}
|
|
|
|
log.Write("api key source: " + apiKeySource);
|
|
|
|
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
|
|
{
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
}
|
|
}
|