dc9c0c950c
Apply the ZB.MOM.WW. prefix to all gateway-side projects, folders,
.csproj/.sln contents, C# namespaces, using directives, generated proto
C# (csharp_namespace + checked-in generated files), InternalsVisibleTo
attributes, project-name string literals (LoadProject, .sln lookups,
worker exe paths, staticwebassets manifest), and the install/script/doc
references that point at any of the above. Migrate the solution from
.sln to .slnx via `dotnet sln migrate` and delete the old file.
External-runtime identifiers are intentionally NOT prefixed so external
configuration keeps working:
- GatewayMetrics.cs MeterName ("MxGateway.Server")
- DashboardAuthenticationDefaults Scheme/Policy ("MxGateway.Dashboard")
- GatewayRequestLoggingMiddleware logger category ("MxGateway.Request")
- StaRuntime thread name ("MxGateway.Worker.STA")
- appsettings.json root section "MxGateway" + env-var prefix
MxGateway__... and secret-name MxGateway:ApiKeyPepper
- C:\ProgramData\MxGateway\ data dir paths
Also fixes two tests that were not rename-related but became visible
while validating the rename:
- WorkerLiveMxAccessSmokeTests.ShutDownAsync: cancellation that the
gateway service correctly maps to RpcException(Cancelled) per gRPC
convention was being misclassified as a stream fault. Added a sibling
catch on RpcException with StatusCode.Cancelled.
- IntegrationTestEnvironment.ResolveRepositoryRoot: extracted IsRepositoryRoot
and made it accept either a .git marker OR a .sln/.slnx next to src/
so the worker-exe walker works in non-git working copies.
clients/proto/proto-inputs.json's protoRoot updated to point at
src/ZB.MOM.WW.MxGateway.Contracts/Protos.
Verified by `dotnet build` and a full `dotnet test` of the .slnx with
MXGATEWAY_RUN_LIVE_{MXACCESS,LDAP,GALAXY}_TESTS=1:
Tests: 472/472 pass
Worker.Tests: 280/280 pass (4 dev-rig [Fact(Skip=...)] skipped)
IntegrationTests: 18/18 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
224 lines
9.7 KiB
Markdown
224 lines
9.7 KiB
Markdown
# Worker Bootstrap
|
|
|
|
The bootstrap layer parses the command-line arguments and environment variables passed to the `ZB.MOM.WW.MxGateway.Worker` process, validates them against the gateway contract, and produces either a populated `WorkerOptions` instance or a structured failure that maps to a `WorkerExitCode`.
|
|
|
|
## Overview
|
|
|
|
The worker process is a short-lived child of the gateway. The gateway side of this contract lives in [WorkerProcessLauncher](./WorkerProcessLauncher.md). On the worker side, `Program.cs` is a single line that delegates to `WorkerApplication.Run(args)`:
|
|
|
|
```csharp
|
|
using ZB.MOM.WW.MxGateway.Worker;
|
|
|
|
return WorkerApplication.Run(args);
|
|
```
|
|
|
|
`WorkerApplication.Run` constructs the bootstrap dependencies (`EnvironmentVariableWorkerEnvironment`, `WorkerConsoleLogger` writing to `Console.Error`, and a `WorkerPipeClient`), runs `WorkerOptionsParser`, and routes the resulting `WorkerBootstrapResult` either into the pipe client or into a non-zero exit. Splitting parsing from process wiring lets tests substitute fakes for the environment, logger, and pipe client without spawning a child process.
|
|
|
|
## Worker Options
|
|
|
|
`WorkerOptions` is the validated input contract for a worker session. The gateway hands every field to the worker; the worker never reads configuration files.
|
|
|
|
```csharp
|
|
public sealed class WorkerOptions
|
|
{
|
|
public const string NonceEnvironmentVariableName = "MXGATEWAY_WORKER_NONCE";
|
|
|
|
public WorkerOptions(
|
|
string sessionId,
|
|
string pipeName,
|
|
uint protocolVersion,
|
|
string nonce)
|
|
{
|
|
SessionId = sessionId;
|
|
PipeName = pipeName;
|
|
ProtocolVersion = protocolVersion;
|
|
Nonce = nonce;
|
|
}
|
|
|
|
public string SessionId { get; }
|
|
public string PipeName { get; }
|
|
public uint ProtocolVersion { get; }
|
|
public string Nonce { get; }
|
|
}
|
|
```
|
|
|
|
### Required inputs
|
|
|
|
All four fields are required. Three arrive on the command line and one arrives via environment variable:
|
|
|
|
| Source | Name | Maps to |
|
|
|--------|------|---------|
|
|
| Argument | `--session-id` | `SessionId` |
|
|
| Argument | `--pipe-name` | `PipeName` |
|
|
| Argument | `--protocol-version` | `ProtocolVersion` |
|
|
| Env var | `MXGATEWAY_WORKER_NONCE` | `Nonce` |
|
|
|
|
The nonce travels via environment variable rather than an argument because process command lines are visible to other users on Windows through `wmic`, `Get-CimInstance Win32_Process`, and the kernel object table; environment variables of another process are not. Treating the nonce as a credential keeps it off the command line.
|
|
|
|
There are no optional options. An unknown flag, a flag without a value, or a flag whose value starts with `--` is reported as an error rather than silently ignored.
|
|
|
|
## The Parser
|
|
|
|
`WorkerOptionsParser` walks `args` once, collects values into a case-insensitive dictionary, and accumulates errors so the caller sees every problem in a single failure rather than fixing them one at a time.
|
|
|
|
```csharp
|
|
for (int index = 0; index < args.Length; index++)
|
|
{
|
|
string arg = args[index];
|
|
if (!IsKnownOption(arg))
|
|
{
|
|
errors.Add($"Unknown option '{arg}'.");
|
|
continue;
|
|
}
|
|
|
|
if (index + 1 >= args.Length || args[index + 1].StartsWith("--", StringComparison.Ordinal))
|
|
{
|
|
errors.Add($"Option '{arg}' requires a value.");
|
|
continue;
|
|
}
|
|
|
|
values[arg] = args[index + 1];
|
|
index++;
|
|
}
|
|
```
|
|
|
|
After argument scanning, the parser cross-checks the protocol version against `GatewayContractInfo.WorkerProtocolVersion`. A version that parses as a `uint` but does not match the contract value is a hard failure with `WorkerExitCode.InvalidProtocolVersion`, separate from `InvalidArguments`, so the gateway can distinguish a malformed launch from a version mismatch and report a useful upgrade message.
|
|
|
|
The nonce is read last so that argument-shape errors are reported before the parser asks the environment for a secret it might not need.
|
|
|
|
## Bootstrap Result
|
|
|
|
`WorkerBootstrapResult` is a discriminated success/failure carrier. `Options` is non-null when `Succeeded` is true; `Errors` is populated only on failure.
|
|
|
|
```csharp
|
|
public static WorkerBootstrapResult Success(WorkerOptions options)
|
|
{
|
|
return new WorkerBootstrapResult(WorkerExitCode.Success, options, []);
|
|
}
|
|
|
|
public static WorkerBootstrapResult Failure(WorkerExitCode exitCode, IEnumerable<string> errors)
|
|
{
|
|
return new WorkerBootstrapResult(exitCode, null, errors.ToArray());
|
|
}
|
|
```
|
|
|
|
`Succeeded` is defined as `ExitCode == WorkerExitCode.Success` rather than as a separate flag, so the exit code and the success state cannot disagree.
|
|
|
|
## Exit Codes
|
|
|
|
`WorkerExitCode` is the worker process's exit contract. The gateway-side launcher decodes these values to decide whether a relaunch is safe.
|
|
|
|
| Value | Numeric | Produced when |
|
|
|-------|---------|---------------|
|
|
| `Success` | 0 | The pipe session ran to a clean close. |
|
|
| `UnexpectedFailure` | 1 | Any unhandled exception not matched by a more specific catch. |
|
|
| `InvalidArguments` | 2 | One or more `--session-id`, `--pipe-name`, or `--protocol-version` errors (missing, empty, unknown flag, or no value). |
|
|
| `InvalidProtocolVersion` | 3 | `--protocol-version` is not a `uint` or does not equal `GatewayContractInfo.WorkerProtocolVersion`. |
|
|
| `MissingNonce` | 4 | The `MXGATEWAY_WORKER_NONCE` environment variable is null, empty, or whitespace. |
|
|
| `PipeConnectionFailed` | 5 | An `IOException` or `TimeoutException` escapes the pipe client. |
|
|
| `ProtocolViolation` | 6 | A `WorkerFrameProtocolException` escapes the pipe client. |
|
|
|
|
`InvalidArguments`, `InvalidProtocolVersion`, and `MissingNonce` originate in the parser; the others originate in `WorkerApplication.Run`'s `try/catch` around the pipe client.
|
|
|
|
## Environment Abstraction
|
|
|
|
`IWorkerEnvironment` exists so tests can supply a fake nonce without mutating the real process environment, which would be a shared mutable global across parallel test runs.
|
|
|
|
```csharp
|
|
public interface IWorkerEnvironment
|
|
{
|
|
string? GetEnvironmentVariable(string name);
|
|
}
|
|
|
|
public sealed class EnvironmentVariableWorkerEnvironment : IWorkerEnvironment
|
|
{
|
|
public string? GetEnvironmentVariable(string name)
|
|
{
|
|
return Environment.GetEnvironmentVariable(name);
|
|
}
|
|
}
|
|
```
|
|
|
|
The production binding in `WorkerApplication.Run(string[])` is `EnvironmentVariableWorkerEnvironment`, which is a thin pass-through to `System.Environment.GetEnvironmentVariable`.
|
|
|
|
## Logging
|
|
|
|
The worker writes structured key/value lines to standard error. Standard error is used rather than standard output because the gateway side reads worker stdout for diagnostic capture only, while stderr is reserved for log output that does not interfere with any future stdout-based channel.
|
|
|
|
### The logger contract
|
|
|
|
`IWorkerLogger` exposes only `Information` and `Error`. There is no `Debug` or `Trace` level, because the worker is launched per session and verbose tracing belongs to the gateway-side launcher.
|
|
|
|
```csharp
|
|
public interface IWorkerLogger
|
|
{
|
|
void Information(string eventName, IReadOnlyDictionary<string, object?> fields);
|
|
|
|
void Error(string eventName, IReadOnlyDictionary<string, object?> fields);
|
|
}
|
|
```
|
|
|
|
`WorkerConsoleLogger` formats each call as `level=<Level> event=<EventName> key=value key=value` after running the field dictionary through `WorkerLogRedactor`:
|
|
|
|
```csharp
|
|
private void Write(
|
|
string level,
|
|
string eventName,
|
|
IReadOnlyDictionary<string, object?> fields)
|
|
{
|
|
Dictionary<string, object?> redactedFields = WorkerLogRedactor.RedactFields(fields);
|
|
string fieldText = string.Join(
|
|
" ",
|
|
redactedFields.Select(field => $"{field.Key}={FormatValue(field.Value)}"));
|
|
|
|
_writer.WriteLine($"level={level} event={eventName} {fieldText}".TrimEnd());
|
|
}
|
|
```
|
|
|
|
### What the redactor redacts and why
|
|
|
|
`gateway.md` "Security" requires that the worker never log raw credential values for `AuthenticateUser`, `WriteSecured`, or related secured operations. The bootstrap nonce is also a credential: anyone who reads it can impersonate the worker to the gateway pipe. `WorkerLogRedactor` enforces this by replacing values whose field name contains any of these substrings (case-insensitive) with the literal `[redacted]`:
|
|
|
|
```csharp
|
|
private static readonly string[] SensitiveFieldNameParts =
|
|
[
|
|
"nonce",
|
|
"secret",
|
|
"password",
|
|
"token",
|
|
"credential",
|
|
"apikey",
|
|
"api_key",
|
|
];
|
|
```
|
|
|
|
The match is on substrings of the field name rather than an exact list, so a field called `auth_token` or `user_password` is redacted automatically without each call site having to remember to opt in. `null` values pass through unchanged so the absence of a value is still visible in logs.
|
|
|
|
## How `Program.cs` Consumes The Result
|
|
|
|
`WorkerApplication.Run` is the single consumer of `WorkerBootstrapResult`. On failure it logs a `WorkerBootstrapFailed` event and returns the numeric `ExitCode` directly:
|
|
|
|
```csharp
|
|
WorkerOptionsParser parser = new(environment);
|
|
WorkerBootstrapResult result = parser.Parse(args);
|
|
|
|
if (!result.Succeeded)
|
|
{
|
|
logger.Error("WorkerBootstrapFailed", new Dictionary<string, object?>
|
|
{
|
|
["exit_code"] = result.ExitCode,
|
|
["errors"] = string.Join(";", result.Errors),
|
|
});
|
|
|
|
return (int)result.ExitCode;
|
|
}
|
|
```
|
|
|
|
On success it logs `WorkerBootstrapSucceeded` with the session fields (the `nonce` field is redacted by `WorkerLogRedactor` because of its name), hands the `WorkerOptions` to `IWorkerPipeClient.RunAsync`, and waits synchronously. The `try/catch` around the pipe call maps `WorkerFrameProtocolException` to `ProtocolViolation`, `IOException`/`TimeoutException` to `PipeConnectionFailed`, and any other exception to `UnexpectedFailure`, so every code path through `Run` returns one of the values in `WorkerExitCode`.
|
|
|
|
## Related Documentation
|
|
|
|
- [Worker Process Launcher](./WorkerProcessLauncher.md)
|
|
- [Worker STA](./WorkerSta.md)
|
|
- [Worker Frame Protocol](./WorkerFrameProtocol.md)
|