This commit is contained in:
龙腾猫跃
2025-12-27 02:48:47 +08:00
parent d6ba445e54
commit 085af160a1
78 changed files with 8426 additions and 4863 deletions
+244 -190
View File
@@ -242,7 +242,7 @@ RequestFinished:
If Header.Key.ToLower = "content-type" Then
Request.Content?.Headers.TryAddWithoutValidation(Header.Key, Header.Value)
Else
Request.Headers.Add(Header.Key, Header.Value)
Request.Headers.TryAddWithoutValidation(Header.Key, Header.Value)
End If
Next
SecretHeadersSign(Url, Request, SimulateBrowserHeaders)
@@ -257,7 +257,7 @@ RequestFinished:
'延迟初始化,以避免在程序启动前加载 CacheCow 导致 DLL 加载失败
If RequestClient Is Nothing Then
RequestClient = CacheCow.Client.ClientExtensions.CreateClient(New CacheCow.Client.FileCacheStore.FileStore(PathTemp & "Cache/Http/"), New HttpClientHandler With {
.AutomaticDecompression = DecompressionMethods.Deflate Or DecompressionMethods.GZip Or DecompressionMethods.None,
.AutomaticDecompression = DecompressionMethods.Deflate Or DecompressionMethods.GZip,
.UseCookies = False '不设为 False 就不能从 Header 手动传入 Cookies
})
End If
@@ -308,15 +308,25 @@ RequestFinished:
''' 若解析失败,则返回 Nothing。
''' </summary>
Private Function DNSLookup(Request As HttpRequestMessage, CancelToken As CancellationTokenSource) As String
Dim GetIPReliability = Function(Key) IPReliability.GetOrDefault(Key.ToString, 0)
'初步 DNS 解析
'若一分钟内已失败过,则不再重复尝试解析,以减少断网时的 Exception 数量
Dim Host = Request.RequestUri.Host
SyncLock DNSFailureRecord.SyncRoot
If DNSFailureRecord.ContainsKey(Host) Then
If Date.Now - DNSFailureRecord(Host) < New TimeSpan(0, 1, 0) Then
Return Nothing
Else
DNSFailureRecord.Remove(Host)
End If
End If
End SyncLock
'初步 DNS 解析
Dim DnsTask As Task(Of IPAddress()) = Nothing
Try
DnsTask = Dns.GetHostAddressesAsync(Host)
DnsTask.Wait(CancelToken.Token)
Catch ex As Exception
Log(ex, $"DNS 解析失败({Host})")
DNSFailureRecord(Host) = Date.Now
Return Nothing
End Try
Dim Candidates As IPAddress() = DnsTask.Result.ToArray
@@ -325,6 +335,7 @@ RequestFinished:
Return Nothing
End If
'若同时存在 IPv4 和 IPv6 地址,仅选择其中一类(因为 GFW 可能只屏蔽了 IPv4 或 IPv6)
Dim GetIPReliability = Function(Key) IPReliability.GetOrDefault(Key.ToString, 0)
Dim IPv4Targets = Candidates.Where(Function(i) i.AddressFamily = AddressFamily.InterNetwork).ToArray
Dim IPv6Targets = Candidates.Where(Function(i) i.AddressFamily = AddressFamily.InterNetworkV6).ToArray
If IPv4Targets.Any AndAlso IPv6Targets.Any Then
@@ -347,6 +358,7 @@ RequestFinished:
End If
Return TargetIp
End Function
Private DNSFailureRecord As New SafeDictionary(Of String, Date)
''' <summary>
''' 记录每个 IP 地址的请求可靠度。
''' 通常取值 -1 ~ +0.5,越高越好。未尝试过的 IP 应视为 0。
@@ -387,7 +399,7 @@ RequestFinished:
#Region "多线程下载引擎"
Private ThreadClient As New HttpClient(New HttpClientHandler With {
.AutomaticDecompression = DecompressionMethods.Deflate Or DecompressionMethods.GZip Or DecompressionMethods.None
.AutomaticDecompression = DecompressionMethods.Deflate Or DecompressionMethods.GZip
})
''' <summary>
@@ -407,7 +419,6 @@ RequestFinished:
''' </summary>
Public NetTaskSpeedLimitLeft As Long = -1
Private ReadOnly NetTaskSpeedLimitLeftLock As New Object
Private NetTaskSpeedLimitLeftLast As Long
''' <summary>
''' 正在运行中的线程数。
''' </summary>
@@ -438,39 +449,35 @@ RequestFinished:
''' <summary>
''' 尚未进行已存在检查。
''' </summary>
WaitForCheck = -1
WaitingToCheck = -1
''' <summary>
''' 尚未开始。
''' </summary>
WaitForDownload = 0
WaitingToDownload = 0
''' <summary>
''' 正在连接,尚未获取文件大小。
''' </summary>
Connect = 1
Connecting = 1
''' <summary>
''' 已获取文件大小,尚未有有效下载。
''' 已连接,正在尝试接收首个下载数据包。
''' </summary>
[Get] = 2
Reading = 2
''' <summary>
''' 正在下载。
''' </summary>
Download = 3
Downloading = 3
''' <summary>
''' 正在合并文件。
''' </summary>
Merge = 4
''' <summary>
''' 不进行下载,因为已发现现存的文件。
''' </summary>
WaitForCopy = 5
Merging = 4
''' <summary>
''' 已完成。
''' </summary>
Finish = 6
Finished = 5
''' <summary>
''' 已失败或中断。
''' </summary>
[Error] = 7
Interrupted = 6
End Enum
''' <summary>
''' 预下载检查行为。
@@ -612,13 +619,13 @@ RequestFinished:
''' <summary>
''' 当前线程的状态。
''' </summary>
Public State As NetState = NetState.WaitForDownload
Public State As NetState = NetState.WaitingToDownload
''' <summary>
''' 是否已经结束。
''' </summary>
Public ReadOnly Property IsEnded As Boolean
Get
Return State = NetState.Finish OrElse State = NetState.Error
Return State = NetState.Finished OrElse State = NetState.Interrupted
End Get
End Property
@@ -716,7 +723,7 @@ RequestFinished:
''' <summary>
''' 当前的下载状态。
''' </summary>
Public State As NetState = NetState.WaitForCheck
Public State As NetState = NetState.WaitingToCheck
''' <summary>
''' 导致下载失败的原因。
''' </summary>
@@ -741,7 +748,7 @@ RequestFinished:
''' </summary>
Public ReadOnly Property IsNoSplit As Boolean
Get
Return IsUnknownSize OrElse FileSize < FilePieceLimit
Return IsUnknownSize OrElse FileSize < 1024 * 1024 '小于 1MB 的文件不分割
End Get
End Property
''' <summary>
@@ -797,24 +804,22 @@ RequestFinished:
Public ReadOnly Property Progress As Double
Get
Select Case State
Case NetState.WaitForCheck
Case NetState.WaitingToCheck
Return 0
Case NetState.WaitForCopy
Return 0.2
Case NetState.WaitForDownload
Case NetState.WaitingToDownload
Return 0.01
Case NetState.Connect
Case NetState.Connecting
Return 0.02
Case NetState.Get
Case NetState.Reading
Return 0.04
Case NetState.Download
Case NetState.Downloading
'正在下载中,对应 5% ~ 98%
Dim OriginalProgress As Double = If(IsUnknownSize, 0.5, DownloadDone / Math.Max(FileSize, 1))
OriginalProgress = 1 - (1 - OriginalProgress) ^ 0.9
Return OriginalProgress * 0.93 + 0.05
Case NetState.Merge
Case NetState.Merging
Return 0.99
Case NetState.Finish, NetState.Error
Case NetState.Finished, NetState.Interrupted
Return 1
Case Else
Throw New ArgumentOutOfRangeException("文件状态未知:" & State)
@@ -859,7 +864,7 @@ RequestFinished:
''' 新建一个需要下载的文件。
''' </summary>
''' <param name="LocalPath">包含文件名的本地地址。</param>
Public Sub New(Urls As IEnumerable(Of String), LocalPath As String, Optional Check As FileChecker = Nothing, Optional SimulateBrowserHeaders As Boolean = False)
Public Sub New(Urls As IEnumerable(Of String), LocalPath As String, Optional Checker As FileChecker = Nothing, Optional SimulateBrowserHeaders As Boolean = False)
Dim Sources As New List(Of NetSource)
Dim Count As Integer = 0
Urls = Urls.Distinct.ToArray
@@ -869,7 +874,7 @@ RequestFinished:
Next
Me.Sources = Sources
Me.LocalPath = LocalPath
Me.Check = Check
Me.Check = Checker
Me.SimulateBrowserHeaders = SimulateBrowserHeaders
Me.LocalName = GetFileNameFromPath(LocalPath)
End Sub
@@ -886,10 +891,10 @@ RequestFinished:
'条件检测
SyncLock LockSource
If NetTaskThreadCount >= NetTaskThreadLimit OrElse Not HasAvailableSource() OrElse
(IsNoSplit AndAlso Threads IsNot Nothing AndAlso Threads.State <> NetState.Error) Then Return Nothing
If State >= NetState.Merge OrElse State = NetState.WaitForCheck Then Return Nothing
(IsNoSplit AndAlso Threads IsNot Nothing AndAlso Threads.State <> NetState.Interrupted) Then Return Nothing
If State >= NetState.Merging OrElse State = NetState.WaitingToCheck Then Return Nothing
SyncLock LockState
If State < NetState.Connect Then State = NetState.Connect
If State < NetState.Connecting Then State = NetState.Connecting
End SyncLock
'初始化参数
'获取线程起点与下载源
@@ -897,10 +902,10 @@ RequestFinished:
If IsNoSplit Then GoTo StartSingleThreadDownload
'只剩下单线程可用点
If Not HasAvailableSource(False) Then
If SourcesOnce.First.SingleThread IsNot Nothing AndAlso SourcesOnce.First.SingleThread.State <> NetState.Error Then Return Nothing
If SourcesOnce.First.SingleThread IsNot Nothing AndAlso SourcesOnce.First.SingleThread.State <> NetState.Interrupted Then Return Nothing
StartSingleThreadDownload:
Reset()
State = NetState.Get
State = NetState.Reading
End If
End SyncLock
SyncLock LockChain
@@ -913,7 +918,7 @@ StartSingleThreadDownload:
End If
'寻找失败点
For Each Thread As NetThread In Threads
If Thread.State = NetState.Error AndAlso Thread.DownloadUndone > 0 Then
If Thread.State = NetState.Interrupted AndAlso Thread.DownloadUndone > 0 Then
StartPosition = Thread.DownloadStart + Thread.DownloadDone
StartSource = GetSource(Thread.Source.Id + 1)
GoTo StartThread
@@ -942,7 +947,7 @@ StartThread:
Dim ThreadUuid As Integer = GetUuid()
If Not Tasks.Any() Then Return Nothing '由于中断,已没有可用任务
Th = New Thread(AddressOf Thread) With {.Name = $"下载 {Tasks(0).Uuid}/{Uuid}/{ThreadUuid}#", .Priority = ThreadPriority.BelowNormal}
ThreadInfo = New NetThread With {.Uuid = ThreadUuid, .DownloadStart = StartPosition, .Thread = Th, .Source = StartSource, .Task = Me, .State = NetState.WaitForDownload}
ThreadInfo = New NetThread With {.Uuid = ThreadUuid, .DownloadStart = StartPosition, .Thread = Th, .Source = StartSource, .Task = Me, .State = NetState.WaitingToDownload}
'链表处理
If ThreadInfo.IsFirstThread OrElse Threads Is Nothing Then
Threads = ThreadInfo
@@ -981,8 +986,8 @@ StartThread:
CancelToken As CancellationTokenSource = Nothing, HostIp As String = Nothing
'部分下载源真的特别慢,并且只需要一个请求,例如 Ping 为 20s,如果增长太慢,就会造成类似 2.5s 5s 7.5s 10s 12.5s... 的极大延迟
'延迟过长会导致某些特别慢的链接迟迟不被掐死
Dim Timeout As Integer = Math.Min(Math.Max(ConnectAverage, 6000) * (1 + Th.Source.FailCount), 30000)
Th.State = NetState.Connect
Dim Timeout As Integer = Math.Min(Math.Max(ConnectAverage, 10000) * (1 + Th.Source.FailCount), 30000)
Th.State = NetState.Connecting
Try
Dim HttpDataCount As Integer = 0
If SourcesOnce.Contains(Th.Source) AndAlso Th <> Th.Source.SingleThread Then GoTo SourceBreak
@@ -994,7 +999,7 @@ StartThread:
Dim ContentLength As Long = 0
Response = ThreadClient.SendAsync(HttpRequest, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout)
If Not Response.IsSuccessStatusCode Then Throw New Exception($"错误码 {Response.StatusCode} ({CInt(Response.StatusCode)}),{Th.Source.Url}") '状态码检查
If State = NetState.Error Then GoTo SourceBreak '快速中断
If State = NetState.Interrupted Then GoTo SourceBreak '快速中断
If ModeDebug AndAlso Response.RequestMessage.RequestUri.ToString <> Th.Source.Url Then Log($"[Download] {LocalName}:重定向至 {Response.RequestMessage.RequestUri}")
'文件大小校验
ContentLength = Response.Content.Headers.ContentLength.GetValueOrDefault(-1)
@@ -1045,9 +1050,9 @@ NotSupportRange:
Throw New RangeNotSupportedException($"获取到的分段大小不一致:Range 起始于 {Th.DownloadStart},预期 ContentLength 为 {FileSize - Th.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}")
End If
'Log($"[Download] {LocalName} {Info.Uuid}#:通过大小检查,文件大小 {FileSize},起始点 {Info.DownloadStart},ContentLength {ContentLength}")
Th.State = NetState.Get
Th.State = NetState.Reading
SyncLock LockState
If State < NetState.Get Then State = NetState.Get
If State < NetState.Reading Then State = NetState.Reading
End SyncLock
'创建缓存文件
If IsNoSplit Then
@@ -1065,7 +1070,7 @@ NotSupportRange:
Dim ResponseBytes As Byte() = New Byte(16384) {}
HttpDataCount = ResponseStream.ReadAsync(ResponseBytes, 0, 16384, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout)
While (IsUnknownSize OrElse Th.DownloadUndone > 0) AndAlso '判断是否下载完成
HttpDataCount > 0 AndAlso Not IsProgramEnded AndAlso State < NetState.Merge AndAlso (Not Th.Source.IsFailed OrElse Th.Source.SingleThread = Th)
HttpDataCount > 0 AndAlso Not IsProgramEnded AndAlso State < NetState.Merging AndAlso (Not Th.Source.IsFailed OrElse Th.Source.SingleThread = Th)
'限速
While NetTaskSpeedLimitHigh > 0 AndAlso NetTaskSpeedLimitLeft <= 0
Threading.Thread.Sleep(16)
@@ -1080,9 +1085,9 @@ NotSupportRange:
'有数据
If Th.DownloadDone = 0 Then
'第一次接受到数据
Th.State = NetState.Download
Th.State = NetState.Downloading
SyncLock LockState
If State < NetState.Download Then State = NetState.Download
If State < NetState.Downloading Then State = NetState.Downloading
End SyncLock
SyncLock LockCount
ConnectCount += 1
@@ -1116,16 +1121,16 @@ NotSupportRange:
HttpDataCount = ResponseStream.ReadAsync(ResponseBytes, 0, 16384, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout)
End While
SourceBreak:
If State = NetState.Error OrElse (Th.Source.IsFailed AndAlso Th.Source.SingleThread <> Th) OrElse (Th.DownloadUndone > 0 AndAlso Not IsUnknownSize) Then
If State = NetState.Interrupted OrElse (Th.Source.IsFailed AndAlso Th.Source.SingleThread <> Th) OrElse (Th.DownloadUndone > 0 AndAlso Not IsUnknownSize) Then
'被外部中断
Th.State = NetState.Error
Th.State = NetState.Interrupted
Log($"[Download] {LocalName}:中断")
ElseIf HttpDataCount = 0 AndAlso Th.DownloadUndone > 0 AndAlso Not IsUnknownSize Then
'服务器无返回数据
Throw New Exception($"返回的 ContentLength 过多:ContentLength 为 {ContentLength},但获取到的总数据量仅为 {Th.DownloadDone}(全文件总数据量 {DownloadDone})")
Else
'本线程完成
Th.State = NetState.Finish
Th.State = NetState.Finished
If ModeDebug Then Log($"[Download] {LocalName}:完成,已下载 {Th.DownloadDone}({Th.DownloadStart}~{Th.DownloadEnd})")
RecordIPReliability(HostIp, 0.5)
End If
@@ -1147,7 +1152,7 @@ SourceBreak:
End SyncLock
'合并
If ((FileSize >= 0 AndAlso DownloadDone >= FileSize) OrElse (FileSize = -1 AndAlso DownloadDone > 0)) AndAlso
State < NetState.Merge AndAlso Th.State <> NetState.Error Then Merge(Th)
State < NetState.Merging AndAlso Th.State <> NetState.Interrupted Then Merge(Th)
End Try
End Sub
Private Sub SourceFail(Th As NetThread, ex As Exception, IsMergeFailure As Boolean)
@@ -1158,7 +1163,7 @@ SourceBreak:
Task.FailCount += 1
Next
End SyncLock
Th.State = NetState.Error
Th.State = NetState.Interrupted
Th.Source.Ex = ex
'根据情况判断,是否在多线程下禁用下载源(连续错误过多,或不支持断点续传)
Dim IsRangeNotSupported As Boolean = TypeOf ex Is RangeNotSupportedException OrElse ex.Message.Contains("(416)")
@@ -1202,13 +1207,13 @@ SourceBreak:
End SyncLock
Reset()
SyncLock LockState
State = NetState.WaitForDownload
State = NetState.WaitingToDownload
End SyncLock
ElseIf HasAvailableSource() AndAlso IsMergeFailure Then
'合并失败且单个源失败:继续下一个源
Reset()
SyncLock LockState
State = NetState.WaitForDownload
State = NetState.WaitingToDownload
End SyncLock
Else
'失败
@@ -1251,8 +1256,8 @@ SourceBreak:
Private Sub Merge(Th As NetThread)
'状态判断
SyncLock LockState
If State < NetState.Merge Then
State = NetState.Merge
If State < NetState.Merging Then
State = NetState.Merging
Else
Return
End If
@@ -1323,7 +1328,11 @@ Retry:
GoTo Retry
End If
'失败,禁用当前下载源并重启下载
If File.Exists(LocalPath) Then File.Delete(LocalPath)
Try
If File.Exists(LocalPath) Then File.Delete(LocalPath)
Catch exx As Exception
Log(ex, $"删除合并失败的文件出错({LocalPath})")
End Try
SourceFail(Th, ex, True)
Finally
Cache?.Dispose()
@@ -1334,10 +1343,10 @@ Retry:
''' </summary>
Private Sub Fail(Optional RaiseEx As Exception = Nothing)
SyncLock LockState
If State >= NetState.Finish Then Return
If State >= NetState.Finished Then Return
If RaiseEx IsNot Nothing Then Ex.Add(RaiseEx)
'凉凉
State = NetState.Error
State = NetState.Interrupted
End SyncLock
AbortInternal()
For Each Task In Tasks
@@ -1353,8 +1362,8 @@ Retry:
If Tasks.Any Then Return
'确认中断
SyncLock LockState
If State >= NetState.Finish Then Return
State = NetState.Error
If State >= NetState.Finished Then Return
State = NetState.Interrupted
End SyncLock
AbortInternal()
End Sub
@@ -1373,8 +1382,8 @@ Retry:
''' </summary>
Public Sub Finish(Optional PrintLog As Boolean = True)
SyncLock LockState
If State >= NetState.Finish Then Return
State = NetState.Finish
If State >= NetState.Finished Then Return
State = NetState.Finished
End SyncLock
SyncLock NetManager.LockRemain
NetManager.FileRemain -= 1
@@ -1483,20 +1492,10 @@ FinishExCatch:
Public Overrides Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
If Input IsNot Nothing Then Files = New SafeList(Of NetFile)(Input)
'去重
Dim ResultArray As New SafeList(Of NetFile)
For i = 0 To Files.Count - 1
For ii = i + 1 To Files.Count - 1
If Files(i).LocalPath = Files(ii).LocalPath Then GoTo NextElement
Next
ResultArray.Add(Files(i))
NextElement:
Next
Files = ResultArray
Files = New SafeList(Of NetFile)(Files.Distinct(Function(a, b) a.LocalPath = b.LocalPath))
'设置剩余文件数
SyncLock FileRemainLock
For Each File In Files
If File.State <> NetState.Finish Then FileRemain += 1
Next
FileRemain += Files.Where(Function(f) f.State <> NetState.Finished).Count
End SyncLock
State = LoadState.Loading
'开始执行
@@ -1517,109 +1516,162 @@ NextElement:
End If
Next
If Not File.HasAvailableSource() Then Throw New ArgumentException("输入的下载链接不正确!")
File.LocalPath = File.LocalPath.Replace("/", "\")
If Not File.LocalPath.ToLower.Contains(":\") Then Throw New ArgumentException("输入的本地文件地址不正确:" & File.LocalPath)
If File.LocalPath.EndsWithF("\") Then Throw New ArgumentException("请输入含文件名的完整文件路径:" & File.LocalPath)
'文件夹检测
Dim DirPath As String = New FileInfo(File.LocalPath).Directory.FullName
If Not Directory.Exists(DirPath) Then Directory.CreateDirectory(DirPath)
Directory.CreateDirectory(GetPathFromFullPath(File.LocalPath)) '创建目标文件夹
Next
'接入下载管理器
NetManager.Start(Me)
'将文件分配给多个线程以进行已存在查找
Dim Folders As New List(Of String) '可能会用于已存在查找的文件夹列表
Dim FoldersFinal As New List(Of String) '最终用于查找的列表
If Not Setup.Get("SystemDebugSkipCopy") Then '在设置中禁用复制
Folders.Add(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\") '总是添加官启文件夹,因为 HMCL 会把所有文件存在这里
For Each Folder In McFolderList
Folders.Add(Folder.Path)
Next
Folders = Folders.Distinct.ToList
For Each Folder In Folders
If Folder <> PathMcFolder AndAlso Directory.Exists(Folder) Then FoldersFinal.Add(Folder)
Next
End If
'最多 5 个线程,最少每个线程分配 10 个文件
Dim FilesPerThread As Integer = Math.Max(5, Files.Count / 10 + 1)
Dim FilesInThread As New List(Of NetFile)
'====================================
' 已存在文件查找
'====================================
'整理允许进行查找的文件
Dim FilesToCheck As New List(Of NetFile)
Dim DisabledCopy As Boolean = Setup.Get("SystemDebugSkipCopy") '在设置中禁用了复制
For Each File In Files
FilesInThread.Add(File)
If FilesInThread.Count = FilesPerThread Then
Dim FilesToRun As New List(Of NetFile)
FilesToRun.AddRange(FilesInThread)
RunInNewThread(Sub() StartCopy(FilesToRun, FoldersFinal), "下载 文件复制 " & Uuid)
FilesInThread.Clear()
If Not DisabledCopy AndAlso File.Check?.CanUseExistsFile Then
FilesToCheck.Add(File)
Else '不允许,直接开始下载
SyncLock LockState
File.State = NetState.WaitingToDownload
File.IsCopy = False
End SyncLock
End If
Next
If FilesInThread.Any Then
Dim FilesToRun As New List(Of NetFile)
FilesToRun.AddRange(FilesInThread)
RunInNewThread(Sub() StartCopy(FilesToRun, FoldersFinal), "下载 文件复制 " & Uuid)
FilesInThread.Clear()
If Not FilesToCheck.Any Then Return
'获取 MC 文件夹列表
Dim Folders As New List(Of String)
Folders.Add(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\") '总是添加官启文件夹,因为 HMCL 会把所有文件存在这里
Folders.AddRange(McFolderList.Select(Function(f) f.Location))
Folders = Folders.Distinct.Where(Function(f) Directory.Exists(f)).ToList
'平均分配到多个检查线程
Dim ThreadCount As Integer = MathClamp(FilesToCheck.Count \ 40, 1, 8) '每个线程至少 40 个文件,最多 8 线程
If ThreadCount = 1 Then '只有一个线程,直接执行
CheckExistingFiles(FilesToCheck, Folders)
Else
Dim BaseSize = FilesToCheck.Count \ ThreadCount
Dim Remainder = FilesToCheck.Count Mod ThreadCount
Dim Index = 0
For i = 0 To ThreadCount - 1
Dim Size = BaseSize + If(i < Remainder, 1, 0)
Dim ThreadFiles = FilesToCheck.GetRange(Index, Size)
Index += Size
RunInNewThread(Sub() CheckExistingFiles(ThreadFiles, Folders), $"下载 文件复制 {Uuid}/{GetUuid()}")
Next
End If
Catch ex As Exception
OnFail(New List(Of Exception) From {ex})
OnFail(New List(Of Exception) From {New Exception("下载初始化失败", ex)})
End Try
End Sub, "L/下载 " & Uuid)
End Sub
Private Sub StartCopy(Files As List(Of NetFile), FolderList As List(Of String))
Private Sub CheckExistingFiles(Files As List(Of NetFile), FolderList As List(Of String))
Try
If ModeDebug Then Log($"[Download] 检查线程分配文件数:{Files.Count},线程名:{Thread.CurrentThread.Name}")
'试图从已存在的 Minecraft 文件夹中寻找目标文件
Dim ExistFiles As New List(Of KeyValuePair(Of NetFile, String)) '{NetFile, Target As String}
For Each File As NetFile In Files
Dim ExistFilePath As String = Nothing
'判断是否有已存在的文件
If File.Check IsNot Nothing AndAlso McFolderList IsNot Nothing AndAlso PathMcFolder IsNot Nothing AndAlso
File.Check.CanUseExistsFile AndAlso File.LocalPath.StartsWithF(PathMcFolder) Then
Dim Relative = File.LocalPath.Replace(PathMcFolder, "")
For Each Folder In FolderList
Dim Target = Folder & Relative
If File.Check.Check(Target) Is Nothing Then
ExistFilePath = Target
Exit For
End If
If ModeDebug Then Log($"[Download] 文件检查线程已启动,分配的文件数:{Files.Count}")
'列出 MC 文件夹中的各个版本文件夹
Dim VersionFolders As New List(Of String)
For Each McFolder In FolderList
Dim VersionsFolder As New DirectoryInfo(McFolder & "versions\")
If VersionsFolder.Exists() Then
For Each VersionFolder In VersionsFolder.GetDirectories
VersionFolders.Add(VersionFolder.FullName & "\")
Next
End If
'若存在,则改变状态
SyncLock LockState
If ExistFilePath IsNot Nothing Then
File.State = NetState.WaitForCopy
File.IsCopy = True
ExistFiles.Add(New KeyValuePair(Of NetFile, String)(File, ExistFilePath))
Else
File.State = NetState.WaitForDownload
File.IsCopy = False
End If
End SyncLock
Next
'复制已存在的文件
For Each FileToken In ExistFiles
Dim File As NetFile = FileToken.Key
SyncLock LockState
If File.State > NetState.WaitForCopy Then Return
End SyncLock
Dim LocalPath As String = FileToken.Value
Dim RetryCount As Integer = 0
'处理每个文件
For Each File As NetFile In Files
Dim Target As String = CheckExistingFile(FolderList, VersionFolders, File)
If File.State >= NetState.WaitingToDownload Then Return '中断
If Target Is Nothing Then
'未找到相同文件
SyncLock LockState
File.State = NetState.WaitingToDownload
File.IsCopy = False
End SyncLock
Else
'已找到相同文件
File.IsCopy = True
Dim RetryCount As Integer = 0
Retry:
Try
Log("[Download] 复制已存在的文件(" & LocalPath & ")")
CopyFile(LocalPath, File.LocalPath)
File.Finish(False)
Catch ex As Exception
RetryCount += 1
Log(ex, $"复制已存在的文件失败,重试第 {RetryCount} 次({LocalPath} -> {File.LocalPath})")
If RetryCount < 3 Then
Thread.Sleep(200)
GoTo Retry
End If
File.State = NetState.WaitForDownload
File.IsCopy = False
End Try
Try
If Target <> File.LocalPath Then
Log($"[Download] 复制已存在的文件:{Target} → {File.LocalPath}")
CopyFile(Target, File.LocalPath)
End If
File.Finish(False)
Catch ex As Exception
RetryCount += 1
Log(ex, $"复制已存在的文件失败,第 {RetryCount} 次重试({Target} → {File.LocalPath})")
If RetryCount < 3 Then
Thread.Sleep(200)
GoTo Retry
End If
'失败,回退到下载
SyncLock LockState
File.State = NetState.WaitingToDownload
File.IsCopy = False
End SyncLock
End Try
End If
Next
Catch ex As Exception
Log(ex, "下载已存在文件查找失败", LogLevel.Feedback)
OnFail(New List(Of Exception) From {New Exception("下载已存在文件查找失败", ex)})
End Try
End Sub
Private Function CheckExistingFile(FolderList As List(Of String), VersionFolders As List(Of String), File As NetFile) As String
'目标文件已存在
If File.Check.Check(File.LocalPath) Is Nothing Then Return File.LocalPath
'没有可用的检查规则,只能开始下载
If File.Check.Hash Is Nothing AndAlso File.Check.ActualSize < 0 Then Return Nothing
'大致判断文件类别
Dim TypeIndexes =
{"\assets\", "\libraries\", "\versions\", "\mods\", "\coremods\", "\lib\", "\resourcepacks\", "\texturepacks\", "\shaderpacks\"}.
Select(Function(FolderName) (FolderName, File.LocalPath.IndexOfF(FolderName, True))).
Where(Function(kv) kv.Item2 >= 0).ToList
If Not TypeIndexes.Any Then
If File.LocalName.EndsWithF(".jar") Then
TypeIndexes.Add(("\versions\", 1)) '总是对 jar 进行版本文件检查,以包括另存为 jar 的情况
Else
Return Nothing
End If
End If
Dim Type = TypeIndexes.MaxOf(Function(kv) kv.Item2).FolderName.TrimStart("\"c)
'根据类别进行查找
Select Case Type
Case "assets\", "libraries\"
'assets/libraries:查找 MC 文件夹下的相同路径
For Each Folder In FolderList
Dim Candidate = Folder & Type & File.LocalPath.AfterFirst(Type)
If File.Check.Check(Candidate) Is Nothing Then Return Candidate
Next
Case "versions\"
'版本 jar 或 json:查找 MC 文件夹下的各个版本文件夹
For Each VersionFolder In VersionFolders
For Each Candidate In Directory.GetFiles(VersionFolder,
"*." & GetFileNameFromPath(File.LocalPath).AfterLast(".").ToLower, SearchOption.TopDirectoryOnly)
If File.Check.Check(Candidate) Is Nothing Then Return Candidate
Next
Next
Case Else
'社区资源
If File.Check.ActualSize < 0 OrElse File.Check.Hash Is Nothing Then Return Nothing '必须要求指定了文件大小和 Hash
For Each Folder In FolderList.Concat(VersionFolders)
Dim TargetFolder = Folder & Type
If Not Directory.Exists(TargetFolder) Then Continue For
For Each Candidate In Directory.GetFiles(TargetFolder)
'快速进行大小校验
Static Sizes As New SafeDictionary(Of String, Long)
If Not Sizes.ContainsKey(Candidate) Then Sizes(Candidate) = New FileInfo(Candidate).Length
If File.Check.ActualSize <> Sizes(Candidate) Then Continue For
'Hash 校验
If File.Check.Check(Candidate) Is Nothing Then Return Candidate
Next
Next
End Select
Return Nothing
End Function
Public Sub OnFileFinish(File As NetFile)
'要求全部文件完成
@@ -1655,7 +1707,7 @@ Retry:
[Error] = If(UsefulExs.FirstOrDefault, ExList.FirstOrDefault)
'获取实际失败的文件
For Each File In Files
If File.State <> NetState.Error Then Continue For
If File.State <> NetState.Interrupted Then Continue For
If File.Sources.All(Function(s) TypeOf s.Ex Is OperationCanceledException OrElse TypeOf s.Ex Is TimeoutException OrElse TypeOf s.Ex Is ThreadInterruptedException) Then Continue For
Dim Detail As String = Join(File.Sources.Select(Function(s) $"{If(s.Ex Is Nothing, "无错误信息。", s.Ex.GetBrief())}({s.Url})"), vbCrLf)
[Error] = New Exception("文件下载失败:" & File.LocalPath & vbCrLf &
@@ -1736,14 +1788,6 @@ Retry:
Public FileRemain As Integer = 0
Public ReadOnly LockRemain As New Object
''' <summary>
''' 上次记速时的已下载大小。
''' </summary>
Private SpeedLastDone As Long = 0
''' <summary>
''' 至多最近 30 次下载速度的记录,较新的在前面。
''' </summary>
Private SpeedLast As New List(Of Long)
'这些属性由 RefreshStat 刷新
''' <summary>
''' 当前的全局下载速度,单位为 Byte / 秒。
@@ -1764,6 +1808,8 @@ Retry:
RefreshStatLast += DeltaTime
#Region "刷新整体速度"
'计算瞬时速度
Static SpeedLast As New List(Of Long) '记录至多最近 30 次下载速度的记录,较新的在前面
Static SpeedLastDone As Long = 0 '上次记速时的已下载大小
Dim ActualSpeed As Double = Math.Max(0, (DownloadDone - SpeedLastDone) / (DeltaTime / 1000))
SpeedLast.Insert(0, ActualSpeed)
If SpeedLast.Count >= 31 Then SpeedLast.RemoveAt(30)
@@ -1799,8 +1845,9 @@ Retry:
''' 启动监控线程,用于新增下载线程。
''' </summary>
Private Sub StartManager()
If IsManagerStarted Then Return
IsManagerStarted = True
Static IsStarted As Boolean = False
If IsStarted Then Return
IsStarted = True
Dim ThreadStarter =
Sub(Id As Integer) '0 或 1
Try
@@ -1816,9 +1863,9 @@ Retry:
Dim OngoingFiles As New List(Of NetFile)
For Each File As NetFile In AllFiles
If File.Uuid Mod 2 = Id Then Continue For
If File.State = NetState.WaitForDownload Then
If File.State = NetState.WaitingToDownload Then
WaitingFiles.Add(File)
ElseIf File.State < NetState.Merge Then
ElseIf File.State < NetState.Merging Then
OngoingFiles.Add(File)
End If
Next
@@ -1826,7 +1873,7 @@ Retry:
For Each File As NetFile In WaitingFiles
If NetTaskThreadCount >= NetTaskThreadLimit Then Continue While '最大线程数检查
Dim NewThread = File.TryBeginThread()
If NewThread IsNot Nothing AndAlso NewThread.Source.Url.Contains("bmclapi") Then Thread.Sleep(40) '减少 BMCLAPI 请求频率(目前每分钟限制 4000 次)
If NewThread IsNot Nothing AndAlso NewThread.Source.Url.Contains("bmclapi") Then Thread.Sleep(100) '减少 BMCLAPI 请求频率
Next
'为进行中的文件追加线程
If Speed >= NetTaskSpeedLimitLow Then Continue While '下载速度足够,无需新增
@@ -1836,9 +1883,9 @@ Retry:
Dim PreparingCount = 0, DownloadingCount = 0
If File.Threads IsNot Nothing Then
For Each Thread As NetThread In File.Threads.ToList
If Thread.State < NetState.Download Then
If Thread.State < NetState.Downloading Then
PreparingCount += 1
ElseIf Thread.State = NetState.Download Then
ElseIf Thread.State = NetState.Downloading Then
DownloadingCount += 1
End If
Next
@@ -1846,7 +1893,7 @@ Retry:
'新增线程
If PreparingCount > DownloadingCount Then Continue For '准备中的线程已多于下载中的线程,不再新增
Dim NewThread = File.TryBeginThread()
If NewThread IsNot Nothing AndAlso NewThread.Source.Url.Contains("bmclapi") Then Thread.Sleep(40) '减少 BMCLAPI 请求频率(目前每分钟限制 4000 次)
If NewThread IsNot Nothing AndAlso NewThread.Source.Url.Contains("bmclapi") Then Thread.Sleep(100) '减少 BMCLAPI 请求频率
Next
End While
Catch ex As Exception
@@ -1858,27 +1905,26 @@ Retry:
RunInNewThread(
Sub()
Try
Dim LastLoopTime As Long
NetTaskSpeedLimitLeftLast = GetTimeTick()
Dim NextTick As Long = GetTimeTick()
While True
Dim TimeNow = GetTimeTick()
LastLoopTime = TimeNow
'增加限速余量
If NetTaskSpeedLimitHigh > 0 Then NetTaskSpeedLimitLeft = NetTaskSpeedLimitHigh / 1000 * (TimeNow - NetTaskSpeedLimitLeftLast)
NetTaskSpeedLimitLeftLast = TimeNow
If NetTaskSpeedLimitHigh > 0 Then NetTaskSpeedLimitLeft = NetTaskSpeedLimitHigh / 10
'刷新公开属性
RefreshStat()
'等待直至 80 ms
Do While GetTimeTick() - LastLoopTime < 80
Thread.Sleep(10)
Loop
'等待 100 ms
NextTick += 100
Dim SleepTime = NextTick - GetTimeTick()
If SleepTime > 0 Then
Thread.Sleep(SleepTime)
Else
NextTick = GetTimeTick() '超时,直接追帧,不等待
End If
End While
Catch ex As Exception
Log(ex, "下载管理刷新线程出错", LogLevel.Critical)
End Try
End Sub, "NetManager StatRefresher")
End Sub
Private IsManagerStarted As Boolean = False
Private DownloadCacheLock As New Object
Private IsDownloadCacheCleared As Boolean = False
@@ -1907,7 +1953,7 @@ Retry:
Dim File = Task.Files(i)
If Files.ContainsKey(File.LocalPath) Then
'已有该文件
If Files(File.LocalPath).State >= NetState.Finish Then
If Files(File.LocalPath).State >= NetState.Finished Then
'该文件已经下载过一次,且下载完成
'将已下载的文件替换成当前文件,重新下载
File.Tasks.Add(Task)
@@ -1915,7 +1961,7 @@ Retry:
SyncLock LockRemain
FileRemain += 1
End SyncLock
If ModeDebug Then Log($"[Download] {File.LocalName}:重新加入下载列表")
If ModeDebug Then Log($"[Download] {File.LocalName}:重新加入下载列表,目标 {File.LocalPath}")
Else
'该文件正在下载中
'将当前文件替换成下载中的文件,即两个任务指向同一个文件
@@ -1929,7 +1975,7 @@ Retry:
SyncLock LockRemain
FileRemain += 1
End SyncLock
If ModeDebug Then Log($"[Download] {File.LocalName}:已加入下载列表")
If ModeDebug Then Log($"[Download] {File.LocalName}:已加入下载列表,目标 {File.LocalPath}")
End If
Task.Files(i) = File '回设
Next
@@ -2017,7 +2063,15 @@ Retry:
''' 判断某个 Exception 是否为网络问题所导致。
''' </summary>
<Extension> Public Function IsNetworkRelated(Ex As Exception) As Boolean
Dim Detail = Ex.GetDetail()
'提取 Message(不能用 GetDetail,因为堆栈的方法参数中就有 timeout 字样)
Dim Detail As String = ""
Dim InnerEx As Exception = Ex
Detail += InnerEx.Message
Do Until InnerEx.InnerException Is Nothing
InnerEx = InnerEx.InnerException
Detail += InnerEx.Message
Loop
'判断
If Detail.Contains("(403)") Then Return False
Return {
"(408)", "超时", "timeout", "网络请求失败", "连接尝试失败", "远程主机强迫关闭了", "远程方已关闭传输流", "未能解析此远程名称",