From 56e4ff6e7f3268f105f26e53ef47802a6645732f Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 9 Jul 2026 00:32:42 +0900 Subject: [PATCH 1/2] Remove legacy native CLI User PATH polling Delete the test-only uninstall branches left behind after package-local CLI launchers were removed. Keep production waiting focused on installed launcher self-removal while preserving cancellation order, timeout timing, and error text. --- .../Tests/Editor/NativeCliInstallerTests.cs | 130 ++---------------- .../CLI/NativeCliInstallPathResolver.cs | 54 -------- .../Infrastructure/CLI/NativeCliInstaller.cs | 4 - .../CLI/NativeCliUninstallCompletionWaiter.cs | 93 +------------ 4 files changed, 10 insertions(+), 271 deletions(-) diff --git a/Assets/Tests/Editor/NativeCliInstallerTests.cs b/Assets/Tests/Editor/NativeCliInstallerTests.cs index a889938f74..60a69b9066 100644 --- a/Assets/Tests/Editor/NativeCliInstallerTests.cs +++ b/Assets/Tests/Editor/NativeCliInstallerTests.cs @@ -298,148 +298,34 @@ public void RunUninstallCommand_WhenCanceledReportsUninstallCommand() } [Test] - public async Task WaitForUninstallTargetRemovalAsync_WhenDeferredRemovalCompletesReturnsSuccess() + public async Task WaitForUninstallCompletionAsync_WhenTargetRemainsReportsTimeout() { - // Verifies that Settings uninstall waits for deferred launcher self-removal before refreshing CLI status. - int remainingExistingChecks = 2; - int delayCount = 0; - - CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallTargetRemovalAsync( - "C:\\Users\\ExampleUser\\AppData\\Local\\Programs\\uloop\\bin\\uloop.exe", - CancellationToken.None, - 1000, - 100, - executablePath => remainingExistingChecks-- > 0, - (delayMs, ct) => - { - delayCount++; - return Task.CompletedTask; - }); - - Assert.That(result.Success, Is.True, result.ErrorOutput); - Assert.That(delayCount, Is.EqualTo(2)); - } - - [Test] - public async Task WaitForUninstallTargetRemovalAsync_WhenTargetRemainsReturnsFailure() - { - // Verifies that delayed launcher removal failures do not recache a stale installed CLI. - int delayCount = 0; - - CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallTargetRemovalAsync( - "C:\\Users\\ExampleUser\\AppData\\Local\\Programs\\uloop\\bin\\uloop.exe", - CancellationToken.None, - 250, - 100, - executablePath => true, - (delayMs, ct) => - { - delayCount++; - return Task.CompletedTask; - }); - - Assert.That(result.Success, Is.False); - Assert.That(result.ErrorOutput, Does.Contain("Timed out waiting for uLoop CLI uninstall")); - Assert.That(delayCount, Is.EqualTo(3)); - } - - [Test] - public async Task WaitForUninstallCompletionAsync_OnWindowsWaitsForUserPathRemoval() - { - // Verifies that Settings uninstall waits until Windows User PATH no longer resolves the native CLI directory. - string installDirectory = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin"; - string userPath = installDirectory + ";C:\\npm"; - int delayCount = 0; + // Verifies uninstall completion reports the launcher path when deferred self-removal times out. + string targetPath = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin\\uloop.exe"; CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync( - installDirectory + "\\uloop.exe", - installDirectory, - RuntimePlatform.WindowsEditor, - CancellationToken.None, - 1000, - 100, - executablePath => false, - true, - (name, target) => userPath, - (delayMs, ct) => - { - delayCount++; - userPath = "C:\\npm"; - return Task.CompletedTask; - }); - - Assert.That(result.Success, Is.True, result.ErrorOutput); - Assert.That(delayCount, Is.EqualTo(1)); - } - - [Test] - public async Task WaitForUninstallCompletionAsync_OnWindowsFailsWhenUserPathRemains() - { - // Verifies that uninstall cannot report success while Windows User PATH still contains the native CLI directory. - string installDirectory = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin"; - int delayCount = 0; - - CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync( - installDirectory + "\\uloop.exe", - installDirectory, - RuntimePlatform.WindowsEditor, - CancellationToken.None, - 250, - 100, - executablePath => false, - true, - (name, target) => installDirectory + ";C:\\npm", - (delayMs, ct) => - { - delayCount++; - return Task.CompletedTask; - }); - - Assert.That(result.Success, Is.False); - Assert.That(result.ErrorOutput, Does.Contain("Windows User PATH")); - Assert.That(result.ErrorOutput, Does.Not.Contain("\\uloop.exe")); - Assert.That(delayCount, Is.EqualTo(3)); - } - - [Test] - public async Task WaitForUninstallCompletionAsync_WhenUserPathRemovalIsNotRequiredReportsTargetTimeoutOnly() - { - // Verifies fallback uninstall failures do not claim ownership of Windows User PATH cleanup. - string installDirectory = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin"; - - CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync( - installDirectory + "\\uloop.exe", - installDirectory, - RuntimePlatform.WindowsEditor, + targetPath, CancellationToken.None, 250, 100, executablePath => true, - false, - (name, target) => installDirectory + ";C:\\npm", (delayMs, ct) => Task.CompletedTask); Assert.That(result.Success, Is.False); - Assert.That(result.ErrorOutput, Does.Contain("\\uloop.exe")); - Assert.That(result.ErrorOutput, Does.Not.Contain("Windows User PATH")); + Assert.That(result.ErrorOutput, Does.Contain(targetPath)); } [Test] - public async Task WaitForUninstallCompletionAsync_WhenUserPathRemovalIsNotRequiredIgnoresUserPath() + public async Task WaitForUninstallCompletionAsync_WhenTargetIsRemovedReturnsSuccess() { - // Verifies fallback launchers can complete uninstall without owning Windows User PATH cleanup. - string installDirectory = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin"; + // Verifies uninstall completion succeeds as soon as deferred launcher self-removal finishes. CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync( - installDirectory + "\\uloop.exe", - installDirectory, - RuntimePlatform.WindowsEditor, + "C:\\Users\\ExampleUser\\Programs\\uloop\\bin\\uloop.exe", CancellationToken.None, 250, 100, executablePath => false, - false, - (name, target) => installDirectory + ";C:\\npm", (delayMs, ct) => Task.CompletedTask); Assert.That(result.Success, Is.True, result.ErrorOutput); diff --git a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstallPathResolver.cs b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstallPathResolver.cs index 10c7be305c..b440ec68ef 100644 --- a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstallPathResolver.cs +++ b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstallPathResolver.cs @@ -220,24 +220,6 @@ internal static string GetPathEnvironmentVariableName(RuntimePlatform platform) : CliConstants.POSIX_PATH_ENVIRONMENT_VARIABLE; } - internal static bool DoesUserPathContainInstallDirectory( - string installDirectory, - RuntimePlatform platform, - Func getEnvironmentVariable) - { - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); - UnityEngine.Debug.Assert(getEnvironmentVariable != null, "getEnvironmentVariable must not be null"); - - if (platform != RuntimePlatform.WindowsEditor) - { - return false; - } - - string pathVariableName = GetPathEnvironmentVariableName(platform); - string currentUserPath = getEnvironmentVariable(pathVariableName, EnvironmentVariableTarget.User); - return DoesPathContainInstallDirectory(currentUserPath, installDirectory, platform); - } - private static string GetPathSeparator(RuntimePlatform platform) { return platform == RuntimePlatform.WindowsEditor @@ -272,42 +254,6 @@ private static string NormalizePathForComparison(string path, RuntimePlatform pl return normalizedPath.TrimEnd('\\', '/').Replace('/', '\\'); } - private static bool DoesPathContainInstallDirectory( - string currentPath, - string installDirectory, - RuntimePlatform platform) - { - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); - - string normalizedPath = currentPath ?? ""; - if (string.IsNullOrEmpty(normalizedPath)) - { - return false; - } - - string separator = GetPathSeparator(platform); - string[] entries = normalizedPath.Split( - new[] { separator }, - StringSplitOptions.RemoveEmptyEntries); - string normalizedInstallDirectory = NormalizePathForComparison(installDirectory, platform); - StringComparison comparison = GetPathComparison(platform); - foreach (string entry in entries) - { - if (string.IsNullOrWhiteSpace(entry)) - { - continue; - } - - string normalizedEntry = NormalizePathForComparison(entry, platform); - if (string.Equals(normalizedEntry, normalizedInstallDirectory, comparison)) - { - return true; - } - } - - return false; - } - private static string GetGlobalCliInstallFileName(RuntimePlatform platform) { return platform == RuntimePlatform.WindowsEditor diff --git a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs index de7817c696..7913756b4a 100644 --- a/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs +++ b/Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs @@ -93,14 +93,10 @@ public static async Task UninstallAsync(RuntimePlatform platfo string installPath = NativeCliInstallPathResolver.GetGlobalCliInstallPath(installDirectory, platform); CliInstallResult removalResult = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync( installPath, - installDirectory, - platform, ct, NativeCliUninstallCompletionWaiter.UNINSTALL_COMPLETION_TIMEOUT_MS, NativeCliSetupCommandRunner.INSTALL_PROCESS_WAIT_SLICE_MS, File.Exists, - false, - Environment.GetEnvironmentVariable, Task.Delay); if (!removalResult.Success) { diff --git a/Packages/src/Editor/Infrastructure/CLI/NativeCliUninstallCompletionWaiter.cs b/Packages/src/Editor/Infrastructure/CLI/NativeCliUninstallCompletionWaiter.cs index f5cbd73ac1..e575c45c68 100644 --- a/Packages/src/Editor/Infrastructure/CLI/NativeCliUninstallCompletionWaiter.cs +++ b/Packages/src/Editor/Infrastructure/CLI/NativeCliUninstallCompletionWaiter.cs @@ -15,69 +15,25 @@ internal static class NativeCliUninstallCompletionWaiter { internal const int UNINSTALL_COMPLETION_TIMEOUT_MS = 30000; - internal static async Task WaitForUninstallTargetRemovalAsync( - string targetPath, - CancellationToken ct, - int timeoutMs, - int pollMs, - Func fileExists, - Func delayAsync) - { - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(targetPath), "targetPath must not be null or empty"); - UnityEngine.Debug.Assert(timeoutMs > 0, "timeoutMs must be greater than zero"); - UnityEngine.Debug.Assert(pollMs > 0, "pollMs must be greater than zero"); - UnityEngine.Debug.Assert(fileExists != null, "fileExists must not be null"); - UnityEngine.Debug.Assert(delayAsync != null, "delayAsync must not be null"); - - int elapsedMs = 0; - while (fileExists(targetPath)) - { - ct.ThrowIfCancellationRequested(); - if (elapsedMs >= timeoutMs) - { - return new CliInstallResult( - false, - $"Timed out waiting for uLoop CLI uninstall to remove {targetPath}."); - } - - int delayMs = Math.Min(pollMs, timeoutMs - elapsedMs); - await delayAsync(delayMs, ct); - elapsedMs += delayMs; - } - - return new CliInstallResult(true, ""); - } - internal static async Task WaitForUninstallCompletionAsync( string targetPath, - string installDirectory, - RuntimePlatform platform, CancellationToken ct, int timeoutMs, int pollMs, Func fileExists, - bool requireUserPathRemoval, - Func getEnvironmentVariable, Func delayAsync) { UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(targetPath), "targetPath must not be null or empty"); - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); UnityEngine.Debug.Assert(timeoutMs > 0, "timeoutMs must be greater than zero"); UnityEngine.Debug.Assert(pollMs > 0, "pollMs must be greater than zero"); UnityEngine.Debug.Assert(fileExists != null, "fileExists must not be null"); - UnityEngine.Debug.Assert(getEnvironmentVariable != null, "getEnvironmentVariable must not be null"); UnityEngine.Debug.Assert(delayAsync != null, "delayAsync must not be null"); int elapsedMs = 0; while (true) { bool targetStillExists = fileExists(targetPath); - bool userPathStillContainsInstallDirectory = ShouldWaitForUserPathRemoval( - requireUserPathRemoval, - installDirectory, - platform, - getEnvironmentVariable); - if (!targetStillExists && !userPathStillContainsInstallDirectory) + if (!targetStillExists) { return new CliInstallResult(true, ""); } @@ -87,12 +43,7 @@ internal static async Task WaitForUninstallCompletionAsync( { return new CliInstallResult( false, - BuildUninstallCompletionTimeoutFailure( - targetPath, - installDirectory, - platform, - targetStillExists, - userPathStillContainsInstallDirectory)); + $"Timed out waiting for uLoop CLI uninstall to remove {targetPath}."); } int delayMs = Math.Min(pollMs, timeoutMs - elapsedMs); @@ -100,45 +51,5 @@ internal static async Task WaitForUninstallCompletionAsync( elapsedMs += delayMs; } } - - private static bool ShouldWaitForUserPathRemoval( - bool requireUserPathRemoval, - string installDirectory, - RuntimePlatform platform, - Func getEnvironmentVariable) - { - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); - UnityEngine.Debug.Assert(getEnvironmentVariable != null, "getEnvironmentVariable must not be null"); - - return requireUserPathRemoval - && NativeCliInstallPathResolver.DoesUserPathContainInstallDirectory( - installDirectory, - platform, - getEnvironmentVariable); - } - - private static string BuildUninstallCompletionTimeoutFailure( - string targetPath, - string installDirectory, - RuntimePlatform platform, - bool targetStillExists, - bool userPathStillContainsInstallDirectory) - { - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(targetPath), "targetPath must not be null or empty"); - UnityEngine.Debug.Assert(!string.IsNullOrWhiteSpace(installDirectory), "installDirectory must not be null or empty"); - UnityEngine.Debug.Assert(targetStillExists || userPathStillContainsInstallDirectory, "at least one uninstall cleanup condition must still be pending"); - - if (platform != RuntimePlatform.WindowsEditor || !userPathStillContainsInstallDirectory) - { - return $"Timed out waiting for uLoop CLI uninstall to remove {targetPath}."; - } - - if (!targetStillExists) - { - return $"Timed out waiting for uLoop CLI uninstall to remove {installDirectory} from Windows User PATH."; - } - - return $"Timed out waiting for uLoop CLI uninstall to remove {targetPath} and remove {installDirectory} from Windows User PATH."; - } } } From d134ca0e2a44a75b97994b2bf0bbb16f47eeec37 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 9 Jul 2026 00:42:02 +0900 Subject: [PATCH 2/2] Preserve uninstall polling cadence coverage Assert the existing timeout and poll interval still produce three delay slices after removing the duplicate legacy waiter tests. --- Assets/Tests/Editor/NativeCliInstallerTests.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Assets/Tests/Editor/NativeCliInstallerTests.cs b/Assets/Tests/Editor/NativeCliInstallerTests.cs index 60a69b9066..0bcb4eef34 100644 --- a/Assets/Tests/Editor/NativeCliInstallerTests.cs +++ b/Assets/Tests/Editor/NativeCliInstallerTests.cs @@ -302,6 +302,7 @@ public async Task WaitForUninstallCompletionAsync_WhenTargetRemainsReportsTimeou { // Verifies uninstall completion reports the launcher path when deferred self-removal times out. string targetPath = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin\\uloop.exe"; + int delayCount = 0; CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync( targetPath, @@ -309,10 +310,15 @@ public async Task WaitForUninstallCompletionAsync_WhenTargetRemainsReportsTimeou 250, 100, executablePath => true, - (delayMs, ct) => Task.CompletedTask); + (delayMs, ct) => + { + delayCount++; + return Task.CompletedTask; + }); Assert.That(result.Success, Is.False); Assert.That(result.ErrorOutput, Does.Contain(targetPath)); + Assert.That(delayCount, Is.EqualTo(3)); } [Test]