4
4
import java .io .File ;
5
5
import java .io .IOException ;
6
6
import java .util .Arrays ;
7
+ import java .util .Collections ;
8
+ import java .util .Comparator ;
7
9
import java .util .Enumeration ;
8
10
import java .util .HashMap ;
9
11
import java .util .LinkedHashMap ;
12
+ import java .util .LinkedList ;
13
+ import java .util .List ;
10
14
import java .util .Map ;
11
15
import java .util .Objects ;
16
+ import java .util .regex .Matcher ;
12
17
import java .util .regex .Pattern ;
13
18
import java .util .zip .ZipEntry ;
14
19
import java .util .zip .ZipFile ;
29
34
30
35
import com .semmle .util .concurrent .LockDirectory ;
31
36
import com .semmle .util .concurrent .LockDirectory .LockingMode ;
37
+ import com .semmle .util .data .Pair ;
32
38
import com .semmle .util .exception .CatastrophicError ;
33
39
import com .semmle .util .exception .NestedError ;
34
40
import com .semmle .util .exception .ResourceError ;
43
49
import com .semmle .util .trap .pathtransformers .PathTransformer ;
44
50
45
51
public class OdasaOutput {
52
+ // By default we use lockless TRAP writing, but this can be set
53
+ // if we want to use the old TRAP locking for any reason.
54
+ private final boolean use_trap_locking = Env .systemEnv ().getBoolean ("CODEQL_EXTRACTOR_JAVA_TRAP_LOCKING" , false );
46
55
47
56
// either these are set ...
48
57
private final File trapFolder ;
@@ -260,22 +269,59 @@ private void deleteTrapFileAndDependencies(IrDeclaration sym, String signature)
260
269
* Any unique suffix needed to distinguish `sym` from other declarations with the same name.
261
270
* For functions for example, this means its parameter signature.
262
271
*/
263
- private TrapFileManager getMembersWriterForDecl (File trap , IrDeclaration sym , String signature ) {
264
- TrapClassVersion currVersion = TrapClassVersion .fromSymbol (sym , log );
265
- String shortName = sym instanceof IrDeclarationWithName ? ((IrDeclarationWithName )sym ).getName ().asString () : "(name unknown)" ;
266
- if (trap .exists ()) {
267
- // Only re-write an existing trap file if we encountered a newer version of the same class.
268
- TrapClassVersion trapVersion = readVersionInfo (trap );
269
- if (!currVersion .isValid ()) {
270
- log .warn ("Not rewriting trap file for: " + shortName + " " + trapVersion + " " + currVersion + " " + trap );
271
- } else if (currVersion .newerThan (trapVersion )) {
272
- log .trace ("Rewriting trap file for: " + shortName + " " + trapVersion + " " + currVersion + " " + trap );
273
- deleteTrapFileAndDependencies (sym , signature );
272
+ private TrapFileManager getMembersWriterForDecl (File trap , File trapFileBase , TrapClassVersion trapFileVersion , IrDeclaration sym , String signature ) {
273
+ if (use_trap_locking ) {
274
+ TrapClassVersion currVersion = TrapClassVersion .fromSymbol (sym , log );
275
+ String shortName = sym instanceof IrDeclarationWithName ? ((IrDeclarationWithName )sym ).getName ().asString () : "(name unknown)" ;
276
+ if (trap .exists ()) {
277
+ // Only re-write an existing trap file if we encountered a newer version of the same class.
278
+ TrapClassVersion trapVersion = readVersionInfo (trap );
279
+ if (!currVersion .isValid ()) {
280
+ log .warn ("Not rewriting trap file for: " + shortName + " " + trapVersion + " " + currVersion + " " + trap );
281
+ } else if (currVersion .newerThan (trapVersion )) {
282
+ log .trace ("Rewriting trap file for: " + shortName + " " + trapVersion + " " + currVersion + " " + trap );
283
+ deleteTrapFileAndDependencies (sym , signature );
284
+ } else {
285
+ return null ;
286
+ }
274
287
} else {
275
- return null ;
288
+ log . trace ( "Writing trap file for: " + shortName + " " + currVersion + " " + trap ) ;
276
289
}
277
290
} else {
278
- log .trace ("Writing trap file for: " + shortName + " " + currVersion + " " + trap );
291
+ // If the TRAP file already exists then we
292
+ // don't need to write it.
293
+ if (trap .exists ()) {
294
+ log .warn ("Not rewriting trap file for " + trap .toString () + " as it exists" );
295
+ return null ;
296
+ }
297
+ // If the TRAP file was written in the past, and
298
+ // then renamed to its trap-old name, then we
299
+ // don't need to rewrite it only to rename it
300
+ // again.
301
+ File trapFileDir = trap .getParentFile ();
302
+ File trapOld = new File (trapFileDir , trap .getName ().replace (".trap.gz" , ".trap-old.gz" ));
303
+ if (trapOld .exists ()) {
304
+ log .warn ("Not rewriting trap file for " + trap .toString () + " as the trap-old exists" );
305
+ return null ;
306
+ }
307
+ // Otherwise, if any newer TRAP file has already
308
+ // been written then we don't need to write
309
+ // anything.
310
+ if (trapFileBase != null && trapFileVersion != null && trapFileDir .exists ()) {
311
+ String trapFileBaseName = trapFileBase .getName ();
312
+
313
+ for (File f : FileUtil .list (trapFileDir )) {
314
+ String name = f .getName ();
315
+ Matcher m = selectClassVersionComponents .matcher (name );
316
+ if (m .matches () && m .group (1 ).equals (trapFileBaseName )) {
317
+ TrapClassVersion v = new TrapClassVersion (Integer .valueOf (m .group (2 )), Integer .valueOf (m .group (3 )), Long .valueOf (m .group (4 )), m .group (5 ));
318
+ if (v .newerThan (trapFileVersion )) {
319
+ log .warn ("Not rewriting trap file for " + trap .toString () + " as " + f .toString () + " exists" );
320
+ return null ;
321
+ }
322
+ }
323
+ }
324
+ }
279
325
}
280
326
return trapWriter (trap , sym , signature );
281
327
}
@@ -328,19 +374,24 @@ public void close() {
328
374
}
329
375
330
376
writeTrapDependencies (trapDependenciesForClass );
331
- // Record major/minor version information for extracted class files.
332
- // This is subsequently used to determine whether to re-extract (a newer version of) the same class.
333
- File metadataFile = new File (trapFile .getAbsolutePath ().replace (".trap.gz" , ".metadata" ));
334
- try {
335
- Map <String , String > versionMap = new LinkedHashMap <>();
336
- TrapClassVersion tcv = TrapClassVersion .fromSymbol (sym , log );
337
- versionMap .put (MAJOR_VERSION , String .valueOf (tcv .getMajorVersion ()));
338
- versionMap .put (MINOR_VERSION , String .valueOf (tcv .getMinorVersion ()));
339
- versionMap .put (LAST_MODIFIED , String .valueOf (tcv .getLastModified ()));
340
- versionMap .put (EXTRACTOR_NAME , tcv .getExtractorName ());
341
- FileUtil .writePropertiesCSV (metadataFile , versionMap );
342
- } catch (IOException e ) {
343
- log .warn ("Could not save trap metadata file: " + metadataFile .getAbsolutePath (), e );
377
+
378
+ // If we are using TRAP locking then we
379
+ // need to write a metadata file.
380
+ if (use_trap_locking ) {
381
+ // Record major/minor version information for extracted class files.
382
+ // This is subsequently used to determine whether to re-extract (a newer version of) the same class.
383
+ File metadataFile = new File (trapFile .getAbsolutePath ().replace (".trap.gz" , ".metadata" ));
384
+ try {
385
+ Map <String , String > versionMap = new LinkedHashMap <>();
386
+ TrapClassVersion tcv = TrapClassVersion .fromSymbol (sym , log );
387
+ versionMap .put (MAJOR_VERSION , String .valueOf (tcv .getMajorVersion ()));
388
+ versionMap .put (MINOR_VERSION , String .valueOf (tcv .getMinorVersion ()));
389
+ versionMap .put (LAST_MODIFIED , String .valueOf (tcv .getLastModified ()));
390
+ versionMap .put (EXTRACTOR_NAME , tcv .getExtractorName ());
391
+ FileUtil .writePropertiesCSV (metadataFile , versionMap );
392
+ } catch (IOException e ) {
393
+ log .warn ("Could not save trap metadata file: " + metadataFile .getAbsolutePath (), e );
394
+ }
344
395
}
345
396
}
346
397
private void writeTrapDependencies (TrapDependencies trapDependencies ) {
@@ -358,6 +409,8 @@ public void setHasError() {
358
409
* Trap file locking.
359
410
*/
360
411
412
+ private final Pattern selectClassVersionComponents = Pattern .compile ("(.*)#(-?[0-9]+)\\ .(-?[0-9]+)-(-?[0-9]+)-(.*)\\ .trap\\ .gz" );
413
+
361
414
/**
362
415
* <b>CAUTION</b>: to avoid the potential for deadlock between multiple concurrent extractor processes,
363
416
* only one source file {@link TrapLocker} may be open at any time, and the lock must be obtained
@@ -414,6 +467,10 @@ public TrapLocker getTrapLockerForDecl(IrDeclaration sym, String signature) {
414
467
public class TrapLocker implements AutoCloseable {
415
468
private final IrDeclaration sym ;
416
469
private final File trapFile ;
470
+ // trapFileBase is used when doing lockless TRAP file writing.
471
+ // It is trapFile without the #metadata.trap.gz suffix.
472
+ private File trapFileBase = null ;
473
+ private TrapClassVersion trapFileVersion = null ;
417
474
private final String signature ;
418
475
private TrapLocker (IrDeclaration decl , String signature ) {
419
476
this .sym = decl ;
@@ -422,7 +479,20 @@ private TrapLocker(IrDeclaration decl, String signature) {
422
479
log .error ("Null symbol passed for Kotlin TRAP locker" );
423
480
trapFile = null ;
424
481
} else {
425
- trapFile = getTrapFileForDecl (sym , signature );
482
+ File normalTrapFile = getTrapFileForDecl (sym , signature );
483
+ if (use_trap_locking ) {
484
+ trapFile = normalTrapFile ;
485
+ } else {
486
+ // We encode the metadata into the filename, so that the
487
+ // TRAP filenames for different metadatas don't overlap.
488
+ trapFileVersion = TrapClassVersion .fromSymbol (sym , log );
489
+ String baseName = normalTrapFile .getName ().replace (".trap.gz" , "" );
490
+ // If a class has lots of inner classes, then we get lots of files
491
+ // in a single directory. This makes our directory listings later slow.
492
+ // To avoid this, rather than using files named .../Foo*, we use .../Foo/Foo*.
493
+ trapFileBase = new File (new File (normalTrapFile .getParentFile (), baseName ), baseName );
494
+ trapFile = new File (trapFileBase .getPath () + '#' + trapFileVersion .toString () + ".trap.gz" );
495
+ }
426
496
}
427
497
}
428
498
private TrapLocker (File jarFile ) {
@@ -437,20 +507,83 @@ private TrapLocker(String moduleName) {
437
507
}
438
508
public TrapFileManager getTrapFileManager () {
439
509
if (trapFile !=null ) {
440
- lockTrapFile (trapFile );
441
- return getMembersWriterForDecl (trapFile , sym , signature );
510
+ if (use_trap_locking ) {
511
+ lockTrapFile (trapFile );
512
+ }
513
+ return getMembersWriterForDecl (trapFile , trapFileBase , trapFileVersion , sym , signature );
442
514
} else {
443
515
return null ;
444
516
}
445
517
}
518
+
446
519
@ Override
447
520
public void close () {
448
521
if (trapFile !=null ) {
449
522
try {
450
- unlockTrapFile (trapFile );
523
+ if (use_trap_locking ) {
524
+ unlockTrapFile (trapFile );
525
+ }
451
526
} catch (NestedError e ) {
452
527
log .warn ("Error unlocking trap file " + trapFile .getAbsolutePath (), e );
453
528
}
529
+
530
+ // If we are writing TRAP file locklessly, then now that we
531
+ // have finished writing our TRAP file, we want to rename
532
+ // and TRAP file that matches our trapFileBase but doesn't
533
+ // have the latest metadata.
534
+ // Renaming it to trap-old means that it won't be imported,
535
+ // but we can still use its presence to avoid future
536
+ // invocations rewriting it, and it means that the information
537
+ // is in the TRAP directory if we need it for debugging.
538
+ if (!use_trap_locking && sym != null ) {
539
+ File trapFileDir = trapFileBase .getParentFile ();
540
+ String trapFileBaseName = trapFileBase .getName ();
541
+
542
+ List <Pair <File , TrapClassVersion >> pairs = new LinkedList <Pair <File , TrapClassVersion >>();
543
+ for (File f : FileUtil .list (trapFileDir )) {
544
+ String name = f .getName ();
545
+ Matcher m = selectClassVersionComponents .matcher (name );
546
+ if (m .matches ()) {
547
+ if (m .group (1 ).equals (trapFileBaseName )) {
548
+ TrapClassVersion v = new TrapClassVersion (Integer .valueOf (m .group (2 )), Integer .valueOf (m .group (3 )), Long .valueOf (m .group (4 )), m .group (5 ));
549
+ pairs .add (new Pair <File , TrapClassVersion >(f , v ));
550
+ } else {
551
+ // Everything in this directory should be for the same TRAP file base
552
+ log .error ("Unexpected sibling " + m .group (1 ) + " when extracting " + trapFileBaseName );
553
+ }
554
+ }
555
+ }
556
+ if (pairs .isEmpty ()) {
557
+ log .error ("Wrote TRAP file, but no TRAP files exist for " + trapFile .getAbsolutePath ());
558
+ } else {
559
+ Comparator <Pair <File , TrapClassVersion >> comparator = new Comparator <Pair <File , TrapClassVersion >>() {
560
+ @ Override
561
+ public int compare (Pair <File , TrapClassVersion > p1 , Pair <File , TrapClassVersion > p2 ) {
562
+ TrapClassVersion v1 = p1 .snd ();
563
+ TrapClassVersion v2 = p2 .snd ();
564
+ if (v1 .equals (v2 )) {
565
+ return 0 ;
566
+ } else if (v1 .newerThan (v2 )) {
567
+ return 1 ;
568
+ } else {
569
+ return -1 ;
570
+ }
571
+ }
572
+ };
573
+ TrapClassVersion latestVersion = Collections .max (pairs , comparator ).snd ();
574
+
575
+ for (Pair <File , TrapClassVersion > p : pairs ) {
576
+ if (!latestVersion .equals (p .snd ())) {
577
+ File f = p .fst ();
578
+ File fOld = new File (f .getParentFile (), f .getName ().replace (".trap.gz" , ".trap-old.gz" ));
579
+ // We aren't interested in whether or not this succeeds;
580
+ // it may fail because a concurrent extractor has already
581
+ // renamed it.
582
+ f .renameTo (fOld );
583
+ }
584
+ }
585
+ }
586
+ }
454
587
}
455
588
}
456
589
@@ -505,6 +638,17 @@ private TrapClassVersion(int majorVersion, int minorVersion, long lastModified,
505
638
this .lastModified = lastModified ;
506
639
this .extractorName = extractorName ;
507
640
}
641
+
642
+ @ Override
643
+ public boolean equals (Object obj ) {
644
+ if (obj instanceof TrapClassVersion ) {
645
+ TrapClassVersion other = (TrapClassVersion )obj ;
646
+ return majorVersion == other .majorVersion && minorVersion == other .minorVersion && lastModified == other .lastModified && extractorName .equals (other .extractorName );
647
+ } else {
648
+ return false ;
649
+ }
650
+ }
651
+
508
652
private boolean newerThan (TrapClassVersion tcv ) {
509
653
// Classes being compiled from source have major version 0 but should take precedence
510
654
// over any classes with the same qualified name loaded from the classpath
0 commit comments