Skip to content

Commit f19b0df

Browse files
authored
Merge pull request #3762 from github/koesie10/fix-codeql-download
Store state of CodeQL distribution on filesystem instead of in `globalState`
2 parents 5b69404 + 8a7f710 commit f19b0df

File tree

7 files changed

+573
-28
lines changed

7 files changed

+573
-28
lines changed

extensions/ql-vscode/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## [UNRELEASED]
44

55
- Support result columns of type `QlBuiltins::BigInt` in quick evaluations. [#3647](https://github.com/github/vscode-codeql/pull/3647)
6+
- Fix a bug where the CodeQL CLI would be re-downloaded if you switched to a different filesystem (for example Codespaces or a remote SSH host). [#3762](https://github.com/github/vscode-codeql/pull/3762)
67

78
## 1.16.0 - 10 October 2024
89

extensions/ql-vscode/package-lock.json

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/ql-vscode/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1986,6 +1986,7 @@
19861986
"msw": "^2.2.13",
19871987
"nanoid": "^5.0.7",
19881988
"p-queue": "^8.0.1",
1989+
"proper-lockfile": "^4.1.2",
19891990
"react": "^18.3.1",
19901991
"react-dom": "^18.3.1",
19911992
"semver": "^7.6.2",
@@ -2040,6 +2041,7 @@
20402041
"@types/js-yaml": "^4.0.6",
20412042
"@types/nanoid": "^3.0.0",
20422043
"@types/node": "20.16.*",
2044+
"@types/proper-lockfile": "^4.1.4",
20432045
"@types/react": "^18.3.1",
20442046
"@types/react-dom": "^18.3.0",
20452047
"@types/sarif": "^2.1.2",

extensions/ql-vscode/src/codeql-cli/distribution.ts

Lines changed: 133 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
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";
310
import { tmpdir } from "os";
411
import { delimiter, dirname, join } from "path";
512
import { 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";
2230
import {
31+
showAndLogExceptionWithTelemetry,
2332
showAndLogErrorMessage,
2433
showAndLogWarningMessage,
2534
} from "../common/logging";
@@ -28,6 +37,11 @@ import { reportUnzipProgress } from "../common/vscode/unzip-progress";
2837
import type { Release } from "./distribution/release";
2938
import { ReleasesApiConsumer } from "./distribution/releases-api-consumer";
3039
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";
3145

3246
/**
3347
* distribution.ts
@@ -53,6 +67,11 @@ const NIGHTLY_DISTRIBUTION_REPOSITORY_NWO = "dsp-testing/codeql-cli-nightlies";
5367
*/
5468
export const DEFAULT_DISTRIBUTION_VERSION_RANGE: Range = new Range("2.x");
5569

70+
export interface DistributionState {
71+
folderIndex: number;
72+
release: Release | null;
73+
}
74+
5675
export 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

282307
class 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
/*
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { lock } from "proper-lockfile";
2+
3+
export async function withDistributionUpdateLock(
4+
lockFile: string,
5+
f: () => Promise<void>,
6+
) {
7+
const release = await lock(lockFile, {
8+
stale: 60_000, // 1 minute. We can take the lock longer than this because that's based on the update interval.
9+
update: 10_000, // 10 seconds
10+
retries: {
11+
minTimeout: 10_000,
12+
maxTimeout: 60_000,
13+
retries: 100,
14+
},
15+
});
16+
17+
try {
18+
await f();
19+
} finally {
20+
await release();
21+
}
22+
}

0 commit comments

Comments
 (0)