diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.cs b/Plain Craft Launcher 2/Modules/Base/ModBase.cs index 58e094e94..27b90a09c 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModBase.cs +++ b/Plain Craft Launcher 2/Modules/Base/ModBase.cs @@ -917,7 +917,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()) { @@ -988,10 +988,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." + Guid.NewGuid().ToString("N"); + File.WriteAllBytes(tempPath, bytes); + File.Move(tempPath, FilePath, true); + } } /// @@ -1345,6 +1349,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 3ff50743e..8f0132c3e 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs @@ -829,17 +829,18 @@ bool FastJsonCheck(string Json) { if (ModBase.RunInUi()) { - ModBase.Log("[Minecraft] 实例 JSON 文件为空或有误,由于代码在主线程运行,将不再进行重试", ModBase.LogLevel.Debug); - ModBase.GetJson(_jsonText); // 触发异常 + 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); - } // 触发异常 + } + if (!FastJsonCheck(_jsonText)) + ModBase.GetJson(_jsonText); } } 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/Downloader/FileDownloader.cs b/Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs index 54199975c..5fd7c4601 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) @@ -93,14 +86,16 @@ 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), - // 传入共享的 SocketsHttpHandler,实现连接池复用 - CustomHttpMessageHandlerFactory = () => SharedHandler + CustomHttpClientFactory = () => NetworkService.GetClient(), + MinimumSizeOfChunking = 1024 * 1024, }; - 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 +123,44 @@ 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)) + { + 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) @@ -155,9 +176,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/Facade/ModNet.cs b/Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs index 79173c71d..89d3922b6 100644 --- a/Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs +++ b/Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs @@ -53,11 +53,29 @@ 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 + { + 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] 获取文件内容失败,尝试下一个源:{url}", ModBase.LogLevel.Debug); + } + } + + throw new Exception("无法获取文件内容", lastException); } public static string NetRequestRetry(string url, string method, string data = "", string? contentType = null, 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(); 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 }; diff --git a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs index 6e7143486..b1f7bf9c9 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs +++ b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs @@ -189,17 +189,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); 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) { @@ -221,7 +222,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")); try { var assetIndex = new ModMinecraft.McInstance(instanceFolder); @@ -1934,6 +1935,7 @@ private static void ForgelikeInjectorLine(string Content, ModLoader.LoaderTask( ForgeType == ModDownload.DlForgelikeEntry.ForgelikeType.Forge ? "安装 Forge(方式 A)" : "安装 " + ForgeType, Task => { + ModBase.WaitForFileReady(InstallerAddress); var Installer = new ZipArchive(new FileStream(InstallerAddress, FileMode.Open)); try { @@ -2179,6 +2182,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")); ModBase.Log("[Download] 开始分析原版与 LabyMod 支持库文件:" + VersionFolder); Task.Output = ModMinecraft.McLibNetFilesFromInstance(new ModMinecraft.McInstance(VersionFolder)); }) 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 @@ - +