From 8b1572821bb0bf9a4da0bb31ce4a9f9c75ead81a Mon Sep 17 00:00:00 2001 From: LuLu-ling Date: Thu, 14 May 2026 23:41:16 +0800 Subject: [PATCH 01/20] =?UTF-8?q?fix:=20=E5=B0=9D=E8=AF=95=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=AE=9E=E4=BE=8B=E4=B8=8B=E8=BD=BD=E7=9A=84=E5=90=84?= =?UTF-8?q?=E7=A7=8D=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Base/ModBase.cs | 66 +++++++++++++++++-- .../Modules/Minecraft/ModMinecraft.cs | 18 ++--- .../Network/Downloader/FileDownloader.cs | 18 +++++ .../Pages/PageDownload/ModDownloadLib.cs | 14 ++-- 4 files changed, 93 insertions(+), 23 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.cs b/Plain Craft Launcher 2/Modules/Base/ModBase.cs index 732d1b96f..6ac5dc53d 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModBase.cs +++ b/Plain Craft Launcher 2/Modules/Base/ModBase.cs @@ -1219,7 +1219,7 @@ public static byte[] ReadFileBytes(string FilePath, Encoding Encoding = null) FilePath = ExePath + FilePath; if (File.Exists(FilePath)) using (var ReadStream = - new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) // 支持读取使用中的文件 + new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { using (var ms = new MemoryStream()) { @@ -1290,10 +1290,14 @@ public static void WriteFile(string FilePath, string Text, bool Append = false, { writer.Write(Text); } - else - // 直接写入字节 - File.WriteAllBytes(FilePath, - Encoding is null ? new UTF8Encoding(false).GetBytes(Text) : Encoding.GetBytes(Text)); + else + { + // 直接写入字节 + var bytes = Encoding is null ? new UTF8Encoding(false).GetBytes(Text) : Encoding.GetBytes(Text); + var tempPath = FilePath + ".pcltmp." + Environment.TickCount.ToString("X8"); + File.WriteAllBytes(tempPath, bytes); + File.Move(tempPath, FilePath, true); + } } /// @@ -1647,6 +1651,58 @@ public string Check(string LocalPath) } } + /// + /// 等待文件就绪可读,在指定超时时间内轮询检查文件是否存在且内容非空。 + /// + /// 文件路径。 + /// 超时时间(毫秒)。 + public static void WaitForFileReady(string filePath, int timeoutMs = 2000) + { + WaitForFileReady(filePath, timeoutMs, false); + } + + /// + /// 等待文件就绪可读,在指定超时时间内轮询检查文件是否存在且内容非空。 + /// + /// 文件路径。 + /// 超时时间(毫秒)。 + /// 是否要求文件为合法 JSON。 + public static void WaitForFileReady(string filePath, int timeoutMs, bool requireJson) + { + filePath = filePath.Contains(@":\") ? filePath : ExePath + filePath; + var start = Environment.TickCount; + long lastSize = -1; + while (Environment.TickCount - start < timeoutMs) + { + if (File.Exists(filePath)) + { + try + { + var info = new FileInfo(filePath); + var size = info.Length; + if (size <= 0) + continue; + if (!requireJson) + { + if (size == lastSize) + return; + lastSize = size; + } + else + { + var content = ReadFile(filePath); + if (!string.IsNullOrEmpty(content) && content.Trim().StartsWith("{")) + return; + } + } + catch (IOException) + { + } + } + Thread.Sleep(50); + } + } + /// /// 尝试根据后缀名判断文件种类并解压文件,支持 gz 与 zip,会尝试将 Jar 以 zip 方式解压。 /// 会尝试创建,但不会清空目标文件夹。 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs index 714191e8e..2ad4dc49f 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs @@ -826,19 +826,11 @@ bool FastJsonCheck(string Json) // 如果 ReadFile 失败会返回空字符串;这可能是由于文件被临时占用,故延时后重试 if (!FastJsonCheck(_jsonText)) { - if (ModBase.RunInUi()) - { - ModBase.Log("[Minecraft] 实例 JSON 文件为空或有误,由于代码在主线程运行,将不再进行重试", ModBase.LogLevel.Debug); - ModBase.GetJson(_jsonText); // 触发异常 - } - else - { - ModBase.Log($"[Minecraft] 实例 JSON 文件为空或有误,将在 2s 后重试读取({JsonPath})", ModBase.LogLevel.Debug); - Thread.Sleep(2000); - _jsonText = ModBase.ReadFile(JsonPath); - if (!FastJsonCheck(_jsonText)) - ModBase.GetJson(_jsonText); - } // 触发异常 + ModBase.Log($"[Minecraft] 实例 JSON 文件为空或有误,将在 2s 后重试读取({JsonPath})", ModBase.LogLevel.Debug); + Thread.Sleep(2000); + _jsonText = ModBase.ReadFile(JsonPath); + if (!FastJsonCheck(_jsonText)) + ModBase.GetJson(_jsonText); } } diff --git a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs index 54199975c..74d700e32 100644 --- a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs +++ b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs @@ -140,6 +140,24 @@ void UpdateDownloadStat(DownloadProgressChangedEventArgs args) try { await downloader.DownloadFileTaskAsync(url, localPath, cancellationToken).ConfigureAwait(false); + var tempPath = localPath + ModNet.NetDownloadEnd; + if (!File.Exists(localPath) && File.Exists(tempPath)) + { + for (var retry = 0; retry < 5; retry++) + { + try + { + File.Move(tempPath, localPath, true); + break; + } + catch (IOException) + { + Thread.Sleep(100); + } + } + } + if (!File.Exists(localPath)) + throw new IOException($"下载未产生任何文件:{localPath}"); ModBase.Log($"[Download] 下载成功:{localPath}"); } catch (TaskCanceledException ex) diff --git a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs index 5ac74ae1e..a1c21bf58 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs +++ b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs @@ -188,17 +188,18 @@ public static void McDownloadClientCore(string Id, string JsonUrl, NetPreDownloa var loadersLib = new List(); loadersLib.Add(new ModLoader.LoaderTask>("分析原版支持库文件(副加载器)", task => { - Thread.Sleep(50); // 等待 JSON 文件实际写入硬盘(#3710) + var jsonPath = Path.Combine(instanceFolder, instanceName + ".json"); + ModBase.WaitForFileReady(jsonPath, 2000); ModBase.Log("[Download] 开始分析原版支持库文件:" + instanceFolder); if (Conversions.ToBoolean(id == "1.16.5" && Config.Download.FixAuthLib != null)) // 1.16.5 Authlib 修复 try { - var json = ModBase.ReadFile(Path.Combine(instanceFolder, instanceName + ".json")); + var json = ModBase.ReadFile(jsonPath); json = json.Replace("2.1.28/authlib-2.1.28.jar", "2.3.31/authlib-2.3.31.jar") .Replace("com.mojang:authlib:2.1.28", "com.mojang:authlib:2.3.31") .Replace("ad54da276bf59983d02d5ed16fc14541354c71fd", "bbd00ca33b052f73a6312254780fc580d2da3535") .Replace("76328", "87662"); - ModBase.WriteFile(Path.Combine(instanceFolder, instanceName + ".json"), json); + ModBase.WriteFile(jsonPath, json); } catch (Exception ex) { @@ -220,7 +221,7 @@ public static void McDownloadClientCore(string Id, string JsonUrl, NetPreDownloa var loadersAssets = new List(); loadersAssets.Add(new ModLoader.LoaderTask>("分析资源文件索引地址(副加载器)", task => { - Thread.Sleep(50); // 等待 JSON 文件实际写入硬盘 + ModBase.WaitForFileReady(Path.Combine(instanceFolder, instanceName + ".json"), 2000); try { var assetIndex = new ModMinecraft.McInstance(instanceFolder); @@ -1933,6 +1934,7 @@ private static void ForgelikeInjectorLine(string Content, ModLoader.LoaderTask( ForgeType == ModDownload.DlForgelikeEntry.ForgelikeType.Forge ? "安装 Forge(方式 A)" : "安装 " + ForgeType, Task => { + ModBase.WaitForFileReady(InstallerAddress, 3000); var Installer = new ZipArchive(new FileStream(InstallerAddress, FileMode.Open)); try { @@ -2178,6 +2181,7 @@ private static void ForgelikeInjectorLine(string Content, ModLoader.LoaderTask(); LoadersLib.Add(new ModLoader.LoaderTask>("分析原版与 LabyMod 支持库文件(副加载器)", Task => { - Thread.Sleep(50); // 等待 JSON 文件实际写入硬盘(#3710) + ModBase.WaitForFileReady(Path.Combine(VersionFolder, VersionName + ".json"), 2000); ModBase.Log("[Download] 开始分析原版与 LabyMod 支持库文件:" + VersionFolder); Task.Output = ModMinecraft.McLibNetFilesFromInstance(new ModMinecraft.McInstance(VersionFolder)); }) From fa7527fdbfd261f37f2602e2eb07dc933244df39 Mon Sep 17 00:00:00 2001 From: LuLu-ling Date: Fri, 15 May 2026 15:41:46 +0800 Subject: [PATCH 02/20] fix: improve JSON file read retry logic based on UI context --- .../Modules/Minecraft/ModMinecraft.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs index 2ad4dc49f..e2647c20e 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs @@ -826,9 +826,18 @@ bool FastJsonCheck(string Json) // 如果 ReadFile 失败会返回空字符串;这可能是由于文件被临时占用,故延时后重试 if (!FastJsonCheck(_jsonText)) { - ModBase.Log($"[Minecraft] 实例 JSON 文件为空或有误,将在 2s 后重试读取({JsonPath})", ModBase.LogLevel.Debug); - Thread.Sleep(2000); - _jsonText = ModBase.ReadFile(JsonPath); + if (ModBase.RunInUi()) + { + ModBase.Log($"[Minecraft] 实例 JSON 文件为空或有误,将进行短暂重试({JsonPath})", ModBase.LogLevel.Debug); + Thread.Sleep(200); + _jsonText = ModBase.ReadFile(JsonPath); + } + else + { + ModBase.Log($"[Minecraft] 实例 JSON 文件为空或有误,将在 2s 后重试读取({JsonPath})", ModBase.LogLevel.Debug); + Thread.Sleep(2000); + _jsonText = ModBase.ReadFile(JsonPath); + } if (!FastJsonCheck(_jsonText)) ModBase.GetJson(_jsonText); } From 579fe1f76b185d7b7a89ea625cde7aa71b77936f Mon Sep 17 00:00:00 2001 From: LuLu-ling Date: Fri, 15 May 2026 16:46:50 +0800 Subject: [PATCH 03/20] apply --- Plain Craft Launcher 2/Modules/Base/ModBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.cs b/Plain Craft Launcher 2/Modules/Base/ModBase.cs index 6ac5dc53d..7b4200265 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModBase.cs +++ b/Plain Craft Launcher 2/Modules/Base/ModBase.cs @@ -1294,7 +1294,7 @@ public static void WriteFile(string FilePath, string Text, bool Append = false, { // 直接写入字节 var bytes = Encoding is null ? new UTF8Encoding(false).GetBytes(Text) : Encoding.GetBytes(Text); - var tempPath = FilePath + ".pcltmp." + Environment.TickCount.ToString("X8"); + var tempPath = FilePath + ".pcltmp." + Guid.NewGuid().ToString("N"); File.WriteAllBytes(tempPath, bytes); File.Move(tempPath, FilePath, true); } From 6cf76c276d4faa0f74c44e954de7ade945f40660 Mon Sep 17 00:00:00 2001 From: LuLu-ling Date: Fri, 15 May 2026 17:01:23 +0800 Subject: [PATCH 04/20] fix: remove unnecessary timeout from WaitForFileReady calls --- .../Pages/PageDownload/ModDownloadLib.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs index a1c21bf58..fc5271ecc 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs +++ b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs @@ -189,7 +189,7 @@ public static void McDownloadClientCore(string Id, string JsonUrl, NetPreDownloa loadersLib.Add(new ModLoader.LoaderTask>("分析原版支持库文件(副加载器)", task => { var jsonPath = Path.Combine(instanceFolder, instanceName + ".json"); - ModBase.WaitForFileReady(jsonPath, 2000); + ModBase.WaitForFileReady(jsonPath); ModBase.Log("[Download] 开始分析原版支持库文件:" + instanceFolder); if (Conversions.ToBoolean(id == "1.16.5" && Config.Download.FixAuthLib != null)) // 1.16.5 Authlib 修复 try @@ -221,7 +221,7 @@ public static void McDownloadClientCore(string Id, string JsonUrl, NetPreDownloa var loadersAssets = new List(); loadersAssets.Add(new ModLoader.LoaderTask>("分析资源文件索引地址(副加载器)", task => { - ModBase.WaitForFileReady(Path.Combine(instanceFolder, instanceName + ".json"), 2000); + ModBase.WaitForFileReady(Path.Combine(instanceFolder, instanceName + ".json")); try { var assetIndex = new ModMinecraft.McInstance(instanceFolder); @@ -1934,7 +1934,7 @@ private static void ForgelikeInjectorLine(string Content, ModLoader.LoaderTask( ForgeType == ModDownload.DlForgelikeEntry.ForgelikeType.Forge ? "安装 Forge(方式 A)" : "安装 " + ForgeType, Task => { - ModBase.WaitForFileReady(InstallerAddress, 3000); + ModBase.WaitForFileReady(InstallerAddress); var Installer = new ZipArchive(new FileStream(InstallerAddress, FileMode.Open)); try { @@ -2181,7 +2181,7 @@ private static void ForgelikeInjectorLine(string Content, ModLoader.LoaderTask(); LoadersLib.Add(new ModLoader.LoaderTask>("分析原版与 LabyMod 支持库文件(副加载器)", Task => { - ModBase.WaitForFileReady(Path.Combine(VersionFolder, VersionName + ".json"), 2000); + ModBase.WaitForFileReady(Path.Combine(VersionFolder, VersionName + ".json")); ModBase.Log("[Download] 开始分析原版与 LabyMod 支持库文件:" + VersionFolder); Task.Output = ModMinecraft.McLibNetFilesFromInstance(new ModMinecraft.McInstance(VersionFolder)); }) From c8e2cab5a55b4802584e2bb56eb9e4b9f641e35f Mon Sep 17 00:00:00 2001 From: LuLu-ling Date: Sat, 16 May 2026 22:18:53 +0800 Subject: [PATCH 05/20] fix: ensure semaphore release only occurs if entered in WaitForFileReady --- Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs | 2 +- .../Modules/Network/Loaders/LoaderDownload.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs index afa00dec8..7c5d33b79 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs @@ -378,7 +378,7 @@ private static LoaderCombo InstallPackCurseForge(string FileAddress, Zip string QuiltVersion = null; foreach (var Entry in (dynamic)Json["minecraft"]["modLoaders"] ?? Array.Empty()) { - var Id = (Entry["id"] ?? "").ToString().ToLower(); + string Id = (Entry["id"] ?? "").ToString().ToLower(); if (Id.StartsWithF("forge-")) { // Forge 指定 diff --git a/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs b/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs index 8ce06983a..f396fc755 100644 --- a/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs +++ b/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs @@ -76,9 +76,11 @@ private void Run(CancellationToken cancellationToken) using var semaphore = new SemaphoreSlim(GetMaxParallelFiles()); var tasks = Files.Select(async file => { - await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + var entered = false; try { + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + entered = true; await ProcessFileAsync(file, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) @@ -93,7 +95,8 @@ private void Run(CancellationToken cancellationToken) } finally { - semaphore.Release(); + if (entered) + semaphore.Release(); RefreshStat(); } }).ToList(); From 079f84f9e18ec6b0568f19054fdd209d548b14b1 Mon Sep 17 00:00:00 2001 From: LuLu-ling Date: Sun, 17 May 2026 01:30:32 +0800 Subject: [PATCH 06/20] fix: downloader [bigcake] --- .../Network/Downloader/FileDownloader.cs | 23 +++++++++++++------ .../Plain Craft Launcher 2.csproj | 2 +- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs index 74d700e32..87e6e7bf9 100644 --- a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs +++ b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs @@ -100,7 +100,8 @@ private static async Task DownloadSingleAsync(string url, string localPath, bool CustomHttpMessageHandlerFactory = () => SharedHandler }; - var downloader = new DownloadService(configuration); + using var downloader = new DownloadService(configuration); + var tcs = new TaskCompletionSource(); void UpdateDownloadStat(DownloadProgressChangedEventArgs args) { if (trackedFile is null) @@ -128,18 +129,26 @@ void UpdateDownloadStat(DownloadProgressChangedEventArgs args) }; downloader.DownloadProgressChanged += (_, args) => UpdateDownloadStat(args); downloader.ChunkDownloadProgressChanged += (_, args) => UpdateDownloadStat(args); - downloader.DownloadFileCompleted += (_, _) => + downloader.DownloadFileCompleted += (_, args) => { - if (trackedFile is null) - return; + if (trackedFile is not null) + { + trackedFile.Speed = 0; + trackedFile.ActiveThreads = 0; + trackedFile.DownloadedBytes = Math.Max(trackedFile.DownloadedBytes, trackedFile.TotalSize); + } - trackedFile.Speed = 0; - trackedFile.ActiveThreads = 0; - trackedFile.DownloadedBytes = Math.Max(trackedFile.DownloadedBytes, trackedFile.TotalSize); + if (args.Cancelled) + tcs.TrySetCanceled(); + else if (args.Error != null) + tcs.TrySetException(args.Error); + else + tcs.TrySetResult(true); }; try { await downloader.DownloadFileTaskAsync(url, localPath, cancellationToken).ConfigureAwait(false); + await tcs.Task.ConfigureAwait(false); var tempPath = localPath + ModNet.NetDownloadEnd; if (!File.Exists(localPath) && File.Exists(tempPath)) { diff --git a/Plain Craft Launcher 2/Plain Craft Launcher 2.csproj b/Plain Craft Launcher 2/Plain Craft Launcher 2.csproj index 3b75e7987..1b097fb65 100644 --- a/Plain Craft Launcher 2/Plain Craft Launcher 2.csproj +++ b/Plain Craft Launcher 2/Plain Craft Launcher 2.csproj @@ -67,7 +67,7 @@ - + From 5e24bef7c3e7037f4ba8071c15ea4bb720ba746d Mon Sep 17 00:00:00 2001 From: Big-Cake-jpg Date: Sun, 17 May 2026 11:26:56 +0800 Subject: [PATCH 07/20] =?UTF-8?q?fix(downloader):=20=E6=88=96=E8=AE=B8?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=20CurseForge=20CDN=20=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=8B=E8=BD=BD=E8=B6=85=E6=97=B6=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Network/Downloader/FileDownloader.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs index 87e6e7bf9..2b5b9bcee 100644 --- a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs +++ b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs @@ -93,6 +93,7 @@ private static async Task DownloadSingleAsync(string url, string localPath, bool ParallelDownload = perFileThreadLimit > 1, MaximumBytesPerSecond = ModNet.NetTaskSpeedLimitHigh > 0 ? ModNet.NetTaskSpeedLimitHigh : 0, MaxTryAgainOnFailure = 2, + BlockTimeout = 30000, DownloadFileExtension = ModNet.NetDownloadEnd, EnableAutoResumeDownload = false, RequestConfiguration = DownloadRequestFactory.Create(url, useBrowserUserAgent, customUserAgent), From d2adb4affc80a8badb99db3afe2c0c28b112d41a Mon Sep 17 00:00:00 2001 From: Big-Cake-jpg Date: Sun, 17 May 2026 14:01:26 +0800 Subject: [PATCH 08/20] =?UTF-8?q?ref(downloader):=20=E4=BD=BF=E7=94=A8=20C?= =?UTF-8?q?ore=20=E7=9A=84=20HttpClient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Network/Downloader/FileDownloader.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs index 2b5b9bcee..1a06ab736 100644 --- a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs +++ b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs @@ -2,20 +2,13 @@ using System.Net.Http; using System.Threading; using Downloader; +using PCL.Core.IO.Net; using PCL.Core.Utils; namespace PCL.Network; public static class FileDownloader { - private static readonly SocketsHttpHandler SharedHandler = new SocketsHttpHandler - { - MaxConnectionsPerServer = 200, // 允许高并发连接 - PooledConnectionLifetime = TimeSpan.FromMinutes(5), // 连接存活时间 - PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), // 空闲连接保留时间 - AllowAutoRedirect = true - }; - public static Task Download(string url, string localPath, bool useBrowserUserAgent = false, string customUserAgent = "", CancellationToken cancellationToken = default, bool enableParallelChunks = true, DownloadFile? trackedFile = null) @@ -97,8 +90,7 @@ private static async Task DownloadSingleAsync(string url, string localPath, bool DownloadFileExtension = ModNet.NetDownloadEnd, EnableAutoResumeDownload = false, RequestConfiguration = DownloadRequestFactory.Create(url, useBrowserUserAgent, customUserAgent), - // 传入共享的 SocketsHttpHandler,实现连接池复用 - CustomHttpMessageHandlerFactory = () => SharedHandler + CustomHttpClientFactory = () => NetworkService.GetClient() }; using var downloader = new DownloadService(configuration); From 2a6d092b72721e8a9a0141f8d3d748eb29800245 Mon Sep 17 00:00:00 2001 From: Big-Cake-jpg Date: Sun, 17 May 2026 14:47:34 +0800 Subject: [PATCH 09/20] =?UTF-8?q?fix(comp-detail):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=95=B4=E5=90=88=E5=8C=85=E6=96=87=E4=BB=B6=E5=8F=A6=E5=AD=98?= =?UTF-8?q?=E4=B8=BA=E6=97=B6=E7=9A=84=20NRE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was generated by Gemini 3.1 Pro --- .../Comp/PageDownloadCompDetail.xaml.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.cs index 12be37558..0cb8d1516 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.cs +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.cs @@ -234,19 +234,11 @@ public void InstallWorld_Click(MyListItem sender, EventArgs e) public void Save_Click(object sender, EventArgs e) { // 获取点击项关联的文件对象 - // 使用模式匹配 (Pattern Matching) 获取目标 Control/Item - object target = sender switch - { - MyListItem item => item, - Control ctrl => ctrl.Parent, - _ => null - }; - - // 安全地访问 Tag 并转换 var File = sender switch { - MyListItem item => item.Tag as ModComp.CompFile, - Control ctrl => (ctrl.Parent as Control)?.Tag as ModComp.CompFile, + FrameworkElement Element when Element.Tag is ModComp.CompFile CompFile => CompFile, + FrameworkElement Element when Element.Parent is FrameworkElement Parent && Parent.Tag is ModComp.CompFile CompFile => CompFile, + FrameworkElement Element when Element.Parent is FrameworkElement Parent && Parent.Parent is FrameworkElement GrandParent && GrandParent.Tag is ModComp.CompFile CompFile => CompFile, _ => null }; From d6fec620e30c4347d44e97ccdf931270f3aef6c5 Mon Sep 17 00:00:00 2001 From: LuLu-ling Date: Sun, 17 May 2026 16:04:01 +0800 Subject: [PATCH 10/20] fix: enhance file deletion logic with retry mechanism and limit parallel downloads --- .../Network/Downloader/FileDownloader.cs | 23 +++++++++++++++---- .../Modules/Network/Loaders/LoaderDownload.cs | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs index 1a06ab736..13f6eec62 100644 --- a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs +++ b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs @@ -175,9 +175,24 @@ void UpdateDownloadStat(DownloadProgressChangedEventArgs args) private static void CleanupTempFiles(string localPath) { var tempPath = localPath + ModNet.NetDownloadEnd; - if (File.Exists(localPath)) - File.Delete(localPath); - if (File.Exists(tempPath)) - File.Delete(tempPath); + TryDeleteFile(localPath); + TryDeleteFile(tempPath); + } + + private static void TryDeleteFile(string path) + { + for (var retry = 0; retry < 5; retry++) + { + try + { + if (File.Exists(path)) + File.Delete(path); + return; + } + catch (IOException) + { + Thread.Sleep(100); + } + } } } diff --git a/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs b/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs index f396fc755..ee68f6112 100644 --- a/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs +++ b/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs @@ -117,7 +117,7 @@ private void Run(CancellationToken cancellationToken) private int GetMaxParallelFiles() { - return Math.Max(1, Math.Min(Files.Count, Math.Clamp(ModNet.NetTaskThreadLimit, 1, 64))); + return Math.Max(1, Math.Min(Files.Count, Math.Clamp(ModNet.NetTaskThreadLimit, 1, 8))); } private async Task ProcessFileAsync(PCL.Network.DownloadFile file, CancellationToken cancellationToken) From 3044fae4dc018f541ca4578edf7d3f0e5e821765 Mon Sep 17 00:00:00 2001 From: LuLu-ling Date: Sun, 17 May 2026 17:06:54 +0800 Subject: [PATCH 11/20] feat: NetTaskThreadLimit 8 -> 64 --- .../Modules/Network/Loaders/LoaderDownload.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs b/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs index ee68f6112..f396fc755 100644 --- a/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs +++ b/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs @@ -117,7 +117,7 @@ private void Run(CancellationToken cancellationToken) private int GetMaxParallelFiles() { - return Math.Max(1, Math.Min(Files.Count, Math.Clamp(ModNet.NetTaskThreadLimit, 1, 8))); + return Math.Max(1, Math.Min(Files.Count, Math.Clamp(ModNet.NetTaskThreadLimit, 1, 64))); } private async Task ProcessFileAsync(PCL.Network.DownloadFile file, CancellationToken cancellationToken) From 3a61bcff0296cf6b9441354e3a306af9155f55b9 Mon Sep 17 00:00:00 2001 From: Big-Cake-jpg Date: Sun, 17 May 2026 17:35:51 +0800 Subject: [PATCH 12/20] =?UTF-8?q?chore(downloader):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=88=86=E5=9D=97=E6=89=80=E9=9C=80=E7=9A=84=E6=9C=80=E5=B0=8F?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=A7=E5=B0=8F=E8=A6=81=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Network/Downloader/FileDownloader.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs index 13f6eec62..5fd7c4601 100644 --- a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs +++ b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs @@ -90,7 +90,8 @@ private static async Task DownloadSingleAsync(string url, string localPath, bool DownloadFileExtension = ModNet.NetDownloadEnd, EnableAutoResumeDownload = false, RequestConfiguration = DownloadRequestFactory.Create(url, useBrowserUserAgent, customUserAgent), - CustomHttpClientFactory = () => NetworkService.GetClient() + CustomHttpClientFactory = () => NetworkService.GetClient(), + MinimumSizeOfChunking = 1024 * 1024, }; using var downloader = new DownloadService(configuration); From 94df301e409a80ead21e9968e57372912c81fb28 Mon Sep 17 00:00:00 2001 From: Big-Cake-jpg Date: Sun, 17 May 2026 17:36:34 +0800 Subject: [PATCH 13/20] =?UTF-8?q?fix(net):=20=E4=BF=AE=E5=A4=8D=20authlib-?= =?UTF-8?q?injector=20=E4=B8=8B=E8=BD=BD=E4=BF=A1=E6=81=AF=E8=A2=AB?= =?UTF-8?q?=E6=88=AA=E6=96=AD=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Network/Facade/ModNet.cs | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs b/Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs index 79173c71d..e4f940e79 100644 --- a/Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs +++ b/Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs @@ -53,11 +53,31 @@ public static string NetGetCodeByLoader(string url, int Timeout = 45000, bool Is public static string NetGetCodeByLoader(IEnumerable urls, int Timeout = 45000, bool IsJson = false, bool UseBrowserUserAgent = false) { - var temp = ModMain.RequestTaskTempFolder() + "download.txt"; - FileDownloader.Download(urls, temp, UseBrowserUserAgent).GetAwaiter().GetResult(); - var content = ModBase.ReadFile(temp); - File.Delete(temp); - return IsJson ? ModBase.GetJson(content).ToString() : content; + Exception? lastException = null; + + foreach (var url in urls) + { + try + { + // 使用原生的 Requester.Fetch,它支持 HttpClient 的自动解压功能,并直接返回字符。 + var content = Requester.Fetch(url, new FetchParam + { + Method = "GET", + Timeout = Timeout, + UseBrowserUserAgent = UseBrowserUserAgent + }); + + return IsJson ? ModBase.GetJson(content).ToString() : content; + } + catch (Exception ex) + { + lastException = ex; + // 如果是多个链接用于重试,这里捕获异常并继续尝试下一个源 + ModBase.Log(ex, $"[Fetch] 获取文本/JSON失败,尝试下一个源:{url}", ModBase.LogLevel.Debug); + } + } + + throw new Exception("所有的获取源均已失败,无法拿到文本数据。", lastException); } public static string NetRequestRetry(string url, string method, string data = "", string? contentType = null, From f94c1d418f5e8ba35b1333b8ec33ccbb2f8da907 Mon Sep 17 00:00:00 2001 From: Big-Cake-jpg Date: Sun, 17 May 2026 17:48:37 +0800 Subject: [PATCH 14/20] =?UTF-8?q?chore(net):=20=E4=BF=AE=E6=AD=A3=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E4=B8=8E=E7=94=A8=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这段代码是 Gemini 3.1 Pro 生成的,上次改的时候我改过了但 revert 了,结果这次忘记再改了…… --- Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs b/Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs index e4f940e79..89d3922b6 100644 --- a/Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs +++ b/Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs @@ -59,7 +59,6 @@ public static string NetGetCodeByLoader(IEnumerable urls, int Timeout = { try { - // 使用原生的 Requester.Fetch,它支持 HttpClient 的自动解压功能,并直接返回字符。 var content = Requester.Fetch(url, new FetchParam { Method = "GET", @@ -72,12 +71,11 @@ public static string NetGetCodeByLoader(IEnumerable urls, int Timeout = catch (Exception ex) { lastException = ex; - // 如果是多个链接用于重试,这里捕获异常并继续尝试下一个源 - ModBase.Log(ex, $"[Fetch] 获取文本/JSON失败,尝试下一个源:{url}", ModBase.LogLevel.Debug); + ModBase.Log(ex, $"[Fetch] 获取文件内容失败,尝试下一个源:{url}", ModBase.LogLevel.Debug); } } - throw new Exception("所有的获取源均已失败,无法拿到文本数据。", lastException); + throw new Exception("无法获取文件内容", lastException); } public static string NetRequestRetry(string url, string method, string data = "", string? contentType = null, From 08963563d7a191e016bc830bc371f1285a434c68 Mon Sep 17 00:00:00 2001 From: Big-Cake-jpg Date: Sat, 23 May 2026 20:26:16 +0800 Subject: [PATCH 15/20] =?UTF-8?q?fix(install):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E6=97=A7=E7=89=88=20Forge=20=E6=97=B6?= =?UTF-8?q?=E7=9A=84=E6=96=87=E4=BB=B6=E6=88=96=E7=9B=AE=E5=BD=95=E5=B7=B2?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Pages/PageDownload/ModDownloadLib.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs index b1f7bf9c9..fdf6433bf 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs +++ b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs @@ -2203,9 +2203,10 @@ private static void ForgelikeInjectorLine(string Content, ModLoader.LoaderTask Date: Sat, 23 May 2026 20:51:21 +0800 Subject: [PATCH 16/20] =?UTF-8?q?ref(downloader):=20=E5=B0=9D=E8=AF=95?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=B8=8B=E8=BD=BD=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was generated by DeepSeek-V4-Pro --- .../Network/Downloader/FileDownloader.cs | 102 +++++++++--------- 1 file changed, 53 insertions(+), 49 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs index 5fd7c4601..17be40767 100644 --- a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs +++ b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs @@ -37,6 +37,9 @@ public static void DownloadByLoader(IEnumerable urls, string localPath, Download(urls, localPath, useBrowserUserAgent, customUserAgent).GetAwaiter().GetResult(); } + private const int BufferSize = 8192; + private const int BlockTimeoutMs = 60000; + private static async Task DownloadCoreAsync(IEnumerable urls, string localPath, bool useBrowserUserAgent, string customUserAgent, CancellationToken cancellationToken, bool enableParallelChunks, DownloadFile? trackedFile) { @@ -86,43 +89,40 @@ private static async Task DownloadSingleAsync(string url, string localPath, bool ParallelDownload = perFileThreadLimit > 1, MaximumBytesPerSecond = ModNet.NetTaskSpeedLimitHigh > 0 ? ModNet.NetTaskSpeedLimitHigh : 0, MaxTryAgainOnFailure = 2, - BlockTimeout = 30000, + BlockTimeout = BlockTimeoutMs, DownloadFileExtension = ModNet.NetDownloadEnd, EnableAutoResumeDownload = false, RequestConfiguration = DownloadRequestFactory.Create(url, useBrowserUserAgent, customUserAgent), CustomHttpClientFactory = () => NetworkService.GetClient(), - MinimumSizeOfChunking = 1024 * 1024, + MinimumSizeOfChunking = 1024L * 1024L, }; using var downloader = new DownloadService(configuration); - var tcs = new TaskCompletionSource(); - void UpdateDownloadStat(DownloadProgressChangedEventArgs args) - { - if (trackedFile is null) - return; - - trackedFile.State = PCL.Network.NetState.Downloading; - trackedFile.TotalSize = args.TotalBytesToReceive > 0 ? args.TotalBytesToReceive : trackedFile.TotalSize; - trackedFile.IsUnknownSize = trackedFile.TotalSize <= 0; - trackedFile.DownloadedBytes = Math.Max(trackedFile.DownloadedBytes, args.ReceivedBytesSize); - trackedFile.Speed = Math.Max(0L, (long)Math.Round(args.BytesPerSecondSpeed)); - trackedFile.ActiveThreads = Math.Max(0, args.ActiveChunks); - } downloader.DownloadStarted += (_, args) => { if (trackedFile is null) return; - - trackedFile.State = PCL.Network.NetState.Reading; + trackedFile.State = NetState.Reading; trackedFile.TotalSize = args.TotalBytesToReceive; trackedFile.IsUnknownSize = args.TotalBytesToReceive <= 0; trackedFile.DownloadedBytes = 0; trackedFile.Speed = 0; trackedFile.ActiveThreads = 0; }; - downloader.DownloadProgressChanged += (_, args) => UpdateDownloadStat(args); - downloader.ChunkDownloadProgressChanged += (_, args) => UpdateDownloadStat(args); + + downloader.DownloadProgressChanged += (_, args) => + { + if (trackedFile is null) + return; + trackedFile.State = NetState.Downloading; + trackedFile.TotalSize = args.TotalBytesToReceive > 0 ? args.TotalBytesToReceive : trackedFile.TotalSize; + trackedFile.IsUnknownSize = trackedFile.TotalSize <= 0; + trackedFile.DownloadedBytes = Math.Max(trackedFile.DownloadedBytes, args.ReceivedBytesSize); + trackedFile.Speed = Math.Max(0L, (long)Math.Round(args.BytesPerSecondSpeed)); + trackedFile.ActiveThreads = Math.Max(0, args.ActiveChunks); + }; + downloader.DownloadFileCompleted += (_, args) => { if (trackedFile is not null) @@ -131,46 +131,50 @@ void UpdateDownloadStat(DownloadProgressChangedEventArgs args) trackedFile.ActiveThreads = 0; trackedFile.DownloadedBytes = Math.Max(trackedFile.DownloadedBytes, trackedFile.TotalSize); } - - if (args.Cancelled) - tcs.TrySetCanceled(); - else if (args.Error != null) - tcs.TrySetException(args.Error); - else - tcs.TrySetResult(true); }; + try { await downloader.DownloadFileTaskAsync(url, localPath, cancellationToken).ConfigureAwait(false); - await tcs.Task.ConfigureAwait(false); - var tempPath = localPath + ModNet.NetDownloadEnd; - if (!File.Exists(localPath) && File.Exists(tempPath)) - { - for (var retry = 0; retry < 5; retry++) - { - try - { - File.Move(tempPath, localPath, true); - break; - } - catch (IOException) - { - Thread.Sleep(100); - } - } - } - if (!File.Exists(localPath)) - throw new IOException($"下载未产生任何文件:{localPath}"); - ModBase.Log($"[Download] 下载成功:{localPath}"); } - catch (TaskCanceledException ex) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + CleanupTempFiles(localPath); + throw; + } + catch (TaskCanceledException ex) when (!cancellationToken.IsCancellationRequested) { throw new TimeoutException($"下载超时({url})", ex); } - catch (Exception ex) + + var tempPath = localPath + ModNet.NetDownloadEnd; + if (!File.Exists(localPath) && File.Exists(tempPath)) + { + FinalizeDownload(tempPath, localPath); + } + + if (!File.Exists(localPath)) + throw new IOException($"下载未产生任何文件:{localPath}"); + + ModBase.Log($"[Download] 下载成功:{localPath}"); + } + + private static void FinalizeDownload(string tempPath, string finalPath) + { + for (var retry = 0; retry < 5; retry++) { - throw new IOException($"下载失败:{url}", ex); + try + { + File.Move(tempPath, finalPath, true); + return; + } + catch (IOException) + { + Thread.Sleep(100); + } } + + throw new IOException($"无法完成文件写入:{finalPath}"); } private static void CleanupTempFiles(string localPath) From df6f0c4073772790c2b8a63333fff8542be99a0d Mon Sep 17 00:00:00 2001 From: Big-Cake-jpg Date: Sun, 24 May 2026 01:49:14 +0800 Subject: [PATCH 17/20] =?UTF-8?q?fix(downloader):=20=E4=B9=9F=E8=AE=B8?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E9=87=8D=E6=9E=84=E5=90=8E=E5=87=BA?= =?UTF-8?q?=E7=8E=B0=E7=9A=84=E2=80=9C=E6=97=A0=E6=B3=95=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=86=99=E5=85=A5=E2=80=9D=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Downloader/FileDownloader.cs | 93 ++++++++++--------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs index 17be40767..b4b3e539c 100644 --- a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs +++ b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs @@ -97,54 +97,56 @@ private static async Task DownloadSingleAsync(string url, string localPath, bool MinimumSizeOfChunking = 1024L * 1024L, }; - using var downloader = new DownloadService(configuration); - - downloader.DownloadStarted += (_, args) => { - if (trackedFile is null) - return; - trackedFile.State = NetState.Reading; - trackedFile.TotalSize = args.TotalBytesToReceive; - trackedFile.IsUnknownSize = args.TotalBytesToReceive <= 0; - trackedFile.DownloadedBytes = 0; - trackedFile.Speed = 0; - trackedFile.ActiveThreads = 0; - }; - - downloader.DownloadProgressChanged += (_, args) => - { - if (trackedFile is null) - return; - trackedFile.State = NetState.Downloading; - trackedFile.TotalSize = args.TotalBytesToReceive > 0 ? args.TotalBytesToReceive : trackedFile.TotalSize; - trackedFile.IsUnknownSize = trackedFile.TotalSize <= 0; - trackedFile.DownloadedBytes = Math.Max(trackedFile.DownloadedBytes, args.ReceivedBytesSize); - trackedFile.Speed = Math.Max(0L, (long)Math.Round(args.BytesPerSecondSpeed)); - trackedFile.ActiveThreads = Math.Max(0, args.ActiveChunks); - }; + using var downloader = new DownloadService(configuration); - downloader.DownloadFileCompleted += (_, args) => - { - if (trackedFile is not null) + downloader.DownloadStarted += (_, args) => { + if (trackedFile is null) + return; + trackedFile.State = NetState.Reading; + trackedFile.TotalSize = args.TotalBytesToReceive; + trackedFile.IsUnknownSize = args.TotalBytesToReceive <= 0; + trackedFile.DownloadedBytes = 0; trackedFile.Speed = 0; trackedFile.ActiveThreads = 0; - trackedFile.DownloadedBytes = Math.Max(trackedFile.DownloadedBytes, trackedFile.TotalSize); - } - }; + }; - try - { - await downloader.DownloadFileTaskAsync(url, localPath, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - CleanupTempFiles(localPath); - throw; - } - catch (TaskCanceledException ex) when (!cancellationToken.IsCancellationRequested) - { - throw new TimeoutException($"下载超时({url})", ex); + downloader.DownloadProgressChanged += (_, args) => + { + if (trackedFile is null) + return; + trackedFile.State = NetState.Downloading; + trackedFile.TotalSize = args.TotalBytesToReceive > 0 ? args.TotalBytesToReceive : trackedFile.TotalSize; + trackedFile.IsUnknownSize = trackedFile.TotalSize <= 0; + trackedFile.DownloadedBytes = Math.Max(trackedFile.DownloadedBytes, args.ReceivedBytesSize); + trackedFile.Speed = Math.Max(0L, (long)Math.Round(args.BytesPerSecondSpeed)); + trackedFile.ActiveThreads = Math.Max(0, args.ActiveChunks); + }; + + downloader.DownloadFileCompleted += (_, args) => + { + if (trackedFile is not null) + { + trackedFile.Speed = 0; + trackedFile.ActiveThreads = 0; + trackedFile.DownloadedBytes = Math.Max(trackedFile.DownloadedBytes, trackedFile.TotalSize); + } + }; + + try + { + await downloader.DownloadFileTaskAsync(url, localPath, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + CleanupTempFiles(localPath); + throw; + } + catch (TaskCanceledException ex) when (!cancellationToken.IsCancellationRequested) + { + throw new TimeoutException($"下载超时({url})", ex); + } } var tempPath = localPath + ModNet.NetDownloadEnd; @@ -161,6 +163,7 @@ private static async Task DownloadSingleAsync(string url, string localPath, bool private static void FinalizeDownload(string tempPath, string finalPath) { + Exception? lastEx = null; for (var retry = 0; retry < 5; retry++) { try @@ -168,13 +171,15 @@ private static void FinalizeDownload(string tempPath, string finalPath) File.Move(tempPath, finalPath, true); return; } - catch (IOException) + catch (IOException ex) { + lastEx = ex; + ModBase.Log(ex, $"[Download] 文件写入重试 {retry + 1}/5:{tempPath} -> {finalPath}", ModBase.LogLevel.Debug); Thread.Sleep(100); } } - throw new IOException($"无法完成文件写入:{finalPath}"); + throw new IOException($"无法完成文件写入:{finalPath}", lastEx); } private static void CleanupTempFiles(string localPath) From aa29b7a646302decf51826109826e35f768dd8bf Mon Sep 17 00:00:00 2001 From: Vaelow233 Date: Sun, 24 May 2026 16:08:18 +0800 Subject: [PATCH 18/20] =?UTF-8?q?fix(downloader):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BA=86=E9=87=8D=E6=9E=84=E5=90=8E=E5=87=BA=E7=8E=B0=E7=9A=84?= =?UTF-8?q?=E2=80=9C=E6=97=A0=E6=B3=95=E5=AE=8C=E6=88=90=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=86=99=E5=85=A5=E2=80=9D=E5=BC=82=E5=B8=B8=20(#2887)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Downloader/FileDownloader.cs | 78 +++++++++++++++---- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs index b4b3e539c..b3c0eea7f 100644 --- a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs +++ b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.IO; using System.Net.Http; using System.Threading; @@ -9,6 +10,27 @@ namespace PCL.Network; public static class FileDownloader { + private static readonly ConcurrentDictionary PathLocks = + new(StringComparer.OrdinalIgnoreCase); + + private static SemaphoreSlim GetPathLock(string localPath) + { + var key = NormalizePathKey(localPath); + return PathLocks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); + } + + private static string NormalizePathKey(string localPath) + { + try + { + return Path.GetFullPath(localPath); + } + catch + { + return localPath; + } + } + public static Task Download(string url, string localPath, bool useBrowserUserAgent = false, string customUserAgent = "", CancellationToken cancellationToken = default, bool enableParallelChunks = true, DownloadFile? trackedFile = null) @@ -50,29 +72,53 @@ private static async Task DownloadCoreAsync(IEnumerable urls, string loc Directory.CreateDirectory(Path.GetDirectoryName(localPath) ?? throw new ArgumentException("下载路径无效", nameof(localPath))); - Exception? lastException = null; - foreach (var url in urlList) + var pathLock = GetPathLock(localPath); + await pathLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try { - try + if (trackedFile?.Check is { CanUseExistsFile: true } existsChecker + && File.Exists(localPath) + && existsChecker.Check(localPath) is null) { - await DownloadSingleAsync(url, localPath, useBrowserUserAgent, customUserAgent, cancellationToken, - enableParallelChunks, trackedFile).ConfigureAwait(false); + ModBase.Log($"[Download] 目标文件已被并行任务下载完成,跳过:{localPath}", ModBase.LogLevel.Debug); + trackedFile.IsCopy = true; + trackedFile.TotalSize = new FileInfo(localPath).Length; + trackedFile.DownloadedBytes = trackedFile.TotalSize; + trackedFile.IsUnknownSize = false; + trackedFile.Speed = 0; + trackedFile.ActiveThreads = 0; + trackedFile.State = NetState.Finished; return; } - catch (OperationCanceledException) - { - CleanupTempFiles(localPath); - throw; - } - catch (Exception ex) + + Exception? lastException = null; + foreach (var url in urlList) { - lastException = ex; - CleanupTempFiles(localPath); - ModBase.Log(ex, $"[Download] 下载失败,尝试下一个源:{url}", ModBase.LogLevel.Debug); + try + { + await DownloadSingleAsync(url, localPath, useBrowserUserAgent, customUserAgent, cancellationToken, + enableParallelChunks, trackedFile).ConfigureAwait(false); + return; + } + catch (OperationCanceledException) + { + CleanupTempFiles(localPath); + throw; + } + catch (Exception ex) + { + lastException = ex; + CleanupTempFiles(localPath); + ModBase.Log(ex, $"[Download] 下载失败,尝试下一个源:{url}", ModBase.LogLevel.Debug); + } } - } - throw new IOException($"下载失败:{localPath}", lastException); + throw new IOException($"下载失败:{localPath}", lastException); + } + finally + { + pathLock.Release(); + } } private static async Task DownloadSingleAsync(string url, string localPath, bool useBrowserUserAgent, From 34e801d09f658ae1b9aeb94079e413ee47582d71 Mon Sep 17 00:00:00 2001 From: Big-Cake-jpg Date: Sun, 24 May 2026 16:10:47 +0800 Subject: [PATCH 19/20] =?UTF-8?q?fix(validate):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=B8=8D=E6=8F=90=E7=A4=BA=E6=96=87=E4=BB=B6=E5=A4=B9=E9=87=8D?= =?UTF-8?q?=E5=90=8D=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PCL.Core/Utils/Validate/FolderNameValidator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PCL.Core/Utils/Validate/FolderNameValidator.cs b/PCL.Core/Utils/Validate/FolderNameValidator.cs index 2150c2488..c3a0b3224 100644 --- a/PCL.Core/Utils/Validate/FolderNameValidator.cs +++ b/PCL.Core/Utils/Validate/FolderNameValidator.cs @@ -11,7 +11,7 @@ public class FolderNameValidator( string? parentFolder = null, bool useMinecraftCharCheck = true, bool ignoreCase = true, - bool ignoreSameNameInParentFolder = true) + bool ignoreSameNameInParentFolder = false) : FileSystemValidator { public bool UseMinecraftCharCheck { get; set; } = useMinecraftCharCheck; @@ -55,7 +55,7 @@ private void BuildRules() if (!dirInfo.Exists) return true; if (IgnoreSameNameInParentFolder) return true; - return !dirInfo.EnumerateFiles().Select(f => f.Name).Contains(x, + return !dirInfo.EnumerateDirectories().Select(f => f.Name).Contains(x, IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); }).WithMessage("不可与现有文件夹重名!"); From 6163fe41762b96d0510e109ce62163cd7870679e Mon Sep 17 00:00:00 2001 From: Big-Cake-jpg Date: Sun, 24 May 2026 16:23:00 +0800 Subject: [PATCH 20/20] =?UTF-8?q?fix(downloader):=20=E4=B9=9F=E8=AE=B8?= =?UTF-8?q?=E4=BF=AE=E5=A5=BD=E4=BA=86=E4=B8=8B=E8=BD=BD=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E4=BD=86=E6=89=BE=E4=B8=8D=E5=88=B0=E6=96=87=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这是 vibe 的,我也不知道到底有没有用,这个问题真的不太好复现,有人要是愿意测一下那可真是谢天谢地了 --- .../Network/Downloader/FileDownloader.cs | 6 ++++- .../Modules/Network/Loaders/LoaderDownload.cs | 27 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs index b3c0eea7f..b9e07de09 100644 --- a/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs +++ b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs @@ -204,6 +204,10 @@ private static async Task DownloadSingleAsync(string url, string localPath, bool if (!File.Exists(localPath)) throw new IOException($"下载未产生任何文件:{localPath}"); + var finalSize = new FileInfo(localPath).Length; + if (finalSize <= 0) + throw new IOException($"下载的文件大小为 0:{localPath}"); + ModBase.Log($"[Download] 下载成功:{localPath}"); } @@ -245,7 +249,7 @@ private static void TryDeleteFile(string path) File.Delete(path); return; } - catch (IOException) + catch (Exception) { Thread.Sleep(100); } diff --git a/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs b/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs index f396fc755..2758fcbb4 100644 --- a/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs +++ b/Plain Craft Launcher 2/Modules/Network/Loaders/LoaderDownload.cs @@ -131,7 +131,8 @@ private async Task ProcessFileAsync(PCL.Network.DownloadFile file, CancellationT { file.IsCopy = true; file.State = PCL.Network.NetState.Finished; - file.TotalSize = new FileInfo(file.LocalPath).Length; + try { file.TotalSize = new FileInfo(file.LocalPath).Length; } + catch (IOException) { file.TotalSize = -1; } file.DownloadedBytes = file.TotalSize; file.Speed = 0; file.ActiveThreads = 0; @@ -141,9 +142,27 @@ private async Task ProcessFileAsync(PCL.Network.DownloadFile file, CancellationT file.State = PCL.Network.NetState.Connecting; var enableParallelChunks = Files.Count <= 1; - await FileDownloader.Download(file.Urls, file.LocalPath, file.UseBrowserUserAgent, file.CustomUserAgent, - cancellationToken, enableParallelChunks, file).ConfigureAwait(false); - file.TotalSize = File.Exists(file.LocalPath) ? new FileInfo(file.LocalPath).Length : -1; + for (var retry = 0; retry < 4; retry++) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + await FileDownloader.Download(file.Urls, file.LocalPath, file.UseBrowserUserAgent, file.CustomUserAgent, + cancellationToken, enableParallelChunks, file).ConfigureAwait(false); + break; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) when (retry < 3) + { + ModBase.Log(ex, $"[Download] 重试 {retry + 1}/3:{file.LocalPath}", ModBase.LogLevel.Debug); + Thread.Sleep(RandomUtils.NextInt(300, 500 + retry * 300)); + } + } + try { file.TotalSize = new FileInfo(file.LocalPath).Length; } + catch (IOException) { file.TotalSize = -1; } file.IsUnknownSize = file.TotalSize < 0; file.DownloadedBytes = Math.Max(0, file.TotalSize); file.Speed = 0;