Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 61 additions & 5 deletions Plain Craft Launcher 2/Modules/Base/ModBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
{
Expand Down Expand Up @@ -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);
}
}

/// <summary>
Expand Down Expand Up @@ -1345,6 +1349,58 @@ public string Check(string LocalPath)
}
}

/// <summary>
/// 等待文件就绪可读,在指定超时时间内轮询检查文件是否存在且内容非空。
/// </summary>
/// <param name="filePath">文件路径。</param>
/// <param name="timeoutMs">超时时间(毫秒)。</param>
public static void WaitForFileReady(string filePath, int timeoutMs = 2000)
{
WaitForFileReady(filePath, timeoutMs, false);
}

/// <summary>
/// 等待文件就绪可读,在指定超时时间内轮询检查文件是否存在且内容非空。
/// </summary>
/// <param name="filePath">文件路径。</param>
/// <param name="timeoutMs">超时时间(毫秒)。</param>
/// <param name="requireJson">是否要求文件为合法 JSON。</param>
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);
}
}

/// <summary>
/// 尝试根据后缀名判断文件种类并解压文件,支持 gz 与 zip,会尝试将 Jar 以 zip 方式解压。
/// 会尝试创建,但不会清空目标文件夹。
Expand Down
11 changes: 6 additions & 5 deletions Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
2 changes: 1 addition & 1 deletion Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ private static LoaderCombo<string> InstallPackCurseForge(string FileAddress, Zip
string QuiltVersion = null;
foreach (var Entry in (dynamic)Json["minecraft"]["modLoaders"] ?? Array.Empty<JToken>())
{
var Id = (Entry["id"] ?? "").ToString().ToLower();
string Id = (Entry["id"] ?? "").ToString().ToLower();
if (Id.StartsWithF("forge-"))
{
// Forge 指定
Expand Down
78 changes: 57 additions & 21 deletions Plain Craft Launcher 2/Modules/Network/Downloader/FileDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<bool>();
void UpdateDownloadStat(DownloadProgressChangedEventArgs args)
{
if (trackedFile is null)
Expand Down Expand Up @@ -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)
Expand All @@ -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);
}
}
}
}
28 changes: 23 additions & 5 deletions Plain Craft Launcher 2/Modules/Network/Facade/ModNet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,29 @@ public static string NetGetCodeByLoader(string url, int Timeout = 45000, bool Is
public static string NetGetCodeByLoader(IEnumerable<string> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -93,7 +95,8 @@ private void Run(CancellationToken cancellationToken)
}
finally
{
semaphore.Release();
if (entered)
semaphore.Release();
RefreshStat();
}
}).ToList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand Down
14 changes: 9 additions & 5 deletions Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,17 +189,18 @@ public static void McDownloadClientCore(string Id, string JsonUrl, NetPreDownloa
var loadersLib = new List<ModLoader.LoaderBase>();
loadersLib.Add(new ModLoader.LoaderTask<string, List<DownloadFile>>("分析原版支持库文件(副加载器)", 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)
{
Expand All @@ -221,7 +222,7 @@ public static void McDownloadClientCore(string Id, string JsonUrl, NetPreDownloa
var loadersAssets = new List<ModLoader.LoaderBase>();
loadersAssets.Add(new ModLoader.LoaderTask<string, List<DownloadFile>>("分析资源文件索引地址(副加载器)", task =>
{
Thread.Sleep(50); // 等待 JSON 文件实际写入硬盘
ModBase.WaitForFileReady(Path.Combine(instanceFolder, instanceName + ".json"));
try
{
var assetIndex = new ModMinecraft.McInstance(instanceFolder);
Expand Down Expand Up @@ -1934,6 +1935,7 @@ private static void ForgelikeInjectorLine(string Content, ModLoader.LoaderTask<b
try
{
// 解压并获取、合并两个 Json 的信息
ModBase.WaitForFileReady(InstallerAddress);
Installer = new ZipArchive(new FileStream(InstallerAddress, FileMode.Open));
Task.Progress = 0.2d;
var Json = (JObject)ModBase.GetJson(
Expand Down Expand Up @@ -2069,6 +2071,7 @@ private static void ForgelikeInjectorLine(string Content, ModLoader.LoaderTask<b
Loaders.Add(new ModLoader.LoaderTask<bool, bool>(
ForgeType == ModDownload.DlForgelikeEntry.ForgelikeType.Forge ? "安装 Forge(方式 A)" : "安装 " + ForgeType, Task =>
{
ModBase.WaitForFileReady(InstallerAddress);
var Installer = new ZipArchive(new FileStream(InstallerAddress, FileMode.Open));
try
{
Expand Down Expand Up @@ -2179,6 +2182,7 @@ private static void ForgelikeInjectorLine(string Content, ModLoader.LoaderTask<b
try
{
// 解压并获取信息
ModBase.WaitForFileReady(InstallerAddress);
Installer = new ZipArchive(new FileStream(InstallerAddress, FileMode.Open));
Task.Progress = 0.2d;
var Json = (JObject)ModBase.GetJson(
Expand Down Expand Up @@ -3317,7 +3321,7 @@ public static void McDownloadLabyModSnapshotLoaderSave()
var LoadersLib = new List<ModLoader.LoaderBase>();
LoadersLib.Add(new ModLoader.LoaderTask<string, List<DownloadFile>>("分析原版与 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));
})
Expand Down
2 changes: 1 addition & 1 deletion Plain Craft Launcher 2/Plain Craft Launcher 2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
<ItemGroup>
<PackageReference Include="CacheCow.Client" Version="2.13.1"/>
<PackageReference Include="Dapper" Version="2.1.66"/>
<PackageReference Include="Downloader" Version="5.1.0" />
<PackageReference Include="Downloader" Version="5.5.0" />
<PackageReference Include="fNbt" Version="1.0.0"/>
<PackageReference Include="LiteDB" Version="5.0.21"/>
<PackageReference Include="Markdig.Wpf" Version="0.5.0.1"/>
Expand Down
Loading