33 createWriteStream ,
44 mkdtemp ,
55 pathExists ,
6+ readJson ,
67 remove ,
78 writeJson ,
89} from "fs-extra" ;
@@ -25,7 +26,9 @@ import {
2526 InvocationRateLimiter ,
2627 InvocationRateLimiterResultKind ,
2728} from "../common/invocation-rate-limiter" ;
29+ import type { NotificationLogger } from "../common/logging" ;
2830import {
31+ showAndLogExceptionWithTelemetry ,
2932 showAndLogErrorMessage ,
3033 showAndLogWarningMessage ,
3134} from "../common/logging" ;
@@ -35,6 +38,10 @@ import type { Release } from "./distribution/release";
3538import { ReleasesApiConsumer } from "./distribution/releases-api-consumer" ;
3639import { createTimeoutSignal } from "../common/fetch-stream" ;
3740import { 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" ;
3845
3946/**
4047 * distribution.ts
@@ -60,6 +67,11 @@ const NIGHTLY_DISTRIBUTION_REPOSITORY_NWO = "dsp-testing/codeql-cli-nightlies";
6067 */
6168export const DEFAULT_DISTRIBUTION_VERSION_RANGE : Range = new Range ( "2.x" ) ;
6269
70+ interface DistributionState {
71+ folderIndex : number ;
72+ release : Release | null ;
73+ }
74+
6375export interface DistributionProvider {
6476 getCodeQlPathWithoutVersionCheck ( ) : Promise < string | undefined > ;
6577 onDidChangeDistribution ?: Event < void > ;
@@ -71,13 +83,15 @@ export class DistributionManager implements DistributionProvider {
7183 public readonly config : DistributionConfig ,
7284 private readonly versionRange : Range ,
7385 extensionContext : ExtensionContext ,
86+ logger : NotificationLogger ,
7487 ) {
7588 this . _onDidChangeDistribution = config . onDidChangeConfiguration ;
7689 this . extensionSpecificDistributionManager =
7790 new ExtensionSpecificDistributionManager (
7891 config ,
7992 versionRange ,
8093 extensionContext ,
94+ logger ,
8195 ) ;
8296 this . updateCheckRateLimiter = new InvocationRateLimiter (
8397 extensionContext . globalState ,
@@ -87,6 +101,10 @@ export class DistributionManager implements DistributionProvider {
87101 ) ;
88102 }
89103
104+ public async initialize ( ) : Promise < void > {
105+ await this . extensionSpecificDistributionManager . initialize ( ) ;
106+ }
107+
90108 /**
91109 * Look up a CodeQL launcher binary.
92110 */
@@ -287,14 +305,53 @@ export class DistributionManager implements DistributionProvider {
287305}
288306
289307class ExtensionSpecificDistributionManager {
308+ private distributionState : DistributionState | undefined ;
309+
290310 constructor (
291311 private readonly config : DistributionConfig ,
292312 private readonly versionRange : Range ,
293313 private readonly extensionContext : ExtensionContext ,
314+ private readonly logger : NotificationLogger ,
294315 ) {
295316 /**/
296317 }
297318
319+ public async initialize ( ) {
320+ const distributionStatePath = this . getDistributionStatePath ( ) ;
321+ try {
322+ this . distributionState = await readJson ( distributionStatePath ) ;
323+ } catch ( e : unknown ) {
324+ if ( isIOError ( e ) && e . code === "ENOENT" ) {
325+ // If the file doesn't exist, that just means we need to create it
326+
327+ this . distributionState = {
328+ folderIndex : this . extensionContext . globalState . get (
329+ ExtensionSpecificDistributionManager . _currentDistributionFolderIndexStateKey ,
330+ 0 ,
331+ ) ,
332+ release : ( this . extensionContext . globalState . get (
333+ ExtensionSpecificDistributionManager . _installedReleaseStateKey ,
334+ ) ?? null ) as Release | null ,
335+ } ;
336+
337+ // This may result in a race condition, but when this happens both processes should write the same file.
338+ await writeJson ( distributionStatePath , this . distributionState ) ;
339+ } else {
340+ void showAndLogExceptionWithTelemetry (
341+ this . logger ,
342+ telemetryListener ,
343+ redactableError (
344+ asError ( e ) ,
345+ ) `Failed to read distribution state from ${ distributionStatePath } : ${ getErrorMessage ( e ) } ` ,
346+ ) ;
347+ this . distributionState = {
348+ folderIndex : 0 ,
349+ release : null ,
350+ } ;
351+ }
352+ }
353+ }
354+
298355 public async getCodeQlPathWithoutVersionCheck ( ) : Promise < string | undefined > {
299356 if ( this . getInstalledRelease ( ) !== undefined ) {
300357 // An extension specific distribution has been installed.
@@ -357,14 +414,7 @@ class ExtensionSpecificDistributionManager {
357414 release : Release ,
358415 progressCallback ?: ProgressCallback ,
359416 ) : Promise < void > {
360- const distributionStatePath = join (
361- this . extensionContext . globalStorageUri . fsPath ,
362- ExtensionSpecificDistributionManager . _distributionStateFilename ,
363- ) ;
364- if ( ! ( await pathExists ( distributionStatePath ) ) ) {
365- // This may result in a race condition, but when this happens both processes should write the same file.
366- await writeJson ( distributionStatePath , { } ) ;
367- }
417+ const distributionStatePath = this . getDistributionStatePath ( ) ;
368418
369419 await withDistributionUpdateLock (
370420 // .lock will be appended to this filename
@@ -586,23 +636,19 @@ class ExtensionSpecificDistributionManager {
586636 }
587637
588638 private async bumpDistributionFolderIndex ( ) : Promise < void > {
589- const index = this . extensionContext . globalState . get (
590- ExtensionSpecificDistributionManager . _currentDistributionFolderIndexStateKey ,
591- 0 ,
592- ) ;
593- await this . extensionContext . globalState . update (
594- ExtensionSpecificDistributionManager . _currentDistributionFolderIndexStateKey ,
595- index + 1 ,
596- ) ;
639+ await this . updateState ( ( oldState ) => {
640+ return {
641+ ...oldState ,
642+ folderIndex : oldState . folderIndex + 1 ,
643+ } ;
644+ } ) ;
597645 }
598646
599647 private getDistributionStoragePath ( ) : string {
648+ const distributionState = this . getDistributionState ( ) ;
649+
600650 // Use an empty string for the initial distribution for backwards compatibility.
601- const distributionFolderIndex =
602- this . extensionContext . globalState . get (
603- ExtensionSpecificDistributionManager . _currentDistributionFolderIndexStateKey ,
604- 0 ,
605- ) || "" ;
651+ const distributionFolderIndex = distributionState . folderIndex || "" ;
606652 return join (
607653 this . extensionContext . globalStorageUri . fsPath ,
608654 ExtensionSpecificDistributionManager . _currentDistributionFolderBaseName +
@@ -617,19 +663,50 @@ class ExtensionSpecificDistributionManager {
617663 ) ;
618664 }
619665
620- private getInstalledRelease ( ) : Release | undefined {
621- return this . extensionContext . globalState . get (
622- ExtensionSpecificDistributionManager . _installedReleaseStateKey ,
666+ private getDistributionStatePath ( ) : string {
667+ return join (
668+ this . extensionContext . globalStorageUri . fsPath ,
669+ ExtensionSpecificDistributionManager . _distributionStateFilename ,
623670 ) ;
624671 }
625672
673+ private getInstalledRelease ( ) : Release | undefined {
674+ return this . getDistributionState ( ) . release ?? undefined ;
675+ }
676+
626677 private async storeInstalledRelease (
627678 release : Release | undefined ,
628679 ) : Promise < void > {
629- await this . extensionContext . globalState . update (
630- ExtensionSpecificDistributionManager . _installedReleaseStateKey ,
631- release ,
632- ) ;
680+ await this . updateState ( ( oldState ) => ( {
681+ ...oldState ,
682+ release : release ?? null ,
683+ } ) ) ;
684+ }
685+
686+ private getDistributionState ( ) : DistributionState {
687+ const distributionState = this . distributionState ;
688+ if ( distributionState === undefined ) {
689+ throw new Error (
690+ "Invariant violation: distribution state not initialized" ,
691+ ) ;
692+ }
693+ return distributionState ;
694+ }
695+
696+ private async updateState (
697+ f : ( oldState : DistributionState ) => DistributionState ,
698+ ) {
699+ const oldState = this . distributionState ;
700+ if ( oldState === undefined ) {
701+ throw new Error (
702+ "Invariant violation: distribution state not initialized" ,
703+ ) ;
704+ }
705+ const newState = f ( oldState ) ;
706+ this . distributionState = newState ;
707+
708+ const distributionStatePath = this . getDistributionStatePath ( ) ;
709+ await writeJson ( distributionStatePath , newState ) ;
633710 }
634711
635712 private static readonly _currentDistributionFolderBaseName = "distribution" ;
0 commit comments