Skip to content

Commit bc8ec58

Browse files
committed
Code review feedback and other fixes
1 parent dd737de commit bc8ec58

File tree

7 files changed

+90
-96
lines changed

7 files changed

+90
-96
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ static IEnumerable<string> GetChannelsForProduct(Product product)
8888
/// Finds the latest fully specified version for a given channel string (major, major.minor, or feature band).
8989
/// </summary>
9090
/// <param name="channel">Channel string (e.g., "9", "9.0", "9.0.1xx", "9.0.103", "lts", "sts", "preview")</param>
91-
/// <param name="mode">InstallMode.SDK or InstallMode.Runtime</param>
91+
/// <param name="component">The component to check (ie SDK or runtime)</param>
9292
/// <returns>Latest fully specified version string, or null if not found</returns>
9393
public ReleaseVersion? GetLatestVersionForChannel(UpdateChannel channel, InstallComponent component)
9494
{
@@ -143,7 +143,7 @@ static IEnumerable<string> GetChannelsForProduct(Product product)
143143

144144
private IEnumerable<Product> GetProductsInMajorOrMajorMinor(IEnumerable<Product> index, int major, int? minor = null)
145145
{
146-
var validProducts = index.Where(p => p.ProductVersion.StartsWith(minor is not null ? $"{major}.{minor}" : $"{major}."));
146+
var validProducts = index.Where(p => minor is not null ? p.ProductVersion.Equals($"{major}.{minor}") : p.ProductVersion.StartsWith($"{major}."));
147147
return validProducts;
148148
}
149149

@@ -161,8 +161,8 @@ private IEnumerable<Product> GetProductsInMajorOrMajorMinor(IEnumerable<Product>
161161
/// Gets the latest version based on support status (LTS or STS).
162162
/// </summary>
163163
/// <param name="index">The product collection to search</param>
164-
/// <param name="isLts">True for LTS (Long-Term Support), false for STS (Standard-Term Support)</param>
165-
/// <param name="mode">InstallComponent.SDK or InstallComponent.Runtime</param>
164+
/// <param name="releaseType">The release type to filter by (LTS or STS)</param>
165+
/// <param name="component">The component to check (ie SDK or runtime)</param>
166166
/// <returns>Latest stable version string matching the support status, or null if none found</returns>
167167
private static ReleaseVersion? GetLatestVersionByReleaseType(IEnumerable<Product> index, ReleaseType releaseType, InstallComponent component)
168168
{
@@ -174,7 +174,7 @@ private IEnumerable<Product> GetProductsInMajorOrMajorMinor(IEnumerable<Product>
174174
/// Gets the latest preview version available.
175175
/// </summary>
176176
/// <param name="index">The product collection to search</param>
177-
/// <param name="mode">InstallComponent.SDK or InstallComponent.Runtime</param>
177+
/// <param name="component">The component to check (ie SDK or runtime)</param>
178178
/// <returns>Latest preview or GoLive version string, or null if none found</returns>
179179
private ReleaseVersion? GetLatestPreviewVersion(IEnumerable<Product> index, InstallComponent component)
180180
{

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

Lines changed: 53 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,33 @@ namespace Microsoft.Dotnet.Installation.Internal;
1919
/// <summary>
2020
/// Handles downloading and parsing .NET release manifests to find the correct installer/archive for a given installation.
2121
/// </summary>
22-
internal class DotnetArchiveDownloader(HttpClient httpClient) : IDisposable
22+
internal class DotnetArchiveDownloader : IDisposable
2323
{
2424
private const int MaxRetryCount = 3;
2525
private const int RetryDelayMilliseconds = 1000;
2626

27-
private readonly HttpClient _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
28-
private ReleaseManifest _releaseManifest = new();
27+
private readonly HttpClient _httpClient;
28+
private readonly bool _shouldDisposeHttpClient;
29+
private ReleaseManifest _releaseManifest;
2930

3031
public DotnetArchiveDownloader()
31-
: this(CreateDefaultHttpClient())
32+
: this(new ReleaseManifest())
3233
{
3334
}
3435

35-
public DotnetArchiveDownloader(ReleaseManifest releaseManifest)
36-
: this(CreateDefaultHttpClient())
36+
public DotnetArchiveDownloader(ReleaseManifest releaseManifest, HttpClient? httpClient = null)
3737
{
3838
_releaseManifest = releaseManifest ?? throw new ArgumentNullException(nameof(releaseManifest));
39+
if (httpClient == null)
40+
{
41+
_httpClient = CreateDefaultHttpClient();
42+
_shouldDisposeHttpClient = true;
43+
}
44+
else
45+
{
46+
_httpClient = httpClient;
47+
_shouldDisposeHttpClient = false;
48+
}
3949
}
4050

4151
/// <summary>
@@ -69,8 +79,7 @@ private static HttpClient CreateDefaultHttpClient()
6979
/// <param name="downloadUrl">The URL to download from</param>
7080
/// <param name="destinationPath">The local path to save the downloaded file</param>
7181
/// <param name="progress">Optional progress reporting</param>
72-
/// <returns>True if download was successful, false otherwise</returns>
73-
protected async Task<bool> DownloadArchiveAsync(string downloadUrl, string destinationPath, IProgress<DownloadProgress>? progress = null)
82+
async Task DownloadArchiveAsync(string downloadUrl, string destinationPath, IProgress<DownloadProgress>? progress = null)
7483
{
7584
// Create temp file path in same directory for atomic move when complete
7685
string tempPath = $"{destinationPath}.download";
@@ -82,15 +91,14 @@ protected async Task<bool> DownloadArchiveAsync(string downloadUrl, string desti
8291
// Ensure the directory exists
8392
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
8493

85-
// Try to get content length for progress reporting
86-
long? totalBytes = await GetContentLengthAsync(downloadUrl);
94+
// Content length for progress reporting
95+
long? totalBytes = null;
8796

8897
// Make the actual download request
8998
using var response = await _httpClient.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
9099
response.EnsureSuccessStatusCode();
91100

92-
// Get the total bytes if we didn't get it before
93-
if (!totalBytes.HasValue && response.Content.Headers.ContentLength.HasValue)
101+
if (response.Content.Headers.ContentLength.HasValue)
94102
{
95103
totalBytes = response.Content.Headers.ContentLength.Value;
96104
}
@@ -102,7 +110,7 @@ protected async Task<bool> DownloadArchiveAsync(string downloadUrl, string desti
102110
long bytesRead = 0;
103111
int read;
104112

105-
var lastProgressReport = DateTime.UtcNow;
113+
var lastProgressReport = DateTime.MinValue;
106114

107115
while ((read = await contentStream.ReadAsync(buffer)) > 0)
108116
{
@@ -133,9 +141,20 @@ protected async Task<bool> DownloadArchiveAsync(string downloadUrl, string desti
133141
}
134142
File.Move(tempPath, destinationPath);
135143

136-
return true;
144+
return;
137145
}
138146
catch (Exception)
147+
{
148+
if (attempt < MaxRetryCount)
149+
{
150+
await Task.Delay(RetryDelayMilliseconds * attempt); // Linear backoff
151+
}
152+
else
153+
{
154+
throw;
155+
}
156+
}
157+
finally
139158
{
140159
// Delete the partial download if it exists
141160
try
@@ -150,35 +169,9 @@ protected async Task<bool> DownloadArchiveAsync(string downloadUrl, string desti
150169
// Ignore cleanup errors
151170
}
152171

153-
if (attempt < MaxRetryCount)
154-
{
155-
await Task.Delay(RetryDelayMilliseconds * attempt); // Exponential backoff
156-
}
157-
else
158-
{
159-
return false;
160-
}
161172
}
162173
}
163174

164-
return false;
165-
}
166-
167-
/// <summary>
168-
/// Gets the content length of a resource.
169-
/// </summary>
170-
private async Task<long?> GetContentLengthAsync(string url)
171-
{
172-
try
173-
{
174-
using var headRequest = new HttpRequestMessage(HttpMethod.Head, url);
175-
using var headResponse = await _httpClient.SendAsync(headRequest);
176-
return headResponse.Content.Headers.ContentLength;
177-
}
178-
catch
179-
{
180-
return null;
181-
}
182175
}
183176

184177
/// <summary>
@@ -187,10 +180,9 @@ protected async Task<bool> DownloadArchiveAsync(string downloadUrl, string desti
187180
/// <param name="downloadUrl">The URL to download from</param>
188181
/// <param name="destinationPath">The local path to save the downloaded file</param>
189182
/// <param name="progress">Optional progress reporting</param>
190-
/// <returns>True if download was successful, false otherwise</returns>
191-
protected bool DownloadArchive(string downloadUrl, string destinationPath, IProgress<DownloadProgress>? progress = null)
183+
void DownloadArchive(string downloadUrl, string destinationPath, IProgress<DownloadProgress>? progress = null)
192184
{
193-
return DownloadArchiveAsync(downloadUrl, destinationPath, progress).GetAwaiter().GetResult();
185+
DownloadArchiveAsync(downloadUrl, destinationPath, progress).GetAwaiter().GetResult();
194186
}
195187

196188
/// <summary>
@@ -200,23 +192,24 @@ protected bool DownloadArchive(string downloadUrl, string destinationPath, IProg
200192
/// <param name="destinationPath">The local path to save the downloaded file</param>
201193
/// <param name="progress">Optional progress reporting</param>
202194
/// <returns>True if download and verification were successful, false otherwise</returns>
203-
public bool DownloadArchiveWithVerification(DotnetInstallRequest installRequest, ReleaseVersion resolvedVersion, string destinationPath, IProgress<DownloadProgress>? progress = null)
195+
public void DownloadArchiveWithVerification(DotnetInstallRequest installRequest, ReleaseVersion resolvedVersion, string destinationPath, IProgress<DownloadProgress>? progress = null)
204196
{
205197
var targetFile = _releaseManifest.FindReleaseFile(installRequest, resolvedVersion);
206198
string? downloadUrl = targetFile?.Address.ToString();
207199
string? expectedHash = targetFile?.Hash.ToString();
208200

209-
if (string.IsNullOrEmpty(expectedHash) || string.IsNullOrEmpty(downloadUrl))
201+
if (string.IsNullOrEmpty(expectedHash))
210202
{
211-
return false;
203+
throw new ArgumentException($"{nameof(expectedHash)} cannot be null or empty");
212204
}
213-
214-
if (!DownloadArchive(downloadUrl, destinationPath, progress))
205+
if (string.IsNullOrEmpty(downloadUrl))
215206
{
216-
return false;
207+
throw new ArgumentException($"{nameof(downloadUrl)} cannot be null or empty");
217208
}
218209

219-
return VerifyFileHash(destinationPath, expectedHash);
210+
DownloadArchive(downloadUrl, destinationPath, progress);
211+
212+
VerifyFileHash(destinationPath, expectedHash);
220213
}
221214

222215

@@ -240,20 +233,25 @@ public static string ComputeFileHash(string filePath)
240233
/// </summary>
241234
/// <param name="filePath">Path to the file to verify</param>
242235
/// <param name="expectedHash">Expected hash value</param>
243-
/// <returns>True if the hash matches, false otherwise</returns>
244-
public static bool VerifyFileHash(string filePath, string expectedHash)
236+
public static void VerifyFileHash(string filePath, string expectedHash)
245237
{
246238
if (string.IsNullOrEmpty(expectedHash))
247239
{
248-
return false;
240+
throw new ArgumentException("Expected hash cannot be null or empty", nameof(expectedHash));
249241
}
250242

251243
string actualHash = ComputeFileHash(filePath);
252-
return string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase);
244+
if (!string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase))
245+
{
246+
throw new Exception($"File hash mismatch. Expected: {expectedHash}, Actual: {actualHash}");
247+
}
253248
}
254249

255250
public void Dispose()
256251
{
257-
_httpClient?.Dispose();
252+
if (_shouldDisposeHttpClient)
253+
{
254+
_httpClient?.Dispose();
255+
}
258256
}
259257
}

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ internal class DotnetArchiveExtractor : IDisposable
1919
private readonly DotnetInstallRequest _request;
2020
private readonly ReleaseVersion _resolvedVersion;
2121
private readonly IProgressTarget _progressTarget;
22-
private readonly ReleaseManifest _releaseManifest;
2322
private string scratchDownloadDirectory;
2423
private string? _archivePath;
2524

@@ -28,26 +27,29 @@ public DotnetArchiveExtractor(DotnetInstallRequest request, ReleaseVersion resol
2827
_request = request;
2928
_resolvedVersion = resolvedVersion;
3029
_progressTarget = progressTarget;
31-
_releaseManifest = new();
3230
scratchDownloadDirectory = Directory.CreateTempSubdirectory().FullName;
3331
}
3432

3533
public void Prepare()
3634
{
3735
using var activity = InstallationActivitySource.ActivitySource.StartActivity("DotnetInstaller.Prepare");
3836

39-
using var archiveDownloader = new DotnetArchiveDownloader(_releaseManifest);
37+
using var archiveDownloader = new DotnetArchiveDownloader();
4038
var archiveName = $"dotnet-{Guid.NewGuid()}";
4139
_archivePath = Path.Combine(scratchDownloadDirectory, archiveName + DnupUtilities.GetArchiveFileExtensionForPlatform());
4240

4341
using (var progressReporter = _progressTarget.CreateProgressReporter())
4442
{
4543
var downloadTask = progressReporter.AddTask($"Downloading .NET SDK {_resolvedVersion}", 100);
4644
var reporter = new DownloadProgressReporter(downloadTask, $"Downloading .NET SDK {_resolvedVersion}");
47-
var downloadSuccess = archiveDownloader.DownloadArchiveWithVerification(_request, _resolvedVersion, _archivePath, reporter);
48-
if (!downloadSuccess)
45+
46+
try
47+
{
48+
archiveDownloader.DownloadArchiveWithVerification(_request, _resolvedVersion, _archivePath, reporter);
49+
}
50+
catch (Exception ex)
4951
{
50-
throw new InvalidOperationException($"Failed to download .NET archive for version {_resolvedVersion}");
52+
throw new Exception($"Failed to download .NET archive for version {_resolvedVersion}", ex);
5153
}
5254

5355
downloadTask.Value = 100;
@@ -242,7 +244,7 @@ private void ExtractTarFileEntry(TarEntry entry, string targetDir, MuxerHandling
242244
private void HandleMuxerUpdateFromTar(TarEntry entry, string muxerTargetPath)
243245
{
244246
// Create a temporary file for the muxer first to avoid locking issues
245-
var tempMuxerPath = Directory.CreateTempSubdirectory().FullName;
247+
var tempMuxerPath = Path.Combine(Directory.CreateTempSubdirectory().FullName, entry.Name);
246248
using (var outStream = File.Create(tempMuxerPath))
247249
{
248250
entry.DataStream?.CopyTo(outStream);
@@ -327,7 +329,7 @@ private void ExtractZipEntry(ZipArchiveEntry entry, string targetDir, MuxerHandl
327329
*/
328330
private void HandleMuxerUpdateFromZip(ZipArchiveEntry entry, string muxerTargetPath)
329331
{
330-
var tempMuxerPath = Directory.CreateTempSubdirectory().FullName;
332+
var tempMuxerPath = Path.Combine(Directory.CreateTempSubdirectory().FullName, entry.Name);
331333
entry.ExtractToFile(tempMuxerPath, overwrite: true);
332334

333335
try

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

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,18 @@ public class ScopedMutex : IDisposable
1515

1616
public ScopedMutex(string name)
1717
{
18-
try
18+
// On Linux and Mac, "Global\" prefix doesn't work - strip it if present
19+
string mutexName = name;
20+
if (Environment.OSVersion.Platform != PlatformID.Win32NT && mutexName.StartsWith("Global\\"))
1921
{
20-
// On Linux and Mac, "Global\" prefix doesn't work - strip it if present
21-
string mutexName = name;
22-
if (Environment.OSVersion.Platform != PlatformID.Win32NT && mutexName.StartsWith("Global\\"))
23-
{
24-
mutexName = mutexName.Substring(7);
25-
}
26-
27-
_mutex = new Mutex(false, mutexName);
28-
_hasHandle = _mutex.WaitOne(TimeSpan.FromSeconds(300), false);
29-
if (_hasHandle)
30-
{
31-
_holdCount.Value = _holdCount.Value + 1;
32-
}
22+
mutexName = mutexName.Substring(7);
3323
}
34-
catch (Exception ex)
24+
25+
_mutex = new Mutex(false, mutexName);
26+
_hasHandle = _mutex.WaitOne(TimeSpan.FromSeconds(300), false);
27+
if (_hasHandle)
3528
{
36-
Console.WriteLine($"Warning: Could not create or acquire mutex '{name}': {ex.Message}");
37-
throw;
29+
_holdCount.Value = _holdCount.Value + 1;
3830
}
3931
}
4032

src/Installer/dnup/DotnetInstallManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public DotnetInstallManager(IEnvironmentProvider? environmentProvider = null)
3636
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
3737
string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
3838
bool isAdminInstall = installDir.StartsWith(Path.Combine(programFiles, "dotnet"), StringComparison.OrdinalIgnoreCase) ||
39-
installDir.StartsWith(Path.Combine(programFilesX86, "dotnet"), StringComparison.OrdinalIgnoreCase); // TODO: This should be improved to not be windows-specific
39+
installDir.StartsWith(Path.Combine(programFilesX86, "dotnet"), StringComparison.OrdinalIgnoreCase); // TODO: This should be improved to not be windows-specific https://github.com/dotnet/sdk/issues/51601
4040

4141
var installRoot = new DotnetInstallRoot(installDir, InstallerUtilities.GetDefaultInstallArchitecture());
4242

0 commit comments

Comments
 (0)