初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
+121
@@ -0,0 +1,121 @@
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashDetector
|
||||
{
|
||||
private readonly CrashEvidenceExtractor _extractor = new();
|
||||
private readonly CrashStackAnalyzer _stackAnalyzer = new();
|
||||
|
||||
public CrashAnalysisResult Analyze(CrashLogSet logs, McInstance? instance)
|
||||
{
|
||||
LogWrapper.Info("Crash", "步骤 3:分析崩溃原因");
|
||||
var result = new CrashAnalysisResult();
|
||||
if (!logs.HasAnalyzableLog)
|
||||
{
|
||||
result.Add(new CrashFinding(CrashCause.NoAnalyzableFile, CrashConfidence.High) { ShouldStop = true });
|
||||
_LogSummary(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
var normalizedLogs = _Normalize(logs);
|
||||
var modIndex = CrashModIndex.Create(normalizedLogs, instance);
|
||||
|
||||
_RunPhase(DetectionPhase.Fatal, normalizedLogs, modIndex, result);
|
||||
if (_ShouldStop(result))
|
||||
{
|
||||
_LogSummary(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
_RunPhase(DetectionPhase.Primary, normalizedLogs, modIndex, result);
|
||||
if (_ShouldStop(result))
|
||||
{
|
||||
_LogSummary(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
var stackFinding = _stackAnalyzer.Analyze(normalizedLogs, modIndex);
|
||||
if (stackFinding is not null)
|
||||
result.Add(stackFinding);
|
||||
if (_ShouldStop(result))
|
||||
{
|
||||
_LogSummary(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
_RunPhase(DetectionPhase.Secondary, normalizedLogs, modIndex, result);
|
||||
_LogSummary(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CrashLogSet _Normalize(CrashLogSet logs)
|
||||
{
|
||||
var all = logs.All;
|
||||
if (all.Contains("quilt", StringComparison.OrdinalIgnoreCase) &&
|
||||
all.Contains("Mod Table Version", StringComparison.Ordinal))
|
||||
{
|
||||
LogWrapper.Info("Crash", "处理 Quilt Mod Table 后再继续分析");
|
||||
all = CrashText.BeforeFirst(all, "| Index") + CrashText.AfterFirst(all, "Mod Table Version:");
|
||||
}
|
||||
|
||||
return new CrashLogSet
|
||||
{
|
||||
Game = logs.Game,
|
||||
Debug = logs.Debug,
|
||||
CrashReport = logs.CrashReport,
|
||||
HsErr = logs.HsErr,
|
||||
All = all
|
||||
};
|
||||
}
|
||||
|
||||
private void _RunPhase(
|
||||
DetectionPhase phase,
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex,
|
||||
CrashAnalysisResult result)
|
||||
{
|
||||
var input = new CrashRuleInput { Logs = logs, ModIndex = modIndex };
|
||||
foreach (var rule in CrashRuleCatalog.Rules.Where(rule => rule.Phase == phase))
|
||||
{
|
||||
var finding = rule.Evaluate(input);
|
||||
if (finding is not null)
|
||||
_AddAndLog(result, finding);
|
||||
}
|
||||
|
||||
foreach (var finding in _extractor.Extract(phase, logs, modIndex))
|
||||
_AddAndLog(result, finding);
|
||||
}
|
||||
|
||||
private static void _AddAndLog(CrashAnalysisResult result, CrashFinding finding)
|
||||
{
|
||||
result.Add(finding);
|
||||
_LogFinding(finding);
|
||||
}
|
||||
|
||||
private static bool _ShouldStop(CrashAnalysisResult result)
|
||||
{
|
||||
return result.Findings.Any(finding => finding.ShouldStop);
|
||||
}
|
||||
|
||||
private static void _LogFinding(CrashFinding finding)
|
||||
{
|
||||
var evidence = string.Join(";", finding.Details);
|
||||
LogWrapper.Info(
|
||||
"Crash",
|
||||
$"可能的崩溃原因:{finding.Cause}{(string.IsNullOrEmpty(evidence) ? "" : "(" + evidence + ")")}");
|
||||
}
|
||||
|
||||
private static void _LogSummary(CrashAnalysisResult result)
|
||||
{
|
||||
if (!result.Any)
|
||||
{
|
||||
LogWrapper.Info("Crash", "步骤 3:分析崩溃原因完成,未找到可能的原因");
|
||||
return;
|
||||
}
|
||||
|
||||
LogWrapper.Info("Crash", $"步骤 3:分析崩溃原因完成,找到 {result.Findings.Count} 条可能的原因");
|
||||
foreach (var finding in result.Findings)
|
||||
_LogFinding(finding);
|
||||
}
|
||||
}
|
||||
+714
@@ -0,0 +1,714 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashEvidenceExtractor
|
||||
{
|
||||
public IEnumerable<CrashFinding> Extract(
|
||||
DetectionPhase phase,
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
return phase switch
|
||||
{
|
||||
DetectionPhase.Fatal => _ExtractFatal(logs, modIndex),
|
||||
DetectionPhase.Primary => _ExtractPrimary(logs, modIndex),
|
||||
DetectionPhase.Secondary => _ExtractSecondary(logs, modIndex),
|
||||
_ => []
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractFatal(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
foreach (var finding in _ExtractOutOfMemory(logs))
|
||||
yield return finding;
|
||||
|
||||
var memoryReservation = _ExtractJvmMemoryReservation(logs);
|
||||
if (memoryReservation is not null)
|
||||
yield return memoryReservation;
|
||||
|
||||
foreach (var finding in _ExtractAccessViolation(logs))
|
||||
yield return finding;
|
||||
|
||||
var signer = _ExtractSignerValidationFailure(logs);
|
||||
if (signer is not null)
|
||||
yield return signer;
|
||||
|
||||
var optifineMissingMods = _ExtractOptiFineMissingMods(logs);
|
||||
if (optifineMissingMods is not null)
|
||||
yield return optifineMissingMods;
|
||||
|
||||
foreach (var finding in _ExtractConfirmedModCrash(logs, modIndex))
|
||||
yield return finding;
|
||||
|
||||
foreach (var finding in _ExtractDuplicateMods(logs))
|
||||
yield return finding;
|
||||
|
||||
foreach (var finding in _ExtractMissingDependency(logs))
|
||||
yield return finding;
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractPrimary(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
foreach (var finding in _ExtractMixinFailure(logs, modIndex))
|
||||
yield return finding;
|
||||
|
||||
foreach (var finding in _ExtractForgeError(logs))
|
||||
yield return finding;
|
||||
|
||||
foreach (var finding in _ExtractFabricSolution(logs))
|
||||
yield return finding;
|
||||
|
||||
foreach (var finding in _ExtractFabricProvidedModCrash(logs, modIndex))
|
||||
yield return finding;
|
||||
|
||||
foreach (var finding in _ExtractSuspectedMods(logs, modIndex))
|
||||
yield return finding;
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractSecondary(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
var shortOutput = _ExtractShortOutput(logs);
|
||||
if (shortOutput is not null)
|
||||
yield return shortOutput;
|
||||
|
||||
var modLoader = _ExtractModLoaderError(logs);
|
||||
if (modLoader is not null)
|
||||
yield return modLoader;
|
||||
|
||||
var modInitialization = _ExtractModInitializationFailure(logs, modIndex);
|
||||
if (modInitialization is not null)
|
||||
yield return modInitialization;
|
||||
|
||||
foreach (var finding in _ExtractSpecificBlockAndEntity(logs))
|
||||
yield return finding;
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractOutOfMemory(CrashLogSet logs)
|
||||
{
|
||||
var evidence = _FindPatterns(
|
||||
logs,
|
||||
[
|
||||
(CrashLogKind.Game, "java.lang.OutOfMemoryError"),
|
||||
(CrashLogKind.Game, "an out of memory error"),
|
||||
(CrashLogKind.HsErr, "The system is out of physical RAM or swap space"),
|
||||
(CrashLogKind.HsErr, "Out of Memory Error"),
|
||||
(CrashLogKind.CrashReport, "java.lang.OutOfMemoryError")
|
||||
])
|
||||
.ToList();
|
||||
|
||||
if (evidence.Count == 0)
|
||||
yield break;
|
||||
|
||||
yield return new CrashFinding(
|
||||
CrashCause.OutOfMemory,
|
||||
CrashConfidence.High,
|
||||
evidence)
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashFinding? _ExtractJvmMemoryReservation(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
!gameLog.Contains("Could not reserve enough space", StringComparison.Ordinal))
|
||||
return null;
|
||||
|
||||
var cause = gameLog.Contains("for 1048576KB object heap", StringComparison.Ordinal)
|
||||
? CrashCause.X86JavaMemoryLimit
|
||||
: CrashCause.OutOfMemory;
|
||||
|
||||
return new CrashFinding(
|
||||
cause,
|
||||
CrashConfidence.High,
|
||||
[
|
||||
_Evidence(
|
||||
"Could not reserve enough space",
|
||||
CrashLogKind.Game,
|
||||
"jvm-message")
|
||||
])
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractAccessViolation(CrashLogSet logs)
|
||||
{
|
||||
var hsLog = logs.HsErr?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(hsLog) ||
|
||||
!hsLog.Contains("EXCEPTION_ACCESS_VIOLATION", StringComparison.Ordinal))
|
||||
yield break;
|
||||
|
||||
if (hsLog.Contains("# C [ig", StringComparison.Ordinal))
|
||||
yield return _DriverFinding(CrashCause.IntelDriverAccessViolation, "# C [ig");
|
||||
|
||||
if (hsLog.Contains("# C [atio", StringComparison.Ordinal))
|
||||
yield return _DriverFinding(CrashCause.AmdDriverAccessViolation, "# C [atio");
|
||||
|
||||
if (hsLog.Contains("# C [nvoglv", StringComparison.Ordinal))
|
||||
yield return _DriverFinding(CrashCause.NvidiaDriverAccessViolation, "# C [nvoglv");
|
||||
}
|
||||
|
||||
private static CrashFinding _DriverFinding(
|
||||
CrashCause cause,
|
||||
string pattern)
|
||||
{
|
||||
return new CrashFinding(
|
||||
cause,
|
||||
CrashConfidence.High,
|
||||
[_Evidence(pattern, CrashLogKind.HsErr, "driver-signature")])
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashFinding? _ExtractSignerValidationFailure(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
!gameLog.Contains(
|
||||
"signer information does not match signer information of other classes in the same package",
|
||||
StringComparison.Ordinal))
|
||||
return null;
|
||||
|
||||
var detail = (CrashRegex.First(gameLog, "(?<=class \")[^']+(?=\"'s signer information)") ?? "")
|
||||
.TrimEnd('\r', '\n');
|
||||
|
||||
return new CrashFinding(
|
||||
CrashCause.FileOrContentValidationFailed,
|
||||
CrashConfidence.High,
|
||||
[_Evidence(detail, CrashLogKind.Game, "class")])
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashFinding? _ExtractOptiFineMissingMods(CrashLogSet logs)
|
||||
{
|
||||
var crash = logs.CrashReport?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(crash) ||
|
||||
!crash.Contains("has mods that were not found", StringComparison.Ordinal) ||
|
||||
!CrashRegex.IsMatch(crash, @"The Mod File [^\n]+optifine\\OptiFine[^\n]+ has mods that were not found"))
|
||||
return null;
|
||||
|
||||
return new CrashFinding(
|
||||
CrashCause.OptiFineForgeIncompatible,
|
||||
CrashConfidence.High,
|
||||
[_Evidence("OptiFine", CrashLogKind.CrashReport, "mod")])
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractConfirmedModCrash(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (!string.IsNullOrEmpty(gameLog) &&
|
||||
gameLog.Contains("Caught exception from ", StringComparison.Ordinal))
|
||||
{
|
||||
var hint = CrashRegex.First(gameLog, @"(?<=Caught exception from )[^\n]+?")
|
||||
?.TrimEnd('\r', '\n', ' ');
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ConfirmedModCrash,
|
||||
modIndex.ResolveToDisplayNames([hint ?? ""]),
|
||||
CrashLogKind.Game,
|
||||
true);
|
||||
}
|
||||
|
||||
var crash = logs.CrashReport?.Text;
|
||||
if (string.IsNullOrEmpty(crash))
|
||||
yield break;
|
||||
|
||||
if (crash.Contains("-- MOD ", StringComparison.Ordinal))
|
||||
{
|
||||
var modSection = CrashText.Between(crash, "-- MOD ", "Failure message:");
|
||||
|
||||
if (modSection.Contains(".jar", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var fileName = (CrashRegex.First(modSection, "(?<=Mod File: ).+") ?? "")
|
||||
.TrimEnd('\r', '\n', ' ');
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ConfirmedModCrash,
|
||||
[fileName],
|
||||
CrashLogKind.CrashReport,
|
||||
true);
|
||||
}
|
||||
else
|
||||
{
|
||||
var message = (CrashRegex.First(crash, @"(?<=Failure message: )[\w\W]+?(?=\tMod)") ?? "")
|
||||
.Replace("\t", " ")
|
||||
.TrimEnd('\r', '\n', ' ');
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ModLoaderError,
|
||||
[message],
|
||||
CrashLogKind.CrashReport,
|
||||
true,
|
||||
"loader-message");
|
||||
}
|
||||
}
|
||||
|
||||
if (crash.Contains("Multiple entries with same key: ", StringComparison.Ordinal))
|
||||
{
|
||||
var hint = (CrashRegex.First(crash, "(?<=Multiple entries with same key: )[^=]+") ?? "")
|
||||
.TrimEnd('\r', '\n', ' ');
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ConfirmedModCrash,
|
||||
modIndex.ResolveToDisplayNames([hint]),
|
||||
CrashLogKind.CrashReport,
|
||||
true);
|
||||
}
|
||||
|
||||
if (crash.Contains("LoaderExceptionModCrash: Caught exception from ", StringComparison.Ordinal))
|
||||
{
|
||||
var hint = (CrashRegex.First(crash, @"(?<=LoaderExceptionModCrash: Caught exception from )[^\n]+") ?? "")
|
||||
.TrimEnd('\r', '\n', ' ');
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ConfirmedModCrash,
|
||||
modIndex.ResolveToDisplayNames([hint]),
|
||||
CrashLogKind.CrashReport,
|
||||
true);
|
||||
}
|
||||
|
||||
if (crash.Contains("Failed loading config file ", StringComparison.Ordinal))
|
||||
{
|
||||
var mod = (CrashRegex.First(crash, @"(?<=Failed loading config file .+ for modid )[^\n]+") ?? "")
|
||||
.TrimEnd('\r', '\n');
|
||||
|
||||
var config = (CrashRegex.First(crash, "(?<=Failed loading config file ).+(?= of type)") ?? "")
|
||||
.TrimEnd('\r', '\n');
|
||||
|
||||
var resolved = modIndex.ResolveToDisplayNames([mod]);
|
||||
|
||||
var evidence = new List<CrashEvidence>();
|
||||
evidence.AddRange(resolved.Select(item => _Evidence(item, CrashLogKind.CrashReport, "mod")));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(config))
|
||||
evidence.Add(_Evidence(config, CrashLogKind.CrashReport, "config"));
|
||||
|
||||
yield return new CrashFinding(
|
||||
CrashCause.ModConfigCrash,
|
||||
CrashConfidence.High,
|
||||
evidence)
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractDuplicateMods(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog))
|
||||
yield break;
|
||||
|
||||
if (gameLog.Contains("DuplicateModsFoundException", StringComparison.Ordinal))
|
||||
yield return _ModFinding(
|
||||
CrashCause.DuplicateMods,
|
||||
CrashRegex.All(
|
||||
gameLog,
|
||||
@"(?<=\n\t[\w]+ : [A-Z]:[^\n]+(/|\\))[^/\\\n]+?.jar",
|
||||
RegexOptions.IgnoreCase),
|
||||
CrashLogKind.Game,
|
||||
true);
|
||||
|
||||
if (gameLog.Contains("Found a duplicate mod", StringComparison.Ordinal))
|
||||
yield return _ModFinding(
|
||||
CrashCause.DuplicateMods,
|
||||
CrashRegex.All(
|
||||
CrashRegex.First(gameLog, @"Found a duplicate mod[^\n]+") ?? "",
|
||||
@"[^\\/]+.jar",
|
||||
RegexOptions.IgnoreCase),
|
||||
CrashLogKind.Game,
|
||||
true);
|
||||
|
||||
if (gameLog.Contains("Found duplicate mods", StringComparison.Ordinal))
|
||||
yield return _ModFinding(
|
||||
CrashCause.DuplicateMods,
|
||||
CrashRegex.All(gameLog, @"(?<=Mod ID: ')\w+?(?=' from mod files:)")
|
||||
.Distinct()
|
||||
.ToList(),
|
||||
CrashLogKind.Game,
|
||||
true);
|
||||
|
||||
if (gameLog.Contains("ModResolutionException: Duplicate", StringComparison.Ordinal))
|
||||
yield return _ModFinding(
|
||||
CrashCause.DuplicateMods,
|
||||
CrashRegex.All(
|
||||
CrashRegex.First(gameLog, @"ModResolutionException: Duplicate[^\n]+") ?? "",
|
||||
@"[^\\/]+.jar",
|
||||
RegexOptions.IgnoreCase),
|
||||
CrashLogKind.Game,
|
||||
true);
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractMissingDependency(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog))
|
||||
yield break;
|
||||
|
||||
if (gameLog.Contains("Incompatible mods found!", StringComparison.Ordinal))
|
||||
yield return _ModFinding(
|
||||
CrashCause.IncompatibleMods,
|
||||
[CrashRegex.First(gameLog, @"(?<=Incompatible mods found![\s\S]+: )[\s\S]+?(?=\tat )") ?? ""],
|
||||
CrashLogKind.Game,
|
||||
true,
|
||||
"loader-message");
|
||||
|
||||
if (gameLog.Contains("Missing or unsupported mandatory dependencies:", StringComparison.Ordinal))
|
||||
{
|
||||
var details = CrashRegex.All(
|
||||
gameLog,
|
||||
@"(?<=Missing or unsupported mandatory dependencies:)([\n\r]+\t(.*))+",
|
||||
RegexOptions.IgnoreCase)
|
||||
.Select(item => item.Trim('\r', '\n', '\t', ' '))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.MissingDependencyOrWrongMcVersion,
|
||||
details,
|
||||
CrashLogKind.Game,
|
||||
true,
|
||||
"loader-message");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractMixinFailure(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
return from source in new[] { CrashLogKind.Game, CrashLogKind.CrashReport }
|
||||
let text = logs.GetText(source)
|
||||
where !string.IsNullOrEmpty(text) && _LooksLikeMixinFailure(text)
|
||||
let modHints = _ExtractMixinModHints(text)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList()
|
||||
let resolved = modIndex.ResolveToDisplayNames(modHints)
|
||||
let hasResolved = modHints.Count > 0 && !modHints.SequenceEqual(resolved)
|
||||
let evidence = (resolved.Count != 0 ? resolved : modHints)
|
||||
.Select(item => _Evidence(item, source, hasResolved ? "mod" : "mod-hint"))
|
||||
select new CrashFinding(
|
||||
CrashCause.ModMixinFailed,
|
||||
hasResolved ? CrashConfidence.High : CrashConfidence.Medium,
|
||||
evidence)
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static bool _LooksLikeMixinFailure(string text)
|
||||
{
|
||||
return text.Contains("Mixin prepare failed ", StringComparison.Ordinal) ||
|
||||
text.Contains("Mixin apply failed ", StringComparison.Ordinal) ||
|
||||
text.Contains("MixinApplyError", StringComparison.Ordinal) ||
|
||||
text.Contains("MixinTransformerError", StringComparison.Ordinal) ||
|
||||
text.Contains("mixin.injection.throwables.", StringComparison.Ordinal) ||
|
||||
text.Contains(".json] FAILED during )", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> _ExtractMixinModHints(string text)
|
||||
{
|
||||
var patterns = new[]
|
||||
{
|
||||
new Regex(
|
||||
@"(?<=from mod )[^.\/ ]+(?=\] from)",
|
||||
RegexOptions.Compiled),
|
||||
|
||||
new Regex(
|
||||
@"(?<=for mod )[^.\/ ]+(?= failed)",
|
||||
RegexOptions.Compiled),
|
||||
|
||||
new Regex(
|
||||
@"(?<=^[^\t]+[ \[{(]{1})[^ \[{(]+\.[^ ]+(?=\.json)",
|
||||
RegexOptions.Compiled | RegexOptions.Multiline)
|
||||
};
|
||||
|
||||
foreach (var pattern in patterns)
|
||||
foreach (Match match in pattern.Matches(text))
|
||||
{
|
||||
var value = match.Value
|
||||
.Replace("mixins", "mixin")
|
||||
.Replace(".mixin", "")
|
||||
.Replace("mixin.", "")
|
||||
.Trim();
|
||||
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
yield return value;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractForgeError(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
!gameLog.Contains(
|
||||
"An exception was thrown, the game will display an error screen and halt.",
|
||||
StringComparison.Ordinal))
|
||||
yield break;
|
||||
|
||||
var message = CrashRegex.First(
|
||||
gameLog,
|
||||
@"(?<=the game will display an error screen and halt.[\n\r]+[^\n]+?Exception: )[\s\S]+?(?=\n\tat)")
|
||||
?.Trim('\r', '\n') ?? "";
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ForgeError,
|
||||
[message],
|
||||
CrashLogKind.Game,
|
||||
true,
|
||||
"loader-message");
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractFabricSolution(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog))
|
||||
yield break;
|
||||
|
||||
var solution = "";
|
||||
|
||||
if (gameLog.Contains("A potential solution has been determined:", StringComparison.Ordinal))
|
||||
solution = string.Join(
|
||||
"\n",
|
||||
CrashRegex.All(
|
||||
CrashRegex.First(gameLog, @"(?<=A potential solution has been determined:\n)(\s+ - [^\n]+\n)+") ??
|
||||
"",
|
||||
@"(?<=\s+)[^\n]+"));
|
||||
else if (gameLog.Contains(
|
||||
"A potential solution has been determined, this may resolve your problem:",
|
||||
StringComparison.Ordinal))
|
||||
solution = string.Join(
|
||||
"\n",
|
||||
CrashRegex.All(
|
||||
CrashRegex.First(
|
||||
gameLog,
|
||||
@"(?<=A potential solution has been determined, this may resolve your problem:\n)(\s+ - [^\n]+\n)+") ??
|
||||
"",
|
||||
@"(?<=\s+)[^\n]+"));
|
||||
else if (gameLog.Contains("确定了一种可能的解决方法,这样做可能会解决你的问题:", StringComparison.Ordinal))
|
||||
solution = string.Join(
|
||||
"\n",
|
||||
CrashRegex.All(
|
||||
CrashRegex.First(gameLog, @"(?<=确定了一种可能的解决方法,这样做可能会解决你的问题:\n)(\s+ - [^\n]+\n)+") ?? "",
|
||||
@"(?<=\s+)[^\n]+"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(solution))
|
||||
yield return _ModFinding(
|
||||
CrashCause.FabricSolutionProvided,
|
||||
[solution],
|
||||
CrashLogKind.Game,
|
||||
true,
|
||||
"loader-message");
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractFabricProvidedModCrash(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
!gameLog.Contains("due to errors, provided by ", StringComparison.Ordinal) ||
|
||||
_LooksLikeMixinFailure(gameLog))
|
||||
yield break;
|
||||
|
||||
var hint = (CrashRegex.First(gameLog, "(?<=due to errors, provided by ')[^']+") ?? "")
|
||||
.TrimEnd('\r', '\n', ' ');
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.ConfirmedModCrash,
|
||||
modIndex.ResolveToDisplayNames([hint]),
|
||||
CrashLogKind.Game,
|
||||
true);
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractSuspectedMods(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
var crash = logs.CrashReport?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(crash) ||
|
||||
!crash.Contains("Suspected Mod", StringComparison.Ordinal))
|
||||
yield break;
|
||||
|
||||
var suspectsRaw = CrashText.Between(crash, "Suspected Mod", "Stacktrace");
|
||||
|
||||
if (suspectsRaw.StartsWith("s: None", StringComparison.Ordinal))
|
||||
yield break;
|
||||
|
||||
var suspects = CrashRegex.All(suspectsRaw, @"(?<=\n\t[^(\t]+\()[^)\n]+");
|
||||
|
||||
if (suspects.Count != 0)
|
||||
yield return _ModFinding(
|
||||
CrashCause.SuspectedModCrash,
|
||||
modIndex.ResolveToDisplayNames(suspects),
|
||||
CrashLogKind.CrashReport,
|
||||
true);
|
||||
}
|
||||
|
||||
private static CrashFinding? _ExtractShortOutput(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
logs.HsErr is not null ||
|
||||
logs.CrashReport is not null ||
|
||||
gameLog.Contains("at net.", StringComparison.Ordinal) ||
|
||||
gameLog.Contains("INFO]", StringComparison.Ordinal) ||
|
||||
gameLog.Length >= 100)
|
||||
return null;
|
||||
|
||||
return _ModFinding(
|
||||
CrashCause.VeryShortOutput,
|
||||
[gameLog],
|
||||
CrashLogKind.Game,
|
||||
false,
|
||||
"raw-output");
|
||||
}
|
||||
|
||||
private static CrashFinding? _ExtractModLoaderError(CrashLogSet logs)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
!gameLog.Contains("Mod resolution failed", StringComparison.Ordinal))
|
||||
return null;
|
||||
|
||||
return new CrashFinding(
|
||||
CrashCause.ModLoaderError,
|
||||
CrashConfidence.High);
|
||||
}
|
||||
|
||||
private static CrashFinding? _ExtractModInitializationFailure(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
var gameLog = logs.Game?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(gameLog) ||
|
||||
!gameLog.Contains("Failed to create mod instance.", StringComparison.Ordinal))
|
||||
return null;
|
||||
|
||||
var hint = (CrashRegex.First(gameLog, "(?<=Failed to create mod instance. ModID: )[^,]+") ??
|
||||
CrashRegex.First(gameLog, @"(?<=Failed to create mod instance. ModId )[^\n]+(?= for )") ?? "")
|
||||
.TrimEnd('\r', '\n');
|
||||
|
||||
return _ModFinding(
|
||||
CrashCause.ModInitializationFailed,
|
||||
modIndex.ResolveToDisplayNames([hint]),
|
||||
CrashLogKind.Game,
|
||||
false);
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashFinding> _ExtractSpecificBlockAndEntity(CrashLogSet logs)
|
||||
{
|
||||
var crash = logs.CrashReport?.Text;
|
||||
|
||||
if (string.IsNullOrEmpty(crash))
|
||||
yield break;
|
||||
|
||||
if (crash.Contains("\tBlock location: World: ", StringComparison.Ordinal))
|
||||
{
|
||||
var value =
|
||||
(CrashRegex.First(crash, @"(?<=\tBlock: Block\{)[^\}]+") ?? "") +
|
||||
" " +
|
||||
(CrashRegex.First(crash, @"(?<=\tBlock location: World: )\([^\)]+\)") ?? "");
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.SpecificBlockCrash,
|
||||
[value],
|
||||
CrashLogKind.CrashReport,
|
||||
false,
|
||||
"block");
|
||||
}
|
||||
|
||||
if (crash.Contains("\tEntity's Exact location: ", StringComparison.Ordinal))
|
||||
{
|
||||
var value =
|
||||
(CrashRegex.First(crash, @"(?<=\tEntity Type: )[^\n]+(?= \()") ?? "") +
|
||||
" (" +
|
||||
(CrashRegex.First(crash, @"(?<=\tEntity's Exact location: )[^\n]+") ?? "")
|
||||
.TrimEnd('\r', '\n') +
|
||||
")";
|
||||
|
||||
yield return _ModFinding(
|
||||
CrashCause.SpecificEntityCrash,
|
||||
[value],
|
||||
CrashLogKind.CrashReport,
|
||||
false,
|
||||
"entity");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<CrashEvidence> _FindPatterns(
|
||||
CrashLogSet logs,
|
||||
IEnumerable<(CrashLogKind Source, string Pattern)> patterns)
|
||||
{
|
||||
foreach (var (source, pattern) in patterns)
|
||||
{
|
||||
var text = logs.GetText(source);
|
||||
|
||||
if (text?.Contains(pattern, StringComparison.Ordinal) == true)
|
||||
yield return _Evidence(pattern, source, "pattern", pattern);
|
||||
}
|
||||
}
|
||||
|
||||
private static CrashFinding _ModFinding(
|
||||
CrashCause cause,
|
||||
IEnumerable<string?> values,
|
||||
CrashLogKind source,
|
||||
bool shouldStop,
|
||||
string displayKind = "mod")
|
||||
{
|
||||
return new CrashFinding(
|
||||
cause,
|
||||
CrashConfidence.High,
|
||||
values
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Select(value => _Evidence(value!, source, displayKind)))
|
||||
{
|
||||
ShouldStop = shouldStop
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashEvidence _Evidence(
|
||||
string value,
|
||||
CrashLogKind source,
|
||||
string displayKind,
|
||||
string? pattern = null)
|
||||
{
|
||||
return new CrashEvidence
|
||||
{
|
||||
Value = value,
|
||||
Source = source,
|
||||
Pattern = pattern,
|
||||
DisplayKind = displayKind
|
||||
};
|
||||
}
|
||||
}
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashModInfo
|
||||
{
|
||||
public string? ModId { get; init; }
|
||||
|
||||
public string? DisplayName { get; init; }
|
||||
|
||||
public string? FileName { get; init; }
|
||||
|
||||
public string? Version { get; init; }
|
||||
|
||||
public string Source { get; init; } = "";
|
||||
|
||||
public string DisplayNameOrFileName =>
|
||||
!string.IsNullOrWhiteSpace(DisplayName) ? DisplayName :
|
||||
!string.IsNullOrWhiteSpace(FileName) ? FileName :
|
||||
ModId ?? "";
|
||||
}
|
||||
|
||||
internal sealed class CrashModIndex
|
||||
{
|
||||
private readonly List<CrashModInfo> _mods = [];
|
||||
|
||||
public static CrashModIndex Create(CrashLogSet logs, McInstance? instance)
|
||||
{
|
||||
var index = new CrashModIndex();
|
||||
index._ReadCrashReport(logs.CrashReport?.Text);
|
||||
index._ReadForgeDebugLog(logs.Debug?.Text);
|
||||
index._ReadLoaderMessages(logs.Game?.Text);
|
||||
if (instance is not null)
|
||||
index._ReadInstanceModFiles(instance);
|
||||
index._NormalizeAndDeduplicate();
|
||||
return index;
|
||||
}
|
||||
|
||||
public IEnumerable<CrashModInfo> ResolveMany(IEnumerable<string> hints)
|
||||
{
|
||||
var found = new List<CrashModInfo>();
|
||||
foreach (var hint in hints)
|
||||
foreach (var mod in Resolve(hint))
|
||||
if (!found.Any(item => _SameDisplay(item, mod)))
|
||||
found.Add(mod);
|
||||
return found;
|
||||
}
|
||||
|
||||
public IEnumerable<CrashModInfo> Resolve(string? hint)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hint))
|
||||
return [];
|
||||
|
||||
var normalizedHint = _Normalize(hint);
|
||||
if (string.IsNullOrWhiteSpace(normalizedHint))
|
||||
return [];
|
||||
|
||||
var exact = _mods
|
||||
.Where(mod =>
|
||||
string.Equals(_Normalize(mod.ModId), normalizedHint, StringComparison.Ordinal) ||
|
||||
string.Equals(_Normalize(mod.FileName), normalizedHint, StringComparison.Ordinal) ||
|
||||
string.Equals(_Normalize(mod.DisplayName), normalizedHint, StringComparison.Ordinal))
|
||||
.ToList();
|
||||
if (exact.Count != 0)
|
||||
return exact;
|
||||
|
||||
return _mods
|
||||
.Where(mod =>
|
||||
_Normalize(mod.ModId).Contains(normalizedHint, StringComparison.Ordinal) ||
|
||||
_Normalize(mod.FileName).Contains(normalizedHint, StringComparison.Ordinal) ||
|
||||
_Normalize(mod.DisplayName).Contains(normalizedHint, StringComparison.Ordinal))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public List<string> ResolveToDisplayNames(IEnumerable<string> hints)
|
||||
{
|
||||
var hintList = hints
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Select(item => item.Trim())
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var mods = ResolveMany(hintList)
|
||||
.Select(mod => mod.DisplayNameOrFileName)
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
return mods.Count != 0 ? mods : hintList;
|
||||
}
|
||||
|
||||
private void _ReadCrashReport(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) ||
|
||||
!text.Contains("A detailed walkthrough of the error", StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
var details = text.Replace("A detailed walkthrough of the error", "¨");
|
||||
var isFabric = details.Contains("Fabric Mods", StringComparison.Ordinal);
|
||||
if (isFabric)
|
||||
{
|
||||
details = details.Replace("Fabric Mods", "¨");
|
||||
LogWrapper.Info("Crash", "崩溃报告中检测到 Fabric Mod 信息格式");
|
||||
}
|
||||
|
||||
var isQuilt = details.Contains("quilt-loader", StringComparison.Ordinal);
|
||||
if (isQuilt)
|
||||
{
|
||||
details = details.Replace("Mod Table Version", "¨");
|
||||
LogWrapper.Info("Crash", "崩溃报告中检测到 Quilt Mod 信息格式");
|
||||
}
|
||||
|
||||
details = CrashText.AfterLast(details, "¨");
|
||||
foreach (var rawLine in details.Split('\n'))
|
||||
{
|
||||
var line = rawLine.Trim('\r', '\n');
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
if (isFabric && line.StartsWith(
|
||||
"\t\t",
|
||||
StringComparison.Ordinal) && !CrashRegex.IsMatch(line, @"\t\tfabric[\w-]*: Fabric"))
|
||||
{
|
||||
_Add(new CrashModInfo
|
||||
{
|
||||
ModId = CrashRegex.First(line, @"(?<=\t\t)[^:]+"),
|
||||
DisplayName = CrashRegex.First(line, @"(?<=: )[^\n]+(?= [^\n]+)"),
|
||||
Version = CrashRegex.First(line, @"(?<= )[\w\.-]+$"),
|
||||
Source = "crash-report-fabric"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.Contains(".jar", StringComparison.OrdinalIgnoreCase) &&
|
||||
line.Length - line.Replace(".jar", "", StringComparison.OrdinalIgnoreCase).Length == 4)
|
||||
_Add(new CrashModInfo
|
||||
{
|
||||
FileName = CrashRegex.First(line,
|
||||
@"(?<=\()[^\t]+.jar(?=\))|(?<=(\t\t)|(\| ))[^\t\|]+.jar",
|
||||
RegexOptions.IgnoreCase),
|
||||
DisplayName = CrashRegex.First(line, @"(?<=\t)[^\t\|]+(?=\s+\()"),
|
||||
ModId = CrashRegex.First(line, @"(?<=\| )[\w\.-]+(?= \|)"),
|
||||
Source = "crash-report-forge"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void _ReadForgeDebugLog(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
foreach (var line in CrashRegex.All(text, "(?<=valid mod file ).*", RegexOptions.Multiline))
|
||||
_Add(new CrashModInfo
|
||||
{
|
||||
FileName = CrashRegex.First(line, ".*(?= with)"),
|
||||
ModId = CrashRegex.First(line, @"(?<=with \{)[^\}]+"),
|
||||
Version = CrashRegex.First(line, @"(?<=versions \{)[^\}]+"),
|
||||
Source = "debug-log"
|
||||
});
|
||||
}
|
||||
|
||||
private void _ReadLoaderMessages(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
foreach (var line in CrashRegex.All(text, @"(?<=ModID: )[^,\n]+|(?<=ModId )[^\n]+(?= for )",
|
||||
RegexOptions.IgnoreCase))
|
||||
_Add(new CrashModInfo
|
||||
{
|
||||
ModId = line.Trim(),
|
||||
Source = "loader-message"
|
||||
});
|
||||
}
|
||||
|
||||
private void _ReadInstanceModFiles(McInstance instance)
|
||||
{
|
||||
foreach (var directory in _GetCandidateModDirectories(instance))
|
||||
try
|
||||
{
|
||||
var info = new DirectoryInfo(directory);
|
||||
if (!info.Exists)
|
||||
continue;
|
||||
|
||||
foreach (var file in info.EnumerateFiles("*.jar"))
|
||||
_Add(new CrashModInfo
|
||||
{
|
||||
FileName = file.Name,
|
||||
DisplayName = Path.GetFileNameWithoutExtension(file.Name),
|
||||
Source = "instance-mods"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "读取实例 Mod 文件列表失败(" + directory + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> _GetCandidateModDirectories(McInstance instance)
|
||||
{
|
||||
yield return Path.Combine(instance.PathInstance, "mods");
|
||||
string? pathIndie = null;
|
||||
try
|
||||
{
|
||||
pathIndie = instance.PathIndie;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "Crash", "读取实例隔离路径失败");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(pathIndie))
|
||||
yield return Path.Combine(pathIndie, "mods");
|
||||
}
|
||||
|
||||
private void _Add(CrashModInfo mod)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mod.ModId) &&
|
||||
string.IsNullOrWhiteSpace(mod.DisplayName) &&
|
||||
string.IsNullOrWhiteSpace(mod.FileName))
|
||||
return;
|
||||
_mods.Add(mod);
|
||||
}
|
||||
|
||||
private void _NormalizeAndDeduplicate()
|
||||
{
|
||||
var distinct = new List<CrashModInfo>();
|
||||
foreach (var mod in _mods
|
||||
.Where(mod => !distinct.Any(item => _SameDisplay(item, mod))))
|
||||
distinct.Add(mod);
|
||||
|
||||
_mods.Clear();
|
||||
_mods.AddRange(distinct);
|
||||
LogWrapper.Info("Crash", "构建 Mod 索引,找到 " + _mods.Count + " 个候选 Mod");
|
||||
}
|
||||
|
||||
private static bool _SameDisplay(CrashModInfo a, CrashModInfo b)
|
||||
{
|
||||
return string.Equals(_Normalize(a.ModId), _Normalize(b.ModId), StringComparison.Ordinal) &&
|
||||
string.Equals(_Normalize(a.FileName), _Normalize(b.FileName), StringComparison.Ordinal) &&
|
||||
string.Equals(_Normalize(a.DisplayName), _Normalize(b.DisplayName), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string _Normalize(string? value)
|
||||
{
|
||||
return (value ?? string.Empty)
|
||||
.ToLowerInvariant()
|
||||
.Replace("_", "")
|
||||
.Replace("-", "")
|
||||
.Replace(" ", "")
|
||||
.Replace(".jar", "");
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
namespace PCL;
|
||||
|
||||
internal sealed class CrashRule
|
||||
{
|
||||
public required CrashCause Cause { get; init; }
|
||||
|
||||
public required DetectionPhase Phase { get; init; }
|
||||
|
||||
public CrashConfidence Confidence { get; init; } = CrashConfidence.High;
|
||||
|
||||
public bool StopOnMatch { get; init; }
|
||||
|
||||
public required Func<CrashRuleInput, CrashFinding?> Evaluate { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class CrashRuleInput
|
||||
{
|
||||
public required CrashLogSet Logs { get; init; }
|
||||
|
||||
public required CrashModIndex ModIndex { get; init; }
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
namespace PCL;
|
||||
|
||||
internal static class CrashRuleCatalog
|
||||
{
|
||||
public static IReadOnlyList<CrashRule> Rules { get; } =
|
||||
[
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.CrashReport,
|
||||
"Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass",
|
||||
CrashCause.JavaTooNew),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"Found multiple arguments for option fml.forgeVersion, but you asked for only one",
|
||||
CrashCause.MultipleForgeInInstanceJson),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"The driver does not appear to support OpenGL",
|
||||
CrashCause.UnsupportedOpenGl),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"java.lang.ClassCastException: java.base/jdk",
|
||||
CrashCause.UsingJdk),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"java.lang.ClassCastException: class jdk.",
|
||||
CrashCause.UsingJdk),
|
||||
_ContainsAny(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"TRANSFORMER/net.optifine/net.optifine.reflect.Reflector.<clinit>(Reflector.java",
|
||||
"java.lang.NoSuchMethodError: 'void net.minecraft.client.renderer.texture.SpriteContents.<init>",
|
||||
"java.lang.NoSuchMethodError: 'java.lang.String com.mojang.blaze3d.systems.RenderSystem.getBackendDescription",
|
||||
"java.lang.NoSuchMethodError: 'void net.minecraft.client.renderer.block.model.BakedQuad.<init>",
|
||||
"java.lang.NoSuchMethodError: 'void net.minecraftforge.client.gui.overlay.ForgeGui.renderSelectedItemName",
|
||||
"java.lang.NoSuchMethodError: 'void net.minecraft.server.level.DistanceManager",
|
||||
"java.lang.NoSuchMethodError: 'net.minecraft.network.chat.FormattedText net.minecraft.client.gui.Font.ellipsize"
|
||||
],
|
||||
CrashCause.OptiFineForgeIncompatible),
|
||||
_ContainsAny(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"Open J9 is not supported",
|
||||
"OpenJ9 is incompatible",
|
||||
".J9VMInternals."
|
||||
],
|
||||
CrashCause.UsingOpenJ9),
|
||||
_ContainsAny(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"java.lang.NoSuchFieldException: ucp",
|
||||
"because module java.base does not export",
|
||||
"java.lang.ClassNotFoundException: jdk.nashorn.api.scripting.NashornScriptEngineFactory",
|
||||
"java.lang.ClassNotFoundException: java.lang.invoke.LambdaMetafactory"
|
||||
],
|
||||
CrashCause.JavaTooNew),
|
||||
_ContainsAny(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"The directories below appear to be extracted jar files. Fix this before you continue.",
|
||||
"Extracted mod jars found, loading will NOT continue"
|
||||
],
|
||||
CrashCause.ExtractedModFile),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"java.lang.ClassNotFoundException: org.spongepowered.asm.launch.MixinTweaker",
|
||||
CrashCause.MissingMixinBootstrap),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"Couldn't set pixel format",
|
||||
CrashCause.PixelFormatNotSupported),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"java.lang.RuntimeException: Shaders Mod detected. Please remove it, OptiFine has built-in support for shaders.",
|
||||
CrashCause.ShadersModWithOptiFine),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier",
|
||||
CrashCause.OldForgeNewJavaIncompatible),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"1282: Invalid operation",
|
||||
CrashCause.OpenGl1282),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"Maybe try a lower resolution resourcepack?",
|
||||
CrashCause.ResourcePackTooLarge),
|
||||
_ContainsAll(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"java.lang.NoSuchMethodError: net.minecraft.world.server.ChunkManager$ProxyTicketManager.shouldForceTicks(J)Z",
|
||||
"OptiFine"
|
||||
],
|
||||
CrashCause.OptiFineWorldLoadCrash),
|
||||
_ContainsAny(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
["Unsupported class file major version", "Unsupported major.minor version"], CrashCause.JavaIncompatible),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"com.electronwill.nightconfig.core.io.ParsingException: Not enough data available",
|
||||
CrashCause.NightConfigBug),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"Cannot find launch target fmlclient, unable to launch",
|
||||
CrashCause.IncompleteForgeInstallation),
|
||||
_ContainsAll(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"Invalid paths argument, contained no existing paths",
|
||||
@"libraries\net\minecraftforge\fmlcore"
|
||||
],
|
||||
CrashCause.IncompleteForgeInstallation),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"Invalid module name: '' is not a Java identifier",
|
||||
CrashCause.InvalidModFileName),
|
||||
_ContainsAny(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
[
|
||||
"has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to",
|
||||
"java.lang.RuntimeException: java.lang.NoSuchMethodException: no such method: sun.misc.Unsafe.defineAnonymousClass(Class,byte[],Object[])Class/invokeVirtual",
|
||||
"java.lang.IllegalArgumentException: The requested compatibility level JAVA_11 could not be set. Level is not supported by the active JRE or ASM version"
|
||||
],
|
||||
CrashCause.ModRequiresJava11),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.Game,
|
||||
"Invalid maximum heap size",
|
||||
CrashCause.X86JavaMemoryLimit),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.CrashReport,
|
||||
"maximum id range exceeded",
|
||||
CrashCause.TooManyModsIdLimit),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.CrashReport,
|
||||
"Pixel format not accelerated",
|
||||
CrashCause.PixelFormatNotSupported),
|
||||
_Contains(
|
||||
DetectionPhase.Fatal,
|
||||
CrashLogKind.CrashReport,
|
||||
"Manually triggered debug crash",
|
||||
CrashCause.ManualDebugCrash)
|
||||
];
|
||||
|
||||
private static CrashRule _Contains(
|
||||
DetectionPhase phase,
|
||||
CrashLogKind source,
|
||||
string pattern,
|
||||
CrashCause cause,
|
||||
bool stopOnMatch = true)
|
||||
{
|
||||
return new CrashRule
|
||||
{
|
||||
Phase = phase,
|
||||
Cause = cause,
|
||||
StopOnMatch = stopOnMatch,
|
||||
Evaluate = input =>
|
||||
{
|
||||
var text = input.Logs.GetText(source);
|
||||
return text?.Contains(pattern, StringComparison.Ordinal) != true
|
||||
? null
|
||||
: _CreateFinding(cause, CrashConfidence.High, source, pattern, stopOnMatch);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashRule _ContainsAny(
|
||||
DetectionPhase phase,
|
||||
CrashLogKind source,
|
||||
IReadOnlyList<string> patterns,
|
||||
CrashCause cause,
|
||||
bool stopOnMatch = true)
|
||||
{
|
||||
return new CrashRule
|
||||
{
|
||||
Phase = phase,
|
||||
Cause = cause,
|
||||
StopOnMatch = stopOnMatch,
|
||||
Evaluate = input =>
|
||||
{
|
||||
var text = input.Logs.GetText(source);
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return null;
|
||||
|
||||
var pattern = patterns.FirstOrDefault(item => text.Contains(item, StringComparison.Ordinal));
|
||||
return pattern is null
|
||||
? null
|
||||
: _CreateFinding(cause, CrashConfidence.High, source, pattern, stopOnMatch);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashRule _ContainsAll(
|
||||
DetectionPhase phase,
|
||||
CrashLogKind source,
|
||||
IReadOnlyList<string> patterns,
|
||||
CrashCause cause,
|
||||
bool stopOnMatch = true)
|
||||
{
|
||||
return new CrashRule
|
||||
{
|
||||
Phase = phase,
|
||||
Cause = cause,
|
||||
StopOnMatch = stopOnMatch,
|
||||
Evaluate = input =>
|
||||
{
|
||||
var text = input.Logs.GetText(source);
|
||||
if (string.IsNullOrEmpty(text) ||
|
||||
patterns.Any(pattern => !text.Contains(pattern, StringComparison.Ordinal)))
|
||||
return null;
|
||||
|
||||
return _CreateFinding(
|
||||
cause,
|
||||
CrashConfidence.High,
|
||||
source,
|
||||
string.Join(" | ", patterns),
|
||||
stopOnMatch);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static CrashFinding _CreateFinding(
|
||||
CrashCause cause,
|
||||
CrashConfidence confidence,
|
||||
CrashLogKind source,
|
||||
string pattern,
|
||||
bool stopOnMatch)
|
||||
{
|
||||
return new CrashFinding(
|
||||
cause,
|
||||
confidence,
|
||||
[
|
||||
new CrashEvidence
|
||||
{
|
||||
Source = source,
|
||||
Pattern = pattern,
|
||||
Value = pattern,
|
||||
DisplayKind = "pattern"
|
||||
}
|
||||
])
|
||||
{
|
||||
ShouldStop = stopOnMatch
|
||||
};
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
internal sealed partial class CrashStackAnalyzer
|
||||
{
|
||||
private static readonly HashSet<string> IgnoredPrefixes = new(StringComparer.Ordinal)
|
||||
{
|
||||
"java", "sun", "javax", "jdk", "com.sun",
|
||||
"com.mojang", "net.minecraft", "MojangTricksIntelDriversForPerformance_javaw",
|
||||
"net.minecraftforge", "cpw.mods", "net.fabricmc", "org.quiltmc", "org.spongepowered", "com.mumfrey",
|
||||
"org.lwjgl", "paulscode.sound", "com.google", "org.apache", "com.electronwill.nightconfig", "it.unimi.dsi",
|
||||
"oolloo"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> IgnoredWords = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"com", "org", "net", "top", "dev", "gitlab", "github",
|
||||
"sun", "lib", "nio", "api", "asm", "reflect", "internal", "microsoft",
|
||||
"mcp", "fml", "forge", "fabricmc", "neoforge", "neoforged", "minecraftforge", "minecraft", "mojang",
|
||||
"mod", "mods", "modapi", "jar", "loader", "launch", "preinit", "preload", "init", "setup", "plugin",
|
||||
"mixin", "mixins", "injection", "transformer", "transformers", "spongepowered",
|
||||
"game", "world", "server", "client", "common", "entity", "block", "item", "tile", "blockentity",
|
||||
"gui", "model", "render", "shader", "optifine",
|
||||
"config", "data", "file", "read", "recipe", "content", "general",
|
||||
"event", "events", "handler", "listeners", "assist", "override",
|
||||
"netty", "packet", "channel",
|
||||
"task", "pool", "scheduler", "systems", "system", "modules", "service", "platform",
|
||||
"core", "main", "base", "util", "impl", "done", "map", "load", "machine",
|
||||
"dsi", "unimi", "fastutil", "lwjgl", "oshi", "electronwill",
|
||||
"compat", "universal", "multipart"
|
||||
};
|
||||
|
||||
public CrashFinding? Analyze(
|
||||
CrashLogSet logs,
|
||||
CrashModIndex modIndex)
|
||||
{
|
||||
if (!_ShouldAnalyzeStack(logs.All))
|
||||
{
|
||||
LogWrapper.Info("Crash", "可能并未安装 Mod,不进行堆栈分析");
|
||||
return null;
|
||||
}
|
||||
|
||||
var stackText = _CollectStackText(logs);
|
||||
var packages = _ExtractPackages(stackText).ToList();
|
||||
var keywords = _ExtractKeywords(packages).ToList();
|
||||
|
||||
if (keywords.Count is 0 or > 10)
|
||||
{
|
||||
if (keywords.Count > 10)
|
||||
LogWrapper.Info("Crash", "关键词过多,考虑匹配出错,不纳入考虑");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var mods = modIndex.ResolveMany(keywords).ToList();
|
||||
|
||||
if (mods.Count != 0)
|
||||
return new CrashFinding(
|
||||
CrashCause.StackModNameFound,
|
||||
CrashConfidence.Medium,
|
||||
mods.Select(mod => new CrashEvidence
|
||||
{
|
||||
Source = CrashLogKind.All,
|
||||
Value = mod.DisplayNameOrFileName,
|
||||
DisplayKind = "mod"
|
||||
}))
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
|
||||
return new CrashFinding(
|
||||
CrashCause.StackKeywordFound,
|
||||
CrashConfidence.Low,
|
||||
keywords.Select(keyword => new CrashEvidence
|
||||
{
|
||||
Source = CrashLogKind.All,
|
||||
Value = keyword,
|
||||
DisplayKind = "keyword"
|
||||
}))
|
||||
{
|
||||
ShouldStop = true
|
||||
};
|
||||
}
|
||||
|
||||
private static bool _ShouldAnalyzeStack(string allLogs)
|
||||
{
|
||||
return allLogs.Contains("forge", StringComparison.OrdinalIgnoreCase) ||
|
||||
allLogs.Contains("fabric", StringComparison.OrdinalIgnoreCase) ||
|
||||
allLogs.Contains("quilt", StringComparison.OrdinalIgnoreCase) ||
|
||||
allLogs.Contains("liteloader", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string _CollectStackText(CrashLogSet logs)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
if (logs.CrashReport?.Text is not null)
|
||||
{
|
||||
LogWrapper.Info("Crash", "开始进行崩溃日志堆栈分析");
|
||||
result.Add(CrashText.BeforeFirst(logs.CrashReport.Text, "System Details"));
|
||||
}
|
||||
|
||||
if (logs.Game?.Text is not null)
|
||||
{
|
||||
var fatals = CrashRegex.All(logs.Game.Text, @"/FATAL] .+?(?=[\n]+\[)");
|
||||
|
||||
if (logs.Game.Text.Contains("Unreported exception thrown!", StringComparison.Ordinal))
|
||||
fatals.Add(
|
||||
CrashText.Between(logs.Game.Text,
|
||||
"Unreported exception thrown!",
|
||||
"at oolloo.jlw.Wrapper"));
|
||||
|
||||
LogWrapper.Info("Crash", "开始进行 Minecraft 日志堆栈分析,发现 " + fatals.Count + " 个报错项");
|
||||
result.AddRange(fatals);
|
||||
}
|
||||
|
||||
if (logs.HsErr?.Text is not null)
|
||||
{
|
||||
LogWrapper.Info("Crash", "开始进行虚拟机堆栈分析");
|
||||
result.Add(CrashText.Between(logs.HsErr.Text, "T H R E A D", "Registers:"));
|
||||
}
|
||||
|
||||
return "\n" + string.Join("\n", result) + "\n";
|
||||
}
|
||||
|
||||
private static IEnumerable<string> _ExtractPackages(string stackText)
|
||||
{
|
||||
var packages = new List<string>();
|
||||
|
||||
packages.AddRange(
|
||||
PackageRegex()
|
||||
.Matches(stackText)
|
||||
.Select(match => match.Value));
|
||||
|
||||
packages.AddRange(
|
||||
MixinStackRegex()
|
||||
.Matches(stackText)
|
||||
.Select(match => match.Value.Replace("$", ".")));
|
||||
|
||||
var possibleStacks = packages
|
||||
.Select(item => item.Trim())
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Where(item => !IgnoredPrefixes.Any(prefix => item.StartsWith(prefix, StringComparison.Ordinal)))
|
||||
.ToList();
|
||||
|
||||
LogWrapper.Info("Crash", "找到 " + possibleStacks.Count + " 条可能的堆栈信息");
|
||||
|
||||
foreach (var stack in possibleStacks)
|
||||
LogWrapper.Info("Crash", " - " + stack);
|
||||
|
||||
return possibleStacks;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> _ExtractKeywords(IEnumerable<string> packages)
|
||||
{
|
||||
var words = new List<string>();
|
||||
|
||||
foreach (var package in packages)
|
||||
{
|
||||
var split = package.Split('.');
|
||||
|
||||
for (var i = 0; i <= Math.Min(3, split.Length - 1); i++)
|
||||
{
|
||||
var word = split[i].Trim();
|
||||
|
||||
if (word.Length <= 2 ||
|
||||
word.StartsWith("func_", StringComparison.Ordinal) ||
|
||||
IgnoredWords.Contains(word))
|
||||
continue;
|
||||
|
||||
words.Add(word);
|
||||
}
|
||||
}
|
||||
|
||||
var result = words
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
LogWrapper.Info("Crash", "从堆栈信息中找到 " + result.Count + " 个可能的 Mod ID 关键词");
|
||||
|
||||
if (result.Count != 0)
|
||||
LogWrapper.Info("Crash", " - " + string.Join(", ", result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[GeneratedRegex(
|
||||
@"(?<=\n[^{]+)[a-zA-Z_]+\w+\.[a-zA-Z_]+[\w\.]+(?=\.[\w\.$]+\.)",
|
||||
RegexOptions.Compiled)]
|
||||
private static partial Regex PackageRegex();
|
||||
|
||||
[GeneratedRegex(
|
||||
@"(?<=at [^(]+?\.\w+\$\w+\$)[\w\$]+?(?=\$\w+\()",
|
||||
RegexOptions.Compiled)]
|
||||
private static partial Regex MixinStackRegex();
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
namespace PCL;
|
||||
|
||||
internal enum DetectionPhase
|
||||
{
|
||||
Fatal,
|
||||
Primary,
|
||||
Secondary
|
||||
}
|
||||
Reference in New Issue
Block a user