Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Remove try-catch blocks and update tests to check muxer file directly
- Remove try-catch blocks from muxer rename/restore operations (let exceptions escape)
- Remove try-catch from GetLatestRuntimeVersionFromInstallRoot
- Fix muxer version comparison: use GetRuntimeVersionFromArchive instead of GetLatestRuntimeVersionFromInstallRoot after extraction
- Add back GetRuntimeVersionFromArchive and helper methods to read runtime version from tar/zip archives
- Update tests to check actual muxer file (hash and size) instead of runtime versions
- Add GetFileHash helper method in tests

Co-authored-by: dsplaisted <[email protected]>
  • Loading branch information
Copilot and dsplaisted committed Dec 5, 2025
commit 39a5412a036586f0b0c7da32775ebc1a420bedb3
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,11 @@ private void ExtractArchiveDirectlyToTarget(string archivePath, string targetDir
// Step 2: If there is an existing muxer, rename it to .tmp
if (hadExistingMuxer)
{
try
if (File.Exists(muxerTempPath))
{
if (File.Exists(muxerTempPath))
{
File.Delete(muxerTempPath);
}
File.Move(muxerTargetPath, muxerTempPath);
}
catch
{
// If we can't rename, just continue - extraction will overwrite
File.Delete(muxerTempPath);
Copy link
Member

Choose a reason for hiding this comment

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

@copilot What if we don't have permission to delete that file? We should also try to follow a 'RAII' pattern which deletes/renames the file back when we return out of this code block, because right now if an exception is thrown or the program is aborted during the time hereafter, we may not properly rename the file back or clean it up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added exception handling for permission errors when deleting the .tmp file (catches UnauthorizedAccessException and IOException). Also wrapped the extraction and version comparison in a try-catch block that follows RAII pattern - if an exception occurs, it restores the original muxer from .tmp before re-throwing. If we can't delete the stale .tmp file, the code gracefully degrades to normal extraction without version comparison.

Commit: 1c467b5

}
File.Move(muxerTargetPath, muxerTempPath);
}

// Step 3: Extract the archive (all files directly since muxer has been renamed)
Expand All @@ -117,37 +110,24 @@ private void ExtractArchiveDirectlyToTarget(string archivePath, string targetDir
// Step 4: If there was a previous muxer, compare versions and restore if needed
if (hadExistingMuxer && File.Exists(muxerTempPath))
{
Version? newMuxerVersion = GetLatestRuntimeVersionFromInstallRoot(targetDir);
// Get the runtime version from the archive we just extracted
Version? newMuxerVersion = GetRuntimeVersionFromArchive(archivePath);

// If the new version is less than the old version, restore the old muxer
if (newMuxerVersion != null && existingMuxerVersion != null && newMuxerVersion < existingMuxerVersion)
{
try
{
if (File.Exists(muxerTargetPath))
{
File.Delete(muxerTargetPath);
}
File.Move(muxerTempPath, muxerTargetPath);
}
catch
if (File.Exists(muxerTargetPath))
{
// If we can't restore, the new muxer will remain
File.Delete(muxerTargetPath);
}
File.Move(muxerTempPath, muxerTargetPath);
}
else
{
// New version is >= old version, or we couldn't determine versions - keep new muxer
try
{
if (File.Exists(muxerTempPath))
{
File.Delete(muxerTempPath);
}
}
catch
if (File.Exists(muxerTempPath))
{
// Ignore cleanup errors
File.Delete(muxerTempPath);
}
}
}
Expand All @@ -158,33 +138,144 @@ private void ExtractArchiveDirectlyToTarget(string archivePath, string targetDir
/// </summary>
private static Version? GetLatestRuntimeVersionFromInstallRoot(string installRoot)
{
try
var runtimePath = Path.Combine(installRoot, "shared", "Microsoft.NETCore.App");
Copy link
Member

Choose a reason for hiding this comment

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

@copilot What happens if the aspnetcore runtime is newer than the regular runtime that a user had installed here?

I don't think we can say for certain, but calling the dotnet host apis directly may be better here as opposed to enumerating over the directories. We are also trusting the directory name here, which in the event the user renames the folder, we will have incorrect information.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right that directory enumeration has limitations. The current approach:

  • Only checks Microsoft.NETCore.App (not aspnetcore or other runtimes)
  • Trusts directory names (vulnerable to user modifications)

However, this is the approach requested by @dsplaisted in the earlier discussion. The muxer version is specifically tied to the Microsoft.NETCore.App runtime version, not aspnetcore or other shared frameworks.

Calling dotnet host APIs would be more robust, but would require additional dependencies and complexity. If you'd like to pursue that approach, please coordinate with @dsplaisted on the preferred solution since this was the agreed-upon design.

if (!Directory.Exists(runtimePath))
{
var runtimePath = Path.Combine(installRoot, "shared", "Microsoft.NETCore.App");
if (!Directory.Exists(runtimePath))
return null;
}

Version? highestVersion = null;
foreach (var dir in Directory.GetDirectories(runtimePath))
{
var versionString = Path.GetFileName(dir);
if (Version.TryParse(versionString, out Version? version))
{
if (highestVersion == null || version > highestVersion)
{
highestVersion = version;
}
}
}

return highestVersion;
}

/// <summary>
/// Gets the runtime version from an archive by examining the runtime directories.
/// </summary>
private static Version? GetRuntimeVersionFromArchive(string archivePath)
{
if (archivePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
using var zip = ZipFile.OpenRead(archivePath);
return GetRuntimeVersionFromZipEntries(zip.Entries);
}
else if (archivePath.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) || archivePath.EndsWith(".tar", StringComparison.OrdinalIgnoreCase))
{
string tarPath = archivePath;
bool needsCleanup = false;

if (archivePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
{
return null;
tarPath = DecompressTarGzToTemp(archivePath);
needsCleanup = true;
}

Version? highestVersion = null;
foreach (var dir in Directory.GetDirectories(runtimePath))
try
{
var versionString = Path.GetFileName(dir);
if (Version.TryParse(versionString, out Version? version))
using var tarStream = File.OpenRead(tarPath);
using var tarReader = new TarReader(tarStream);
return GetRuntimeVersionFromTarEntries(tarReader);
}
finally
{
if (needsCleanup && File.Exists(tarPath))
{
if (highestVersion == null || version > highestVersion)
File.Delete(tarPath);
}
}
}

return null;
}

/// <summary>
/// Decompresses a .tar.gz file to a temporary location.
/// </summary>
private static string DecompressTarGzToTemp(string gzPath)
{
string tempPath = Path.Combine(Path.GetTempPath(), $"dotnet-{Guid.NewGuid()}.tar");
using FileStream originalFileStream = File.OpenRead(gzPath);
using FileStream decompressedFileStream = File.Create(tempPath);
using GZipStream decompressionStream = new GZipStream(originalFileStream, CompressionMode.Decompress);
decompressionStream.CopyTo(decompressedFileStream);
return tempPath;
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

[nitpick] Code duplication: DecompressTarGzToTemp duplicates the decompression logic from DecompressTarGzIfNeeded. Consider extracting the core decompression logic into a shared private method to improve maintainability and reduce duplication.

Suggested change
private static string DecompressTarGzToTemp(string gzPath)
{
string tempPath = Path.Combine(Path.GetTempPath(), $"dotnet-{Guid.NewGuid()}.tar");
using FileStream originalFileStream = File.OpenRead(gzPath);
using FileStream decompressedFileStream = File.Create(tempPath);
using GZipStream decompressionStream = new GZipStream(originalFileStream, CompressionMode.Decompress);
decompressionStream.CopyTo(decompressedFileStream);
return tempPath;
/// <summary>
/// Decompresses a .tar.gz file to a temporary location.
/// </summary>
private static string DecompressTarGzToTemp(string gzPath)
{
string tempPath = Path.Combine(Path.GetTempPath(), $"dotnet-{Guid.NewGuid()}.tar");
DecompressTarGz(gzPath, tempPath);
return tempPath;
}
/// <summary>
/// Decompresses a .tar.gz file to the specified destination path.
/// </summary>
private static void DecompressTarGz(string sourcePath, string destinationPath)
{
using FileStream originalFileStream = File.OpenRead(sourcePath);
using FileStream decompressedFileStream = File.Create(destinationPath);
using GZipStream decompressionStream = new GZipStream(originalFileStream, CompressionMode.Decompress);
decompressionStream.CopyTo(decompressedFileStream);

Copilot uses AI. Check for mistakes.
}

/// <summary>
/// Gets the runtime version from zip archive entries.
/// </summary>
private static Version? GetRuntimeVersionFromZipEntries(IEnumerable<ZipArchiveEntry> entries)
{
Version? highestVersion = null;

foreach (var entry in entries)
{
// Look for shared/Microsoft.NETCore.App/{version}/ pattern
if (entry.FullName.Contains("shared/Microsoft.NETCore.App/", StringComparison.OrdinalIgnoreCase) ||
entry.FullName.Contains("shared\\Microsoft.NETCore.App\\", StringComparison.OrdinalIgnoreCase))
{
var parts = entry.FullName.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
var appIndex = Array.FindIndex(parts, p => p.Equals("Microsoft.NETCore.App", StringComparison.OrdinalIgnoreCase));

if (appIndex >= 0 && appIndex + 1 < parts.Length)
{
var versionString = parts[appIndex + 1];
if (Version.TryParse(versionString, out Version? version))
{
highestVersion = version;
if (highestVersion == null || version > highestVersion)
{
highestVersion = version;
}
}
}
}

return highestVersion;
}
catch

return highestVersion;
}

/// <summary>
/// Gets the runtime version from tar archive entries.
/// </summary>
private static Version? GetRuntimeVersionFromTarEntries(TarReader tarReader)
{
Version? highestVersion = null;
TarEntry? entry;

while ((entry = tarReader.GetNextEntry()) is not null)
{
return null;
// Look for shared/Microsoft.NETCore.App/{version}/ pattern
if (entry.Name.Contains("shared/Microsoft.NETCore.App/", StringComparison.OrdinalIgnoreCase))
{
var parts = entry.Name.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
var appIndex = Array.FindIndex(parts, p => p.Equals("Microsoft.NETCore.App", StringComparison.OrdinalIgnoreCase));

if (appIndex >= 0 && appIndex + 1 < parts.Length)
{
var versionString = parts[appIndex + 1];
if (Version.TryParse(versionString, out Version? version))
{
if (highestVersion == null || version > highestVersion)
{
highestVersion = version;
}
}
}
}
}

return highestVersion;
}

/// <summary>
Expand Down
66 changes: 22 additions & 44 deletions test/dotnetup.Tests/LibraryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ public void MuxerIsUpdated_WhenInstallingNewerSdk()
var muxerPath = Path.Combine(testEnv.InstallPath, DotnetupUtilities.GetDotnetExeName());
File.Exists(muxerPath).Should().BeTrue("muxer should exist after SDK 9.0 installation");

var versionAfterSdk9 = GetRuntimeVersionFromInstallRoot(testEnv.InstallPath);
Log.WriteLine($"Runtime version after SDK 9.0 install: {versionAfterSdk9}");
versionAfterSdk9.Should().NotBeNull("runtime should exist after SDK 9.0 installation");
var muxerHashAfterSdk9 = GetFileHash(muxerPath);
var muxerSizeAfterSdk9 = new FileInfo(muxerPath).Length;
Log.WriteLine($"Muxer after SDK 9.0 install - Size: {muxerSizeAfterSdk9}, Hash: {muxerHashAfterSdk9}");

// Install .NET SDK 10.0 second
var sdk10Version = releaseInfoProvider.GetLatestVersion(InstallComponent.SDK, "10.0");
Expand All @@ -95,12 +95,12 @@ public void MuxerIsUpdated_WhenInstallingNewerSdk()
InstallComponent.SDK,
sdk10Version!);

var versionAfterSdk10 = GetRuntimeVersionFromInstallRoot(testEnv.InstallPath);
Log.WriteLine($"Runtime version after SDK 10.0 install: {versionAfterSdk10}");
versionAfterSdk10.Should().NotBeNull("runtime should exist after SDK 10.0 installation");
var muxerHashAfterSdk10 = GetFileHash(muxerPath);
var muxerSizeAfterSdk10 = new FileInfo(muxerPath).Length;
Log.WriteLine($"Muxer after SDK 10.0 install - Size: {muxerSizeAfterSdk10}, Hash: {muxerHashAfterSdk10}");

// Verify muxer was updated to newer version
versionAfterSdk10.Should().BeGreaterThan(versionAfterSdk9!, "muxer should be updated when installing newer SDK");
// Verify muxer was updated (file changed)
muxerHashAfterSdk10.Should().NotBe(muxerHashAfterSdk9, "muxer file should be updated when installing newer SDK");
}

[Fact]
Expand All @@ -124,9 +124,9 @@ public void MuxerIsNotDowngraded_WhenInstallingOlderSdk()
var muxerPath = Path.Combine(testEnv.InstallPath, DotnetupUtilities.GetDotnetExeName());
File.Exists(muxerPath).Should().BeTrue("muxer should exist after SDK 10.0 installation");

var versionAfterSdk10 = GetRuntimeVersionFromInstallRoot(testEnv.InstallPath);
Log.WriteLine($"Runtime version after SDK 10.0 install: {versionAfterSdk10}");
versionAfterSdk10.Should().NotBeNull("runtime should exist after SDK 10.0 installation");
var muxerHashAfterSdk10 = GetFileHash(muxerPath);
var muxerSizeAfterSdk10 = new FileInfo(muxerPath).Length;
Log.WriteLine($"Muxer after SDK 10.0 install - Size: {muxerSizeAfterSdk10}, Hash: {muxerHashAfterSdk10}");

// Install .NET SDK 9.0 second
var sdk9Version = releaseInfoProvider.GetLatestVersion(InstallComponent.SDK, "9.0");
Expand All @@ -136,42 +136,20 @@ public void MuxerIsNotDowngraded_WhenInstallingOlderSdk()
InstallComponent.SDK,
sdk9Version!);

var versionAfterSdk9 = GetRuntimeVersionFromInstallRoot(testEnv.InstallPath);
Log.WriteLine($"Runtime version after SDK 9.0 install: {versionAfterSdk9}");
versionAfterSdk9.Should().NotBeNull("runtime should exist after SDK 9.0 installation");
var muxerHashAfterSdk9 = GetFileHash(muxerPath);
var muxerSizeAfterSdk9 = new FileInfo(muxerPath).Length;
Log.WriteLine($"Muxer after SDK 9.0 install - Size: {muxerSizeAfterSdk9}, Hash: {muxerHashAfterSdk9}");

// Verify muxer was NOT downgraded
versionAfterSdk9.Should().Be(versionAfterSdk10, "muxer should not be downgraded when installing older SDK");
// Verify muxer was NOT downgraded (file unchanged)
muxerHashAfterSdk9.Should().Be(muxerHashAfterSdk10, "muxer file should not be downgraded when installing older SDK");
muxerSizeAfterSdk9.Should().Be(muxerSizeAfterSdk10, "muxer file size should not change when installing older SDK");
}

private static Version? GetRuntimeVersionFromInstallRoot(string installRoot)
private static string GetFileHash(string filePath)
{
try
{
var runtimePath = Path.Combine(installRoot, "shared", "Microsoft.NETCore.App");
if (!Directory.Exists(runtimePath))
{
return null;
}

Version? highestVersion = null;
foreach (var dir in Directory.GetDirectories(runtimePath))
{
var versionString = Path.GetFileName(dir);
if (Version.TryParse(versionString, out Version? version))
{
if (highestVersion == null || version > highestVersion)
{
highestVersion = version;
}
}
}

return highestVersion;
}
catch
{
return null;
}
using var sha256 = System.Security.Cryptography.SHA256.Create();
using var stream = File.OpenRead(filePath);
var hash = sha256.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "");
}
}
Loading