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
2 changes: 1 addition & 1 deletion azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 1.28?

Install-WinGetPackage -Id Microsoft.Sysinternals.PsTools -Source winget
displayName: Install Sysinternals PsTools Using Winget
condition: succeededOrFailed()
Expand Down
38 changes: 35 additions & 3 deletions src/AppInstallerCLICore/PortableInstaller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}
}

Expand Down
34 changes: 32 additions & 2 deletions src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}

/// <summary>
Expand Down
3 changes: 3 additions & 0 deletions src/AppInstallerTestExeInstaller/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,9 @@ int wmain(int argc, const wchar_t** argv)
{
return -1;
}

WaitForSingleObject(execInfo.hProcess, INFINITE);
CloseHandle(execInfo.hProcess);
}

if (displayName.empty())
Expand Down
Loading