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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal class DotnetArchiveExtractor : IDisposable
private readonly IProgressTarget _progressTarget;
private readonly IArchiveDownloader _archiveDownloader;
private readonly bool _shouldDisposeDownloader;
private MuxerHandler? _muxerHandler;
private string scratchDownloadDirectory;
private string? _archivePath;
private IProgressReporter? _progressReporter;
Expand Down Expand Up @@ -102,127 +103,72 @@ private void ExtractArchiveDirectlyToTarget(string archivePath, string targetDir
{
Directory.CreateDirectory(targetDir);

string muxerName = DotnetupUtilities.GetDotnetExeName();
string muxerTargetPath = Path.Combine(targetDir, muxerName);
string muxerTempPath = $"{muxerTargetPath}.{Guid.NewGuid().ToString()}.tmp";

// Step 1: Read the version of the existing muxer (if any) by looking at the latest runtime
Version? existingMuxerVersion = null;
bool hadExistingMuxer = File.Exists(muxerTargetPath);
if (hadExistingMuxer)
// Capture pre-extraction muxer/runtime state right before extraction so
// the snapshot is as accurate as possible (caller holds the mutex here).
if (_muxerHandler is null && _request.InstallRoot.Path is not null)
{
existingMuxerVersion = GetLatestRuntimeVersionFromInstallRoot(targetDir);
_muxerHandler = new MuxerHandler(_request.InstallRoot.Path, _request.Options.RequireMuxerUpdate);
}

// Step 2: If there is an existing muxer, rename it to .tmp
if (hadExistingMuxer)
// Extract everything, redirecting muxer to temp path
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
File.Move(muxerTargetPath, muxerTempPath);
ExtractTarArchive(archivePath, targetDir, installTask, _muxerHandler);
}

try
{
// Step 3: Extract the archive (all files directly since muxer has been renamed)
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
ExtractTarArchive(archivePath, targetDir, installTask);
}
else
{
ExtractZipArchive(archivePath, targetDir, installTask);
}

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

// If the latest runtime version after extraction is the same as before,
// then a newer runtime was NOT installed, so the new muxer is actually older.
// In that case, restore the old muxer.
if (newMuxerVersion != null && existingMuxerVersion != null && newMuxerVersion == existingMuxerVersion)
{
if (File.Exists(muxerTargetPath))
{
File.Delete(muxerTargetPath);
}
File.Move(muxerTempPath, muxerTargetPath);
}
else
{
// Latest runtime version increased (or we couldn't determine versions) - keep new muxer
if (File.Exists(muxerTempPath))
{
File.Delete(muxerTempPath);
}
}
}
}
catch
else
{
// If an exception occurs during extraction or version comparison, restore the original muxer if it exists
if (hadExistingMuxer && File.Exists(muxerTempPath) && !File.Exists(muxerTargetPath))
{
try
{
File.Move(muxerTempPath, muxerTargetPath);
}
catch
{
// Ignore errors during cleanup - the original exception is more important
}
}
throw;
ExtractZipArchive(archivePath, targetDir, installTask, _muxerHandler);
}

// After extraction, decide whether to keep or discard the temp muxer
_muxerHandler?.FinalizeAfterExtraction();
}

/// <summary>
/// Gets the latest runtime version from the install root by checking the shared/Microsoft.NETCore.App directory.
/// Resolves the destination path for an archive entry, redirecting the muxer to a temp path if needed.
/// </summary>
private static Version? GetLatestRuntimeVersionFromInstallRoot(string installRoot)
/// <param name="entryName">The entry name/path from the archive.</param>
/// <param name="targetDir">The target extraction directory.</param>
/// <param name="muxerHandler">Optional muxer handler for redirecting muxer entries.</param>
/// <returns>The resolved destination path.</returns>
private static string ResolveEntryDestPath(string entryName, string targetDir, MuxerHandler? muxerHandler)
{
var runtimePath = Path.Combine(installRoot, "shared", "Microsoft.NETCore.App");
if (!Directory.Exists(runtimePath))
// Normalize entry name by stripping leading "./" prefix (common in tar archives)
string normalizedName = entryName.StartsWith("./", StringComparison.Ordinal)
? entryName.Substring(2)
: entryName;

if (muxerHandler != null && normalizedName == muxerHandler.MuxerEntryName)
{
return null;
muxerHandler.MuxerWasExtracted = true;
return muxerHandler.TempMuxerPath;
}

Version? highestVersion = null;
foreach (var dir in Directory.GetDirectories(runtimePath))
return Path.Combine(targetDir, entryName);
}

/// <summary>
/// Initializes progress tracking for extraction by setting the max value.
/// </summary>
private static void InitializeExtractionProgress(IProgressTask? installTask, long totalEntries)
{
if (installTask is not null)
{
var versionString = Path.GetFileName(dir);
if (Version.TryParse(versionString, out Version? version))
{
if (highestVersion == null || version > highestVersion)
{
highestVersion = version;
}
}
installTask.MaxValue = totalEntries > 0 ? totalEntries : 1;
}

return highestVersion;
}

/// <summary>
/// Extracts a tar or tar.gz archive to the target directory.
/// </summary>
private void ExtractTarArchive(string archivePath, string targetDir, IProgressTask? installTask)
private void ExtractTarArchive(string archivePath, string targetDir, IProgressTask? installTask, MuxerHandler? muxerHandler = null)
{
string decompressedPath = DecompressTarGzIfNeeded(archivePath, out bool needsDecompression);

try
{
// Count files in tar for progress reporting
long totalFiles = CountTarEntries(decompressedPath);

// Set progress maximum
if (installTask is not null)
{
installTask.MaxValue = totalFiles > 0 ? totalFiles : 1;
}

// Extract files directly to target
ExtractTarContents(decompressedPath, targetDir, installTask);
InitializeExtractionProgress(installTask, CountTarEntries(decompressedPath));
ExtractTarContents(decompressedPath, targetDir, installTask, muxerHandler);
}
finally
{
Expand Down Expand Up @@ -272,8 +218,9 @@ private long CountTarEntries(string tarPath)

/// <summary>
/// Extracts the contents of a tar file to the target directory.
/// Exposed as internal static for testing.
/// </summary>
private void ExtractTarContents(string tarPath, string targetDir, IProgressTask? installTask)
internal static void ExtractTarContents(string tarPath, string targetDir, IProgressTask? installTask, MuxerHandler? muxerHandler = null)
{
using var tarStream = File.OpenRead(tarPath);
var tarReader = new TarReader(tarStream);
Expand All @@ -283,89 +230,50 @@ private void ExtractTarContents(string tarPath, string targetDir, IProgressTask?
{
if (entry.EntryType == TarEntryType.RegularFile)
{
ExtractTarFileEntry(entry, targetDir, installTask);
string destPath = ResolveEntryDestPath(entry.Name, targetDir, muxerHandler);
Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
// ExtractToFile handles Unix permissions automatically via entry.Mode
entry.ExtractToFile(destPath, overwrite: true);
}
else if (entry.EntryType == TarEntryType.Directory)
{
// Create directory if it doesn't exist
var dirPath = Path.Combine(targetDir, entry.Name);
string dirPath = Path.Combine(targetDir, entry.Name);
Directory.CreateDirectory(dirPath);
installTask?.Value += 1;
}
else
{
// Skip other entry types
installTask?.Value += 1;
}
}
}

/// <summary>
/// Extracts a single file entry from a tar archive.
/// </summary>
private void ExtractTarFileEntry(TarEntry entry, string targetDir, IProgressTask? installTask)
{
var destPath = Path.Combine(targetDir, entry.Name);
Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
using var outStream = File.Create(destPath);
entry.DataStream?.CopyTo(outStream);
installTask?.Value += 1;
if (entry.Mode != default && !OperatingSystem.IsWindows())
{
File.SetUnixFileMode(dirPath, entry.Mode);
}
}

// On Unix platforms, set the file permissions after extraction
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
File.SetUnixFileMode(destPath, entry.Mode);
installTask?.Value += 1;
}

}

/// <summary>
/// Extracts a zip archive to the target directory.
/// </summary>
private void ExtractZipArchive(string archivePath, string targetDir, IProgressTask? installTask)
private void ExtractZipArchive(string archivePath, string targetDir, IProgressTask? installTask, MuxerHandler? muxerHandler = null)
{
long totalFiles = CountZipEntries(archivePath);

if (installTask is not null)
{
installTask.MaxValue = totalFiles > 0 ? totalFiles : 1;
}

using var zip = ZipFile.OpenRead(archivePath);
InitializeExtractionProgress(installTask, zip.Entries.Count);

foreach (var entry in zip.Entries)
{
ExtractZipEntry(entry, targetDir, installTask);
}
}

/// <summary>
/// Counts the number of entries in a zip file for progress reporting.
/// </summary>
private long CountZipEntries(string zipPath)
{
using var zip = ZipFile.OpenRead(zipPath);
return zip.Entries.Count;
}

/// <summary>
/// Extracts a single entry from a zip archive.
/// </summary>
private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, IProgressTask? installTask)
{
var fileName = Path.GetFileName(entry.FullName);
var destPath = Path.Combine(targetDir, entry.FullName);
// Directory entries have no file name
if (string.IsNullOrEmpty(Path.GetFileName(entry.FullName)))
{
Directory.CreateDirectory(Path.Combine(targetDir, entry.FullName));
}
else
{
string destPath = ResolveEntryDestPath(entry.FullName, targetDir, muxerHandler);
Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
entry.ExtractToFile(destPath, overwrite: true);
}

// Skip directories (we'll create them for files as needed)
if (string.IsNullOrEmpty(fileName))
{
Directory.CreateDirectory(destPath);
installTask?.Value += 1;
return;
}

Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
entry.ExtractToFile(destPath, overwrite: true);
installTask?.Value += 1;
}

public void Dispose()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,10 @@ internal record InstallRequestOptions()
{
// Include options such as the custom feed, manifest path, etc.
public string? ManifestPath { get; init; }

/// <summary>
/// If true, the installation will fail if the muxer (dotnet executable) cannot be updated.
/// If false (default), a warning is displayed but installation continues.
/// </summary>
public bool RequireMuxerUpdate { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -77,4 +78,33 @@ public static string GetArchiveFileExtensionForPlatform()
return ".tar.gz"; // Unix-like systems use tar.gz
}
}

/// <summary>
/// Attempts to find running dotnet processes and returns a formatted string with their PIDs.
/// Returns an empty string if no processes are found or an error occurs.
/// </summary>
public static string GetDotnetProcessPidInfo()
{
try
{
var processes = Process.GetProcessesByName("dotnet");
if (processes.Length == 0)
{
return string.Empty;
}

var pids = new int[processes.Length];
for (int i = 0; i < processes.Length; i++)
{
pids[i] = processes[i].Id;
processes[i].Dispose();
}

return $" (dotnet process PIDs: {string.Join(", ", pids)})";
}
catch
{
return string.Empty;
}
}
}
Loading