Skip to content

Commit a878f6e

Browse files
committed
Make CrashReportHelper upload files to GitHub Releases
This will only work if the ZeroK-RTS/CrashReports repository has a branch named "main". Currently the files that are uploaded are: infolog_full.txt (not truncated) GameState files (if there is a desync)
1 parent 8706b0d commit a878f6e

File tree

3 files changed

+131
-5
lines changed

3 files changed

+131
-5
lines changed

ChobbyLauncher/ChobbylaLocalListener.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@ private async Task Process(StartNewSpring args)
656656
StartScriptContent = args.StartScriptContent
657657
});
658658

659-
CrashReportHelper.CheckAndReportErrors(logs.ToString(), isOk, "Externally launched spring crashed with code " + process.ExitCode, null, args.Engine);
659+
CrashReportHelper.CheckAndReportErrors(logs.ToString(), isOk, paths, "Externally launched spring crashed with code " + process.ExitCode, null, args.Engine);
660660
};
661661
process.EnableRaisingEvents = true;
662662
process.Start();

ChobbyLauncher/CrashReportHelper.cs

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4+
using System.IO;
5+
using System.IO.Compression;
46
using System.Linq;
57
using System.Text;
68
using System.Text.RegularExpressions;
@@ -26,6 +28,8 @@ public static class CrashReportHelper
2628
private const string CrashReportsRepoName = "CrashReports";
2729

2830
private const int MaxInfologSize = 62000;
31+
private const int IssuesPerRelease = 250;
32+
2933
private const string InfoLogLineStartPattern = @"(^\[t=\d+:\d+:\d+\.\d+\]\[f=-?\d+\] )";
3034
private const string InfoLogLineEndPattern = @"(\r?\n|\Z)";
3135
private sealed class GameFromLog
@@ -153,6 +157,81 @@ public void AddGameIDs(IEnumerable<(int, string)> gameIDs)
153157

154158
return result;
155159
}
160+
//See https://github.com/beyond-all-reason/spring/blob/f3ba23635e1462ae2084f10bf9ba777467d16090/rts/System/Sync/DumpState.cpp#L155
161+
private static readonly Regex GameStateFileRegex = new Regex(@"\A(Server|Client)GameState--?\d+-\[-?\d+--?\d+\]\.txt\z", RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.ExplicitCapture, TimeSpan.FromSeconds(30));
162+
163+
private static (Stream, List<string>) CreateZipArchiveFromFiles(string writableDirectory, string[] fileNames, (string, string)[] extraFiles)
164+
{
165+
var outStream = new MemoryStream();
166+
var fullPathWritableDirectory = Path.GetFullPath(writableDirectory + Path.DirectorySeparatorChar.ToString());
167+
var archiveManifest = new List<string>(fileNames.Length);
168+
try
169+
{
170+
using (var archive = new ZipArchive(outStream, ZipArchiveMode.Create, leaveOpen: true))
171+
{
172+
foreach (
173+
var fileName in
174+
fileNames
175+
.Select(f => Path.GetFullPath(Path.Combine(writableDirectory, f)))
176+
.Distinct(StringComparer.OrdinalIgnoreCase))
177+
{
178+
if (!fileName.StartsWith(fullPathWritableDirectory, StringComparison.Ordinal))
179+
{
180+
//Only upload files that are in WritableDirectory.
181+
//This avoids inadvertent directory traversal, which could
182+
//upload private data from the player's computer.
183+
Trace.TraceWarning("[CrashReportHelper] Tried to upload file that is not in WritableDirectory: {0}", fileName);
184+
continue;
185+
}
186+
var relativePath = fileName.Remove(0, fullPathWritableDirectory.Length);
187+
if (!GameStateFileRegex.IsMatch(relativePath))
188+
{
189+
//Only upload files that we expect to upload. Currently, we only upload GameState files.
190+
Trace.TraceWarning("[CrashReportHelper] Tried to upload unexpected file: {0}", relativePath);
191+
continue;
192+
}
193+
var entryPath = Path.Combine("zk", relativePath);
194+
var entry = archive.CreateEntry(entryPath, CompressionLevel.Optimal);
195+
FileStream fsPre;
196+
try
197+
{
198+
fsPre = new FileStream(fileName, System.IO.FileMode.Open, FileAccess.Read);
199+
}
200+
catch
201+
{
202+
Trace.TraceWarning("[CrashReportHelper] Could not read file to add to archive: {0}", relativePath);
203+
continue;
204+
}
205+
//Errors from here onwards could corrupt the ZipArchive; so do not continue.
206+
using (var fs = fsPre)
207+
using (var entryStream = entry.Open())
208+
{
209+
fs.CopyTo(entryStream);
210+
}
211+
archiveManifest.Add(entryPath);
212+
}
213+
foreach (var extra in extraFiles)
214+
{
215+
var entryPath = Path.Combine("ex", extra.Item1);
216+
var entry = archive.CreateEntry(entryPath, CompressionLevel.Optimal);
217+
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(extra.Item2)))
218+
using (var entryStream = entry.Open())
219+
{
220+
ms.CopyTo(entryStream);
221+
}
222+
archiveManifest.Add(entryPath);
223+
}
224+
}
225+
outStream.Position = 0;
226+
return (archiveManifest.Count != 0 ? outStream : null, archiveManifest);
227+
}
228+
catch (Exception ex)
229+
{
230+
Trace.TraceWarning("[CrashReportHelper] Could not create archive: {0}", ex);
231+
outStream.Dispose();
232+
return (null, null);
233+
}
234+
}
156235

157236
private static string EscapeMarkdownTableCell(string str) => str.Replace("\r", "").Replace("\n", " ").Replace("|", @"\|");
158237
private static string MakeDesyncGameTable(GameFromLogCollection gamesFromLog)
@@ -205,7 +284,19 @@ private static void FillIssueLabels(System.Collections.ObjectModel.Collection<st
205284
}
206285
}
207286

208-
private static async Task<Issue> ReportCrash(string infolog, CrashType type, string engine, string bugReportTitle, string bugReportDescription, GameFromLogCollection gamesFromLog)
287+
private static string MakeArchiveManifestTable(IEnumerable<string> archiveManifest)
288+
{
289+
var sb = new StringBuilder();
290+
sb.AppendLine("\n\n|Contents|");
291+
sb.AppendLine("|-|");
292+
foreach (var f in archiveManifest)
293+
{
294+
sb.AppendLine($"|{EscapeMarkdownTableCell(f)}|");
295+
}
296+
return sb.ToString();
297+
}
298+
299+
private static async Task<Issue> ReportCrash(string infolog, CrashType type, string engine, SpringPaths paths, string bugReportTitle, string bugReportDescription, GameFromLogCollection gamesFromLog)
209300
{
210301
try
211302
{
@@ -247,6 +338,40 @@ private static async Task<Issue> ReportCrash(string infolog, CrashType type, str
247338

248339
await client.Issue.Comment.Create(CrashReportsRepoOwner, CrashReportsRepoName, createdIssue.Number, $"infolog_full.txt (truncated):\n\n```{infologTruncated}```");
249340

341+
342+
var releaseNumber = (createdIssue.Number - 1) / IssuesPerRelease;
343+
var issueRangeString = $"{releaseNumber * IssuesPerRelease + 1}-{(releaseNumber + 1) * IssuesPerRelease}";
344+
345+
var releaseName = $"FilesForIssues-{issueRangeString}";
346+
Release releaseForUpload;
347+
try
348+
{
349+
releaseForUpload = await client.Repository.Release.Create(CrashReportsRepoOwner, CrashReportsRepoName, new NewRelease(releaseName) { TargetCommitish = "main", Prerelease = true, Name = $"Files for Issues {issueRangeString}", Body = $"Files for Issues {issueRangeString}" });
350+
}
351+
catch (ApiValidationException ex)
352+
{
353+
if (!(ex.ApiError.Errors.Count == 1 && ex.ApiError.Errors[0].Code == "already_exists")) throw;
354+
//Release already exists
355+
releaseForUpload = await client.Repository.Release.Get(CrashReportsRepoOwner, CrashReportsRepoName, releaseName);
356+
}
357+
358+
var zar =
359+
CreateZipArchiveFromFiles(
360+
paths.WritableDirectory,
361+
gamesFromLog.Games.SelectMany(g => g.GameStateFileNames ?? Enumerable.Empty<string>()).ToArray(),
362+
new[] { ("infolog_full.txt", infolog) });
363+
364+
if (zar.Item1 != null)
365+
{
366+
using (var zipArchive = zar.Item1)
367+
{
368+
var upload = await client.Repository.Release.UploadAsset(releaseForUpload, new ReleaseAssetUpload($"FilesForIssue-{createdIssue.Number}.zip", "application/zip", zipArchive, timeout: null));
369+
370+
var archiveManifestTable = MakeArchiveManifestTable(zar.Item2);
371+
var comment = await client.Issue.Comment.Create(CrashReportsRepoOwner, CrashReportsRepoName, createdIssue.Number, $"See {upload.BrowserDownloadUrl}{archiveManifestTable}");
372+
}
373+
}
374+
250375
return createdIssue;
251376
}
252377
catch (Exception ex)
@@ -305,7 +430,7 @@ private static (int, string)[] ReadGameStateFileNames(string logStr)
305430
Regex
306431
.Matches(
307432
logStr,
308-
$@"(?<={InfoLogLineStartPattern})\[DumpState\] using dump-file ""(?<d>[^{Regex.Escape(System.IO.Path.DirectorySeparatorChar.ToString())}{Regex.Escape(System.IO.Path.AltDirectorySeparatorChar.ToString())}""]+)""{InfoLogLineEndPattern}",
433+
$@"(?<={InfoLogLineStartPattern})\[DumpState\] using dump-file ""(?<d>[^{Regex.Escape(Path.DirectorySeparatorChar.ToString())}{Regex.Escape(Path.AltDirectorySeparatorChar.ToString())}""]+)""{InfoLogLineEndPattern}",
309434
RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.Multiline,
310435
TimeSpan.FromSeconds(30))
311436
.Cast<Match>().Select(m => (m.Index, m.Groups["d"].Value)).Distinct()
@@ -365,7 +490,7 @@ private static (int, string)[] ReadGameIDs(string logStr)
365490
}
366491
}
367492

368-
public static void CheckAndReportErrors(string logStr, bool springRunOk, string bugReportTitle, string bugReportDescription, string engineVersion)
493+
public static void CheckAndReportErrors(string logStr, bool springRunOk, SpringPaths paths, string bugReportTitle, string bugReportDescription, string engineVersion)
369494
{
370495
var gamesFromLog = new GameFromLogCollection(ReadGameReloads(logStr));
371496

@@ -428,6 +553,7 @@ public static void CheckAndReportErrors(string logStr, bool springRunOk, string
428553
logStr,
429554
crashType,
430555
engineVersion,
556+
paths,
431557
bugReportTitle,
432558
bugReportDescription,
433559
gamesFromLog)

ChobbyLauncher/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ private static void RunWrapper(Chobbyla chobbyla, ulong connectLobbyID, TextWrit
168168
logWriter.Flush();
169169
var logStr = logSb.ToString();
170170

171-
CrashReportHelper.CheckAndReportErrors(logStr, springRunOk, chobbyla.BugReportTitle, chobbyla.BugReportDescription, chobbyla.engine);
171+
CrashReportHelper.CheckAndReportErrors(logStr, springRunOk, chobbyla.paths, chobbyla.BugReportTitle, chobbyla.BugReportDescription, chobbyla.engine);
172172
}
173173

174174
static async Task<bool> PrepareWithoutGui(Chobbyla chobbyla)

0 commit comments

Comments
 (0)