Server-054/055/056, Contracts-020/021/022, Tests-036/038/039, IntegrationTests-030/031/032 (+033 deferred to live rig), Client.Dotnet-026/028/029 (+027 won't-fix), Client.Go-030..034, Client.Python-032..036, Client.Rust-033..038. Key fix: SessionEventDistributor orphaned a subscriber that registered after the pump completed but before disposal (Server-056) -> register paths now complete late registrants under _lifecycleLock; regression test added. The racy dashboard-mirror gRPC test made deterministic (Tests-039). Verified green locally: gateway Tests targeted classes (GatewaySession, SessionEventDistributor, GatewayOptionsValidator, ProtobufContractRoundTrip, GatewaySessionDashboardMirror) + dotnet/go/python/rust client suites.
14 KiB
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
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:
./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:
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:
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:
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:
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.
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.
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:
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 for full
request and filter semantics.
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:
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:
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:
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). |
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:
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:
$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:
go get gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go@v0.1.1
Then import:
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:
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.