diff --git a/MinecraftLaunch/Components/Installer/Modpack/CurseforgeModpackInstaller.cs b/MinecraftLaunch/Components/Installer/Modpack/CurseforgeModpackInstaller.cs index e321c0b..2ed5c7f 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/CurseforgeModpackInstaller.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/CurseforgeModpackInstaller.cs @@ -193,36 +193,25 @@ private async Task DownloadModsAsync(IEnumerable asyncUrls, Cancellation } private async Task ExtractModpackAsync(CancellationToken cancellationToken) { - var zipArchive = ZipFile.OpenRead(ModpackPath); - var entries = zipArchive?.Entries; - ReportProgress(InstallStep.ExtractModpack, 0.85d, TaskStatus.Running, entries.Count, 0); - - int count = 0; - var tasks = entries.Select(x => Task.Run(() => { - lock (zipArchive) { - ReportProgress(InstallStep.ExtractModpack, - ((double)Interlocked.Increment(ref count) / (double)entries.Count).ToPercentage(0.85d, 1.0d), - TaskStatus.Running, entries.Count, count); - - if (!Entry.IsOverride || - !x.FullName.StartsWith(Entry.Overrides, StringComparison.OrdinalIgnoreCase)) return; - - var subPath = x.FullName[(Entry.Overrides.Length + 1)..]; - if (string.IsNullOrEmpty(subPath)) - return; - - var filePath = new FileInfo(Path.Combine(Path.GetFullPath(Minecraft.ToWorkingPath(true)), subPath)); - if (x.FullName.EndsWith('/')) { - Directory.CreateDirectory(filePath.FullName); - return; - } - - x.ExtractTo(filePath.FullName); - } - }, cancellationToken)); - - await Task.WhenAll(tasks); - zipArchive.Dispose(); + + ReportProgress(InstallStep.ExtractModpack, 0.85d, TaskStatus.Running, 0, 0); // 此处未开始解析,返回0 + + var count = 0; + await ModPackUtils.ExtractSingleThreadAsync( + srcZipPath: ModpackPath, + overridesPrefix: Entry.Overrides, + independentAndFullWorkingPath: Minecraft.ToWorkingPath(true), + whenEachEntryCompleted: ReportEntryExtractingProgress, + cancellationToken: cancellationToken); + return; + + void ReportEntryExtractingProgress(ZipArchive zipArchive) => + ReportProgress( + step: InstallStep.ExtractModpack, + progress: (Interlocked.Increment(ref count) / (double)zipArchive.Entries.Count).ToPercentage(0.85d, 1.0d), + status: TaskStatus.Running, + totalCount: zipArchive.Entries.Count, + finshedCount: count); } #endregion diff --git a/MinecraftLaunch/Components/Installer/Modpack/McbbsModpackInstaller.cs b/MinecraftLaunch/Components/Installer/Modpack/McbbsModpackInstaller.cs index d1f754b..e5f05bb 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/McbbsModpackInstaller.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/McbbsModpackInstaller.cs @@ -75,39 +75,28 @@ public override async Task InstallAsync(CancellationToken cancel #region Privates private async Task ExtractModpackAsync(CancellationToken cancellationToken) { - var zipArchive = ZipFile.OpenRead(ModpackPath); - var entries = zipArchive?.Entries; - ReportProgress(InstallStep.ExtractModpack, 0.10d, TaskStatus.Running, entries.Count, 0); + + ReportProgress(InstallStep.ExtractModpack, 0.85d, TaskStatus.Running, 0, 0); // 此处未开始解析,返回0 const string decompressPrefix = "overrides"; - string woringPath = Minecraft.ToWorkingPath(true); - - int count = 0; - var tasks = entries.Select(x => Task.Run(() => { - lock (zipArchive) { - ReportProgress(InstallStep.ExtractModpack, - ((double)Interlocked.Increment(ref count) / (double)entries.Count).ToPercentage(0.1d, 1.0d), - TaskStatus.Running, entries.Count, count); - - if (!x.FullName.StartsWith(decompressPrefix)) - return; - - var subPath = x.FullName[(decompressPrefix.Length + 1)..]; - if (string.IsNullOrEmpty(subPath)) - return; - - var filePath = new FileInfo(Path.Combine(woringPath, subPath)); - if (x.FullName.EndsWith('/')) { - filePath.Directory.Create(); - return; - } - - x.ExtractTo(filePath.FullName); - } - }, cancellationToken)); - - await Task.WhenAll(tasks); - zipArchive.Dispose(); + + var count = 0; + await ModPackUtils.ExtractSingleThreadAsync( + srcZipPath: ModpackPath, + overridesPrefix: decompressPrefix, + independentAndFullWorkingPath: Minecraft.ToWorkingPath(true), + whenEachEntryCompleted: ReportEntryExtractingProgress, + cancellationToken: cancellationToken); + return; + + + void ReportEntryExtractingProgress(ZipArchive zipArchive) => + ReportProgress( + step: InstallStep.ExtractModpack, + progress: (Interlocked.Increment(ref count) / (double)zipArchive.Entries.Count).ToPercentage(0.85d, 1.0d), + status: TaskStatus.Running, + totalCount: zipArchive.Entries.Count, + finshedCount: count); } #endregion diff --git a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs new file mode 100644 index 0000000..c2c1694 --- /dev/null +++ b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs @@ -0,0 +1,74 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO.Compression; +using System.Net.Sockets; +using MinecraftLaunch.Extensions; + +namespace MinecraftLaunch.Components.Installer.Modpack; + +internal static class ModPackUtils +{ + public const char ZipPathSeparator = '/'; + + + public static async Task ExtractSingleThreadAsync( + string srcZipPath, + string overridesPrefix, + string independentAndFullWorkingPath, + /*执行线程不保证*/Action whenEachEntryCompleted = null, + CancellationToken cancellationToken = default) + { + Debug.Assert(srcZipPath is not null || overridesPrefix is not null || independentAndFullWorkingPath is not null); + cancellationToken.ThrowIfCancellationRequested(); + using var zip = ZipFile.OpenRead(srcZipPath); + foreach (var item in zip.Entries) + { + cancellationToken.ThrowIfCancellationRequested(); + // 排除非 /文件 + if (!item.FullName.StartsWith(overridesPrefix, StringComparison.OrdinalIgnoreCase)) continue; + var targetPath = Path.Combine( + independentAndFullWorkingPath, + RemoveOverridesPrefix(item.FullName, overridesPrefix)); + if (item.FullName.EndsWith(ZipPathSeparator)) + { + Directory.CreateDirectory(targetPath); + continue; + } + if (!IsShouldExtract(item, overridesPrefix)) continue; + await item.ExtractToFileAsync( + targetPath, + overwrite:true,cancellationToken + ).ConfigureAwait(false); + whenEachEntryCompleted?.Invoke(zip); + } + } + + #region Util + + private static bool IsShouldExtract( + ZipArchiveEntry entry, + string overridesPrefix) + { + // 排除目录 + if (entry.FullName.EndsWith(ZipPathSeparator)) return false; + // 排除非 /文件 + if (!entry.FullName.StartsWith(overridesPrefix, StringComparison.OrdinalIgnoreCase)) return false; + return true; + } + + // 修正路径 + private static string RemoveOverridesPrefix( + string source, + string overridesPrefix) + { + if (overridesPrefix.EndsWith('/')) + { + return source[overridesPrefix.Length..]; + } + + // 补充/ + return source[(overridesPrefix.Length + 1)..]; + } + + #endregion +} diff --git a/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs b/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs index dff6125..efc36e0 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs @@ -25,18 +25,18 @@ public static ModrinthModpackInstallEntry ParseModpackInstallEntry(string modpac } public static async Task ParseModLoaderEntryAsync(ModrinthModpackInstallEntry modpack, CancellationToken cancellationToken = default) { - if (modpack.Dependencies.ContainsKey("fabric-loader")) + if (modpack.Dependencies.TryGetValue("fabric-loader", out var modpackDependency1)) return (await FabricInstaller.EnumerableFabricAsync(modpack.McVersion, cancellationToken: cancellationToken)) - .First(x => x.BuildVersion.Equals(modpack.Dependencies["fabric-loader"])); - else if (modpack.Dependencies.ContainsKey("quilt-loader")) + .First(x => x.BuildVersion.Equals(modpackDependency1)); + else if (modpack.Dependencies.TryGetValue("quilt-loader", out var dependency1)) return (await QuiltInstaller.EnumerableQuiltAsync(modpack.McVersion, cancellationToken)) - .First(x => x.BuildVersion.Equals(modpack.Dependencies["quilt-loader"])); - else if (modpack.Dependencies.ContainsKey("forge")) + .First(x => x.BuildVersion.Equals(dependency1)); + else if (modpack.Dependencies.TryGetValue("forge", out var modpackDependency)) return (await ForgeInstaller.EnumerableForgeAsync(modpack.McVersion, false, cancellationToken)) - .First(x => x.ForgeVersion.Equals(modpack.Dependencies["forge"])); - else if (modpack.Dependencies.ContainsKey("neoforge")) + .First(x => x.ForgeVersion.Equals(modpackDependency)); + else if (modpack.Dependencies.TryGetValue("neoforge", out var dependency)) return (await ForgeInstaller.EnumerableForgeAsync(modpack.McVersion, true, cancellationToken)) - .First(x => x.ForgeVersion.Equals(modpack.Dependencies["neoforge"])); + .First(x => x.ForgeVersion.Equals(dependency)); else throw new NotSupportedException(); } @@ -70,30 +70,37 @@ public override async Task InstallAsync(CancellationToken cancel #region Privates - private IEnumerable ParseModFiles(CancellationToken cancellationToken) { - int totalCount = Entry.Files.Count(); - ReportProgress(InstallStep.ParseDownloadUrls, 0.1d, TaskStatus.Running, totalCount, 0); - - int count = 0; - string versionPath = Minecraft.ToWorkingPath(true); - foreach (var file in Entry.Files.AsParallel()) { + private IEnumerable ParseModFiles(CancellationToken cancellationToken) + { + const double minProgress = 0.1d; + const double maxProgress = 0.45d; + var fileArray = Entry.Files.ToArray(); + var constTotalCount = fileArray.Length; + ReportProgress( + step: InstallStep.ParseDownloadUrls, + progress: 0.1d, + status: TaskStatus.Running, + totalCount: constTotalCount, + finshedCount: 0); + double count = 0; + var versionPath = Minecraft.ToWorkingPath(true); + //不对Parallel进行Foreach,直接不Parallel + return fileArray.Select(fileItem => + { cancellationToken.ThrowIfCancellationRequested(); - - lock (Entry) { - double progress = (double)Interlocked.Increment(ref count) / (double)totalCount; - ReportProgress(InstallStep.ParseDownloadUrls, progress.ToPercentage(0.1d, 0.45d), - TaskStatus.Running, totalCount, count); - } - - if (!file.Downloads.Any()) - continue; - - if (string.IsNullOrEmpty(file.Path)) - continue; - - var filePath = Path.Combine(versionPath, file.Path); - yield return new DownloadRequest(file.Downloads.First(), filePath); - } + // 非多线程且同个闭包可见性好,无需原子操作 + ReportProgress( + step: InstallStep.ParseDownloadUrls, + // ReSharper disable once AccessToModifiedClosure + progress: (++count / constTotalCount).ToPercentage(minProgress, maxProgress), + status: TaskStatus.Running, + totalCount: constTotalCount, + finshedCount: 0); + if (!fileItem.Downloads.Any()) return null; + if (string.IsNullOrEmpty(fileItem.Path)) return null; + var filePath = Path.Combine(versionPath, fileItem.Path); + return new DownloadRequest(fileItem.Downloads.First(), filePath); + }).Where(static x => x is not null); } private Task DownloadModsAsync(IEnumerable downloadRequests, CancellationToken cancellationToken) { @@ -111,39 +118,28 @@ private Task DownloadModsAsync(IEnumerable } private async Task ExtractModpackAsync(CancellationToken cancellationToken) { - var zipArchive = ZipFile.OpenRead(ModpackPath); - var entries = zipArchive?.Entries; - ReportProgress(InstallStep.ExtractModpack, 0.85d, TaskStatus.Running, entries.Count, 0); + + ReportProgress(InstallStep.ExtractModpack, 0.85d, TaskStatus.Running, 0, 0); // 此处未开始解析,返回0 const string decompressPrefix = "overrides"; - string woringPath = Minecraft.ToWorkingPath(true); - - int count = 0; - var tasks = entries.Select(x => Task.Run(() => { - lock (zipArchive) { - ReportProgress(InstallStep.ExtractModpack, - ((double)Interlocked.Increment(ref count) / (double)entries.Count).ToPercentage(0.85d, 1.0d), - TaskStatus.Running, entries.Count, count); - - if (!x.FullName.StartsWith(decompressPrefix)) - return; - - var subPath = x.FullName[(decompressPrefix.Length + 1)..]; - if (string.IsNullOrEmpty(subPath)) - return; - - var filePath = new FileInfo(Path.Combine(woringPath, subPath)); - if (x.FullName.EndsWith('/')) { - filePath.Directory.Create(); - return; - } - - x.ExtractTo(filePath.FullName); - } - }, cancellationToken)); - - await Task.WhenAll(tasks); - zipArchive.Dispose(); + + var count = 0; + await ModPackUtils.ExtractSingleThreadAsync( + srcZipPath: ModpackPath, + overridesPrefix: decompressPrefix, + independentAndFullWorkingPath: Minecraft.ToWorkingPath(true), + whenEachEntryCompleted: ReportEntryExtractingProgress, + cancellationToken: cancellationToken); + return; + + + void ReportEntryExtractingProgress(ZipArchive zipArchive) => + ReportProgress( + step: InstallStep.ExtractModpack, + progress: (Interlocked.Increment(ref count) / (double)zipArchive.Entries.Count).ToPercentage(0.85d, 1.0d), + status: TaskStatus.Running, + totalCount: zipArchive.Entries.Count, + finshedCount: count); } #endregion diff --git a/MinecraftLaunch/Extensions/ZipArchiveExtension.cs b/MinecraftLaunch/Extensions/ZipArchiveExtension.cs index 181a846..fdb8851 100644 --- a/MinecraftLaunch/Extensions/ZipArchiveExtension.cs +++ b/MinecraftLaunch/Extensions/ZipArchiveExtension.cs @@ -19,4 +19,48 @@ public static void ExtractTo(this ZipArchiveEntry zipArchiveEntry, string destin zipArchiveEntry.ExtractToFile(destinationFile, true); } + + // Licensed to the .NET Foundation under one or more agreements. + // The .NET Foundation licenses this file to you under the MIT license. + // -> The .NET Foundation licenses this function to us under the MIT license. + // 使用了.net标准库代码 + private static void ExtractToFileInitialize(ZipArchiveEntry source, string destinationFileName, bool overwrite, out FileStreamOptions fileStreamOptions) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(destinationFileName); + + fileStreamOptions = new() + { + Access = FileAccess.Write, + Mode = overwrite ? FileMode.Create : FileMode.CreateNew, + Share = FileShare.None, + BufferSize = 16384 + }; + + const UnixFileMode OwnershipPermissions = + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute; + + // Restore Unix permissions. + // For security, limit to ownership permissions, and respect umask (through UnixCreateMode). + // We don't apply UnixFileMode.None because .zip files created on Windows and .zip files created + // with previous versions of .NET don't include permissions. + UnixFileMode mode = (UnixFileMode)(source.ExternalAttributes >> 16) & OwnershipPermissions; + if (mode != UnixFileMode.None && !OperatingSystem.IsWindows()) + { + fileStreamOptions.UnixCreateMode = mode; + } + } + // Licensed to the .NET Foundation under one or more agreements. + // The .NET Foundation licenses this file to you under the MIT license. + internal static async Task ExtractToFileAsync(this ZipArchiveEntry zipArchiveEntry, string destinationFile,bool overwrite,CancellationToken cts) + { + cts.ThrowIfCancellationRequested(); + ExtractToFileInitialize(zipArchiveEntry, destinationFile, overwrite, out var fileStreamOptions); + await using var dst = new FileStream(destinationFile, fileStreamOptions); + await using var src = zipArchiveEntry.Open(); // OpenAsync真搬不了吧(),等xilu速速换NET10单目标即可 + await src.CopyToAsync(dst, cts).ConfigureAwait(false); + File.SetLastWriteTime(destinationFile,DateTime.Now); + } } \ No newline at end of file