11import 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" ;
310import { tmpdir } from "os" ;
411import { delimiter , dirname , join } from "path" ;
512import { Range , satisfies } from "semver" ;
@@ -19,7 +26,9 @@ import {
1926 InvocationRateLimiter ,
2027 InvocationRateLimiterResultKind ,
2128} from "../common/invocation-rate-limiter" ;
29+ import type { NotificationLogger } from "../common/logging" ;
2230import {
31+ showAndLogExceptionWithTelemetry ,
2332 showAndLogErrorMessage ,
2433 showAndLogWarningMessage ,
2534} from "../common/logging" ;
@@ -28,6 +37,11 @@ import { reportUnzipProgress } from "../common/vscode/unzip-progress";
2837import type { Release } from "./distribution/release" ;
2938import { ReleasesApiConsumer } from "./distribution/releases-api-consumer" ;
3039import { 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" ;
3145
3246/**
3347 * distribution.ts
@@ -53,6 +67,11 @@ const NIGHTLY_DISTRIBUTION_REPOSITORY_NWO = "dsp-testing/codeql-cli-nightlies";
5367 */
5468export const DEFAULT_DISTRIBUTION_VERSION_RANGE : Range = new Range ( "2.x" ) ;
5569
70+ export interface DistributionState {
71+ folderIndex : number ;
72+ release : Release | null ;
73+ }
74+
5675export interface DistributionProvider {
5776 getCodeQlPathWithoutVersionCheck ( ) : Promise < string | undefined > ;
5877 onDidChangeDistribution ?: Event < void > ;
@@ -64,13 +83,15 @@ export class DistributionManager implements DistributionProvider {
6483 public readonly config : DistributionConfig ,
6584 private readonly versionRange : Range ,
6685 extensionContext : ExtensionContext ,
86+ logger : NotificationLogger ,
6787 ) {
6888 this . _onDidChangeDistribution = config . onDidChangeConfiguration ;
6989 this . extensionSpecificDistributionManager =
7090 new ExtensionSpecificDistributionManager (
7191 config ,
7292 versionRange ,
7393 extensionContext ,
94+ logger ,
7495 ) ;
7596 this . updateCheckRateLimiter = new InvocationRateLimiter (
7697 extensionContext . globalState ,
@@ -80,6 +101,10 @@ export class DistributionManager implements DistributionProvider {
80101 ) ;
81102 }
82103
104+ public async initialize ( ) : Promise < void > {
105+ await this . extensionSpecificDistributionManager . initialize ( ) ;
106+ }
107+
83108 /**
84109 * Look up a CodeQL launcher binary.
85110 */
@@ -280,14 +305,58 @@ export class DistributionManager implements DistributionProvider {
280305}
281306
282307class ExtensionSpecificDistributionManager {
308+ private distributionState : DistributionState | undefined ;
309+
283310 constructor (
284311 private readonly config : DistributionConfig ,
285312 private readonly versionRange : Range ,
286313 private readonly extensionContext : ExtensionContext ,
314+ private readonly logger : NotificationLogger ,
287315 ) {
288316 /**/
289317 }
290318
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+
291360 public async getCodeQlPathWithoutVersionCheck ( ) : Promise < string | undefined > {
292361 if ( this . getInstalledRelease ( ) !== undefined ) {
293362 // An extension specific distribution has been installed.
@@ -350,9 +419,21 @@ class ExtensionSpecificDistributionManager {
350419 release : Release ,
351420 progressCallback ?: ProgressCallback ,
352421 ) : 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+ ) ;
356437 }
357438
358439 private async downloadDistribution (
@@ -564,23 +645,19 @@ class ExtensionSpecificDistributionManager {
564645 }
565646
566647 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+ } ) ;
575654 }
576655
577656 private getDistributionStoragePath ( ) : string {
657+ const distributionState = this . getDistributionState ( ) ;
658+
578659 // 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 || "" ;
584661 return join (
585662 this . extensionContext . globalStorageUri . fsPath ,
586663 ExtensionSpecificDistributionManager . _currentDistributionFolderBaseName +
@@ -595,26 +672,55 @@ class ExtensionSpecificDistributionManager {
595672 ) ;
596673 }
597674
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 ,
601679 ) ;
602680 }
603681
682+ private getInstalledRelease ( ) : Release | undefined {
683+ return this . getDistributionState ( ) . release ?? undefined ;
684+ }
685+
604686 private async storeInstalledRelease (
605687 release : Release | undefined ,
606688 ) : 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 ) ;
611719 }
612720
613721 private static readonly _currentDistributionFolderBaseName = "distribution" ;
614- private static readonly _currentDistributionFolderIndexStateKey =
615- "distributionFolderIndex" ;
616- private static readonly _installedReleaseStateKey = "distributionRelease" ;
617722 private static readonly _codeQlExtractedFolderName = "codeql" ;
723+ private static readonly _distributionStateFilename = "distribution.json" ;
618724}
619725
620726/*
0 commit comments