Skip to content

Commit a6043f2

Browse files
committed
feat: Save log files per query
This feature adds logging per-query. Each query will be logged in its own location in either workspace or globally shared location in vscode. There are limitations here. We are only guessing when one query ends and another begins. We assume that queries don't occur in parallel. If they do, the previous query will have its results intermingled with the current query's results. To fix that, we will need to update how the query-server emits log messages so that each query message is attached to a tag that specifies the query that emitted it.
1 parent 6a746ae commit a6043f2

File tree

10 files changed

+360
-82
lines changed

10 files changed

+360
-82
lines changed

extensions/ql-vscode/src/cli.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,11 +243,12 @@ export class CodeQLCliServer implements Disposable {
243243
// Kill the process if it isn't already dead.
244244
this.killProcessIfRunning();
245245
// Report the error (if there is a stderr then use that otherwise just report the error cod or nodejs error)
246-
if (stderrBuffers.length == 0) {
247-
throw new Error(`${description} failed: ${err}`)
248-
} else {
249-
throw new Error(`${description} failed: ${Buffer.concat(stderrBuffers).toString("utf8")}`);
250-
}
246+
const newError =
247+
stderrBuffers.length == 0
248+
? new Error(`${description} failed: ${err}`)
249+
: new Error(`${description} failed: ${Buffer.concat(stderrBuffers).toString("utf8")}`);
250+
newError.stack += (err.stack || '');
251+
throw newError;
251252
} finally {
252253
this.logger.log(Buffer.concat(stderrBuffers).toString("utf8"));
253254
// Remove the listeners we set up.
@@ -413,7 +414,7 @@ export class CodeQLCliServer implements Disposable {
413414
const subcommandArgs = [
414415
'--query', queryPath,
415416
"--additional-packs",
416-
workspaces.join(path.delimiter)
417+
path.join(...workspaces)
417418
];
418419
return await this.runJsonCodeQlCliCommand<QuerySetup>(['resolve', 'library-path'], subcommandArgs, "Resolving library paths");
419420
}
@@ -604,7 +605,7 @@ export class CodeQLCliServer implements Disposable {
604605
resolveQlpacks(additionalPacks: string[], searchPath?: string[]): Promise<QlpacksInfo> {
605606
const args = ['--additional-packs', additionalPacks.join(path.delimiter)];
606607
if (searchPath !== undefined) {
607-
args.push('--search-path', searchPath.join(path.delimiter));
608+
args.push('--search-path', path.join(...searchPath));
608609
}
609610

610611
return this.runJsonCodeQlCliCommand<QlpacksInfo>(

extensions/ql-vscode/src/config.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,19 @@ const DEBUG_SETTING = new Setting('debug', RUNNING_QUERIES_SETTING);
6464
const QUERY_SERVER_RESTARTING_SETTINGS = [NUMBER_OF_THREADS_SETTING, MEMORY_SETTING, DEBUG_SETTING];
6565

6666
export interface QueryServerConfig {
67-
codeQlPath: string,
68-
debug: boolean,
69-
numThreads: number,
70-
queryMemoryMb?: number,
71-
timeoutSecs: number,
67+
codeQlPath: string;
68+
debug: boolean;
69+
numThreads: number;
70+
queryMemoryMb?: number;
71+
timeoutSecs: number;
7272
onDidChangeQueryServerConfiguration?: Event<void>;
7373
}
7474

7575
/** When these settings change, the query history should be refreshed. */
7676
const QUERY_HISTORY_SETTINGS = [QUERY_HISTORY_FORMAT_SETTING];
7777

7878
export interface QueryHistoryConfig {
79-
format: string,
79+
format: string;
8080
onDidChangeQueryHistoryConfiguration: Event<void>;
8181
}
8282

extensions/ql-vscode/src/distribution.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,23 @@ import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-versi
1919

2020
/**
2121
* Default value for the owner name of the extension-managed distribution on GitHub.
22-
*
22+
*
2323
* We set the default here rather than as a default config value so that this default is invoked
2424
* upon blanking the setting.
2525
*/
2626
const DEFAULT_DISTRIBUTION_OWNER_NAME = "github";
2727

2828
/**
2929
* Default value for the repository name of the extension-managed distribution on GitHub.
30-
*
30+
*
3131
* We set the default here rather than as a default config value so that this default is invoked
3232
* upon blanking the setting.
3333
*/
3434
const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-binaries";
3535

3636
/**
3737
* Version constraint for the CLI.
38-
*
38+
*
3939
* This applies to both extension-managed and CLI distributions.
4040
*/
4141
export const DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT: VersionConstraint = {
@@ -46,8 +46,8 @@ export const DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT: VersionConstraint = {
4646
}
4747

4848
export interface DistributionProvider {
49-
getCodeQlPathWithoutVersionCheck(): Promise<string | undefined>,
50-
onDidChangeDistribution?: Event<void>
49+
getCodeQlPathWithoutVersionCheck(): Promise<string | undefined>;
50+
onDidChangeDistribution?: Event<void>;
5151
}
5252

5353
export class DistributionManager implements DistributionProvider {
@@ -130,7 +130,7 @@ export class DistributionManager implements DistributionProvider {
130130
/**
131131
* Check for updates to the extension-managed distribution. If one has not already been installed,
132132
* this will return an update available result with the latest available release.
133-
*
133+
*
134134
* Returns a failed promise if an unexpected error occurs during installation.
135135
*/
136136
public async checkForUpdatesToExtensionManagedDistribution(
@@ -152,7 +152,7 @@ export class DistributionManager implements DistributionProvider {
152152

153153
/**
154154
* Installs a release of the extension-managed distribution.
155-
*
155+
*
156156
* Returns a failed promise if an unexpected error occurs during installation.
157157
*/
158158
public installExtensionManagedDistributionRelease(release: Release,
@@ -200,7 +200,7 @@ class ExtensionSpecificDistributionManager {
200200
/**
201201
* Check for updates to the extension-managed distribution. If one has not already been installed,
202202
* this will return an update available result with the latest available release.
203-
*
203+
*
204204
* Returns a failed promise if an unexpected error occurs during installation.
205205
*/
206206
public async checkForUpdatesToDistribution(): Promise<DistributionUpdateCheckResult> {
@@ -216,7 +216,7 @@ class ExtensionSpecificDistributionManager {
216216

217217
/**
218218
* Installs a release of the extension-managed distribution.
219-
*
219+
*
220220
* Returns a failed promise if an unexpected error occurs during installation.
221221
*/
222222
public async installDistributionRelease(release: Release,
@@ -247,8 +247,8 @@ class ExtensionSpecificDistributionManager {
247247

248248
if (progressCallback && contentLength !== null) {
249249
const totalNumBytes = parseInt(contentLength, 10);
250-
const bytesToDisplayMB = (numBytes: number) => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
251-
const updateProgress = () => {
250+
const bytesToDisplayMB = (numBytes: number): string => `${(numBytes / (1024 * 1024)).toFixed(1)} MB`;
251+
const updateProgress = (): void => {
252252
progressCallback({
253253
step: numBytesDownloaded,
254254
maxStep: totalNumBytes,
@@ -282,7 +282,7 @@ class ExtensionSpecificDistributionManager {
282282

283283
/**
284284
* Remove the extension-managed distribution.
285-
*
285+
*
286286
* This should not be called for a distribution that is currently in use, as remove may fail.
287287
*/
288288
private async removeDistribution(): Promise<void> {
@@ -357,7 +357,7 @@ export class ReleasesApiConsumer {
357357
this._repoName = repoName;
358358
}
359359

360-
public async getLatestRelease(versionConstraint: VersionConstraint, includePrerelease: boolean = false): Promise<Release> {
360+
public async getLatestRelease(versionConstraint: VersionConstraint, includePrerelease = false): Promise<Release> {
361361
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases`;
362362
const allReleases: GithubRelease[] = await (await this.makeApiCall(apiPath)).json();
363363
const compatibleReleases = allReleases.filter(release => {
@@ -428,7 +428,7 @@ export class ReleasesApiConsumer {
428428
private async makeRawRequest(
429429
requestUrl: string,
430430
headers: { [key: string]: string },
431-
redirectCount: number = 0): Promise<fetch.Response> {
431+
redirectCount = 0): Promise<fetch.Response> {
432432
const response = await fetch.default(requestUrl, {
433433
headers,
434434
redirect: "manual"
@@ -480,7 +480,7 @@ export async function extractZipArchive(archivePath: string, outPath: string): P
480480

481481
/**
482482
* Comparison of semantic versions.
483-
*
483+
*
484484
* Returns a positive number if a is greater than b.
485485
* Returns 0 if a equals b.
486486
* Returns a negative number if a is less than b.
@@ -526,7 +526,7 @@ export type FindDistributionResult = CompatibleDistributionResult | UnknownCompa
526526
interface CompatibleDistributionResult {
527527
codeQlPath: string;
528528
kind: FindDistributionResultKind.CompatibleDistribution;
529-
version: Version
529+
version: Version;
530530
}
531531

532532
interface UnknownCompatibilityDistributionResult {
@@ -555,7 +555,7 @@ type DistributionUpdateCheckResult = AlreadyCheckedRecentlyResult | AlreadyUpToD
555555
UpdateAvailableResult;
556556

557557
export interface AlreadyCheckedRecentlyResult {
558-
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
558+
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult;
559559
}
560560

561561
export interface AlreadyUpToDateResult {

extensions/ql-vscode/src/extension.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command:
7676
}
7777

7878
export async function activate(ctx: ExtensionContext): Promise<void> {
79-
// Initialise logging, and ensure all loggers are disposed upon exit.
80-
ctx.subscriptions.push(logger);
8179
logger.log('Starting CodeQL extension');
8280

81+
initializeLogging(ctx);
82+
8383
const distributionConfigListener = new DistributionConfigListener();
8484
ctx.subscriptions.push(distributionConfigListener);
8585
const distributionManager = new DistributionManager(ctx, distributionConfigListener, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT);
@@ -238,10 +238,6 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
238238
const qlConfigurationListener = await QueryServerConfigListener.createQueryServerConfigListener(distributionManager);
239239
ctx.subscriptions.push(qlConfigurationListener);
240240

241-
ctx.subscriptions.push(queryServerLogger);
242-
ctx.subscriptions.push(ideServerLogger);
243-
244-
245241
const cliServer = new CodeQLCliServer(distributionManager, logger);
246242
ctx.subscriptions.push(cliServer);
247243

@@ -327,4 +323,13 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
327323
ctx.subscriptions.push(client.start());
328324
}
329325

326+
function initializeLogging(ctx: ExtensionContext): void {
327+
logger.init(ctx);
328+
queryServerLogger.init(ctx);
329+
ideServerLogger.init(ctx);
330+
ctx.subscriptions.push(logger);
331+
ctx.subscriptions.push(queryServerLogger);
332+
ctx.subscriptions.push(ideServerLogger);
333+
}
334+
330335
const checkForUpdatesCommand = 'codeQL.checkForUpdatesToCLI';

extensions/ql-vscode/src/ide-server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export async function spawnIdeServer(config: QueryServerConfig): Promise<StreamI
1717
['execute', 'language-server'],
1818
['--check-errors', 'ON_CHANGE'],
1919
ideServerLogger,
20-
data => ideServerLogger.logWithoutTrailingNewline(data.toString()),
21-
data => ideServerLogger.logWithoutTrailingNewline(data.toString()),
20+
data => ideServerLogger.log(data.toString(), { trailingNewline: false }),
21+
data => ideServerLogger.log(data.toString(), { trailingNewline: false }),
2222
progressReporter
2323
);
2424
return { writer: child.stdin!, reader: child.stdout! };
Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,124 @@
1-
import { window as Window, OutputChannel, Progress } from 'vscode';
1+
import { window as Window, OutputChannel, Progress, ExtensionContext, Disposable } from 'vscode';
22
import { DisposableObject } from 'semmle-vscode-utils';
3+
import * as fs from 'fs-extra';
4+
import * as path from 'path';
5+
6+
interface LogOptions {
7+
/** If false, don't output a trailing newline for the log entry. Default true. */
8+
trailingNewline?: boolean;
9+
10+
/** If specified, add this log entry to the log file at the specified location. */
11+
additionalLogLocation?: string;
12+
}
313

414
export interface Logger {
5-
/** Writes the given log message, followed by a newline. */
6-
log(message: string): void;
7-
/** Writes the given log message, not followed by a newline. */
8-
logWithoutTrailingNewline(message: string): void;
15+
/** Writes the given log message, optionally followed by a newline. */
16+
log(message: string, options?: LogOptions): Promise<void>;
917
/**
1018
* Reveal this channel in the UI.
1119
*
1220
* @param preserveFocus When `true` the channel will not take focus.
1321
*/
1422
show(preserveFocus?: boolean): void;
23+
24+
/**
25+
* Remove the log at the specified location
26+
* @param location log to remove
27+
*/
28+
removeAdditionalLogLocation(location: string): Promise<void>;
1529
}
1630

1731
export type ProgressReporter = Progress<{ message: string }>;
1832

1933
/** A logger that writes messages to an output channel in the Output tab. */
2034
export class OutputChannelLogger extends DisposableObject implements Logger {
2135
public readonly outputChannel: OutputChannel;
36+
private readonly additionalLocations = new Map<string, AdditionalLogLocation>();
37+
private additionalLogLocationPath: string | undefined;
2238

23-
constructor(title: string) {
39+
constructor(private title: string) {
2440
super();
2541
this.outputChannel = Window.createOutputChannel(title);
2642
this.push(this.outputChannel);
2743
}
2844

29-
log(message: string) {
30-
this.outputChannel.appendLine(message);
45+
init(ctx: ExtensionContext): void {
46+
this.additionalLogLocationPath = path.join(ctx.storagePath || ctx.globalStoragePath, this.title);
47+
48+
// clear out any old state from previous runs
49+
fs.remove(this.additionalLogLocationPath);
3150
}
3251

33-
logWithoutTrailingNewline(message: string) {
34-
this.outputChannel.append(message);
52+
/**
53+
* This function is asynchronous and will only resolve once the message is written
54+
* to the side log (if required). It is not necessary to await the results of this
55+
* function if you don't need to guarantee that the log writing is complete before
56+
* continuing.
57+
*/
58+
async log(message: string, options = { } as LogOptions): Promise<void> {
59+
if (options.trailingNewline === undefined) {
60+
options.trailingNewline = true;
61+
}
62+
63+
if (options.trailingNewline) {
64+
this.outputChannel.appendLine(message);
65+
} else {
66+
this.outputChannel.append(message);
67+
}
68+
69+
if (this.additionalLogLocationPath && options.additionalLogLocation) {
70+
const logPath = path.join(this.additionalLogLocationPath, options.additionalLogLocation);
71+
let additional = this.additionalLocations.get(logPath);
72+
if (!additional) {
73+
const msg = `| Log being saved to ${logPath} |`;
74+
const separator = new Array(msg.length).fill('-').join('');
75+
this.outputChannel.appendLine(separator);
76+
this.outputChannel.appendLine(msg);
77+
this.outputChannel.appendLine(separator);
78+
additional = new AdditionalLogLocation(logPath);
79+
this.additionalLocations.set(logPath, additional);
80+
this.track(additional);
81+
}
82+
83+
await additional.log(message, options);
84+
}
3585
}
3686

37-
show(preserveFocus?: boolean) {
87+
show(preserveFocus?: boolean): void {
3888
this.outputChannel.show(preserveFocus);
3989
}
90+
91+
async removeAdditionalLogLocation(location: string): Promise<void> {
92+
if (this.additionalLogLocationPath) {
93+
const logPath = path.join(this.additionalLogLocationPath, location);
94+
const additional = this.additionalLocations.get(logPath);
95+
if (additional) {
96+
this.disposeAndStopTracking(additional);
97+
this.additionalLocations.delete(logPath);
98+
}
99+
}
100+
}
101+
}
102+
103+
class AdditionalLogLocation extends Disposable {
104+
constructor(private location: string) {
105+
super(() => { /**/ });
106+
}
107+
108+
async log(message: string, options = { } as LogOptions): Promise<void> {
109+
if (options.trailingNewline === undefined) {
110+
options.trailingNewline = true;
111+
}
112+
await fs.ensureFile(this.location);
113+
114+
await fs.appendFile(this.location, message + (options.trailingNewline ? '\n' : ''), {
115+
encoding: 'utf8'
116+
});
117+
}
118+
119+
async dispose(): Promise<void> {
120+
await fs.remove(this.location);
121+
}
40122
}
41123

42124
/** The global logger for the extension. */
@@ -46,7 +128,9 @@ export const logger = new OutputChannelLogger('CodeQL Extension Log');
46128
export const queryServerLogger = new OutputChannelLogger('CodeQL Query Server');
47129

48130
/** The logger for messages from the language server. */
49-
export const ideServerLogger = new OutputChannelLogger('CodeQL Language Server');
131+
export const ideServerLogger = new OutputChannelLogger(
132+
'CodeQL Language Server'
133+
);
50134

51135
/** The logger for messages from tests. */
52136
export const testLogger = new OutputChannelLogger('CodeQL Tests');

0 commit comments

Comments
 (0)