This commit is contained in:
龙腾猫跃
2026-03-24 22:53:27 +08:00
parent 43963f8f7c
commit 6a140ce5b9
72 changed files with 28308 additions and 27967 deletions
@@ -8,7 +8,12 @@ Public Module ModAnimation
''' <summary>
''' 动画速度。最大为 200。
''' </summary>
Public AniSpeed As Double = 1
Public ReadOnly Property AniSpeed As Double
Get
Dim Setting = PCL.Settings.Get("SystemDebugAnim")
Return If(Setting >= 30, 200, MathClamp(Setting * 0.1 + 0.1, 0.1, 3))
End Get
End Property
''' <summary>
''' 动画组列表。
''' </summary>
+156 -52
View File
@@ -14,13 +14,13 @@ Public Module ModBase
#Region "声明"
'下列版本信息由更新器自动修改
Public Const VersionBaseName As String = "2.12.3" '不含分支前缀的显示用版本名
Public Const VersionStandardCode As String = "2.12.3." & VersionBranchCode '标准格式的四段式版本号
Public Const VersionBaseName As String = "2.12.4" '不含分支前缀的显示用版本名
Public Const VersionStandardCode As String = "2.12.4." & VersionBranchCode '标准格式的四段式版本号
Public Const CommitHash As String = "" 'Commit Hash,由 GitHub Workflow 自动替换
#If BETA Then
Public Const VersionCode As Integer = 381 'Release
Public Const VersionCode As Integer = 383 'Release
#Else
Public Const VersionCode As Integer = 382 'Snapshot
Public Const VersionCode As Integer = 384 'Snapshot
#End If
'自动生成的版本信息
Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName
@@ -56,10 +56,6 @@ Public Module ModBase
''' </summary>
Public Lang As String = "zh_CN"
''' <summary>
''' 设置对象。
''' </summary>
Public Setup As New ModSetup
''' <summary>
''' 程序的打开计时。
''' </summary>
Public ApplicationStartTick As Long = GetTimeMs()
@@ -86,11 +82,11 @@ Public Module ModBase
''' <summary>
''' 系统盘盘符,以 \ 结尾。例如 “C:\”。
''' </summary>
Public OsDrive As String = Environment.GetLogicalDrives().Where(Function(p) Directory.Exists(p)).First.ToUpper.First & ":\" '#3799
Public OsDrive As String = Environment.GetLogicalDrives().Where(Function(p) Directory.Exists(p)).First.Upper.First & ":\" '#3799
''' <summary>
''' 程序的缓存文件夹路径,以 \ 结尾。
''' </summary>
Public PathTemp As String = If(Setup.Get("SystemSystemCache") = "", IO.Path.GetTempPath() & "PCL\", Setup.Get("SystemSystemCache")).ToString.Replace("/", "\").TrimEnd("\") & "\"
Public PathTemp As String = If(Settings.Get("SystemSystemCache") = "", IO.Path.GetTempPath() & "PCL\", Settings.Get("SystemSystemCache")).ToString.Replace("/", "\").TrimEnd("\") & "\"
''' <summary>
''' AppData 中的 PCL 文件夹路径,以 \ 结尾。
''' </summary>
@@ -416,17 +412,13 @@ Public Module ModBase
Loading
Finished
Failed
Aborted
Interrupted
End Enum
''' <summary>
''' 执行返回值。
''' </summary>
Public Enum ProcessReturnValues
''' <summary>
''' 执行成功,或进程被中断。
''' </summary>
Aborted = -1
''' <summary>
''' 执行成功。
''' </summary>
@@ -620,7 +612,8 @@ Public Module ModBase
Public Sub DeleteReg(Key As String, Optional ThrowException As Boolean = False)
Try
Dim SubKey As Microsoft.Win32.RegistryKey = My.Computer.Registry.CurrentUser.OpenSubKey("Software\" & RegFolder, True)
SubKey?.DeleteValue(Key)
If SubKey?.GetValue(Key) Is Nothing Then Return
SubKey.DeleteValue(Key)
Catch ex As Exception
Log(ex, "删除注册表出错:" & Key, If(ThrowException, LogLevel.Hint, LogLevel.Developer))
If ThrowException Then Throw
@@ -1253,11 +1246,11 @@ Re:
End If
If Not String.IsNullOrEmpty(Hash) Then
If Hash.Length < 35 Then 'MD5
If Hash.ToLowerInvariant <> GetFileMD5(LocalPath) Then Return "文件 MD5 应为 " & Hash & ",实际为 " & GetFileMD5(LocalPath)
If Hash.Lower <> GetFileMD5(LocalPath) Then Return "文件 MD5 应为 " & Hash & ",实际为 " & GetFileMD5(LocalPath)
ElseIf Hash.Length = 64 Then 'SHA256
If Hash.ToLowerInvariant <> GetFileSHA256(LocalPath) Then Return "文件 SHA256 应为 " & Hash & ",实际为 " & GetFileSHA256(LocalPath)
If Hash.Lower <> GetFileSHA256(LocalPath) Then Return "文件 SHA256 应为 " & Hash & ",实际为 " & GetFileSHA256(LocalPath)
Else 'SHA1 (40)
If Hash.ToLowerInvariant <> GetFileSHA1(LocalPath) Then Return "文件 SHA1 应为 " & Hash & ",实际为 " & GetFileSHA1(LocalPath)
If Hash.Lower <> GetFileSHA1(LocalPath) Then Return "文件 SHA1 应为 " & Hash & ",实际为 " & GetFileSHA1(LocalPath)
End If
End If
If IsJson Then
@@ -1287,7 +1280,7 @@ Re:
'解压 gz(gz 不需要考虑编码)
If CompressFilePath.EndsWithF(".gz", True) Then
Using stream As New GZipStream(New FileStream(CompressFilePath, FileMode.Open, FileAccess.Read, FileShare.Read), CompressionMode.Decompress)
Using decompressedFile As New FileStream(DestDirectory & GetFileNameFromPath(CompressFilePath).ToLower.Replace(".tar", "").Replace(".gz", ""),
Using decompressedFile As New FileStream(DestDirectory & GetFileNameFromPath(CompressFilePath).Lower.Replace(".tar", "").Replace(".gz", ""),
FileMode.OpenOrCreate, FileAccess.Write)
Dim data As Integer = stream.ReadByte()
While data <> -1
@@ -1332,7 +1325,7 @@ Re:
Public Function DeleteDirectory(Path As String, Optional IgnoreIssue As Boolean = False) As Integer
If Not Directory.Exists(Path) Then Return 0
If Not Path.EndsWithF("\") Then Path &= "\"
If Path.EndsWith(":\") OrElse
If Path.EndsWithF(":\") OrElse
Path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) & "\" OrElse
Path = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) & "\" OrElse
Path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) & "\" Then '#7030
@@ -1416,17 +1409,25 @@ RetryDir:
''' <summary>
''' 若路径长度大于指定值,则将长路径转换为短路径。
''' 如果路径不存在,则返回原始路径。
''' </summary>
Public Function ShortenPath(LongPath As String) As String
If LongPath.Length <= 200 OrElse LongPath.StartsWithF("http", True) Then Return LongPath
If Not Directory.Exists(LongPath) AndAlso Not File.Exists(LongPath) Then Return LongPath
Dim ShortPath As New StringBuilder(260)
GetShortPathName(LongPath.Replace("/", "\"), ShortPath, 260) '第一个参数也会在调用后被修改,使用 Replace 后不会影响原始字符串
ShortenPath = ShortPath.ToString
If String.IsNullOrEmpty(ShortenPath) Then Return LongPath
Public Function ShortenPath(Raw As String) As String
If Raw.Length <= 200 OrElse Raw.StartsWithF("http", True) Then Return Raw
If Not Directory.Exists(Raw) AndAlso Not File.Exists(Raw) Then Return Raw
Dim PathToShorten As String, Ignored As String
If Not Directory.Exists(Raw) AndAlso Raw.EndsWithF(".jar", True) Then '只能缩短文件夹部分,必须保留 jar 文件名,否则会导致 Forge 1.20.1 无法通过文件名识别模块名
PathToShorten = GetPathFromFullPath(Raw)
Ignored = GetFileNameFromPath(Raw)
Else
PathToShorten = Raw
Ignored = ""
End If
Dim ShortPathBuffer As New StringBuilder(260)
GetShortPathNameW(PathToShorten.Replace("/", "\"), ShortPathBuffer, 260)
Dim DirShortPath As String = ShortPathBuffer.ToString()
If String.IsNullOrEmpty(DirShortPath) Then Return Raw
Return If(String.IsNullOrEmpty(Ignored), DirShortPath, IO.Path.Combine(DirShortPath, Ignored))
End Function
Private Declare Function GetShortPathName Lib "kernel32" Alias "GetShortPathNameA" (lpszLongPath As String, lpszShortPath As StringBuilder, cchBuffer As Integer) As Integer
Private Declare Unicode Function GetShortPathNameW Lib "kernel32" Alias "GetShortPathNameW" (lpszLongPath As String, lpszShortPath As StringBuilder, cchBuffer As Integer) As Integer
#End Region
@@ -1493,7 +1494,7 @@ RetryDir:
Ex = Ex.InnerException
Loop
DescList = DescList.Distinct.ToList
Dim Desc As String = Join(DescList, vbCrLf & "→ ")
Dim Desc As String = DescList.Join(vbCrLf & "→ ")
'构造输出信息
Dim UsualReason As String = AnalyzeUsualReason(InnerEx, OuterEx, DescList)
@@ -1501,7 +1502,7 @@ RetryDir:
Return UsualReason & "详细错误:" & DescList.First
Else
DescList.Reverse() '让最深层错误在最左边
Return Join(DescList, " ← ")
Return DescList.Join(" ← ")
End If
End Function
Private Function AnalyzeUsualReason(InnerEx As Exception, OuterEx As Exception, DescList As List(Of String)) As String
@@ -1584,7 +1585,7 @@ RetryDir:
''' </summary>
<Extension> Public Function Capitalize(word As String) As String
If String.IsNullOrEmpty(word) Then Return word
Return word.Substring(0, 1).ToUpperInvariant() & word.Substring(1).ToLowerInvariant()
Return word.Substring(0, 1).Upper() & word.Substring(1).Lower()
End Function
''' <summary>
@@ -1604,7 +1605,24 @@ RetryDir:
''' <summary>
''' 连接字符串。
''' </summary>
<Extension> Public Function Join(List As IEnumerable, Split As Char) As String
Dim Builder As New StringBuilder
Dim IsFirst As Boolean = True
For Each Element In List
If IsFirst Then
IsFirst = False
Else
Builder.Append(Split)
End If
If Element IsNot Nothing Then Builder.Append(Element)
Next
Return Builder.ToString
End Function
''' <summary>
''' 连接字符串。
''' </summary>
<Extension> Public Function Join(List As IEnumerable, Split As String) As String
If Split.IsSingle Then Return List.Join(Split(0))
Dim Builder As New StringBuilder
Dim IsFirst As Boolean = True
For Each Element In List
@@ -1622,7 +1640,7 @@ RetryDir:
''' 若原始字符串为空,则返回 {""}。
''' </summary>
<Extension> Public Function Split(FullStr As String, SplitStr As String) As String()
If SplitStr.Length = 1 Then
If SplitStr.IsSingle Then
Return FullStr.Split(SplitStr(0))
Else
Return FullStr.Split({SplitStr}, StringSplitOptions.None)
@@ -1795,6 +1813,19 @@ RetryDir:
End Try
End Function
''' <summary>
''' 等效 ToLowerInvariant。
''' </summary>
<Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)> Public Function Lower(Str As String) As String
Return Str.ToLowerInvariant()
End Function
''' <summary>
''' 等效 ToUpperInvariant。
''' </summary>
<Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)> Public Function Upper(Str As String) As String
Return Str.ToUpperInvariant()
End Function
'转义
''' <summary>
''' 为字符串进行 XML 转义。
@@ -1887,8 +1918,8 @@ RetryDir:
If String.IsNullOrEmpty(Source) OrElse String.IsNullOrEmpty(Query) Then Return 0
Dim qp As Integer = 0, lenSum As Double = 0
Dim str As New StringBuilder(Source.Length)
str.Append(Source.ToLower().Replace(" ", ""))
Query = Query.ToLower().Replace(" ", "")
str.Append(Source.Lower().Replace(" ", ""))
Query = Query.Lower().Replace(" ", "")
Dim sourceLength As Integer = str.Length, queryLength As Integer = Query.Length '用于计算最后因数的长度缓存
If queryLength = 0 Then Return 0
Do While qp < queryLength
@@ -1977,6 +2008,7 @@ RetryDir:
''' <summary>
''' 进行多段文本加权搜索,获取相似度较高的数项结果。
''' 在搜索时,会对每个文本源单独加权,但单个文本源内的多个别名只取最高的一个的相似度。
''' 这会修改 Entries 中每项的 Similarity 与 AbsoluteRight 字段。
''' </summary>
''' <param name="MaxBlurCount">返回的最大模糊结果数。</param>
''' <param name="MinBlurSimilarity">返回结果要求的最低相似度。</param>
@@ -1997,14 +2029,7 @@ RetryDir:
If Entry.AbsoluteRight OrElse Entry.Similarity >= MinBlurSimilarity Then Candidates.Add(Entry)
Next
'按照相似度进行排序
Candidates = Candidates.SortByComparison(
Function(Left, Right) As Boolean
If Left.AbsoluteRight Xor Right.AbsoluteRight Then
Return Left.AbsoluteRight
Else
Return Left.Similarity > Right.Similarity
End If
End Function)
Candidates = Candidates.SortByComparison(Function(Left, Right) If(Left.AbsoluteRight <> Right.AbsoluteRight, Left.AbsoluteRight, Left.Similarity > Right.Similarity))
'返回结果
Dim BlurCount As Integer = 0
For Each Entry In Candidates
@@ -2319,6 +2344,22 @@ NextElement:
Return Arr?.Count > 0
End Function
''' <summary>
''' 判断这是否是仅有一个元素的集合。
''' </summary>
<Extension> Public Function IsSingle(Of T)(Source As IEnumerable(Of T)) As Boolean
If Source Is Nothing Then
Return False
ElseIf TypeOf Source Is IList(Of T) Then
Return DirectCast(Source, IList(Of T)).Count = 1
Else
Using Enumerator = Source.GetEnumerator()
If Not Enumerator.MoveNext() Then Return False
If Enumerator.MoveNext() Then Return False
Return True
End Using
End If
End Function
''' <summary>
''' 对集合的每个元素执行指定操作。
''' </summary>
<Extension> Public Function ForEach(Of T)(Collection As IEnumerable(Of T), Action As Action(Of T)) As IEnumerable(Of T)
@@ -2548,6 +2589,58 @@ NextElement:
End If
End Sub
''' <summary>
''' 选择所有最大值对应的对象。
''' 若没有元素则返回空列表。
''' </summary>
<Extension> Public Function MaxByAll(Of T, C As IComparable(Of C))(Source As IEnumerable(Of T), Selector As Func(Of T, C)) As List(Of T)
Dim Results As New List(Of T)
Using Enumerator = Source.GetEnumerator()
If Not Enumerator.MoveNext() Then Return Results
Dim MaxItem As T = Enumerator.Current
Dim MaxValue As C = Selector(MaxItem)
Results.Add(MaxItem)
While Enumerator.MoveNext()
Dim CurrentItem = Enumerator.Current
Dim CurrentValue = Selector(CurrentItem)
Dim Comparison = CurrentValue.CompareTo(MaxValue)
If Comparison > 0 Then
MaxValue = CurrentValue
Results.Clear()
Results.Add(CurrentItem)
ElseIf Comparison = 0 Then
Results.Add(CurrentItem)
End If
End While
End Using
Return Results
End Function
''' <summary>
''' 选择所有最小值对应的对象。
''' 若没有元素则返回空列表。
''' </summary>
<Extension> Public Function MinByAll(Of T, C As IComparable(Of C))(List As IEnumerable(Of T), Selector As Func(Of T, C)) As List(Of T)
Dim Results As New List(Of T)
Using Enumerator = List.GetEnumerator()
If Not Enumerator.MoveNext() Then Return Results
Dim MinItem As T = Enumerator.Current
Dim MinValue As C = Selector(MinItem)
Results.Add(MinItem)
While Enumerator.MoveNext()
Dim CurrentItem = Enumerator.Current
Dim CurrentValue = Selector(CurrentItem)
Dim Comparison = CurrentValue.CompareTo(MinValue)
If Comparison < 0 Then
MinValue = CurrentValue
Results.Clear()
Results.Add(CurrentItem)
ElseIf Comparison = 0 Then
Results.Add(CurrentItem)
End If
End While
End Using
Return Results
End Function
''' <summary>
''' 选择最大值对应的对象。
''' 若没有元素则返回 Nothing。
@@ -2683,7 +2776,7 @@ NextElement:
Try
Location = Location.Replace("/", "\").Trim(" "c, """"c)
Log("[System] 正在打开资源管理器:" & Location)
If Location.EndsWith("\") Then
If Location.EndsWithF("\") Then
StartProcess(Location)
Else
StartProcess("explorer", $"/select,""{Location}""")
@@ -2883,7 +2976,7 @@ Retry:
'类型检查
Using Reader As New XamlXmlReader(Stream)
While Reader.Read()
For Each BlackListType In {GetType(WebBrowser), GetType(Frame), GetType(MediaElement), GetType(ObjectDataProvider), GetType(XamlReader), GetType(Window), GetType(XmlDataProvider)}
For Each BlackListType In {GetType(WebBrowser), GetType(Frame), GetType(MediaElement), GetType(ObjectDataProvider), GetType(XamlReader), GetType(Window), GetType(XmlDataProvider), GetType(SettingService)}
If Reader.Type IsNot Nothing AndAlso BlackListType.IsAssignableFrom(Reader.Type.UnderlyingType) Then Throw New UnauthorizedAccessException($"基于安全考虑,不允许使用 {BlackListType.Name} 类型。")
If Reader.Value IsNot Nothing AndAlso Reader.Value = BlackListType.Name Then Throw New UnauthorizedAccessException($"基于安全考虑,不允许使用 {BlackListType.Name} 值。")
Next
@@ -2940,11 +3033,22 @@ Retry:
Return formattedText.Width
End Function
''' <summary>
''' 将布尔值转换为 Visibility。True 转换为 Visible,False 转换为 Collapsed。
''' </summary>
<Extension> Public Function ToVisibility(IsVisible As Boolean) As Visibility
Return If(IsVisible, Visibility.Visible, Visibility.Collapsed)
End Function
#End Region
#Region "Debug"
Public ModeDebug As Boolean = False
Public ReadOnly Property ModeDebug As Boolean
Get
Return Settings.Get("SystemDebugMode")
End Get
End Property
'Log
Public Enum LogLevel
@@ -3217,7 +3321,7 @@ Retry:
'遥测
Public Sub Telemetry([Event] As String, ParamArray Datas As String())
If Not Setup.Get("SystemSystemTelemetry") Then Return '用户关闭了遥测
If Not Settings.Get("SystemSystemTelemetry") Then Return '用户关闭了遥测
If Not ClsBaseUrl.StartsWithF("http") Then Return '开源版没有设置遥测地址
If VersionBranchName = "Debug" Then Return '开发版本不上传遥测
RunInNewThread(
@@ -3237,10 +3341,10 @@ Retry:
'获取当前的堆栈信息
Public Function GetStackTrace() As String
Dim Stack As New StackTrace()
Return Join(Stack.GetFrames().Skip(1).Select(Function(f) f.GetMethod).
Select(Function(f) f.Name & "(" & Join(f.GetParameters.Select(Function(p) p.ToString).ToList, ", ") & ") - " & f.Module.ToString).ToList,
vbCrLf).Replace(vbCrLf & vbCrLf, vbCrLf)
Return (New StackTrace).GetFrames().Skip(1).
Select(Function(f) f.GetMethod).
Select(Function(f) $"{f.Name}({f.GetParameters.Select(Function(p) p.ToString).Join(", ")}) - {f.Module}").
Join(vbCrLf).Replace(vbCrLf & vbCrLf, vbCrLf)
End Function
#End Region
@@ -88,7 +88,7 @@
Set(value As LoadState)
If _State = value Then Return
Dim OldState = _State
If value = LoadState.Finished AndAlso Setup.Get("SystemDebugDelay") Then Thread.Sleep(RandomInteger(100, 2000))
If value = LoadState.Finished AndAlso Settings.Get("SystemDebugDelay") Then Thread.Sleep(RandomInteger(100, 2000))
_State = value
If Not LogBlackList.Contains(Name) Then Log($"[Loader] 加载器 {Name} 状态改变:{GetStringFromEnum(value)}")
'实现 ILoadingTrigger 接口与 OnStateChanged 回调
@@ -149,7 +149,7 @@
Case LoadState.Waiting
Return 0
Case LoadState.Loading
Return If(_Progress = -1, 0.02, _Progress)
Return If(_Progress = -1, 0, _Progress)
Case Else
Return 1
End Select
@@ -170,7 +170,7 @@
'状态变化
Public MustOverride Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
Public MustOverride Sub Abort()
Public MustOverride Sub Interrupt()
Public MustOverride Sub Failed(Ex As Exception)
'等待结束
@@ -187,7 +187,7 @@
Loop
If State = LoadState.Finished Then
Return
ElseIf State = LoadState.Aborted Then
ElseIf State = LoadState.Interrupted Then
Throw New ThreadInterruptedException("加载器执行已中断。")
ElseIf IsNothing([Error]) Then
Throw New Exception("未知错误!")
@@ -210,7 +210,7 @@
Loop
If State = LoadState.Finished Then
Return
ElseIf State = LoadState.Aborted Then
ElseIf State = LoadState.Interrupted Then
Throw New ThreadInterruptedException("加载器执行已中断。")
ElseIf IsNothing([Error]) Then
Throw New Exception("未知错误!")
@@ -241,16 +241,16 @@
''' <summary>
''' 当前执行线程是否应当中断。只应用在加载器的工作线程中判断,不可跨线程调用。
''' </summary>
Public ReadOnly Property IsAborted As Boolean
Public ReadOnly Property IsInterrupted As Boolean
Get
Return IsAbortedWithThread(Thread.CurrentThread)
Return IsInterruptedWithThread(Thread.CurrentThread)
End Get
End Property
''' <summary>
''' 当前执行线程是否应当中断。需要手动提供加载器线程,用于需要跨线程检查的情况。
''' </summary>
Public Function IsAbortedWithThread(Thread As Thread) As Boolean
Return LastRunningThread Is Nothing OrElse Not ReferenceEquals(Thread, LastRunningThread) OrElse State = LoadState.Aborted
Public Function IsInterruptedWithThread(Thread As Thread) As Boolean
Return LastRunningThread Is Nothing OrElse Not ReferenceEquals(Thread, LastRunningThread) OrElse State = LoadState.Interrupted
End Function
''' <summary>
''' 在输入相同时使用原有结果的超时,单位为毫秒。
@@ -305,7 +305,7 @@
'确认是否开始加载
If ShouldStart(Input, IsForceRestart) Then
'输入不同或失败,开始加载
If State = LoadState.Loading Then TriggerThreadAbort()
If State = LoadState.Loading Then TriggerThreadInterrupt()
Me.Input = Input
SyncLock LockState
State = LoadState.Loading
@@ -321,7 +321,7 @@
IsForceRestarting = IsForceRestart
If ModeDebug AndAlso Not LogBlackList.Contains(Name) Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已{If(IsForceRestarting, "强制", "")}启动")
LoadDelegate(Me)
If IsAborted Then
If IsInterrupted Then
Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId)})", LogLevel.Developer)
Return
End If
@@ -331,18 +331,18 @@
LastFinishedTime = GetTimeMs() '未中断,本次输出有效
Catch ex As CancelledException
If ModeDebug Then Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已触发取消中断,已完成 {Math.Round(Progress * 100)}%")
If Not IsAborted Then State = LoadState.Aborted
If Not IsInterrupted Then State = LoadState.Interrupted
Catch ex As ThreadInterruptedException
If ModeDebug Then Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已触发线程中断,已完成 {Math.Round(Progress * 100)}%")
'如果线程是因为判断到 IsAborted 而提前中止,则代表已有新线程被重启,此时不应当改为 Aborted
'如果线程是在没有 IsAborted 时手动引发了 ThreadInterruptedException,则代表没有重启线程,这通常代表用户手动取消,应当改为 Aborted
If Not IsAborted Then State = LoadState.Aborted
'如果线程是因为判断到 IsInterrupted 而提前中止,则代表已有新线程被重启,此时不应当改为 Interrupted
'如果线程是在没有 IsInterrupted 时手动引发了 ThreadInterruptedException,则代表没有重启线程,这通常代表用户手动取消,应当改为 Interrupted
If Not IsInterrupted Then State = LoadState.Interrupted
Catch ex As Exception
Failed(ex)
End Try
End Sub) With {.Name = "L/" & Name, .Priority = ThreadPriority}
Try
LastRunningThread.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted
LastRunningThread.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsInterrupted
Catch ex As ThreadStateException '若遇到偶发的 “线程正在运行或被终止”,则等待后重试
Thread.Sleep(500)
LastRunningThread.Start()
@@ -351,20 +351,20 @@
Public Overrides Sub Failed(ex As Exception)
[Error] = ex
SyncLock LockState
If IsAborted OrElse State >= LoadState.Finished Then Return
If IsInterrupted OrElse State >= LoadState.Finished Then Return
State = LoadState.Failed
End SyncLock
Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 出错,已完成 {Math.Round(Progress * 100)}%", LogLevel.Developer)
TriggerThreadAbort()
TriggerThreadInterrupt()
End Sub
Public Overrides Sub Abort()
Public Overrides Sub Interrupt()
SyncLock LockState
If State <> LoadState.Loading Then Return
State = LoadState.Aborted
State = LoadState.Interrupted
End SyncLock
TriggerThreadAbort()
TriggerThreadInterrupt()
End Sub
Private Sub TriggerThreadAbort()
Private Sub TriggerThreadInterrupt()
If LastRunningThread Is Nothing Then Return
If LastRunningThread.IsAlive Then
LastRunningThread.Interrupt()
@@ -451,11 +451,11 @@
End If
RunInThread(AddressOf Update)
End Sub
Public Overrides Sub Abort()
Public Overrides Sub Interrupt()
'改变状态
SyncLock LockState
If State = LoadState.Loading OrElse State = LoadState.Waiting Then
State = LoadState.Aborted
State = LoadState.Interrupted
Else
Return
End If
@@ -464,7 +464,7 @@
Sub()
'中断加载器
For Each Loader In Loaders
Loader.Abort()
Loader.Interrupt()
Next
End Sub)
End Sub
@@ -475,7 +475,7 @@
State = LoadState.Failed
End SyncLock
For Each Loader In Loaders
Loader.Abort()
Loader.Interrupt()
Next
FrmMain.BtnExtraDownload.ShowRefresh()
End Sub
@@ -493,9 +493,9 @@
Case LoadState.Finished
'正常结束,触发刷新
Update()
Case LoadState.Aborted
Case LoadState.Interrupted
'被中断,这个任务也中断
Abort()
Interrupt()
Case LoadState.Failed
'完蛋,出错了
Failed(New Exception(Loader.Name & "失败", Loader.Error))
@@ -505,7 +505,7 @@
''' 触发一次更新,以启动新加载器或完成。
''' </summary>
Private Sub Update()
If State = LoadState.Finished OrElse State = LoadState.Failed OrElse State = LoadState.Aborted Then Return
If State = LoadState.Finished OrElse State = LoadState.Failed OrElse State = LoadState.Interrupted Then Return
Dim IsFinished As Boolean = True
Dim Blocked As Boolean = False
Dim Input As Object = Me.Input
@@ -607,7 +607,7 @@ Restart:
'若单个任务已中止,或全部任务已完成,则刷新并移除
For Each Task In LoaderTaskbar
If LoaderTaskbar.All(Function(l) l.State <> LoadState.Loading) OrElse
(Task.State = LoadState.Waiting OrElse Task.State = LoadState.Aborted) Then
(Task.State = LoadState.Waiting OrElse Task.State = LoadState.Interrupted) Then
FrmSpeedLeft?.TaskRefresh(Task)
LoaderTaskbar.Remove(Task)
Log($"[Taskbar] {Task.Name} 已移出任务列表")
+69 -23
View File
@@ -160,7 +160,7 @@ RequestFinished:
NetRequestByLoader = ReadFile(Temp)
File.Delete(Temp)
Finally
NewTask.Abort()
NewTask.Interrupt()
End Try
End Function
@@ -208,7 +208,7 @@ RequestFinished:
Catch ex As Exception
Throw New WebException($"多线程直接下载文件失败(第一下载源:" & Urls.First() & ")", ex)
Finally
NewTask.Abort()
NewTask.Interrupt()
End Try
End Sub
@@ -238,7 +238,7 @@ RequestFinished:
End If
'写入 Headers
For Each Header In If(Headers, New Dictionary(Of String, String))
If Header.Key.ToLower = "content-type" Then
If Header.Key.Lower = "content-type" Then
Request.Content?.Headers.TryAddWithoutValidation(Header.Key, Header.Value)
Else
Request.Headers.TryAddWithoutValidation(Header.Key, Header.Value)
@@ -307,8 +307,10 @@ RequestFinished:
''' 返回请求时应使用的 IP。若解析失败,则返回 Nothing。
''' </summary>
Private Function DNSLookup(Request As HttpRequestMessage, CancelToken As CancellationToken) As String
'若一分钟内已失败过,则不再重复尝试解析,以减少断网时的 Exception 数量
Dim Host = Request.RequestUri.Host
'不对部分有严格 SNI 限制的域名进行 DNS 解析(#8295)
If {"mojang.com", "minecraft.net", "minecraftservices.com"}.Any(Function(h) Host.ContainsF(h, True)) Then Return Nothing
'若一分钟内已失败过,则不再重复尝试解析,以减少断网时的 Exception 数量
SyncLock DNSFailureRecord.SyncRoot
If DNSFailureRecord.ContainsKey(Host) Then
If Date.Now - DNSFailureRecord(Host) < New TimeSpan(0, 1, 0) Then
@@ -399,7 +401,11 @@ RequestFinished:
''' <summary>
''' 最大线程数。
''' </summary>
Public NetTaskThreadLimit As Integer
Public ReadOnly Property NetTaskThreadLimit As Integer
Get
Return Settings.Get("ToolDownloadThread") + 1
End Get
End Property
''' <summary>
''' 速度下限。
''' </summary>
@@ -407,7 +413,27 @@ RequestFinished:
''' <summary>
''' 速度上限。若无限制则为 -1。
''' </summary>
Public NetTaskSpeedLimitHigh As Long = -1
Public ReadOnly Property NetTaskSpeedLimitHigh As Long
Get
If _NetTaskSpeedLimitHigh Is Nothing Then
Dim Setting = PCL.Settings.Get("ToolDownloadSpeed")
If Setting <= 14 Then
_NetTaskSpeedLimitHigh = (Setting + 1) * 0.1 * 1024 * 1024L
ElseIf Setting <= 31 Then
_NetTaskSpeedLimitHigh = (Setting - 11) * 0.5 * 1024 * 1024L
ElseIf Setting <= 41 Then
_NetTaskSpeedLimitHigh = (Setting - 21) * 1024 * 1024L
Else
_NetTaskSpeedLimitHigh = -1
End If
End If
Return _NetTaskSpeedLimitHigh.Value
End Get
End Property
Private _NetTaskSpeedLimitHigh As Long? = Nothing
Public Sub UpdateNetTaskSpeedLimitHigh(Value As Integer)
_NetTaskSpeedLimitHigh = Nothing
End Sub
''' <summary>
''' 基于限速,当前可以下载的剩余量。
''' </summary>
@@ -917,7 +943,7 @@ StartSingleThreadDownload:
Dim Source As NetSource = GetSource()
If Source Is Nothing Then Return Nothing
Dim TargetUrl As String = Source.Url
If TargetUrl.Contains("pcl2-server") OrElse TargetUrl.Contains("bmclapi") OrElse TargetUrl.Contains("github.com") OrElse
If TargetUrl.Contains("pcl2-server") OrElse TargetUrl.Contains("meloong.com") OrElse TargetUrl.Contains("bmclapi") OrElse TargetUrl.Contains("github.com") OrElse
TargetUrl.Contains("optifine.net") OrElse TargetUrl.Contains("momot.rs") Then Return Nothing
'寻找最大碎片
'FUTURE: 下载引擎重做,计算下载源平均链接时间和线程下载速度,按最高时间节省来开启多线程
@@ -1053,7 +1079,7 @@ NotSupportRange:
End If
'开始下载
ResponseStream = Response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
If Setup.Get("SystemDebugDelay") Then Threading.Thread.Sleep(RandomInteger(50, 3000))
If Settings.Get("SystemDebugDelay") Then Threading.Thread.Sleep(RandomInteger(50, 3000))
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 '判断是否下载完成
@@ -1145,7 +1171,7 @@ SourceBreak:
If IsMergeFailure OrElse IsRangeNotSupported OrElse
ex.Message.Contains("(502)") OrElse ex.Message.Contains("(404)") OrElse
ex.Message.Contains("未能解析") OrElse ex.Message.Contains("无返回数据") OrElse ex.Message.Contains("空间不足") OrElse
((ex.Message.Contains("(403)") OrElse ex.Message.Contains("(429)")) AndAlso Not Th.Source.Url.ContainsF("bmclapi")) OrElse 'BMCLAPI 的部分源在高频率请求下会返回 403/429,所以不应因此禁用下载源
((ex.Message.Contains("(403)") OrElse ex.Message.Contains("(429)")) AndAlso Not Th.Source.Url.Contains("bmclapi")) OrElse 'BMCLAPI 的部分源在高频率请求下会返回 403/429,所以不应因此禁用下载源
(Th.Source.FailCount >= MathClamp(NetTaskThreadLimit, 5, 30) AndAlso DownloadDone < 1) OrElse Th.Source.FailCount > NetTaskThreadLimit + 2 Then
'当一个下载源有多个线程在下载时,只选择其中一个线程进行后续处理
Dim IsThisFail As Boolean = False
@@ -1323,7 +1349,7 @@ Retry:
Log($"[Download] {LocalName}:已失败,当前状态 {State}")
State = NetState.Interrupted
End SyncLock
AbortInternal()
InterruptInternal()
For Each Task In Tasks
Task.OnFileFail(Me)
Next
@@ -1331,9 +1357,9 @@ Retry:
''' <summary>
''' 下载中断。
''' </summary>
Public Sub Abort(AbortedTask As LoaderDownload)
Public Sub Interrupt(InterruptedTask As LoaderDownload)
'从特定任务中移除,如果它还属于其他任务,则继续下载
Tasks.Remove(AbortedTask)
Tasks.Remove(InterruptedTask)
If Tasks.Any Then Return
'确认中断
SyncLock LockState
@@ -1341,9 +1367,9 @@ Retry:
Log($"[Download] {LocalName}:已中断,当前状态 {State}")
State = NetState.Interrupted
End SyncLock
AbortInternal()
InterruptInternal()
End Sub
Private Sub AbortInternal()
Private Sub InterruptInternal()
On Error Resume Next
Reset()
Interlocked.Decrement(NetManager.FileRemain)
@@ -1488,7 +1514,7 @@ FinishExCatch:
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 Not File.LocalPath.Lower.Contains(":\") Then Throw New ArgumentException("输入的本地文件地址不正确:" & File.LocalPath)
If File.LocalPath.EndsWithF("\") Then Throw New ArgumentException("请输入含文件名的完整文件路径:" & File.LocalPath)
Directory.CreateDirectory(GetPathFromFullPath(File.LocalPath)) '创建目标文件夹
Next
@@ -1591,7 +1617,7 @@ Retry:
'目标文件已存在
If File.Check.Check(File.LocalPath) Is Nothing Then Return File.LocalPath
'在设置中禁用了复制
If Setup.Get("SystemDebugSkipCopy") Then Return Nothing
If Settings.Get("SystemDebugSkipCopy") Then Return Nothing
'没有可用的检查规则,只能开始下载
If File.Check.Hash Is Nothing AndAlso File.Check.ActualSize < 0 Then Return Nothing
'大致判断文件类别
@@ -1620,7 +1646,7 @@ Retry:
'版本 jar 或 json:查找 MC 文件夹下的各个版本文件夹
For Each VersionFolder In VersionFolders
For Each Candidate In Directory.GetFiles(VersionFolder,
"*." & GetFileNameFromPath(File.LocalPath).AfterLast(".").ToLower, SearchOption.TopDirectoryOnly)
"*." & GetFileNameFromPath(File.LocalPath).AfterLast(".").Lower, SearchOption.TopDirectoryOnly)
'快速进行大小校验
If Not Sizes.ContainsKey(Candidate) Then Sizes(Candidate) = New FileInfo(Candidate).Length
If File.Check.ActualSize <> Sizes(Candidate) Then Continue For
@@ -1682,7 +1708,7 @@ Retry:
For Each File In Files
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)
Dim Detail As String = File.Sources.Select(Function(s) $"{If(s.Ex Is Nothing, "无错误信息。", s.Ex.GetBrief())}({s.Url})").Join(vbCrLf)
[Error] = New Exception("文件下载失败:" & File.LocalPath & vbCrLf &
"各下载源的错误如下:" & vbCrLf & Detail, [Error])
'上报
@@ -1696,24 +1722,24 @@ Retry:
End SyncLock
'中断所有文件
For Each TaskFile In Files
TaskFile.Abort(Me)
TaskFile.Interrupt(Me)
Next
'在退出同步锁后再进行日志输出
Dim ErrOutput As New List(Of String)
For Each Ex As Exception In ExList
ErrOutput.Add(Ex.GetDetail())
Next
Log("[Download] " & Join(ErrOutput.Distinct.ToArray, vbCrLf))
Log("[Download] " & ErrOutput.Distinct.Join(vbCrLf))
End Sub
Public Overrides Sub Abort()
Public Overrides Sub Interrupt()
SyncLock LockState
If State >= LoadState.Finished Then Return
State = LoadState.Aborted
State = LoadState.Interrupted
End SyncLock
Log("[Download] " & Name & " 已取消!")
'中断所有文件
For Each TaskFile In Files
TaskFile.Abort(Me)
TaskFile.Interrupt(Me)
Next
End Sub
@@ -2023,4 +2049,24 @@ Retry:
}.Any(Function(k) Detail.ContainsF(k, True))
End Function
''' <summary>
''' 设置是否应当在正版登录时校验 SSL 证书。
''' </summary>
Public Sub ShouldValidateSslCertificateOnLogin(Enabled As Boolean)
ServicePointManager.ServerCertificateValidationCallback =
Function(Sender, Certificate, Chain, Failure)
Dim Request As HttpWebRequest = TryCast(Sender, HttpWebRequest)
If Failure = Net.Security.SslPolicyErrors.None Then Return True '已通过验证
'基于 #3018 和 #5879,只在访问正版登录 API 时跳过证书验证
Log($"[System] 未通过 SSL 证书验证({Failure}),提供的证书为 {Certificate?.Subject},URL:{Request?.Address}", LogLevel.Debug)
If Request Is Nothing Then
Return Not Enabled
ElseIf Request.Address.Host.Contains("xboxlive") OrElse Request.Address.Host.Contains("minecraftservices") Then
Return Not Enabled '根据设置决定是否忽略错误
Else
Return False
End If
End Function
End Sub
End Module
@@ -208,7 +208,7 @@ Public Class ValidateExceptSame
If Str Is Nothing Then Return ErrorMessage.Replace("%", "null")
For Each Ch As String In Excepts
If IgnoreCase Then
If Str.ToLower = Ch.ToLower Then Return ErrorMessage.Replace("%", Ch)
If Str.Lower = Ch.Lower Then Return ErrorMessage.Replace("%", Ch)
Else
If Str.Equals(Ch) Then Return ErrorMessage.Replace("%", Ch) '使用 = 不确定是否会忽略大小写
End If
@@ -218,6 +218,27 @@ Public Class ValidateExceptSame
End Class
''' <summary>
''' 必须为特定字符串。
''' </summary>
Public Class ValidateSame
Inherits Validate
Public Property Required As String
Public Property ErrorMessage As String
Public Property IgnoreCase As Boolean = False
Public Sub New()
End Sub
Public Sub New(Required As String, Optional ErrorMessage As String = "输入内容有误", Optional IgnoreCase As Boolean = False)
Me.Required = Required
Me.ErrorMessage = ErrorMessage
Me.IgnoreCase = IgnoreCase
End Sub
Public Overrides Function Validate(Str As String) As String
Return If(String.Compare(Str, Required, IgnoreCase) = 0, "", ErrorMessage)
End Function
End Class
''' <summary>
''' 对文件夹名的粗略的特化检测。
''' </summary>
@@ -355,7 +376,7 @@ Public Class ValidateFolderPath
'检查开头
If Str.StartsWithF("\\Mac\") Then GoTo Fin
For Each Drive As DriveInfo In My.Computer.FileSystem.Drives
If Str.ToUpper = Drive.Name Then Return ""
If Str.Upper = Drive.Name Then Return ""
If Str.StartsWithF(Drive.Name, True) Then GoTo Fin
Next
Return "文件夹路径头存在错误!"