Files
ScadaBridge/docs/components/DelmiaNotifier.md
T

126 lines
7.7 KiB
Markdown

# Delmia Notifier
Delmia Notifier (#27) is a compact console executable that DELMIA Apriso shells out to on each recipe or NC-program download. It POSTs the notification to the ScadaBridge Inbound API method `DelmiaRecipeDownload` and reports the result in the legacy `WWNotifier` `YES` / `NO` stdout contract. It is an external client tool, not a cluster component: it never joins the cluster and has no actors or database.
## Overview
The project lives in `src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/`. Its `csproj` sets `AssemblyName` to `WWNotifier`, `PublishAot` to `true` and `InvariantGlobalization` to `true`, so the published `WWNotifier.exe` is a file-for-file replacement for the legacy tool. No `RuntimeIdentifier` is fixed in the project; the runtime artifact is produced with `dotnet publish -c Release -r win-x64` (Native AOT does not cross-compile from macOS or Linux). `appsettings.json` is copied to the output directory.
Per invocation the call path is:
```text
DELMIA → WWNotifier.exe → POST {baseUrl}/api/DelmiaRecipeDownload (X-API-Key: <key>)
← { "Result": true/false, "ResultText": "..." }
← stdout: YES | NO\n<reason> · exit: 0 | -1
```
The spec for this component is the project `README.md` plus `docs/plans/2026-06-26-delmia-recipe-notifier-design.md`; there is no `Component-DelmiaNotifier.md` requirements document. The inbound method it targets is described in `docs/api-specs/Delmia-Recipe-Download-API.md`.
## Key Concepts
### Output contract
`Reporter.Report` writes the only stdout Delmia parses. Success writes `YES\n` and returns `0`. Failure writes `NO\n` followed by the reason (no trailing newline) and returns `-1`. All diagnostics go to stderr and, optionally, a log file, never to stdout.
### Pre-execution-only failover
`Notifier.RunAsync` walks the configured base URLs in order and moves to the next URL only when the previous attempt provably did not execute. The sender classifies each attempt as one of four `AttemptKind` values:
| Kind | Raised when | Loop behaviour |
|---|---|---|
| `ConnectFailed` | `HttpRequestException` with `HttpRequestError.NameResolutionError`, or an inner `SocketException` with `ConnectionRefused` | Try the next URL |
| `StandbyRejected` | HTTP 503 whose `X-ScadaBridge-Standby` header is exactly `1` | Try the next URL |
| `OutcomeUnknown` | Any other `HttpRequestException`, or a timeout (`OperationCanceledException` not caused by the caller's token) | Stop; return `NO` with `OUTCOME_UNKNOWN: the request may have been applied; verify before retrying.` |
| `Connected` | Any HTTP response not matched above | Stop; the node's answer is final |
For a `Connected` outcome with a 2xx status, `Body.Result == true` yields `YES` (reason is `ResultText`); a false or unparseable body yields `NO` with `ResultText`, or `request rejected` if that is empty. A non-2xx status yields `NO` with `HTTP <status>`. If every URL fails pre-execution the reason is `all URLs unreachable: <last error>`. A generic 503 without the standby marker is final.
### API key resolution
`ConfigLoader.ResolveApiKey` prefers the `SCADABRIDGE_API_KEY` environment variable when non-blank, then `ScadaBridge:ApiKey` from `appsettings.json`; both are trimmed. The chosen source name is logged (`environment (SCADABRIDGE_API_KEY)` or `appsettings.json (ScadaBridge:ApiKey)`), never the key. The tracked `appsettings.json` must keep `ApiKey` as `""`.
## Architecture
| File | Role |
|---|---|
| `Program.cs` | `Main`: load config, parse args, resolve URLs and key, build the `HttpClient` with `TimeoutSeconds`, run `Notifier`, report. Any exception becomes `NO` + `unexpected error: ...`. A private `LoggingRecipeSender` decorator logs one line per attempt. |
| `ArgParser.cs` | Hand-rolled, reflection-free parser for the legacy flags. |
| `ConfigLoader.cs` | Deserialises `appsettings.json` (next to the exe, `AppContext.BaseDirectory`) via the source-generated `NotifierJsonContext`; validates every `BaseUrls` entry; splits the list; resolves the key. |
| `Notifier.cs` | The failover loop and `NotifyResult` mapping. |
| `IRecipeSender.cs` / `HttpRecipeSender.cs` | The send seam and its HTTP implementation. |
| `Reporter.cs` | Legacy stdout and exit-code mapping. |
| `DiagLog.cs` | Best-effort UTC-timestamped diagnostics. |
| `RecipeDownload.cs` / `RecipeDownloadResult.cs` | Request and response DTOs. |
### Command-line flags
Each flag takes exactly one value. A flag without a value, or an unknown flag, fails the parse. Parsing failures produce `NO` + reason and exit `-1` with no HTTP attempt.
| Short | Long | Required | Payload field |
|---|---|---|---|
| `-m` | `--machine` | yes | `MachineCode` |
| `-d` | `--downloadpath` | yes | `DownloadPath` |
| `-w` | `--workorder` | yes | `WorkOrderNumber` |
| `-p` | `--partnumber` | yes | `PartNumber` |
| `-s` | `--seqop` | no | `JobStepNumber` |
| `-u` | `--username` | no | `Username` |
### `HttpRecipeSender`
The URL is `baseUrl.TrimEnd('/') + "/api/DelmiaRecipeDownload"`. The JSON body is serialised through the source-generated context, the `X-API-Key` header is attached, and the response body is read and parsed only for 2xx statuses. A `JsonException` on a 2xx body yields a null body, which the loop treats as a failure.
### `DiagLog`
Each line is `yyyy-MM-ddTHH:mm:ss.fffZ <message>`, written to stderr and appended to `LogPath` when set (relative paths resolve against the exe directory, directories are created on demand). File errors are swallowed so the `YES`/`NO` contract is unaffected.
## Usage
```powershell
$env:SCADABRIDGE_API_KEY = "sbk_..."
.\WWNotifier.exe -m Z28061 -d "C:\recipes\job.nc" -w WO12345 -p PN-7788 -s 0100 -u operator1
```
The README documents a Docker-rig verification recipe (`docker/seed-delmia-sim.sh`, `docker/seed-delmia-api.sh`, `docker/verify-delmia-notifier.sh`) covering active-first, standby-first, bad-key and failover cases.
## Configuration
`appsettings.json` sits next to the executable; the section is `ScadaBridge`.
| Key | Default | Description |
|---|---|---|
| `BaseUrls` | none | Comma-separated base URLs (no path). Each entry must be an absolute HTTP(S) URI with a host, or config load fails. Empty yields `no BaseUrls configured`. |
| `TimeoutSeconds` | `30` | Per-attempt `HttpClient` timeout. |
| `LogPath` | none | Optional diagnostic log file; omit for stderr only. |
| `ApiKey` | none | Fallback key source; `SCADABRIDGE_API_KEY` wins. |
## Dependencies & Interactions
- [Inbound API (#14)](./InboundAPI.md) — hosts the `DelmiaRecipeDownload` method the notifier calls; the standby marker `X-ScadaBridge-Standby: 1` on a 503 is a server-side contract the notifier relies on.
- [Site Runtime (#3)](./SiteRuntime.md) — the inbound method routes to the machine instance's script, which performs the Delmia receiver handshake.
- The project has no project references to other ScadaBridge assemblies; the test project accesses internals through `InternalsVisibleTo`.
## Troubleshooting
### `NO` with `API key not configured`
Neither `SCADABRIDGE_API_KEY` nor `ScadaBridge:ApiKey` is non-blank. A stale environment variable shadows an edited file; check the `api key source:` log line.
### `NO` with `HTTP 401`
The node answered, so the answer is final and no failover happens. Inbound API keys are a per-node store, so a key valid on one central may be absent on the other.
### `NO` with `OUTCOME_UNKNOWN`
A timeout or non-refusal transport error. The download may have been applied; verify before retrying manually.
### `NO` with `config load failed`
`appsettings.json` is missing or malformed, or a `BaseUrls` entry is not an absolute HTTP(S) URI.
## Related Documentation
- [Inbound API](./InboundAPI.md)
- [Site Runtime](./SiteRuntime.md)
- `src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/README.md`
- `docs/api-specs/Delmia-Recipe-Download-API.md`