Compare commits
3 Commits
docs-refre
...
rename-cli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9bc301c33 | ||
|
|
12d748c4f3 | ||
| e9b1d107ab |
@@ -87,13 +87,14 @@ The server supports non-transparent warm/hot redundancy via the `Redundancy` sec
|
|||||||
|
|
||||||
## LDAP Authentication
|
## LDAP Authentication
|
||||||
|
|
||||||
The server uses LDAP-based user authentication via the `Authentication.Ldap` section in `appsettings.json`. When enabled, credentials are validated by LDAP bind against a GLAuth server (installed at `C:\publish\glauth\`), and LDAP group membership maps to OPC UA permissions: `ReadOnly` (browse/read), `WriteOperate` (write FreeAccess/Operate attributes), `WriteTune` (write Tune attributes), `WriteConfigure` (write Configure attributes), `AlarmAck` (alarm acknowledgment). `LdapAuthenticationProvider` implements both `IUserAuthenticationProvider` and `IRoleProvider`. See `docs/Security.md` for the full guide and `C:\publish\glauth\auth.md` for LDAP user/group reference.
|
The server uses LDAP-based user authentication via the `Authentication.Ldap` section in `appsettings.json`. When enabled, credentials are validated by LDAP bind against a GLAuth server (installed at `C:\publish\glauth\`), and LDAP group membership maps to OPC UA permissions: `ReadOnly` (browse/read), `WriteOperate` (write FreeAccess/Operate attributes), `WriteTune` (write Tune attributes), `WriteConfigure` (write Configure attributes), `AlarmAck` (alarm acknowledgment). `LdapUserAuthenticator` (`src/ZB.MOM.WW.OtOpcUa.Server/Security/LdapUserAuthenticator.cs`) implements `IUserAuthenticator`. See `docs/Security.md` for the full guide and `C:\publish\glauth\auth.md` for LDAP user/group reference.
|
||||||
|
|
||||||
## Library Preferences
|
## Library Preferences
|
||||||
|
|
||||||
- **Logging**: Serilog with rolling daily file sink
|
- **Logging**: Serilog with rolling daily file sink
|
||||||
- **Unit tests**: xUnit + Shouldly for assertions
|
- **Unit tests**: xUnit + Shouldly for assertions
|
||||||
- **Service hosting**: TopShelf (Windows service install/uninstall/run as console)
|
- **Service hosting (Server, Admin)**: .NET generic host with `AddWindowsService` (decision #30 — replaced TopShelf in v2; see `src/ZB.MOM.WW.OtOpcUa.Server/OpcUaServerService.cs`)
|
||||||
|
- **Service hosting (Galaxy.Host)**: plain console app wrapped by NSSM (`.NET Framework 4.8 x86` — required by MXAccess COM bitness)
|
||||||
- **OPC UA**: OPC Foundation UA .NET Standard stack (https://github.com/opcfoundation/ua-.netstandard) — NuGet: `OPCFoundation.NetStandard.Opc.Ua.Server`
|
- **OPC UA**: OPC Foundation UA .NET Standard stack (https://github.com/opcfoundation/ua-.netstandard) — NuGet: `OPCFoundation.NetStandard.Opc.Ua.Server`
|
||||||
|
|
||||||
## OPC UA .NET Standard Documentation
|
## OPC UA .NET Standard Documentation
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ dotnet build
|
|||||||
dotnet run -- <command> [options]
|
dotnet run -- <command> [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
The executable name is still `lmxopcua-cli` — a residual from the pre-v2 rename (`Program.cs:SetExecutableName`). Scripts + operator muscle memory depend on the name; flipping it to `otopcua-cli` is a follow-up that also needs to move the client-side PKI store folder (<code>{LocalAppData}/LmxOpcUaClient/pki/</code> — used by the shared client for its application certificate) so trust relationships survive the rename.
|
The executable name is `otopcua-cli`. Dev boxes carrying a pre-task-#208 install may still have the legacy `{LocalAppData}/LmxOpcUaClient/` folder on disk; on first launch of any post-#208 CLI or UI build, `ClientStoragePaths` (`src/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs`) migrates it to `{LocalAppData}/OtOpcUaClient/` automatically so trusted certificates + saved settings survive the rename.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ All commands accept these options:
|
|||||||
When `-U` and `-P` are provided, the shared service passes a `UserIdentity(username, password)` to the OPC UA session. Without credentials, anonymous identity is used.
|
When `-U` and `-P` are provided, the shared service passes a `UserIdentity(username, password)` to the OPC UA session. Without credentials, anonymous identity is used.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lmxopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42 -U operator -P op123
|
otopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42 -U operator -P op123
|
||||||
```
|
```
|
||||||
|
|
||||||
### Failover
|
### Failover
|
||||||
@@ -54,20 +54,20 @@ lmxopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42 -U opera
|
|||||||
When `-F` is provided, the shared service tries the primary URL first, then each failover URL in order. For long-running commands (`subscribe`, `alarms`), the service monitors the session via keep-alive and automatically reconnects to the next available server on failure.
|
When `-F` is provided, the shared service tries the primary URL first, then each failover URL in order. For long-running commands (`subscribe`, `alarms`), the service monitors the session via keep-alive and automatically reconnects to the next available server on failure.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lmxopcua-cli connect -u opc.tcp://localhost:4840/OtOpcUa -F opc.tcp://localhost:4841/OtOpcUa
|
otopcua-cli connect -u opc.tcp://localhost:4840/OtOpcUa -F opc.tcp://localhost:4841/OtOpcUa
|
||||||
```
|
```
|
||||||
|
|
||||||
### Transport Security
|
### Transport Security
|
||||||
|
|
||||||
When `sign` or `encrypt` is specified, the shared service:
|
When `sign` or `encrypt` is specified, the shared service:
|
||||||
|
|
||||||
1. Ensures a client application certificate exists under `{LocalAppData}/LmxOpcUaClient/pki/` (auto-created if missing)
|
1. Ensures a client application certificate exists under `{LocalAppData}/OtOpcUaClient/pki/` (auto-created if missing; pre-rename `LmxOpcUaClient/` is migrated in place on first launch)
|
||||||
2. Discovers server endpoints and selects one matching the requested security mode
|
2. Discovers server endpoints and selects one matching the requested security mode
|
||||||
3. Prefers `Basic256Sha256` when multiple matching endpoints exist
|
3. Prefers `Basic256Sha256` when multiple matching endpoints exist
|
||||||
4. Fails with a clear error if no matching endpoint is found
|
4. Fails with a clear error if no matching endpoint is found
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lmxopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -S encrypt -U admin -P secret -r -d 2
|
otopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -S encrypt -U admin -P secret -r -d 2
|
||||||
```
|
```
|
||||||
|
|
||||||
### Verbose Logging
|
### Verbose Logging
|
||||||
@@ -81,7 +81,7 @@ The `--verbose` flag switches Serilog output from `Warning` to `Debug` level, sh
|
|||||||
Tests connectivity to an OPC UA server. Creates a session, prints connection metadata, and disconnects.
|
Tests connectivity to an OPC UA server. Creates a session, prints connection metadata, and disconnects.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lmxopcua-cli connect -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123
|
otopcua-cli connect -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123
|
||||||
```
|
```
|
||||||
|
|
||||||
Output:
|
Output:
|
||||||
@@ -99,7 +99,7 @@ Connection successful.
|
|||||||
Reads the current value of a single node and prints the value, status code, and timestamps.
|
Reads the current value of a single node and prints the value, status code, and timestamps.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lmxopcua-cli read -u opc.tcp://localhost:4840/OtOpcUa -n "ns=3;s=DEV.ScanState" -U admin -P admin123
|
otopcua-cli read -u opc.tcp://localhost:4840/OtOpcUa -n "ns=3;s=DEV.ScanState" -U admin -P admin123
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
@@ -121,7 +121,7 @@ Server Time: 2026-03-30T19:58:38.0971257Z
|
|||||||
Writes a value to a node. The shared service reads the current value first to determine the target data type, then converts the supplied string value using `ValueConverter.ConvertValue()`.
|
Writes a value to a node. The shared service reads the current value first to determine the target data type, then converts the supplied string value using `ValueConverter.ConvertValue()`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lmxopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42
|
otopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
@@ -135,10 +135,10 @@ Browses the OPC UA address space starting from the Objects folder or a specified
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Browse top-level Objects folder
|
# Browse top-level Objects folder
|
||||||
lmxopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123
|
otopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123
|
||||||
|
|
||||||
# Browse a specific node recursively to depth 3
|
# Browse a specific node recursively to depth 3
|
||||||
lmxopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123 -r -d 3 -n "ns=3;s=ZB"
|
otopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123 -r -d 3 -n "ns=3;s=ZB"
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
@@ -152,7 +152,7 @@ lmxopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123 -r
|
|||||||
Monitors a node for value changes using OPC UA subscriptions. Prints each data change notification with timestamp, value, and status code. Runs until Ctrl+C, then unsubscribes and disconnects cleanly.
|
Monitors a node for value changes using OPC UA subscriptions. Prints each data change notification with timestamp, value, and status code. Runs until Ctrl+C, then unsubscribes and disconnects cleanly.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lmxopcua-cli subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -i 500
|
otopcua-cli subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -i 500
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
@@ -166,12 +166,12 @@ Reads historical data from a node. Supports raw history reads and aggregate (pro
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Raw history
|
# Raw history
|
||||||
lmxopcua-cli historyread -u opc.tcp://localhost:4840/OtOpcUa \
|
otopcua-cli historyread -u opc.tcp://localhost:4840/OtOpcUa \
|
||||||
-n "ns=1;s=TestMachine_001.TestHistoryValue" \
|
-n "ns=1;s=TestMachine_001.TestHistoryValue" \
|
||||||
--start "2026-03-25" --end "2026-03-30"
|
--start "2026-03-25" --end "2026-03-30"
|
||||||
|
|
||||||
# Aggregate: 1-hour average
|
# Aggregate: 1-hour average
|
||||||
lmxopcua-cli historyread -u opc.tcp://localhost:4840/OtOpcUa \
|
otopcua-cli historyread -u opc.tcp://localhost:4840/OtOpcUa \
|
||||||
-n "ns=1;s=TestMachine_001.TestHistoryValue" \
|
-n "ns=1;s=TestMachine_001.TestHistoryValue" \
|
||||||
--start "2026-03-25" --end "2026-03-30" \
|
--start "2026-03-25" --end "2026-03-30" \
|
||||||
--aggregate Average --interval 3600000
|
--aggregate Average --interval 3600000
|
||||||
@@ -203,10 +203,10 @@ Subscribes to alarm events on a node. Prints structured alarm output including s
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Subscribe to alarm events on the Server node
|
# Subscribe to alarm events on the Server node
|
||||||
lmxopcua-cli alarms -u opc.tcp://localhost:4840/OtOpcUa
|
otopcua-cli alarms -u opc.tcp://localhost:4840/OtOpcUa
|
||||||
|
|
||||||
# Subscribe to a specific source node with condition refresh
|
# Subscribe to a specific source node with condition refresh
|
||||||
lmxopcua-cli alarms -u opc.tcp://localhost:4840/OtOpcUa \
|
otopcua-cli alarms -u opc.tcp://localhost:4840/OtOpcUa \
|
||||||
-n "ns=1;s=TestMachine_001" --refresh
|
-n "ns=1;s=TestMachine_001" --refresh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ lmxopcua-cli alarms -u opc.tcp://localhost:4840/OtOpcUa \
|
|||||||
Reads the OPC UA redundancy state from a server: redundancy mode, service level, server URIs, and application URI.
|
Reads the OPC UA redundancy state from a server: redundancy mode, service level, server URIs, and application URI.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lmxopcua-cli redundancy -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123
|
otopcua-cli redundancy -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123
|
||||||
```
|
```
|
||||||
|
|
||||||
Example output:
|
Example output:
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ The top bar provides the endpoint URL, Connect, and Disconnect buttons. The **Co
|
|||||||
|
|
||||||
### Settings Persistence
|
### Settings Persistence
|
||||||
|
|
||||||
Connection settings are saved to `{LocalAppData}/LmxOpcUaClient/settings.json` after each successful connection and on window close. The folder name is a residual from the pre-v2 rename (the `Client.Shared` session factory still calls itself `LmxOpcUaClient` at `OpcUaClientService.cs:428`); renaming to `OtOpcUaClient` is a follow-up that needs a migration shim so existing users don't lose their settings on upgrade. The settings are reloaded on next launch, including:
|
Connection settings are saved to `{LocalAppData}/OtOpcUaClient/settings.json` after each successful connection and on window close. Dev boxes upgrading from a pre-task-#208 build still have the legacy `LmxOpcUaClient/` folder on disk; `ClientStoragePaths` in `Client.Shared` moves it to the canonical path on first launch so existing trusted certs + saved settings persist without operator action. The settings are reloaded on next launch, including:
|
||||||
|
|
||||||
- All connection parameters
|
- All connection parameters
|
||||||
- Active subscription node IDs (restored after reconnection)
|
- Active subscription node IDs (restored after reconnection)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ return await new CliApplicationBuilder()
|
|||||||
if (type.IsSubclassOf(typeof(CommandBase))) return Activator.CreateInstance(type, CommandBase.DefaultFactory)!;
|
if (type.IsSubclassOf(typeof(CommandBase))) return Activator.CreateInstance(type, CommandBase.DefaultFactory)!;
|
||||||
return Activator.CreateInstance(type)!;
|
return Activator.CreateInstance(type)!;
|
||||||
})
|
})
|
||||||
.SetExecutableName("lmxopcua-cli")
|
.SetExecutableName("otopcua-cli")
|
||||||
.SetDescription("LmxOpcUa CLI - command-line client for the LmxOpcUa OPC UA server")
|
.SetDescription("OtOpcUa CLI - command-line client for the OtOpcUa OPC UA server")
|
||||||
.Build()
|
.Build()
|
||||||
.RunAsync(args);
|
.RunAsync(args);
|
||||||
@@ -18,8 +18,8 @@ internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfi
|
|||||||
|
|
||||||
var config = new ApplicationConfiguration
|
var config = new ApplicationConfiguration
|
||||||
{
|
{
|
||||||
ApplicationName = "LmxOpcUaClient",
|
ApplicationName = "OtOpcUaClient",
|
||||||
ApplicationUri = "urn:localhost:LmxOpcUaClient",
|
ApplicationUri = "urn:localhost:OtOpcUaClient",
|
||||||
ApplicationType = ApplicationType.Client,
|
ApplicationType = ApplicationType.Client,
|
||||||
SecurityConfiguration = new SecurityConfiguration
|
SecurityConfiguration = new SecurityConfiguration
|
||||||
{
|
{
|
||||||
@@ -60,7 +60,7 @@ internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfi
|
|||||||
{
|
{
|
||||||
var app = new ApplicationInstance
|
var app = new ApplicationInstance
|
||||||
{
|
{
|
||||||
ApplicationName = "LmxOpcUaClient",
|
ApplicationName = "OtOpcUaClient",
|
||||||
ApplicationType = ApplicationType.Client,
|
ApplicationType = ApplicationType.Client,
|
||||||
ApplicationConfiguration = config
|
ApplicationConfiguration = config
|
||||||
};
|
};
|
||||||
|
|||||||
90
src/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs
Normal file
90
src/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
namespace ZB.MOM.WW.OtOpcUa.Client.Shared;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the canonical under-LocalAppData folder for the shared OPC UA client's PKI
|
||||||
|
/// store + persisted settings. Renamed from <c>LmxOpcUaClient</c> to <c>OtOpcUaClient</c>
|
||||||
|
/// in task #208; a one-shot migration shim moves a pre-rename folder in place on first
|
||||||
|
/// resolution so existing developer boxes keep their trusted server certs + saved
|
||||||
|
/// connection settings on upgrade.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Thread-safe: the rename uses <see cref="Directory.Move"/> which is atomic on NTFS
|
||||||
|
/// within the same volume. The lock guarantees the migration runs at most once per
|
||||||
|
/// process even under concurrent first-touch from CLI + UI.
|
||||||
|
/// </remarks>
|
||||||
|
public static class ClientStoragePaths
|
||||||
|
{
|
||||||
|
/// <summary>Canonical client folder name. Post-#208.</summary>
|
||||||
|
public const string CanonicalFolderName = "OtOpcUaClient";
|
||||||
|
|
||||||
|
/// <summary>Pre-#208 folder name. Used only by the migration shim.</summary>
|
||||||
|
public const string LegacyFolderName = "LmxOpcUaClient";
|
||||||
|
|
||||||
|
private static readonly Lock _migrationLock = new();
|
||||||
|
private static bool _migrationChecked;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Absolute path to the client's top-level folder under LocalApplicationData. Runs the
|
||||||
|
/// one-shot legacy-folder migration before returning so callers that depend on this
|
||||||
|
/// path (PKI store, settings file) find their existing state at the canonical name.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetRoot()
|
||||||
|
{
|
||||||
|
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
|
var canonical = Path.Combine(localAppData, CanonicalFolderName);
|
||||||
|
MigrateLegacyFolderIfNeeded(localAppData, canonical);
|
||||||
|
return canonical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Subfolder for the application's PKI store — used by both CLI + UI.</summary>
|
||||||
|
public static string GetPkiPath() => Path.Combine(GetRoot(), "pki");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Expose the migration probe for tests + for callers that want to check whether the
|
||||||
|
/// legacy folder still exists without forcing the rename. Returns true when a legacy
|
||||||
|
/// folder existed + was moved to canonical, false when no migration was needed or
|
||||||
|
/// canonical was already present.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryRunLegacyMigration()
|
||||||
|
{
|
||||||
|
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
|
var canonical = Path.Combine(localAppData, CanonicalFolderName);
|
||||||
|
return MigrateLegacyFolderIfNeeded(localAppData, canonical);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MigrateLegacyFolderIfNeeded(string localAppData, string canonical)
|
||||||
|
{
|
||||||
|
// Fast-path out of the lock when the migration has already been attempted this process
|
||||||
|
// — saves the IO on every subsequent call, + the migration is idempotent within the
|
||||||
|
// same process anyway.
|
||||||
|
if (_migrationChecked) return false;
|
||||||
|
|
||||||
|
lock (_migrationLock)
|
||||||
|
{
|
||||||
|
if (_migrationChecked) return false;
|
||||||
|
_migrationChecked = true;
|
||||||
|
|
||||||
|
var legacy = Path.Combine(localAppData, LegacyFolderName);
|
||||||
|
|
||||||
|
// Only migrate when the legacy folder is present + canonical isn't. Either of the
|
||||||
|
// other three combinations (neither / only-canonical / both) means migration
|
||||||
|
// should NOT run: no-op fresh install, already-migrated, or manual state the
|
||||||
|
// developer has set up — don't clobber.
|
||||||
|
if (!Directory.Exists(legacy)) return false;
|
||||||
|
if (Directory.Exists(canonical)) return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Move(legacy, canonical);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
// Concurrent another-process-moved-it or volume-boundary or permissions — leave
|
||||||
|
// the legacy folder alone; callers that need it can either re-run migration
|
||||||
|
// manually or point CertificateStorePath explicitly.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,11 +41,11 @@ public sealed class ConnectionSettings
|
|||||||
public bool AutoAcceptCertificates { get; set; } = true;
|
public bool AutoAcceptCertificates { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Path to the certificate store. Defaults to a subdirectory under LocalApplicationData.
|
/// Path to the certificate store. Defaults to a subdirectory under LocalApplicationData
|
||||||
|
/// resolved via <see cref="ClientStoragePaths"/> so the one-shot legacy-folder migration
|
||||||
|
/// runs before the path is returned.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string CertificateStorePath { get; set; } = Path.Combine(
|
public string CertificateStorePath { get; set; } = ClientStoragePaths.GetPkiPath();
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
||||||
"LmxOpcUaClient", "pki");
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates the settings and throws if any required values are missing or invalid.
|
/// Validates the settings and throws if any required values are missing or invalid.
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
|||||||
: new UserIdentity();
|
: new UserIdentity();
|
||||||
|
|
||||||
var sessionTimeoutMs = (uint)(settings.SessionTimeoutSeconds * 1000);
|
var sessionTimeoutMs = (uint)(settings.SessionTimeoutSeconds * 1000);
|
||||||
return await _sessionFactory.CreateSessionAsync(config, endpoint, "LmxOpcUaClient", sessionTimeoutMs, identity,
|
return await _sessionFactory.CreateSessionAsync(config, endpoint, "OtOpcUaClient", sessionTimeoutMs, identity,
|
||||||
ct);
|
ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Client.Shared;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
||||||
|
|
||||||
@@ -7,9 +8,9 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class JsonSettingsService : ISettingsService
|
public sealed class JsonSettingsService : ISettingsService
|
||||||
{
|
{
|
||||||
private static readonly string SettingsDir = Path.Combine(
|
// ClientStoragePaths.GetRoot runs the one-shot legacy-folder migration so pre-#208
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
// developer boxes pick up their existing settings.json on first launch post-rename.
|
||||||
"LmxOpcUaClient");
|
private static readonly string SettingsDir = ClientStoragePaths.GetRoot();
|
||||||
|
|
||||||
private static readonly string SettingsPath = Path.Combine(SettingsDir, "settings.json");
|
private static readonly string SettingsPath = Path.Combine(SettingsDir, "settings.json");
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
|
|
||||||
[ObservableProperty] private bool _autoAcceptCertificates = true;
|
[ObservableProperty] private bool _autoAcceptCertificates = true;
|
||||||
|
|
||||||
[ObservableProperty] private string _certificateStorePath = Path.Combine(
|
[ObservableProperty] private string _certificateStorePath = ClientStoragePaths.GetPkiPath();
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
||||||
"LmxOpcUaClient", "pki");
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyCanExecuteChangedFor(nameof(ConnectCommand))]
|
[NotifyCanExecuteChangedFor(nameof(ConnectCommand))]
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ public class SubscribeCommandTests
|
|||||||
await task;
|
await task;
|
||||||
|
|
||||||
var output = TestConsoleHelper.GetOutput(console);
|
var output = TestConsoleHelper.GetOutput(console);
|
||||||
output.ShouldContain("Subscribed to ns=2;s=TestVar (interval: 2000ms)");
|
// CLI now prints aggregate form "Subscribed to {count}/{total} nodes (interval: ...)" rather than
|
||||||
output.ShouldContain("Unsubscribed.");
|
// the single-node form the original test asserted — the command supports multi-node now.
|
||||||
|
output.ShouldContain("Subscribed to 1/1 nodes (interval: 2000ms)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Client.Shared;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests;
|
||||||
|
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public sealed class ClientStoragePathsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetRoot_ReturnsCanonicalFolderName_UnderLocalAppData()
|
||||||
|
{
|
||||||
|
var root = ClientStoragePaths.GetRoot();
|
||||||
|
root.ShouldEndWith(ClientStoragePaths.CanonicalFolderName);
|
||||||
|
root.ShouldContain(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPkiPath_NestsPkiUnderRoot()
|
||||||
|
{
|
||||||
|
var pki = ClientStoragePaths.GetPkiPath();
|
||||||
|
pki.ShouldEndWith(Path.Combine(ClientStoragePaths.CanonicalFolderName, "pki"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanonicalFolderName_IsOtOpcUaClient()
|
||||||
|
{
|
||||||
|
ClientStoragePaths.CanonicalFolderName.ShouldBe("OtOpcUaClient");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LegacyFolderName_IsLmxOpcUaClient()
|
||||||
|
{
|
||||||
|
// The shim depends on this specific spelling — a typo here would leak the legacy
|
||||||
|
// folder past the migration + break every dev-box upgrade.
|
||||||
|
ClientStoragePaths.LegacyFolderName.ShouldBe("LmxOpcUaClient");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryRunLegacyMigration_Returns_False_On_Repeat_Invocation()
|
||||||
|
{
|
||||||
|
// Once the guard in-process has fired, subsequent calls short-circuit to false
|
||||||
|
// regardless of filesystem state. This is the behaviour that keeps the migration
|
||||||
|
// cheap on hot paths (CertificateStorePath property getter is called frequently).
|
||||||
|
_ = ClientStoragePaths.GetRoot(); // arms the guard
|
||||||
|
ClientStoragePaths.TryRunLegacyMigration().ShouldBeFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ public class ConnectionSettingsTests
|
|||||||
settings.SecurityMode.ShouldBe(SecurityMode.None);
|
settings.SecurityMode.ShouldBe(SecurityMode.None);
|
||||||
settings.SessionTimeoutSeconds.ShouldBe(60);
|
settings.SessionTimeoutSeconds.ShouldBe(60);
|
||||||
settings.AutoAcceptCertificates.ShouldBeTrue();
|
settings.AutoAcceptCertificates.ShouldBeTrue();
|
||||||
settings.CertificateStorePath.ShouldContain("LmxOpcUaClient");
|
settings.CertificateStorePath.ShouldContain("OtOpcUaClient");
|
||||||
settings.CertificateStorePath.ShouldContain("pki");
|
settings.CertificateStorePath.ShouldContain("pki");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ public class MainWindowViewModelTests
|
|||||||
_vm.FailoverUrls.ShouldBeNull();
|
_vm.FailoverUrls.ShouldBeNull();
|
||||||
_vm.SessionTimeoutSeconds.ShouldBe(60);
|
_vm.SessionTimeoutSeconds.ShouldBe(60);
|
||||||
_vm.AutoAcceptCertificates.ShouldBeTrue();
|
_vm.AutoAcceptCertificates.ShouldBeTrue();
|
||||||
_vm.CertificateStorePath.ShouldContain("LmxOpcUaClient");
|
_vm.CertificateStorePath.ShouldContain("OtOpcUaClient");
|
||||||
_vm.CertificateStorePath.ShouldContain("pki");
|
_vm.CertificateStorePath.ShouldContain("pki");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user