825 lines
30 KiB
C#
825 lines
30 KiB
C#
using System.ComponentModel;
|
||||
|
|
using System.Diagnostics;
|
|||
|
|
using System.Runtime.InteropServices;
|
|||
|
|
using System.Text;
|
|||
|
|
using System.Windows.Media;
|
|||
|
|
using PCL.Core.App;
|
|||
|
|
using PCL.Core.Logging;
|
|||
|
|
using PCL.Core.App.Localization;
|
|||
|
|
|
|||
|
|
namespace PCL;
|
|||
|
|
|
|||
|
|
public static class ModWatcher
|
|||
|
|
{
|
|||
|
|
// 对全体的监视
|
|||
|
|
public static List<Watcher> mcWatcherList = new();
|
|||
|
|
private static bool isWatcherRunning;
|
|||
|
|
public static bool hasRunningMinecraft;
|
|||
|
|
|
|||
|
|
private static void WatcherStateChanged()
|
|||
|
|
{
|
|||
|
|
var isRunning = false;
|
|||
|
|
var triggerLauncherShutdown = true;
|
|||
|
|
foreach (var Watcher in mcWatcherList)
|
|||
|
|
{
|
|||
|
|
if (Watcher.State == Watcher.MinecraftState.Loading || Watcher.State == Watcher.MinecraftState.Running)
|
|||
|
|
{
|
|||
|
|
isRunning = true;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (Watcher.State == Watcher.MinecraftState.Crashed || Watcher.State == Watcher.MinecraftState.Canceled)
|
|||
|
|
triggerLauncherShutdown = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (isWatcherRunning == isRunning)
|
|||
|
|
return;
|
|||
|
|
isWatcherRunning = isRunning;
|
|||
|
|
if (isWatcherRunning)
|
|||
|
|
MinecraftStart();
|
|||
|
|
else
|
|||
|
|
MinecraftStop(triggerLauncherShutdown);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void MinecraftStart()
|
|||
|
|
{
|
|||
|
|
ModLaunch.McLaunchLog("[全局] 出现运行中的 Minecraft");
|
|||
|
|
hasRunningMinecraft = true;
|
|||
|
|
ModMain.frmMain.BtnExtraShutdown.ShowRefresh();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void MinecraftStop(bool triggerLauncherShutdown)
|
|||
|
|
{
|
|||
|
|
ModLaunch.McLaunchLog("[全局] 已无运行中的 Minecraft");
|
|||
|
|
hasRunningMinecraft = false;
|
|||
|
|
ModMain.frmMain.BtnExtraShutdown.ShowRefresh();
|
|||
|
|
// 音乐播放
|
|||
|
|
if (Config.Preference.Music.StopInGame)
|
|||
|
|
ModBase.RunInUi(() =>
|
|||
|
|
{
|
|||
|
|
if (ModMusic.MusicResume()) ModBase.Log("[Music] 已根据设置,在结束后开始音乐播放");
|
|||
|
|
});
|
|||
|
|
else if (Config.Preference.Music.StartInGame)
|
|||
|
|
ModBase.RunInUi(() =>
|
|||
|
|
{
|
|||
|
|
if (ModMusic.MusicPause()) ModBase.Log("[Music] 已根据设置,在结束后暂停音乐播放");
|
|||
|
|
});
|
|||
|
|
// 开始视频背景播放
|
|||
|
|
ModVideoBack.IsGaming = false;
|
|||
|
|
ModVideoBack.VideoPlay();
|
|||
|
|
// 启动器可见性
|
|||
|
|
switch (Config.Launch.LauncherVisibility)
|
|||
|
|
{
|
|||
|
|
case LauncherVisibility.HideAndExit:
|
|||
|
|
// 直接关闭
|
|||
|
|
if (triggerLauncherShutdown)
|
|||
|
|
ModBase.RunInUi(() => ModMain.frmMain.EndProgram(false));
|
|||
|
|
else
|
|||
|
|
ModBase.RunInUi(() => ModMain.frmMain.Hidden = false);
|
|||
|
|
break;
|
|||
|
|
case LauncherVisibility.HideAndReopen:
|
|||
|
|
// 恢复
|
|||
|
|
ModBase.RunInUi(() => ModMain.frmMain.Hidden = false);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static GameLogLevel GetLevel(string line, GameLogLevel lastLevel)
|
|||
|
|
{
|
|||
|
|
Func<string, SolidColorBrush> getColorBrush =
|
|||
|
|
name => (SolidColorBrush)System.Windows.Application.Current.Resources[name];
|
|||
|
|
var starting = line.Split(": ")[0];
|
|||
|
|
if (starting.ContainsF("FATAL"))
|
|||
|
|
return GameLogLevel.Fatal;
|
|||
|
|
if (starting.ContainsF("ERROR"))
|
|||
|
|
return GameLogLevel.Error;
|
|||
|
|
if (starting.ContainsF("WARN"))
|
|||
|
|
return GameLogLevel.Warn;
|
|||
|
|
if (starting.ContainsF("INFO"))
|
|||
|
|
return GameLogLevel.Info;
|
|||
|
|
if (starting.ContainsF("DEBUG"))
|
|||
|
|
return GameLogLevel.Debug;
|
|||
|
|
if (line.StartsWithF("Exception in thread \""))
|
|||
|
|
return GameLogLevel.Error;
|
|||
|
|
if ((line.ContainsF("Exception") || line.ContainsF("Realms authentication error with message ")) &&
|
|||
|
|
lastLevel >= GameLogLevel.Warn)
|
|||
|
|
return lastLevel;
|
|||
|
|
if (line.StartsWithF(" at ") && lastLevel >= GameLogLevel.Warn)
|
|||
|
|
return lastLevel;
|
|||
|
|
return GameLogLevel.Info;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static SolidColorBrush GetColor(GameLogLevel level)
|
|||
|
|
{
|
|||
|
|
Func<string, SolidColorBrush> getColorBrush =
|
|||
|
|
name => (SolidColorBrush)System.Windows.Application.Current.Resources[name];
|
|||
|
|
switch (level)
|
|||
|
|
{
|
|||
|
|
case GameLogLevel.Debug:
|
|||
|
|
{
|
|||
|
|
return getColorBrush("ColorBrushDebug");
|
|||
|
|
}
|
|||
|
|
case GameLogLevel.Info:
|
|||
|
|
{
|
|||
|
|
getColorBrush(ThemeManager.IsDarkMode ? "ColorBrushInfoDark" : "ColorBrushInfo");
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
case GameLogLevel.Warn:
|
|||
|
|
{
|
|||
|
|
return getColorBrush("ColorBrushWarn");
|
|||
|
|
}
|
|||
|
|
case GameLogLevel.Error:
|
|||
|
|
{
|
|||
|
|
return getColorBrush("ColorBrushError");
|
|||
|
|
}
|
|||
|
|
case GameLogLevel.Fatal:
|
|||
|
|
{
|
|||
|
|
return getColorBrush("ColorBrushFatal");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return getColorBrush(ThemeManager.IsDarkMode ? "ColorBrushInfoDark" : "ColorBrushInfo");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 实时日志处理
|
|||
|
|
public class LogOutputEventArgs : EventArgs
|
|||
|
|
{
|
|||
|
|
public SolidColorBrush color;
|
|||
|
|
public string logText;
|
|||
|
|
|
|||
|
|
public LogOutputEventArgs(string logText, SolidColorBrush color)
|
|||
|
|
{
|
|||
|
|
this.logText = logText;
|
|||
|
|
this.color = color;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private enum GameLogLevel
|
|||
|
|
{
|
|||
|
|
Debug = 0,
|
|||
|
|
Info = 1,
|
|||
|
|
Warn = 2,
|
|||
|
|
Error = 3,
|
|||
|
|
Fatal = 4
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 对单个进程的监视
|
|||
|
|
public class Watcher
|
|||
|
|
{
|
|||
|
|
public delegate void GameExitEventHandler();
|
|||
|
|
|
|||
|
|
public delegate void LogOutputEventHandler(Watcher sender, LogOutputEventArgs e);
|
|||
|
|
|
|||
|
|
public enum MinecraftState
|
|||
|
|
{
|
|||
|
|
Loading,
|
|||
|
|
Running,
|
|||
|
|
Crashed,
|
|||
|
|
Ended,
|
|||
|
|
Canceled
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private readonly int pid;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 是否处理实时日志。
|
|||
|
|
/// </summary>
|
|||
|
|
private readonly bool realTime;
|
|||
|
|
|
|||
|
|
private readonly object waitingLogLock = new();
|
|||
|
|
public uint countDebug;
|
|||
|
|
public uint countError;
|
|||
|
|
public uint countFatal;
|
|||
|
|
public uint countInfo;
|
|||
|
|
public uint countWarn;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 游戏的所有日志输出,只有处理实时日志的情况下才会记录。
|
|||
|
|
/// </summary>
|
|||
|
|
public List<string> fullLog = new();
|
|||
|
|
|
|||
|
|
// 初始化
|
|||
|
|
public Process gameProcess;
|
|||
|
|
|
|||
|
|
// 窗口检查
|
|||
|
|
private bool isWindowAppeared;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 窗口检查是否已经完成。这不一定代表着找到了窗口(如果没有找到,IsWindowAppeared 仍为 False)。
|
|||
|
|
/// </summary>
|
|||
|
|
private bool isWindowFinished;
|
|||
|
|
|
|||
|
|
public string jStackPath;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 上一行日志级别。
|
|||
|
|
/// </summary>
|
|||
|
|
private GameLogLevel lastLevel = GameLogLevel.Info;
|
|||
|
|
|
|||
|
|
public Queue<string> latestLog = new();
|
|||
|
|
public ModLoader.LoaderTask<Process, int> loader;
|
|||
|
|
|
|||
|
|
// 进度更新
|
|||
|
|
private int logProgress;
|
|||
|
|
public McInstance version;
|
|||
|
|
|
|||
|
|
// 日志
|
|||
|
|
public List<string> waitingLog = new(1000);
|
|||
|
|
private nint windowHandle;
|
|||
|
|
private string windowTitle = "";
|
|||
|
|
|
|||
|
|
public Watcher(ModLoader.LoaderTask<Process, int> loader, McInstance version, string windowTitle,
|
|||
|
|
string jStackPath, bool outputRealTime = false)
|
|||
|
|
{
|
|||
|
|
this.loader = loader;
|
|||
|
|
this.version = version;
|
|||
|
|
this.windowTitle = windowTitle;
|
|||
|
|
realTime = outputRealTime;
|
|||
|
|
pid = loader.input.Id;
|
|||
|
|
this.jStackPath = jStackPath;
|
|||
|
|
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Start"));
|
|||
|
|
if (string.IsNullOrWhiteSpace(windowTitle))
|
|||
|
|
WatcherLog("要求窗口标题:" + windowTitle);
|
|||
|
|
|
|||
|
|
// 更改列表
|
|||
|
|
var newWatcherList = new List<Watcher>();
|
|||
|
|
foreach (var Watch in mcWatcherList)
|
|||
|
|
{
|
|||
|
|
if (Watch.State == MinecraftState.Crashed || Watch.State == MinecraftState.Ended ||
|
|||
|
|
Watch.State == MinecraftState.Canceled)
|
|||
|
|
continue;
|
|||
|
|
newWatcherList.Add(Watch);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
newWatcherList.Add(this);
|
|||
|
|
mcWatcherList = newWatcherList;
|
|||
|
|
WatcherStateChanged();
|
|||
|
|
|
|||
|
|
// 初始化进程与日志读取
|
|||
|
|
gameProcess = loader.input;
|
|||
|
|
gameProcess.BeginOutputReadLine();
|
|||
|
|
gameProcess.BeginErrorReadLine();
|
|||
|
|
gameProcess.OutputDataReceived += LogReceived;
|
|||
|
|
gameProcess.ErrorDataReceived += LogReceived;
|
|||
|
|
|
|||
|
|
// 初始化时钟
|
|||
|
|
// 设置窗口标题
|
|||
|
|
|
|||
|
|
ModBase.RunInNewThread(() =>
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
while (State != MinecraftState.Ended && State != MinecraftState.Crashed &&
|
|||
|
|
State != MinecraftState.Canceled && loader.State != ModBase.LoadState.Aborted)
|
|||
|
|
{
|
|||
|
|
TimerWindow();
|
|||
|
|
TimerLog();
|
|||
|
|
if (!string.IsNullOrWhiteSpace(windowTitle))
|
|||
|
|
for (var i = 1; i <= 3; i++)
|
|||
|
|
{
|
|||
|
|
if (State == MinecraftState.Running && !gameProcess.HasExited)
|
|||
|
|
{
|
|||
|
|
var realTitle = windowTitle.Replace("{date}", Lang.Date(DateTime.Now, "d"))
|
|||
|
|
.Replace("{time}", Lang.Date(DateTime.Now, "T"));
|
|||
|
|
SetWindowText(windowHandle, realTitle);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Thread.Sleep(64);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Thread.Sleep(10);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Exited"));
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
ModBase.Log(
|
|||
|
|
ex,
|
|||
|
|
"Minecraft 日志监控主循环出错",
|
|||
|
|
ModBase.LogLevel.Feedback,
|
|||
|
|
userSummary: Lang.Text("Minecraft.Launch.Error.WatcherOperationFailed"));
|
|||
|
|
State = MinecraftState.Ended;
|
|||
|
|
}
|
|||
|
|
}, "Minecraft Watcher PID " + pid);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public MinecraftState State
|
|||
|
|
{
|
|||
|
|
get => field;
|
|||
|
|
set
|
|||
|
|
{
|
|||
|
|
if (field == value)
|
|||
|
|
return;
|
|||
|
|
field = value;
|
|||
|
|
WatcherStateChanged();
|
|||
|
|
}
|
|||
|
|
} = MinecraftState.Loading;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 是否处理实时日志。
|
|||
|
|
/// </summary>
|
|||
|
|
public bool RealTimeLog => realTime;
|
|||
|
|
|
|||
|
|
// 状态
|
|||
|
|
/// <summary>
|
|||
|
|
/// 游戏退出时触发。
|
|||
|
|
/// </summary>
|
|||
|
|
public event GameExitEventHandler? GameExit;
|
|||
|
|
|
|||
|
|
private void LogReceived(object sender, DataReceivedEventArgs e)
|
|||
|
|
{
|
|||
|
|
lock (waitingLogLock)
|
|||
|
|
{
|
|||
|
|
waitingLog.Add(e.Data);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (realTime)
|
|||
|
|
{
|
|||
|
|
LogRealTime(e.Data, ref lastLevel);
|
|||
|
|
if (e.Data is not null)
|
|||
|
|
fullLog.Add(e.Data);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 触发日志改变事件,并统计日志行数。
|
|||
|
|
/// </summary>
|
|||
|
|
private void LogRealTime(string line, ref GameLogLevel level)
|
|||
|
|
{
|
|||
|
|
if (line is null)
|
|||
|
|
return; // 杀游戏进程时有概率传 null
|
|||
|
|
level = line.StartsWithF(" at ") || line.StartsWithF("Caused by: ") || line.StartsWithF(" ... ")
|
|||
|
|
? level
|
|||
|
|
: GetLevel(line, level);
|
|||
|
|
|
|||
|
|
// “ ... 4 more”
|
|||
|
|
var color = GetColor(level);
|
|||
|
|
switch (level)
|
|||
|
|
{
|
|||
|
|
case GameLogLevel.Debug:
|
|||
|
|
{
|
|||
|
|
countDebug = (uint)(countDebug + 1L);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
case GameLogLevel.Info:
|
|||
|
|
{
|
|||
|
|
countInfo = (uint)(countInfo + 1L);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
case GameLogLevel.Warn:
|
|||
|
|
{
|
|||
|
|
countWarn = (uint)(countWarn + 1L);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
case GameLogLevel.Error:
|
|||
|
|
{
|
|||
|
|
countError = (uint)(countError + 1L);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
case GameLogLevel.Fatal:
|
|||
|
|
{
|
|||
|
|
countFatal = (uint)(countFatal + 1L);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
LogOutput?.Invoke(this, new LogOutputEventArgs(line, color));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 有新的日志输出,日志计数器发生改变时触发。
|
|||
|
|
/// </summary>
|
|||
|
|
public event LogOutputEventHandler? LogOutput;
|
|||
|
|
|
|||
|
|
private void TimerLog()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 输出文本
|
|||
|
|
var copyed = new List<string>();
|
|||
|
|
lock (waitingLogLock)
|
|||
|
|
{
|
|||
|
|
if (!waitingLog.Any())
|
|||
|
|
return;
|
|||
|
|
copyed = waitingLog;
|
|||
|
|
waitingLog = new List<string>(1000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
foreach (var Str in copyed)
|
|||
|
|
GameLog(Str);
|
|||
|
|
if (State == MinecraftState.Loading)
|
|||
|
|
ProgressUpdate();
|
|||
|
|
// 游戏退出检查
|
|||
|
|
if (gameProcess.HasExited)
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.ProcessExited", gameProcess.ExitCode));
|
|||
|
|
// 实时日志输出
|
|||
|
|
if (realTime)
|
|||
|
|
{
|
|||
|
|
var arglevel = GameLogLevel.Info;
|
|||
|
|
LogRealTime(Lang.Text("Watcher.ProcessExited", gameProcess.ExitCode), ref arglevel);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
GameExit?.Invoke();
|
|||
|
|
// If Process.ExitCode = 1 Then
|
|||
|
|
// '返回值为 1,考虑是任务管理器结束
|
|||
|
|
// WatcherLog("Minecraft 返回值为 1,考虑为任务管理器结束") '并不,崩了照样是 1
|
|||
|
|
// State = MinecraftState.Ended
|
|||
|
|
// Else
|
|||
|
|
if (State == MinecraftState.Loading)
|
|||
|
|
{
|
|||
|
|
// 窗口未出现
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Crash.Suspected"));
|
|||
|
|
Crashed();
|
|||
|
|
}
|
|||
|
|
else if (gameProcess.ExitCode != 0 && State == MinecraftState.Running &&
|
|||
|
|
version.releaseTime.Year >= 2012)
|
|||
|
|
{
|
|||
|
|
// 返回值不为 0 且未结束
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Crash.AbnormalExit"));
|
|||
|
|
Crashed();
|
|||
|
|
}
|
|||
|
|
else if (State != MinecraftState.Crashed)
|
|||
|
|
{
|
|||
|
|
// 正常关闭
|
|||
|
|
State = MinecraftState.Ended;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
ModBase.Log(
|
|||
|
|
ex,
|
|||
|
|
"输出 Minecraft 日志失败",
|
|||
|
|
ModBase.LogLevel.Feedback,
|
|||
|
|
userSummary: Lang.Text("Minecraft.Launch.Error.WatcherOperationFailed"));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void GameLog(string text)
|
|||
|
|
{
|
|||
|
|
// 预处理
|
|||
|
|
if (text is null)
|
|||
|
|
return;
|
|||
|
|
text = text.Replace("\r\n", "\r").Replace("\n", "\r")
|
|||
|
|
.Replace("\r", "\r\n");
|
|||
|
|
// If Text.Contains("�����") Then Hint("检测到错误的日志编码:" & Text)
|
|||
|
|
// 加入预存储
|
|||
|
|
latestLog.Enqueue(text);
|
|||
|
|
if (latestLog.Count >= 501)
|
|||
|
|
latestLog.Dequeue();
|
|||
|
|
// 进度处理
|
|||
|
|
if (logProgress < 1)
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Progress.LogAppeared"));
|
|||
|
|
logProgress = 1;
|
|||
|
|
} // 可能第一句就是后面需要判断的 Log(重现:启动 1.15.2 原版)
|
|||
|
|
|
|||
|
|
if (logProgress < 2 && text.Contains("Setting user:"))
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Progress.UserSet")); // 仅确保支持 Minecraft 1.7+
|
|||
|
|
logProgress = 2;
|
|||
|
|
}
|
|||
|
|
else if (logProgress < 3 && text.ContainsF("lwjgl version", true))
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Progress.LwjglConfirmed"));
|
|||
|
|
logProgress = 3;
|
|||
|
|
}
|
|||
|
|
else if (logProgress < 4 &&
|
|||
|
|
(text.Contains("OpenAL initialized") || text.Contains("Starting up SoundSystem")))
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Progress.OpenAlLoaded")); // 仅确保支持 Minecraft 1.7+
|
|||
|
|
logProgress = 4;
|
|||
|
|
}
|
|||
|
|
else if (logProgress < 5 &&
|
|||
|
|
((text.Contains("Created") && text.Contains("textures") && text.Contains("-atlas")) ||
|
|||
|
|
text.Contains("Found animation info")))
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Progress.TexturesLoaded")); // 仅确保支持 Minecraft 1.7+
|
|||
|
|
logProgress = 5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 输出日志
|
|||
|
|
// Log(Text)
|
|||
|
|
// 关闭与崩溃检测
|
|||
|
|
if (!text.Contains("[CHAT]"))
|
|||
|
|
{
|
|||
|
|
if (text.Contains("Someone is closing me!") ||
|
|||
|
|
text.Contains("Restarting Minecraft with command")) // #1258
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Log.CloseDetected", text));
|
|||
|
|
State = MinecraftState.Ended;
|
|||
|
|
}
|
|||
|
|
else if (text.Contains("Crash report saved to") ||
|
|||
|
|
text.Contains("This crash report has been saved to:"))
|
|||
|
|
{
|
|||
|
|
// Text.Contains("Minecraft ran into a problem! Report saved to:") Then
|
|||
|
|
// Minecraft 崩溃,忽略 VanillaFix
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Log.CrashDetected", text));
|
|||
|
|
Crashed();
|
|||
|
|
}
|
|||
|
|
else if (text.Contains("Could not save crash report to"))
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Log.CrashDetected", text));
|
|||
|
|
Crashed();
|
|||
|
|
}
|
|||
|
|
else if (text.Contains("/ERROR]: Unable to launch") ||
|
|||
|
|
text.Contains("An exception was thrown, the game will display an error screen and halt."))
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Log.CrashDetected", text));
|
|||
|
|
Crashed();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void WatcherLog(string text)
|
|||
|
|
{
|
|||
|
|
ModLaunch.McLaunchLog("[" + pid + "] " + text);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void ProgressUpdate()
|
|||
|
|
{
|
|||
|
|
double currentProgress;
|
|||
|
|
if (isWindowAppeared || logProgress >= 4)
|
|||
|
|
{
|
|||
|
|
currentProgress = 0.95d;
|
|||
|
|
WatcherLog(Lang.Text("Watcher.LoadComplete"));
|
|||
|
|
State = MinecraftState.Running;
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
currentProgress = Math.Min(logProgress, 3) / 3d * 0.9d;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
loader.Progress = currentProgress;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void TimerWindow()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (gameProcess.HasExited)
|
|||
|
|
return;
|
|||
|
|
if (isWindowFinished)
|
|||
|
|
return;
|
|||
|
|
// 获取全部窗口,检查是否有新增的
|
|||
|
|
KeyValuePair<nint, string>? minecraftWindow = default;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
minecraftWindow = TryGetMinecraftWindow();
|
|||
|
|
}
|
|||
|
|
catch (Win32Exception ex)
|
|||
|
|
{
|
|||
|
|
// 拒绝访问(#1062)
|
|||
|
|
ModBase.Log(
|
|||
|
|
ex,
|
|||
|
|
Lang.Text("Watcher.SecurityBlocked"),
|
|||
|
|
ModBase.LogLevel.Hint,
|
|||
|
|
userSummary: Lang.Text("Watcher.SecurityBlocked"));
|
|||
|
|
isWindowFinished = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (minecraftWindow is null)
|
|||
|
|
return;
|
|||
|
|
var minecraftWindowName = minecraftWindow.Value.Value;
|
|||
|
|
var minecraftWindowHandle = minecraftWindow.Value.Key;
|
|||
|
|
// 已找到窗口
|
|||
|
|
if (!minecraftWindowName.StartsWithF("FML") && !minecraftWindowName.StartsWithF("Quilt Loader"))
|
|||
|
|
{
|
|||
|
|
// 已找到 Minecraft 窗口
|
|||
|
|
windowHandle = minecraftWindowHandle;
|
|||
|
|
WatcherLog(Lang.Text("Watcher.WindowLoaded", minecraftWindowName, minecraftWindowHandle.ToInt64()));
|
|||
|
|
isWindowFinished = true;
|
|||
|
|
// 最大化
|
|||
|
|
if (Config.Launch.GameWindowMode == GameWindowSizeMode.Maximized)
|
|||
|
|
// 如果最大化导致屏幕渲染大小不对,那是 MC 的 Bug,不是我的 Bug
|
|||
|
|
// ……虽然我很想这样说,但总有人反馈,算了
|
|||
|
|
ModBase.RunInNewThread(() =>
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
Thread.Sleep(2000);
|
|||
|
|
ShowWindow(windowHandle, 3U);
|
|||
|
|
WatcherLog(Lang.Text("Watcher.WindowMaximized", minecraftWindowHandle.ToInt64()));
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
ModBase.Log(ex, "最大化 Minecraft 窗口时出现错误");
|
|||
|
|
}
|
|||
|
|
}, "MinecraftWindowMaximize");
|
|||
|
|
}
|
|||
|
|
else if (!isWindowAppeared)
|
|||
|
|
{
|
|||
|
|
// 已找到 FML 窗口
|
|||
|
|
WatcherLog(Lang.Text("Watcher.FmlWindowLoaded", minecraftWindowName, minecraftWindowHandle.ToInt64()));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isWindowAppeared = true;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
ModBase.Log(
|
|||
|
|
ex,
|
|||
|
|
"检查 Minecraft 窗口失败",
|
|||
|
|
ModBase.LogLevel.Feedback,
|
|||
|
|
userSummary: Lang.Text("Minecraft.Launch.Error.WatcherOperationFailed"));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取可能是当前进程对应的 Minecraft 窗口的句柄和标题。
|
|||
|
|
/// Nothing 代表未找到。
|
|||
|
|
/// </summary>
|
|||
|
|
private KeyValuePair<nint, string>? TryGetMinecraftWindow()
|
|||
|
|
{
|
|||
|
|
KeyValuePair<nint, string>? tryGetMinecraftWindowRet = default;
|
|||
|
|
tryGetMinecraftWindowRet = default;
|
|||
|
|
EnumWindows((hwnd, lParam) =>
|
|||
|
|
{
|
|||
|
|
if (tryGetMinecraftWindowRet is not null)
|
|||
|
|
return false; // 找到后停止枚举
|
|||
|
|
|
|||
|
|
var str = new StringBuilder(512);
|
|||
|
|
GetClassName(hwnd, str, str.Capacity);
|
|||
|
|
var className = str.ToString();
|
|||
|
|
|
|||
|
|
if (!(className == "GLFW30" || className == "LWJGL" || className == "SunAwtFrame"))
|
|||
|
|
return true;
|
|||
|
|
|
|||
|
|
// 获取窗口标题名
|
|||
|
|
str = new StringBuilder(512);
|
|||
|
|
GetWindowText(hwnd, str, str.Capacity);
|
|||
|
|
var windowText = str.ToString();
|
|||
|
|
|
|||
|
|
// 部分版本会搞个 GLFW message window 出来所以得反选
|
|||
|
|
if (!(windowText.StartsWithF("FML") ||
|
|||
|
|
(windowText != "PopupMessageWindow" && !windowText.StartsWithF("GLFW"))))
|
|||
|
|
return true;
|
|||
|
|
|
|||
|
|
// 获取窗口关联的进程
|
|||
|
|
var processId = default(int);
|
|||
|
|
GetWindowThreadProcessId(hwnd, ref processId);
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (processId != gameProcess.Id)
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 找到目标,赋值并停止枚举
|
|||
|
|
tryGetMinecraftWindowRet = new KeyValuePair<nint, string>(hwnd, windowText);
|
|||
|
|
return false;
|
|||
|
|
}, nint.Zero);
|
|||
|
|
return tryGetMinecraftWindowRet;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[DllImport("user32")]
|
|||
|
|
private static extern bool EnumWindows(EnumWindowsSub lpEnumFunc, nint lParam);
|
|||
|
|
|
|||
|
|
[DllImport("user32", EntryPoint = "GetClassNameW", CharSet = CharSet.Unicode)]
|
|||
|
|
private static extern int GetClassName(nint hWnd, StringBuilder lpClassName, int nMaxCount);
|
|||
|
|
|
|||
|
|
[DllImport("user32", EntryPoint = "GetWindowTextW", CharSet = CharSet.Unicode)]
|
|||
|
|
private static extern int GetWindowText(nint hWnd, StringBuilder lpString, int nMaxCount);
|
|||
|
|
|
|||
|
|
[DllImport("user32", EntryPoint = "SetWindowTextW", CharSet = CharSet.Unicode)]
|
|||
|
|
private static extern bool SetWindowText(nint hWnd, string lpString);
|
|||
|
|
|
|||
|
|
[DllImport("user32")]
|
|||
|
|
private static extern bool ShowWindow(nint hWnd, uint cmdWindow);
|
|||
|
|
|
|||
|
|
[DllImport("user32")]
|
|||
|
|
private static extern int GetWindowThreadProcessId(nint hWnd, ref int lpdwProcessId);
|
|||
|
|
|
|||
|
|
// 崩溃处理
|
|||
|
|
private void Crashed()
|
|||
|
|
{
|
|||
|
|
if (State is MinecraftState.Crashed or MinecraftState.Ended)
|
|||
|
|
return;
|
|||
|
|
State = MinecraftState.Crashed;
|
|||
|
|
// 崩溃分析
|
|||
|
|
if (!Config.Launch.DisableCrashAnalysis)
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Crash.Detected"));
|
|||
|
|
HintService.Hint(Lang.Text("Watcher.Crash.Hint"));
|
|||
|
|
|
|||
|
|
ModBase.FeedbackInfo();
|
|||
|
|
ModBase.RunInNewThread(() =>
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
Thread.Sleep(2000);
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Crash.AnalysisStart"));
|
|||
|
|
var analyzer = new CrashAnalyzer(pid);
|
|||
|
|
analyzer.Collect(version.PathIndie, latestLog.ToList());
|
|||
|
|
analyzer.Prepare();
|
|||
|
|
analyzer.Analyze(version);
|
|||
|
|
analyzer.Output(
|
|||
|
|
false,
|
|||
|
|
[
|
|||
|
|
version.PathInstance + version.Name + ".json",
|
|||
|
|
LogWrapper.CurrentLogger.CurrentLogFiles.Last(),
|
|||
|
|
ModBase.exePath + @"PCL\LatestLaunch.bat"
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
ModBase.Log(
|
|||
|
|
ex,
|
|||
|
|
"崩溃分析失败",
|
|||
|
|
ModBase.LogLevel.Feedback,
|
|||
|
|
userSummary: Lang.Text("Crash.Analysis.Error.Failed"));
|
|||
|
|
}
|
|||
|
|
}, "Crash Analyzer");
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Crash.DetectedDisabled"));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 强制关闭
|
|||
|
|
public bool CheckAlive(Process p)
|
|||
|
|
{
|
|||
|
|
if (!p.HasExited)
|
|||
|
|
return true;
|
|||
|
|
var exists = Array.Exists(Process.GetProcesses(), item => item.Id == p.Id);
|
|||
|
|
if (exists)
|
|||
|
|
return true;
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void Kill()
|
|||
|
|
{
|
|||
|
|
State = MinecraftState.Canceled;
|
|||
|
|
ModBase.RunInNewThread(() =>
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Kill.Attempt"));
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (CheckAlive(gameProcess))
|
|||
|
|
gameProcess.Kill();
|
|||
|
|
gameProcess.WaitForExit(5000);
|
|||
|
|
if (CheckAlive(gameProcess))
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Kill.TaskkillAttempt"));
|
|||
|
|
var taskkillInfo = new ProcessStartInfo
|
|||
|
|
{
|
|||
|
|
FileName = "taskkill.exe",
|
|||
|
|
Arguments = $"/PID {gameProcess.Id} /F /T",
|
|||
|
|
RedirectStandardOutput = true,
|
|||
|
|
UseShellExecute = false
|
|||
|
|
};
|
|||
|
|
var taskkillProcess = Process.Start(taskkillInfo);
|
|||
|
|
var output = taskkillProcess.StandardOutput.ReadToEnd();
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Kill.TaskkillResult", output));
|
|||
|
|
gameProcess.WaitForExit(5000);
|
|||
|
|
if (CheckAlive(gameProcess))
|
|||
|
|
{
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Kill.Timeout"));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
WatcherLog(Lang.Text("Watcher.Kill.Success"));
|
|||
|
|
if (realTime)
|
|||
|
|
{
|
|||
|
|
var arglevel = GameLogLevel.Info;
|
|||
|
|
LogRealTime(Lang.Text("Watcher.ProcessExited", gameProcess.ExitCode), ref arglevel);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
GameExit?.Invoke();
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
ModBase.Log(
|
|||
|
|
ex,
|
|||
|
|
Lang.Text("Watcher.Kill.Failed"),
|
|||
|
|
ModBase.LogLevel.Hint,
|
|||
|
|
userSummary: Lang.Text("Watcher.Kill.Failed"));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 导出运行栈
|
|||
|
|
public List<string> ExportStackDump(string savePath)
|
|||
|
|
{
|
|||
|
|
var dump = new List<string>();
|
|||
|
|
for (var i = 1; i <= 3; i++)
|
|||
|
|
{
|
|||
|
|
dump.Add(ModBase.ShellAndGetOutput(jStackPath, "-l -e " + gameProcess.Id));
|
|||
|
|
Thread.Sleep(3000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return dump;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private delegate bool EnumWindowsSub(nint hwnd, nint lParam);
|
|||
|
|
}
|
|||
|
|
}
|