ModWatcher.vb 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. Public Module ModWatcher
  2. '对全体的监视
  3. Public McWatcherList As New List(Of Watcher)
  4. Private IsWatcherRunning As Boolean = False
  5. Public HasRunningMinecraft As Boolean = False
  6. Private Sub WatcherStateChanged()
  7. Dim IsRunning As Boolean = False
  8. Dim TriggerLauncherShutdown As Boolean = True
  9. For Each Watcher In McWatcherList
  10. If Watcher.State = Watcher.MinecraftState.Loading OrElse Watcher.State = Watcher.MinecraftState.Running Then
  11. IsRunning = True
  12. Exit For
  13. ElseIf Watcher.State = Watcher.MinecraftState.Crashed OrElse Watcher.State = Watcher.MinecraftState.Canceled Then
  14. TriggerLauncherShutdown = False
  15. End If
  16. Next
  17. If IsWatcherRunning = IsRunning Then Return
  18. IsWatcherRunning = IsRunning
  19. If IsWatcherRunning Then
  20. MinecraftStart()
  21. Else
  22. MinecraftStop(TriggerLauncherShutdown)
  23. End If
  24. End Sub
  25. Private Sub MinecraftStart()
  26. McLaunchLog("[全局] 出现运行中的 Minecraft")
  27. HasRunningMinecraft = True
  28. FrmMain.BtnExtraShutdown.ShowRefresh()
  29. End Sub
  30. Private Sub MinecraftStop(TriggerLauncherShutdown As Boolean)
  31. McLaunchLog("[全局] 已无运行中的 Minecraft")
  32. HasRunningMinecraft = False
  33. FrmMain.BtnExtraShutdown.ShowRefresh()
  34. '音乐播放
  35. If Settings.Get("UiMusicStop") Then
  36. RunInUi(Sub() If MusicResume() Then Log("[Music] 已根据设置,在结束后开始音乐播放"))
  37. ElseIf Settings.Get("UiMusicStart") Then
  38. RunInUi(Sub() If MusicPause() Then Log("[Music] 已根据设置,在结束后暂停音乐播放"))
  39. End If
  40. '启动器可见性
  41. Select Case Settings.Get("LaunchArgumentVisible")
  42. Case 2
  43. '直接关闭
  44. If TriggerLauncherShutdown Then
  45. RunInUi(Sub() FrmMain.EndProgram(False))
  46. Else
  47. RunInUi(Sub() FrmMain.Hidden = False)
  48. End If
  49. Case 3
  50. '恢复
  51. RunInUi(Sub() FrmMain.Hidden = False)
  52. End Select
  53. End Sub
  54. '对单个进程的监视
  55. Public Class Watcher
  56. '初始化
  57. Public GameProcess As Process
  58. Public Instance As McInstance
  59. Private WindowTitle As String = ""
  60. Private PID As Integer
  61. Public Loader As LoaderTask(Of Process, Integer)
  62. Public Sub New(Loader As LoaderTask(Of Process, Integer), Instance As McInstance, WindowTitle As String)
  63. Me.Loader = Loader
  64. Me.Instance = Instance
  65. Me.WindowTitle = WindowTitle
  66. Me.PID = Loader.Input.Id
  67. WatcherLog("开始 Minecraft 日志监控")
  68. If Me.WindowTitle <> "" Then WatcherLog("要求窗口标题:" & WindowTitle)
  69. '更改列表
  70. Dim NewWatcherList As New List(Of Watcher)
  71. For Each Watch In McWatcherList
  72. If Watch.State = MinecraftState.Crashed OrElse Watch.State = MinecraftState.Ended OrElse Watch.State = MinecraftState.Canceled Then Continue For
  73. NewWatcherList.Add(Watch)
  74. Next
  75. NewWatcherList.Add(Me)
  76. McWatcherList = NewWatcherList
  77. WatcherStateChanged()
  78. '初始化进程与日志读取
  79. GameProcess = Loader.Input
  80. GameProcess.BeginOutputReadLine()
  81. GameProcess.BeginErrorReadLine()
  82. AddHandler GameProcess.OutputDataReceived, AddressOf LogReceived
  83. AddHandler GameProcess.ErrorDataReceived, AddressOf LogReceived
  84. '初始化时钟
  85. RunInNewThread(
  86. Sub()
  87. Try
  88. Do Until State = MinecraftState.Ended OrElse State = MinecraftState.Crashed OrElse State = MinecraftState.Canceled OrElse Loader.State = LoadState.Interrupted
  89. TimerWindow()
  90. TimerLog()
  91. '设置窗口标题
  92. For i = 1 To 3
  93. If IsWindowFinished AndAlso IsWindowAppeared AndAlso WindowTitle <> "" AndAlso State = MinecraftState.Running AndAlso Not GameProcess.HasExited Then
  94. Dim RealTitle As String = WindowTitle.Replace("{date}", Date.Now.ToString("yyyy'/'M'/'d")).Replace("{time}", Date.Now.ToString("HH':'mm':'ss"))
  95. SetWindowText(WindowHandle, RealTitle.ToCharArray)
  96. End If
  97. Thread.Sleep(64)
  98. Next
  99. Loop
  100. WatcherLog("Minecraft 日志监控已退出")
  101. Catch ex As Exception
  102. Log(ex, "Minecraft 日志监控主循环出错", NotifyLevel.MsgBoxAndFeedback)
  103. State = MinecraftState.Ended
  104. End Try
  105. End Sub, "Minecraft Watcher PID " & PID)
  106. End Sub
  107. '状态
  108. Private _State As MinecraftState = MinecraftState.Loading
  109. Public Property State As MinecraftState
  110. Get
  111. Return _State
  112. End Get
  113. Set(value As MinecraftState)
  114. If _State = value Then Return
  115. _State = value
  116. WatcherStateChanged()
  117. End Set
  118. End Property
  119. Public Enum MinecraftState
  120. Loading
  121. Running
  122. Crashed
  123. Ended
  124. Canceled
  125. End Enum
  126. '日志
  127. Public WaitingLog As New List(Of String)(1000)
  128. Private ReadOnly WaitingLogLock As New Object
  129. Private Sub LogReceived(sender As Object, e As DataReceivedEventArgs)
  130. SyncLock WaitingLogLock
  131. WaitingLog.Add(e.Data)
  132. End SyncLock
  133. End Sub
  134. Private Sub TimerLog()
  135. Try
  136. '输出文本
  137. Dim Copyed As New List(Of String)
  138. SyncLock WaitingLogLock
  139. If Not WaitingLog.Any() Then Return
  140. Copyed = WaitingLog
  141. WaitingLog = New List(Of String)(1000)
  142. End SyncLock
  143. For Each Str As String In Copyed
  144. GameLog(Str)
  145. Next
  146. If State = MinecraftState.Loading Then ProgressUpdate()
  147. '游戏退出检查
  148. If GameProcess.HasExited Then
  149. WatcherLog("Minecraft 已退出,返回值:" & GameProcess.ExitCode)
  150. 'If Process.ExitCode = 1 Then
  151. ' '返回值为 1,考虑是任务管理器结束
  152. ' WatcherLog("Minecraft 返回值为 1,考虑为任务管理器结束") '并不,崩了照样是 1
  153. ' State = MinecraftState.Ended
  154. 'Else
  155. If State = MinecraftState.Loading Then
  156. '窗口未出现
  157. WatcherLog("Minecraft 尚未加载完成,可能已崩溃")
  158. Crashed()
  159. ElseIf GameProcess.ExitCode <> 0 AndAlso State = MinecraftState.Running AndAlso Instance.ReleaseTime.Year >= 2012 Then
  160. '返回值不为 0 且未结束
  161. WatcherLog("Minecraft 返回值异常,可能已崩溃")
  162. Crashed()
  163. ElseIf State <> MinecraftState.Crashed Then
  164. '正常关闭
  165. State = MinecraftState.Ended
  166. End If
  167. End If
  168. Catch ex As Exception
  169. Log(ex, "输出 Minecraft 日志失败", NotifyLevel.MsgBoxAndFeedback)
  170. End Try
  171. End Sub
  172. Public LatestLog As New Queue(Of String)
  173. Private Sub GameLog(Text As String)
  174. '预处理
  175. If Text Is Nothing Then Return
  176. Text = Text.ReplaceLineEndings(vbCrLf)
  177. 'If Text.Contains("�����") Then Hint("检测到错误的日志编码:" & Text)
  178. '加入预存储
  179. LatestLog.Enqueue(Text)
  180. If LatestLog.Count >= 501 Then LatestLog.Dequeue()
  181. '进度处理
  182. If LogProgress < 1 Then
  183. WatcherLog("日志 1/5:已出现日志输出")
  184. LogProgress = 1
  185. End If '可能第一句就是后面需要判断的 Log(重现:启动 1.15.2 原版)
  186. If LogProgress < 2 AndAlso Text.Contains("Setting user:") Then
  187. WatcherLog("日志 2/5:游戏用户已设置") '仅确保支持 Minecraft 1.7+
  188. LogProgress = 2
  189. ElseIf LogProgress < 3 AndAlso Text.ContainsIgnoreCase("lwjgl version") Then
  190. WatcherLog("日志 3/5:LWJGL 版本已确认")
  191. LogProgress = 3
  192. ElseIf LogProgress < 4 AndAlso (Text.Contains("OpenAL initialized") OrElse Text.Contains("Starting up SoundSystem")) Then
  193. WatcherLog("日志 4/5:OpenAL 已加载") '仅确保支持 Minecraft 1.7+
  194. LogProgress = 4
  195. ElseIf LogProgress < 5 AndAlso ((Text.Contains("Created") AndAlso Text.Contains("textures") AndAlso Text.Contains("-atlas")) OrElse Text.Contains("Found animation info")) Then
  196. WatcherLog("日志 5/5:材质已加载") '仅确保支持 Minecraft 1.7+
  197. LogProgress = 5
  198. End If
  199. '输出日志
  200. 'Log(Text)
  201. '关闭与崩溃检测
  202. If Not Text.Contains("[CHAT]") Then
  203. If Text.Contains("Someone is closing me!") OrElse Text.Contains("Restarting Minecraft with command") Then '#1258
  204. WatcherLog("识别为关闭的 Log:" & Text)
  205. State = MinecraftState.Ended
  206. ElseIf Text.Contains("Crash report saved to") OrElse Text.Contains("This crash report has been saved to:") Then
  207. ' Text.Contains("Minecraft ran into a problem! Report saved to:") Then
  208. 'Minecraft 崩溃,忽略 VanillaFix
  209. WatcherLog("识别为崩溃的 Log:" & Text)
  210. Crashed()
  211. ElseIf Text.Contains("Could not save crash report to") Then
  212. 'Minecraft 崩溃,无法保存崩溃日志
  213. WatcherLog("识别为崩溃的 Log:" & Text)
  214. Crashed()
  215. ElseIf Text.Contains("/ERROR]: Unable to launch") OrElse Text.Contains("An exception was thrown, the game will display an error screen and halt.") Then
  216. 'Forge 崩溃
  217. WatcherLog("识别为崩溃的 Log:" & Text)
  218. Crashed()
  219. 'ElseIf Text.Contains("Shutdown failure!") Then
  220. ' 'Minecraft 强行崩溃,由于点 X 强行关闭也会触发这句话,所以不可用
  221. ' Crashed(Nothing)
  222. End If
  223. End If
  224. End Sub
  225. Private Sub WatcherLog(Text As String)
  226. McLaunchLog("[" & PID & "] " & Text)
  227. End Sub
  228. '进度更新
  229. Private LogProgress As Integer = 0
  230. Private Sub ProgressUpdate()
  231. Dim CurrentProgress As Double
  232. If IsWindowAppeared OrElse LogProgress = 5 Then
  233. CurrentProgress = 0.95
  234. WatcherLog("Minecraft 加载已完成")
  235. State = MinecraftState.Running
  236. Else
  237. CurrentProgress = Math.Min(LogProgress, 3) / 3 * 0.9
  238. End If
  239. Loader.Progress = CurrentProgress
  240. End Sub
  241. '窗口检查
  242. Private IsWindowAppeared As Boolean = False
  243. ''' <summary>
  244. ''' 窗口检查是否已经完成。这不一定代表着找到了窗口(如果没有找到,IsWindowAppeared 仍为 False)。
  245. ''' </summary>
  246. Private IsWindowFinished As Boolean = False
  247. Private WindowHandle As IntPtr
  248. Private Sub TimerWindow()
  249. Try
  250. If GameProcess.HasExited Then Return
  251. If IsWindowFinished Then Return
  252. '获取全部窗口,检查是否有新增的
  253. Dim MinecraftWindow As KeyValuePair(Of IntPtr, String)? = Nothing
  254. Try
  255. MinecraftWindow = TryGetMinecraftWindow()
  256. Catch ex As ComponentModel.Win32Exception
  257. '拒绝访问(#1062)
  258. Log(ex, "由于反作弊或安全软件拦截,PCL 无法操作游戏窗口", NotifyLevel.AllUsers)
  259. IsWindowFinished = True
  260. End Try
  261. If MinecraftWindow Is Nothing Then Return
  262. Dim MinecraftWindowName = MinecraftWindow.Value.Value, MinecraftWindowHandle = MinecraftWindow.Value.Key
  263. '已找到窗口
  264. If Not MinecraftWindowName.StartsWithF("FML") AndAlso Not MinecraftWindowName.StartsWithF("Quilt Loader") Then
  265. '已找到 Minecraft 窗口
  266. WindowHandle = MinecraftWindowHandle
  267. WatcherLog($"Minecraft 窗口已加载:{MinecraftWindowName}({MinecraftWindowHandle.ToInt64})")
  268. IsWindowFinished = True
  269. '最大化
  270. If Settings.Get("LaunchArgumentWindowType") = 4 Then
  271. RunInNewThread(
  272. Sub()
  273. Try
  274. '如果最大化导致屏幕渲染大小不对,那是 MC 的 Bug,不是我的 Bug
  275. '……虽然我很想这样说,但总有人反馈,算了
  276. Thread.Sleep(2000)
  277. ShowWindow(WindowHandle, 3)
  278. WatcherLog($"已最大化 Minecraft 窗口:{MinecraftWindowHandle.ToInt64}")
  279. Catch ex As Exception
  280. Log(ex, "最大化 Minecraft 窗口时出现错误")
  281. End Try
  282. End Sub, "MinecraftWindowMaximize")
  283. End If
  284. ElseIf Not IsWindowAppeared Then
  285. '已找到 Mod Loader 窗口
  286. WatcherLog("Mod Loader 窗口已加载:" & MinecraftWindowName & "(" & MinecraftWindowHandle.ToInt64 & ")")
  287. End If
  288. IsWindowAppeared = True
  289. Catch ex As Exception
  290. Log(ex, "检查 Minecraft 窗口失败", NotifyLevel.MsgBoxAndFeedback)
  291. End Try
  292. End Sub
  293. ''' <summary>
  294. ''' 获取可能是当前进程对应的 Minecraft 窗口的句柄和标题。
  295. ''' Nothing 代表未找到。
  296. ''' </summary>
  297. Private Function TryGetMinecraftWindow() As KeyValuePair(Of IntPtr, String)?
  298. TryGetMinecraftWindow = Nothing
  299. EnumWindows(
  300. Sub(hwnd As IntPtr, lParam As Integer)
  301. If TryGetMinecraftWindow IsNot Nothing Then Return
  302. '检查类名
  303. Dim str As New StringBuilder(512)
  304. GetClassName(hwnd, str, str.Capacity)
  305. Dim ClassName As String = str.ToString
  306. If Not (ClassName = "GLFW30" OrElse ClassName = "LWJGL" OrElse ClassName = "SunAwtFrame") Then Return
  307. '获取窗口标题名
  308. str = New StringBuilder(512)
  309. GetWindowText(hwnd, str, str.Capacity)
  310. Dim WindowText As String = str.ToString
  311. '有的 Mod 可以修改窗口标题,所以不能检测是否为 Minecraft 打头,这并不准确
  312. '部分版本会搞个 GLFW message window 出来所以得反选
  313. If Not (WindowText.StartsWithF("FML") OrElse (WindowText <> "PopupMessageWindow") AndAlso Not WindowText.StartsWithF("GLFW")) Then Return
  314. '获取窗口关联的进程
  315. Dim ProcessId As Integer
  316. GetWindowThreadProcessId(hwnd, ProcessId)
  317. Try
  318. If Process.GetProcessById(ProcessId).StartTime < GameProcess.StartTime Then Return '需要是此后启动的进程
  319. Catch ex As Exception
  320. Log(ex, "枚举 Minecraft 窗口进程失败")
  321. Return
  322. End Try
  323. '返回
  324. TryGetMinecraftWindow = New KeyValuePair(Of IntPtr, String)(hwnd, WindowText)
  325. End Sub, 0)
  326. Return TryGetMinecraftWindow
  327. End Function
  328. Private Delegate Sub EnumWindowsSub(hwnd As IntPtr, lParam As Integer)
  329. Private Declare Function EnumWindows Lib "user32" (hWnd As EnumWindowsSub, lParam As Integer) As Boolean
  330. Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (hWnd As Integer, str As StringBuilder, maxCount As Integer) As Integer
  331. Private Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" (hWnd As Integer, str As StringBuilder, maxCount As Integer) As Integer
  332. Private Declare Function SetWindowText Lib "user32" Alias "SetWindowTextA" (hWnd As Integer, str As String) As Boolean
  333. Private Declare Function ShowWindow Lib "user32" (hWnd As IntPtr, cmdWindow As UInteger) As Boolean
  334. Private Declare Function GetWindowThreadProcessId Lib "user32" (hWnd As IntPtr, ByRef lpdwProcessId As Integer) As Integer
  335. '崩溃处理
  336. Private Sub Crashed()
  337. If State = MinecraftState.Crashed OrElse State = MinecraftState.Ended Then Return
  338. State = MinecraftState.Crashed
  339. '崩溃分析
  340. WatcherLog("Minecraft 已崩溃,将在 2 秒后开始崩溃分析")
  341. Hint("检测到 Minecraft 出现错误,错误分析已开始……", HintType.Red)
  342. FeedbackInfo()
  343. RunInNewThread(
  344. Sub()
  345. Try
  346. Thread.Sleep(2000)
  347. WatcherLog("崩溃分析开始")
  348. Dim Analyzer As New CrashAnalyzer(Instance)
  349. Analyzer.Collect(Instance.PathIndie, LatestLog.ToList)
  350. Analyzer.Prepare()
  351. Analyzer.Analyze()
  352. Analyzer.Output(False, New List(Of String) From
  353. {Instance.PathVersion & Instance.Name & ".json", PathExeFolder & "PCL\Log1.txt", PathExeFolder & "PCL\LatestLaunch.bat"})
  354. Catch ex As Exception
  355. Log(ex, "崩溃分析失败", NotifyLevel.MsgBoxAndFeedback)
  356. End Try
  357. End Sub, "Crash Analyzer")
  358. End Sub
  359. '强制关闭
  360. Public Sub Kill()
  361. State = MinecraftState.Canceled
  362. WatcherLog("尝试强制结束 Minecraft 进程")
  363. Try
  364. If Not GameProcess.HasExited Then GameProcess.Kill()
  365. WatcherLog("已强制结束 Minecraft 进程")
  366. Catch ex As Exception
  367. Log(ex, "强制结束 Minecraft 进程失败", NotifyLevel.AllUsers)
  368. End Try
  369. End Sub
  370. End Class
  371. End Module