Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions Assets/Tests/Editor/NativeCliInstallerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ public async Task WaitForUninstallTargetRemovalAsync_WhenDeferredRemovalComplete
int remainingExistingChecks = 2;
int delayCount = 0;

CliInstallResult result = await NativeCliInstaller.WaitForUninstallTargetRemovalAsync(
CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallTargetRemovalAsync(
"C:\\Users\\ExampleUser\\AppData\\Local\\Programs\\uloop\\bin\\uloop.exe",
CancellationToken.None,
1000,
Expand All @@ -320,7 +320,7 @@ public async Task WaitForUninstallTargetRemovalAsync_WhenTargetRemainsReturnsFai
// Verifies that delayed launcher removal failures do not recache a stale installed CLI.
int delayCount = 0;

CliInstallResult result = await NativeCliInstaller.WaitForUninstallTargetRemovalAsync(
CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallTargetRemovalAsync(
"C:\\Users\\ExampleUser\\AppData\\Local\\Programs\\uloop\\bin\\uloop.exe",
CancellationToken.None,
250,
Expand All @@ -345,7 +345,7 @@ public async Task WaitForUninstallCompletionAsync_OnWindowsWaitsForUserPathRemov
string userPath = installDirectory + ";C:\\npm";
int delayCount = 0;

CliInstallResult result = await NativeCliInstaller.WaitForUninstallCompletionAsync(
CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync(
installDirectory + "\\uloop.exe",
installDirectory,
RuntimePlatform.WindowsEditor,
Expand Down Expand Up @@ -373,7 +373,7 @@ public async Task WaitForUninstallCompletionAsync_OnWindowsFailsWhenUserPathRema
string installDirectory = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin";
int delayCount = 0;

CliInstallResult result = await NativeCliInstaller.WaitForUninstallCompletionAsync(
CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync(
installDirectory + "\\uloop.exe",
installDirectory,
RuntimePlatform.WindowsEditor,
Expand Down Expand Up @@ -401,7 +401,7 @@ public async Task WaitForUninstallCompletionAsync_WhenUserPathRemovalIsNotRequir
// Verifies fallback uninstall failures do not claim ownership of Windows User PATH cleanup.
string installDirectory = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin";

CliInstallResult result = await NativeCliInstaller.WaitForUninstallCompletionAsync(
CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync(
installDirectory + "\\uloop.exe",
installDirectory,
RuntimePlatform.WindowsEditor,
Expand All @@ -424,7 +424,7 @@ public async Task WaitForUninstallCompletionAsync_WhenUserPathRemovalIsNotRequir
// Verifies fallback launchers can complete uninstall without owning Windows User PATH cleanup.
string installDirectory = "C:\\Users\\ExampleUser\\Programs\\uloop\\bin";

CliInstallResult result = await NativeCliInstaller.WaitForUninstallCompletionAsync(
CliInstallResult result = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync(
installDirectory + "\\uloop.exe",
installDirectory,
RuntimePlatform.WindowsEditor,
Expand All @@ -443,7 +443,9 @@ public async Task WaitForUninstallCompletionAsync_WhenUserPathRemovalIsNotRequir
public void UninstallCompletionTimeout_IsLongEnoughForDeferredWindowsPowerShellCleanup()
{
// Verifies Settings uninstall does not report failure before Windows deferred cleanup can finish.
Assert.That(NativeCliInstaller.UNINSTALL_COMPLETION_TIMEOUT_MS, Is.GreaterThanOrEqualTo(30000));
Assert.That(
NativeCliUninstallCompletionWaiter.UNINSTALL_COMPLETION_TIMEOUT_MS,
Is.GreaterThanOrEqualTo(30000));
}

[Test]
Expand Down
131 changes: 2 additions & 129 deletions Packages/src/Editor/Infrastructure/CLI/NativeCliInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ public static class NativeCliInstaller
{
private const int INSTALL_PROCESS_TIMEOUT_MS = 300000;
private const int INSTALL_PROCESS_WAIT_SLICE_MS = 250;
internal const int UNINSTALL_COMPLETION_TIMEOUT_MS = 30000;

public static NativeCliInstallCommand GetInstallCommand(
RuntimePlatform platform,
Expand Down Expand Up @@ -91,12 +90,12 @@ public static async Task<CliInstallResult> UninstallAsync(RuntimePlatform platfo
}

string installPath = NativeCliInstallPathResolver.GetGlobalCliInstallPath(installDirectory, platform);
CliInstallResult removalResult = await WaitForUninstallCompletionAsync(
CliInstallResult removalResult = await NativeCliUninstallCompletionWaiter.WaitForUninstallCompletionAsync(
installPath,
installDirectory,
platform,
ct,
UNINSTALL_COMPLETION_TIMEOUT_MS,
NativeCliUninstallCompletionWaiter.UNINSTALL_COMPLETION_TIMEOUT_MS,
INSTALL_PROCESS_WAIT_SLICE_MS,
File.Exists,
false,
Expand Down Expand Up @@ -237,92 +236,6 @@ private static CliInstallResult RunCliSetupCommand(
: new CliInstallResult(false, BuildCliSetupCommandFailure(commandDescription, errorOutput, standardOutput));
}

internal static async Task<CliInstallResult> WaitForUninstallTargetRemovalAsync(
string targetPath,
CancellationToken ct,
int timeoutMs,
int pollMs,
Func<string, bool> fileExists,
Func<int, CancellationToken, Task> 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<CliInstallResult> WaitForUninstallCompletionAsync(
string targetPath,
string installDirectory,
RuntimePlatform platform,
CancellationToken ct,
int timeoutMs,
int pollMs,
Func<string, bool> fileExists,
bool requireUserPathRemoval,
Func<string, EnvironmentVariableTarget, string> getEnvironmentVariable,
Func<int, CancellationToken, Task> 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)
{
return new CliInstallResult(true, "");
}

ct.ThrowIfCancellationRequested();
if (elapsedMs >= timeoutMs)
{
return new CliInstallResult(
false,
BuildUninstallCompletionTimeoutFailure(
targetPath,
installDirectory,
platform,
targetStillExists,
userPathStillContainsInstallDirectory));
}

int delayMs = Math.Min(pollMs, timeoutMs - elapsedMs);
await delayAsync(delayMs, ct);
elapsedMs += delayMs;
}
}

internal static CliInstallResult FinishSuccessfulInstall(
CliInstallResult installResult,
string installDirectory,
Expand Down Expand Up @@ -371,46 +284,6 @@ private static void RemoveInstallDirectoryFromCurrentProcessPath(RuntimePlatform
Environment.SetEnvironmentVariable(pathVariableName, updatedPath);
}

private static bool ShouldWaitForUserPathRemoval(
bool requireUserPathRemoval,
string installDirectory,
RuntimePlatform platform,
Func<string, EnvironmentVariableTarget, string> 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.";
}

private static string BuildCliSetupCommandFailure(
string commandDescription,
string errorOutput,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using System;
using System.Threading;
using System.Threading.Tasks;

using UnityEngine;

using io.github.hatayama.UnityCliLoop.Application;

namespace io.github.hatayama.UnityCliLoop.Infrastructure
{
/// <summary>
/// Waits for native CLI uninstall cleanup to finish.
/// </summary>
internal static class NativeCliUninstallCompletionWaiter
{
internal const int UNINSTALL_COMPLETION_TIMEOUT_MS = 30000;

internal static async Task<CliInstallResult> WaitForUninstallTargetRemovalAsync(
string targetPath,
CancellationToken ct,
int timeoutMs,
int pollMs,
Func<string, bool> fileExists,
Func<int, CancellationToken, Task> 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<CliInstallResult> WaitForUninstallCompletionAsync(
string targetPath,
string installDirectory,
RuntimePlatform platform,
CancellationToken ct,
int timeoutMs,
int pollMs,
Func<string, bool> fileExists,
bool requireUserPathRemoval,
Func<string, EnvironmentVariableTarget, string> getEnvironmentVariable,
Func<int, CancellationToken, Task> 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)
{
return new CliInstallResult(true, "");
}

ct.ThrowIfCancellationRequested();
if (elapsedMs >= timeoutMs)
{
return new CliInstallResult(
false,
BuildUninstallCompletionTimeoutFailure(
targetPath,
installDirectory,
platform,
targetStillExists,
userPathStillContainsInstallDirectory));
}

int delayMs = Math.Min(pollMs, timeoutMs - elapsedMs);
await delayAsync(delayMs, ct);
elapsedMs += delayMs;
}
}

private static bool ShouldWaitForUserPathRemoval(
bool requireUserPathRemoval,
string installDirectory,
RuntimePlatform platform,
Func<string, EnvironmentVariableTarget, string> 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
Comment thread
hatayama marked this conversation as resolved.
&& 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.";
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading