1
1
using System ;
2
2
using System . Collections . Generic ;
3
3
using System . Diagnostics ;
4
+ using System . IO ;
5
+ using System . IO . Compression ;
4
6
using System . Linq ;
5
7
using System . Text ;
6
8
using System . Text . RegularExpressions ;
@@ -26,6 +28,8 @@ public static class CrashReportHelper
26
28
private const string CrashReportsRepoName = "CrashReports" ;
27
29
28
30
private const int MaxInfologSize = 62000 ;
31
+ private const int IssuesPerRelease = 250 ;
32
+
29
33
private const string InfoLogLineStartPattern = @"(^\[t=\d+:\d+:\d+\.\d+\]\[f=-?\d+\] )" ;
30
34
private const string InfoLogLineEndPattern = @"(\r?\n|\Z)" ;
31
35
private sealed class GameFromLog
@@ -153,6 +157,81 @@ public void AddGameIDs(IEnumerable<(int, string)> gameIDs)
153
157
154
158
return result ;
155
159
}
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
+ }
156
235
157
236
private static string EscapeMarkdownTableCell ( string str ) => str . Replace ( "\r " , "" ) . Replace ( "\n " , " " ) . Replace ( "|" , @"\|" ) ;
158
237
private static string MakeDesyncGameTable ( GameFromLogCollection gamesFromLog )
@@ -205,7 +284,19 @@ private static void FillIssueLabels(System.Collections.ObjectModel.Collection<st
205
284
}
206
285
}
207
286
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 )
209
300
{
210
301
try
211
302
{
@@ -247,6 +338,40 @@ private static async Task<Issue> ReportCrash(string infolog, CrashType type, str
247
338
248
339
await client . Issue . Comment . Create ( CrashReportsRepoOwner , CrashReportsRepoName , createdIssue . Number , $ "infolog_full.txt (truncated):\n \n ```{ infologTruncated } ```") ;
249
340
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
+
250
375
return createdIssue ;
251
376
}
252
377
catch ( Exception ex )
@@ -305,7 +430,7 @@ private static (int, string)[] ReadGameStateFileNames(string logStr)
305
430
Regex
306
431
. Matches (
307
432
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 } ",
309
434
RegexOptions . CultureInvariant | RegexOptions . Compiled | RegexOptions . ExplicitCapture | RegexOptions . Multiline ,
310
435
TimeSpan . FromSeconds ( 30 ) )
311
436
. Cast < Match > ( ) . Select ( m => ( m . Index , m . Groups [ "d" ] . Value ) ) . Distinct ( )
@@ -365,7 +490,7 @@ private static (int, string)[] ReadGameIDs(string logStr)
365
490
}
366
491
}
367
492
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 )
369
494
{
370
495
var gamesFromLog = new GameFromLogCollection ( ReadGameReloads ( logStr ) ) ;
371
496
@@ -428,6 +553,7 @@ public static void CheckAndReportErrors(string logStr, bool springRunOk, string
428
553
logStr ,
429
554
crashType ,
430
555
engineVersion ,
556
+ paths ,
431
557
bugReportTitle ,
432
558
bugReportDescription ,
433
559
gamesFromLog )
0 commit comments