clients: document supervisory/array-write parity gotchas and add advise-supervisory to all CLIs
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.
This commit is contained in:
@@ -121,6 +121,53 @@ can keep the full `MxCommandReply`, HRESULT, and status array when MXAccess
|
|||||||
itself rejects a command. `MxAccessException.Reply` contains the raw generated
|
itself rejects a command. `MxAccessException.Reply` contains the raw generated
|
||||||
reply.
|
reply.
|
||||||
|
|
||||||
|
## 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 library exposes `Advise`/`UnAdvise` as named helpers but not supervisory
|
||||||
|
advise, so send it through the generic command channel:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await session.InvokeAsync(new MxCommandRequest
|
||||||
|
{
|
||||||
|
SessionId = session.SessionId,
|
||||||
|
Command = new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.AdviseSupervisory,
|
||||||
|
AdviseSupervisory = new AdviseSupervisoryCommand
|
||||||
|
{
|
||||||
|
ServerHandle = serverHandle,
|
||||||
|
ItemHandle = itemHandle,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await session.WriteAsync(serverHandle, itemHandle, value.ToMxValue(), 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.
|
||||||
|
|
||||||
## CLI Usage
|
## CLI Usage
|
||||||
|
|
||||||
The test CLI supports deterministic JSON output for automation:
|
The test CLI supports deterministic JSON output for automation:
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ public static class MxGatewayClientCli
|
|||||||
.ConfigureAwait(false),
|
.ConfigureAwait(false),
|
||||||
"advise" => await AdviseAsync(arguments, client, standardOutput, cancellation.Token)
|
"advise" => await AdviseAsync(arguments, client, standardOutput, cancellation.Token)
|
||||||
.ConfigureAwait(false),
|
.ConfigureAwait(false),
|
||||||
|
"advise-supervisory" => await AdviseSupervisoryAsync(arguments, client, standardOutput, cancellation.Token)
|
||||||
|
.ConfigureAwait(false),
|
||||||
"subscribe-bulk" => await SubscribeBulkAsync(arguments, client, standardOutput, cancellation.Token)
|
"subscribe-bulk" => await SubscribeBulkAsync(arguments, client, standardOutput, cancellation.Token)
|
||||||
.ConfigureAwait(false),
|
.ConfigureAwait(false),
|
||||||
"unsubscribe-bulk" => await UnsubscribeBulkAsync(arguments, client, standardOutput, cancellation.Token)
|
"unsubscribe-bulk" => await UnsubscribeBulkAsync(arguments, client, standardOutput, cancellation.Token)
|
||||||
@@ -451,6 +453,28 @@ public static class MxGatewayClientCli
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Task<int> AdviseSupervisoryAsync(
|
||||||
|
CliArguments arguments,
|
||||||
|
IMxGatewayCliClient client,
|
||||||
|
TextWriter output,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return InvokeAndWriteAsync(
|
||||||
|
arguments,
|
||||||
|
client,
|
||||||
|
output,
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.AdviseSupervisory,
|
||||||
|
AdviseSupervisory = new AdviseSupervisoryCommand
|
||||||
|
{
|
||||||
|
ServerHandle = arguments.GetInt32("server-handle"),
|
||||||
|
ItemHandle = arguments.GetInt32("item-handle"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
private static Task<int> SubscribeBulkAsync(
|
private static Task<int> SubscribeBulkAsync(
|
||||||
CliArguments arguments,
|
CliArguments arguments,
|
||||||
IMxGatewayCliClient client,
|
IMxGatewayCliClient client,
|
||||||
@@ -2066,6 +2090,7 @@ public static class MxGatewayClientCli
|
|||||||
writer.WriteLine("mxgw-dotnet register --session-id <id> --client-name <name> [--json]");
|
writer.WriteLine("mxgw-dotnet register --session-id <id> --client-name <name> [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet add-item --session-id <id> --server-handle <n> --item <ref> [--json]");
|
writer.WriteLine("mxgw-dotnet add-item --session-id <id> --server-handle <n> --item <ref> [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet advise --session-id <id> --server-handle <n> --item-handle <n> [--json]");
|
writer.WriteLine("mxgw-dotnet advise --session-id <id> --server-handle <n> --item-handle <n> [--json]");
|
||||||
|
writer.WriteLine("mxgw-dotnet advise-supervisory --session-id <id> --server-handle <n> --item-handle <n> [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet subscribe-bulk --session-id <id> --server-handle <n> --items <ref,ref> [--json]");
|
writer.WriteLine("mxgw-dotnet subscribe-bulk --session-id <id> --server-handle <n> --items <ref,ref> [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet unsubscribe-bulk --session-id <id> --server-handle <n> --item-handles <n,n> [--json]");
|
writer.WriteLine("mxgw-dotnet unsubscribe-bulk --session-id <id> --server-handle <n> --item-handles <n,n> [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet read-bulk --session-id <id> --server-handle <n> --items <ref,ref> [--timeout-ms <n>] [--json]");
|
writer.WriteLine("mxgw-dotnet read-bulk --session-id <id> --server-handle <n> --items <ref,ref> [--timeout-ms <n>] [--json]");
|
||||||
|
|||||||
@@ -99,6 +99,52 @@ call returns a `StreamAlarmsClient`; cancel its context to terminate the
|
|||||||
stream. All three pass straight through to the gateway's central alarm
|
stream. All three pass straight through to the gateway's central alarm
|
||||||
monitor.
|
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
|
## Galaxy Repository browse
|
||||||
|
|
||||||
The `GalaxyRepository` service (proto package `galaxy_repository.v1`) is a
|
The `GalaxyRepository` service (proto package `galaxy_repository.v1`) is a
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
||||||
"gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/mxgateway"
|
"gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/mxgateway"
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
"google.golang.org/protobuf/reflect/protoreflect"
|
"google.golang.org/protobuf/reflect/protoreflect"
|
||||||
@@ -87,6 +88,8 @@ func runWithIO(ctx context.Context, args []string, stdout, stderr io.Writer) err
|
|||||||
return runAddItem(ctx, args[1:], stdout, stderr)
|
return runAddItem(ctx, args[1:], stdout, stderr)
|
||||||
case "advise":
|
case "advise":
|
||||||
return runAdvise(ctx, args[1:], stdout, stderr)
|
return runAdvise(ctx, args[1:], stdout, stderr)
|
||||||
|
case "advise-supervisory":
|
||||||
|
return runAdviseSupervisory(ctx, args[1:], stdout, stderr)
|
||||||
case "subscribe-bulk":
|
case "subscribe-bulk":
|
||||||
return runSubscribeBulk(ctx, args[1:], stdout, stderr)
|
return runSubscribeBulk(ctx, args[1:], stdout, stderr)
|
||||||
case "unsubscribe-bulk":
|
case "unsubscribe-bulk":
|
||||||
@@ -358,6 +361,43 @@ func runAdvise(ctx context.Context, args []string, stdout, stderr io.Writer) err
|
|||||||
return writeCommandOutput(stdout, *jsonOutput, "advise", options, reply, err)
|
return writeCommandOutput(stdout, *jsonOutput, "advise", options, reply, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runAdviseSupervisory(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
|
flags := flag.NewFlagSet("advise-supervisory", flag.ContinueOnError)
|
||||||
|
flags.SetOutput(stderr)
|
||||||
|
common := bindCommonFlags(flags)
|
||||||
|
jsonOutput := flags.Bool("json", false, "write JSON output")
|
||||||
|
sessionID := flags.String("session-id", "", "gateway session id")
|
||||||
|
serverHandle := flags.Int("server-handle", 0, "MXAccess server handle")
|
||||||
|
itemHandle := flags.Int("item-handle", 0, "MXAccess item handle")
|
||||||
|
|
||||||
|
if err := flags.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if *sessionID == "" {
|
||||||
|
return errors.New("session-id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, options, err := dialForCommand(ctx, common)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
reply, err := client.Invoke(ctx, &pb.MxCommandRequest{
|
||||||
|
SessionId: *sessionID,
|
||||||
|
Command: &pb.MxCommand{
|
||||||
|
Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_SUPERVISORY,
|
||||||
|
Payload: &pb.MxCommand_AdviseSupervisory{
|
||||||
|
AdviseSupervisory: &pb.AdviseSupervisoryCommand{
|
||||||
|
ServerHandle: int32(*serverHandle),
|
||||||
|
ItemHandle: int32(*itemHandle),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return writeCommandOutput(stdout, *jsonOutput, "advise-supervisory", options, reply, err)
|
||||||
|
}
|
||||||
|
|
||||||
func runSubscribeBulk(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
func runSubscribeBulk(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
flags := flag.NewFlagSet("subscribe-bulk", flag.ContinueOnError)
|
flags := flag.NewFlagSet("subscribe-bulk", flag.ContinueOnError)
|
||||||
flags.SetOutput(stderr)
|
flags.SetOutput(stderr)
|
||||||
|
|||||||
@@ -84,6 +84,46 @@ yields alarm-feed messages from the gateway's central monitor), and
|
|||||||
`acknowledgeAlarm` (ack by full alarm reference with an optional comment and
|
`acknowledgeAlarm` (ack by full alarm reference with an optional comment and
|
||||||
ack target). Close the subscription to cancel the underlying gRPC stream.
|
ack target). Close the subscription to cancel the underlying gRPC stream.
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
```java
|
||||||
|
session.invokeCommand(MxCommand.newBuilder()
|
||||||
|
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE_SUPERVISORY)
|
||||||
|
.setAdviseSupervisory(AdviseSupervisoryCommand.newBuilder()
|
||||||
|
.setServerHandle(serverHandle)
|
||||||
|
.setItemHandle(itemHandle))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
session.write(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
|
## Galaxy Repository Browse
|
||||||
|
|
||||||
The Galaxy Repository service is a separate metadata-only gRPC service exposed
|
The Galaxy Repository service is a separate metadata-only gRPC service exposed
|
||||||
|
|||||||
+43
@@ -153,6 +153,8 @@ public final class MxGatewayCli implements Callable<Integer> {
|
|||||||
commandLine.addSubcommand("register", new RegisterCommand(clientFactory));
|
commandLine.addSubcommand("register", new RegisterCommand(clientFactory));
|
||||||
commandLine.addSubcommand("add-item", new AddItemCommand(clientFactory));
|
commandLine.addSubcommand("add-item", new AddItemCommand(clientFactory));
|
||||||
commandLine.addSubcommand("advise", new AdviseCommand(clientFactory));
|
commandLine.addSubcommand("advise", new AdviseCommand(clientFactory));
|
||||||
|
commandLine.addSubcommand(
|
||||||
|
"advise-supervisory", new AdviseSupervisoryCommand(clientFactory));
|
||||||
commandLine.addSubcommand("subscribe-bulk", new SubscribeBulkCommand(clientFactory));
|
commandLine.addSubcommand("subscribe-bulk", new SubscribeBulkCommand(clientFactory));
|
||||||
commandLine.addSubcommand("unsubscribe-bulk", new UnsubscribeBulkCommand(clientFactory));
|
commandLine.addSubcommand("unsubscribe-bulk", new UnsubscribeBulkCommand(clientFactory));
|
||||||
commandLine.addSubcommand("read-bulk", new ReadBulkCommand(clientFactory));
|
commandLine.addSubcommand("read-bulk", new ReadBulkCommand(clientFactory));
|
||||||
@@ -1044,6 +1046,34 @@ public final class MxGatewayCli implements Callable<Integer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "advise-supervisory",
|
||||||
|
description = "Invokes MXAccess AdviseSupervisory.")
|
||||||
|
static final class AdviseSupervisoryCommand extends GatewayCommand {
|
||||||
|
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
||||||
|
String sessionId;
|
||||||
|
|
||||||
|
@Option(names = "--server-handle", required = true, description = "MXAccess server handle.")
|
||||||
|
int serverHandle;
|
||||||
|
|
||||||
|
@Option(names = "--item-handle", required = true, description = "MXAccess item handle.")
|
||||||
|
int itemHandle;
|
||||||
|
|
||||||
|
AdviseSupervisoryCommand(MxGatewayCliClientFactory clientFactory) {
|
||||||
|
super(clientFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer call() {
|
||||||
|
try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) {
|
||||||
|
MxCommandReply reply =
|
||||||
|
client.session(sessionId).adviseSupervisoryRaw(serverHandle, itemHandle);
|
||||||
|
writeOutput("advise-supervisory", common, json, reply, () -> reply.getKind().name());
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Command(name = "subscribe-bulk", description = "Invokes MXAccess SubscribeBulk.")
|
@Command(name = "subscribe-bulk", description = "Invokes MXAccess SubscribeBulk.")
|
||||||
static final class SubscribeBulkCommand extends GatewayCommand {
|
static final class SubscribeBulkCommand extends GatewayCommand {
|
||||||
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
||||||
@@ -1830,6 +1860,8 @@ public final class MxGatewayCli implements Callable<Integer> {
|
|||||||
|
|
||||||
MxCommandReply adviseRaw(int serverHandle, int itemHandle);
|
MxCommandReply adviseRaw(int serverHandle, int itemHandle);
|
||||||
|
|
||||||
|
MxCommandReply adviseSupervisoryRaw(int serverHandle, int itemHandle);
|
||||||
|
|
||||||
MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId);
|
MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId);
|
||||||
|
|
||||||
List<SubscribeResult> subscribeBulk(int serverHandle, List<String> items);
|
List<SubscribeResult> subscribeBulk(int serverHandle, List<String> items);
|
||||||
@@ -1948,6 +1980,17 @@ public final class MxGatewayCli implements Callable<Integer> {
|
|||||||
return session.adviseRaw(serverHandle, itemHandle);
|
return session.adviseRaw(serverHandle, itemHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MxCommandReply adviseSupervisoryRaw(int serverHandle, int itemHandle) {
|
||||||
|
return session.invokeCommand(MxCommand.newBuilder()
|
||||||
|
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE_SUPERVISORY)
|
||||||
|
.setAdviseSupervisory(
|
||||||
|
mxaccess_gateway.v1.MxaccessGateway.AdviseSupervisoryCommand.newBuilder()
|
||||||
|
.setServerHandle(serverHandle)
|
||||||
|
.setItemHandle(itemHandle))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
|
public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
|
||||||
return session.writeRaw(serverHandle, itemHandle, value, userId);
|
return session.writeRaw(serverHandle, itemHandle, value, userId);
|
||||||
|
|||||||
+9
@@ -1302,6 +1302,15 @@ final class MxGatewayCliTests {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MxCommandReply adviseSupervisoryRaw(int serverHandle, int itemHandle) {
|
||||||
|
adviseCalled = true;
|
||||||
|
return MxCommandReply.newBuilder()
|
||||||
|
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE_SUPERVISORY)
|
||||||
|
.setProtocolStatus(ok())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
|
public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
|
||||||
lastWriteValue = value;
|
lastWriteValue = value;
|
||||||
|
|||||||
@@ -105,6 +105,49 @@ terminate the stream.
|
|||||||
Canceling a Python task cancels the client-side gRPC call or stream wait. It
|
Canceling a Python task cancels the client-side gRPC call or stream wait. It
|
||||||
does not abort an in-flight MXAccess COM call inside the worker process.
|
does not abort an in-flight MXAccess COM call inside the worker process.
|
||||||
|
|
||||||
|
## 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 `authenticate_user`
|
||||||
|
|
||||||
|
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 (`authenticate_user` → `write_secured`/`write_secured2`)
|
||||||
|
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 `user_id` on a plain write is ignored.
|
||||||
|
|
||||||
|
The session exposes `advise`/`unadvise` but not supervisory advise, so send it
|
||||||
|
through the generic command channel:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await session.invoke(
|
||||||
|
pb.MxCommand(
|
||||||
|
kind=pb.MX_COMMAND_KIND_ADVISE_SUPERVISORY,
|
||||||
|
advise_supervisory=pb.AdviseSupervisoryCommand(
|
||||||
|
server_handle=server_handle,
|
||||||
|
item_handle=item_handle,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await session.write(server_handle, item_handle, value, user_id=user_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
## Galaxy Repository Browse
|
||||||
|
|
||||||
The `GalaxyRepositoryClient` wraps the read-only `GalaxyRepository` gRPC
|
The `GalaxyRepositoryClient` wraps the read-only `GalaxyRepository` gRPC
|
||||||
|
|||||||
@@ -277,6 +277,23 @@ def advise(**kwargs: Any) -> None:
|
|||||||
_run(_advise(**kwargs), output_json=kwargs["output_json"], secrets=_secrets(kwargs))
|
_run(_advise(**kwargs), output_json=kwargs["output_json"], secrets=_secrets(kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
@main.command("advise-supervisory")
|
||||||
|
@gateway_options
|
||||||
|
@click.option("--session-id", required=True, help="Gateway session id.")
|
||||||
|
@click.option("--server-handle", required=True, type=int, help="MXAccess server handle.")
|
||||||
|
@click.option("--item-handle", required=True, type=int, help="MXAccess item handle.")
|
||||||
|
@click.option("--correlation-id", default="", help="Client correlation id.")
|
||||||
|
@click.option("--json", "output_json", is_flag=True, help="Emit JSON output.")
|
||||||
|
def advise_supervisory(**kwargs: Any) -> None:
|
||||||
|
"""Invoke MXAccess AdviseSupervisory."""
|
||||||
|
|
||||||
|
_run(
|
||||||
|
_advise_supervisory(**kwargs),
|
||||||
|
output_json=kwargs["output_json"],
|
||||||
|
secrets=_secrets(kwargs),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@main.command("subscribe-bulk")
|
@main.command("subscribe-bulk")
|
||||||
@gateway_options
|
@gateway_options
|
||||||
@click.option("--session-id", required=True, help="Gateway session id.")
|
@click.option("--session-id", required=True, help="Gateway session id.")
|
||||||
@@ -725,6 +742,22 @@ async def _advise(**kwargs: Any) -> dict[str, Any]:
|
|||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
async def _advise_supervisory(**kwargs: Any) -> dict[str, Any]:
|
||||||
|
async with await _connect(kwargs) as client:
|
||||||
|
session = _session(client, kwargs["session_id"])
|
||||||
|
await session.invoke(
|
||||||
|
pb.MxCommand(
|
||||||
|
kind=pb.MX_COMMAND_KIND_ADVISE_SUPERVISORY,
|
||||||
|
advise_supervisory=pb.AdviseSupervisoryCommand(
|
||||||
|
server_handle=kwargs["server_handle"],
|
||||||
|
item_handle=kwargs["item_handle"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
correlation_id=kwargs["correlation_id"],
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
async def _subscribe_bulk(**kwargs: Any) -> dict[str, Any]:
|
async def _subscribe_bulk(**kwargs: Any) -> dict[str, Any]:
|
||||||
async with await _connect(kwargs) as client:
|
async with await _connect(kwargs) as client:
|
||||||
session = _session(client, kwargs["session_id"])
|
session = _session(client, kwargs["session_id"])
|
||||||
|
|||||||
@@ -125,6 +125,49 @@ preserving the raw message for parity diagnostics. Command replies whose
|
|||||||
protocol status is not `PROTOCOL_STATUS_CODE_OK` become `Error::Command` and
|
protocol status is not `PROTOCOL_STATUS_CODE_OK` become `Error::Command` and
|
||||||
retain the raw `MxCommandReply`.
|
retain the raw `MxCommandReply`.
|
||||||
|
|
||||||
|
## 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 `authenticate_user`
|
||||||
|
|
||||||
|
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 (`authenticate_user` → `write_secured`/`write_secured2`)
|
||||||
|
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 `user_id` on a plain write is ignored.
|
||||||
|
|
||||||
|
The session exposes `advise`/`un_advise` but not supervisory advise, so send it
|
||||||
|
through the generic command channel:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
session
|
||||||
|
.invoke(
|
||||||
|
MxCommandKind::AdviseSupervisory,
|
||||||
|
Payload::AdviseSupervisory(AdviseSupervisoryCommand {
|
||||||
|
server_handle,
|
||||||
|
item_handle,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
session.write(server_handle, item_handle, value, user_id).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
## Galaxy Repository browse
|
||||||
|
|
||||||
The Galaxy Repository service exposes a read-only browse over the AVEVA System
|
The Galaxy Repository service exposes a read-only browse over the AVEVA System
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ use serde_json::Value;
|
|||||||
use zb_mom_ww_mxgateway_client::galaxy::{BrowseChildrenOptions, LazyBrowseNode};
|
use zb_mom_ww_mxgateway_client::galaxy::{BrowseChildrenOptions, LazyBrowseNode};
|
||||||
use zb_mom_ww_mxgateway_client::generated::galaxy_repository::v1::DeployEvent;
|
use zb_mom_ww_mxgateway_client::generated::galaxy_repository::v1::DeployEvent;
|
||||||
use zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::{
|
use zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::{
|
||||||
alarm_feed_message, AcknowledgeAlarmRequest, AlarmFeedMessage, CloseSessionRequest, MxCommand,
|
alarm_feed_message, AcknowledgeAlarmRequest, AdviseSupervisoryCommand, AlarmFeedMessage,
|
||||||
MxCommandKind, MxCommandRequest, MxEvent, MxEventFamily, MxValue as ProtoMxValue,
|
CloseSessionRequest, MxCommand, MxCommandKind, MxCommandRequest, MxEvent, MxEventFamily,
|
||||||
OpenSessionRequest, PingCommand, StreamAlarmsRequest, StreamEventsRequest, Write2BulkEntry,
|
MxValue as ProtoMxValue, OpenSessionRequest, PingCommand, StreamAlarmsRequest,
|
||||||
WriteBulkEntry, WriteSecured2BulkEntry, WriteSecuredBulkEntry,
|
StreamEventsRequest, Write2BulkEntry, WriteBulkEntry, WriteSecured2BulkEntry,
|
||||||
|
WriteSecuredBulkEntry,
|
||||||
};
|
};
|
||||||
use zb_mom_ww_mxgateway_client::{
|
use zb_mom_ww_mxgateway_client::{
|
||||||
next_correlation_id, ApiKey, ClientOptions, Error, GalaxyClient, GatewayClient, MxValue,
|
next_correlation_id, ApiKey, ClientOptions, Error, GalaxyClient, GatewayClient, MxValue,
|
||||||
@@ -105,6 +106,18 @@ enum Command {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
|
AdviseSupervisory {
|
||||||
|
#[command(flatten)]
|
||||||
|
connection: ConnectionArgs,
|
||||||
|
#[arg(long)]
|
||||||
|
session_id: String,
|
||||||
|
#[arg(long)]
|
||||||
|
server_handle: i32,
|
||||||
|
#[arg(long)]
|
||||||
|
item_handle: i32,
|
||||||
|
#[arg(long)]
|
||||||
|
json: bool,
|
||||||
|
},
|
||||||
SubscribeBulk {
|
SubscribeBulk {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
connection: ConnectionArgs,
|
connection: ConnectionArgs,
|
||||||
@@ -647,6 +660,27 @@ async fn dispatch(command: Command) -> Result<(), Error> {
|
|||||||
session.advise(server_handle, item_handle).await?;
|
session.advise(server_handle, item_handle).await?;
|
||||||
print_ok("advise", json);
|
print_ok("advise", json);
|
||||||
}
|
}
|
||||||
|
Command::AdviseSupervisory {
|
||||||
|
connection,
|
||||||
|
session_id,
|
||||||
|
server_handle,
|
||||||
|
item_handle,
|
||||||
|
json,
|
||||||
|
} => {
|
||||||
|
let session = session_for(connection, session_id).await?;
|
||||||
|
session
|
||||||
|
.invoke(
|
||||||
|
MxCommandKind::AdviseSupervisory,
|
||||||
|
zb_mom_ww_mxgateway_client::generated::mxaccess_gateway::v1::mx_command::Payload::AdviseSupervisory(
|
||||||
|
AdviseSupervisoryCommand {
|
||||||
|
server_handle,
|
||||||
|
item_handle,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
print_ok("advise-supervisory", json);
|
||||||
|
}
|
||||||
Command::SubscribeBulk {
|
Command::SubscribeBulk {
|
||||||
connection,
|
connection,
|
||||||
session_id,
|
session_id,
|
||||||
|
|||||||
@@ -1049,6 +1049,15 @@ Known important parity areas from existing captures:
|
|||||||
- Invalid handles and cross-server handles have specific exception/status
|
- Invalid handles and cross-server handles have specific exception/status
|
||||||
behavior.
|
behavior.
|
||||||
- STA message pumping is required for event delivery.
|
- STA message pumping is required for event delivery.
|
||||||
|
- A plain `Write`/`Write2` only honors its `user_id` when the item has an active
|
||||||
|
supervisory advise. Callers that do not go through the
|
||||||
|
`AuthenticateUser` → `WriteSecured`/`WriteSecured2` path must send
|
||||||
|
`AdviseSupervisory` for the item before a user id on a plain write is
|
||||||
|
recorded; otherwise the user id is ignored.
|
||||||
|
- Writing an array attribute replaces the whole array — it is not an
|
||||||
|
element-wise patch. To change a subset of elements the caller must send the
|
||||||
|
full array (unchanged elements included); sending only the changed elements
|
||||||
|
resizes the attribute.
|
||||||
|
|
||||||
The gateway should not "fix" these behaviors unless the client explicitly opts
|
The gateway should not "fix" these behaviors unless the client explicitly opts
|
||||||
into a non-parity mode.
|
into a non-parity mode.
|
||||||
|
|||||||
Reference in New Issue
Block a user