Client rename residuals: lmxopcua-cli → otopcua-cli + LmxOpcUaClient → OtOpcUaClient with migration shim. Closes task #208 (the executable-name + LocalAppData-folder slice that was called out in Client.CLI.md / Client.UI.md as a deliberately-deferred residual of the Phase 0 rename). Six source references flipped to the canonical OtOpcUaClient spelling: Program.cs CliFx executable name + description (lmxopcua-cli → otopcua-cli), DefaultApplicationConfigurationFactory.cs ApplicationName + ApplicationUri (LmxOpcUaClient + urn:localhost:LmxOpcUaClient → OtOpcUaClient + urn:localhost:OtOpcUaClient), OpcUaClientService.CreateSessionAsync session-name arg, ConnectionSettings.CertificateStorePath default, MainWindowViewModel.CertificateStorePath default, JsonSettingsService.SettingsDir. Two consuming tests (ConnectionSettingsTests + MainWindowViewModelTests) updated to assert the new canonical name. New ClientStoragePaths static helper at src/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs is the migration shim — single entry point for the PKI root + pki subpath, runs a one-shot legacy-folder probe on first resolution: if {LocalAppData}/LmxOpcUaClient/ exists + {LocalAppData}/OtOpcUaClient/ does not, Directory.Move renames it in place (atomic on NTFS within the same volume) so trusted server certs + saved connection settings persist across the rename without operator action. Idempotent per-process via a Lock-guarded _migrationChecked flag so repeated CertificateStorePath getter calls on the hot path pay no IO cost beyond the first. Fresh-install path (neither folder exists) + already-migrated path (only canonical exists) + manual-override path (both exist — developer has set up something explicit) are all no-ops that leave state alone. IOException on the Directory.Move is swallowed + logged as a false return so a concurrent peer process losing the race doesn't crash the consumer; the losing process falls back to whatever state exists. Five new ClientStoragePathsTests assert: GetRoot ends with canonical name under LocalAppData, GetPkiPath nests pki under root, CanonicalFolderName is OtOpcUaClient, LegacyFolderName is LmxOpcUaClient (the migration contract — a typo here would leak the legacy folder past the shim), repeat invocation returns false after first-touch arms the in-process guard. Doc-side residual-explanation notes in docs/Client.CLI.md + docs/Client.UI.md are dropped now that the rename is real; replaced with a short "pre-#208 dev boxes migrate automatically on first launch" note that points at ClientStoragePaths. Sample CLI invocations in Client.CLI.md updated via sed from lmxopcua-cli to otopcua-cli across every command block (14 replacements). Pre-existing staleness in SubscribeCommandTests.Execute_PrintsSubscriptionMessage surfaced during the test run — the CLI's subscribe command has long since switched to an aggregate "Subscribed to {count}/{total} nodes (interval: ...)" output format but the test still asserted the original single-node form. Updated the assertion to match current output + added a comment explaining the change; this is unrelated to the rename but was blocking a green Client.CLI.Tests run. Full solution build 0 errors; Client.Shared.Tests 136/136 + 5 new shim tests passing; Client.UI.Tests 98/98; Client.CLI.Tests 52/52 (was 51/52 before the subscribe-test fix). No Admin/Core/Server changes — this touches only the client layer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user