This commit is contained in:
龙腾猫跃
2026-07-02 23:24:29 +08:00
parent 124e3be6bf
commit c6d0457946
193 changed files with 3112 additions and 3747 deletions
@@ -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
'设置文本
+30 -307
View File
@@ -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>
+127 -48
View File
@@ -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} 已移出任务列表")
+95 -96
View File
@@ -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/>