CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
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);
|
||
}
|
||
}
|