From 06030dd1efb186227342c482901e5a212e4a53f7 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 19 May 2026 14:45:35 -0400 Subject: [PATCH] Implement MXAccess write commands in the worker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Conversion/VariantConverterTests.cs | 72 ++++ .../MxAccess/AlarmCommandExecutorTests.cs | 8 +- .../MxAccess/MxAccessComServerTests.cs | 21 + .../MxAccess/MxAccessCommandExecutorTests.cs | 370 ++++++++++++++++++ .../Conversion/VariantConverter.cs | 58 +++ .../MxAccess/IMxAccessServer.cs | 52 +++ .../MxAccess/MxAccessComServer.cs | 68 ++++ .../MxAccess/MxAccessCommandExecutor.cs | 106 +++++ .../MxAccess/MxAccessSession.cs | 72 ++++ 9 files changed, 823 insertions(+), 4 deletions(-) diff --git a/src/MxGateway.Worker.Tests/Conversion/VariantConverterTests.cs b/src/MxGateway.Worker.Tests/Conversion/VariantConverterTests.cs index 1933f47..fa16fcf 100644 --- a/src/MxGateway.Worker.Tests/Conversion/VariantConverterTests.cs +++ b/src/MxGateway.Worker.Tests/Conversion/VariantConverterTests.cs @@ -46,6 +46,78 @@ public sealed class VariantConverterTests Assert.Equal("VT_DATE", converted.VariantType); } + /// Verifies that scalar MxValue kinds convert to the matching boxed CLR type for a COM write. + [Fact] + public void ConvertToComValue_WithInt32_ReturnsBoxedInt() + { + object? result = _converter.ConvertToComValue(new MxValue { Int32Value = 123 }); + + Assert.Equal(123, Assert.IsType(result)); + } + + /// Verifies that a boolean MxValue converts to a boxed bool for a COM write. + [Fact] + public void ConvertToComValue_WithBool_ReturnsBoxedBool() + { + object? result = _converter.ConvertToComValue(new MxValue { BoolValue = true }); + + Assert.True(Assert.IsType(result)); + } + + /// Verifies that a string MxValue converts to a string for a COM write. + [Fact] + public void ConvertToComValue_WithString_ReturnsString() + { + object? result = _converter.ConvertToComValue(new MxValue { StringValue = "abc" }); + + Assert.Equal("abc", Assert.IsType(result)); + } + + /// Verifies that a timestamp MxValue converts to a UTC DateTime the COM marshaler renders as VT_DATE. + [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(result)); + } + + /// Verifies that an MXAccess-null MxValue converts to a CLR null. + [Fact] + public void ConvertToComValue_WithNull_ReturnsNull() + { + object? result = _converter.ConvertToComValue(new MxValue { IsNull = true }); + + Assert.Null(result); + } + + /// Verifies that an integer-array MxValue converts to an int array the COM marshaler renders as a SAFEARRAY. + [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(result)); + } + + /// Verifies that an MxValue with no value kind set cannot be converted for a COM write. + [Fact] + public void ConvertToComValue_WithNoKind_Throws() + { + Assert.Throws(() => _converter.ConvertToComValue(new MxValue())); + } + /// Verifies that file time values with expected time data type are converted to protobuf timestamps. [Fact] public void Convert_WithFileTimeAndExpectedTime_ProjectsTimestamp() diff --git a/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs index b9f512f..86a0231 100644 --- a/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs +++ b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs @@ -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; } diff --git a/src/MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs b/src/MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs index fd121c4..faf9ec1 100644 --- a/src/MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs +++ b/src/MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs @@ -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}"); + } } } diff --git a/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs b/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs index 43fa4f2..b4bc61e 100644 --- a/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs +++ b/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs @@ -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); } + /// Verifies that Write dispatches the converted value to MXAccess on the STA thread. + [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); + } + + /// Verifies that Write2 forwards the converted value and timestamp to MXAccess. + [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); + } + + /// Verifies that WriteSecured forwards the operator and verifier user ids to MXAccess. + [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); + } + + /// Verifies that WriteSecured2 forwards user ids, value, and timestamp to MXAccess. + [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); + } + + /// Verifies that Write without a payload returns an invalid request error. + [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); + } + + /// Verifies that Write without a value returns an invalid request error. + [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; } } + + /// Gets the server handle passed to the most recent write, if called. + public int? WriteServerHandle { get; private set; } + + /// Gets the item handle passed to the most recent write, if called. + public int? WriteItemHandle { get; private set; } + + /// Gets the value passed to the most recent write, if called. + public object? WriteValue { get; private set; } + + /// Gets the timestamp passed to the most recent timestamped write, if called. + public object? WriteTimestamp { get; private set; } + + /// Gets the user id passed to the most recent Write/Write2, if called. + public int? WriteUserId { get; private set; } + + /// Gets the current user id passed to the most recent secured write, if called. + public int? WriteCurrentUserId { get; private set; } + + /// Gets the verifier user id passed to the most recent secured write, if called. + public int? WriteVerifierUserId { get; private set; } + + /// Gets the thread ID on which the most recent write was called. + public int? WriteThreadId { get; private set; } + + /// Writes a value to an item and tracks the operation. + /// Server handle for the write. + /// Item handle to write to. + /// Value to write. + /// MXAccess user id for the write. + 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; + } + + /// Writes a timestamped value to an item and tracks the operation. + /// Server handle for the write. + /// Item handle to write to. + /// Value to write. + /// Source timestamp for the write. + /// MXAccess user id for the write. + 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; + } + + /// Performs a secured write to an item and tracks the operation. + /// Server handle for the write. + /// Item handle to write to. + /// Operator user id. + /// Verifier user id. + /// Value to write. + 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; + } + + /// Performs a secured timestamped write to an item and tracks the operation. + /// Server handle for the write. + /// Item handle to write to. + /// Operator user id. + /// Verifier user id. + /// Value to write. + /// Source timestamp for the write. + 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; + } } /// Factory for creating fake MXAccess COM objects in tests. diff --git a/src/MxGateway.Worker/Conversion/VariantConverter.cs b/src/MxGateway.Worker/Conversion/VariantConverter.cs index c368769..757960b 100644 --- a/src/MxGateway.Worker/Conversion/VariantConverter.cs +++ b/src/MxGateway.Worker/Conversion/VariantConverter.cs @@ -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 } } + /// + /// Converts an 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 . + /// + /// Protobuf value to convert. + /// A COM-marshalable value, or for an MXAccess null. + 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) diff --git a/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs b/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs index 2645e54..33b251f 100644 --- a/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs +++ b/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs @@ -56,4 +56,56 @@ public interface IMxAccessServer void AdviseSupervisory( int serverHandle, int itemHandle); + + /// Writes a value to an item. + /// Server handle identifying the registration. + /// Item handle to write to. + /// COM-marshalable value to write; writes an MXAccess null. + /// MXAccess user id (security classification) for the write. + void Write( + int serverHandle, + int itemHandle, + object? value, + int userId); + + /// Writes a value with an explicit source timestamp to an item. + /// Server handle identifying the registration. + /// Item handle to write to. + /// COM-marshalable value to write; writes an MXAccess null. + /// COM-marshalable source timestamp for the write. + /// MXAccess user id (security classification) for the write. + void Write2( + int serverHandle, + int itemHandle, + object? value, + object? timestamp, + int userId); + + /// Performs a secured/verified write to an item. + /// Server handle identifying the registration. + /// Item handle to write to. + /// MXAccess user id of the operator performing the write. + /// MXAccess user id of the verifier authorizing the write. + /// COM-marshalable value to write; writes an MXAccess null. + void WriteSecured( + int serverHandle, + int itemHandle, + int currentUserId, + int verifierUserId, + object? value); + + /// Performs a secured/verified write with an explicit source timestamp. + /// Server handle identifying the registration. + /// Item handle to write to. + /// MXAccess user id of the operator performing the write. + /// MXAccess user id of the verifier authorizing the write. + /// COM-marshalable value to write; writes an MXAccess null. + /// COM-marshalable source timestamp for the write. + void WriteSecured2( + int serverHandle, + int itemHandle, + int currentUserId, + int verifierUserId, + object? value, + object? timestamp); } diff --git a/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs b/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs index 7ddd2f7..9b4e35d 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs @@ -140,6 +140,74 @@ public sealed class MxAccessComServer : IMxAccessServer AsProxyServer4().AdviseSupervisory(serverHandle, itemHandle); } + /// + 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); + } + + /// + 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); + } + + /// + 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!); + } + + /// + 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 diff --git a/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs b/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs index da49ffa..a9a8a20 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs @@ -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) diff --git a/src/MxGateway.Worker/MxAccess/MxAccessSession.cs b/src/MxGateway.Worker/MxAccess/MxAccessSession.cs index 965bdfb..81f0d78 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessSession.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessSession.cs @@ -227,6 +227,78 @@ public sealed class MxAccessSession : IDisposable MxAccessAdviceKind.Supervisory); } + /// Writes a value to an item. + /// Handle returned by the worker. + /// Handle returned by the worker. + /// COM-marshalable value to write. + /// MXAccess user id (security classification) for the write. + public void Write( + int serverHandle, + int itemHandle, + object? value, + int userId) + { + ThrowIfDisposed(); + + mxAccessServer.Write(serverHandle, itemHandle, value, userId); + } + + /// Writes a value with an explicit source timestamp to an item. + /// Handle returned by the worker. + /// Handle returned by the worker. + /// COM-marshalable value to write. + /// COM-marshalable source timestamp for the write. + /// MXAccess user id (security classification) for the write. + public void Write2( + int serverHandle, + int itemHandle, + object? value, + object? timestamp, + int userId) + { + ThrowIfDisposed(); + + mxAccessServer.Write2(serverHandle, itemHandle, value, timestamp, userId); + } + + /// Performs a secured/verified write to an item. + /// Handle returned by the worker. + /// Handle returned by the worker. + /// MXAccess user id of the operator performing the write. + /// MXAccess user id of the verifier authorizing the write. + /// COM-marshalable value to write. + public void WriteSecured( + int serverHandle, + int itemHandle, + int currentUserId, + int verifierUserId, + object? value) + { + ThrowIfDisposed(); + + mxAccessServer.WriteSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value); + } + + /// Performs a secured/verified write with an explicit source timestamp. + /// Handle returned by the worker. + /// Handle returned by the worker. + /// MXAccess user id of the operator performing the write. + /// MXAccess user id of the verifier authorizing the write. + /// COM-marshalable value to write. + /// COM-marshalable source timestamp for the write. + public void WriteSecured2( + int serverHandle, + int itemHandle, + int currentUserId, + int verifierUserId, + object? value, + object? timestamp) + { + ThrowIfDisposed(); + + mxAccessServer.WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value, timestamp); + } + /// Adds multiple items in bulk, returning success/failure results. /// Handle returned by the worker. /// Enumerable of item definitions to add.