Skip to content

Commit dfafef5

Browse files
MankarseLicho1
authored andcommitted
Improve parsing of infolog_full.txt
The new parser identifies regions of the infolog that correspond to individual games. Each game is further parsed to find: GameID FirstDesyncIndex GameState file names If the infolog contains GameState file names, include them in a table in the Issue description. If the infolog contains multiple games with desyncs, try to include all of the desync messages in the truncated infolog. (cherry picked from commit b98e593)
1 parent 4c32a82 commit dfafef5

2 files changed

Lines changed: 305 additions & 39 deletions

File tree

ChobbyLauncher/CrashReportHelper.cs

Lines changed: 276 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4-
using System.IO;
54
using System.Linq;
6-
using System.Runtime.InteropServices;
75
using System.Text;
86
using System.Text.RegularExpressions;
97
using System.Threading.Tasks;
@@ -12,7 +10,6 @@
1210
using Octokit;
1311
using PlasmaShared;
1412
using ZkData;
15-
using FileMode = System.IO.FileMode;
1613

1714
namespace ChobbyLauncher
1815
{
@@ -25,71 +22,306 @@ public enum CrashType {
2522

2623
public static class CrashReportHelper
2724
{
25+
private const string CrashReportsRepoOwner = "ZeroK-RTS";
26+
private const string CrashReportsRepoName = "CrashReports";
27+
2828
private const int MaxInfologSize = 62000;
2929
private const string InfoLogLineStartPattern = @"(^\[t=\d+:\d+:\d+\.\d+\]\[f=-?\d+\] )";
3030
private const string InfoLogLineEndPattern = @"(\r?\n|\Z)";
31-
public static Issue ReportCrash(string infolog, CrashType type, string engine, string bugReportTitle, string bugReportDescription)
31+
private sealed class GameFromLog
32+
{
33+
public int StartIdxInLog { get; set; }
34+
public string GameID { get; set; }
35+
public bool HasDesync { get => FirstDesyncIdxInLog.HasValue; }
36+
public int? FirstDesyncIdxInLog { get; set; }
37+
public List<string> GameStateFileNames { get; set; }
38+
39+
//Perhaps in future versions, these could be added?
40+
//PlayerName
41+
//DemoFileName
42+
//MapName
43+
//ModName
44+
}
45+
46+
private sealed class GameFromLogCollection
47+
{
48+
public IReadOnlyList<GameFromLog> Games { get; private set; }
49+
public GameFromLogCollection(IEnumerable<int> startIndexes)
50+
{
51+
Games = startIndexes.Select(idx => new GameFromLog { StartIdxInLog = idx }).ToArray();
52+
}
53+
54+
private readonly struct GameStartList : IReadOnlyList<int>
55+
{
56+
private readonly GameFromLogCollection _this;
57+
public GameStartList(GameFromLogCollection v) => _this = v;
58+
59+
int IReadOnlyList<int>.this[int index] => _this.Games[index].StartIdxInLog;
60+
61+
int IReadOnlyCollection<int>.Count { get => _this.Games.Count; }
62+
63+
IEnumerator<int> IEnumerable<int>.GetEnumerator() => _this.Games.Select(g => g.StartIdxInLog).GetEnumerator();
64+
65+
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => ((IEnumerable<int>)this).GetEnumerator();
66+
}
67+
public IReadOnlyList<int> AsGameStartReadOnlyList() => new GameStartList(this);
68+
69+
private GameFromLog GetGameForIndex(int idx)
70+
{
71+
//Equivalent to:
72+
//return Games.LastOrDefault(g => g.StartIdxInLog < idx);
73+
//but takes advantage of the fact that Games is sorted to have log rather than linear runtime.
74+
var lb = AsGameStartReadOnlyList().LowerBoundIndex(idx);
75+
return lb == 0 ? null : Games[lb - 1];
76+
}
77+
public void AddGameStateFileNames(IEnumerable<(int, string)> gameStateFileNames)
78+
{
79+
foreach (var file in gameStateFileNames)
80+
{
81+
var game = GetGameForIndex(file.Item1);
82+
if (game == null)
83+
{
84+
Trace.TraceWarning("[CrashReportHelper] Unable to match GameState file to Game");
85+
continue;
86+
}
87+
if (game.GameStateFileNames == null)
88+
{
89+
game.GameStateFileNames = new List<string>();
90+
}
91+
92+
game.GameStateFileNames.Add(file.Item2);
93+
}
94+
}
95+
public void AddDesyncs(IEnumerable<int> desyncs)
96+
{
97+
foreach (var desync in desyncs)
98+
{
99+
var game = GetGameForIndex(desync);
100+
if (game == null)
101+
{
102+
Trace.TraceWarning("[CrashReportHelper] Unable to match Desync to Game");
103+
continue;
104+
}
105+
if (!game.HasDesync)
106+
{
107+
game.FirstDesyncIdxInLog = desync;
108+
}
109+
}
110+
}
111+
public void AddGameIDs(IEnumerable<(int, string)> gameIDs)
112+
{
113+
foreach (var gameID in gameIDs)
114+
{
115+
var game = GetGameForIndex(gameID.Item1);
116+
if (game == null)
117+
{
118+
Trace.TraceWarning("[CrashReportHelper] Unable to match GameID to Game");
119+
continue;
120+
}
121+
if (game.GameID != null)
122+
{
123+
Trace.TraceWarning("[CrashReportHelper] Found multiple GameIDs for Game");
124+
continue;
125+
}
126+
127+
game.GameID = gameID.Item2;
128+
}
129+
}
130+
}
131+
private static IReadOnlyList<TextTruncator.RegionOfInterest> MakeRegionsOfInterest(int stringLength, IEnumerable<int> pointsOfInterest, IReadOnlyList<int> regionBoundaries)
132+
{
133+
//Pre:
134+
// pointsOfInterest is sorted
135+
// regionBoundaries is sorted
136+
137+
//Automatically adds a region at the start and end of the string, that can expand to cover the whole string.
138+
//For every element of pointsOfInterest, adds a region with StartLimit/EndLimit corresponding to the relevant regionBoundaries (or the start/end of the string)
139+
140+
var result = new List<TextTruncator.RegionOfInterest>();
141+
142+
result.Add(new TextTruncator.RegionOfInterest { PointOfInterest = 0, StartLimit = 0, EndLimit = stringLength });
143+
result.AddRange(pointsOfInterest.Select(poi => {
144+
var regionEndIndex = Utils.LowerBoundIndex(regionBoundaries, poi);
145+
return new TextTruncator.RegionOfInterest
146+
{
147+
PointOfInterest = poi,
148+
StartLimit = regionEndIndex == 0 ? 0 : regionBoundaries[regionEndIndex - 1],
149+
EndLimit = regionEndIndex == regionBoundaries.Count ? stringLength : regionBoundaries[regionEndIndex],
150+
};
151+
}));
152+
result.Add(new TextTruncator.RegionOfInterest { PointOfInterest = stringLength, StartLimit = 0, EndLimit = stringLength });
153+
154+
return result;
155+
}
156+
157+
private static string EscapeMarkdownTableCell(string str) => str.Replace("\r", "").Replace("\n", " ").Replace("|", @"\|");
158+
private static string MakeDesyncGameTable(GameFromLogCollection gamesFromLog)
159+
{
160+
var tableEmpty = true;
161+
var sb = new StringBuilder();
162+
sb.AppendLine("\n\n|GameID|GameState File|");
163+
sb.AppendLine("|-|-|");
164+
165+
foreach (var game in gamesFromLog.Games.Where(g => g.HasDesync && g.GameStateFileNames != null))
166+
{
167+
var gameIDString = EscapeMarkdownTableCell(game.GameID ?? "Unknown");
168+
foreach (var gameStateFileName in game.GameStateFileNames)
169+
{
170+
tableEmpty = false;
171+
sb.AppendLine($"|{gameIDString}|{EscapeMarkdownTableCell(gameStateFileName)}|");
172+
}
173+
}
174+
return tableEmpty ? string.Empty : sb.ToString();
175+
}
176+
177+
private static async Task<Issue> ReportCrash(string infolog, CrashType type, string engine, string bugReportTitle, string bugReportDescription, GameFromLogCollection gamesFromLog)
32178
{
33179
try
34180
{
35-
var client = new GitHubClient(new ProductHeaderValue("chobbyla"));
36-
client.Credentials = new Credentials(GlobalConst.CrashReportGithubToken);
181+
var client = new GitHubClient(new ProductHeaderValue("chobbyla"))
182+
{
183+
Credentials = new Credentials(GlobalConst.CrashReportGithubToken)
184+
};
185+
186+
var infologTruncated = TextTruncator.Truncate(infolog, MaxInfologSize, MakeRegionsOfInterest(infolog.Length, gamesFromLog.Games.Where(g => g.HasDesync).Select(g => g.FirstDesyncIdxInLog.Value), gamesFromLog.AsGameStartReadOnlyList()));
37187

38-
infolog = Truncate(infolog, MaxInfologSize);
188+
var desyncDebugInfo = MakeDesyncGameTable(gamesFromLog);
39189

40-
var createdIssue =
41-
client.Issue.Create("ZeroK-RTS", "CrashReports", new NewIssue($"Spring {type} [{engine}] {bugReportTitle}") { Body = $"{bugReportDescription}\n\n```{infolog}```", })
42-
.Result;
190+
var newIssueRequest = new NewIssue($"Spring {type} [{engine}] {bugReportTitle}")
191+
{
192+
Body = $"{bugReportDescription}{desyncDebugInfo}"
193+
};
194+
var createdIssue = await client.Issue.Create(CrashReportsRepoOwner, CrashReportsRepoName, newIssueRequest);
195+
196+
await client.Issue.Comment.Create(CrashReportsRepoOwner, CrashReportsRepoName, createdIssue.Number, $"infolog_full.txt (truncated):\n\n```{infologTruncated}```");
43197

44198
return createdIssue;
45199
}
46200
catch (Exception ex)
47201
{
48-
Trace.TraceWarning("Problem reporting a bug: {0}", ex);
202+
Trace.TraceWarning("[CrashReportHelper] Problem reporting a bug: {0}", ex);
49203
}
50204
return null;
51205
}
52-
public static int FindFirstDesyncMessage(string logStr)
206+
207+
//All infolog parsing is best-effort only.
208+
//The infolog file format does not have enough structure to guarantee that it is never misinterpreted.
209+
210+
private static int[] ReadGameReloads(string logStr)
53211
{
54-
//[t=00:22:43.533864][f=0003461] Sync error for mankarse in frame 3451 (got 927a6f33, correct is 6b550dd1)
212+
//Game reload detected by [f=-000001] that follows either the start of the file, or [f={non-negative value}]
213+
214+
var list = new List<int>();
215+
216+
var negativeFrameRegex = new Regex(@"f=-(?<=(?<s>^)\[t=\d+:\d+:\d+\.\d+\]\[f=-)\d+\]", RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Multiline, TimeSpan.FromSeconds(30));
217+
var nonNegativeFrameRegex = new Regex(@"f=\d(?<=^\[t=\d+:\d+:\d+\.\d+\]\[f=\d)\d*\]", RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Multiline, TimeSpan.FromSeconds(30));
218+
219+
var idx = 0;
220+
55221
try
56222
{
57-
//See ZkData.Account.IsValidLobbyName
58-
var accountNamePattern = @"[_[\]a-zA-Z0-9]{1,25}";
59-
var match =
60-
Regex.Match(
61-
logStr,
62-
$@"Sync error for(?<={InfoLogLineStartPattern}Sync error for) {accountNamePattern} in frame \d+ \(got [a-z0-9]+, correct is [a-z0-9]+\){InfoLogLineEndPattern}",
63-
RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Multiline,
64-
TimeSpan.FromSeconds(30));
223+
while (true)
224+
{
225+
{
226+
var m = negativeFrameRegex.Match(logStr, idx);
227+
if (!m.Success) break;
228+
idx = m.Index;
229+
list.Add(m.Groups["s"].Index);
230+
}
231+
{
232+
var m = nonNegativeFrameRegex.Match(logStr, idx);
233+
if (!m.Success) break;
234+
idx = m.Index;
235+
}
236+
}
237+
return list.ToArray();
238+
}
239+
catch (RegexMatchTimeoutException)
240+
{
241+
Trace.TraceError("\"[CrashReportHelper] RegexMatchTimeoutException in ReadGameReloads");
242+
return Array.Empty<int>();
243+
}
244+
}
245+
private static (int, string)[] ReadGameStateFileNames(string logStr)
246+
{
247+
//See https://github.com/beyond-all-reason/spring/blob/f3ba23635e1462ae2084f10bf9ba777467d16090/rts/System/Sync/DumpState.cpp#L155
65248

66-
return match.Success ? match.Index : -1;
249+
//[t=00:22:43.353840][f=0003461] [DumpState] using dump-file "ClientGameState--749245531-[3461-3461].txt"
250+
try
251+
{
252+
return
253+
Regex
254+
.Matches(
255+
logStr,
256+
$@"(?<={InfoLogLineStartPattern})\[DumpState\] using dump-file ""(?<d>[^{Regex.Escape(System.IO.Path.DirectorySeparatorChar.ToString())}{Regex.Escape(System.IO.Path.AltDirectorySeparatorChar.ToString())}""]+)""{InfoLogLineEndPattern}",
257+
RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Multiline,
258+
TimeSpan.FromSeconds(30))
259+
.Cast<Match>().Select(m => (m.Index, m.Groups["d"].Value)).Distinct()
260+
.ToArray();
67261
}
68-
catch(RegexMatchTimeoutException)
262+
catch (RegexMatchTimeoutException)
69263
{
70-
Trace.TraceError("[CrashReportHelper] RegexMatchTimeoutException in FindFirstDesyncMessage");
71-
return -1;
264+
Trace.TraceError("\"[CrashReportHelper] RegexMatchTimeoutException in ReadClientStateFileNames");
265+
return Array.Empty<(int, string)>();
72266
}
73267
}
74268

75-
private static string Truncate(string infolog, int maxSize)
269+
private static int[] ReadDesyncs(string logStr)
76270
{
77-
var firstDesync = FindFirstDesyncMessage(infolog);
78-
var regionsOfInterest = new List<TextTruncator.RegionOfInterest>(firstDesync == -1 ? 2 : 3);
271+
//[t=00:22:43.533864][f=0003461] Sync error for mankarse in frame 3451 (got 927a6f33, correct is 6b550dd1)
79272

80-
regionsOfInterest.Add(new TextTruncator.RegionOfInterest { PointOfInterest = 0, StartLimit = 0, EndLimit = infolog.Length });
81-
if (firstDesync != -1)
273+
//See ZkData.Account.IsValidLobbyName
274+
var accountNamePattern = @"[_[\]a-zA-Z0-9]{1,25}";
275+
try
276+
{
277+
return
278+
Regex
279+
.Matches(
280+
logStr,
281+
$@"Sync error for(?<={InfoLogLineStartPattern}Sync error for) {accountNamePattern} in frame \d+ \(got [a-z0-9]+, correct is [a-z0-9]+\){InfoLogLineEndPattern}",
282+
RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Multiline,
283+
TimeSpan.FromSeconds(30))
284+
.Cast<Match>().Select(m => m.Index).ToArray();
285+
}
286+
catch (RegexMatchTimeoutException)
82287
{
83-
regionsOfInterest.Add(new TextTruncator.RegionOfInterest { PointOfInterest = firstDesync, StartLimit = 0, EndLimit = infolog.Length });
288+
Trace.TraceError("\"[CrashReportHelper] RegexMatchTimeoutException in ReadDesyncs");
289+
return Array.Empty<int>();
84290
}
85-
regionsOfInterest.Add(new TextTruncator.RegionOfInterest { PointOfInterest = infolog.Length, StartLimit = 0, EndLimit = infolog.Length });
291+
}
292+
293+
294+
private static (int, string)[] ReadGameIDs(string logStr)
295+
{
296+
//[t=00:19:00.246149][f=-000001] GameID: 6065f665e92c7942def2c0c17c703e72
86297

87-
return TextTruncator.Truncate(infolog, maxSize, regionsOfInterest);
298+
try
299+
{
300+
return
301+
Regex
302+
.Matches(
303+
logStr,
304+
$@"GameID:(?<={InfoLogLineStartPattern}GameID:) (?<g>[0-9a-zA-Z]+){InfoLogLineEndPattern}",
305+
RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Multiline,
306+
TimeSpan.FromSeconds(30))
307+
.Cast<Match>().Select(m => { var g = m.Groups["g"]; return (g.Index, g.Value); }).ToArray();
308+
}
309+
catch (RegexMatchTimeoutException)
310+
{
311+
Trace.TraceError("\"[CrashReportHelper] RegexMatchTimeoutException in ReadGameIDs");
312+
return Array.Empty<(int, string)>();
313+
}
88314
}
89315

90316
public static void CheckAndReportErrors(string logStr, bool springRunOk, string bugReportTitle, string bugReportDescription, string engineVersion)
91317
{
92-
var syncError = FindFirstDesyncMessage(logStr) != -1;
318+
var gamesFromLog = new GameFromLogCollection(ReadGameReloads(logStr));
319+
320+
gamesFromLog.AddGameStateFileNames(ReadGameStateFileNames(logStr));
321+
gamesFromLog.AddDesyncs(ReadDesyncs(logStr));
322+
gamesFromLog.AddGameIDs(ReadGameIDs(logStr));
323+
324+
var syncError = gamesFromLog.Games.Any(g => g.HasDesync);
93325
if (syncError) Trace.TraceWarning("Sync error detected");
94326

95327
var openGlFail = logStr.Contains("No OpenGL drivers installed.") ||
@@ -139,11 +371,16 @@ public static void CheckAndReportErrors(string logStr, bool springRunOk, string
139371
: luaErr ? CrashType.LuaError
140372
: CrashType.Crash;
141373

142-
var ret = ReportCrash(logStr,
143-
crashType,
144-
engineVersion,
145-
bugReportTitle,
146-
bugReportDescription);
374+
var ret =
375+
ReportCrash(
376+
logStr,
377+
crashType,
378+
engineVersion,
379+
bugReportTitle,
380+
bugReportDescription,
381+
gamesFromLog)
382+
.GetAwaiter().GetResult();
383+
147384
if (ret != null)
148385
try
149386
{

0 commit comments

Comments
 (0)