Skip to content

Commit a903f93

Browse files
authored
Improve dotnetup Muxer Handling, In-Use Muxer + Perf (#52696)
2 parents 46f95c2 + e542eb4 commit a903f93

16 files changed

+1155
-179
lines changed

src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetArchiveExtractor.cs

Lines changed: 67 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ internal class DotnetArchiveExtractor : IDisposable
2121
private readonly IProgressTarget _progressTarget;
2222
private readonly IArchiveDownloader _archiveDownloader;
2323
private readonly bool _shouldDisposeDownloader;
24+
private MuxerHandler? _muxerHandler;
2425
private string scratchDownloadDirectory;
2526
private string? _archivePath;
2627
private IProgressReporter? _progressReporter;
@@ -102,127 +103,72 @@ private void ExtractArchiveDirectlyToTarget(string archivePath, string targetDir
102103
{
103104
Directory.CreateDirectory(targetDir);
104105

105-
string muxerName = DotnetupUtilities.GetDotnetExeName();
106-
string muxerTargetPath = Path.Combine(targetDir, muxerName);
107-
string muxerTempPath = $"{muxerTargetPath}.{Guid.NewGuid().ToString()}.tmp";
108-
109-
// Step 1: Read the version of the existing muxer (if any) by looking at the latest runtime
110-
Version? existingMuxerVersion = null;
111-
bool hadExistingMuxer = File.Exists(muxerTargetPath);
112-
if (hadExistingMuxer)
106+
// Capture pre-extraction muxer/runtime state right before extraction so
107+
// the snapshot is as accurate as possible (caller holds the mutex here).
108+
if (_muxerHandler is null && _request.InstallRoot.Path is not null)
113109
{
114-
existingMuxerVersion = GetLatestRuntimeVersionFromInstallRoot(targetDir);
110+
_muxerHandler = new MuxerHandler(_request.InstallRoot.Path, _request.Options.RequireMuxerUpdate);
115111
}
116112

117-
// Step 2: If there is an existing muxer, rename it to .tmp
118-
if (hadExistingMuxer)
113+
// Extract everything, redirecting muxer to temp path
114+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
119115
{
120-
File.Move(muxerTargetPath, muxerTempPath);
116+
ExtractTarArchive(archivePath, targetDir, installTask, _muxerHandler);
121117
}
122-
123-
try
124-
{
125-
// Step 3: Extract the archive (all files directly since muxer has been renamed)
126-
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
127-
{
128-
ExtractTarArchive(archivePath, targetDir, installTask);
129-
}
130-
else
131-
{
132-
ExtractZipArchive(archivePath, targetDir, installTask);
133-
}
134-
135-
// Step 4: If there was a previous muxer, compare versions and restore if needed
136-
if (hadExistingMuxer && File.Exists(muxerTempPath))
137-
{
138-
Version? newMuxerVersion = GetLatestRuntimeVersionFromInstallRoot(targetDir);
139-
140-
// If the latest runtime version after extraction is the same as before,
141-
// then a newer runtime was NOT installed, so the new muxer is actually older.
142-
// In that case, restore the old muxer.
143-
if (newMuxerVersion != null && existingMuxerVersion != null && newMuxerVersion == existingMuxerVersion)
144-
{
145-
if (File.Exists(muxerTargetPath))
146-
{
147-
File.Delete(muxerTargetPath);
148-
}
149-
File.Move(muxerTempPath, muxerTargetPath);
150-
}
151-
else
152-
{
153-
// Latest runtime version increased (or we couldn't determine versions) - keep new muxer
154-
if (File.Exists(muxerTempPath))
155-
{
156-
File.Delete(muxerTempPath);
157-
}
158-
}
159-
}
160-
}
161-
catch
118+
else
162119
{
163-
// If an exception occurs during extraction or version comparison, restore the original muxer if it exists
164-
if (hadExistingMuxer && File.Exists(muxerTempPath) && !File.Exists(muxerTargetPath))
165-
{
166-
try
167-
{
168-
File.Move(muxerTempPath, muxerTargetPath);
169-
}
170-
catch
171-
{
172-
// Ignore errors during cleanup - the original exception is more important
173-
}
174-
}
175-
throw;
120+
ExtractZipArchive(archivePath, targetDir, installTask, _muxerHandler);
176121
}
122+
123+
// After extraction, decide whether to keep or discard the temp muxer
124+
_muxerHandler?.FinalizeAfterExtraction();
177125
}
178126

179127
/// <summary>
180-
/// Gets the latest runtime version from the install root by checking the shared/Microsoft.NETCore.App directory.
128+
/// Resolves the destination path for an archive entry, redirecting the muxer to a temp path if needed.
181129
/// </summary>
182-
private static Version? GetLatestRuntimeVersionFromInstallRoot(string installRoot)
130+
/// <param name="entryName">The entry name/path from the archive.</param>
131+
/// <param name="targetDir">The target extraction directory.</param>
132+
/// <param name="muxerHandler">Optional muxer handler for redirecting muxer entries.</param>
133+
/// <returns>The resolved destination path.</returns>
134+
private static string ResolveEntryDestPath(string entryName, string targetDir, MuxerHandler? muxerHandler)
183135
{
184-
var runtimePath = Path.Combine(installRoot, "shared", "Microsoft.NETCore.App");
185-
if (!Directory.Exists(runtimePath))
136+
// Normalize entry name by stripping leading "./" prefix (common in tar archives)
137+
string normalizedName = entryName.StartsWith("./", StringComparison.Ordinal)
138+
? entryName.Substring(2)
139+
: entryName;
140+
141+
if (muxerHandler != null && normalizedName == muxerHandler.MuxerEntryName)
186142
{
187-
return null;
143+
muxerHandler.MuxerWasExtracted = true;
144+
return muxerHandler.TempMuxerPath;
188145
}
189146

190-
Version? highestVersion = null;
191-
foreach (var dir in Directory.GetDirectories(runtimePath))
147+
return Path.Combine(targetDir, entryName);
148+
}
149+
150+
/// <summary>
151+
/// Initializes progress tracking for extraction by setting the max value.
152+
/// </summary>
153+
private static void InitializeExtractionProgress(IProgressTask? installTask, long totalEntries)
154+
{
155+
if (installTask is not null)
192156
{
193-
var versionString = Path.GetFileName(dir);
194-
if (Version.TryParse(versionString, out Version? version))
195-
{
196-
if (highestVersion == null || version > highestVersion)
197-
{
198-
highestVersion = version;
199-
}
200-
}
157+
installTask.MaxValue = totalEntries > 0 ? totalEntries : 1;
201158
}
202-
203-
return highestVersion;
204159
}
205160

206161
/// <summary>
207162
/// Extracts a tar or tar.gz archive to the target directory.
208163
/// </summary>
209-
private void ExtractTarArchive(string archivePath, string targetDir, IProgressTask? installTask)
164+
private void ExtractTarArchive(string archivePath, string targetDir, IProgressTask? installTask, MuxerHandler? muxerHandler = null)
210165
{
211166
string decompressedPath = DecompressTarGzIfNeeded(archivePath, out bool needsDecompression);
212167

213168
try
214169
{
215-
// Count files in tar for progress reporting
216-
long totalFiles = CountTarEntries(decompressedPath);
217-
218-
// Set progress maximum
219-
if (installTask is not null)
220-
{
221-
installTask.MaxValue = totalFiles > 0 ? totalFiles : 1;
222-
}
223-
224-
// Extract files directly to target
225-
ExtractTarContents(decompressedPath, targetDir, installTask);
170+
InitializeExtractionProgress(installTask, CountTarEntries(decompressedPath));
171+
ExtractTarContents(decompressedPath, targetDir, installTask, muxerHandler);
226172
}
227173
finally
228174
{
@@ -272,8 +218,9 @@ private long CountTarEntries(string tarPath)
272218

273219
/// <summary>
274220
/// Extracts the contents of a tar file to the target directory.
221+
/// Exposed as internal static for testing.
275222
/// </summary>
276-
private void ExtractTarContents(string tarPath, string targetDir, IProgressTask? installTask)
223+
internal static void ExtractTarContents(string tarPath, string targetDir, IProgressTask? installTask, MuxerHandler? muxerHandler = null)
277224
{
278225
using var tarStream = File.OpenRead(tarPath);
279226
var tarReader = new TarReader(tarStream);
@@ -283,89 +230,50 @@ private void ExtractTarContents(string tarPath, string targetDir, IProgressTask?
283230
{
284231
if (entry.EntryType == TarEntryType.RegularFile)
285232
{
286-
ExtractTarFileEntry(entry, targetDir, installTask);
233+
string destPath = ResolveEntryDestPath(entry.Name, targetDir, muxerHandler);
234+
Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
235+
// ExtractToFile handles Unix permissions automatically via entry.Mode
236+
entry.ExtractToFile(destPath, overwrite: true);
287237
}
288238
else if (entry.EntryType == TarEntryType.Directory)
289239
{
290-
// Create directory if it doesn't exist
291-
var dirPath = Path.Combine(targetDir, entry.Name);
240+
string dirPath = Path.Combine(targetDir, entry.Name);
292241
Directory.CreateDirectory(dirPath);
293-
installTask?.Value += 1;
294-
}
295-
else
296-
{
297-
// Skip other entry types
298-
installTask?.Value += 1;
299-
}
300-
}
301-
}
302242

303-
/// <summary>
304-
/// Extracts a single file entry from a tar archive.
305-
/// </summary>
306-
private void ExtractTarFileEntry(TarEntry entry, string targetDir, IProgressTask? installTask)
307-
{
308-
var destPath = Path.Combine(targetDir, entry.Name);
309-
Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
310-
using var outStream = File.Create(destPath);
311-
entry.DataStream?.CopyTo(outStream);
312-
installTask?.Value += 1;
243+
if (entry.Mode != default && !OperatingSystem.IsWindows())
244+
{
245+
File.SetUnixFileMode(dirPath, entry.Mode);
246+
}
247+
}
313248

314-
// On Unix platforms, set the file permissions after extraction
315-
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
316-
{
317-
File.SetUnixFileMode(destPath, entry.Mode);
249+
installTask?.Value += 1;
318250
}
319-
320251
}
321252

322253
/// <summary>
323254
/// Extracts a zip archive to the target directory.
324255
/// </summary>
325-
private void ExtractZipArchive(string archivePath, string targetDir, IProgressTask? installTask)
256+
private void ExtractZipArchive(string archivePath, string targetDir, IProgressTask? installTask, MuxerHandler? muxerHandler = null)
326257
{
327-
long totalFiles = CountZipEntries(archivePath);
328-
329-
if (installTask is not null)
330-
{
331-
installTask.MaxValue = totalFiles > 0 ? totalFiles : 1;
332-
}
333-
334258
using var zip = ZipFile.OpenRead(archivePath);
259+
InitializeExtractionProgress(installTask, zip.Entries.Count);
260+
335261
foreach (var entry in zip.Entries)
336262
{
337-
ExtractZipEntry(entry, targetDir, installTask);
338-
}
339-
}
340-
341-
/// <summary>
342-
/// Counts the number of entries in a zip file for progress reporting.
343-
/// </summary>
344-
private long CountZipEntries(string zipPath)
345-
{
346-
using var zip = ZipFile.OpenRead(zipPath);
347-
return zip.Entries.Count;
348-
}
349-
350-
/// <summary>
351-
/// Extracts a single entry from a zip archive.
352-
/// </summary>
353-
private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, IProgressTask? installTask)
354-
{
355-
var fileName = Path.GetFileName(entry.FullName);
356-
var destPath = Path.Combine(targetDir, entry.FullName);
263+
// Directory entries have no file name
264+
if (string.IsNullOrEmpty(Path.GetFileName(entry.FullName)))
265+
{
266+
Directory.CreateDirectory(Path.Combine(targetDir, entry.FullName));
267+
}
268+
else
269+
{
270+
string destPath = ResolveEntryDestPath(entry.FullName, targetDir, muxerHandler);
271+
Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
272+
entry.ExtractToFile(destPath, overwrite: true);
273+
}
357274

358-
// Skip directories (we'll create them for files as needed)
359-
if (string.IsNullOrEmpty(fileName))
360-
{
361-
Directory.CreateDirectory(destPath);
362275
installTask?.Value += 1;
363-
return;
364276
}
365-
366-
Directory.CreateDirectory(Path.GetDirectoryName(destPath)!);
367-
entry.ExtractToFile(destPath, overwrite: true);
368-
installTask?.Value += 1;
369277
}
370278

371279
public void Dispose()

src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetInstall.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,10 @@ internal record InstallRequestOptions()
2929
{
3030
// Include options such as the custom feed, manifest path, etc.
3131
public string? ManifestPath { get; init; }
32+
33+
/// <summary>
34+
/// If true, the installation will fail if the muxer (dotnet executable) cannot be updated.
35+
/// If false (default), a warning is displayed but installation continues.
36+
/// </summary>
37+
public bool RequireMuxerUpdate { get; init; }
3238
}

src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetupUtilities.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.IO;
78
using System.Linq;
89
using System.Runtime.InteropServices;
@@ -77,4 +78,33 @@ public static string GetArchiveFileExtensionForPlatform()
7778
return ".tar.gz"; // Unix-like systems use tar.gz
7879
}
7980
}
81+
82+
/// <summary>
83+
/// Attempts to find running dotnet processes and returns a formatted string with their PIDs.
84+
/// Returns an empty string if no processes are found or an error occurs.
85+
/// </summary>
86+
public static string GetDotnetProcessPidInfo()
87+
{
88+
try
89+
{
90+
var processes = Process.GetProcessesByName("dotnet");
91+
if (processes.Length == 0)
92+
{
93+
return string.Empty;
94+
}
95+
96+
var pids = new int[processes.Length];
97+
for (int i = 0; i < processes.Length; i++)
98+
{
99+
pids[i] = processes[i].Id;
100+
processes[i].Dispose();
101+
}
102+
103+
return $" (dotnet process PIDs: {string.Join(", ", pids)})";
104+
}
105+
catch
106+
{
107+
return string.Empty;
108+
}
109+
}
80110
}

0 commit comments

Comments
 (0)