diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f52ded986a..de3fbcfdc3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -348,7 +348,7 @@ jobs: - powershell: | Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force Install-Module Microsoft.WinGet.Client -Repository PSGallery -Force - try { Repair-WingetPackageManager -Latest -Verbose } catch { $_.Exception } + try { Repair-WingetPackageManager -Version 1.28.240 -Verbose } catch { $_.Exception } Install-WinGetPackage -Id Microsoft.Sysinternals.PsTools -Source winget displayName: Install Sysinternals PsTools Using Winget condition: succeededOrFailed() diff --git a/src/AppInstallerCLICore/PortableInstaller.cpp b/src/AppInstallerCLICore/PortableInstaller.cpp index 1e3498e55a..a5d42b6410 100644 --- a/src/AppInstallerCLICore/PortableInstaller.cpp +++ b/src/AppInstallerCLICore/PortableInstaller.cpp @@ -216,10 +216,27 @@ namespace AppInstaller::CLI::Portable { PortableIndex existingIndex = PortableIndex::Open(existingIndexPath.u8string(), SQLiteStorageBase::OpenDisposition::ReadWrite); + std::exception_ptr firstRemoveException; for (auto expectedEntry : m_expectedEntries) { - RemoveFile(expectedEntry); - existingIndex.RemovePortableFile(expectedEntry); + try + { + RemoveFile(expectedEntry); + existingIndex.RemovePortableFile(expectedEntry); + } + catch (...) + { + // Capture first failure but continue so that all entries (especially symlinks) + // are cleaned up, preventing cascade failures in shared state like PATH. + if (!firstRemoveException) + { + firstRemoveException = std::current_exception(); + } + } + } + if (firstRemoveException) + { + std::rethrow_exception(firstRemoveException); } deleteIndex = existingIndex.IsEmpty(); @@ -233,9 +250,24 @@ namespace AppInstaller::CLI::Portable } else { + std::exception_ptr firstRemoveException; for (auto expectedEntry : m_expectedEntries) { - RemoveFile(expectedEntry); + try + { + RemoveFile(expectedEntry); + } + catch (...) + { + if (!firstRemoveException) + { + firstRemoveException = std::current_exception(); + } + } + } + if (firstRemoveException) + { + std::rethrow_exception(firstRemoveException); } } diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs index b0a8f84801..97afd4f037 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -477,22 +477,52 @@ public static void VerifyPortablePackage( } bool isAddedToPath; + string pathDiagnostics; string pathSubKey = scope == Scope.User ? Constants.PathSubKey_User : Constants.PathSubKey_Machine; using (RegistryKey environmentRegistryKey = baseKey.OpenSubKey(pathSubKey, true)) { string pathName = "Path"; var currentPathValue = (string)environmentRegistryKey.GetValue(pathName); + var rawPathValue = (string)environmentRegistryKey.GetValue(pathName, null, RegistryValueOptions.DoNotExpandEnvironmentNames); + var valueKind = environmentRegistryKey.GetValueKind(pathName); var portablePathValue = (installDirectoryAddedToPath ? installDir : symlinkDirectory) + ';'; isAddedToPath = currentPathValue.Contains(portablePathValue); + + string symlinkDirContents = Directory.Exists(symlinkDirectory) + ? (Directory.GetFileSystemEntries(symlinkDirectory) is string[] entries && entries.Length > 0 + ? string.Join(", ", entries.Select(s => Path.GetFileName(s))) + : "(empty)") + : "(does not exist)"; + + pathDiagnostics = $"\n Registry value kind: {valueKind}" + + $"\n Expanded PATH value: {currentPathValue}" + + $"\n Raw PATH value: {rawPathValue}" + + $"\n Searching for: {portablePathValue}" + + $"\n Links dir contents: {symlinkDirContents}"; } // Always clean up as best effort. - RunAICLICommand("uninstall", $"--product-code {productCode} --force"); + var cleanupResult = RunAICLICommand("uninstall", $"--product-code {productCode} --force"); + + // If the uninstall cleanup failed (e.g., the exe was still in use), manually remove the symlink + // to prevent cascade failures in other parallel tests that check the shared Links directory. + if (cleanupResult.ExitCode != 0 && File.Exists(symlinkPath)) + { + TestContext.Out.WriteLine($"WARNING: Cleanup uninstall failed with exit code {cleanupResult.ExitCode}. Manually removing symlink to prevent cascade: {symlinkPath}"); + try + { + File.Delete(symlinkPath); + } + catch (Exception ex) + { + TestContext.Out.WriteLine($"WARNING: Failed to manually remove symlink: {ex.Message}"); + } + } Assert.AreEqual(shouldExist, exeExists, $"Expected portable exe path: {exePath}"); Assert.AreEqual(shouldExist && !installDirectoryAddedToPath, symlinkExists, $"Expected portable symlink path: {symlinkPath}"); Assert.AreEqual(shouldExist, portableEntryExists, $"Expected {productCode} subkey in path: {uninstallSubKey}"); - Assert.AreEqual(shouldExist, isAddedToPath, $"Expected path variable: {(installDirectoryAddedToPath ? installDir : symlinkDirectory)}"); + Assert.AreEqual(shouldExist, isAddedToPath, $"Expected path variable: {(installDirectoryAddedToPath ? installDir : symlinkDirectory)}{pathDiagnostics}"); } /// diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp index a016d02c63..94386ca591 100644 --- a/src/AppInstallerTestExeInstaller/main.cpp +++ b/src/AppInstallerTestExeInstaller/main.cpp @@ -661,6 +661,9 @@ int wmain(int argc, const wchar_t** argv) { return -1; } + + WaitForSingleObject(execInfo.hProcess, INFINITE); + CloseHandle(execInfo.hProcess); } if (displayName.empty())