1
1
import type { WriteStream } from "fs" ;
2
- import { createWriteStream , mkdtemp , pathExists , remove } from "fs-extra" ;
2
+ import {
3
+ createWriteStream ,
4
+ mkdtemp ,
5
+ outputJson ,
6
+ pathExists ,
7
+ readJson ,
8
+ remove ,
9
+ } from "fs-extra" ;
3
10
import { tmpdir } from "os" ;
4
11
import { delimiter , dirname , join } from "path" ;
5
12
import { Range , satisfies } from "semver" ;
@@ -19,7 +26,9 @@ import {
19
26
InvocationRateLimiter ,
20
27
InvocationRateLimiterResultKind ,
21
28
} from "../common/invocation-rate-limiter" ;
29
+ import type { NotificationLogger } from "../common/logging" ;
22
30
import {
31
+ showAndLogExceptionWithTelemetry ,
23
32
showAndLogErrorMessage ,
24
33
showAndLogWarningMessage ,
25
34
} from "../common/logging" ;
@@ -28,6 +37,11 @@ import { reportUnzipProgress } from "../common/vscode/unzip-progress";
28
37
import type { Release } from "./distribution/release" ;
29
38
import { ReleasesApiConsumer } from "./distribution/releases-api-consumer" ;
30
39
import { createTimeoutSignal } from "../common/fetch-stream" ;
40
+ import { withDistributionUpdateLock } from "./lock" ;
41
+ import { asError , getErrorMessage } from "../common/helpers-pure" ;
42
+ import { isIOError } from "../common/files" ;
43
+ import { telemetryListener } from "../common/vscode/telemetry" ;
44
+ import { redactableError } from "../common/errors" ;
31
45
32
46
/**
33
47
* distribution.ts
@@ -53,6 +67,11 @@ const NIGHTLY_DISTRIBUTION_REPOSITORY_NWO = "dsp-testing/codeql-cli-nightlies";
53
67
*/
54
68
export const DEFAULT_DISTRIBUTION_VERSION_RANGE : Range = new Range ( "2.x" ) ;
55
69
70
+ export interface DistributionState {
71
+ folderIndex : number ;
72
+ release : Release | null ;
73
+ }
74
+
56
75
export interface DistributionProvider {
57
76
getCodeQlPathWithoutVersionCheck ( ) : Promise < string | undefined > ;
58
77
onDidChangeDistribution ?: Event < void > ;
@@ -64,13 +83,15 @@ export class DistributionManager implements DistributionProvider {
64
83
public readonly config : DistributionConfig ,
65
84
private readonly versionRange : Range ,
66
85
extensionContext : ExtensionContext ,
86
+ logger : NotificationLogger ,
67
87
) {
68
88
this . _onDidChangeDistribution = config . onDidChangeConfiguration ;
69
89
this . extensionSpecificDistributionManager =
70
90
new ExtensionSpecificDistributionManager (
71
91
config ,
72
92
versionRange ,
73
93
extensionContext ,
94
+ logger ,
74
95
) ;
75
96
this . updateCheckRateLimiter = new InvocationRateLimiter (
76
97
extensionContext . globalState ,
@@ -80,6 +101,10 @@ export class DistributionManager implements DistributionProvider {
80
101
) ;
81
102
}
82
103
104
+ public async initialize ( ) : Promise < void > {
105
+ await this . extensionSpecificDistributionManager . initialize ( ) ;
106
+ }
107
+
83
108
/**
84
109
* Look up a CodeQL launcher binary.
85
110
*/
@@ -280,14 +305,58 @@ export class DistributionManager implements DistributionProvider {
280
305
}
281
306
282
307
class ExtensionSpecificDistributionManager {
308
+ private distributionState : DistributionState | undefined ;
309
+
283
310
constructor (
284
311
private readonly config : DistributionConfig ,
285
312
private readonly versionRange : Range ,
286
313
private readonly extensionContext : ExtensionContext ,
314
+ private readonly logger : NotificationLogger ,
287
315
) {
288
316
/**/
289
317
}
290
318
319
+ public async initialize ( ) {
320
+ await this . ensureDistributionStateExists ( ) ;
321
+ }
322
+
323
+ private async ensureDistributionStateExists ( ) {
324
+ const distributionStatePath = this . getDistributionStatePath ( ) ;
325
+ try {
326
+ this . distributionState = await readJson ( distributionStatePath ) ;
327
+ } catch ( e : unknown ) {
328
+ if ( isIOError ( e ) && e . code === "ENOENT" ) {
329
+ // If the file doesn't exist, that just means we need to create it
330
+
331
+ this . distributionState = {
332
+ folderIndex :
333
+ this . extensionContext . globalState . get (
334
+ "distributionFolderIndex" ,
335
+ 0 ,
336
+ ) ?? 0 ,
337
+ release : ( this . extensionContext . globalState . get (
338
+ "distributionRelease" ,
339
+ ) ?? null ) as Release | null ,
340
+ } ;
341
+
342
+ // This may result in a race condition, but when this happens both processes should write the same file.
343
+ await outputJson ( distributionStatePath , this . distributionState ) ;
344
+ } else {
345
+ void showAndLogExceptionWithTelemetry (
346
+ this . logger ,
347
+ telemetryListener ,
348
+ redactableError (
349
+ asError ( e ) ,
350
+ ) `Failed to read distribution state from ${ distributionStatePath } : ${ getErrorMessage ( e ) } ` ,
351
+ ) ;
352
+ this . distributionState = {
353
+ folderIndex : 0 ,
354
+ release : null ,
355
+ } ;
356
+ }
357
+ }
358
+ }
359
+
291
360
public async getCodeQlPathWithoutVersionCheck ( ) : Promise < string | undefined > {
292
361
if ( this . getInstalledRelease ( ) !== undefined ) {
293
362
// An extension specific distribution has been installed.
@@ -350,9 +419,21 @@ class ExtensionSpecificDistributionManager {
350
419
release : Release ,
351
420
progressCallback ?: ProgressCallback ,
352
421
) : Promise < void > {
353
- await this . downloadDistribution ( release , progressCallback ) ;
354
- // Store the installed release within the global extension state.
355
- await this . storeInstalledRelease ( release ) ;
422
+ if ( ! this . distributionState ) {
423
+ await this . ensureDistributionStateExists ( ) ;
424
+ }
425
+
426
+ const distributionStatePath = this . getDistributionStatePath ( ) ;
427
+
428
+ await withDistributionUpdateLock (
429
+ // .lock will be appended to this filename
430
+ distributionStatePath ,
431
+ async ( ) => {
432
+ await this . downloadDistribution ( release , progressCallback ) ;
433
+ // Store the installed release within the global extension state.
434
+ await this . storeInstalledRelease ( release ) ;
435
+ } ,
436
+ ) ;
356
437
}
357
438
358
439
private async downloadDistribution (
@@ -564,23 +645,19 @@ class ExtensionSpecificDistributionManager {
564
645
}
565
646
566
647
private async bumpDistributionFolderIndex ( ) : Promise < void > {
567
- const index = this . extensionContext . globalState . get (
568
- ExtensionSpecificDistributionManager . _currentDistributionFolderIndexStateKey ,
569
- 0 ,
570
- ) ;
571
- await this . extensionContext . globalState . update (
572
- ExtensionSpecificDistributionManager . _currentDistributionFolderIndexStateKey ,
573
- index + 1 ,
574
- ) ;
648
+ await this . updateState ( ( oldState ) => {
649
+ return {
650
+ ...oldState ,
651
+ folderIndex : ( oldState . folderIndex ?? 0 ) + 1 ,
652
+ } ;
653
+ } ) ;
575
654
}
576
655
577
656
private getDistributionStoragePath ( ) : string {
657
+ const distributionState = this . getDistributionState ( ) ;
658
+
578
659
// Use an empty string for the initial distribution for backwards compatibility.
579
- const distributionFolderIndex =
580
- this . extensionContext . globalState . get (
581
- ExtensionSpecificDistributionManager . _currentDistributionFolderIndexStateKey ,
582
- 0 ,
583
- ) || "" ;
660
+ const distributionFolderIndex = distributionState . folderIndex || "" ;
584
661
return join (
585
662
this . extensionContext . globalStorageUri . fsPath ,
586
663
ExtensionSpecificDistributionManager . _currentDistributionFolderBaseName +
@@ -595,26 +672,55 @@ class ExtensionSpecificDistributionManager {
595
672
) ;
596
673
}
597
674
598
- private getInstalledRelease ( ) : Release | undefined {
599
- return this . extensionContext . globalState . get (
600
- ExtensionSpecificDistributionManager . _installedReleaseStateKey ,
675
+ private getDistributionStatePath ( ) : string {
676
+ return join (
677
+ this . extensionContext . globalStorageUri . fsPath ,
678
+ ExtensionSpecificDistributionManager . _distributionStateFilename ,
601
679
) ;
602
680
}
603
681
682
+ private getInstalledRelease ( ) : Release | undefined {
683
+ return this . getDistributionState ( ) . release ?? undefined ;
684
+ }
685
+
604
686
private async storeInstalledRelease (
605
687
release : Release | undefined ,
606
688
) : Promise < void > {
607
- await this . extensionContext . globalState . update (
608
- ExtensionSpecificDistributionManager . _installedReleaseStateKey ,
609
- release ,
610
- ) ;
689
+ await this . updateState ( ( oldState ) => ( {
690
+ ...oldState ,
691
+ release : release ?? null ,
692
+ } ) ) ;
693
+ }
694
+
695
+ private getDistributionState ( ) : DistributionState {
696
+ const distributionState = this . distributionState ;
697
+ if ( distributionState === undefined ) {
698
+ throw new Error (
699
+ "Invariant violation: distribution state not initialized" ,
700
+ ) ;
701
+ }
702
+ return distributionState ;
703
+ }
704
+
705
+ private async updateState (
706
+ f : ( oldState : DistributionState ) => DistributionState ,
707
+ ) {
708
+ const oldState = this . distributionState ;
709
+ if ( oldState === undefined ) {
710
+ throw new Error (
711
+ "Invariant violation: distribution state not initialized" ,
712
+ ) ;
713
+ }
714
+ const newState = f ( oldState ) ;
715
+ this . distributionState = newState ;
716
+
717
+ const distributionStatePath = this . getDistributionStatePath ( ) ;
718
+ await outputJson ( distributionStatePath , newState ) ;
611
719
}
612
720
613
721
private static readonly _currentDistributionFolderBaseName = "distribution" ;
614
- private static readonly _currentDistributionFolderIndexStateKey =
615
- "distributionFolderIndex" ;
616
- private static readonly _installedReleaseStateKey = "distributionRelease" ;
617
722
private static readonly _codeQlExtractedFolderName = "codeql" ;
723
+ private static readonly _distributionStateFilename = "distribution.json" ;
618
724
}
619
725
620
726
/*
0 commit comments