mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-17 05:11:13 +08:00
3690341c80
# Description
**Software update methods** (`fix(di): wire PrepareForUpdate
Abort/Resume and Installation Resume`)
- New internal `SoftwareUpdateTransitions` (one per `SoftwareUpdate`)
checks the current state and moves it under one lock, so concurrent
calls cannot both pass a check.
- `Prepare` (§8.4.8.3) is accepted only from `Idle`. The application's
`OnPrepare` callback gets a token that `Abort` cancels; the token source
is disposed only after both the Prepare call and a racing Abort are done
with it, so a handler still running after Abort can use its token.
- `Abort` (§8.4.8.4) moves `Preparing` to `Idle` (`PreparingToIdle`),
cancels the preparation in flight and reports `Failed` ("Aborted")
through `OnPrepareStateChanged`; the aborted `Prepare` call returns
`Bad_InvalidState`. Outside `Preparing` it returns `Bad_InvalidState`.
- `Resume` (§8.4.8.5) no longer runs the Prepare callback. It moves
`PreparedForUpdate` through `Resuming` to `Idle` and is rejected with
`Bad_InvalidState` outside `PreparedForUpdate` or while the installation
is `Installing`.
- `Installation.Resume` (§8.4.9) moves `Error` to `Idle`
(`ErrorToIdle`),
`Bad_InvalidState` otherwise.
- The dispatcher knows the `Resuming` state and the `ResumingToIdle`
transition, so `CurrentState` / `LastTransition` show their names.
**Advertising** (`fix(di): advertise only the DI facets whose mandatory
units are met`)
- `DI Offline`, `DI SU UpdateStatus`, `DI SU FileSystem Loading`,
`DI SU Installation for File System` and `DI SU Installation for Cached
Loading` are no longer claimed (see the issue for why each is unmet).
- The facets are derived from the advertised units with the mandatory
units of the DI 1.05 profile database, so a facet cannot be listed
without them. Today this leaves Locking and Software Update Base;
DeviceIntegrationHost, FileSystem, Direct and Cached Loading come back
by themselves once the missing pieces are built.
- `docs/DeviceIntegration.md` documents both tables and the missing
pieces.
Tests: `SoftwareUpdateMethodTransitionTests` (9 tests) and
`DiNodeManagerConformanceTests` (5 tests) are new;
`DiConformanceFacetTests`
and `PumpAddressSpaceComplianceTests` expected the old claims and are
updated. All 9 transition tests and 8 of the 10 conformance tests fail
against master; the other two are the unchanged locking test and a guard
that every advertised facet carries its mandatory units (master
over-claims
the units too, so the guard alone holds there).
Behaviour change for users: a plain DI server no longer lists the
DeviceIntegrationHost facet, and Direct/Cached/Package software updates
list only Software Update Base.
Fork CI (full matrix incl. macOS) and CodeQL: green — [CI run
36779552603](https://github.com/biancode/UA-.NETStandard/actions/runs/36779552603),
[CodeQL run
36779556125](https://github.com/biancode/UA-.NETStandard/actions/runs/36779556125).
## Related Issues
- Fixes #4616
## Checklist
- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
---------
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
445 lines
20 KiB
C#
445 lines
20 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Di.Server.Builders;
|
|
using Opc.Ua.Di.Server.SoftwareUpdate;
|
|
|
|
namespace Opc.Ua.Di.Tests
|
|
{
|
|
/// <summary>
|
|
/// Verifies the method preconditions and transitions of the
|
|
/// PrepareForUpdate (OPC 10000-100 §8.4.8) and Installation (§8.4.9)
|
|
/// state machines of a device's software update.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("DI")]
|
|
[Category("DeviceBuilder")]
|
|
[Category("SoftwareUpdate")]
|
|
public sealed class SoftwareUpdateMethodTransitionTests
|
|
{
|
|
private static readonly TimeSpan s_timeout = TimeSpan.FromSeconds(30);
|
|
private DiServerFixture m_fixture = null!;
|
|
private ISoftwarePackageStore m_store = null!;
|
|
|
|
[OneTimeSetUp]
|
|
public async Task SetUpAsync()
|
|
{
|
|
m_fixture = new DiServerFixture();
|
|
await m_fixture.StartAsync().ConfigureAwait(false);
|
|
m_store = new MemoryPackageStore();
|
|
}
|
|
|
|
[OneTimeTearDown]
|
|
public async Task TearDownAsync()
|
|
{
|
|
await m_fixture.DisposeAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
[Test]
|
|
public async Task PrepareIsRejectedUnlessIdleAsync()
|
|
{
|
|
int prepareCalls = 0;
|
|
SoftwareUpdateNodes nodes = await CreateAsync("PrepareTwice", su => su.OnPrepare((_, _) =>
|
|
{
|
|
Interlocked.Increment(ref prepareCalls);
|
|
return default;
|
|
})).ConfigureAwait(false);
|
|
|
|
ServiceResult first = await CallAsync(nodes.Prepare.Prepare, nodes).ConfigureAwait(false);
|
|
ServiceResult second = await CallAsync(nodes.Prepare.Prepare, nodes).ConfigureAwait(false);
|
|
|
|
Assert.That(first, Is.EqualTo(ServiceResult.Good));
|
|
Assert.That(second.StatusCode, Is.EqualTo(StatusCodes.BadInvalidState));
|
|
Assert.That(Volatile.Read(ref prepareCalls), Is.EqualTo(1));
|
|
Assert.That(nodes.Prepare.CurrentState!.Value.Text, Is.EqualTo("PreparedForUpdate"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task AbortCancelsTheRunningPreparationAsync()
|
|
{
|
|
var started = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
int cancelled = 0;
|
|
var phases = new ConcurrentQueue<SoftwareUpdatePhase>();
|
|
SoftwareUpdateNodes nodes = await CreateAsync("PrepareAbort", su => su
|
|
.OnPrepare(async (_, ct) =>
|
|
{
|
|
started.TrySetResult(true);
|
|
try
|
|
{
|
|
await Task.Delay(Timeout.Infinite, ct).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
Interlocked.Exchange(ref cancelled, 1);
|
|
throw;
|
|
}
|
|
})
|
|
.OnPrepareStateChanged((_, change) =>
|
|
{
|
|
phases.Enqueue(change.Phase);
|
|
return default;
|
|
})).ConfigureAwait(false);
|
|
|
|
Task<ServiceResult> prepare = CallAsync(nodes.Prepare.Prepare, nodes);
|
|
Assert.That(await CompletesAsync(started.Task).ConfigureAwait(false), Is.True);
|
|
Assert.That(nodes.Prepare.CurrentState!.Value.Text, Is.EqualTo("Preparing"));
|
|
|
|
ServiceResult abort = await CallAsync(nodes.Prepare.Abort, nodes).ConfigureAwait(false);
|
|
|
|
Assert.That(abort, Is.EqualTo(ServiceResult.Good));
|
|
Assert.That(await CompletesAsync(prepare).ConfigureAwait(false), Is.True);
|
|
Assert.That(Volatile.Read(ref cancelled), Is.EqualTo(1));
|
|
Assert.That((await prepare.ConfigureAwait(false)).StatusCode, Is.EqualTo(StatusCodes.BadInvalidState));
|
|
Assert.That(nodes.Prepare.CurrentState!.Value.Text, Is.EqualTo("Idle"));
|
|
Assert.That(nodes.Prepare.LastTransition!.Value.Text, Is.EqualTo("PreparingToIdle"));
|
|
Assert.That(phases, Is.EqualTo(new[] { SoftwareUpdatePhase.Started, SoftwareUpdatePhase.Failed }));
|
|
}
|
|
|
|
[Test]
|
|
public async Task AHandlerStillRunningAfterAbortCanUseItsTokenAsync()
|
|
{
|
|
var started = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var release = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
bool cancellationSeen = false;
|
|
Exception? tokenError = null;
|
|
SoftwareUpdateNodes nodes = await CreateAsync("PrepareAbortLate", su => su
|
|
.OnPrepare(async (_, ct) =>
|
|
{
|
|
started.TrySetResult(true);
|
|
await release.Task.ConfigureAwait(false);
|
|
try
|
|
{
|
|
cancellationSeen = ct.WaitHandle.WaitOne(0);
|
|
}
|
|
catch (ObjectDisposedException ex)
|
|
{
|
|
tokenError = ex;
|
|
}
|
|
})).ConfigureAwait(false);
|
|
|
|
Task<ServiceResult> prepare = CallAsync(nodes.Prepare.Prepare, nodes);
|
|
Assert.That(await CompletesAsync(started.Task).ConfigureAwait(false), Is.True);
|
|
ServiceResult abort;
|
|
try
|
|
{
|
|
abort = await CallAsync(nodes.Prepare.Abort, nodes).ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
release.TrySetResult(true);
|
|
}
|
|
Assert.That(await CompletesAsync(prepare).ConfigureAwait(false), Is.True);
|
|
ServiceResult prepared = await prepare.ConfigureAwait(false);
|
|
|
|
Assert.That(abort, Is.EqualTo(ServiceResult.Good));
|
|
Assert.That(tokenError, Is.Null, "The token source must outlive the running handler.");
|
|
Assert.That(cancellationSeen, Is.True);
|
|
Assert.That(prepared.StatusCode, Is.EqualTo(StatusCodes.BadInvalidState));
|
|
Assert.That(nodes.Prepare.CurrentState!.Value.Text, Is.EqualTo("Idle"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task AbortIsRejectedWhenNothingIsPreparingAsync()
|
|
{
|
|
SoftwareUpdateNodes nodes = await CreateAsync("AbortIdle").ConfigureAwait(false);
|
|
|
|
ServiceResult abort = await CallAsync(nodes.Prepare.Abort, nodes).ConfigureAwait(false);
|
|
|
|
Assert.That(abort.StatusCode, Is.EqualTo(StatusCodes.BadInvalidState));
|
|
Assert.That(nodes.Prepare.CurrentState!.Value.Text, Is.EqualTo("Idle"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResumeReturnsAPreparedDeviceToIdleAsync()
|
|
{
|
|
int prepareCalls = 0;
|
|
var phases = new ConcurrentQueue<SoftwareUpdatePhase>();
|
|
SoftwareUpdateNodes nodes = await CreateAsync("PrepareResume", su => su
|
|
.OnPrepare((_, _) =>
|
|
{
|
|
Interlocked.Increment(ref prepareCalls);
|
|
return default;
|
|
})
|
|
.OnPrepareStateChanged((_, change) =>
|
|
{
|
|
phases.Enqueue(change.Phase);
|
|
return default;
|
|
})).ConfigureAwait(false);
|
|
|
|
Assert.That(
|
|
await CallAsync(nodes.Prepare.Prepare, nodes).ConfigureAwait(false),
|
|
Is.EqualTo(ServiceResult.Good));
|
|
ServiceResult resume = await CallAsync(nodes.Prepare.Resume, nodes).ConfigureAwait(false);
|
|
|
|
Assert.That(resume, Is.EqualTo(ServiceResult.Good));
|
|
Assert.That(Volatile.Read(ref prepareCalls), Is.EqualTo(1), "Resume must not prepare again.");
|
|
Assert.That(nodes.Prepare.CurrentState!.Value.Text, Is.EqualTo("Idle"));
|
|
Assert.That(nodes.Prepare.LastTransition!.Value.Text, Is.EqualTo("ResumingToIdle"));
|
|
Assert.That(phases, Is.EqualTo(new[]
|
|
{
|
|
SoftwareUpdatePhase.Started,
|
|
SoftwareUpdatePhase.Completed,
|
|
SoftwareUpdatePhase.Started,
|
|
SoftwareUpdatePhase.Completed
|
|
}));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResumeIsRejectedUnlessPreparedForUpdateAsync()
|
|
{
|
|
int prepareCalls = 0;
|
|
SoftwareUpdateNodes nodes = await CreateAsync("ResumeIdle", su => su.OnPrepare((_, _) =>
|
|
{
|
|
Interlocked.Increment(ref prepareCalls);
|
|
return default;
|
|
})).ConfigureAwait(false);
|
|
|
|
ServiceResult resume = await CallAsync(nodes.Prepare.Resume, nodes).ConfigureAwait(false);
|
|
|
|
Assert.That(resume.StatusCode, Is.EqualTo(StatusCodes.BadInvalidState));
|
|
Assert.That(Volatile.Read(ref prepareCalls), Is.Zero);
|
|
Assert.That(nodes.Prepare.CurrentState!.Value.Text, Is.EqualTo("Idle"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task ResumeIsRejectedWhileInstallingAsync()
|
|
{
|
|
var installing = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var release = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
SoftwareUpdateNodes nodes = await CreateAsync("ResumeInstalling", su => su
|
|
.OnInstall(async (_, _, _) =>
|
|
{
|
|
installing.TrySetResult(true);
|
|
await release.Task.ConfigureAwait(false);
|
|
})).ConfigureAwait(false);
|
|
|
|
Assert.That(
|
|
await CallAsync(nodes.Prepare.Prepare, nodes).ConfigureAwait(false),
|
|
Is.EqualTo(ServiceResult.Good));
|
|
Task<ServiceResult> install = CallAsync(
|
|
nodes.Installation.InstallSoftwarePackage, nodes, InstallInputs());
|
|
Assert.That(await CompletesAsync(installing.Task).ConfigureAwait(false), Is.True);
|
|
|
|
ServiceResult resume;
|
|
try
|
|
{
|
|
resume = await CallAsync(nodes.Prepare.Resume, nodes).ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
release.TrySetResult(true);
|
|
}
|
|
|
|
Assert.That(resume.StatusCode, Is.EqualTo(StatusCodes.BadInvalidState));
|
|
Assert.That(nodes.Prepare.CurrentState!.Value.Text, Is.EqualTo("PreparedForUpdate"));
|
|
Assert.That(await CompletesAsync(install).ConfigureAwait(false), Is.True);
|
|
Assert.That(await install.ConfigureAwait(false), Is.EqualTo(ServiceResult.Good));
|
|
}
|
|
|
|
[Test]
|
|
public async Task AnInstallationCannotStartWhileResumeIsMovingThePreparationAsync()
|
|
{
|
|
var installing = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
SoftwareUpdateNodes nodes = await CreateAsync("ResumeRacesInstall", su => su
|
|
.OnInstall((_, _, _) =>
|
|
{
|
|
installing.TrySetResult(true);
|
|
return default;
|
|
})).ConfigureAwait(false);
|
|
Assert.That(
|
|
await CallAsync(nodes.Prepare.Prepare, nodes).ConfigureAwait(false),
|
|
Is.EqualTo(ServiceResult.Good));
|
|
|
|
// Resume has passed its "no installation running" check when it
|
|
// writes the move to Resuming. Start an installation from that
|
|
// very moment and see whether it gets to run before Resume is done.
|
|
Task<ServiceResult>? install = null;
|
|
bool installRanDuringResume = false;
|
|
int started = 0;
|
|
nodes.Prepare.CurrentState!.OnStateChanged = (_, _, _) =>
|
|
{
|
|
if (Interlocked.Exchange(ref started, 1) == 0)
|
|
{
|
|
install = Task.Run(() => CallAsync(
|
|
nodes.Installation.InstallSoftwarePackage, nodes, InstallInputs()));
|
|
installRanDuringResume = installing.Task.Wait(TimeSpan.FromMilliseconds(500));
|
|
}
|
|
};
|
|
|
|
ServiceResult resume = await CallAsync(nodes.Prepare.Resume, nodes).ConfigureAwait(false);
|
|
nodes.Prepare.CurrentState.OnStateChanged = null;
|
|
|
|
Assert.That(resume, Is.EqualTo(ServiceResult.Good));
|
|
Assert.That(install, Is.Not.Null);
|
|
Assert.That(
|
|
installRanDuringResume, Is.False,
|
|
"An installation must not start between the check of Resume and its move.");
|
|
Assert.That(await CompletesAsync(install!).ConfigureAwait(false), Is.True);
|
|
Assert.That(await install!.ConfigureAwait(false), Is.EqualTo(ServiceResult.Good));
|
|
Assert.That(nodes.Installation.CurrentState!.Value.Text, Is.EqualTo("Idle"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task AnInstallationIsRejectedUnlessIdleAsync()
|
|
{
|
|
var release = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var installing = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
int installCalls = 0;
|
|
SoftwareUpdateNodes nodes = await CreateAsync("InstallTwice", su => su
|
|
.OnInstall(async (_, _, _) =>
|
|
{
|
|
Interlocked.Increment(ref installCalls);
|
|
installing.TrySetResult(true);
|
|
await release.Task.ConfigureAwait(false);
|
|
})).ConfigureAwait(false);
|
|
|
|
Task<ServiceResult> first = CallAsync(
|
|
nodes.Installation.InstallSoftwarePackage, nodes, InstallInputs());
|
|
Assert.That(await CompletesAsync(installing.Task).ConfigureAwait(false), Is.True);
|
|
Task<ServiceResult> secondCall = CallAsync(
|
|
nodes.Installation.InstallSoftwarePackage, nodes, InstallInputs());
|
|
bool secondRejected;
|
|
try
|
|
{
|
|
secondRejected = await Task.WhenAny(secondCall, Task.Delay(TimeSpan.FromSeconds(5)))
|
|
.ConfigureAwait(false) == secondCall;
|
|
}
|
|
finally
|
|
{
|
|
release.TrySetResult(true);
|
|
}
|
|
|
|
Assert.That(secondRejected, Is.True, "The second installation must be rejected, not run.");
|
|
ServiceResult second = await secondCall.ConfigureAwait(false);
|
|
Assert.That(second.StatusCode, Is.EqualTo(StatusCodes.BadInvalidState));
|
|
Assert.That(await first.ConfigureAwait(false), Is.EqualTo(ServiceResult.Good));
|
|
Assert.That(Volatile.Read(ref installCalls), Is.EqualTo(1));
|
|
Assert.That(nodes.Installation.CurrentState!.Value.Text, Is.EqualTo("Idle"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task InstallationResumeLeavesErrorForIdleAsync()
|
|
{
|
|
SoftwareUpdateNodes nodes = await CreateAsync("InstallResume", su => su
|
|
.OnInstall((_, _, _) => throw new InvalidOperationException("flash failed")))
|
|
.ConfigureAwait(false);
|
|
|
|
ServiceResult install = await CallAsync(
|
|
nodes.Installation.InstallSoftwarePackage, nodes, InstallInputs()).ConfigureAwait(false);
|
|
Assert.That(ServiceResult.IsBad(install), Is.True);
|
|
Assert.That(nodes.Installation.CurrentState!.Value.Text, Is.EqualTo("Error"));
|
|
|
|
ServiceResult resume = await CallAsync(nodes.Installation.Resume, nodes).ConfigureAwait(false);
|
|
|
|
Assert.That(resume, Is.EqualTo(ServiceResult.Good));
|
|
Assert.That(nodes.Installation.CurrentState!.Value.Text, Is.EqualTo("Idle"));
|
|
Assert.That(nodes.Installation.LastTransition!.Value.Text, Is.EqualTo("ErrorToIdle"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task InstallationResumeIsRejectedOutsideErrorAsync()
|
|
{
|
|
SoftwareUpdateNodes nodes = await CreateAsync("InstallResumeIdle").ConfigureAwait(false);
|
|
|
|
ServiceResult resume = await CallAsync(nodes.Installation.Resume, nodes).ConfigureAwait(false);
|
|
|
|
Assert.That(resume.StatusCode, Is.EqualTo(StatusCodes.BadInvalidState));
|
|
Assert.That(nodes.Installation.CurrentState!.Value.Text, Is.EqualTo("Idle"));
|
|
}
|
|
|
|
private async Task<SoftwareUpdateNodes> CreateAsync(
|
|
string deviceName,
|
|
Action<ISoftwareUpdateBuilder>? configure = null)
|
|
{
|
|
IDeviceBuilder<DeviceState> builder = await m_fixture.Manager
|
|
.CreateDeviceAsync(new QualifiedName(deviceName, m_fixture.Manager.DiNamespaceIndex))
|
|
.ConfigureAwait(false);
|
|
builder.WithSoftwareUpdate(m_store, configure);
|
|
|
|
ushort diNs = m_fixture.Manager.DiNamespaceIndex;
|
|
ISystemContext context = m_fixture.Manager.SystemContext;
|
|
NodeState softwareUpdate = builder.Device.FindChild(
|
|
context, new QualifiedName("SoftwareUpdate", diNs))!;
|
|
return new SoftwareUpdateNodes(
|
|
softwareUpdate.NodeId,
|
|
(PrepareForUpdateStateMachineState)softwareUpdate.FindChild(
|
|
context, new QualifiedName("PrepareForUpdate", diNs))!,
|
|
(InstallationStateMachineState)softwareUpdate.FindChild(
|
|
context, new QualifiedName("Installation", diNs))!);
|
|
}
|
|
|
|
private static ArrayOf<Variant> InstallInputs()
|
|
{
|
|
return new ArrayOf<Variant>(new[]
|
|
{
|
|
new Variant("urn:acme:firmware"),
|
|
new Variant("9.9.9"),
|
|
new Variant(Array.Empty<string>()),
|
|
new Variant(Array.Empty<byte>())
|
|
});
|
|
}
|
|
|
|
private static async Task<bool> CompletesAsync(Task task)
|
|
{
|
|
return await Task.WhenAny(task, Task.Delay(s_timeout)).ConfigureAwait(false) == task;
|
|
}
|
|
|
|
private static async Task<ServiceResult> CallAsync(
|
|
MethodState? method,
|
|
SoftwareUpdateNodes nodes,
|
|
ArrayOf<Variant> inputs = default)
|
|
{
|
|
Assert.That(method, Is.Not.Null, "the method is materialised");
|
|
var outputs = new List<Variant>();
|
|
if (method!.OnCallMethod2Async is not null)
|
|
{
|
|
return await method.OnCallMethod2Async(
|
|
null!, method, nodes.SoftwareUpdateId, inputs, outputs,
|
|
CancellationToken.None).ConfigureAwait(false);
|
|
}
|
|
if (method.OnCallMethod2 is not null)
|
|
{
|
|
return method.OnCallMethod2(null!, method, nodes.SoftwareUpdateId, inputs, outputs);
|
|
}
|
|
return new ServiceResult(StatusCodes.BadNotImplemented);
|
|
}
|
|
|
|
private sealed record SoftwareUpdateNodes(
|
|
NodeId SoftwareUpdateId,
|
|
PrepareForUpdateStateMachineState Prepare,
|
|
InstallationStateMachineState Installation);
|
|
}
|
|
}
|