Compare commits

...

2 Commits

Author SHA1 Message Date
Joseph Doherty 758aca2355 Make the e2e write phase work live across all five clients
Running the matrix against a live gateway surfaced several issues:

- The write phase is now opt-in (-VerifyWrite, was -SkipWrite). It runs
  right after register so only a small event backlog precedes the write,
  and asserts the reliable OnWriteComplete signal (the written value is
  not echoed back by a provider-driven attribute like TestChangingInt, so
  the value compare is best-effort).
- Java was launched as bare "gradle", which .NET's Process.Start cannot
  exec (it is gradle.bat) — resolve the launcher and run it via cmd.exe.
- The Java client's MxEventStream queue capacity was 16, which overflows
  on any active session's backlog-replay burst; raised to 1024.
- The Rust stream-events CLI now renders the event family as the proto
  enum name, matching the protobuf-JSON the other four clients emit.

Update docs/GatewayTesting.md for the reworked write phase.

Verified live: the full five-client matrix passes with -VerifyWrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:45:47 -04:00
Joseph Doherty 06030dd1ef Implement MXAccess write commands in the worker
The .proto contract and MxCommandKind already defined Write, Write2,
WriteSecured, and WriteSecured2, but the worker's MxAccessCommandExecutor
had no case for any of them — every write kind fell through to
CreateInvalidRequestReply ("Unsupported MXAccess command kind Write").

Implement all four:

- VariantConverter.ConvertToComValue projects an MxValue into a
  COM-marshalable object (scalars, arrays, null) — the inverse of the
  existing COM-to-MxValue projection.
- IMxAccessServer / MxAccessComServer gain Write/Write2/WriteSecured/
  WriteSecured2, routed to ILMXProxyServer / ILMXProxyServer4.
- MxAccessSession and MxAccessCommandExecutor add the four write paths,
  following the existing ExecuteAdvise pattern; the reply is a plain OK
  reply and the outcome surfaces later as an OnWriteComplete event.

Verified live: a Write now returns PROTOCOL_STATUS_CODE_OK and produces
an OnWriteComplete event where it previously returned InvalidRequest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:45:35 -04:00
13 changed files with 991 additions and 99 deletions
@@ -260,7 +260,12 @@ public final class MxGatewayClient implements AutoCloseable {
* @return an iterator-style stream of events
*/
public MxEventStream streamEvents(StreamEventsRequest request) {
MxEventStream stream = new MxEventStream(16);
// The buffer must absorb the gateway's session-backlog replay burst,
// which arrives far faster than the iterator drains it. A small queue
// overflows on any moderately active session; 1024 covers a realistic
// backlog while still bounding memory and preserving overflow
// detection for a genuinely unbounded stream.
MxEventStream stream = new MxEventStream(1024);
MxGatewayChannels.withStreamDeadline(rawAsyncStub(), options).streamEvents(request, stream.observer());
return stream;
}
+7 -2
View File
@@ -17,7 +17,7 @@ use clap::{Args, Parser, Subcommand, ValueEnum};
use futures_util::StreamExt;
use mxgateway_client::generated::galaxy_repository::v1::DeployEvent;
use mxgateway_client::generated::mxaccess_gateway::v1::{
CloseSessionRequest, MxCommand, MxCommandKind, MxCommandRequest, MxEvent,
CloseSessionRequest, MxCommand, MxCommandKind, MxCommandRequest, MxEvent, MxEventFamily,
MxValue as ProtoMxValue, OpenSessionRequest, PingCommand, StreamEventsRequest,
};
use mxgateway_client::{
@@ -842,8 +842,13 @@ fn print_deploy_event(event: &DeployEvent, use_json: bool) {
/// matrix can extract and compare event values uniformly across all five
/// client CLIs.
fn event_to_json(event: &MxEvent) -> Value {
// Render the family as the proto enum name (e.g. MX_EVENT_FAMILY_ON_WRITE_COMPLETE)
// so it matches the protobuf-JSON the .NET/Go/Java/Python CLIs emit.
let family = MxEventFamily::try_from(event.family)
.map(|family| family.as_str_name().to_owned())
.unwrap_or_else(|_| event.family.to_string());
json!({
"family": event.family,
"family": family,
"sessionId": event.session_id,
"serverHandle": event.server_handle,
"itemHandle": event.item_handle,
+28 -16
View File
@@ -180,19 +180,30 @@ path and writes a JSON report under `artifacts/e2e/`:
2. **Bulk** — verifies `SubscribeBulk` / `UnsubscribeBulk` on a bounded tag
subset (skip with `-SkipBulk`).
3. **Add-item / advise** — adds and advises every discovered test tag.
4. **Write round-trip** — writes a per-client sentinel value to a configurable
writable attribute (`-WriteAttribute`, default `TestChangingInt`), then
asserts the same value is echoed back through the event stream. Skip with
`-SkipWrite`. The Rust `stream-events` CLI emits full per-event JSON
(`itemHandle` + `value`) so all five clients run an identical value compare.
5. **Stream** — asserts a bounded event stream delivers at least one event
4. **Stream** — asserts a bounded event stream delivers at least one event
(skip with `-SkipStream`).
6. **Parity** — asserts MXAccess error paths are rejected rather than silently
5. **Parity** — asserts MXAccess error paths are rejected rather than silently
succeeding: an invalid item handle and an unknown session id (skip with
`-SkipParity`).
7. **Auth rejection** — asserts `open-session` is rejected when the API key is
6. **Auth rejection** — asserts `open-session` is rejected when the API key is
missing, and (when `-RejectScopeApiKeyEnv` names an insufficient-scope key)
when the key lacks the required scope. Skip with `-SkipAuth`.
7. **Write round-trip***opt-in (`-VerifyWrite`).* Runs right after
`register`: adds and advises a configurable writable attribute
(`-WriteAttribute`, default `TestChangingInt`), writes a per-client
sentinel value, then streams events and asserts an `OnWriteComplete` event
for that item is observed — proof the write round-tripped through the
gateway, worker, and MXAccess provider. The written value being echoed back
in an `OnDataChange` is recorded best-effort (`echoObserved`): a
provider-driven attribute such as `TestChangingInt` accepts the write but
immediately overwrites it, so no data-change carries the value back. The
Rust `stream-events` CLI emits full per-event JSON (`family`, `itemHandle`,
`value`) so all five clients apply the same checks.
It is opt-in because it mutates live tag state. The phase fails fast if the
write command is rejected — e.g. against a gateway whose worker predates
write support (`MxAccessCommandExecutor` returning `InvalidRequest` for
`Write`/`Write2`/`WriteSecured`/`WriteSecured2`).
Build the gateway and worker, start the gateway, and provide a valid API key
before running the client e2e script:
@@ -209,9 +220,9 @@ powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -Clien
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -BulkTagCount 10
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -SkipStream
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -SkipBulk
# Write round-trip: point at a writable scalar attribute and its value type.
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -WriteAttribute TestChangingInt -WriteType int32
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -SkipWrite
# Write round-trip (opt-in): point at a writable scalar attribute and its
# value type.
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -VerifyWrite -WriteAttribute TestChangingInt -WriteType int32
# Auth rejection: also assert an insufficient-scope key is denied.
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -RejectScopeApiKeyEnv MXGATEWAY_READONLY_API_KEY
# Run all five clients concurrently as isolated child processes.
@@ -221,11 +232,12 @@ powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -DryRu
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -Endpoint localhost:5000 -ApiKeyEnv MXGATEWAY_API_KEY
```
The write round-trip fails loudly if `-WriteAttribute` does not name a writable
scalar attribute, if the write is rejected, or if the sentinel value is not
observed within `-WriteEchoMaxEvents` (default 200) streamed events. Point
`-WriteAttribute` at a stable writable attribute, raise `-WriteEchoMaxEvents`,
or pass `-SkipWrite` if no suitable attribute is deployed.
When `-VerifyWrite` is enabled, the write round-trip fails loudly if the write
command is rejected, if `-WriteAttribute` does not name a writable scalar
attribute, or if no `OnWriteComplete` event is observed for the written item
within `-WriteEchoMaxEvents` (default 200) streamed events. Raise
`-WriteEchoMaxEvents` if the gateway's per-session event backlog is large
enough to push `OnWriteComplete` past that bound.
## Focused Commands
+127 -76
View File
@@ -33,8 +33,10 @@ param(
[int]$BulkTagCount = 6,
[switch]$SkipStream,
[switch]$SkipBulk,
# Write round-trip + value assertion.
[switch]$SkipWrite,
# Write round-trip. Opt-in because it mutates live tag state: it writes a
# sentinel value to -WriteAttribute and asserts an OnWriteComplete event
# confirms the write reached the MXAccess provider.
[switch]$VerifyWrite,
[string]$WriteAttribute = "TestChangingInt",
[string]$WriteType = "int32",
[int]$WriteValueBase = 424200,
@@ -335,6 +337,14 @@ function Get-EventItemHandle {
return [int]$handle
}
# Extracts the event family as a string. All five CLIs render it as the
# protobuf enum name (e.g. MX_EVENT_FAMILY_ON_WRITE_COMPLETE).
function Get-EventFamily {
param([object]$Event)
return [string](Get-PropertyValue -Object $Event -Names @("family"))
}
# Extracts the scalar payload from a streamed event's MxValue as a string.
# The MxValue oneof renders to one protobuf-JSON `*Value` key; all five
# CLIs (after the Rust stream-events extension) emit the same key names.
@@ -595,7 +605,20 @@ function Get-ClientCommand {
$cliArgs += @("--session-id", $Values.sessionId)
}
$arguments = @("--quiet", ":mxgateway-cli:run", "--args=$($cliArgs -join ' ')")
return [pscustomobject]@{ file = "gradle"; args = $arguments; cwd = (Join-Path $repoRoot "clients/java"); env = @{} }
# Gradle ships as gradle.bat on Windows; .NET's Process.Start
# (UseShellExecute=false) cannot launch a batch file directly, so
# resolve the launcher and run it through cmd.exe.
$gradleCommand = Get-Command "gradle.bat", "gradle.cmd", "gradle.exe", "gradle" `
-ErrorAction SilentlyContinue | Select-Object -First 1
if ($null -eq $gradleCommand) {
throw "The 'gradle' command was not found on PATH; the Java client e2e flow requires Gradle."
}
return [pscustomobject]@{
file = "cmd.exe"
args = @("/c", $gradleCommand.Source) + $arguments
cwd = (Join-Path $repoRoot "clients/java")
env = @{}
}
}
}
}
@@ -627,23 +650,30 @@ function Get-DryRunReply {
return [pscustomobject]@{ unsubscribeBulk = [pscustomobject]@{ results = $results }; results = $results }
}
"stream-events" {
# Echo the requested write value back so the write round-trip
# assertion passes under -DryRun. The reply is shaped per client:
# Go and Java emit one event object per line (Read-JsonObject
# collapses NDJSON to a bare array), the others aggregate the
# events under an `events` property.
# Synthesize an OnDataChange (carrying the written value) and an
# OnWriteComplete so the write round-trip assertion passes under
# -DryRun. The reply is shaped per client: Go and Java emit one
# event object per line (Read-JsonObject collapses NDJSON to a
# bare array), the others aggregate the events under `events`.
$itemHandle = if ($Values.ContainsKey("echoItemHandle")) { [int]$Values.echoItemHandle } else { 1 }
$echoValue = if ($Values.ContainsKey("echoValue")) { $Values.echoValue } else { 1 }
$event = [pscustomobject]@{
$dataEvent = [pscustomobject]@{
workerSequence = 1
family = "MX_EVENT_FAMILY_ON_DATA_CHANGE"
itemHandle = $itemHandle
value = [pscustomobject]@{ int32Value = $echoValue }
}
$writeCompleteEvent = [pscustomobject]@{
workerSequence = 2
family = "MX_EVENT_FAMILY_ON_WRITE_COMPLETE"
itemHandle = $itemHandle
}
$events = @($dataEvent, $writeCompleteEvent)
switch ($Client) {
"go" { return ,@($event) }
"java" { return ,@($event) }
"rust" { return [pscustomobject]@{ eventCount = 1; events = @($event) } }
default { return [pscustomobject]@{ events = @($event) } }
"go" { return ,$events }
"java" { return ,$events }
"rust" { return [pscustomobject]@{ eventCount = $events.Count; events = $events } }
default { return [pscustomobject]@{ events = $events } }
}
}
default { return [pscustomobject]@{ ok = $true; reply = [pscustomobject]@{} } }
@@ -735,6 +765,83 @@ function Invoke-ClientFlow {
$serverHandle = Get-ServerHandle -Client $Client -Json $registerJson
$clientResult.serverHandle = $serverHandle
# --- Write round-trip + value assertion ---------------------------
# Runs right after register, before the bulk and add-item phases, so
# only a small backlog of events precedes the write. The gateway
# replays the per-session event buffer from the start, so the
# post-write OnWriteComplete must be reachable within the bounded
# -WriteEchoMaxEvents window.
if ($VerifyWrite) {
$writeTag = @($Tags | Where-Object {
$_.attributeName -eq $WriteAttribute
}) | Select-Object -First 1
if ($null -eq $writeTag) {
Write-Warning "$Client write phase skipped: no discovered tag has attribute '$WriteAttribute'."
} else {
$writeAddJson = Invoke-ClientOperation -Client $Client -Operation "add-item" -Values @{
sessionId = $sessionId
serverHandle = $serverHandle
item = $writeTag.fullTagReference
}
$writeItemHandle = Get-ItemHandle -Client $Client -Json $writeAddJson
Invoke-ClientOperation -Client $Client -Operation "advise" -Values @{
sessionId = $sessionId
serverHandle = $serverHandle
itemHandle = $writeItemHandle
} | Out-Null
$sentinelValue = "$($WriteValueBase + $script:clientFlowIndex)"
Invoke-ClientOperation -Client $Client -Operation "write" -Values @{
sessionId = $sessionId
serverHandle = $serverHandle
itemHandle = $writeItemHandle
valueType = $WriteType
value = $sentinelValue
} | Out-Null
$writeStreamJson = Invoke-ClientOperation -Client $Client -Operation "stream-events" -Values @{
sessionId = $sessionId
maxEvents = $WriteEchoMaxEvents
echoItemHandle = $writeItemHandle
echoValue = $sentinelValue
}
$writeEvents = @(Get-StreamEvents -Client $Client -Json $writeStreamJson)
$writeItemEvents = @($writeEvents | Where-Object {
(Get-EventItemHandle -Event $_) -eq $writeItemHandle
})
# The reliable write round-trip signal: MXAccess fires
# OnWriteComplete once the write reaches the provider. The
# value echo is best-effort — a provider-driven attribute
# (e.g. a simulated counter) accepts the write but does not
# hold the value, so no OnDataChange carries it back.
$writeCompleteEvent = $writeItemEvents | Where-Object {
(Get-EventFamily -Event $_) -match "WRITE_COMPLETE"
} | Select-Object -First 1
$echoEvent = $writeItemEvents | Where-Object {
Test-ValueEquals -Expected $sentinelValue -Observed (Get-EventScalar -Event $_)
} | Select-Object -First 1
if ($null -eq $writeCompleteEvent) {
throw ("$Client write round-trip failed: wrote $WriteType=$sentinelValue to " +
"'$($writeTag.fullTagReference)' (item handle $writeItemHandle) but no " +
"OnWriteComplete event was observed in $($writeEvents.Count) streamed event(s). " +
"Increase -WriteEchoMaxEvents, or drop -VerifyWrite.")
}
$clientResult.write = [ordered]@{
attributeName = $WriteAttribute
fullTagReference = $writeTag.fullTagReference
itemHandle = $writeItemHandle
valueType = $WriteType
value = $sentinelValue
writeCompleteObserved = $true
echoObserved = ($null -ne $echoEvent)
}
}
}
if (-not $SkipBulk) {
$bulkTags = @($Tags | Select-Object -First ([Math]::Min($BulkTagCount, $Tags.Count)))
$bulkItems = ($bulkTags | ForEach-Object { $_.fullTagReference }) -join ","
@@ -788,71 +895,15 @@ function Invoke-ClientFlow {
}
}
# --- Write round-trip + value assertion ---------------------------
# Write a per-client sentinel value to a configured writable
# attribute, then assert it is echoed back through the event stream.
$writeTarget = $null
if (-not $SkipWrite) {
$writeTarget = @($clientResult.addedItems | Where-Object {
$_.attributeName -eq $WriteAttribute
}) | Select-Object -First 1
}
$doWrite = $null -ne $writeTarget
$sentinelValue = $null
if ($doWrite) {
$sentinelValue = "$($WriteValueBase + $script:clientFlowIndex)"
Invoke-ClientOperation -Client $Client -Operation "write" -Values @{
# --- Event streaming ----------------------------------------------
if (-not $SkipStream) {
$streamJson = Invoke-ClientOperation -Client $Client -Operation "stream-events" -Values @{
sessionId = $sessionId
serverHandle = $serverHandle
itemHandle = $writeTarget.itemHandle
valueType = $WriteType
value = $sentinelValue
} | Out-Null
} elseif (-not $SkipWrite) {
Write-Warning "$Client write phase skipped: no discovered tag has attribute '$WriteAttribute'."
}
# --- Event streaming (also serves the write echo assertion) -------
$captureEvents = (-not $SkipStream) -or $doWrite
if ($captureEvents) {
$streamValues = @{ sessionId = $sessionId }
if ($doWrite) {
$streamValues.maxEvents = $WriteEchoMaxEvents
$streamValues.echoItemHandle = $writeTarget.itemHandle
$streamValues.echoValue = $sentinelValue
}
$streamJson = Invoke-ClientOperation -Client $Client -Operation "stream-events" -Values $streamValues
$events = @(Get-StreamEvents -Client $Client -Json $streamJson)
$clientResult.eventCount = Get-StreamEventCount -Client $Client -Json $streamJson
if (-not $SkipStream -and $clientResult.eventCount -lt 1) {
if ($clientResult.eventCount -lt 1) {
throw "The $Client stream-events command returned no events."
}
if ($doWrite) {
$echoEvent = $events | Where-Object {
(Get-EventItemHandle -Event $_) -eq $writeTarget.itemHandle -and
(Test-ValueEquals -Expected $sentinelValue -Observed (Get-EventScalar -Event $_))
} | Select-Object -First 1
if ($null -eq $echoEvent) {
throw ("$Client write round-trip failed: wrote $WriteType=$sentinelValue to " +
"'$($writeTarget.fullTagReference)' (item handle $($writeTarget.itemHandle)) " +
"but no matching value was observed in $($events.Count) streamed event(s). " +
"Increase -WriteEchoMaxEvents, point -WriteAttribute at a writable attribute, or pass -SkipWrite.")
}
$clientResult.write = [ordered]@{
attributeName = $WriteAttribute
fullTagReference = $writeTarget.fullTagReference
itemHandle = $writeTarget.itemHandle
valueType = $WriteType
value = $sentinelValue
echoObserved = $true
echoWorkerSequence = (Get-PropertyValue -Object $echoEvent -Names @("workerSequence", "worker_sequence"))
}
}
}
# --- Error-path (parity) checks -----------------------------------
@@ -965,7 +1016,7 @@ function Get-ChildArgumentList {
}
if ($SkipStream) { $childArgs += "-SkipStream" }
if ($SkipBulk) { $childArgs += "-SkipBulk" }
if ($SkipWrite) { $childArgs += "-SkipWrite" }
if ($VerifyWrite) { $childArgs += "-VerifyWrite" }
if ($SkipParity) { $childArgs += "-SkipParity" }
if ($SkipAuth) { $childArgs += "-SkipAuth" }
if ($DryRun) { $childArgs += "-DryRun" }
@@ -1043,7 +1094,7 @@ if ($Parallel -and $Clients.Count -gt 1) {
bulkTagCount = $BulkTagCount
skipStream = [bool]$SkipStream
skipBulk = [bool]$SkipBulk
skipWrite = [bool]$SkipWrite
verifyWrite = [bool]$VerifyWrite
skipParity = [bool]$SkipParity
skipAuth = [bool]$SkipAuth
writeAttribute = $WriteAttribute
@@ -1101,7 +1152,7 @@ $run = [ordered]@{
bulkTagCount = $BulkTagCount
skipStream = [bool]$SkipStream
skipBulk = [bool]$SkipBulk
skipWrite = [bool]$SkipWrite
verifyWrite = [bool]$VerifyWrite
skipParity = [bool]$SkipParity
skipAuth = [bool]$SkipAuth
writeAttribute = $WriteAttribute
@@ -46,6 +46,78 @@ public sealed class VariantConverterTests
Assert.Equal("VT_DATE", converted.VariantType);
}
/// <summary>Verifies that scalar MxValue kinds convert to the matching boxed CLR type for a COM write.</summary>
[Fact]
public void ConvertToComValue_WithInt32_ReturnsBoxedInt()
{
object? result = _converter.ConvertToComValue(new MxValue { Int32Value = 123 });
Assert.Equal(123, Assert.IsType<int>(result));
}
/// <summary>Verifies that a boolean MxValue converts to a boxed bool for a COM write.</summary>
[Fact]
public void ConvertToComValue_WithBool_ReturnsBoxedBool()
{
object? result = _converter.ConvertToComValue(new MxValue { BoolValue = true });
Assert.True(Assert.IsType<bool>(result));
}
/// <summary>Verifies that a string MxValue converts to a string for a COM write.</summary>
[Fact]
public void ConvertToComValue_WithString_ReturnsString()
{
object? result = _converter.ConvertToComValue(new MxValue { StringValue = "abc" });
Assert.Equal("abc", Assert.IsType<string>(result));
}
/// <summary>Verifies that a timestamp MxValue converts to a UTC DateTime the COM marshaler renders as VT_DATE.</summary>
[Fact]
public void ConvertToComValue_WithTimestamp_ReturnsUtcDateTime()
{
DateTime dateTime = new(2026, 5, 19, 12, 0, 0, DateTimeKind.Utc);
object? result = _converter.ConvertToComValue(
new MxValue { TimestampValue = ProtobufTimestamp.FromDateTime(dateTime) });
Assert.Equal(dateTime, Assert.IsType<DateTime>(result));
}
/// <summary>Verifies that an MXAccess-null MxValue converts to a CLR null.</summary>
[Fact]
public void ConvertToComValue_WithNull_ReturnsNull()
{
object? result = _converter.ConvertToComValue(new MxValue { IsNull = true });
Assert.Null(result);
}
/// <summary>Verifies that an integer-array MxValue converts to an int array the COM marshaler renders as a SAFEARRAY.</summary>
[Fact]
public void ConvertToComValue_WithInt32Array_ReturnsInt32Array()
{
MxValue value = new()
{
ArrayValue = new MxArray
{
Int32Values = new Int32Array { Values = { 1, 2, 3 } },
},
};
object? result = _converter.ConvertToComValue(value);
Assert.Equal(new[] { 1, 2, 3 }, Assert.IsType<int[]>(result));
}
/// <summary>Verifies that an MxValue with no value kind set cannot be converted for a COM write.</summary>
[Fact]
public void ConvertToComValue_WithNoKind_Throws()
{
Assert.Throws<ArgumentException>(() => _converter.ConvertToComValue(new MxValue()));
}
/// <summary>Verifies that file time values with expected time data type are converted to protobuf timestamps.</summary>
[Fact]
public void Convert_WithFileTimeAndExpectedTime_ProjectsTimestamp()
@@ -379,10 +379,10 @@ public sealed class AlarmCommandExecutorTests
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds) { }
public void Suspend(int serverHandle, int itemHandle) { }
public void Activate(int serverHandle, int itemHandle) { }
public void Write(int serverHandle, int itemHandle, object value, int userId) { }
public void Write2(int serverHandle, int itemHandle, object value, object timestampValue, int userId) { }
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value) { }
public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value, object timestampValue) { }
public void Write(int serverHandle, int itemHandle, object? value, int userId) { }
public void Write2(int serverHandle, int itemHandle, object? value, object? timestampValue, int userId) { }
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value) { }
public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestampValue) { }
public int AuthenticateUser(string userName, string password) => 0;
public int ArchestrAUserToId(string userName) => 0;
}
@@ -134,5 +134,26 @@ public sealed class MxAccessComServerTests
{
calls.Add($"AdviseSupervisory:{serverHandle}:{itemHandle}");
}
public void Write(int serverHandle, int itemHandle, object? value, int userId)
{
calls.Add($"Write:{serverHandle}:{itemHandle}:{value}:{userId}");
}
public void Write2(int serverHandle, int itemHandle, object? value, object? timestamp, int userId)
{
calls.Add($"Write2:{serverHandle}:{itemHandle}:{value}:{timestamp}:{userId}");
}
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value)
{
calls.Add($"WriteSecured:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}");
}
public void WriteSecured2(
int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestamp)
{
calls.Add($"WriteSecured2:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}:{timestamp}");
}
}
}
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Google.Protobuf.WellKnownTypes;
using MxGateway.Contracts.Proto;
using MxGateway.Worker.MxAccess;
using MxGateway.Worker.Sta;
@@ -617,6 +618,143 @@ public sealed class MxAccessCommandExecutorTests
Assert.Null(factory.FakeComObject.AdviseServerHandle);
}
/// <summary>Verifies that Write dispatches the converted value to MXAccess on the STA thread.</summary>
[Fact]
public async Task DispatchAsync_Write_CallsMxAccessOnStaWithConvertedValue()
{
FakeMxAccessComObject fakeComObject = new(registerHandle: 70);
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
MxCommandReply reply = await session.DispatchAsync(CreateWriteCommand(
"write", serverHandle: 70, itemHandle: 700, value: 123, userId: 5));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.Equal(MxCommandKind.Write, reply.Kind);
Assert.Equal(70, fakeComObject.WriteServerHandle);
Assert.Equal(700, fakeComObject.WriteItemHandle);
Assert.Equal(123, fakeComObject.WriteValue);
Assert.Equal(5, fakeComObject.WriteUserId);
Assert.Equal(runtime.StaThreadId, fakeComObject.WriteThreadId);
}
/// <summary>Verifies that Write2 forwards the converted value and timestamp to MXAccess.</summary>
[Fact]
public async Task DispatchAsync_Write2_ForwardsValueAndTimestamp()
{
FakeMxAccessComObject fakeComObject = new(registerHandle: 71);
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
DateTime timestamp = new(2026, 5, 19, 12, 0, 0, DateTimeKind.Utc);
MxCommandReply reply = await session.DispatchAsync(CreateWrite2Command(
"write2", serverHandle: 71, itemHandle: 710, value: 456, timestamp: timestamp, userId: 6));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.Equal(MxCommandKind.Write2, reply.Kind);
Assert.Equal(710, fakeComObject.WriteItemHandle);
Assert.Equal(456, fakeComObject.WriteValue);
Assert.Equal(timestamp, fakeComObject.WriteTimestamp);
Assert.Equal(6, fakeComObject.WriteUserId);
}
/// <summary>Verifies that WriteSecured forwards the operator and verifier user ids to MXAccess.</summary>
[Fact]
public async Task DispatchAsync_WriteSecured_ForwardsUserIds()
{
FakeMxAccessComObject fakeComObject = new(registerHandle: 72);
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecuredCommand(
"write-secured", serverHandle: 72, itemHandle: 720, value: 789, currentUserId: 11, verifierUserId: 22));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.Equal(MxCommandKind.WriteSecured, reply.Kind);
Assert.Equal(720, fakeComObject.WriteItemHandle);
Assert.Equal(789, fakeComObject.WriteValue);
Assert.Equal(11, fakeComObject.WriteCurrentUserId);
Assert.Equal(22, fakeComObject.WriteVerifierUserId);
}
/// <summary>Verifies that WriteSecured2 forwards user ids, value, and timestamp to MXAccess.</summary>
[Fact]
public async Task DispatchAsync_WriteSecured2_ForwardsUserIdsValueAndTimestamp()
{
FakeMxAccessComObject fakeComObject = new(registerHandle: 73);
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
DateTime timestamp = new(2026, 5, 19, 13, 30, 0, DateTimeKind.Utc);
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecured2Command(
"write-secured2", serverHandle: 73, itemHandle: 730, value: 1011,
timestamp: timestamp, currentUserId: 33, verifierUserId: 44));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.Equal(MxCommandKind.WriteSecured2, reply.Kind);
Assert.Equal(1011, fakeComObject.WriteValue);
Assert.Equal(timestamp, fakeComObject.WriteTimestamp);
Assert.Equal(33, fakeComObject.WriteCurrentUserId);
Assert.Equal(44, fakeComObject.WriteVerifierUserId);
}
/// <summary>Verifies that Write without a payload returns an invalid request error.</summary>
[Fact]
public async Task DispatchAsync_WriteWithoutPayload_ReturnsInvalidRequest()
{
FakeMxAccessComObject fakeComObject = new(registerHandle: 74);
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
MxCommandReply reply = await session.DispatchAsync(new StaCommand(
"session-1",
"missing-write-payload",
new MxCommand
{
Kind = MxCommandKind.Write,
}));
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
Assert.Null(fakeComObject.WriteServerHandle);
}
/// <summary>Verifies that Write without a value returns an invalid request error.</summary>
[Fact]
public async Task DispatchAsync_WriteWithoutValue_ReturnsInvalidRequest()
{
FakeMxAccessComObject fakeComObject = new(registerHandle: 75);
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
MxCommandReply reply = await session.DispatchAsync(new StaCommand(
"session-1",
"missing-write-value",
new MxCommand
{
Kind = MxCommandKind.Write,
Write = new WriteCommand
{
ServerHandle = 75,
ItemHandle = 750,
},
}));
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
Assert.Null(fakeComObject.WriteServerHandle);
}
private static StaCommand CreateRegisterCommand(
string correlationId,
string clientName)
@@ -729,6 +867,126 @@ public sealed class MxAccessCommandExecutorTests
});
}
private static MxValue CreateIntegerValue(int value)
{
return new MxValue
{
DataType = MxDataType.Integer,
VariantType = "VT_I4",
Int32Value = value,
};
}
private static MxValue CreateTimestampValue(DateTime timestamp)
{
return new MxValue
{
DataType = MxDataType.Time,
VariantType = "VT_DATE",
TimestampValue = Timestamp.FromDateTime(timestamp),
};
}
private static StaCommand CreateWriteCommand(
string correlationId,
int serverHandle,
int itemHandle,
int value,
int userId)
{
return new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.Write,
Write = new WriteCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
Value = CreateIntegerValue(value),
UserId = userId,
},
});
}
private static StaCommand CreateWrite2Command(
string correlationId,
int serverHandle,
int itemHandle,
int value,
DateTime timestamp,
int userId)
{
return new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.Write2,
Write2 = new Write2Command
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
Value = CreateIntegerValue(value),
TimestampValue = CreateTimestampValue(timestamp),
UserId = userId,
},
});
}
private static StaCommand CreateWriteSecuredCommand(
string correlationId,
int serverHandle,
int itemHandle,
int value,
int currentUserId,
int verifierUserId)
{
return new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.WriteSecured,
WriteSecured = new WriteSecuredCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
CurrentUserId = currentUserId,
VerifierUserId = verifierUserId,
Value = CreateIntegerValue(value),
},
});
}
private static StaCommand CreateWriteSecured2Command(
string correlationId,
int serverHandle,
int itemHandle,
int value,
DateTime timestamp,
int currentUserId,
int verifierUserId)
{
return new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.WriteSecured2,
WriteSecured2 = new WriteSecured2Command
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
CurrentUserId = currentUserId,
VerifierUserId = verifierUserId,
Value = CreateIntegerValue(value),
TimestampValue = CreateTimestampValue(timestamp),
},
});
}
private static StaCommand CreateUnAdviseCommand(
string correlationId,
int serverHandle,
@@ -1080,6 +1338,118 @@ public sealed class MxAccessCommandExecutorTests
throw adviseSupervisoryException;
}
}
/// <summary>Gets the server handle passed to the most recent write, if called.</summary>
public int? WriteServerHandle { get; private set; }
/// <summary>Gets the item handle passed to the most recent write, if called.</summary>
public int? WriteItemHandle { get; private set; }
/// <summary>Gets the value passed to the most recent write, if called.</summary>
public object? WriteValue { get; private set; }
/// <summary>Gets the timestamp passed to the most recent timestamped write, if called.</summary>
public object? WriteTimestamp { get; private set; }
/// <summary>Gets the user id passed to the most recent Write/Write2, if called.</summary>
public int? WriteUserId { get; private set; }
/// <summary>Gets the current user id passed to the most recent secured write, if called.</summary>
public int? WriteCurrentUserId { get; private set; }
/// <summary>Gets the verifier user id passed to the most recent secured write, if called.</summary>
public int? WriteVerifierUserId { get; private set; }
/// <summary>Gets the thread ID on which the most recent write was called.</summary>
public int? WriteThreadId { get; private set; }
/// <summary>Writes a value to an item and tracks the operation.</summary>
/// <param name="serverHandle">Server handle for the write.</param>
/// <param name="itemHandle">Item handle to write to.</param>
/// <param name="value">Value to write.</param>
/// <param name="userId">MXAccess user id for the write.</param>
public void Write(
int serverHandle,
int itemHandle,
object? value,
int userId)
{
operationNames.Add($"Write:{serverHandle}:{itemHandle}");
WriteServerHandle = serverHandle;
WriteItemHandle = itemHandle;
WriteValue = value;
WriteUserId = userId;
WriteThreadId = Environment.CurrentManagedThreadId;
}
/// <summary>Writes a timestamped value to an item and tracks the operation.</summary>
/// <param name="serverHandle">Server handle for the write.</param>
/// <param name="itemHandle">Item handle to write to.</param>
/// <param name="value">Value to write.</param>
/// <param name="timestamp">Source timestamp for the write.</param>
/// <param name="userId">MXAccess user id for the write.</param>
public void Write2(
int serverHandle,
int itemHandle,
object? value,
object? timestamp,
int userId)
{
operationNames.Add($"Write2:{serverHandle}:{itemHandle}");
WriteServerHandle = serverHandle;
WriteItemHandle = itemHandle;
WriteValue = value;
WriteTimestamp = timestamp;
WriteUserId = userId;
WriteThreadId = Environment.CurrentManagedThreadId;
}
/// <summary>Performs a secured write to an item and tracks the operation.</summary>
/// <param name="serverHandle">Server handle for the write.</param>
/// <param name="itemHandle">Item handle to write to.</param>
/// <param name="currentUserId">Operator user id.</param>
/// <param name="verifierUserId">Verifier user id.</param>
/// <param name="value">Value to write.</param>
public void WriteSecured(
int serverHandle,
int itemHandle,
int currentUserId,
int verifierUserId,
object? value)
{
operationNames.Add($"WriteSecured:{serverHandle}:{itemHandle}");
WriteServerHandle = serverHandle;
WriteItemHandle = itemHandle;
WriteCurrentUserId = currentUserId;
WriteVerifierUserId = verifierUserId;
WriteValue = value;
WriteThreadId = Environment.CurrentManagedThreadId;
}
/// <summary>Performs a secured timestamped write to an item and tracks the operation.</summary>
/// <param name="serverHandle">Server handle for the write.</param>
/// <param name="itemHandle">Item handle to write to.</param>
/// <param name="currentUserId">Operator user id.</param>
/// <param name="verifierUserId">Verifier user id.</param>
/// <param name="value">Value to write.</param>
/// <param name="timestamp">Source timestamp for the write.</param>
public void WriteSecured2(
int serverHandle,
int itemHandle,
int currentUserId,
int verifierUserId,
object? value,
object? timestamp)
{
operationNames.Add($"WriteSecured2:{serverHandle}:{itemHandle}");
WriteServerHandle = serverHandle;
WriteItemHandle = itemHandle;
WriteCurrentUserId = currentUserId;
WriteVerifierUserId = verifierUserId;
WriteValue = value;
WriteTimestamp = timestamp;
WriteThreadId = Environment.CurrentManagedThreadId;
}
}
/// <summary>Factory for creating fake MXAccess COM objects in tests.</summary>
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
using System.Linq;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using MxGateway.Contracts.Proto;
@@ -118,6 +119,63 @@ public sealed class VariantConverter
}
}
/// <summary>
/// Converts an <see cref="MxValue"/> into a CLR object suitable for an
/// MXAccess COM write. The COM marshaler boxes the returned value into the
/// matching VARIANT, so this is the inverse of <see cref="Convert(object?)"/>.
/// </summary>
/// <param name="value">Protobuf value to convert.</param>
/// <returns>A COM-marshalable value, or <see langword="null"/> for an MXAccess null.</returns>
public object? ConvertToComValue(MxValue value)
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
if (value.IsNull)
{
return null;
}
return value.KindCase switch
{
MxValue.KindOneofCase.BoolValue => value.BoolValue,
MxValue.KindOneofCase.Int32Value => value.Int32Value,
MxValue.KindOneofCase.Int64Value => value.Int64Value,
MxValue.KindOneofCase.FloatValue => value.FloatValue,
MxValue.KindOneofCase.DoubleValue => value.DoubleValue,
MxValue.KindOneofCase.StringValue => value.StringValue,
// The COM marshaler renders a DateTime as VT_DATE; MXAccess accepts
// it as the timestamped-write time argument.
MxValue.KindOneofCase.TimestampValue => value.TimestampValue.ToDateTime(),
MxValue.KindOneofCase.ArrayValue => ConvertToComArray(value.ArrayValue),
MxValue.KindOneofCase.RawValue => throw new ArgumentException(
"MxValue raw payloads cannot be written to MXAccess.", nameof(value)),
_ => throw new ArgumentException(
"MxValue has no value kind set; nothing to write.", nameof(value)),
};
}
private static Array ConvertToComArray(MxArray array)
{
return array.ValuesCase switch
{
MxArray.ValuesOneofCase.BoolValues => array.BoolValues.Values.ToArray(),
MxArray.ValuesOneofCase.Int32Values => array.Int32Values.Values.ToArray(),
MxArray.ValuesOneofCase.Int64Values => array.Int64Values.Values.ToArray(),
MxArray.ValuesOneofCase.FloatValues => array.FloatValues.Values.ToArray(),
MxArray.ValuesOneofCase.DoubleValues => array.DoubleValues.Values.ToArray(),
MxArray.ValuesOneofCase.StringValues => array.StringValues.Values.ToArray(),
MxArray.ValuesOneofCase.TimestampValues =>
array.TimestampValues.Values.Select(timestamp => timestamp.ToDateTime()).ToArray(),
MxArray.ValuesOneofCase.RawValues => throw new ArgumentException(
"MxArray raw payloads cannot be written to MXAccess.", nameof(array)),
_ => throw new ArgumentException(
"MxArray has no element values set; nothing to write.", nameof(array)),
};
}
private static MxValue ConvertScalar(
object value,
MxDataType expectedDataType)
@@ -56,4 +56,56 @@ public interface IMxAccessServer
void AdviseSupervisory(
int serverHandle,
int itemHandle);
/// <summary>Writes a value to an item.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to write to.</param>
/// <param name="value">COM-marshalable value to write; <see langword="null"/> writes an MXAccess null.</param>
/// <param name="userId">MXAccess user id (security classification) for the write.</param>
void Write(
int serverHandle,
int itemHandle,
object? value,
int userId);
/// <summary>Writes a value with an explicit source timestamp to an item.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to write to.</param>
/// <param name="value">COM-marshalable value to write; <see langword="null"/> writes an MXAccess null.</param>
/// <param name="timestamp">COM-marshalable source timestamp for the write.</param>
/// <param name="userId">MXAccess user id (security classification) for the write.</param>
void Write2(
int serverHandle,
int itemHandle,
object? value,
object? timestamp,
int userId);
/// <summary>Performs a secured/verified write to an item.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to write to.</param>
/// <param name="currentUserId">MXAccess user id of the operator performing the write.</param>
/// <param name="verifierUserId">MXAccess user id of the verifier authorizing the write.</param>
/// <param name="value">COM-marshalable value to write; <see langword="null"/> writes an MXAccess null.</param>
void WriteSecured(
int serverHandle,
int itemHandle,
int currentUserId,
int verifierUserId,
object? value);
/// <summary>Performs a secured/verified write with an explicit source timestamp.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to write to.</param>
/// <param name="currentUserId">MXAccess user id of the operator performing the write.</param>
/// <param name="verifierUserId">MXAccess user id of the verifier authorizing the write.</param>
/// <param name="value">COM-marshalable value to write; <see langword="null"/> writes an MXAccess null.</param>
/// <param name="timestamp">COM-marshalable source timestamp for the write.</param>
void WriteSecured2(
int serverHandle,
int itemHandle,
int currentUserId,
int verifierUserId,
object? value,
object? timestamp);
}
@@ -140,6 +140,74 @@ public sealed class MxAccessComServer : IMxAccessServer
AsProxyServer4().AdviseSupervisory(serverHandle, itemHandle);
}
/// <inheritdoc />
public void Write(
int serverHandle,
int itemHandle,
object? value,
int userId)
{
if (mxAccessComObject is IMxAccessServer typedFake)
{
typedFake.Write(serverHandle, itemHandle, value, userId);
return;
}
AsProxyServer().Write(serverHandle, itemHandle, value!, userId);
}
/// <inheritdoc />
public void Write2(
int serverHandle,
int itemHandle,
object? value,
object? timestamp,
int userId)
{
if (mxAccessComObject is IMxAccessServer typedFake)
{
typedFake.Write2(serverHandle, itemHandle, value, timestamp, userId);
return;
}
AsProxyServer4().Write2(serverHandle, itemHandle, value!, timestamp!, userId);
}
/// <inheritdoc />
public void WriteSecured(
int serverHandle,
int itemHandle,
int currentUserId,
int verifierUserId,
object? value)
{
if (mxAccessComObject is IMxAccessServer typedFake)
{
typedFake.WriteSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value);
return;
}
AsProxyServer().WriteSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value!);
}
/// <inheritdoc />
public void WriteSecured2(
int serverHandle,
int itemHandle,
int currentUserId,
int verifierUserId,
object? value,
object? timestamp)
{
if (mxAccessComObject is IMxAccessServer typedFake)
{
typedFake.WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value, timestamp);
return;
}
AsProxyServer4().WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value!, timestamp!);
}
private ILMXProxyServer AsProxyServer()
{
return mxAccessComObject as ILMXProxyServer
@@ -74,6 +74,10 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
MxCommandKind.Advise => ExecuteAdvise(command),
MxCommandKind.UnAdvise => ExecuteUnAdvise(command),
MxCommandKind.AdviseSupervisory => ExecuteAdviseSupervisory(command),
MxCommandKind.Write => ExecuteWrite(command),
MxCommandKind.Write2 => ExecuteWrite2(command),
MxCommandKind.WriteSecured => ExecuteWriteSecured(command),
MxCommandKind.WriteSecured2 => ExecuteWriteSecured2(command),
MxCommandKind.AddItemBulk => ExecuteAddItemBulk(command),
MxCommandKind.AdviseItemBulk => ExecuteAdviseItemBulk(command),
MxCommandKind.RemoveItemBulk => ExecuteRemoveItemBulk(command),
@@ -223,6 +227,108 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
return CreateOkReply(command);
}
private MxCommandReply ExecuteWrite(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.Write)
{
return CreateInvalidRequestReply(command, "Write command payload is required.");
}
WriteCommand writeCommand = command.Command.Write;
if (writeCommand.Value is null)
{
return CreateInvalidRequestReply(command, "Write command value is required.");
}
session.Write(
writeCommand.ServerHandle,
writeCommand.ItemHandle,
variantConverter.ConvertToComValue(writeCommand.Value),
writeCommand.UserId);
return CreateOkReply(command);
}
private MxCommandReply ExecuteWrite2(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.Write2)
{
return CreateInvalidRequestReply(command, "Write2 command payload is required.");
}
Write2Command write2Command = command.Command.Write2;
if (write2Command.Value is null)
{
return CreateInvalidRequestReply(command, "Write2 command value is required.");
}
if (write2Command.TimestampValue is null)
{
return CreateInvalidRequestReply(command, "Write2 command timestamp value is required.");
}
session.Write2(
write2Command.ServerHandle,
write2Command.ItemHandle,
variantConverter.ConvertToComValue(write2Command.Value),
variantConverter.ConvertToComValue(write2Command.TimestampValue),
write2Command.UserId);
return CreateOkReply(command);
}
private MxCommandReply ExecuteWriteSecured(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.WriteSecured)
{
return CreateInvalidRequestReply(command, "WriteSecured command payload is required.");
}
WriteSecuredCommand writeSecuredCommand = command.Command.WriteSecured;
if (writeSecuredCommand.Value is null)
{
return CreateInvalidRequestReply(command, "WriteSecured command value is required.");
}
session.WriteSecured(
writeSecuredCommand.ServerHandle,
writeSecuredCommand.ItemHandle,
writeSecuredCommand.CurrentUserId,
writeSecuredCommand.VerifierUserId,
variantConverter.ConvertToComValue(writeSecuredCommand.Value));
return CreateOkReply(command);
}
private MxCommandReply ExecuteWriteSecured2(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.WriteSecured2)
{
return CreateInvalidRequestReply(command, "WriteSecured2 command payload is required.");
}
WriteSecured2Command writeSecured2Command = command.Command.WriteSecured2;
if (writeSecured2Command.Value is null)
{
return CreateInvalidRequestReply(command, "WriteSecured2 command value is required.");
}
if (writeSecured2Command.TimestampValue is null)
{
return CreateInvalidRequestReply(command, "WriteSecured2 command timestamp value is required.");
}
session.WriteSecured2(
writeSecured2Command.ServerHandle,
writeSecured2Command.ItemHandle,
writeSecured2Command.CurrentUserId,
writeSecured2Command.VerifierUserId,
variantConverter.ConvertToComValue(writeSecured2Command.Value),
variantConverter.ConvertToComValue(writeSecured2Command.TimestampValue));
return CreateOkReply(command);
}
private MxCommandReply ExecuteAddItemBulk(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AddItemBulk)
@@ -227,6 +227,78 @@ public sealed class MxAccessSession : IDisposable
MxAccessAdviceKind.Supervisory);
}
/// <summary>Writes a value to an item.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <param name="value">COM-marshalable value to write.</param>
/// <param name="userId">MXAccess user id (security classification) for the write.</param>
public void Write(
int serverHandle,
int itemHandle,
object? value,
int userId)
{
ThrowIfDisposed();
mxAccessServer.Write(serverHandle, itemHandle, value, userId);
}
/// <summary>Writes a value with an explicit source timestamp to an item.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <param name="value">COM-marshalable value to write.</param>
/// <param name="timestamp">COM-marshalable source timestamp for the write.</param>
/// <param name="userId">MXAccess user id (security classification) for the write.</param>
public void Write2(
int serverHandle,
int itemHandle,
object? value,
object? timestamp,
int userId)
{
ThrowIfDisposed();
mxAccessServer.Write2(serverHandle, itemHandle, value, timestamp, userId);
}
/// <summary>Performs a secured/verified write to an item.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <param name="currentUserId">MXAccess user id of the operator performing the write.</param>
/// <param name="verifierUserId">MXAccess user id of the verifier authorizing the write.</param>
/// <param name="value">COM-marshalable value to write.</param>
public void WriteSecured(
int serverHandle,
int itemHandle,
int currentUserId,
int verifierUserId,
object? value)
{
ThrowIfDisposed();
mxAccessServer.WriteSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value);
}
/// <summary>Performs a secured/verified write with an explicit source timestamp.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <param name="currentUserId">MXAccess user id of the operator performing the write.</param>
/// <param name="verifierUserId">MXAccess user id of the verifier authorizing the write.</param>
/// <param name="value">COM-marshalable value to write.</param>
/// <param name="timestamp">COM-marshalable source timestamp for the write.</param>
public void WriteSecured2(
int serverHandle,
int itemHandle,
int currentUserId,
int verifierUserId,
object? value,
object? timestamp)
{
ThrowIfDisposed();
mxAccessServer.WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value, timestamp);
}
/// <summary>Adds multiple items in bulk, returning success/failure results.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="tagAddresses">Enumerable of item definitions to add.</param>