Skip to content

Commit

Permalink
Ensure databases are re-registered when query server restarts
Browse files Browse the repository at this point in the history
This commit fixes #733. It does it by ensuring that the query server
emits an event when it restarts the query server. The database manager
listens for this even and properly re-registers its databases.

A few caveats though:

1. Convert query restarts to using a command that includes progress.
   This will ensure that errors on restart are logged properly.
2. Because we want to log errors, we cannot use the vscode standard
   EventEmitters. They run in the next tick and therefore any errors
   will not be associated with this command execution.
3. Update the default cli version to run integration tests against to
   2.4.2.
4. Add a new integration test that fails if databases are not
   re-registered.
  • Loading branch information
aeisenberg committed Jan 29, 2021
1 parent 89b8605 commit 2ec2f15
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
version: ['v2.2.6', 'v2.3.3', 'v2.4.0']
version: ['v2.2.6', 'v2.3.3', 'v2.4.2']
env:
CLI_VERSION: ${{ matrix.version }}
TEST_CODEQL_PATH: '${{ github.workspace }}/codeql'
Expand Down
2 changes: 2 additions & 0 deletions extensions/ql-vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [UNRELEASED]

- Fix bug where databases are not reregistered when the query server restarts. [#734](https://github.com/github/vscode-codeql/pull/734)

## 1.3.10 - 20 January 2021

- Include the full stack in error log messages to help with debugging. [#726](https://github.com/github/vscode-codeql/pull/726)
Expand Down
21 changes: 20 additions & 1 deletion extensions/ql-vscode/src/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,10 @@ export class DatabaseManager extends DisposableObject {
) {
super();

this.loadPersistedState(); // Let this run async.
qs.onDidStartQueryServer(this.reregisterDatabases.bind(this));

// Let this run async.
this.loadPersistedState();
}

public async openDatabase(
Expand Down Expand Up @@ -542,6 +545,22 @@ export class DatabaseManager extends DisposableObject {
return databaseItem;
}

private async reregisterDatabases(
progress: ProgressCallback,
token: vscode.CancellationToken
) {
let completed = 0;
await Promise.all(this._databaseItems.map(async (databaseItem) => {
await this.registerDatabase(progress, token, databaseItem);
completed++;
progress({
maxStep: this._databaseItems.length,
step: completed,
message: 'Re-registering databases'
});
}));
}

private async addDatabaseSourceArchiveFolder(item: DatabaseItem) {
// The folder may already be in workspace state from a previous
// session. If not, add it.
Expand Down
24 changes: 16 additions & 8 deletions extensions/ql-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,28 +594,36 @@ async function activateWithInstalledDistribution(
);

ctx.subscriptions.push(
commandRunner('codeQL.restartQueryServer', async () => {
await qs.restartQueryServer();
commandRunnerWithProgress('codeQL.restartQueryServer', async (
progress: ProgressCallback,
token: CancellationToken
) => {
await qs.restartQueryServer(progress, token);
helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', {
outputLogger: queryServerLogger,
});
}, {
title: 'Restarting Query Server'
})
);

ctx.subscriptions.push(
commandRunner('codeQL.chooseDatabaseFolder', (
commandRunnerWithProgress('codeQL.chooseDatabaseFolder', (
progress: ProgressCallback,
token: CancellationToken
) =>
databaseUI.handleChooseDatabaseFolder(progress, token)
)
databaseUI.handleChooseDatabaseFolder(progress, token), {
title: 'Choose a Database from a Folder'
})
);
ctx.subscriptions.push(
commandRunner('codeQL.chooseDatabaseArchive', (
commandRunnerWithProgress('codeQL.chooseDatabaseArchive', (
progress: ProgressCallback,
token: CancellationToken
) =>
databaseUI.handleChooseDatabaseArchive(progress, token)
)
databaseUI.handleChooseDatabaseArchive(progress, token), {
title: 'Choose a Database from an Archive'
})
);
ctx.subscriptions.push(
commandRunnerWithProgress('codeQL.chooseDatabaseLgtm', (
Expand Down
33 changes: 26 additions & 7 deletions extensions/ql-vscode/src/queryserver-client.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import * as cp from 'child_process';
import * as path from 'path';
import { DisposableObject } from './vscode-utils/disposable-object';
import { Disposable } from 'vscode';
import { CancellationToken, createMessageConnection, MessageConnection, RequestType } from 'vscode-jsonrpc';
import { Disposable, CancellationToken, commands } from 'vscode';
import { createMessageConnection, MessageConnection, RequestType } from 'vscode-jsonrpc';
import * as cli from './cli';
import { QueryServerConfig } from './config';
import { Logger, ProgressReporter } from './logging';
import { completeQuery, EvaluationResult, progress, ProgressMessage, WithProgressId } from './pure/messages';
import * as messages from './pure/messages';
import { SemVer } from 'semver';
import { ProgressCallback, ProgressTask } from './commandRunner';

type ServerOpts = {
logger: Logger;
Expand Down Expand Up @@ -60,6 +61,16 @@ export class QueryServerClient extends DisposableObject {
nextCallback: number;
nextProgress: number;
withProgressReporting: WithProgressReporting;

private readonly queryServerStartListeners = [] as ProgressTask<void>[];

// Can't use standard vscode EventEmitter here since they do not cause the calling
// function to fail if one of the event handlers fail. This is something that
// we need here.
readonly onDidStartQueryServer = (e: ProgressTask<void>) => {
this.queryServerStartListeners.push(e);
}

public activeQueryName: string | undefined;

constructor(
Expand All @@ -71,10 +82,8 @@ export class QueryServerClient extends DisposableObject {
super();
// When the query server configuration changes, restart the query server.
if (config.onDidChangeConfiguration !== undefined) {
this.push(config.onDidChangeConfiguration(async () => {
this.logger.log('Restarting query server due to configuration changes...');
await this.restartQueryServer();
}, this));
this.push(config.onDidChangeConfiguration(() =>
commands.executeCommand('codeQL.restartQueryServer')));
}
this.withProgressReporting = withProgressReporting;
this.nextCallback = 0;
Expand All @@ -97,9 +106,19 @@ export class QueryServerClient extends DisposableObject {
}

/** Restarts the query server by disposing of the current server process and then starting a new one. */
async restartQueryServer(): Promise<void> {
async restartQueryServer(
progress: ProgressCallback,
token: CancellationToken
): Promise<void> {
this.stopQueryServer();
await this.startQueryServer();

// Ensure we await all responses from event handlers so that
// errors can be properly reported to the user.
await Promise.all(this.queryServerStartListeners.map(handler => handler(
progress,
token
)));
}

showLog(): void {
Expand Down
8 changes: 6 additions & 2 deletions extensions/ql-vscode/src/run-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ export async function compileAndRunQueryAgainstDatabase(

const query = new QueryInfo(qlProgram, db, packConfig.dbscheme, quickEvalPosition, metadata, templates);

const upgradeDir = await tmp.dir({ dir: upgradesTmpDir.name });
const upgradeDir = await tmp.dir({ dir: upgradesTmpDir.name, unsafeCleanup: true });
try {
let upgradeQlo;
if (await hasNondestructiveUpgradeCapabilities(qs)) {
Expand Down Expand Up @@ -615,7 +615,11 @@ export async function compileAndRunQueryAgainstDatabase(
return createSyntheticResult(query, db, historyItemOptions, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
}
} finally {
upgradeDir.cleanup();
try {
upgradeDir.cleanup();
} catch (e) {
qs.logger.log(`Could not clean up the upgrades dir. Reason: ${e.message || e}`);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fail } from 'assert';
import { CancellationToken, extensions, Uri } from 'vscode';
import { CancellationToken, commands, extensions, Uri } from 'vscode';
import * as sinon from 'sinon';
import * as path from 'path';
import * as fs from 'fs-extra';
Expand All @@ -14,6 +14,7 @@ import { compileAndRunQueryAgainstDatabase } from '../../run-queries';
import { CodeQLCliServer } from '../../cli';
import { QueryServerClient } from '../../queryserver-client';
import { skipIfNoCodeQL } from '../ensureCli';
import { QueryResultType } from '../../pure/messages';


/**
Expand Down Expand Up @@ -94,10 +95,35 @@ describe('Queries', function() {
// just check that the query was successful
expect(result.database.name).to.eq('db');
expect(result.options.queryText).to.eq(fs.readFileSync(queryPath, 'utf8'));
expect(result.result.resultType).to.eq(QueryResultType.SUCCESS);
} catch (e) {
console.error('Test Failed');
fail(e);
}
});

// Asserts a fix for bug https://github.com/github/vscode-codeql/issues/733
it('should restart the database and run a query', async () => {
try {
await commands.executeCommand('codeQL.restartQueryServer');
const queryPath = path.join(__dirname, 'data', 'simple-query.ql');
const result = await compileAndRunQueryAgainstDatabase(
cli,
qs,
dbItem,
false,
Uri.file(queryPath),
progress,
token
);

// this message would indicate that the databases were not properly reregistered
expect(result.result.message).not.to.eq('No result from server');
expect(result.options.queryText).to.eq(fs.readFileSync(queryPath, 'utf8'));
expect(result.result.resultType).to.eq(QueryResultType.SUCCESS);
} catch (e) {
console.error('Test Failed');
fail(e);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ class Checkpoint<T> {
constructor() {
this.res = () => { /**/ };
this.rej = () => { /**/ };
this.promise = new Promise((res, rej) => { this.res = res; this.rej = rej; });
this.promise = new Promise((res, rej) => {
this.res = res as () => {};
this.rej = rej;
});
}

async done(): Promise<T> {
Expand Down Expand Up @@ -81,6 +84,11 @@ const queryTestCases: QueryTestCase[] = [
}
];

const db: messages.Dataset = {
dbDir: path.join(__dirname, '../test-db'),
workingSet: 'default',
};

describe('using the query server', function() {
before(function() {
skipIfNoCodeQL(this);
Expand Down Expand Up @@ -120,6 +128,12 @@ describe('using the query server', function() {
const evaluationSucceeded = new Checkpoint<void>();
const parsedResults = new Checkpoint<void>();

it('should register the database if necessary', async () => {
if (await qs.supportsDatabaseRegistration()) {
await qs.sendRequest(messages.registerDatabases, { databases: [db] }, token, (() => { /**/ }) as any);
}
});

it(`should be able to compile query ${queryName}`, async function() {
await queryServerStarted.done();
expect(fs.existsSync(queryTestCase.queryPath)).to.be.true;
Expand Down Expand Up @@ -166,15 +180,11 @@ describe('using the query server', function() {
id: callbackId,
timeoutSecs: 1000,
};
const db: messages.Dataset = {
dbDir: path.join(__dirname, '../test-db'),
workingSet: 'default',
};
const params: messages.EvaluateQueriesParams = {
db,
evaluateId: callbackId,
queries: [queryToRun],
stopOnError: false,
stopOnError: true,
useSequenceHint: false
};
await qs.sendRequest(messages.runQueries, params, token, () => { /**/ });
Expand Down
7 changes: 5 additions & 2 deletions extensions/ql-vscode/src/vscode-tests/ensureCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@ import { workspace } from 'vscode';
process.on('unhandledRejection', e => {
console.error('Unhandled rejection.');
console.error(e);
process.exit(-1);
// Must use a setTimeout in order to ensure the log is fully flushed before exiting
setTimeout(() => {
process.exit(-1);
}, 2000);
});

const _1MB = 1024 * 1024;
const _10MB = _1MB * 10;

// CLI version to test. Hard code the latest as default. And be sure
// to update the env if it is not otherwise set.
const CLI_VERSION = process.env.CLI_VERSION || 'v2.4.0';
const CLI_VERSION = process.env.CLI_VERSION || 'v2.4.2';
process.env.CLI_VERSION = CLI_VERSION;

// Base dir where CLIs will be downloaded into
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ describe('databases', () => {
} as unknown as ExtensionContext,
{
sendRequest: sendRequestSpy,
supportsDatabaseRegistration: supportsDatabaseRegistrationSpy
supportsDatabaseRegistration: supportsDatabaseRegistrationSpy,
onDidStartQueryServer: () => { /**/ }
} as unknown as QueryServerClient,
{
supportsLanguageName: supportsLanguageNameSpy,
Expand Down

0 comments on commit 2ec2f15

Please sign in to comment.