| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- Public Module ModWatcher
- '对全体的监视
- Public McWatcherList As New List(Of Watcher)
- Private IsWatcherRunning As Boolean = False
- Public HasRunningMinecraft As Boolean = False
- Private Sub WatcherStateChanged()
- Dim IsRunning As Boolean = False
- Dim TriggerLauncherShutdown As Boolean = True
- For Each Watcher In McWatcherList
- If Watcher.State = Watcher.MinecraftState.Loading OrElse Watcher.State = Watcher.MinecraftState.Running Then
- IsRunning = True
- Exit For
- ElseIf Watcher.State = Watcher.MinecraftState.Crashed OrElse Watcher.State = Watcher.MinecraftState.Canceled Then
- TriggerLauncherShutdown = False
- End If
- Next
- If IsWatcherRunning = IsRunning Then Return
- IsWatcherRunning = IsRunning
- If IsWatcherRunning Then
- MinecraftStart()
- Else
- MinecraftStop(TriggerLauncherShutdown)
- End If
- End Sub
- Private Sub MinecraftStart()
- McLaunchLog("[全局] 出现运行中的 Minecraft")
- HasRunningMinecraft = True
- FrmMain.BtnExtraShutdown.ShowRefresh()
- End Sub
- Private Sub MinecraftStop(TriggerLauncherShutdown As Boolean)
- McLaunchLog("[全局] 已无运行中的 Minecraft")
- HasRunningMinecraft = False
- FrmMain.BtnExtraShutdown.ShowRefresh()
- '音乐播放
- If Setup.Get("UiMusicStop") Then
- RunInUi(Sub() If MusicResume() Then Log("[Music] 已根据设置,在结束后开始音乐播放"))
- ElseIf Setup.Get("UiMusicStart") Then
- RunInUi(Sub() If MusicPause() Then Log("[Music] 已根据设置,在结束后暂停音乐播放"))
- End If
- '启动器可见性
- Select Case Setup.Get("LaunchArgumentVisible")
- Case 2
- '直接关闭
- If TriggerLauncherShutdown Then
- RunInUi(Sub() FrmMain.EndProgram(False))
- Else
- RunInUi(Sub() FrmMain.Hidden = False)
- End If
- Case 3
- '恢复
- RunInUi(Sub() FrmMain.Hidden = False)
- End Select
- End Sub
- '对单个进程的监视
- Public Class Watcher
- '初始化
- Public GameProcess As Process
- Public Version As McVersion
- Private WindowTitle As String = ""
- Private PID As Integer
- Public Loader As LoaderTask(Of Process, Integer)
- Public Sub New(Loader As LoaderTask(Of Process, Integer), Version As McVersion, WindowTitle As String)
- Me.Loader = Loader
- Me.Version = Version
- Me.WindowTitle = WindowTitle
- Me.PID = Loader.Input.Id
- WatcherLog("开始 Minecraft 日志监控")
- If Me.WindowTitle <> "" Then WatcherLog("要求窗口标题:" & WindowTitle)
- '更改列表
- Dim NewWatcherList As New List(Of Watcher)
- For Each Watch In McWatcherList
- If Watch.State = MinecraftState.Crashed OrElse Watch.State = MinecraftState.Ended OrElse Watch.State = MinecraftState.Canceled Then Continue For
- NewWatcherList.Add(Watch)
- Next
- NewWatcherList.Add(Me)
- McWatcherList = NewWatcherList
- WatcherStateChanged()
- '初始化进程与日志读取
- GameProcess = Loader.Input
- GameProcess.BeginOutputReadLine()
- GameProcess.BeginErrorReadLine()
- AddHandler GameProcess.OutputDataReceived, AddressOf LogReceived
- AddHandler GameProcess.ErrorDataReceived, AddressOf LogReceived
- '初始化时钟
- RunInNewThread(
- Sub()
- Try
- Do Until State = MinecraftState.Ended OrElse State = MinecraftState.Crashed OrElse State = MinecraftState.Canceled OrElse Loader.State = LoadState.Aborted
- TimerWindow()
- TimerLog()
- '设置窗口标题
- For i = 1 To 3
- If IsWindowFinished AndAlso IsWindowAppeared AndAlso WindowTitle <> "" AndAlso State = MinecraftState.Running AndAlso Not GameProcess.HasExited Then
- Dim RealTitle As String = WindowTitle.Replace("{date}", Date.Now.ToString("yyyy/M/d")).Replace("{time}", Date.Now.ToString("HH:mm:ss"))
- SetWindowText(WindowHandle, RealTitle.ToCharArray)
- End If
- Thread.Sleep(64)
- Next
- Loop
- WatcherLog("Minecraft 日志监控已退出")
- Catch ex As Exception
- Log(ex, "Minecraft 日志监控主循环出错", LogLevel.Feedback)
- State = MinecraftState.Ended
- End Try
- End Sub, "Minecraft Watcher PID " & PID)
- End Sub
- '状态
- Private _State As MinecraftState = MinecraftState.Loading
- Public Property State As MinecraftState
- Get
- Return _State
- End Get
- Set(value As MinecraftState)
- If _State = value Then Return
- _State = value
- WatcherStateChanged()
- End Set
- End Property
- Public Enum MinecraftState
- Loading
- Running
- Crashed
- Ended
- Canceled
- End Enum
- '日志
- Public WaitingLog As New List(Of String)(1000)
- Private ReadOnly WaitingLogLock As New Object
- Private Sub LogReceived(sender As Object, e As DataReceivedEventArgs)
- SyncLock WaitingLogLock
- WaitingLog.Add(e.Data)
- End SyncLock
- End Sub
- Private Sub TimerLog()
- Try
- '输出文本
- Dim Copyed As New List(Of String)
- SyncLock WaitingLogLock
- If Not WaitingLog.Any() Then Return
- Copyed = WaitingLog
- WaitingLog = New List(Of String)(1000)
- End SyncLock
- For Each Str As String In Copyed
- GameLog(Str)
- Next
- If State = MinecraftState.Loading Then ProgressUpdate()
- '游戏退出检查
- If GameProcess.HasExited Then
- WatcherLog("Minecraft 已退出,返回值:" & GameProcess.ExitCode)
- 'If Process.ExitCode = 1 Then
- ' '返回值为 1,考虑是任务管理器结束
- ' WatcherLog("Minecraft 返回值为 1,考虑为任务管理器结束") '并不,崩了照样是 1
- ' State = MinecraftState.Ended
- 'Else
- If State = MinecraftState.Loading Then
- '窗口未出现
- WatcherLog("Minecraft 尚未加载完成,可能已崩溃")
- Crashed()
- ElseIf GameProcess.ExitCode <> 0 AndAlso State = MinecraftState.Running AndAlso Version.ReleaseTime.Year >= 2012 Then
- '返回值不为 0 且未结束
- WatcherLog("Minecraft 返回值异常,可能已崩溃")
- Crashed()
- ElseIf State <> MinecraftState.Crashed Then
- '正常关闭
- State = MinecraftState.Ended
- End If
- End If
- Catch ex As Exception
- Log(ex, "输出 Minecraft 日志失败", LogLevel.Feedback)
- End Try
- End Sub
- Public LatestLog As New Queue(Of String)
- Private Sub GameLog(Text As String)
- '预处理
- If Text Is Nothing Then Return
- Text = Text.Replace(vbCrLf, vbCr).Replace(vbLf, vbCr).Replace(vbCr, vbCrLf)
- 'If Text.Contains("�����") Then Hint("检测到错误的日志编码:" & Text)
- '加入预存储
- LatestLog.Enqueue(Text)
- If LatestLog.Count >= 501 Then LatestLog.Dequeue()
- '进度处理
- If LogProgress < 1 Then
- WatcherLog("日志 1/5:已出现日志输出")
- LogProgress = 1
- End If '可能第一句就是后面需要判断的 Log(重现:启动 1.15.2 原版)
- If LogProgress < 2 AndAlso Text.Contains("Setting user:") Then
- WatcherLog("日志 2/5:游戏用户已设置") '仅确保支持 Minecraft 1.7+
- LogProgress = 2
- ElseIf LogProgress < 3 AndAlso Text.ContainsF("lwjgl version", True) Then
- WatcherLog("日志 3/5:LWJGL 版本已确认")
- LogProgress = 3
- ElseIf LogProgress < 4 AndAlso (Text.Contains("OpenAL initialized") OrElse Text.Contains("Starting up SoundSystem")) Then
- WatcherLog("日志 4/5:OpenAL 已加载") '仅确保支持 Minecraft 1.7+
- LogProgress = 4
- ElseIf LogProgress < 5 AndAlso ((Text.Contains("Created") AndAlso Text.Contains("textures") AndAlso Text.Contains("-atlas")) OrElse Text.Contains("Found animation info")) Then
- WatcherLog("日志 5/5:材质已加载") '仅确保支持 Minecraft 1.7+
- LogProgress = 5
- End If
- '输出日志
- 'Log(Text)
- '关闭与崩溃检测
- If Not Text.Contains("[CHAT]") Then
- If Text.Contains("Someone is closing me!") OrElse Text.Contains("Restarting Minecraft with command") Then '#1258
- WatcherLog("识别为关闭的 Log:" & Text)
- State = MinecraftState.Ended
- ElseIf Text.Contains("Crash report saved to") OrElse Text.Contains("This crash report has been saved to:") Then
- ' Text.Contains("Minecraft ran into a problem! Report saved to:") Then
- 'Minecraft 崩溃,忽略 VanillaFix
- WatcherLog("识别为崩溃的 Log:" & Text)
- Crashed()
- ElseIf Text.Contains("Could not save crash report to") Then
- 'Minecraft 崩溃,无法保存崩溃日志
- WatcherLog("识别为崩溃的 Log:" & Text)
- Crashed()
- ElseIf Text.Contains("/ERROR]: Unable to launch") OrElse Text.Contains("An exception was thrown, the game will display an error screen and halt.") Then
- 'Forge 崩溃
- WatcherLog("识别为崩溃的 Log:" & Text)
- Crashed()
- 'ElseIf Text.Contains("Shutdown failure!") Then
- ' 'Minecraft 强行崩溃,由于点 X 强行关闭也会触发这句话,所以不可用
- ' Crashed(Nothing)
- End If
- End If
- End Sub
- Private Sub WatcherLog(Text As String)
- McLaunchLog("[" & PID & "] " & Text)
- End Sub
- '进度更新
- Private LogProgress As Integer = 0
- Private Sub ProgressUpdate()
- Dim CurrentProgress As Double
- If IsWindowAppeared OrElse LogProgress = 5 Then
- CurrentProgress = 0.95
- WatcherLog("Minecraft 加载已完成")
- State = MinecraftState.Running
- Else
- CurrentProgress = Math.Min(LogProgress, 3) / 3 * 0.9
- End If
- Loader.Progress = CurrentProgress
- End Sub
- '窗口检查
- Private IsWindowAppeared As Boolean = False
- ''' <summary>
- ''' 窗口检查是否已经完成。这不一定代表着找到了窗口(如果没有找到,IsWindowAppeared 仍为 False)。
- ''' </summary>
- Private IsWindowFinished As Boolean = False
- Private WindowHandle As IntPtr
- Private Sub TimerWindow()
- Try
- If GameProcess.HasExited Then Return
- If IsWindowFinished Then Return
- '获取全部窗口,检查是否有新增的
- Dim MinecraftWindow As KeyValuePair(Of IntPtr, String)? = Nothing
- Try
- MinecraftWindow = TryGetMinecraftWindow()
- Catch ex As ComponentModel.Win32Exception
- '拒绝访问(#1062)
- Log(ex, "由于反作弊或安全软件拦截,PCL 无法操作游戏窗口", LogLevel.Hint)
- IsWindowFinished = True
- End Try
- If MinecraftWindow Is Nothing Then Return
- Dim MinecraftWindowName = MinecraftWindow.Value.Value, MinecraftWindowHandle = MinecraftWindow.Value.Key
- '已找到窗口
- If Not MinecraftWindowName.StartsWithF("FML") AndAlso Not MinecraftWindowName.StartsWithF("Quilt Loader") Then
- '已找到 Minecraft 窗口
- WindowHandle = MinecraftWindowHandle
- WatcherLog($"Minecraft 窗口已加载:{MinecraftWindowName}({MinecraftWindowHandle.ToInt64})")
- IsWindowFinished = True
- '最大化
- If Setup.Get("LaunchArgumentWindowType") = 4 Then
- RunInNewThread(
- Sub()
- Try
- '如果最大化导致屏幕渲染大小不对,那是 MC 的 Bug,不是我的 Bug
- '……虽然我很想这样说,但总有人反馈,算了
- Thread.Sleep(2000)
- ShowWindow(WindowHandle, 3)
- WatcherLog($"已最大化 Minecraft 窗口:{MinecraftWindowHandle.ToInt64}")
- Catch ex As Exception
- Log(ex, "最大化 Minecraft 窗口时出现错误")
- End Try
- End Sub, "MinecraftWindowMaximize")
- End If
- ElseIf Not IsWindowAppeared Then
- '已找到 Mod Loader 窗口
- WatcherLog("Mod Loader 窗口已加载:" & MinecraftWindowName & "(" & MinecraftWindowHandle.ToInt64 & ")")
- End If
- IsWindowAppeared = True
- Catch ex As Exception
- Log(ex, "检查 Minecraft 窗口失败", LogLevel.Feedback)
- End Try
- End Sub
- ''' <summary>
- ''' 获取可能是当前进程对应的 Minecraft 窗口的句柄和标题。
- ''' Nothing 代表未找到。
- ''' </summary>
- Private Function TryGetMinecraftWindow() As KeyValuePair(Of IntPtr, String)?
- TryGetMinecraftWindow = Nothing
- EnumWindows(
- Sub(hwnd As IntPtr, lParam As Integer)
- If TryGetMinecraftWindow IsNot Nothing Then Return
- '检查类名
- Dim str As New StringBuilder(512)
- GetClassName(hwnd, str, str.Capacity)
- Dim ClassName As String = str.ToString
- If Not (ClassName = "GLFW30" OrElse ClassName = "LWJGL" OrElse ClassName = "SunAwtFrame") Then Return
- '获取窗口标题名
- str = New StringBuilder(512)
- GetWindowText(hwnd, str, str.Capacity)
- Dim WindowText As String = str.ToString
- '有的 Mod 可以修改窗口标题,所以不能检测是否为 Minecraft 打头,这并不准确
- '部分版本会搞个 GLFW message window 出来所以得反选
- If Not (WindowText.StartsWithF("FML") OrElse (WindowText <> "PopupMessageWindow") AndAlso Not WindowText.StartsWithF("GLFW")) Then Return
- '获取窗口关联的进程
- Dim ProcessId As Integer
- GetWindowThreadProcessId(hwnd, ProcessId)
- Try
- If Process.GetProcessById(ProcessId).StartTime < GameProcess.StartTime Then Return '需要是此后启动的进程
- Catch ex As Exception
- Log(ex, "枚举 Minecraft 窗口进程失败")
- Return
- End Try
- '返回
- TryGetMinecraftWindow = New KeyValuePair(Of IntPtr, String)(hwnd, WindowText)
- End Sub, 0)
- Return TryGetMinecraftWindow
- End Function
- Private Delegate Sub EnumWindowsSub(hwnd As IntPtr, lParam As Integer)
- Private Declare Function EnumWindows Lib "user32" (hWnd As EnumWindowsSub, lParam As Integer) As Boolean
- Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (hWnd As Integer, str As StringBuilder, maxCount As Integer) As Integer
- Private Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" (hWnd As Integer, str As StringBuilder, maxCount As Integer) As Integer
- Private Declare Function SetWindowText Lib "user32" Alias "SetWindowTextA" (hWnd As Integer, str As String) As Boolean
- Private Declare Function ShowWindow Lib "user32" (hWnd As IntPtr, cmdWindow As UInteger) As Boolean
- Private Declare Function GetWindowThreadProcessId Lib "user32" (hWnd As IntPtr, ByRef lpdwProcessId As Integer) As Integer
- '崩溃处理
- Private Sub Crashed()
- If State = MinecraftState.Crashed OrElse State = MinecraftState.Ended Then Return
- State = MinecraftState.Crashed
- '崩溃分析
- WatcherLog("Minecraft 已崩溃,将在 2 秒后开始崩溃分析")
- Hint("检测到 Minecraft 出现错误,错误分析已开始……", HintType.Red)
- FeedbackInfo()
- RunInNewThread(
- Sub()
- Try
- Thread.Sleep(2000)
- WatcherLog("崩溃分析开始")
- Dim Analyzer As New CrashAnalyzer(Version)
- Analyzer.Collect(Version.PathIndie, LatestLog.ToList)
- Analyzer.Prepare()
- Analyzer.Analyze(Version)
- Analyzer.Output(False, New List(Of String) From
- {Version.Path & Version.Name & ".json", Path & "PCL\Log1.txt", Path & "PCL\LatestLaunch.bat"})
- Catch ex As Exception
- Log(ex, "崩溃分析失败", LogLevel.Feedback)
- End Try
- End Sub, "Crash Analyzer")
- End Sub
- '强制关闭
- Public Sub Kill()
- State = MinecraftState.Canceled
- WatcherLog("尝试强制结束 Minecraft 进程")
- Try
- If Not GameProcess.HasExited Then GameProcess.Kill()
- WatcherLog("已强制结束 Minecraft 进程")
- Catch ex As Exception
- Log(ex, "强制结束 Minecraft 进程失败", LogLevel.Hint)
- End Try
- End Sub
- End Class
- End Module
|