@@ -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 ( )
0 commit comments