9eedf9d6a9
A consuming project hit two MXAccess parity surprises: a plain Write only records its user_id when the item has an active supervisory advise (the path to take when not authenticating), and array writes replace the whole array rather than patching individual elements. Document both across the five client READMEs and gateway.md's compatibility baseline, and expose the missing advise-supervisory subcommand in the go/python/rust/java CLIs (plus the .NET help text) so callers can establish the supervisory advise without dropping to the raw command API.
426 lines
16 KiB
Markdown
426 lines
16 KiB
Markdown
# Go Client
|
|
|
|
The Go client module contains the generated MXAccess Gateway protobuf bindings,
|
|
a small handwritten `mxgateway` package, and the `mxgw-go` test CLI scaffold.
|
|
The module uses the shared proto inputs documented in
|
|
`../../docs/ClientProtoGeneration.md` so gateway and client contracts stay in
|
|
sync.
|
|
|
|
## Layout
|
|
|
|
```text
|
|
clients/go/
|
|
go.mod
|
|
generate-proto.ps1
|
|
internal/generated/
|
|
mxgateway/
|
|
cmd/mxgw-go/
|
|
```
|
|
|
|
`internal/generated` contains code produced by `protoc`, `protoc-gen-go`, and
|
|
`protoc-gen-go-grpc`. Do not edit generated files by hand.
|
|
|
|
## Regenerating Protobuf Bindings
|
|
|
|
Run generation after the shared `.proto` files or the Go output path changes:
|
|
|
|
```powershell
|
|
./generate-proto.ps1
|
|
```
|
|
|
|
The script uses the tool paths recorded in `../../docs/ToolchainLinks.md`.
|
|
|
|
## Build And Test
|
|
|
|
Run the Go module checks from `clients/go`:
|
|
|
|
```powershell
|
|
go test ./...
|
|
go build ./...
|
|
go vet ./...
|
|
```
|
|
|
|
The tests parse the shared JSON fixtures, exercise value and status conversion,
|
|
use `bufconn` for fake gateway auth and streaming behavior, and cover CLI JSON
|
|
redaction.
|
|
|
|
## Packaging
|
|
|
|
Build a local CLI executable from `clients/go`:
|
|
|
|
```powershell
|
|
New-Item -ItemType Directory -Force ../../artifacts/clients/go | Out-Null
|
|
go build -o ../../artifacts/clients/go/mxgw-go.exe ./cmd/mxgw-go
|
|
```
|
|
|
|
Install the CLI into the active `GOBIN` or `GOPATH/bin`:
|
|
|
|
```powershell
|
|
go install ./cmd/mxgw-go
|
|
```
|
|
|
|
Other Go modules can consume the library package with the module path
|
|
`gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/mxgateway`.
|
|
|
|
## Client API
|
|
|
|
Use `mxgateway.Dial` with `mxgateway.Options` to configure plaintext or TLS
|
|
transport, API-key metadata, dial timeout, and per-call timeout:
|
|
|
|
```go
|
|
client, err := mxgateway.Dial(ctx, mxgateway.Options{
|
|
Endpoint: "localhost:5000",
|
|
APIKey: os.Getenv("MXGATEWAY_API_KEY"),
|
|
Plaintext: true,
|
|
})
|
|
```
|
|
|
|
The gateway can auto-generate its own self-signed certificate (it has no PKI), so
|
|
the client is **lenient by default**: a TLS connection (`Plaintext: false`) with
|
|
no `CACertFile`/`TLSConfig` accepts whatever certificate the gateway presents
|
|
(`InsecureSkipVerify`, with `ServerNameOverride` as the SNI when set). To verify
|
|
instead, set `CACertFile` to pin a CA, or set `RequireCertificateValidation:
|
|
true` to verify against the OS/system trust roots without pinning. See
|
|
[Gateway Configuration](../../docs/GatewayConfiguration.md#automatic-self-signed-certificate).
|
|
|
|
`Client.OpenSession` returns a `Session` with helpers for `Register`,
|
|
`AddItem`, `AddItem2`, `Advise`, `Write`, `Events`, and `Close`. Prefer
|
|
`SubscribeEvents` or `SubscribeEventsAfter` for long-running streams because the
|
|
returned subscription owns cancellation and exposes `Close` for deterministic
|
|
goroutine cleanup. Raw protobuf messages remain available through the
|
|
`mxgateway` package aliases and the `Raw` helper methods. Typed errors support
|
|
`errors.As` for `GatewayError`, `CommandError`, and `MxAccessError`; command
|
|
errors preserve the raw reply.
|
|
|
|
For alarms, the package exposes `Client.QueryActiveAlarms` for one-shot
|
|
snapshots, `Client.StreamAlarms` for the server-streaming feed, and
|
|
`Client.AcknowledgeAlarm` to ack an alarm by full reference. The streaming
|
|
call returns a `StreamAlarmsClient`; cancel its context to terminate the
|
|
stream. All three pass straight through to the gateway's central alarm
|
|
monitor.
|
|
|
|
## Write Semantics And Common Pitfalls
|
|
|
|
These are MXAccess parity behaviors that surprise new callers. The gateway
|
|
forwards them unchanged — it does not paper over them.
|
|
|
|
### Attributing a write to a user without `AuthenticateUser`
|
|
|
|
MXAccess only stamps a plain `Write`/`Write2` with a Galaxy user id when the
|
|
item carries an active *supervisory* advise. If you are **not** using the
|
|
verified/secured path (`AuthenticateUser` → `WriteSecured`/`WriteSecured2`) but
|
|
still need the write attributed to a user id, you must first advise the item
|
|
supervisory and then pass that user id on the write. Without the supervisory
|
|
advise the `userID` on a plain write is ignored.
|
|
|
|
The session exposes `Advise`/`UnAdvise` but not supervisory advise, so send it
|
|
through the generic command channel:
|
|
|
|
```go
|
|
_, err := client.Invoke(ctx, &pb.MxCommandRequest{
|
|
SessionId: session.ID(),
|
|
Command: &pb.MxCommand{
|
|
Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_SUPERVISORY,
|
|
Payload: &pb.MxCommand_AdviseSupervisory{
|
|
AdviseSupervisory: &pb.AdviseSupervisoryCommand{
|
|
ServerHandle: serverHandle,
|
|
ItemHandle: itemHandle,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
err = session.Write(ctx, serverHandle, itemHandle, value, userID)
|
|
```
|
|
|
|
The CLI exposes the same command as `advise-supervisory`, and `write` /
|
|
`write2` take `--user-id`.
|
|
|
|
### Array writes replace the whole array
|
|
|
|
A write to an array attribute **replaces the entire array**; it is not an
|
|
element-wise patch. To change a subset of elements, send the full array with
|
|
the unchanged elements included. For example, to change 2 elements of a
|
|
20-element array, build the `MxValue` from all 20 values (the 18 unchanged plus
|
|
the 2 new ones). Sending only the 2 changed values overwrites the attribute
|
|
with a 2-element array.
|
|
|
|
## Galaxy Repository browse
|
|
|
|
The `GalaxyRepository` service (proto package `galaxy_repository.v1`) is a
|
|
read-only metadata-only browse over the AVEVA System Platform Galaxy
|
|
Repository. It uses the same API-key authentication as the MXAccess Gateway
|
|
and requires the `metadata:read` scope. Use `mxgateway.DialGalaxy` to obtain a
|
|
`*GalaxyClient` that mirrors the connection-management conventions of
|
|
`Client`:
|
|
|
|
```go
|
|
galaxy, err := mxgateway.DialGalaxy(ctx, mxgateway.Options{
|
|
Endpoint: "localhost:5000",
|
|
APIKey: os.Getenv("MXGATEWAY_API_KEY"),
|
|
Plaintext: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer galaxy.Close()
|
|
|
|
ok, err := galaxy.TestConnection(ctx)
|
|
deployTime, present, err := galaxy.GetLastDeployTime(ctx)
|
|
objects, err := galaxy.DiscoverHierarchy(ctx)
|
|
```
|
|
|
|
`GetLastDeployTime` returns `(time.Time{}, false, nil)` when the server
|
|
reports `present=false` (no deploy recorded). `DiscoverHierarchy` returns
|
|
the generated `*GalaxyObject` slice with each object's dynamic attributes
|
|
populated for direct contract access.
|
|
|
|
### Browsing lazily
|
|
|
|
For UI trees or OPC UA bridges, use `BrowseChildren` to walk one level at a
|
|
time instead of loading the full hierarchy. Pass an empty request for root
|
|
objects; subsequent calls set `ParentGobjectId`, `ParentTagName`, or
|
|
`ParentContainedPath`. Filter fields match `DiscoverHierarchy`. Each response
|
|
pairs `Children` with `ChildHasChildren` so you know which nodes to expand. See
|
|
[Galaxy Repository](../../docs/GalaxyRepository.md#browsechildren) for full
|
|
request and filter semantics.
|
|
|
|
```go
|
|
import pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated/galaxy_repository/v1"
|
|
|
|
reply, err := galaxy.BrowseChildren(ctx, &pb.BrowseChildrenRequest{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i, child := range reply.GetChildren() {
|
|
fmt.Printf("%s expand=%v\n", child.GetTagName(), reply.GetChildHasChildren()[i])
|
|
}
|
|
```
|
|
|
|
#### High-level walker
|
|
|
|
For UI trees, the client provides a `LazyBrowseNode` walker that handles
|
|
sibling pagination and the `child_has_children` hint for you:
|
|
|
|
```go
|
|
galaxy, err := mxgateway.DialGalaxy(ctx, mxgateway.Options{
|
|
Endpoint: "localhost:5000",
|
|
APIKey: os.Getenv("MXGATEWAY_API_KEY"),
|
|
Plaintext: true,
|
|
})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer galaxy.Close()
|
|
|
|
roots, err := galaxy.Browse(ctx, nil)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
for _, root := range roots {
|
|
if root.HasChildrenHint() {
|
|
if err := root.Expand(ctx); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
for _, child := range root.Children() {
|
|
kind := "leaf"
|
|
if child.HasChildrenHint() {
|
|
kind = "has children"
|
|
}
|
|
fmt.Printf("%s (%s)\n", child.Object().GetTagName(), kind)
|
|
}
|
|
}
|
|
```
|
|
|
|
`Expand` is idempotent — calling it twice fires only one RPC,
|
|
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
|
|
`Browse` again from the root.
|
|
|
|
### Watching deploy events
|
|
|
|
`WatchDeployEvents` opens a server-streaming subscription. The server emits a
|
|
bootstrap event with the current Galaxy state immediately on subscribe, then
|
|
one `DeployEvent` per new deploy. `Sequence` is monotonic per server start;
|
|
gaps signal dropped events. Pass a non-nil `lastSeenDeployTime` to suppress the
|
|
bootstrap event when resuming from a known checkpoint:
|
|
|
|
```go
|
|
streamCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
events, errs, err := galaxy.WatchDeployEvents(streamCtx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case ev, ok := <-events:
|
|
if !ok {
|
|
return nil // stream completed (server EOF or ctx cancelled)
|
|
}
|
|
log.Printf("seq=%d objects=%d attrs=%d",
|
|
ev.GetSequence(), ev.GetObjectCount(), ev.GetAttributeCount())
|
|
case streamErr := <-errs:
|
|
if streamErr != nil {
|
|
return streamErr // *GatewayError
|
|
}
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
```
|
|
|
|
Cancel the supplied context to tear down the stream cleanly. Both channels
|
|
close after EOF, cancellation, or a terminal error; surfaced errors are wrapped
|
|
in `*GatewayError`.
|
|
|
|
The CLI exposes the same RPC via `galaxy-watch`:
|
|
|
|
```powershell
|
|
go run ./cmd/mxgw-go galaxy-watch -plaintext
|
|
go run ./cmd/mxgw-go galaxy-watch -plaintext -json
|
|
go run ./cmd/mxgw-go galaxy-watch -plaintext -last-seen-deploy-time 2026-04-28T10:00:00Z
|
|
go run ./cmd/mxgw-go galaxy-watch -plaintext -limit 5
|
|
```
|
|
|
|
The command runs until Ctrl+C (or the optional `-limit` is reached) and prints
|
|
one line per event in text mode or one JSON object per event with `-json`.
|
|
|
|
## CLI
|
|
|
|
The `mxgw-go` CLI emits JSON with redacted API keys for commands that connect to
|
|
the gateway:
|
|
|
|
### Subcommand reference
|
|
|
|
Every subcommand wired into the CLI. All accept the common flags
|
|
(`-endpoint`, `-plaintext`, `-api-key` / `-api-key-env`, `-ca-cert`,
|
|
`-server-name-override`, `-call-timeout`) and most accept `-json`.
|
|
|
|
| Command | Purpose |
|
|
|---|---|
|
|
| `version` | Print client/contract versions. |
|
|
| `open-session` | Open a gateway session and print its id. |
|
|
| `close-session` | Close a session by id. |
|
|
| `ping` | Round-trip a `PING` command (`-session-id`, `-message`). |
|
|
| `register` | Register a client name on a session (`-session-id`, `-client-name`). |
|
|
| `add-item` | Add an item handle (`-session-id`, `-server-handle`, `-item`). |
|
|
| `advise` | Advise (subscribe) one item (`-session-id`, `-server-handle`, `-item-handle`). |
|
|
| `subscribe-bulk` | Advise many items in one call. |
|
|
| `unsubscribe-bulk` | Unadvise many item handles in one call. |
|
|
| `read-bulk` | Read snapshots for many item handles in one call. |
|
|
| `write` | Write one value (`-type`, `-value`). |
|
|
| `write-bulk` | Write many values (`-item-handles`, `-values`, counts must match). |
|
|
| `write2-bulk` | `write-bulk` with a shared `-timestamp-value` (RFC 3339). |
|
|
| `write-secured-bulk` | Secured bulk write (`-current-user-id`, `-verifier-user-id`). |
|
|
| `write-secured2-bulk` | Secured bulk write with a shared timestamp. |
|
|
| `bench-read-bulk` | Throughput benchmark (`-duration-seconds`, `-warmup-seconds`, `-bulk-size`). |
|
|
| `stream-events` | Stream item-value events for a session (`-session-id`, `-limit`). |
|
|
| `stream-alarms` | Stream the alarm feed (`-filter-prefix`, `-limit`). |
|
|
| `acknowledge-alarm` | Acknowledge an alarm reference. |
|
|
| `smoke` | End-to-end smoke workflow against one item. |
|
|
| `galaxy-test-connection` | Probe the Galaxy Repository RPC connection. |
|
|
| `galaxy-last-deploy` | Print the most recent deploy event. |
|
|
| `galaxy-discover` | Discover deployed objects. |
|
|
| `galaxy-watch` | Stream deploy events until Ctrl+C or `-limit`. |
|
|
| `galaxy-browse` | Lazy/eager browse of the Galaxy object tree. |
|
|
| `batch` | Read commands from stdin (see below). |
|
|
|
|
```powershell
|
|
go run ./cmd/mxgw-go version -json
|
|
go run ./cmd/mxgw-go open-session -endpoint localhost:5000 -plaintext -json
|
|
go run ./cmd/mxgw-go ping -session-id <id> -plaintext -json
|
|
go run ./cmd/mxgw-go register -session-id <id> -client-name mxgw-go -plaintext -json
|
|
go run ./cmd/mxgw-go add-item -session-id <id> -server-handle 1 -item Area001.Tag.Value -plaintext -json
|
|
go run ./cmd/mxgw-go advise -session-id <id> -server-handle 1 -item-handle 1 -plaintext -json
|
|
go run ./cmd/mxgw-go write -session-id <id> -server-handle 1 -item-handle 1 -type int32 -value 123 -plaintext -json
|
|
go run ./cmd/mxgw-go write-bulk -session-id <id> -server-handle 1 -item-handles 1,2 -values 10,20 -type int32 -plaintext -json
|
|
go run ./cmd/mxgw-go read-bulk -session-id <id> -item-handles 1,2 -plaintext -json
|
|
go run ./cmd/mxgw-go stream-events -session-id <id> -plaintext -json
|
|
go run ./cmd/mxgw-go stream-alarms -plaintext -json
|
|
go run ./cmd/mxgw-go smoke -item Area001.Tag.Value -plaintext -json
|
|
go run ./cmd/mxgw-go galaxy-test-connection -plaintext -json
|
|
go run ./cmd/mxgw-go galaxy-last-deploy -plaintext -json
|
|
go run ./cmd/mxgw-go galaxy-discover -plaintext -json
|
|
go run ./cmd/mxgw-go galaxy-watch -plaintext -json
|
|
go run ./cmd/mxgw-go galaxy-browse -plaintext -json
|
|
```
|
|
|
|
Use `-api-key-env MXGATEWAY_API_KEY` or `-api-key <key>` when authentication is
|
|
enabled. CLI output redacts the key value and never writes the raw secret.
|
|
|
|
### `batch` mode
|
|
|
|
`batch` reads one command line at a time from stdin and dispatches each through
|
|
the same routing as the standalone subcommands; it is the interface the
|
|
cross-language E2E harness drives. After every command's output it writes the
|
|
end-of-result sentinel line `__MXGW_BATCH_EOR__` to stdout and flushes, so the
|
|
harness can frame each result. Blank/whitespace-only lines are skipped; only
|
|
stdin EOF ends the session. Command errors are serialised as a JSON object
|
|
(`{"error":...,"type":"error"}`) to stdout (not stderr) and still followed by the
|
|
sentinel, so a failing command does not abort the batch. The input scanner
|
|
buffer is widened to 16 MiB so a single long command line (e.g. a bulk write with
|
|
thousands of handles) does not trip bufio's default 64 KiB token-too-long limit;
|
|
a line that still exceeds 16 MiB surfaces as a framed error and ends the session.
|
|
|
|
Use TLS options for a secured gateway:
|
|
|
|
```powershell
|
|
go run ./cmd/mxgw-go smoke -endpoint mxgateway.example.local:5001 -ca-cert C:\certs\mxgateway-ca.pem -server-name-override mxgateway.example.local -api-key-env MXGATEWAY_API_KEY -item Area001.Tag.Value -json
|
|
```
|
|
|
|
## Integration Checks
|
|
|
|
Run live checks only when a gateway and MXAccess-backed worker are available:
|
|
|
|
```powershell
|
|
$env:MXGATEWAY_INTEGRATION = '1'
|
|
$env:MXGATEWAY_ENDPOINT = 'localhost:5000'
|
|
$env:MXGATEWAY_API_KEY = '<gateway-api-key>'
|
|
$env:MXGATEWAY_TEST_ITEM = 'Area001.Tag.Value'
|
|
go run ./cmd/mxgw-go smoke -endpoint $env:MXGATEWAY_ENDPOINT -plaintext -api-key-env MXGATEWAY_API_KEY -item $env:MXGATEWAY_TEST_ITEM -json
|
|
```
|
|
|
|
## Installing the Go client
|
|
|
|
The module is resolved directly from the git repo — no package registry:
|
|
|
|
````bash
|
|
go get gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go@v0.1.1
|
|
````
|
|
|
|
Then import:
|
|
|
|
````go
|
|
import "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/mxgateway"
|
|
````
|
|
|
|
If your build environment cannot reach `gitea.dohertylan.com` directly,
|
|
configure `GOPROXY` to point at an internal proxy that fronts the Gitea
|
|
repo, or set `GOPRIVATE=gitea.dohertylan.com/*` to fetch the module
|
|
straight from the VCS — this both bypasses the public module proxy and
|
|
disables checksum-database (`sum.golang.org`) verification for that path.
|
|
Add `GOINSECURE=gitea.dohertylan.com/*` if the host serves the module over
|
|
plain HTTP rather than HTTPS.
|
|
|
|
## Releasing a new version
|
|
|
|
Go modules in monorepo subdirectories use prefixed tags. To tag a release
|
|
from this repo:
|
|
|
|
````bash
|
|
pwsh scripts/tag-go-module.ps1 -Version v0.1.1 -Push
|
|
````
|
|
|
|
The script validates semver, refuses to tag with uncommitted tracked
|
|
changes, creates an annotated tag `clients/go/v0.1.1`, and (with `-Push`)
|
|
pushes it to origin.
|
|
|
|
## Related Documentation
|
|
|
|
- [Client Packaging](../../docs/ClientPackaging.md)
|
|
- [Client Proto Generation](../../docs/ClientProtoGeneration.md)
|
|
- [Go Client Detailed Design](./GoClientDesign.md)
|