2.13.0.0
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
'动画引擎模块
|
||||
'动画引擎模块
|
||||
'使用 Ani 作为方法或属性的开头,使用 Aa 作为单个动画对象的开头(便于自动补全)
|
||||
|
||||
Imports System.Windows.Media.Effects
|
||||
@@ -832,7 +832,12 @@ Public Module ModAnimation
|
||||
AniCount = 0
|
||||
AniTimer(DeltaTime * AniSpeed)
|
||||
If RandomInteger(0, 64 * If(ModeDebug, 5, 30)) = 0 AndAlso ((AniFPS < 62 AndAlso AniFPS > 0) OrElse AniCount > 4 OrElse NetManager.FileRemain <> 0) Then
|
||||
Logger.Info($"FPS {AniFPS}, 动画 {AniCount}, 下载中 {NetManager.FileRemain}({FormatFileSize(NetManager.Speed)}/s)")
|
||||
Logger.Info($"FPS {AniFPS}, 动画 {AniCount}, 下载中 {NetManager.FileRemain}({StringUtils.FormatByteSize(NetManager.Speed)}/s)")
|
||||
If NetManager.FileRemain < 5 Then
|
||||
For Each File In NetManager.AllFiles.Values
|
||||
If File.State < NetState.Finished Then Logger.Trace($"- {File.State} {StringUtils.FormatByteSize(File.Speed)}/s {File.LocalPath}")
|
||||
Next
|
||||
End If
|
||||
End If
|
||||
End Sub)
|
||||
Sleeper:
|
||||
@@ -1018,7 +1023,7 @@ NextAni:
|
||||
If Convert.ToInt32(Convert.ToChar(NextText)) >= Convert.ToInt32(Convert.ToChar(128)) Then
|
||||
NewText &= Encoding.GetEncoding("GB18030").GetString({RandomInteger(16 + 160, 87 + 160), RandomInteger(1 + 160, 89 + 160)})
|
||||
Else
|
||||
NewText &= RandomOne("0123456789./*-+\[]{};':/?,!@#$%^&*()_+-=qwwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM".ToCharArray)
|
||||
NewText &= RandomInteger(0, 85).ConvertRadix(86)
|
||||
End If
|
||||
End If
|
||||
'设置文本
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
Imports System.Globalization
|
||||
Imports System.Runtime.CompilerServices
|
||||
Imports System.Security.Cryptography
|
||||
Imports System.Threading.Tasks
|
||||
Imports System.Xaml
|
||||
Imports Newtonsoft.Json
|
||||
|
||||
Public Module ModBase
|
||||
|
||||
#Region "声明"
|
||||
|
||||
'下列版本信息由更新器自动修改
|
||||
Public Const VersionBaseName As String = "2.12.8.2" '显示用版本名
|
||||
Public Const VersionBaseName As String = "2.13.0.0" '显示用版本名
|
||||
Public Const CommitHash As String = "" 'Commit Hash,由 GitHub Workflow 自动替换
|
||||
#If RELEASE Then
|
||||
Public Const VersionCode As Integer = 404 '正式版
|
||||
Public Const VersionCode As Integer = 406 '正式版
|
||||
#Else
|
||||
Public Const VersionCode As Integer = 403 '快照版
|
||||
Public Const VersionCode As Integer = 405 '快照版
|
||||
#End If
|
||||
|
||||
'版本信息
|
||||
@@ -41,13 +37,9 @@ Public Module ModBase
|
||||
''' </summary>
|
||||
Public Handle As IntPtr
|
||||
''' <summary>
|
||||
''' 程序的启动路径,以 \ 结尾。
|
||||
''' </summary>
|
||||
Public PathExeFolder As String = AppDomain.CurrentDomain.SetupInformation.ApplicationBase
|
||||
''' <summary>
|
||||
''' 包含程序名的完整路径。
|
||||
''' </summary>
|
||||
Public PathExe As String = PathExeFolder & AppDomain.CurrentDomain.SetupInformation.ApplicationName
|
||||
Public PathExe As String = Paths.Base & AppDomain.CurrentDomain.SetupInformation.ApplicationName
|
||||
''' <summary>
|
||||
''' 程序内嵌图片文件夹路径,以 / 结尾。
|
||||
''' </summary>
|
||||
@@ -57,10 +49,6 @@ Public Module ModBase
|
||||
''' </summary>
|
||||
Public PathTemp As String = If(Settings.Get(Of String)("SystemSystemCache") = "", Path.GetTempPath() & "PCL\", Settings.Get(Of String)("SystemSystemCache")).ToString.Replace("/", "\").TrimEnd("\") & "\"
|
||||
''' <summary>
|
||||
''' AppData 中的 PCL 文件夹路径,以 \ 结尾。
|
||||
''' </summary>
|
||||
Public PathAppdata As String = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\PCL\"
|
||||
''' <summary>
|
||||
''' 当前程序的语言。
|
||||
''' </summary>
|
||||
Public Lang As String = "zh_CN"
|
||||
@@ -81,10 +69,6 @@ Public Module ModBase
|
||||
''' </summary>
|
||||
Public IsProgramEnding As Boolean = False
|
||||
''' <summary>
|
||||
''' 是否为 32 位系统。
|
||||
''' </summary>
|
||||
Public Is32BitSystem As Boolean = Not Environment.Is64BitOperatingSystem
|
||||
''' <summary>
|
||||
''' 是否使用 GBK 编码。
|
||||
''' </summary>
|
||||
Public IsGBKEncoding As Boolean = Encoding.Default.CodePage = 936
|
||||
@@ -139,7 +123,7 @@ Public Module ModBase
|
||||
''' <summary>
|
||||
''' 图标按钮,刷新,0.85x
|
||||
''' </summary>
|
||||
Public Const IconButtonRefresh As String = "M875.52 148.48C783.36 56.32 655.36 0 512 0 291.84 0 107.52 138.24 30.72 332.8l122.88 46.08C204.8 230.4 348.16 128 512 128c107.52 0 199.68 40.96 271.36 112.64L640 384h384V0L875.52 148.48zM512 896c-107.52 0-199.68-40.96-271.36-112.64L384 640H0v384l148.48-148.48C240.64 967.68 368.64 1024 512 1024c220.16 0 404.48-138.24 481.28-332.8L870.4 645.12C819.2 793.6 675.84 896 512 896z"
|
||||
Public Const IconButtonRefresh As String = "M512.0 838.3c-80.2 0-153.4-29.3-210.2-77.4l75.5-75.5c11.5-11.5 25.8-22.0 25.8-37.0a27.2 27.2 0 0 0-27.1-27.1H104.0c-27.1 0-27.1 23.9-27.1 27.1v271.9a27.1 27.1 0 0 0 27.1 27.1c15.0 0 27.8-16.6 42.5-31.2l77.9-77.9c76.6 67.7 177.1 108.9 287.4 108.9 221.7 0 404.5-166.0 431.2-380.6h-109.8c-25.9 154.2-159.7 271.9-321.3 271.9zM919.9 76.6c-15.0 0-27.8 16.6-42.5 31.3L799.5 185.8c-76.5-67.7-177.1-108.9-287.4-108.9-221.8 0-404.5 166.1-431.3 380.6H190.6c25.9-154.2 159.7-271.9 321.4-271.9 80.2 0 153.4 29.3 210.1 77.4l-75.5 75.5c-11.6 11.5-25.8 22.0-25.8 37.1a27.2 27.2 0 0 0 27.1 27.1h271.9c27.1 0 27.1-23.9 27.1-27.1V103.8a27.1 27.1 0 0 0-27.1-27.1z"
|
||||
''' <summary>
|
||||
''' 图标按钮,软盘,1x
|
||||
''' </summary>
|
||||
@@ -153,10 +137,18 @@ Public Module ModBase
|
||||
''' </summary>
|
||||
Public Const IconButtonList As String = "M384 128h640v128H384zM160 192m-96 0a96 96 0 1 0 192 0 96 96 0 1 0-192 0ZM384 448h640v128H384zM160 512m-96 0a96 96 0 1 0 192 0 96 96 0 1 0-192 0ZM384 768h640v128H384zM160 832m-96 0a96 96 0 1 0 192 0 96 96 0 1 0-192 0Z"
|
||||
''' <summary>
|
||||
''' 图标按钮,文件夹,1.15x
|
||||
''' 图标按钮,文件夹,1.1x
|
||||
''' </summary>
|
||||
Public Const IconButtonOpen As String = "M889.018182 418.909091H884.363636V316.509091a93.090909 93.090909 0 0 0-99.607272-89.832727h-302.545455l-93.090909-76.334546A46.545455 46.545455 0 0 0 358.865455 139.636364H146.152727A93.090909 93.090909 0 0 0 46.545455 229.469091V837.818182a46.545455 46.545455 0 0 0 46.545454 46.545454 46.545455 46.545455 0 0 0 16.756364-3.258181 109.381818 109.381818 0 0 0 25.134545 3.258181h586.472727a85.178182 85.178182 0 0 0 87.04-63.301818l163.374546-302.545454a46.545455 46.545455 0 0 0 5.585454-21.876364A82.385455 82.385455 0 0 0 889.018182 418.909091z m-744.727273-186.181818h198.283636l93.09091 76.334545a46.545455 46.545455 0 0 0 29.323636 10.705455h319.301818a12.101818 12.101818 0 0 1 6.516364 0V418.909091H302.545455a85.178182 85.178182 0 0 0-87.04 63.301818L139.636364 622.778182V232.727273a19.549091 19.549091 0 0 1 6.516363 0z m578.094546 552.029091a27.461818 27.461818 0 0 0-2.792728 6.516363H154.530909l147.083636-272.290909a27.461818 27.461818 0 0 0 2.792728-6.981818h565.061818z"
|
||||
''' <summary>
|
||||
''' 图标按钮,上箭头,0.95x
|
||||
''' </summary>
|
||||
Public Const IconButtonArrowUp As String = "M554 333V853h-85V333l-228 228-60-60L512 170l331 331-60 60L554 333z"
|
||||
''' <summary>
|
||||
''' 图标按钮,下箭头,0.95x
|
||||
''' </summary>
|
||||
Public Const IconButtonArrowDown As String = "M554 691V171h-85V691L241 463l-60 60L512 854l331-331-60-60L554 691z"
|
||||
''' <summary>
|
||||
''' 图标按钮,名片,1.1x
|
||||
''' </summary>
|
||||
Public Const IconButtonCard As String = "M834.5 684.1c-31.2-70.4-98.9-120.9-179.1-127.3 63.5-8.5 112.6-63 112.6-128.8 0-71.8-58.2-130-130-130s-130 58.2-130 130c0 65.9 49 120.3 112.6 128.8-80.2 6.4-148 57-179.1 127.3-8.7 19.7 6 42 27.6 42 12.1 0 22.7-7.5 27.7-18.5 24.3-53.9 78.5-91.5 141.3-91.5s117 37.6 141.3 91.5c5 11.1 15.6 18.5 27.7 18.5 21.4 0 36.1-22.3 27.4-42zM567.9 427.9c0-38.6 31.4-70 70-70s70 31.4 70 70-31.4 70-70 70-70-31.4-70-70zM460.3 347.9H216.9c-16.6 0-30 13.4-30 30s13.4 30 30 30h243.3c16.6 0 30-13.4 30-30 0.1-16.5-13.4-30-29.9-30zM367.4 459.6H216.9c-16.6 0-30 13.4-30 30s13.4 30 30 30h150.4c16.6 0 30-13.4 30-30 0.1-16.6-13.4-30-29.9-30zM297.4 571.2H217c-16.6 0-30 13.4-30 30s13.4 30 30 30h80.4c16.6 0 30-13.4 30-30 0-16.5-13.5-30-30-30zM900 236v552H124V236h776m0-60H124c-33.1 0-60 26.9-60 60v552c0 33.1 26.9 60 60 60h776c33.1 0 60-26.9 60-60V236c0-33.1-26.9-60-60-60z"
|
||||
@@ -436,7 +428,7 @@ Public Module ModBase
|
||||
Loading
|
||||
Finished
|
||||
Failed
|
||||
Interrupted
|
||||
Canceled
|
||||
End Enum
|
||||
|
||||
''' <summary>
|
||||
@@ -531,7 +523,7 @@ Public Module ModBase
|
||||
''' </summary>
|
||||
''' <param name="FileName">文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。</param>
|
||||
Public Sub IniClearCache(FileName As String)
|
||||
If Not FileName.Contains(":\") Then FileName = $"{PathExeFolder}PCL\{FileName}.ini"
|
||||
If Not FileName.Contains(":\") Then FileName = $"{Paths.Base}PCL\{FileName}.ini"
|
||||
IniCache.Remove(FileName)
|
||||
End Sub
|
||||
''' <summary>
|
||||
@@ -542,7 +534,7 @@ Public Module ModBase
|
||||
Private Function IniGetContent(FileName As String) As ConcurrentDictionary(Of String, String)
|
||||
Try
|
||||
'还原文件路径
|
||||
If Not FileName.Contains(":\") Then FileName = $"{PathExeFolder}PCL\{FileName}.ini"
|
||||
If Not FileName.Contains(":\") Then FileName = $"{Paths.Base}PCL\{FileName}.ini"
|
||||
'检索缓存
|
||||
Dim Cache As ConcurrentDictionary(Of String, String) = Nothing
|
||||
If IniCache.TryGetValue(FileName, Cache) Then Return Cache
|
||||
@@ -616,9 +608,9 @@ Public Module ModBase
|
||||
FileContent.Append(Pair.Value)
|
||||
FileContent.Append(vbCrLf)
|
||||
Next
|
||||
If Not FileName.Contains(":\") Then FileName = $"{PathExeFolder}PCL\{FileName}.ini"
|
||||
If Not FileName.Contains(":\") Then FileName = $"{Paths.Base}PCL\{FileName}.ini"
|
||||
'处理相对路径
|
||||
FileName = If(FileName.Contains(":\"), FileName, PathExeFolder & FileName)
|
||||
FileName = If(FileName.Contains(":\"), FileName, Paths.Base & FileName)
|
||||
FileUtils.Write(FileName, FileContent.ToString)
|
||||
Catch ex As Exception
|
||||
Logger.Error(ex, $"写入文件失败({FileName} → {Key}:{Value})", LogBehavior.Toast)
|
||||
@@ -657,142 +649,12 @@ Public Module ModBase
|
||||
FileUtils.Delete(TestFilePath)
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' 文件的校验规则。
|
||||
''' </summary>
|
||||
Public Class FileChecker
|
||||
''' <summary>
|
||||
''' 文件的准确大小。
|
||||
''' 不检查则为 -1。
|
||||
''' </summary>
|
||||
Public ActualSize As Long = -1
|
||||
''' <summary>
|
||||
''' 文件的最小大小。
|
||||
''' 不检查则为 -1。
|
||||
''' </summary>
|
||||
Public MinSize As Long = -1
|
||||
''' <summary>
|
||||
''' 文件的 MD5、SHA1 或 SHA256。会根据输入字符串的长度自动判断种类。
|
||||
''' 不检查则为 Nothing。
|
||||
''' </summary>
|
||||
Public Hash As String = Nothing
|
||||
''' <summary>
|
||||
''' 是否可以使用已经存在的文件。
|
||||
''' </summary>
|
||||
Public CanUseExistsFile As Boolean = True
|
||||
''' <summary>
|
||||
''' 是否要求为 JSON 文件。
|
||||
''' 即,开头结尾必须为 {} 或 []。
|
||||
''' </summary>
|
||||
Public IsJson As Boolean = False
|
||||
Public Sub New(Optional MinSize As Long = -1, Optional ActualSize As Long = -1, Optional Hash As String = Nothing, Optional CanUseExistsFile As Boolean = True, Optional IsJson As Boolean = False)
|
||||
Me.ActualSize = ActualSize
|
||||
Me.MinSize = MinSize
|
||||
Me.Hash = Hash
|
||||
Me.CanUseExistsFile = CanUseExistsFile
|
||||
Me.IsJson = IsJson
|
||||
End Sub
|
||||
''' <summary>
|
||||
''' 检查文件。若成功则返回 Nothing,失败则返回错误的描述文本,描述文本不以句号结尾。不会抛出错误。
|
||||
''' </summary>
|
||||
Public Function Check(LocalPath As String) As String
|
||||
Try
|
||||
Dim Info = FileUtils.GetInfo(LocalPath)
|
||||
If Not Info.Exists Then Return "文件不存在:" & LocalPath
|
||||
Dim FileSize As Long = Info.Length
|
||||
If ActualSize >= 0 AndAlso ActualSize <> FileSize Then
|
||||
Return $"文件大小应为 {ActualSize} B,实际为 {FileSize} B" &
|
||||
If(FileSize < 2000, ",内容为:" & FileUtils.ReadAsString(LocalPath), "")
|
||||
End If
|
||||
If MinSize >= 0 AndAlso MinSize > FileSize Then
|
||||
Return $"文件大小应大于 {MinSize} B,实际为 {FileSize} B" &
|
||||
If(FileSize < 2000, ",内容为:" & FileUtils.ReadAsString(LocalPath), "")
|
||||
End If
|
||||
If Not String.IsNullOrEmpty(Hash) Then
|
||||
If Hash.Length < 35 Then 'MD5
|
||||
Dim md5 As String = CryptographyUtils.ComputeFileHash(LocalPath, CryptographyUtils.HashMethod.Md5)
|
||||
If Hash.Lower <> md5 Then Return "文件 MD5 应为 " & Hash & ",实际为 " & md5
|
||||
ElseIf Hash.Length = 64 Then 'SHA256
|
||||
Dim sha256 As String = CryptographyUtils.ComputeFileHash(LocalPath, CryptographyUtils.HashMethod.Sha256)
|
||||
If Hash.Lower <> sha256 Then Return "文件 SHA256 应为 " & Hash & ",实际为 " & sha256
|
||||
Else 'SHA1 (40)
|
||||
Dim sha1 As String = CryptographyUtils.ComputeFileHash(LocalPath, CryptographyUtils.HashMethod.Sha1)
|
||||
If Hash.Lower <> sha1 Then Return "文件 SHA1 应为 " & Hash & ",实际为 " & sha1
|
||||
End If
|
||||
End If
|
||||
If IsJson Then
|
||||
Dim Content As String = FileUtils.ReadAsString(LocalPath)
|
||||
If Content = "" Then Throw New Exception("读取到的文件为空")
|
||||
Try
|
||||
GetJson(Content)
|
||||
Catch ex As Exception
|
||||
Throw New Exception("不是有效的 json 文件", ex)
|
||||
End Try
|
||||
End If
|
||||
Return Nothing
|
||||
Catch ex As Exception
|
||||
Logger.Warn(ex, "检查文件出错")
|
||||
Return ex.GetDisplay(False)
|
||||
End Try
|
||||
End Function
|
||||
End Class
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "文本"
|
||||
Public vbLQ As Char = Convert.ToChar(8220)
|
||||
Public vbRQ As Char = Convert.ToChar(8221)
|
||||
|
||||
''' <summary>
|
||||
''' 将对象转换为枚举类型,支持数字或原文两种格式,若为空字符串或 Nothing 则返回 0。
|
||||
''' 若转换失败会抛出异常。
|
||||
''' </summary>
|
||||
<Extension> Public Function ToEnum(Of T As Structure)(Value As Object) As T
|
||||
If IsNumeric(Value) Then
|
||||
Return CType(CInt(Value), Object)
|
||||
ElseIf String.IsNullOrEmpty(Value?.ToString) Then
|
||||
Return CType(0, Object)
|
||||
Else
|
||||
Return [Enum].Parse(GetType(T), Value.ToString(), True)
|
||||
End If
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 将文件大小转化为适合的文本形式,如“1.28 M”。
|
||||
''' </summary>
|
||||
''' <param name="FileSize">以字节为单位的大小表示。</param>
|
||||
Public Function FormatFileSize(FileSize As Long) As String
|
||||
Dim IsNegative = FileSize < 0
|
||||
If IsNegative Then FileSize *= -1
|
||||
If FileSize < 1000 Then
|
||||
'B 级
|
||||
Return If(IsNegative, "-", "") & FileSize & " B"
|
||||
ElseIf FileSize < 1024 * 1000 Then
|
||||
'K 级
|
||||
Dim RoundResult As String = Math.Round(FileSize / 1024)
|
||||
Return If(IsNegative, "-", "") & Math.Round(FileSize / 1024, CInt((3 - RoundResult.Length).Clamp(0, 2))) & " K"
|
||||
ElseIf FileSize < 1024 * 1024 * 1000 Then
|
||||
'M 级
|
||||
Dim RoundResult As String = Math.Round(FileSize / 1024 / 1024)
|
||||
Return If(IsNegative, "-", "") & Math.Round(FileSize / 1024 / 1024, CInt((3 - RoundResult.Length).Clamp(0, 2))) & " M"
|
||||
Else
|
||||
'G 级
|
||||
Dim RoundResult As String = Math.Round(FileSize / 1024 / 1024 / 1024)
|
||||
Return If(IsNegative, "-", "") & Math.Round(FileSize / 1024 / 1024 / 1024, CInt((3 - RoundResult.Length).Clamp(0, 2))) & " G"
|
||||
End If
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
''' 获取 JSON 对象。
|
||||
''' </summary>
|
||||
Public Function GetJson(Data As String) As JToken
|
||||
Try
|
||||
Return JsonConvert.DeserializeObject(Data, New JsonSerializerSettings With {.DateTimeZoneHandling = DateTimeZoneHandling.Local})
|
||||
Catch ex As Exception
|
||||
Dim Length As Integer = If(Data, "").Length
|
||||
Throw New Exception("格式化 JSON 失败:" & If(Length > 2000, Data.Substring(0, 500) & $"...(全长 {Length} 个字符)..." & Right(Data, 500), Data), ex)
|
||||
End Try
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
''' 不会报错的 Val。
|
||||
''' 如果输入有误,返回 0。
|
||||
@@ -958,7 +820,7 @@ Public Module ModBase
|
||||
TokenSource.Cancel()
|
||||
Throw New TimeoutException($"任务超时({TimeoutMs} ms)")
|
||||
End If
|
||||
Return TargetTask.GetAwaiter().GetResult()
|
||||
Return TargetTask.Run()
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 为 Task 设置超时,在超时时抛出 TimeoutException。
|
||||
@@ -969,7 +831,7 @@ Public Module ModBase
|
||||
TokenSource.Cancel()
|
||||
Throw New TimeoutException($"任务超时({TimeoutMs} ms)")
|
||||
End If
|
||||
TargetTask.GetAwaiter().GetResult()
|
||||
TargetTask.Run()
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
@@ -977,10 +839,10 @@ Public Module ModBase
|
||||
''' </summary>
|
||||
Public PathPure As String = GetPureASCIIDir()
|
||||
Private Function GetPureASCIIDir() As String
|
||||
If (PathExeFolder & "PCL").IsAsciiOnly() Then
|
||||
Return PathExeFolder & "PCL\"
|
||||
ElseIf PathAppdata.IsAsciiOnly() Then
|
||||
Return PathAppdata
|
||||
If Paths.Base.IsAsciiOnly() Then
|
||||
Return Paths.Base & "PCL\"
|
||||
ElseIf Paths.AppDataThenName.IsAsciiOnly() Then
|
||||
Return Paths.AppDataThenName
|
||||
ElseIf PathTemp.IsAsciiOnly() Then
|
||||
Return PathTemp
|
||||
Else
|
||||
@@ -994,12 +856,6 @@ Public Module ModBase
|
||||
Public Class RestartException
|
||||
Inherits Exception
|
||||
End Class
|
||||
''' <summary>
|
||||
''' 指示用户手动取消了操作,或用户已知晓操作被取消的原因。
|
||||
''' </summary>
|
||||
Public Class CancelledException
|
||||
Inherits Exception
|
||||
End Class
|
||||
|
||||
''' <summary>
|
||||
''' 以管理员权限运行当前程序,并等待程序运行结束。
|
||||
@@ -1011,19 +867,6 @@ Public Module ModBase
|
||||
Return NewProcess.ExitCode
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
''' 判断对象是否为某个泛型类型的实例。
|
||||
''' </summary>
|
||||
<Extension> Public Function IsInstanceOfGenericType(genericType As Type, obj As Object) As Boolean
|
||||
If obj Is Nothing Then Return False
|
||||
Dim t = obj.GetType()
|
||||
While t IsNot Nothing
|
||||
If t.IsGenericType AndAlso t.GetGenericTypeDefinition() Is genericType Then Return True
|
||||
t = t.BaseType
|
||||
End While
|
||||
Return False
|
||||
End Function
|
||||
|
||||
Private Uuid As Integer = 1
|
||||
Private UuidLock As Object
|
||||
''' <summary>
|
||||
@@ -1045,79 +888,6 @@ Public Module ModBase
|
||||
Return Stopwatch.GetTimestamp() \ (Stopwatch.Frequency \ 1000L)
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 将时间间隔转换为类似“5 分 10 秒前”的易于阅读的形式。
|
||||
''' </summary>
|
||||
Public Function GetTimeSpanString(Span As TimeSpan, IsShortForm As Boolean) As String
|
||||
Dim EndFix = If(Span.TotalMilliseconds > 0, "后", "前")
|
||||
If Span.TotalMilliseconds < 0 Then Span = -Span
|
||||
Dim TotalMonthes = Math.Floor(Span.Days / 30)
|
||||
Dim Result As String
|
||||
If IsShortForm Then
|
||||
If TotalMonthes >= 12 Then
|
||||
'1+ 年,“3 年”
|
||||
Result = Math.Floor(TotalMonthes / 12) & " 年"
|
||||
ElseIf TotalMonthes >= 2 Then
|
||||
'2~11 月,“5 个月”
|
||||
Result = TotalMonthes & " 个月"
|
||||
ElseIf Span.TotalDays >= 2 Then
|
||||
'2 天 ~ 2 月,“23 天”
|
||||
Result = Span.Days & " 天"
|
||||
ElseIf Span.TotalHours >= 1 Then
|
||||
'1 小时 ~ 2 天,“15 小时”
|
||||
Result = Span.Hours & " 小时"
|
||||
ElseIf Span.TotalMinutes >= 1 Then
|
||||
'1 分钟 ~ 1 小时,“49 分钟”
|
||||
Result = Span.Minutes & " 分钟"
|
||||
ElseIf Span.TotalSeconds >= 1 Then
|
||||
'1 秒 ~ 1 分钟,“23 秒”
|
||||
Result = Span.Seconds & " 秒"
|
||||
Else
|
||||
'不到 1 秒
|
||||
Result = "1 秒"
|
||||
End If
|
||||
Else
|
||||
If TotalMonthes >= 61 Then
|
||||
'5+ 年,“5 年”
|
||||
Result = Math.Floor(TotalMonthes / 12) & " 年"
|
||||
ElseIf TotalMonthes >= 12 Then
|
||||
'12~60 月,“1 年 2 个月”
|
||||
Result = Math.Floor(TotalMonthes / 12) & " 年" & If((TotalMonthes Mod 12) > 0, " " & (TotalMonthes Mod 12) & " 个月", "")
|
||||
ElseIf TotalMonthes >= 4 Then
|
||||
'4~11 月,“5 个月”
|
||||
Result = TotalMonthes & " 个月"
|
||||
ElseIf TotalMonthes >= 1 Then
|
||||
'1~4 月,“2 个月 13 天”
|
||||
Result = TotalMonthes & " 月" & If((Span.Days Mod 30) > 0, " " & (Span.Days Mod 30) & " 天", "")
|
||||
ElseIf Span.TotalDays >= 4 Then
|
||||
'4~30 天,“23 天”
|
||||
Result = Span.Days & " 天"
|
||||
ElseIf Span.TotalDays >= 1 Then
|
||||
'1~3 天,“2 天 20 小时”
|
||||
Result = Span.Days & " 天" & If(Span.Hours > 0, " " & Span.Hours & " 小时", "")
|
||||
ElseIf Span.TotalHours >= 10 Then
|
||||
'10 小时 ~ 1 天,“15 小时”
|
||||
Result = Span.Hours & " 小时"
|
||||
ElseIf Span.TotalHours >= 1 Then
|
||||
'1~10 小时,“1 小时 20 分钟”
|
||||
Result = Span.Hours & " 小时" & If(Span.Minutes > 0, " " & Span.Minutes & " 分钟", "")
|
||||
ElseIf Span.TotalMinutes >= 10 Then
|
||||
'10 分钟 ~ 1 小时,“49 分钟”
|
||||
Result = Span.Minutes & " 分钟"
|
||||
ElseIf Span.TotalMinutes >= 1 Then
|
||||
'1~10 分钟,“9 分 23 秒”
|
||||
Result = Span.Minutes & " 分" & If(Span.Seconds > 0, " " & Span.Seconds & " 秒", "")
|
||||
ElseIf Span.TotalSeconds >= 1 Then
|
||||
'1 秒 ~ 1 分钟,“23 秒”
|
||||
Result = Span.Seconds & " 秒"
|
||||
Else
|
||||
'不到 1 秒
|
||||
Result = "1 秒"
|
||||
End If
|
||||
End If
|
||||
Result += EndFix
|
||||
Return Result
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 获取十进制 Unix 时间戳。
|
||||
''' </summary>
|
||||
Public Function GetUnixTimestampUtc() As Long
|
||||
@@ -1165,41 +935,6 @@ Public Module ModBase
|
||||
Logger.Info($"启动进程:{StartInfo.FileName} {StartInfo.Arguments}")
|
||||
Return Process.Start(StartInfo)
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 静默运行文件并返回输出流字符串。执行失败会抛出异常。
|
||||
''' </summary>
|
||||
''' <param name="FileName">文件名。可以为“notepad”等缩写。</param>
|
||||
''' <param name="Arguments">运行参数。</param>
|
||||
''' <param name="Timeout">等待该程序结束的最长时间(毫秒)。超时会抛出错误。</param>
|
||||
Public Function StartProcessAndGetOutput(FileName As String, Optional Arguments As String = "", Optional Timeout As Integer = 1000000,
|
||||
Optional WorkingDirectory As String = Nothing, Optional Encoding As Encoding = Nothing, Optional PrintLog As Boolean = True) As String
|
||||
Dim Info = New ProcessStartInfo With {
|
||||
.Arguments = Arguments,
|
||||
.FileName = PathUtils.ToShortPath(FileName),
|
||||
.UseShellExecute = False,
|
||||
.CreateNoWindow = True,
|
||||
.RedirectStandardError = True,
|
||||
.RedirectStandardOutput = True,
|
||||
.WorkingDirectory = PathUtils.ToShortPath(If(WorkingDirectory, PathExeFolder.TrimEnd("\"c))),
|
||||
.StandardErrorEncoding = Encoding,
|
||||
.StandardOutputEncoding = Encoding
|
||||
}
|
||||
If WorkingDirectory IsNot Nothing Then
|
||||
If Info.EnvironmentVariables.ContainsKey("appdata") Then
|
||||
Info.EnvironmentVariables("appdata") = WorkingDirectory
|
||||
Else
|
||||
Info.EnvironmentVariables.Add("appdata", WorkingDirectory)
|
||||
End If
|
||||
End If
|
||||
If PrintLog Then Logger.Info($"执行命令并等待返回结果:{FileName} {Arguments}")
|
||||
Using Program As New Process With {.StartInfo = Info}
|
||||
Program.Start()
|
||||
Dim Result As String = Program.StandardOutput.ReadToEnd & Program.StandardError.ReadToEnd
|
||||
Program.WaitForExit(Timeout)
|
||||
If Not Program.HasExited Then Program.Kill()
|
||||
Return Result
|
||||
End Using
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
''' 在新的工作线程中执行代码。
|
||||
@@ -1209,8 +944,6 @@ Public Module ModBase
|
||||
Sub()
|
||||
Try
|
||||
Action()
|
||||
Catch ex As ThreadInterruptedException
|
||||
Logger.Info($"{Name}:线程已中止")
|
||||
Catch ex As Exception
|
||||
Logger.Error(ex, $"{Name}:线程执行失败")
|
||||
End Try
|
||||
@@ -1341,7 +1074,7 @@ Public Module ModBase
|
||||
RunInThread(
|
||||
Sub()
|
||||
Try
|
||||
ResilientUtils.Retry(
|
||||
Retrier.Attempt(delay:=Function(Attempt) TimeSpan.FromMilliseconds(200), maxAttempts:=4, isRetryAllowed:=Function(ex) True, action:=
|
||||
Sub()
|
||||
RunInUi(
|
||||
Sub()
|
||||
@@ -1476,20 +1209,10 @@ Public Module ModBase
|
||||
''' <summary>
|
||||
''' 控件是否受到 TextTrimming 属性影响,导致内容被截取。
|
||||
''' </summary>
|
||||
<Extension> Public Function IsTextTrimmed(Control As TextBlock) As Boolean
|
||||
Control.Measure(New Size(Double.MaxValue, Double.MaxValue))
|
||||
Return Control.DesiredSize.Width > Control.ActualWidth
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
''' 获取文本在被渲染后的宽度。
|
||||
''' </summary>
|
||||
Public Function MeasureStringWidth(text As String, Optional fontSize As Double = 14, Optional fontFamily As FontFamily = Nothing) As Double
|
||||
If fontFamily Is Nothing Then fontFamily = New FontFamily("微软雅黑")
|
||||
Dim formattedText = New FormattedText(
|
||||
text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, New Typeface(fontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
|
||||
fontSize, Brushes.Black, New NumberSubstitution(), TextFormattingMode.Display, New DpiScale(1, 1).PixelsPerDip)
|
||||
Return formattedText.Width
|
||||
<Extension> Public Function IsTextTrimmed(TextBlock As TextBlock) As Boolean
|
||||
Dim typeface As New Typeface(TextBlock.FontFamily, TextBlock.FontStyle, TextBlock.FontWeight, TextBlock.FontStretch)
|
||||
Dim formattedText As New FormattedText(TextBlock.Text, Thread.CurrentThread.CurrentCulture, TextBlock.FlowDirection, typeface, TextBlock.FontSize, TextBlock.Foreground, DPI)
|
||||
Return formattedText.Width > TextBlock.ActualWidth
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Public Module ModLoader
|
||||
Public Module ModLoader
|
||||
|
||||
'各类加载器
|
||||
''' <summary>
|
||||
@@ -89,7 +89,8 @@
|
||||
Dim OldState = _State
|
||||
If value = LoadState.Finished AndAlso Settings.Get(Of Boolean)("SystemDebugDelay") Then Thread.Sleep(RandomInteger(100, 2000))
|
||||
_State = value
|
||||
Logger.Log($"加载器 {Name} 状态改变:{value}", If(value = LoadState.Failed, LogLevel.Error, LogLevel.Info))
|
||||
If value = LoadState.Canceled Then CancellationTokenSource?.Cancel()
|
||||
Logger.Log($"加载器 {[GetType].Name} {Name} 状态改变:{value}", If(value = LoadState.Failed, LogLevel.Error, LogLevel.Info))
|
||||
'实现 ILoadingTrigger 接口与 OnStateChanged 回调
|
||||
RunInUi(
|
||||
Sub()
|
||||
@@ -163,14 +164,42 @@
|
||||
Private _Progress As Double = -1
|
||||
Public Event ProgressChanged(NewProgress As Double, OldProgress As Double) Implements ILoadingTrigger.ProgressChanged
|
||||
''' <summary>
|
||||
''' 创建一个 ProgressProvider,在其进度改变时,并将其进度单向映射到此加载器上。
|
||||
''' 这是 Loader 对新框架的兼容层。
|
||||
''' </summary>
|
||||
Public Function CreateSyncProgressProvider(Optional FromPercentage As Double = 0, Optional ToPercentage As Double = 1) As ProgressProvider
|
||||
Dim Provider As New ProgressProvider
|
||||
AddHandler Provider.ProgressChanged,
|
||||
Sub(ChangedProgress)
|
||||
Dim RawProgress = Provider.GetRaw()
|
||||
Progress = FromPercentage + (RawProgress.actual + RawProgress.skiped) * (ToPercentage - FromPercentage)
|
||||
End Sub
|
||||
Return Provider
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 计算总进度时的权重。它应该为预计时间(秒)。
|
||||
''' </summary>
|
||||
Public Property ProgressWeight As Double = 1
|
||||
|
||||
'状态变化
|
||||
Public MustOverride Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
|
||||
Public MustOverride Sub Interrupt()
|
||||
Public MustOverride Sub Cancel()
|
||||
Public MustOverride Sub Failed(Ex As Exception)
|
||||
Private CancellationTokenSource As CancellationTokenSource
|
||||
''' <summary>
|
||||
''' 创建一个随此加载器取消的取消令牌;令牌取消时也会调用此加载器的 Cancel。
|
||||
''' 这是 Loader 对新框架的兼容层。
|
||||
''' </summary>
|
||||
Public Function CreateCancellationToken() As CancellationToken
|
||||
SyncLock LockState
|
||||
If CancellationTokenSource Is Nothing OrElse CancellationTokenSource.IsCancellationRequested Then
|
||||
CancellationTokenSource = New CancellationTokenSource
|
||||
CancellationTokenSource.Token.Register(AddressOf Cancel)
|
||||
End If
|
||||
If State = LoadState.Canceled Then CancellationTokenSource.Cancel()
|
||||
Return CancellationTokenSource.Token
|
||||
End SyncLock
|
||||
End Function
|
||||
|
||||
'等待结束
|
||||
Public Const WaitForExitTimeoutMessage As String = "等待加载器执行超时。"
|
||||
@@ -178,16 +207,19 @@
|
||||
''' <summary>
|
||||
''' 无限期地等待加载器完成,直到结束或抛出异常。若加载器尚未开始,则会开始执行。
|
||||
''' </summary>
|
||||
Public Sub WaitForExit(Optional Input As Object = Nothing, Optional LoaderToSyncProgress As LoaderBase = Nothing, Optional IsForceRestart As Boolean = False)
|
||||
Public Sub WaitForExit(Optional Input As Object = Nothing, Optional LoaderToSyncProgress As LoaderBase = Nothing, Optional IsForceRestart As Boolean = False,
|
||||
Optional c As CancellationToken = Nothing, Optional p As ProgressProvider = Nothing)
|
||||
Start(Input, IsForceRestart)
|
||||
Do While State = LoadState.Loading
|
||||
If LoaderToSyncProgress IsNot Nothing Then LoaderToSyncProgress.Progress = Progress
|
||||
p?.Set(Progress)
|
||||
c.ThrowIfCancellationRequested()
|
||||
Thread.Sleep(10)
|
||||
Loop
|
||||
If State = LoadState.Finished Then
|
||||
Return
|
||||
ElseIf State = LoadState.Interrupted Then
|
||||
Throw New ThreadInterruptedException("加载器执行已中断。")
|
||||
ElseIf State = LoadState.Canceled Then
|
||||
Throw New OperationCanceledException("加载器执行已中断")
|
||||
ElseIf IsNothing([Error]) Then
|
||||
Throw New Exception("未知错误!")
|
||||
Else
|
||||
@@ -209,8 +241,8 @@
|
||||
Loop
|
||||
If State = LoadState.Finished Then
|
||||
Return
|
||||
ElseIf State = LoadState.Interrupted Then
|
||||
Throw New ThreadInterruptedException("加载器执行已中断。")
|
||||
ElseIf State = LoadState.Canceled Then
|
||||
Throw New OperationCanceledException("加载器执行已中断")
|
||||
ElseIf IsNothing([Error]) Then
|
||||
Throw New Exception("未知错误!")
|
||||
Else
|
||||
@@ -240,16 +272,16 @@
|
||||
''' <summary>
|
||||
''' 当前执行线程是否应当中断。只应用在加载器的工作线程中判断,不可跨线程调用。
|
||||
''' </summary>
|
||||
Public ReadOnly Property IsInterrupted As Boolean
|
||||
Public ReadOnly Property IsCanceled As Boolean
|
||||
Get
|
||||
Return IsInterruptedWithThread(Thread.CurrentThread)
|
||||
Return IsCanceledWithThread(Thread.CurrentThread)
|
||||
End Get
|
||||
End Property
|
||||
''' <summary>
|
||||
''' 当前执行线程是否应当中断。需要手动提供加载器线程,用于需要跨线程检查的情况。
|
||||
''' </summary>
|
||||
Public Function IsInterruptedWithThread(Thread As Thread) As Boolean
|
||||
Return LastRunningThread Is Nothing OrElse Not ReferenceEquals(Thread, LastRunningThread) OrElse State = LoadState.Interrupted
|
||||
Public Function IsCanceledWithThread(Thread As Thread) As Boolean
|
||||
Return LastRunningThread Is Nothing OrElse Not ReferenceEquals(Thread, LastRunningThread) OrElse State = LoadState.Canceled
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 在输入相同时使用原有结果的超时,单位为毫秒。
|
||||
@@ -318,30 +350,29 @@
|
||||
Sub()
|
||||
Try
|
||||
IsForceRestarting = IsForceRestart
|
||||
Logger.Trace(Function() $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已{If(IsForceRestarting, "强制", "")}启动")
|
||||
Logger.Trace(Function() $"加载线程 {Name} 已{If(IsForceRestarting, "强制", "")}启动")
|
||||
LoadDelegate(Me)
|
||||
If IsInterrupted Then
|
||||
Logger.Warn($"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId)})")
|
||||
If IsCanceled Then
|
||||
Logger.Warn($"加载线程 {Name} 已中断但线程正常运行至结束,输出被弃用(最新线程:{If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId)})")
|
||||
Return
|
||||
End If
|
||||
Logger.Trace(Function() $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已完成")
|
||||
Logger.Trace(Function() $"加载线程 {Name} 已完成")
|
||||
RaisePreviewFinish()
|
||||
State = LoadState.Finished
|
||||
LastFinishedTime = GetTimeMs() '未中断,本次输出有效
|
||||
Catch ex As CancelledException
|
||||
Logger.Trace(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已触发取消中断,已完成 {Math.Round(Progress * 100)}%")
|
||||
If Not IsInterrupted Then State = LoadState.Interrupted
|
||||
Catch ex As ThreadInterruptedException
|
||||
Logger.Trace(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已触发线程中断,已完成 {Math.Round(Progress * 100)}%")
|
||||
'如果线程是因为判断到 IsInterrupted 而提前中止,则代表已有新线程被重启,此时不应当改为 Interrupted
|
||||
'如果线程是在没有 IsInterrupted 时手动引发了 ThreadInterruptedException,则代表没有重启线程,这通常代表用户手动取消,应当改为 Interrupted
|
||||
If Not IsInterrupted Then State = LoadState.Interrupted
|
||||
Catch ex As Exception
|
||||
Failed(ex)
|
||||
If ex.IsCanceled Then
|
||||
'如果线程是因为判断到 IsCanceled 而提前中止,则代表已有新线程被重启,此时不应当改为 Canceled
|
||||
'如果线程是在没有 IsCanceled 时手动引发了取消异常,则代表没有重启线程,这通常代表用户手动取消,应当改为 Canceled
|
||||
Logger.Trace(ex, $"加载线程 {Name} 已取消,当前已完成 {Math.Round(Progress * 100)}%")
|
||||
If Not IsCanceled Then State = LoadState.Canceled
|
||||
Else
|
||||
Failed(ex)
|
||||
End If
|
||||
End Try
|
||||
End Sub) With {.Name = "L/" & Name, .Priority = ThreadPriority}
|
||||
Try
|
||||
LastRunningThread.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsInterrupted
|
||||
LastRunningThread.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsCanceled
|
||||
Catch ex As ThreadStateException '若遇到偶发的 “线程正在运行或被终止”,则等待后重试
|
||||
Thread.Sleep(500)
|
||||
LastRunningThread.Start()
|
||||
@@ -350,16 +381,16 @@
|
||||
Public Overrides Sub Failed(ex As Exception)
|
||||
[Error] = ex
|
||||
SyncLock LockState
|
||||
If IsInterrupted OrElse State >= LoadState.Finished Then Return
|
||||
If IsCanceled OrElse State >= LoadState.Finished Then Return
|
||||
State = LoadState.Failed
|
||||
End SyncLock
|
||||
Logger.Warn(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 出错,已完成 {Math.Round(Progress * 100)}%")
|
||||
Logger.Warn(ex, $"加载线程 {Name} 出错,已完成 {Math.Round(Progress * 100)}%")
|
||||
TriggerThreadInterrupt()
|
||||
End Sub
|
||||
Public Overrides Sub Interrupt()
|
||||
Public Overrides Sub Cancel()
|
||||
SyncLock LockState
|
||||
If State <> LoadState.Loading Then Return
|
||||
State = LoadState.Interrupted
|
||||
State = LoadState.Canceled
|
||||
End SyncLock
|
||||
TriggerThreadInterrupt()
|
||||
End Sub
|
||||
@@ -367,7 +398,7 @@
|
||||
If LastRunningThread Is Nothing Then Return
|
||||
If LastRunningThread.IsAlive Then
|
||||
LastRunningThread.Interrupt()
|
||||
Logger.Trace(Function() $"加载线程 {Name} ({LastRunningThread.ManagedThreadId}) 已中断")
|
||||
Logger.Trace(Function() $"加载线程 {Name} 已中断")
|
||||
End If
|
||||
LastRunningThread = Nothing
|
||||
End Sub
|
||||
@@ -383,6 +414,53 @@
|
||||
End Sub
|
||||
|
||||
End Class
|
||||
|
||||
''' <summary>
|
||||
''' 新 Worker 对老 Loader 的兼容包装类。
|
||||
''' </summary>
|
||||
Public Class LoaderWorker
|
||||
Inherits LoaderBase
|
||||
Public WithEvents Worker As IWorker
|
||||
Public Sub New(Name As String, Worker As IWorker)
|
||||
Me.Name = Name
|
||||
Me.Worker = Worker
|
||||
AddHandler Worker.Progress.ProgressChanged, Sub(Raw) Progress = Raw.actual + Raw.skiped
|
||||
End Sub
|
||||
|
||||
Public Overrides Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
|
||||
SyncLock LockState
|
||||
State = LoadState.Loading
|
||||
End SyncLock
|
||||
Logger.Trace(Function() $"Worker {Name} 已启动")
|
||||
IsForceRestarting = IsForceRestart
|
||||
Worker.Start()
|
||||
End Sub
|
||||
Private Sub Worker_Succeeded() Handles Worker.Succeeded
|
||||
Logger.Trace(Function() $"Worker {Name} 已完成")
|
||||
RaisePreviewFinish()
|
||||
SyncLock LockState
|
||||
State = LoadState.Finished
|
||||
End SyncLock
|
||||
End Sub
|
||||
Public Overrides Sub Cancel() Handles Worker.Canceled
|
||||
SyncLock LockState
|
||||
If State <> LoadState.Loading Then Return
|
||||
State = LoadState.Canceled
|
||||
End SyncLock
|
||||
Logger.Trace(Function() $"Worker {Name} 已取消,当前已完成 {Math.Round(Progress * 100)}%")
|
||||
Worker.Cancel()
|
||||
End Sub
|
||||
Public Overrides Sub Failed(Ex As Exception) Handles Worker.Failed
|
||||
SyncLock LockState
|
||||
If State >= LoadState.Finished Then Return
|
||||
State = LoadState.Failed
|
||||
End SyncLock
|
||||
Logger.Warn(Ex, $"Worker {Name} 出错,已完成 {Math.Round(Progress * 100)}%")
|
||||
[Error] = Ex
|
||||
Worker.Cancel()
|
||||
End Sub
|
||||
End Class
|
||||
|
||||
''' <summary>
|
||||
''' 支持多个加载器连续运作的复合加载器。
|
||||
''' </summary>
|
||||
@@ -450,11 +528,11 @@
|
||||
End If
|
||||
RunInThread(AddressOf Update)
|
||||
End Sub
|
||||
Public Overrides Sub Interrupt()
|
||||
Public Overrides Sub Cancel()
|
||||
'改变状态
|
||||
SyncLock LockState
|
||||
If State = LoadState.Loading OrElse State = LoadState.Waiting Then
|
||||
State = LoadState.Interrupted
|
||||
State = LoadState.Canceled
|
||||
Else
|
||||
Return
|
||||
End If
|
||||
@@ -463,7 +541,7 @@
|
||||
Sub()
|
||||
'中断加载器
|
||||
For Each Loader In Loaders
|
||||
Loader.Interrupt()
|
||||
Loader.Cancel()
|
||||
Next
|
||||
End Sub)
|
||||
End Sub
|
||||
@@ -474,7 +552,7 @@
|
||||
State = LoadState.Failed
|
||||
End SyncLock
|
||||
For Each Loader In Loaders
|
||||
Loader.Interrupt()
|
||||
Loader.Cancel()
|
||||
Next
|
||||
FrmMain.BtnExtraDownload.ShowRefresh()
|
||||
End Sub
|
||||
@@ -492,9 +570,9 @@
|
||||
Case LoadState.Finished
|
||||
'正常结束,触发刷新
|
||||
Update()
|
||||
Case LoadState.Interrupted
|
||||
Case LoadState.Canceled
|
||||
'被中断,这个任务也中断
|
||||
Interrupt()
|
||||
Cancel()
|
||||
Case LoadState.Failed
|
||||
'完蛋,出错了
|
||||
Failed(New Exception(Loader.Name & "失败", Loader.Error))
|
||||
@@ -504,15 +582,15 @@
|
||||
''' 触发一次更新,以启动新加载器或完成。
|
||||
''' </summary>
|
||||
Private Sub Update()
|
||||
If State = LoadState.Finished OrElse State = LoadState.Failed OrElse State = LoadState.Interrupted Then Return
|
||||
If State = LoadState.Finished OrElse State = LoadState.Failed OrElse State = LoadState.Canceled Then Return
|
||||
Dim IsFinished As Boolean = True
|
||||
Dim Blocked As Boolean = False
|
||||
Dim Input As Object = Me.Input
|
||||
For Each Loader In Loaders
|
||||
Select Case Loader.State
|
||||
Case LoadState.Finished
|
||||
'检查是否需要重启
|
||||
If Loader.GetType.Name.StartsWithF("LoaderTask") Then '类型名后面带有泛型,必须用 StartsWith
|
||||
'检查 LoaderTask 是否需要重启
|
||||
If GeneralUtils.IsGenericInstanceOf(Loader, GetType(LoaderTask(Of ,))) Then
|
||||
If CType(Loader, Object).ShouldStart(If(Input IsNot Nothing AndAlso Loader.GetType.GenericTypeArguments.First Is Input.GetType, Input, Nothing), IgnoreReloadTimeout:=True) Then
|
||||
Logger.Info($"由于输入条件变更,重启已完成的加载器 {Loader.Name}")
|
||||
GoTo Restart
|
||||
@@ -523,8 +601,8 @@
|
||||
'如果不让继续启动,且已有加载器正在加载中,就不继续启动
|
||||
If Loader.Block AndAlso Not IsFinished Then Blocked = True
|
||||
Case LoadState.Loading
|
||||
'检查是否需要重启
|
||||
If Loader.GetType.Name.StartsWithF("LoaderTask") Then
|
||||
'检查 LoaderTask 是否需要重启
|
||||
If GeneralUtils.IsGenericInstanceOf(Loader, GetType(LoaderTask(Of ,))) Then
|
||||
If CType(Loader, Object).ShouldStart(If(Input IsNot Nothing AndAlso Loader.GetType.GenericTypeArguments.First Is Input.GetType, Input, Nothing), IgnoreReloadTimeout:=True) Then
|
||||
Logger.Warn($"由于输入条件变更,重启进行中的加载器 {Loader.Name}")
|
||||
GoTo Restart
|
||||
@@ -540,13 +618,14 @@ Restart:
|
||||
If Blocked Then Continue For
|
||||
If Input IsNot Nothing Then
|
||||
'若输入类型与下一个加载器相同才继续
|
||||
Dim LoaderType As String = Loader.GetType.Name
|
||||
If LoaderType.StartsWithF("LoaderTask") OrElse LoaderType.StartsWithF("LoaderCombo") Then
|
||||
If GeneralUtils.IsGenericInstanceOf(Loader, GetType(LoaderTask(Of ,))) OrElse GeneralUtils.IsGenericInstanceOf(Loader, GetType(LoaderCombo(Of ))) Then
|
||||
Loader.Start(If(Loader.GetType.GenericTypeArguments.First Is Input.GetType, Input, Nothing), IsForceRestarting)
|
||||
ElseIf LoaderType.StartsWithF("LoaderDownload") Then
|
||||
ElseIf TypeOf Loader Is LoaderWorker Then
|
||||
Loader.Start(IsForceRestart:=IsForceRestarting)
|
||||
ElseIf TypeOf Loader Is LoaderDownload Then
|
||||
Loader.Start(If(TypeOf Input Is List(Of NetFile), Input, Nothing), IsForceRestarting)
|
||||
Else
|
||||
Throw New Exception("未知的加载器类型(" & LoaderType & ")")
|
||||
Throw New Exception("未知的加载器类型(" & Loader.GetType.Name & ")")
|
||||
End If
|
||||
Else
|
||||
Loader.Start(IsForceRestart:=IsForceRestarting)
|
||||
@@ -569,7 +648,7 @@ Restart:
|
||||
Public Shared Sub GetLoaderList(Loader As Object, ByRef List As List(Of LoaderBase), Optional RequireShow As Boolean = True)
|
||||
For Each SubLoader In Loader.Loaders
|
||||
If SubLoader.Show OrElse Not RequireShow Then List.Add(SubLoader)
|
||||
If SubLoader.GetType.Name.StartsWithF("LoaderCombo") Then GetLoaderList(SubLoader, List)
|
||||
If GeneralUtils.IsGenericInstanceOf(SubLoader, GetType(LoaderCombo(Of ))) Then GetLoaderList(SubLoader, List)
|
||||
Next
|
||||
End Sub
|
||||
''' <summary>
|
||||
@@ -606,7 +685,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.Interrupted) Then
|
||||
(Task.State = LoadState.Waiting OrElse Task.State = LoadState.Canceled) Then
|
||||
FrmSpeedLeft?.TaskRefresh(Task)
|
||||
LoaderTaskbar.Remove(Task)
|
||||
Logger.Info($"{Task.Name} 已移出任务列表")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
Imports System.Net.Sockets
|
||||
Imports System.Threading.Tasks
|
||||
Imports System.Net.Sockets
|
||||
|
||||
Public Module ModNet
|
||||
|
||||
@@ -32,10 +31,8 @@ Public Module ModNet
|
||||
Return Result
|
||||
Catch ex As FormatException
|
||||
Throw
|
||||
Catch ex As ThreadInterruptedException
|
||||
Throw
|
||||
Catch ex As Exception
|
||||
If MakeLog Then Logger.Warn(ex, "网络请求失败")
|
||||
If MakeLog Then Logger.Warn(ex, $"网络请求失败({Method.Method},{Url})")
|
||||
Throw
|
||||
End Try
|
||||
End Function
|
||||
@@ -68,9 +65,8 @@ Retry:
|
||||
Throw RetryException
|
||||
End If
|
||||
End Select
|
||||
Catch ex As ThreadInterruptedException
|
||||
Throw
|
||||
Catch ex As Exception
|
||||
ex.ThrowIfCanceled()
|
||||
If TypeOf ex Is HttpRequestCodeException Then
|
||||
If CType(ex, HttpRequestCodeException).StatusCode = HttpStatusCode.Forbidden Then Throw
|
||||
If CType(ex, HttpRequestCodeException).StatusCode = HttpStatusCode.NotFound Then Throw
|
||||
@@ -161,7 +157,7 @@ RequestFinished:
|
||||
FileUtils.Delete(Temp)
|
||||
Return Result
|
||||
Finally
|
||||
NewTask.Interrupt()
|
||||
NewTask.Cancel()
|
||||
End Try
|
||||
End Function
|
||||
|
||||
@@ -179,11 +175,9 @@ RequestFinished:
|
||||
Retry:
|
||||
Try
|
||||
FileUtils.Write(LocalPath, SendRequest(Url, HttpMethod.Get, SimulateBrowserHeaders:=SimulateBrowserHeaders))
|
||||
Catch ex As ThreadInterruptedException
|
||||
FileUtils.Delete(LocalPath)
|
||||
Throw
|
||||
Catch ex As Exception
|
||||
FileUtils.Delete(LocalPath)
|
||||
ex.ThrowIfCanceled()
|
||||
If RetryCount < 2 Then
|
||||
RetryCount += 1
|
||||
GoTo Retry
|
||||
@@ -210,7 +204,7 @@ Retry:
|
||||
Catch ex As Exception
|
||||
Throw New WebException($"多线程直接下载文件失败(第一下载源:" & Urls.First() & ")", ex)
|
||||
Finally
|
||||
NewTask.Interrupt()
|
||||
NewTask.Cancel()
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
@@ -226,32 +220,7 @@ Retry:
|
||||
Dim Request As HttpRequestMessage = Nothing, CancelToken As CancellationTokenSource = Nothing,
|
||||
Response As HttpResponseMessage = Nothing, HostIp As String = Nothing
|
||||
Try
|
||||
Url = SecretCdnSign(Url)
|
||||
Request = New HttpRequestMessage(Method, Url)
|
||||
'写入 Content
|
||||
If Content IsNot Nothing AndAlso Request.Method <> HttpMethod.Get AndAlso Request.Method <> HttpMethod.Head Then
|
||||
If TypeOf Content Is HttpContent Then
|
||||
Request.Content = DirectCast(Content, HttpContent)
|
||||
ElseIf TypeOf Content Is Byte() Then
|
||||
Request.Content = New ByteArrayContent(DirectCast(Content, Byte()))
|
||||
ElseIf Content IsNot Nothing Then
|
||||
Request.Content = New ByteArrayContent(If(Encoding, Encoding.UTF8).GetBytes(Content.ToString()))
|
||||
End If
|
||||
End If
|
||||
'写入 Headers
|
||||
For Each Header In If(Headers, New Dictionary(Of String, String))
|
||||
If Header.Key.Lower = "content-type" Then
|
||||
Request.Content?.Headers.TryAddWithoutValidation(Header.Key, Header.Value)
|
||||
Else
|
||||
Request.Headers.TryAddWithoutValidation(Header.Key, Header.Value)
|
||||
End If
|
||||
Next
|
||||
SecretHeadersSign(Url, Request, SimulateBrowserHeaders)
|
||||
'DNS 解析
|
||||
CancelToken = New CancellationTokenSource(Timeout)
|
||||
HostIp = DNSLookup(Request, CancelToken.Token)
|
||||
If HostIp IsNot Nothing Then IPReliability.GetOrAdd(HostIp, -0.01) '预先降低一点,这样快速的重复请求会使用不同的 IP 以提高成功率
|
||||
'发送请求
|
||||
'构建 RequestClient
|
||||
Directory.CreateDirectory(PathTemp & "Cache\Http\")
|
||||
SyncLock RequestClientLock
|
||||
If RequestClient Is Nothing Then '延迟初始化,以避免在程序启动前加载 CacheCow 导致 DLL 加载失败
|
||||
@@ -261,11 +230,41 @@ Retry:
|
||||
})
|
||||
End If
|
||||
End SyncLock
|
||||
ResilientUtils.RetryOn(Of FileNotFoundException, IOException)( 'CacheCow 读取缓存时若文件被占用,会调用不存在的 System.Net.Http.Formatting.resources,抛出此异常(#8150)
|
||||
Sub()
|
||||
CancelToken.Dispose() : CancelToken = New CancellationTokenSource(Timeout)
|
||||
Response = RequestClient.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout)
|
||||
'构建请求
|
||||
Url = SecretCdnSign(Url)
|
||||
Request = New HttpRequestMessage(Method, Url)
|
||||
If Content IsNot Nothing AndAlso Request.Method <> HttpMethod.Get AndAlso Request.Method <> HttpMethod.Head Then '写入 Content
|
||||
If TypeOf Content Is HttpContent Then
|
||||
Request.Content = DirectCast(Content, HttpContent).Clone()
|
||||
ElseIf TypeOf Content Is Byte() Then
|
||||
Request.Content = New ByteArrayContent(DirectCast(Content, Byte()))
|
||||
ElseIf Content IsNot Nothing Then
|
||||
Request.Content = New ByteArrayContent(If(Encoding, Encoding.UTF8).GetBytes(Content.ToString()))
|
||||
End If
|
||||
End If
|
||||
For Each Header In If(Headers, New Dictionary(Of String, String)) '写入 Headers
|
||||
If Header.Key.Lower = "content-type" Then
|
||||
Request.Content?.Headers.TryAddWithoutValidation(Header.Key, Header.Value)
|
||||
Else
|
||||
Request.Headers.TryAddWithoutValidation(Header.Key, Header.Value)
|
||||
End If
|
||||
Next
|
||||
SecretHeadersSign(Url, Request, SimulateBrowserHeaders)
|
||||
'发送请求
|
||||
'CacheCow 读取缓存时若文件被占用,会调用不存在的 System.Net.Http.Formatting.resources,抛出异常(#8150)
|
||||
Retrier.Attempt(delay:=Function() TimeSpan.FromMilliseconds(200), isRetryAllowed:=Function(ex) TypeOf ex Is FileNotFoundException OrElse TypeOf ex Is IOException, action:=
|
||||
Sub(Attempt)
|
||||
CancelToken?.Dispose() : CancelToken = New CancellationTokenSource(Timeout)
|
||||
HostIp = DNSLookup(Request, CancelToken.Token)
|
||||
If HostIp IsNot Nothing Then IPReliability.GetOrAdd(HostIp, -0.01) '预先降低一点,这样快速的重复请求会使用不同的 IP 以提高成功率
|
||||
Dim ClonedRequest As HttpRequestMessage = Request.Clone() 'HttpRequestMessage 只能发送一次,重试时需要克隆一个新的
|
||||
Try
|
||||
Response = RequestClient.SendAsync(ClonedRequest, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout)
|
||||
Finally
|
||||
ClonedRequest.Dispose()
|
||||
End Try
|
||||
End Sub)
|
||||
'读取响应
|
||||
Dim ResponseBytes As Byte()
|
||||
CancelToken.Dispose() : CancelToken = New CancellationTokenSource(Timeout)
|
||||
Using ResponseStream = Response.Content.ReadAsStreamAsync().GetResultWithTimeout(CancelToken, Timeout)
|
||||
@@ -274,26 +273,24 @@ Retry:
|
||||
ResponseBytes = Stream.ToArray()
|
||||
End Using
|
||||
End Using
|
||||
'输出
|
||||
If Response.IsSuccessStatusCode Then
|
||||
RecordIPReliability(HostIp, 0.5)
|
||||
Return ResponseBytes
|
||||
Else
|
||||
If Not Response.IsSuccessStatusCode Then
|
||||
RecordIPReliability(HostIp, -0.7)
|
||||
Dim ResponseMessage = ResponseBytes.GetString(Encoding)
|
||||
Throw New HttpRequestCodeException(
|
||||
$"错误码 {Response.StatusCode} ({CInt(Response.StatusCode)}),{Method},{Url},{HostIp}" &
|
||||
If(String.IsNullOrEmpty(ResponseMessage), "", vbCrLf & ResponseMessage), Response.StatusCode, ResponseMessage)
|
||||
End If
|
||||
Catch ex As ThreadInterruptedException
|
||||
Throw
|
||||
'输出
|
||||
RecordIPReliability(HostIp, 0.5)
|
||||
Return ResponseBytes
|
||||
Catch ex As HttpRequestException
|
||||
Throw
|
||||
Catch ex As TimeoutException
|
||||
Throw New WebException($"连接服务器超时,请稍后再试,或使用 VPN 改善网络环境({Method}, {Url},IP:{HostIp})", WebExceptionStatus.Timeout)
|
||||
Catch ex As Exception
|
||||
ex.ThrowIfCanceled()
|
||||
RecordIPReliability(HostIp, -1)
|
||||
If TypeOf ex Is OperationCanceledException OrElse TypeOf ex Is TimeoutException Then 'CancellationToken 超时
|
||||
Throw New WebException($"连接服务器超时,请稍后再试,或使用 VPN 改善网络环境({Method}, {Url},IP:{HostIp})", WebExceptionStatus.Timeout)
|
||||
ElseIf ex.IsBadNetwork Then
|
||||
If ex.IsBadNetwork Then
|
||||
Throw New WebException($"网络请求失败,请稍后再试,或使用 VPN 改善网络环境({Method}, {Url},IP:{HostIp})", WebExceptionStatus.Timeout)
|
||||
Else
|
||||
Throw New Exception($"网络请求出现意外异常({Method}, {Url},{HostIp})", ex)
|
||||
@@ -478,7 +475,7 @@ Retry:
|
||||
''' <summary>
|
||||
''' 已失败或中断。
|
||||
''' </summary>
|
||||
Interrupted = 6
|
||||
Canceled = 6
|
||||
End Enum
|
||||
''' <summary>
|
||||
''' 预下载检查行为。
|
||||
@@ -626,7 +623,7 @@ Retry:
|
||||
''' </summary>
|
||||
Public ReadOnly Property IsEnded As Boolean
|
||||
Get
|
||||
Return State = NetState.Finished OrElse State = NetState.Interrupted
|
||||
Return State = NetState.Finished OrElse State = NetState.Canceled
|
||||
End Get
|
||||
End Property
|
||||
|
||||
@@ -663,7 +660,7 @@ Retry:
|
||||
''' <summary>
|
||||
''' 所有下载源。
|
||||
''' </summary>
|
||||
Public Sources As ConcurrentList(Of NetSource)
|
||||
Public Sources As List(Of NetSource)
|
||||
''' <summary>
|
||||
''' 用于在第一个线程出错时切换下载源。
|
||||
''' </summary>
|
||||
@@ -819,7 +816,7 @@ Retry:
|
||||
Return OriginalProgress * 0.93 + 0.05
|
||||
Case NetState.Merging
|
||||
Return 0.99
|
||||
Case NetState.Finished, NetState.Interrupted
|
||||
Case NetState.Finished, NetState.Canceled
|
||||
Return 1
|
||||
Case Else
|
||||
Throw New ArgumentOutOfRangeException("文件状态未知:" & State)
|
||||
@@ -889,7 +886,7 @@ Retry:
|
||||
'条件检测
|
||||
SyncLock LockSource
|
||||
If NetTaskThreadCount >= NetTaskThreadLimit OrElse Not HasAvailableSource() OrElse
|
||||
(IsNoSplit AndAlso Threads IsNot Nothing AndAlso Threads.State <> NetState.Interrupted) Then Return Nothing
|
||||
(IsNoSplit AndAlso Threads IsNot Nothing AndAlso Threads.State <> NetState.Canceled) Then Return Nothing
|
||||
If State >= NetState.Merging OrElse State = NetState.WaitingToCheck Then Return Nothing
|
||||
SyncLock LockState
|
||||
If State < NetState.Connecting Then State = NetState.Connecting
|
||||
@@ -900,7 +897,7 @@ Retry:
|
||||
If IsNoSplit Then GoTo StartSingleThreadDownload
|
||||
'只剩下单线程可用点
|
||||
If Not HasAvailableSource(False) Then
|
||||
If SourcesOnce.First.SingleThread IsNot Nothing AndAlso SourcesOnce.First.SingleThread.State <> NetState.Interrupted Then Return Nothing
|
||||
If SourcesOnce.First.SingleThread IsNot Nothing AndAlso SourcesOnce.First.SingleThread.State <> NetState.Canceled Then Return Nothing
|
||||
StartSingleThreadDownload:
|
||||
Reset()
|
||||
State = NetState.Reading
|
||||
@@ -916,7 +913,7 @@ StartSingleThreadDownload:
|
||||
End If
|
||||
'寻找失败点
|
||||
For Each Thread As NetThread In Threads
|
||||
If Thread.State = NetState.Interrupted AndAlso Thread.DownloadUndone > 0 Then
|
||||
If Thread.State = NetState.Canceled AndAlso Thread.DownloadUndone > 0 Then
|
||||
StartPosition = Thread.DownloadStart + Thread.DownloadDone
|
||||
StartSource = GetSource(Thread.Source.Id + 1)
|
||||
GoTo StartThread
|
||||
@@ -997,7 +994,7 @@ StartThread:
|
||||
If Not Response.IsSuccessStatusCode Then '状态码检查
|
||||
Throw New HttpRequestCodeException($"错误码 {Response.StatusCode} ({CInt(Response.StatusCode)}),{Th.Source.Url}", Response.StatusCode, Nothing)
|
||||
End If
|
||||
If State = NetState.Interrupted Then GoTo SourceBreak '快速中断
|
||||
If State = NetState.Canceled Then GoTo SourceBreak '快速中断
|
||||
If Response.RequestMessage.RequestUri.ToString <> Th.Source.Url Then Logger.Trace(Function() $"{LocalName}:重定向至 {Response.RequestMessage.RequestUri}")
|
||||
'文件大小校验
|
||||
ContentLength = Response.Content.Headers.ContentLength.GetValueOrDefault(-1)
|
||||
@@ -1023,7 +1020,7 @@ StartThread:
|
||||
End If
|
||||
End If
|
||||
FileSize = ContentLength : IsUnknownSize = False
|
||||
Logger.Info($"{LocalName}:文件大小 {ContentLength}({FormatFileSize(ContentLength)})")
|
||||
Logger.Info($"{LocalName}:文件大小 {ContentLength}({StringUtils.FormatByteSize(ContentLength)})")
|
||||
'若文件大小大于 50 M,进行剩余磁盘空间校验
|
||||
If ContentLength > 50 * 1024 * 1024 Then
|
||||
For Each Drive As DriveInfo In DriveInfo.GetDrives
|
||||
@@ -1031,7 +1028,7 @@ StartThread:
|
||||
Dim RequiredSpace = If(PathTemp.StartsWithF(DriveName), ContentLength * 1.1, 0) +
|
||||
If(LocalPath.StartsWithF(DriveName), ContentLength + 5 * 1024 * 1024, 0)
|
||||
If RequiredSpace > 0 AndAlso Drive.TotalFreeSpace < RequiredSpace Then
|
||||
Throw New Exception(DriveName & " 盘空间不足,无法进行下载。" & vbCrLf & "需要至少 " & FormatFileSize(RequiredSpace) & " 空间,但当前仅剩余 " & FormatFileSize(Drive.TotalFreeSpace) & "。" &
|
||||
Throw New Exception(DriveName & " 盘空间不足,无法进行下载。" & vbCrLf & "需要至少 " & StringUtils.FormatByteSize(RequiredSpace) & " 空间,但当前仅剩余 " & StringUtils.FormatByteSize(Drive.TotalFreeSpace) & "。" &
|
||||
If(PathTemp.StartsWithF(DriveName), vbCrLf & vbCrLf & "下载时需要与文件同等大小的空间存放缓存,你可以在设置中调整缓存文件夹的位置。", ""))
|
||||
End If
|
||||
Next
|
||||
@@ -1111,9 +1108,9 @@ NotSupportRange:
|
||||
HttpDataCount = ResponseStream.ReadAsync(ResponseBytes, 0, 16384, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout)
|
||||
End While
|
||||
SourceBreak:
|
||||
If State = NetState.Interrupted OrElse (Th.Source.IsFailed AndAlso Th.Source.SingleThread <> Th) OrElse (Th.DownloadUndone > 0 AndAlso Not IsUnknownSize) Then
|
||||
If State = NetState.Canceled OrElse (Th.Source.IsFailed AndAlso Th.Source.SingleThread <> Th) OrElse (Th.DownloadUndone > 0 AndAlso Not IsUnknownSize) Then
|
||||
'被外部中断
|
||||
Th.State = NetState.Interrupted
|
||||
Th.State = NetState.Canceled
|
||||
Logger.Info($"{LocalName}:中断")
|
||||
ElseIf HttpDataCount = 0 AndAlso Th.DownloadUndone > 0 AndAlso Not IsUnknownSize Then
|
||||
'服务器无返回数据
|
||||
@@ -1143,7 +1140,7 @@ SourceBreak:
|
||||
Interlocked.Decrement(NetTaskThreadCount)
|
||||
'合并
|
||||
If ((FileSize >= 0 AndAlso DownloadDone >= FileSize) OrElse (FileSize = -1 AndAlso DownloadDone > 0)) AndAlso
|
||||
State < NetState.Merging AndAlso Th.State <> NetState.Interrupted Then Merge(Th)
|
||||
State < NetState.Merging AndAlso Th.State <> NetState.Canceled Then Merge(Th)
|
||||
End Try
|
||||
End Sub
|
||||
Private Sub SourceFail(Th As NetThread, ex As Exception, IsMergeFailure As Boolean)
|
||||
@@ -1152,7 +1149,7 @@ SourceBreak:
|
||||
For Each Task In Tasks
|
||||
Interlocked.Increment(Task.FailCount)
|
||||
Next
|
||||
Th.State = NetState.Interrupted
|
||||
Th.State = NetState.Canceled
|
||||
Th.Source.Ex = ex
|
||||
'根据情况判断,是否在多线程下禁用下载源(连续错误过多,或不支持断点续传)
|
||||
Dim IsRangeNotSupported As Boolean = TypeOf ex Is RangeNotSupportedException OrElse ex.Message.Contains("(416)")
|
||||
@@ -1333,29 +1330,29 @@ Retry:
|
||||
If RaiseEx IsNot Nothing Then Ex.Add(RaiseEx)
|
||||
'凉凉
|
||||
Logger.Info($"{LocalName}:已失败,当前状态 {State}")
|
||||
State = NetState.Interrupted
|
||||
State = NetState.Canceled
|
||||
End SyncLock
|
||||
InterruptInternal()
|
||||
CancelInternal()
|
||||
For Each Task In Tasks
|
||||
Task.OnFileFail(Me)
|
||||
Next
|
||||
End Sub
|
||||
''' <summary>
|
||||
''' 下载中断。
|
||||
''' 取消下载。
|
||||
''' </summary>
|
||||
Public Sub Interrupt(InterruptedTask As LoaderDownload)
|
||||
Public Sub Cancel(CanceledTask As LoaderDownload)
|
||||
'从特定任务中移除,如果它还属于其他任务,则继续下载
|
||||
Tasks.Remove(InterruptedTask)
|
||||
Tasks.Remove(CanceledTask)
|
||||
If Tasks.Any Then Return
|
||||
'确认中断
|
||||
SyncLock LockState
|
||||
If State >= NetState.Finished Then Return
|
||||
Logger.Info($"{LocalName}:已中断,当前状态 {State}")
|
||||
State = NetState.Interrupted
|
||||
State = NetState.Canceled
|
||||
End SyncLock
|
||||
InterruptInternal()
|
||||
CancelInternal()
|
||||
End Sub
|
||||
Private Sub InterruptInternal()
|
||||
Private Sub CancelInternal()
|
||||
On Error Resume Next
|
||||
Reset()
|
||||
Interlocked.Decrement(NetManager.FileRemain)
|
||||
@@ -1395,7 +1392,7 @@ Retry:
|
||||
''' <summary>
|
||||
''' 需要下载的文件。
|
||||
''' </summary>
|
||||
Public Files As ConcurrentList(Of NetFile)
|
||||
Public Files As ConcurrentQueue(Of NetFile)
|
||||
''' <summary>
|
||||
''' 剩余未完成的文件数。(用于减轻 FilesLock 的占用)
|
||||
''' </summary>
|
||||
@@ -1470,12 +1467,12 @@ FinishExCatch:
|
||||
|
||||
Public Sub New(Name As String, FileTasks As IEnumerable(Of NetFile))
|
||||
Me.Name = Name
|
||||
Files = New ConcurrentList(Of NetFile)(FileTasks)
|
||||
Files = New ConcurrentQueue(Of NetFile)(FileTasks)
|
||||
End Sub
|
||||
Public Overrides Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
|
||||
If Input IsNot Nothing Then Files = New ConcurrentList(Of NetFile)(Input)
|
||||
If Input IsNot Nothing Then Files = New ConcurrentQueue(Of NetFile)(CType(Input, IEnumerable(Of NetFile)))
|
||||
'去重
|
||||
Files = New ConcurrentList(Of NetFile)(Files.DistinctBy(Function(a) a.LocalPath))
|
||||
Files = New ConcurrentQueue(Of NetFile)(Files.DistinctBy(Function(a) a.LocalPath))
|
||||
'设置剩余文件数
|
||||
SyncLock FileRemainLock
|
||||
FileRemain += Files.Where(Function(f) f.State <> NetState.Finished).Count
|
||||
@@ -1507,6 +1504,7 @@ FinishExCatch:
|
||||
'接入下载管理器,获取新开始下载的文件列表
|
||||
Dim NewFiles = NetManager.Start(Me)
|
||||
|
||||
|
||||
'====================================
|
||||
' 已存在文件查找
|
||||
'====================================
|
||||
@@ -1514,9 +1512,9 @@ FinishExCatch:
|
||||
'整理允许进行查找的文件
|
||||
Dim FilesToCheck As New List(Of NetFile)
|
||||
For Each File In NewFiles
|
||||
If File.Check?.CanUseExistsFile Then
|
||||
If File.Check IsNot Nothing Then
|
||||
FilesToCheck.Add(File)
|
||||
Else '不允许,直接开始下载
|
||||
Else '没有校验信息,直接开始下载
|
||||
SyncLock LockState
|
||||
File.State = NetState.WaitingToDownload
|
||||
File.IsCopy = False
|
||||
@@ -1526,7 +1524,7 @@ FinishExCatch:
|
||||
If Not FilesToCheck.Any Then Return
|
||||
'获取 MC 文件夹列表
|
||||
Dim Folders As New List(Of String)
|
||||
Folders.Add(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\") '总是添加官启文件夹,因为 HMCL 会把所有文件存在这里
|
||||
Folders.Add(Paths.AppData & ".minecraft\") '总是添加官启文件夹,因为 HMCL 会把所有文件存在这里
|
||||
Folders.AddRange(McFolderList.Select(Function(f) f.Location))
|
||||
Folders = Folders.Distinct.Where(Function(f) DirectoryUtils.Exists(f)).ToList
|
||||
'平均分配到多个检查线程
|
||||
@@ -1555,7 +1553,7 @@ FinishExCatch:
|
||||
'列出 MC 文件夹中的各个版本文件夹
|
||||
Dim VersionFolders As New List(Of String)
|
||||
For Each McFolder In FolderList
|
||||
For Each VersionFolder In DirectoryUtils.GetDirectories(McFolder & "versions\", True)
|
||||
For Each VersionFolder In DirectoryUtils.EnumerateDirectories(McFolder & "versions\")
|
||||
VersionFolders.Add(PathUtils.AddSlashSuffix(VersionFolder))
|
||||
Next
|
||||
Next
|
||||
@@ -1625,7 +1623,7 @@ FinishExCatch:
|
||||
Case "versions\"
|
||||
'版本 jar 或 json:查找 MC 文件夹下的各个版本文件夹
|
||||
For Each VersionFolder In VersionFolders
|
||||
For Each Candidate In DirectoryUtils.GetFiles(VersionFolder, True, "*." & PathUtils.GetLastPart(File.LocalPath).AfterLast(".").Lower)
|
||||
For Each Candidate In DirectoryUtils.EnumerateFiles(VersionFolder, searchPattern:="*." & PathUtils.GetLastPart(File.LocalPath).AfterLast(".").Lower)
|
||||
If CheckCandidate(Candidate, File.Check) Then Return Candidate
|
||||
Next
|
||||
Next
|
||||
@@ -1633,7 +1631,7 @@ FinishExCatch:
|
||||
'社区资源
|
||||
If File.Check.ActualSize < 0 OrElse File.Check.Hash Is Nothing Then Return Nothing '必须要求指定了文件大小和 Hash
|
||||
For Each Folder In FolderList.Concat(VersionFolders)
|
||||
For Each Candidate In DirectoryUtils.GetFiles(Folder & Type, True)
|
||||
For Each Candidate In DirectoryUtils.EnumerateFiles(Folder & Type)
|
||||
If CheckCandidate(Candidate, File.Check) Then Return Candidate
|
||||
Next
|
||||
Next
|
||||
@@ -1671,12 +1669,12 @@ FinishExCatch:
|
||||
If State >= LoadState.Finished Then Return
|
||||
If ExList Is Nothing OrElse Not ExList.Any() Then ExList = New List(Of Exception) From {New Exception("未知错误!")}
|
||||
'寻找有效的错误信息
|
||||
Dim UsefulExs = ExList.Where(Function(e) TypeOf e IsNot OperationCanceledException AndAlso TypeOf e IsNot TimeoutException AndAlso TypeOf e IsNot ThreadInterruptedException).ToList
|
||||
Dim UsefulExs = ExList.Where(Function(e) Not e.IsCanceled AndAlso TypeOf e IsNot TimeoutException).ToList
|
||||
[Error] = If(UsefulExs.FirstOrDefault, ExList.FirstOrDefault)
|
||||
'获取实际失败的文件
|
||||
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
|
||||
If File.State <> NetState.Canceled Then Continue For
|
||||
If File.Sources.All(Function(s) s.Ex.IsCanceled OrElse TypeOf s.Ex Is TimeoutException) Then Continue For
|
||||
Dim Detail As String = File.Sources.Select(Function(s) $"{If(s.Ex Is Nothing, "无错误信息。", s.Ex.GetDisplay(False))}({s.Url})").Join(vbCrLf)
|
||||
[Error] = New Exception("文件下载失败:" & File.LocalPath & vbCrLf &
|
||||
"各下载源的错误如下:" & vbCrLf & Detail, [Error])
|
||||
@@ -1691,20 +1689,20 @@ FinishExCatch:
|
||||
End SyncLock
|
||||
'中断所有文件
|
||||
For Each TaskFile In Files
|
||||
TaskFile.Interrupt(Me)
|
||||
TaskFile.Cancel(Me)
|
||||
Next
|
||||
'在退出同步锁后再进行日志输出
|
||||
Logger.Error(New AggregateException(ExList), "下载加载器失败", LogBehavior.None)
|
||||
End Sub
|
||||
Public Overrides Sub Interrupt()
|
||||
Public Overrides Sub Cancel()
|
||||
SyncLock LockState
|
||||
If State >= LoadState.Finished Then Return
|
||||
State = LoadState.Interrupted
|
||||
State = LoadState.Canceled
|
||||
End SyncLock
|
||||
Logger.Info($"{Name} 已取消!")
|
||||
'中断所有文件
|
||||
For Each TaskFile In Files
|
||||
TaskFile.Interrupt(Me)
|
||||
TaskFile.Cancel(Me)
|
||||
Next
|
||||
End Sub
|
||||
|
||||
@@ -1726,7 +1724,7 @@ FinishExCatch:
|
||||
''' <summary>
|
||||
''' 当前的所有下载任务。
|
||||
''' </summary>
|
||||
Public Tasks As New ConcurrentList(Of LoaderDownload)
|
||||
Public Tasks As New ConcurrentBag(Of LoaderDownload)
|
||||
|
||||
''' <summary>
|
||||
''' 已下载完成的大小。
|
||||
@@ -1777,7 +1775,7 @@ FinishExCatch:
|
||||
If SpeedLast.Count >= 10 Then Limit = SpeedLast.Take(10).Average * 0.85 '取近 1 秒的平均速度的 85%
|
||||
If Limit > NetTaskSpeedLimitLow Then
|
||||
NetTaskSpeedLimitLow = Limit
|
||||
Logger.Info($"速度下限已提升到 {FormatFileSize(Limit)}")
|
||||
Logger.Info($"速度下限已提升到 {StringUtils.FormatByteSize(Limit)}")
|
||||
End If
|
||||
#End Region
|
||||
#Region "刷新下载任务属性"
|
||||
@@ -1894,14 +1892,13 @@ FinishExCatch:
|
||||
End SyncLock
|
||||
'添加每个文件
|
||||
Dim NewFiles As New List(Of NetFile)
|
||||
For i = 0 To Task.Files.Count - 1
|
||||
Dim File As NetFile = Task.Files(i)
|
||||
Dim NewFilesList As New List(Of NetFile)
|
||||
For Each File In Task.Files
|
||||
Dim OngoingFile As NetFile = Nothing
|
||||
If AllFiles.TryGetValue(File.LocalPath, OngoingFile) AndAlso OngoingFile.State < NetState.Finished Then
|
||||
'该文件正在被另一个任务下载
|
||||
Logger.Trace(Function() $"{File.LocalName}:无需开始,该文件已在下载中,目标 {File.LocalPath}")
|
||||
File = OngoingFile '将列表中的文件替换成下载中的文件,即两个任务指向同一个文件;抛弃现在的 File 对象
|
||||
Task.Files(i) = File
|
||||
Else
|
||||
'常规情况(该文件可能已在其他任务中下载完成,但这并不影响)
|
||||
Logger.Trace(Function() $"{File.LocalName}:开始,目标 {File.LocalPath}")
|
||||
@@ -1909,8 +1906,10 @@ FinishExCatch:
|
||||
NewFiles.Add(File)
|
||||
Interlocked.Increment(FileRemain)
|
||||
End If
|
||||
NewFilesList.Add(File)
|
||||
File.Tasks.Add(Task)
|
||||
Next
|
||||
Task.Files = New ConcurrentQueue(Of NetFile)(NewFilesList)
|
||||
Tasks.Add(Task)
|
||||
Return NewFiles
|
||||
End Function
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
'提供不同的输入验证方法,名称以 Validate 开头
|
||||
'提供不同的输入验证方法,名称以 Validate 开头
|
||||
Public Module ModValidate
|
||||
|
||||
''' <summary>
|
||||
@@ -66,6 +66,20 @@ Public Class ValidateNullOrWhiteSpace
|
||||
End Function
|
||||
End Class
|
||||
|
||||
''' <summary>
|
||||
''' 自定义的 Validate。
|
||||
''' </summary>
|
||||
Public Class ValidateFunc
|
||||
Inherits Validate
|
||||
Private ReadOnly _func As Func(Of String, String)
|
||||
Public Sub New(F As Func(Of String, String))
|
||||
_func = F
|
||||
End Sub
|
||||
Public Overrides Function Validate(Str As String) As String
|
||||
Return _func(Str)
|
||||
End Function
|
||||
End Class
|
||||
|
||||
''' <summary>
|
||||
''' 必须满足正则表达式。
|
||||
''' </summary>
|
||||
@@ -255,7 +269,7 @@ Public Class ValidateFolderName
|
||||
Me.IgnoreCase = IgnoreCase
|
||||
Me.UseMinecraftCharCheck = UseMinecraftCharCheck
|
||||
On Error Resume Next
|
||||
PathIgnore = DirectoryUtils.GetDirectories(Folder, True)
|
||||
PathIgnore = DirectoryUtils.EnumerateDirectories(Folder)
|
||||
End Sub
|
||||
Public Overrides Function Validate(Str As String) As String
|
||||
Try
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
'一个万能的自动图片类型转换工具类
|
||||
'一个万能的自动图片类型转换工具类
|
||||
|
||||
Imports System.Drawing.Imaging
|
||||
|
||||
@@ -126,9 +126,7 @@ Public Class MyBitmap
|
||||
End Sub
|
||||
Private Class WebPDecoder '将代码隔离在另外一个类中,这样只要不调用这个方法就不会加载 Imazen.WebP.dll
|
||||
Public Shared Function DecodeFromBytes(Bytes As Byte()) As System.Drawing.Bitmap
|
||||
If Is32BitSystem Then Throw New Exception("不支持在 32 位系统下加载 WebP 图片。")
|
||||
Dim Decoder As New Imazen.WebP.SimpleDecoder()
|
||||
Return Decoder.DecodeFromBytes(Bytes, Bytes.Length)
|
||||
Return New Imazen.WebP.SimpleDecoder().DecodeFromBytes(Bytes, Bytes.Length)
|
||||
End Function
|
||||
End Class
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Public Class PclLogger
|
||||
Public Class PclLogger
|
||||
Inherits FileLogger
|
||||
|
||||
''' <inheritdoc/>
|
||||
|
||||
Reference in New Issue
Block a user