Skip to content

Commit b9a16b3

Browse files
committed
Resolve database lock contention w/ vscode-codeql
Resolves #117 Fixes a known compatibility issue for databases added, and therefore locked, via the GitHub.vscode-codeql extension. The vscode-codeql query server creates .lock files in the cache directory of every registered CodeQL database, preventing the ql-mcp server from running CLI commands (codeql_query_run, codeql_database_analyze) against those same databases. Add a DatabaseCopier that syncs databases from vscode-codeql storage into a managed directory under the `vscode-codeql-development-mcp-server` extension's globalStorage, stripping .lock files from the copy. The EnvironmentBuilder now sets CODEQL_DATABASES_BASE_DIRS to this managed directory by default (configurable via codeql-mcp.copyDatabases). - New DatabaseCopier class with incremental sync (skips unchanged databases) - StoragePaths.getManagedDatabaseStoragePath() for the managed databases/ dir - EnvironmentBuilder accepts injectable DatabaseCopierFactory for testability - codeql-mcp.copyDatabases setting (default: true) - 11 unit tests for DatabaseCopier (real filesystem operations) - 15 unit tests for EnvironmentBuilder (updated for copy mode + fallback) - 3 bridge integration tests (managed dir structure, no .lock files) - 4 E2E integration tests: inject .lock → copy → codeql_query_run + codeql_database_analyze succeed against the lock-free copy
1 parent bb2cc42 commit b9a16b3

11 files changed

+886
-48
lines changed

extensions/vscode/esbuild.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const testSuiteConfig = {
3535
entryPoints: [
3636
'test/suite/index.ts',
3737
'test/suite/bridge.integration.test.ts',
38+
'test/suite/copydb-e2e.integration.test.ts',
3839
'test/suite/extension.integration.test.ts',
3940
'test/suite/mcp-resource-e2e.integration.test.ts',
4041
'test/suite/mcp-server.integration.test.ts',

extensions/vscode/package.json

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -48,33 +48,13 @@
4848
"configuration": {
4949
"title": "CodeQL MCP Server",
5050
"properties": {
51-
"codeql-mcp.autoInstall": {
52-
"type": "boolean",
53-
"default": true,
54-
"description": "Automatically install and update the CodeQL Development MCP Server on activation."
55-
},
56-
"codeql-mcp.serverVersion": {
57-
"type": "string",
58-
"default": "latest",
59-
"description": "The npm version of codeql-development-mcp-server to install. Use 'latest' for the most recent release."
60-
},
61-
"codeql-mcp.serverCommand": {
62-
"type": "string",
63-
"default": "node",
64-
"description": "Command to launch the MCP server. The default 'node' runs the bundled server. Override to 'npx' to download from npm, or provide a custom path."
65-
},
66-
"codeql-mcp.serverArgs": {
51+
"codeql-mcp.additionalDatabaseDirs": {
6752
"type": "array",
6853
"items": {
6954
"type": "string"
7055
},
7156
"default": [],
72-
"description": "Custom arguments for the MCP server command. When empty, the bundled server entry point is used automatically. Set to e.g. ['/path/to/server/dist/codeql-development-mcp-server.js'] for local development."
73-
},
74-
"codeql-mcp.watchCodeqlExtension": {
75-
"type": "boolean",
76-
"default": true,
77-
"description": "Watch for CodeQL databases and query results created by the CodeQL extension."
57+
"description": "Additional directories to search for CodeQL databases. Appended to CODEQL_DATABASES_BASE_DIRS alongside the vscode-codeql storage paths."
7858
},
7959
"codeql-mcp.additionalEnv": {
8060
"type": "object",
@@ -84,29 +64,54 @@
8464
"type": "string"
8565
}
8666
},
87-
"codeql-mcp.additionalDatabaseDirs": {
67+
"codeql-mcp.additionalMrvaRunResultsDirs": {
8868
"type": "array",
8969
"items": {
9070
"type": "string"
9171
},
9272
"default": [],
93-
"description": "Additional directories to search for CodeQL databases. Appended to CODEQL_DATABASES_BASE_DIRS alongside the vscode-codeql storage paths."
73+
"description": "Additional directories containing MRVA run result subdirectories. Appended to CODEQL_MRVA_RUN_RESULTS_DIRS alongside the vscode-codeql variant analysis storage path."
9474
},
95-
"codeql-mcp.additionalMrvaRunResultsDirs": {
75+
"codeql-mcp.additionalQueryRunResultsDirs": {
9676
"type": "array",
9777
"items": {
9878
"type": "string"
9979
},
10080
"default": [],
101-
"description": "Additional directories containing MRVA run result subdirectories. Appended to CODEQL_MRVA_RUN_RESULTS_DIRS alongside the vscode-codeql variant analysis storage path."
81+
"description": "Additional directories containing query run result subdirectories. Appended to CODEQL_QUERY_RUN_RESULTS_DIRS alongside the vscode-codeql query storage path."
10282
},
103-
"codeql-mcp.additionalQueryRunResultsDirs": {
83+
"codeql-mcp.autoInstall": {
84+
"type": "boolean",
85+
"default": true,
86+
"description": "Automatically install and update the CodeQL Development MCP Server on activation."
87+
},
88+
"codeql-mcp.copyDatabases": {
89+
"type": "boolean",
90+
"default": true,
91+
"markdownDescription": "Copy CodeQL databases from the `GitHub.vscode-codeql` extension storage into a managed directory, removing query-server lock files so the MCP server CLI can operate without contention. Disable to use databases in-place (may fail when the CodeQL query server is running)."
92+
},
93+
"codeql-mcp.serverArgs": {
10494
"type": "array",
10595
"items": {
10696
"type": "string"
10797
},
10898
"default": [],
109-
"description": "Additional directories containing query run result subdirectories. Appended to CODEQL_QUERY_RUN_RESULTS_DIRS alongside the vscode-codeql query storage path."
99+
"description": "Custom arguments for the MCP server command. When empty, the bundled server entry point is used automatically. Set to e.g. ['/path/to/server/dist/codeql-development-mcp-server.js'] for local development."
100+
},
101+
"codeql-mcp.serverCommand": {
102+
"type": "string",
103+
"default": "node",
104+
"description": "Command to launch the MCP server. The default 'node' runs the bundled server. Override to 'npx' to download from npm, or provide a custom path."
105+
},
106+
"codeql-mcp.serverVersion": {
107+
"type": "string",
108+
"default": "latest",
109+
"description": "The npm version of codeql-development-mcp-server to install. Use 'latest' for the most recent release."
110+
},
111+
"codeql-mcp.watchCodeqlExtension": {
112+
"type": "boolean",
113+
"default": true,
114+
"description": "Watch for CodeQL databases and query results created by the CodeQL extension."
110115
}
111116
}
112117
},
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync } from 'fs';
2+
import { join } from 'path';
3+
import type { Logger } from '../common/logger';
4+
5+
/**
6+
* Copies CodeQL databases from `GitHub.vscode-codeql` extension storage
7+
* to a managed directory, removing `.lock` files that the CodeQL query
8+
* server creates in `<dataset>/default/cache/`.
9+
*
10+
* This avoids lock contention when the `ql-mcp` server runs CLI commands
11+
* against databases that are simultaneously registered by the
12+
* `vscode-codeql` query server.
13+
*
14+
* Each database is identified by its top-level directory name (which
15+
* contains `codeql-database.yml`). A database is only re-copied when its
16+
* source has been modified more recently than the existing copy.
17+
*/
18+
export class DatabaseCopier {
19+
constructor(
20+
private readonly destinationBase: string,
21+
private readonly logger: Logger,
22+
) {}
23+
24+
/**
25+
* Synchronise databases from one or more source directories into the
26+
* managed destination. Only databases that are newer than the existing
27+
* copy (or missing entirely) are re-copied.
28+
*
29+
* @returns The list of database paths in the managed destination that
30+
* are ready for use (absolute paths).
31+
*/
32+
syncAll(sourceDirs: string[]): string[] {
33+
mkdirSync(this.destinationBase, { recursive: true });
34+
35+
const copied: string[] = [];
36+
37+
for (const sourceDir of sourceDirs) {
38+
if (!existsSync(sourceDir)) {
39+
continue;
40+
}
41+
42+
let entries: string[];
43+
try {
44+
entries = readdirSync(sourceDir);
45+
} catch {
46+
continue;
47+
}
48+
49+
for (const entry of entries) {
50+
const srcDbPath = join(sourceDir, entry);
51+
if (!isCodeQLDatabase(srcDbPath)) {
52+
continue;
53+
}
54+
55+
const destDbPath = join(this.destinationBase, entry);
56+
57+
if (this.needsCopy(srcDbPath, destDbPath)) {
58+
this.copyDatabase(srcDbPath, destDbPath);
59+
}
60+
61+
copied.push(destDbPath);
62+
}
63+
}
64+
65+
return copied;
66+
}
67+
68+
/**
69+
* Copy a single database directory, then strip any `.lock` files that
70+
* the CodeQL query server may have left behind.
71+
*/
72+
private copyDatabase(src: string, dest: string): void {
73+
this.logger.info(`Copying database ${src}${dest}`);
74+
try {
75+
// Remove stale destination if present
76+
if (existsSync(dest)) {
77+
rmSync(dest, { recursive: true, force: true });
78+
}
79+
80+
cpSync(src, dest, { recursive: true });
81+
removeLockFiles(dest);
82+
this.logger.info(`Database copied successfully: ${dest}`);
83+
} catch (err) {
84+
this.logger.error(
85+
`Failed to copy database ${src}: ${err instanceof Error ? err.message : String(err)}`,
86+
);
87+
}
88+
}
89+
90+
/**
91+
* A copy is needed when the destination does not exist, or the source
92+
* `codeql-database.yml` is newer than the destination's.
93+
*/
94+
private needsCopy(src: string, dest: string): boolean {
95+
const destYml = join(dest, 'codeql-database.yml');
96+
if (!existsSync(destYml)) {
97+
return true;
98+
}
99+
100+
const srcYml = join(src, 'codeql-database.yml');
101+
try {
102+
const srcMtime = statSync(srcYml).mtimeMs;
103+
const destMtime = statSync(destYml).mtimeMs;
104+
return srcMtime > destMtime;
105+
} catch {
106+
// If stat fails, re-copy to be safe
107+
return true;
108+
}
109+
}
110+
}
111+
112+
/** Check whether a directory looks like a CodeQL database. */
113+
function isCodeQLDatabase(dirPath: string): boolean {
114+
try {
115+
return statSync(dirPath).isDirectory() && existsSync(join(dirPath, 'codeql-database.yml'));
116+
} catch {
117+
return false;
118+
}
119+
}
120+
121+
/**
122+
* Recursively remove all `.lock` files under the given directory.
123+
* These are empty sentinel files created by the CodeQL query server in
124+
* `<dataset>/default/cache/.lock`.
125+
*/
126+
function removeLockFiles(dir: string): void {
127+
let entries: string[];
128+
try {
129+
entries = readdirSync(dir);
130+
} catch {
131+
return;
132+
}
133+
134+
for (const entry of entries) {
135+
const fullPath = join(dir, entry);
136+
try {
137+
const stat = statSync(fullPath);
138+
if (stat.isDirectory()) {
139+
removeLockFiles(fullPath);
140+
} else if (entry === '.lock') {
141+
unlinkSync(fullPath);
142+
}
143+
} catch {
144+
// Best-effort removal
145+
}
146+
}
147+
}

extensions/vscode/src/bridge/environment-builder.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import { DisposableObject } from '../common/disposable';
44
import type { Logger } from '../common/logger';
55
import type { CliResolver } from '../codeql/cli-resolver';
66
import type { StoragePaths } from './storage-paths';
7+
import { DatabaseCopier } from './database-copier';
8+
9+
/** Factory that creates a DatabaseCopier for a given destination. */
10+
export type DatabaseCopierFactory = (dest: string, logger: Logger) => DatabaseCopier;
11+
12+
const defaultCopierFactory: DatabaseCopierFactory = (dest, logger) =>
13+
new DatabaseCopier(dest, logger);
714

815
/**
916
* Assembles the environment variables for the MCP server process.
@@ -19,14 +26,17 @@ import type { StoragePaths } from './storage-paths';
1926
*/
2027
export class EnvironmentBuilder extends DisposableObject {
2128
private cachedEnv: Record<string, string> | null = null;
29+
private readonly copierFactory: DatabaseCopierFactory;
2230

2331
constructor(
2432
private readonly context: vscode.ExtensionContext,
2533
private readonly cliResolver: CliResolver,
2634
private readonly storagePaths: StoragePaths,
2735
private readonly logger: Logger,
36+
copierFactory?: DatabaseCopierFactory,
2837
) {
2938
super();
39+
this.copierFactory = copierFactory ?? defaultCopierFactory;
3040
}
3141

3242
/** Invalidate the cached environment so the next `build()` recomputes. */
@@ -83,9 +93,22 @@ export class EnvironmentBuilder extends DisposableObject {
8393

8494
// Database discovery directories for list_codeql_databases
8595
// Includes: global storage, workspace storage, and user-configured dirs
86-
const dbDirs = [...this.storagePaths.getAllDatabaseStoragePaths()];
96+
const sourceDirs = this.storagePaths.getAllDatabaseStoragePaths();
8797
const userDbDirs = config.get<string[]>('additionalDatabaseDirs', []);
88-
dbDirs.push(...userDbDirs);
98+
99+
// When copyDatabases is enabled, copy databases from vscode-codeql
100+
// storage to our own managed directory, removing query-server lock
101+
// files so the MCP server CLI can operate without contention.
102+
const copyEnabled = config.get<boolean>('copyDatabases', true);
103+
let dbDirs: string[];
104+
if (copyEnabled) {
105+
const managedDir = this.storagePaths.getManagedDatabaseStoragePath();
106+
const copier = this.copierFactory(managedDir, this.logger);
107+
copier.syncAll(sourceDirs);
108+
dbDirs = [managedDir, ...userDbDirs];
109+
} else {
110+
dbDirs = [...sourceDirs, ...userDbDirs];
111+
}
89112
env.CODEQL_DATABASES_BASE_DIRS = dbDirs.join(':');
90113

91114
// MRVA run results directory for variant analysis discovery

extensions/vscode/src/bridge/storage-paths.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ export class StoragePaths extends DisposableObject {
8888
return join(this.getCodeqlGlobalStoragePath(), 'variant-analyses');
8989
}
9090

91+
/**
92+
* Directory where the MCP extension stores lock-free copies of databases.
93+
* Path: `<our-globalStorageUri>/databases/`
94+
*/
95+
getManagedDatabaseStoragePath(): string {
96+
return join(this.context.globalStorageUri.fsPath, 'databases');
97+
}
98+
9199
/** The VS Code global storage root (parent of all extension storage dirs). */
92100
getGlobalStorageRoot(): string {
93101
return this.vsCodeGlobalStorageRoot;

0 commit comments

Comments
 (0)