Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 2, 2025

  • Remove MuxerVersionHandlingTests.cs unit tests
  • Add two integration tests to LibraryTests that verify muxer version handling
    • Test 1: Install .NET SDK 9.0 then 10.0, verify muxer is updated ✅
    • Test 2: Install 10.0 then 9.0, verify muxer is NOT updated ✅
  • Support muxer version detection on Linux/Mac
    • Runtime version determined from shared/Microsoft.NETCore.App directories
  • Tests now run on all platforms (Linux, Mac, Windows)
  • Refactor to use same approach on all platforms
    • Use runtime version from install root for all platforms
    • Simpler rename-extract-compare-restore workflow
    • No special handling during extraction
  • Remove try-catch blocks (let exceptions escape)
  • Use GetLatestRuntimeVersionFromInstallRoot for version comparison
  • Update tests to check actual muxer file (hash/size) and FileVersionInfo on Windows
  • Fix version comparison logic: check if runtime version stayed the same (not newer) after extraction
  • Assert file versions are not null on Windows (fail test if version can't be read)
  • Add exception handling and RAII cleanup pattern for muxer temp file
Original prompt

This section details on the original issue you should resolve

<issue_title>Fix muxer version handling for dotnetup (and installation library)</issue_title>
<issue_description>When installing the .NET SDK or runtime, there may be existing versions of the muxer installed in the dotnet root. We should install a new one if it is newer than the existing one. Currently DotnetArchiveExtractor tries to do this but does it incorrectly by comparing SDK versions to runtime versions.

If possible, the right way to do this is to probably look at the file metadata for the existing version and the version coming from the new install, to determine which one is newer.</issue_description>

<agent_instructions>The DotnetArchiveExtractor class should be updated so that it compares the file version of the existing muxer (dotnet.exe on Windows) to the version that would come from the tar or zip archive. It should use the one with the newer file version, so if the existing version is newer it should skip copying that file from the archive.

The ConfigureMuxerHandling method can probably be deleted entirely in favor of a new method that is called by the zip or tar extraction logic.

Also, you should add tests for this. If the .NET 9 SDK is installed first and then the .NET 10 SDK is installed, then the muxer should be updated. However, if the .NET 10 SDK is installed first, then the muxer should not be updated when the .NET 9 SDK is installed.</agent_instructions>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Fix muxer version handling for dotnetup installation library Fix muxer version handling to compare file versions instead of SDK versions Dec 2, 2025
Copilot AI requested a review from dsplaisted December 2, 2025 15:31
/// <summary>
/// Gets the file version of a muxer executable.
/// </summary>
internal static Version? GetMuxerFileVersion(string muxerPath)
Copy link
Member

Choose a reason for hiding this comment

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

@JeremyKuhne Copilot wrote this. Does the logic make sense for getting a file version to find out which version is newer? What's the difference between FileVersionInfo.FileVersion and the FileMajorPart etc. properties?

Copy link
Member

@dsplaisted dsplaisted left a comment

Choose a reason for hiding this comment

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

Please remove the tests you added in MuxerVersionHandlingTests.cs. I don't think unit-level tests of this are going to be very valuable.

Instead, add two tests to LibraryTests that follow the pattern of those tests. One test should install .NET SDK 9.0 and then 10.0, and verify that the file version of the muxer has been updated. The other test should install 10.0 and then 9.0 and verify that the muxer version isn't updated.

- Remove MuxerVersionHandlingTests.cs as requested
- Add two integration tests to LibraryTests:
  - MuxerIsUpdated_WhenInstallingNewerSdk: verifies muxer is upgraded when installing SDK 10 after SDK 9
  - MuxerIsNotDowngraded_WhenInstallingOlderSdk: verifies muxer is not downgraded when installing SDK 9 after SDK 10
- Tests skip on non-Windows platforms since FileVersionInfo doesn't work with ELF binaries

Co-authored-by: dsplaisted <[email protected]>
Copy link
Contributor Author

Copilot AI commented Dec 2, 2025

Please remove the tests you added in MuxerVersionHandlingTests.cs. I don't think unit-level tests of this are going to be very valuable.

Instead, add two tests to LibraryTests that follow the patt...

Done. Removed MuxerVersionHandlingTests.cs and added two integration tests to LibraryTests:

  • MuxerIsUpdated_WhenInstallingNewerSdk: Installs SDK 9.0 then 10.0, verifies muxer version increases
  • MuxerIsNotDowngraded_WhenInstallingOlderSdk: Installs SDK 10.0 then 9.0, verifies muxer version stays the same

Both tests skip on non-Windows since FileVersionInfo doesn't extract version info from ELF binaries.

Commit: 665d370

Copy link
Member

@dsplaisted dsplaisted left a comment

Choose a reason for hiding this comment

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

This logic also needs to work on Linux and Mac, where the file version info is not available.

On those platforms, assume that the version of the muxer corresponds to the version of the runtime included in the archive or the latest runtime installed in the dotnet install root folder. The latest runtime version will be the highest version folder under shared\Microsoft.NETCore.App in the install root.

When this is done, update the tests you added to run on all platforms.

/// </summary>
private void HandleMuxerFromZip(ZipArchiveEntry entry, string muxerTargetPath)
{
var tempMuxerPath = Path.Combine(Directory.CreateTempSubdirectory().FullName, entry.Name);
Copy link
Member

Choose a reason for hiding this comment

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

It seems like we should move the temp directory creation and extraction to happen if and only if there already exists a muxer, so we avoid this unnecessary work to compare muxer versions.

/// <summary>
/// Handles the muxer from a tar entry, comparing file versions to determine if update is needed.
/// </summary>
private void HandleMuxerFromTar(TarEntry entry, string muxerTargetPath)
Copy link
Member

Choose a reason for hiding this comment

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

Is there a generic Entry class that can be used for TarEntry and ZipEntry? I don't see why this logic needs to be different based on the archive type.

- Update GetMuxerFileVersion to fallback to runtime version detection on non-Windows platforms
- Add GetRuntimeVersionFromArchive to extract runtime version from tar/zip archives
- Add GetLatestRuntimeVersionFromInstallRoot to get highest runtime version from shared/Microsoft.NETCore.App
- Update ShouldUpdateMuxer to accept archive path and install root for runtime version fallback
- Pass archive and install root context through MuxerHandlingConfig
- Remove Windows-only test restrictions - tests now run on all platforms

Co-authored-by: dsplaisted <[email protected]>
Copy link
Contributor Author

Copilot AI commented Dec 2, 2025

This logic also needs to work on Linux and Mac, where the file version info is not available.

On those platforms, assume that the version of the muxer corresponds to the version of the runtime incl...

Done. The muxer version handling now works on all platforms:

On Windows: Uses FileVersionInfo.GetVersionInfo to read the PE file version

On Linux/Mac: Falls back to runtime version detection when FileVersionInfo returns null:

  • For new muxer: extracts runtime version from the archive being installed by scanning shared/Microsoft.NETCore.App/{version} directories
  • For existing muxer: gets the latest runtime version from the install root's shared/Microsoft.NETCore.App directory

Tests updated to run on all platforms without skipping.

Commit: 0c5e201

Copy link
Member

@nagilson nagilson left a comment

Choose a reason for hiding this comment

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

It seems like we should move the temp directory creation and extraction to happen if and only if there already exists a muxer, so we avoid this unnecessary work to compare muxer versions.

Is there a generic Entry class that can be used for TarEntry and ZipEntry? I don't see why this logic needs to be different based on the archive type.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request fixes a critical bug in muxer version handling where SDK versions were incorrectly compared to determine muxer updates. The muxer version is tied to runtime version, not SDK version, so the fix replaces SDK version comparison with actual file version comparison using PE file metadata on Windows and runtime version detection as a fallback on Linux/Mac.

Key Changes

  • Replaced flawed SDK comparison logic with file version comparison using FileVersionInfo.GetVersionInfo on Windows and runtime version detection from archives/install root on Linux/Mac
  • Added robust version detection methods that scan shared/Microsoft.NETCore.App/{version} directories in archives and install roots to determine runtime versions
  • Added comprehensive integration tests to verify muxer is upgraded when installing newer SDKs and not downgraded when installing older SDKs

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs Core fix: removed ConfigureMuxerHandling method and added ShouldUpdateMuxer, GetMuxerFileVersion, and runtime version detection methods; updated tar/zip extraction to use version comparison
test/dotnetup.Tests/LibraryTests.cs Added two integration tests to verify muxer upgrade/no-downgrade behavior when installing SDKs in different orders
test/dotnetup.Tests/Utilities/UpdateChannelExtensions.cs Minor: alphabetized using statements
test/dotnetup.Tests/DnupE2Etest.cs Minor: alphabetized using statements and formatting adjustments
Comments suppressed due to low confidence (1)

test/dotnetup.Tests/LibraryTests.cs:10

  • Missing required using directives for test framework. Add using Xunit; and using FluentAssertions; to support the [Fact] attributes and .Should() assertion methods used in the test methods below.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.Dotnet.Installation;
using Microsoft.Dotnet.Installation.Internal;
using Microsoft.DotNet.Tools.Dotnetup.Tests.Utilities;

Comment on lines 358 to 359
catch
{
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] Silent exception swallowing: The catch block silently suppresses all exceptions without any logging or diagnostic information. Consider adding logging or at least a comment explaining why exceptions are being suppressed. This can make debugging issues with version detection difficult in production.

Suggested change
catch
{
catch (Exception ex)
{
// Swallowing exceptions here because failure to detect a runtime version is not always exceptional,
// but log the exception for diagnostic purposes.
Debug.WriteLine($"Failed to get latest runtime version from install root '{installRoot}': {ex}");

Copilot uses AI. Check for mistakes.
Comment on lines 254 to 261
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.
Comment on lines 151 to 158
if (versionInfo.FileMajorPart > 0 || versionInfo.FileMinorPart > 0)
{
return new Version(
versionInfo.FileMajorPart,
versionInfo.FileMinorPart,
versionInfo.FileBuildPart,
versionInfo.FilePrivatePart);
}
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.

Potentially incorrect fallback condition: The check versionInfo.FileMajorPart > 0 || versionInfo.FileMinorPart > 0 will skip valid version 0.0.x.y. Consider changing the condition to check if any part is non-zero, or if the FileVersion property exists but failed to parse, to handle edge cases more robustly. For example: if (versionInfo.FileMajorPart != 0 || versionInfo.FileMinorPart != 0 || versionInfo.FileBuildPart != 0 || versionInfo.FilePrivatePart != 0)

Copilot uses AI. Check for mistakes.
}
catch
{
// If we can't read the archive, return null
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] Silent exception swallowing: The catch block silently suppresses all exceptions without any logging or diagnostic information. Consider adding logging or at least a comment explaining why exceptions are being suppressed. This can make debugging issues with version detection difficult in production.

Suggested change
// If we can't read the archive, return null
// Intentionally suppressing all exceptions here because failure to read the archive
// should result in a null version (no version found). If debugging issues with version
// detection, consider adding logging here to capture exception details.
// Example: Console.WriteLine($"Failed to read archive '{archivePath}': {ex}");
// (Logging is omitted here to avoid introducing dependencies.)

Copilot uses AI. Check for mistakes.
Comment on lines 169 to 177
catch
{
// On error, try fallback for non-Windows
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && contextPath != null)
{
return GetRuntimeVersionFromContext(contextPath);
}
return null;
}
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] Silent exception swallowing: The catch block silently suppresses all exceptions without any logging or diagnostic information. Consider adding logging or at least a comment explaining why exceptions are being suppressed. This can make debugging issues with version detection difficult in production.

Copilot uses AI. Check for mistakes.
Copy link
Member

@dsplaisted dsplaisted left a comment

Choose a reason for hiding this comment

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

After discussion with @JeremyKuhne, please get rid of the logic to read the version info from the FileVersionInfo. Rather, we should use the same logic on Windows and non-Windows, which is to look at the version of the runtime to get a version of the muxer to compare.

Furthermore, try this, which may speed things up and simplify the logic:

  • First read the version of the existing muxer, if any (by looking at the latest version of the Microsoft.NETCore.App shared runtime)
  • Then, if there is an existing muxer, rename it to something else (add ".tmp" to the filename)
  • Then extract the archive. Since the muxer has been renamed, all of the files of the archive can be extracted directly
  • Then, if there was previously a muxer, read the new latest runtime version, and if it's less than the previously read version, then delete the newly copied muxer and rename the temporary one back.

@dsplaisted
Copy link
Member

I switched back because there was a test failure: when installing SDK 9.0 after SDK 10.0, both runtimes (9.0 and 10.0) are present in the install root after extraction. Using GetLatestRuntimeVersionFromInstallRoot would return 10.0 for both existingMuxerVersion and newMuxerVersion, making them equal, so the 9.0 muxer (which was just extracted) would be kept instead of restoring the 10.0 muxer.

@copilot Thanks for the explanation. You're right that you can't use GetLatestRuntimeVersionFromInstallRoot to get the version of the runtime associated with the muxer that was just extracted if installing an older runtime.

So I think the logic should be this: Check the latest version of the runtime after the extraction. If it's the same as the version before the extraction, then a newer runtime was NOT installed, so the new muxer that was copied over was actually older. In that case delete the new one and rename the temporary one back.

On the other hand, if the version of the latest runtime after extraction is newer, then a newer muxer was installed and the temporary one can be deleted.

Can you make these changes?

- Change logic to compare runtime version AFTER extraction with version BEFORE extraction
- If versions are EQUAL after extraction, a newer runtime was NOT installed, so restore old muxer
- If version INCREASED after extraction, a newer runtime was installed, so keep new muxer
- This correctly handles installing SDK 9.0 after SDK 10.0 (both runtimes present, latest is still 10.0, so restore 10.0 muxer)
- All tests now pass

Co-authored-by: dsplaisted <[email protected]>
Copy link
Contributor Author

Copilot AI commented Dec 5, 2025

I switched back because there was a test failure: when installing SDK 9.0 after SDK 10.0, both runtimes (9.0 and 10.0) are present in the install root after extraction. Using `GetLatestRuntimeVersio...

Done. Changed the logic to compare runtime versions before and after extraction:

  • If latest runtime version AFTER extraction == version BEFORE extraction → older runtime was installed, restore old muxer
  • If latest runtime version AFTER extraction > version BEFORE extraction → newer runtime was installed, keep new muxer

This correctly handles both scenarios:

  • Installing SDK 10.0 then SDK 9.0: Both runtimes present, latest stays 10.0, old muxer restored ✅
  • Installing SDK 9.0 then SDK 10.0: Latest runtime increases from 9.0 to 10.0, new muxer kept ✅

All tests now pass.

Commit: 3a81ab6

muxerHashAfterSdk10.Should().NotBe(muxerHashAfterSdk9, "muxer file should be updated when installing newer SDK");

// On Windows, also verify FileVersion was upgraded
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && fileVersionAfterSdk9 != null && fileVersionAfterSdk10 != null)
Copy link
Member

Choose a reason for hiding this comment

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

Let's assert that on Windows the file version before and after the upgrade should not be null, instead of skipping the test if they are null.

Same thing for the other test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Changed both tests to assert that file versions are not null on Windows instead of skipping the assertions:

  • Added fileVersionAfterSdk9.Should().NotBeNull() and fileVersionAfterSdk10.Should().NotBeNull() assertions
  • Removed the null checks from the conditional (&& fileVersionAfterSdk9 != null && fileVersionAfterSdk10 != null)
  • Now the tests will fail on Windows if file versions can't be read

Commit: 5d66e54

- On Windows, assert that fileVersionAfterSdk9 and fileVersionAfterSdk10 are not null
- Remove null checks from conditional - now asserts fail if versions can't be read on Windows
- Apply to both MuxerIsUpdated_WhenInstallingNewerSdk and MuxerIsNotDowngraded_WhenInstallingOlderSdk tests

Co-authored-by: dsplaisted <[email protected]>
Copilot AI requested a review from dsplaisted December 5, 2025 02:56
@dsplaisted dsplaisted marked this pull request as ready for review December 5, 2025 17:16
{
if (File.Exists(muxerTempPath))
{
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

ReleaseVersion? existingMuxerVersion = existingSdkVersions.Any() ? existingSdkVersions.Max() : (ReleaseVersion?)null;
ReleaseVersion newRuntimeVersion = _resolvedVersion;
bool shouldUpdateMuxer = existingMuxerVersion is null || newRuntimeVersion.CompareTo(existingMuxerVersion) > 0;
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.

}

Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
entry.ExtractToFile(destPath, overwrite: true);
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 is the logic in 'ExtractToFile' with 'overwrite: true'?

We should make sure that this tries to overwrite akin to so:

  1. If the target file to be overwrite is locked, we try to delete the existing file
  2. If the target file cannot be deleted, try to rename the file with a suffix, such as .rm

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ExtractToFile is a method on TarEntry and ZipArchiveEntry from the .NET BCL. With overwrite: true, it attempts to overwrite the target file but doesn't have special logic for locked files or retries.

Adding the suggested retry logic (delete, then rename to .rm) would be an improvement, but it's beyond the scope of this PR which focuses on muxer version handling. The original code already used ExtractToFile(destPath, overwrite: true) - I haven't changed that behavior.

If you'd like to improve the extraction robustness for locked files, that would be a good follow-up PR. For this PR, I've focused on adding RAII cleanup for the muxer temp file specifically.

@nagilson nagilson self-requested a review December 5, 2025 17:58
- Handle UnauthorizedAccessException and IOException when deleting stale .tmp file
- Wrap extraction in try-catch block to restore original muxer on exception
- If extraction fails, restore muxer from .tmp if it exists and target doesn't
- Gracefully degrade to normal extraction if .tmp file can't be deleted
- Ignore cleanup errors in exception handler (original exception takes priority)

Co-authored-by: nagilson <[email protected]>
Copy link
Contributor Author

Copilot AI commented Dec 5, 2025

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • go.microsoft.com
    • Triggering command: /home/REDACTED/work/sdk/sdk/artifacts/bin/redist/Debug/dotnet/dotnet /home/REDACTED/work/sdk/sdk/artifacts/bin/redist/Debug/dotnet/dotnet new search template-does-not-exist -v m --debug:custom-hive /home/REDACTED/work/sdk/sdk/artifacts/tmp/Debug/testing/dotnet-new.IntegrationTests/SharedHomeDirectory/20251205175906389 lysis.resources.chmod chmod mod chmod 644 acollector.deps.json chmod ndor/bin/chmod es.dll chmod t/chmod chmod (dns block)
    • Triggering command: /home/REDACTED/work/sdk/sdk/artifacts/bin/redist/Debug/dotnet/dotnet /home/REDACTED/work/sdk/sdk/artifacts/bin/redist/Debug/dotnet/dotnet new search template-does-not-exist -v minimal --debug:custom-hive /home/REDACTED/work/sdk/sdk/artifacts/tmp/Debug/testing/dotnet-new.IntegrationTests/SharedHomeDirectory/20251205175906389 lysis.resources.chmod chmod (dns block)
    • Triggering command: /home/REDACTED/work/sdk/sdk/artifacts/bin/redist/Debug/dotnet/dotnet /home/REDACTED/work/sdk/sdk/artifacts/bin/redist/Debug/dotnet/dotnet new search template-does-not-exist --verbosity m --debug:custom-hive /home/REDACTED/work/sdk/sdk/artifacts/tmp/Debug/testing/dotnet-new.IntegrationTests/SharedHomeDirectory/20251205175906389 lysis.VisualBasichmod chmod mod chmod 644 acollector.deps.json chmod t/chmod es.dll chmod t/chmod chmod (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Member

@nagilson nagilson left a comment

Choose a reason for hiding this comment

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

@dsplaisted I've realized via testing that upon uninstall using the MSIs, we don't actually downgrade the muxer (dotnet.exe) version.

I tested with .NET 10, having .NET 9 installed on disk. I uninstalled .NET 10 successfully, but the .NET 10 muxer remained, despite .NET 10 otherwise being absent. This makes me concerned about using the folders as a heuristic for the version.

The version information tag in the file itself is also interesting. It's got a major.minor and then a build number. I need to see how that is on Unix. I know this is a flawed metric, but I'm starting to believe it's the most consistent.

Whether it really matters how accurate this code is about muxer replacement is up for debate. I'll have to think about this more tomorrow.

@nagilson
Copy link
Member

nagilson commented Dec 9, 2025

So, I ran a very basic benchmark of the 2 approaches.
https://gist.github.com/nagilson/3dcf7eb92a980ca3ca2c0c84fa534c13

Approach 1 (File version): 10,0,25,52117 @Commit: 50d79701f2d748e965fefb4ba42580368598b9cc
  First attempt: 1.8655 ms
  Average (post-warmup): 0.3168 ms across 100 iterations

Approach 2 (Directory scan): 9.0.307
  First attempt: 1.3020 ms
  Average (post-warmup): 0.1250 ms across 100 iterations

Also, neither approach will be correct. Approach 1 is only available on windows and only gets the build date and a major minor. Approach 2 will be wrong if the folder version is wrong.... I'm ok with approach 2, and we could consider a backup file version check on windows for if the major.minor is higher to not replace it. What do you think?

// Step 4: If there was a previous muxer, compare versions and restore if needed
if (hadExistingMuxer && File.Exists(muxerTempPath))
{
Version? newMuxerVersion = GetLatestRuntimeVersionFromInstallRoot(targetDir);
Copy link
Member

Choose a reason for hiding this comment

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

Whether we are installing the sdk, runtime, aspnetcore runtime, or otherwise, the .zip and .tar always have (zip_root)\shared\Microsoft.NETCore.App(version) with (version) as the runtime version and therefore the implied muxer version. We should be able to leverage that instead of having to rescan the install root.

Copy link
Member

Choose a reason for hiding this comment

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

If I am remembering and understanding correctly what @JeremyKuhne said, it may be faster to extract the archive and read the filesystem afterwards rather than to traverse the directory structure inside of the archive.

A side benefit is that we only need one implementation for getting the version.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, idea is:

  • Figure out what the latest installed version is by looking at the current folder structure
  • Backup the current muxer dotnet.exe -> dotnet.exe.old
  • Extract the new archive
  • See if a newer version folder exists
  • If so, delete the backup, otherwise copy the backup over the new dotnet.exe.

Copy link
Member

Choose a reason for hiding this comment

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

Idea is it is consistent and the archive has the opportunity extract more optimally.

Note that anything done to fiddle with the muxer that was laid down by an .msi might cause grief with .msi installers (repair might undo what we do here).

var muxerConfig = ConfigureMuxerHandling(existingSdkVersions);
string muxerName = DotnetupUtilities.GetDotnetExeName();
string muxerTargetPath = Path.Combine(targetDir, muxerName);
string muxerTempPath = muxerTargetPath + ".tmp";
Copy link
Member

Choose a reason for hiding this comment

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

instead of doing .tmp and then failing in the rare event we can no longer delete the .tmp file, we should generate or use a unique per session hash (note that we already have one of those!)

Copy link
Member

Choose a reason for hiding this comment

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

Where is the unique per session hash?

For now I just added a new GUID to the filename.

{
// If we can't delete the existing temp file, we can't proceed with muxer version handling
// Just extract without version comparison
hadExistingMuxer = false;
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't just assume we're installing latest if we can't delete .tmp, so this code should be removed following the new .tmp name pattern.

{
try
{
File.Move(muxerTempPath, muxerTargetPath);
Copy link
Member

Choose a reason for hiding this comment

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

We should probably utilize finally over this pattern

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure. The logic in the catch statement is only supposed to happen if there is a failure. There is somewhat similar logic at the end of the install, but it compares the versions to figure out which version should be kept. So I'm not sure there's a simple way to factor the code so that it uses the same code in a finally block.

@dsplaisted
Copy link
Member

Also, neither approach will be correct. Approach 1 is only available on windows and only gets the build date and a major minor. Approach 2 will be wrong if the folder version is wrong.... I'm ok with approach 2, and we could consider a backup file version check on windows for if the major.minor is higher to not replace it. What do you think?

I think approach 1 will be correct on Windows (barring multiple versions released in a single day). I think the main issue with approach 2 is what you identified, if you uninstall an SDK the muxer won't be downgraded, but the version check will treat it as if it was. I think that's probably OK though, the impact will be that sometimes the muxer will be downgraded to the version from the latest runtime that's currently installed, rather than something that was previously installed.

I think for now we can stick with approach 2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants