Second slice of the 2026-09-09 whole-system review remediation
(docs/plans/2026-09-09-arch-review.md §10), on top of the High slice 146a1323.
Runtime/devices: SR-2 alarm on-trigger integration scope; SR-3 correlated NativeAlarmActor
subscribe retry; SR-4 immutable audited DB target; SR-5 ordered InstanceActor stop/flush;
SR-12 rehydration fence; SR-13 S&F gate recheck per delivery; DC-4 no OPC UA security
downgrade; DC-5 alarm snapshot for every new subscriber; ORCH-1 sync-dispose joins the pump.
Management/UI: MG-2 site scope on audit tree + Flow Debugger Replay; MG-3 stale-probe memo
removed; MG-4 server-owned bulk-deploy deadline; UI-5 sandbox runs in a killable child
process (new CentralUI.SandboxWorker project); UI-14 bounded sandbox output/IPC.
Edge/audit: EI-1 relay audit ownership; EI-2 SkipBodyCapture suppresses error text; EI-3/EI-11
error-field redaction + caps; EI-4 single relay deadline, pre-execution-only retry; EI-5
authenticated relay provenance; EI-10 body cap before audit read.
Persistence/contracts: DL-1 global Instance.UniqueName unique index + conflict translation;
DL-2 DataConnection protocol config encrypted at rest (migration
20260909203046_EnforceGlobalInstanceNamesAndProtectProtocolConfiguration — coordinated
central upgrade, old binaries cannot read protected rows); DL-3 bounded Replay paging;
DL-6 cross-site Overwrite of a deployed instance rejected; XC-4 proto descriptor contract test.
Config/hygiene: XC-1 central listener isolation by local port; XC-2/XC-3 inert SMTP + cron
keys removed from rig settings; CM-2 env2 bootstrap guard enabled; DC-6 deploy site exit code;
DC-7 notifier retries only explicit standby rejection; XC-5 ClusterClient purged from docs.
Gates recorded in the report: build 0/0, 9 397 passed / 0 failed, rig redeployed on all
eight nodes with the migration applied. 46 Low findings remain open.
8.4 KiB
DelmiaNotifier (WWNotifier.exe)
A compact, self-contained Windows console app that DELMIA Apriso shells out to on each recipe / NC-program download to notify the plant that a recipe was placed at a path for a machine.
It is the modern, drop-in replacement for the legacy WWNotifier (see
../../docs/former-api-specs/dnc/Delmia-Integration-API.md,
Surface B), repointed from the old Wonderware /notify receiver to the ScadaBridge Inbound API
method DelmiaRecipeDownload (as-deployed spec: docs/api-specs/Delmia-Recipe-Download-API.md). The command-line flags, the YES/NO stdout contract, and the
exit-code semantics are unchanged, so Delmia's call site and output parsing need no changes — only
the executable on disk and its appsettings.json are swapped.
The published assembly name is
WWNotifierprecisely so it is a literal file-for-file drop-in for Delmia's existing call path.
What it does
Per invocation it POSTs the recipe-download notification to the configured ScadaBridge node(s) and maps the result back to the legacy contract:
DELMIA → WWNotifier.exe → POST {baseUrl}/api/DelmiaRecipeDownload (X-API-Key: <key>)
← { "Result": true/false, "ResultText": "..." }
← stdout: YES | NO\n<reason> · exit: 0 | -1
Command-line flags
| Short | Long | Required | → payload field |
|---|---|---|---|
-d |
--downloadpath |
yes | DownloadPath |
-m |
--machine |
yes | MachineCode |
-w |
--workorder |
yes | WorkOrderNumber |
-p |
--partnumber |
yes | PartNumber |
-s |
--seqop |
no | JobStepNumber |
-u |
--username |
no | Username |
Each flag takes one value (-m Z28061). A missing required flag → NO + reason, exit -1, no
HTTP attempt.
WWNotifier.exe -m Z28061 -d "C:\recipes\job.nc" -w WO12345 -p PN-7788 -s 0100 -u operator1
Output contract (drop-in parity)
- stdout is reserved for the contract Delmia parses:
- success →
YES(exit0) - failure →
NOon the first line, then a human-readable reason on the next line (exit-1)
- success →
- exit code:
0on success,-1on failure. (On POSIX shells-1surfaces as255; on Windows%ERRORLEVEL%is-1.) - All diagnostics (per-URL attempt, status code, which URL answered, exceptions) go to stderr and,
if
LogPathis set, an appended log file — never to stdout.
Delivery semantics
The notifier retries only when a request is known not to have executed: connection refusal,
name-resolution failure, or the server's explicit standby rejection. A connection reset after
sending or a response timeout returns OUTCOME_UNKNOWN without replay. Operators must verify
an uncertain result before manually retrying. Idempotent method implementations remain useful
because callers can deliberately retry, but the notifier does not blindly repeat ambiguous sends.
Configuration — appsettings.json
Placed next to the exe (copied to the output on build/publish):
{
"ScadaBridge": {
"BaseUrls": "http://host-a:8085,http://host-b:8085",
"TimeoutSeconds": 30,
"LogPath": "logs/delmia-notifier.log",
"ApiKey": ""
}
}
BaseUrls— comma-separated failover list of base URLs only; the app appends/api/DelmiaRecipeDownload. URLs are tried in order after proven DNS/connection refusal or explicit standby rejection. Other HTTP responses are final; ambiguous transport errors returnNO+OUTCOME_UNKNOWNwithout trying another URL.TimeoutSeconds— per-attempt HTTP timeout (default30).LogPath— optional diagnostic log file, relative to the exe. Omit to log to stderr only.
Secret — SCADABRIDGE_API_KEY (or ScadaBridge:ApiKey)
The inbound API key is sent as X-API-Key. It is resolved from SCADABRIDGE_API_KEY first
(recommended for production — the key never touches disk), falling back to ScadaBridge:ApiKey in
appsettings.json when the variable is unset or blank (a blank file value counts as unset too; both
values are trimmed). The diagnostic log records which source supplied the key — never the key itself — so
a stale environment variable silently shadowing an edited file is visible. If neither is set → NO +
API key not configured (SCADABRIDGE_API_KEY or ScadaBridge:ApiKey), exit -1, no HTTP attempt.
Set the variable on the machine/account that DELMIA runs under (e.g. a system/user environment
variable, or the service account's environment). If you use the file instead, restrict its ACL to the
account DELMIA runs under — the key is stored in plain text there. Fill ApiKey in only on the deployed
copy: the repo's appsettings.json is tracked and copied to the build output, so a key pasted there to
test locally is one git add away from being committed. It must stay "" in source control.
Building & publishing (Native AOT, win-x64)
The runtime artifact is a self-contained, single-file native exe. Native AOT does not cross-compile from macOS/Linux — publish on Windows with the MSVC build tools (Desktop C++ workload) installed:
# On Windows:
dotnet publish src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier -c Release -r win-x64
# → bin/Release/net10.0/win-x64/publish/WWNotifier.exe (+ appsettings.json)
Copy WWNotifier.exe and appsettings.json to the Delmia host, set SCADABRIDGE_API_KEY (or fill in
ScadaBridge:ApiKey on that copy), and
point BaseUrls at the central node(s).
Managed
dotnet build/dotnet testrun cross-platform and are the development/CI gate; only the final native publish requires Windows.
Manual smoke test
After deploying (or against a reachable test cluster such as wonder-app-vd03):
$env:SCADABRIDGE_API_KEY = "sbk_..."
.\WWNotifier.exe -m Z28061Sim -d "C:\recipes\test.nc" -w WO-TEST -p PN-TEST -s 0100 -u smoketest
# expect: YES (exit 0) — or NO + reason (exit -1) with details on stderr / in the log file
This mirrors the earlier curl verification of POST /api/DelmiaRecipeDownload with the
X-API-Key header.
Docker rig recipe (no Galaxy needed)
The local rig (docker/) carries an in-memory stand-in for a vd03-style machine, so the whole
path — notifier → inbound method → site Call script → simulated DelmiaReceiver handshake — runs
on a laptop, including a 2-node central failover. Verified 2026-09-02
(docs/api-specs/Delmia-Recipe-Download-API.md §8):
bash docker/seed-delmia-sim.sh # SimMachine/DelmiaReceiverSim templates + DelmiaSim01 on site-a, deployed
bash docker/seed-delmia-api.sh # DelmiaRecipeDownload method + scoped key; prints DELMIA_API_KEY=sbk_... ONCE
export DELMIA_API_KEY=sbk_...
bash docker/sync-inbound-api-keys.sh # keys are a per-node store: mirror central-a -> central-b (register row 108)
bash docker/verify-delmia-notifier.sh # runs the managed build from a scratch copy; the key goes in THAT copy's appsettings.json
| Case | Setup | Expected |
|---|---|---|
| A | active node first in BaseUrls |
YES |
| B | standby first (standby relays to the active) | YES |
| C | bad key | NO + HTTP 401 (an answer is final — no failover) |
| D | active docker killed, attempt inside the takeover window |
historical baseline: NO + HTTP 503; the updated client retries explicit standby refusals and still fails if no node is active |
| E | survivor /health/active = 200 (~27 s) |
YES (first URL refused → second URL) |
| F | victim restarted and rejoined as standby | YES (victim relays) |
| G | DownloadPath ending in .bad |
NO + Simulated reject: bad path or missing work order |
Safe failover
The notifier advances to the next configured URL after a proven connection refusal/name-resolution failure, or HTTP 503 carrying the server's exact X-ScadaBridge-Standby: 1 pre-execution marker. A generic 503 or business rejection is final. Both centrals may therefore be listed even when their internal standby-forwarding peer lists are absent.
A timeout or other response loss is reported as OUTCOME_UNKNOWN with a failed result and no automatic replay: the first node may already have applied the download. Verify the operation before manually retrying. Configure trusted central endpoints; the standby marker is a server protocol contract, not permission to retry arbitrary HTTP errors.