Skip to content

Commit

Permalink
feat: Save log files per query
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
aeisenberg committed Mar 19, 2020
1 parent 6a746ae commit a6043f2
Show file tree
Hide file tree
Showing 10 changed files with 360 additions and 82 deletions.
15 changes: 8 additions & 7 deletions extensions/ql-vscode/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,12 @@ export class CodeQLCliServer implements Disposable {
// Kill the process if it isn't already dead.
this.killProcessIfRunning();
// Report the error (if there is a stderr then use that otherwise just report the error cod or nodejs error)
if (stderrBuffers.length == 0) {
throw new Error(`${description} failed: ${err}`)
} else {
throw new Error(`${description} failed: ${Buffer.concat(stderrBuffers).toString("utf8")}`);
}
const newError =
stderrBuffers.length == 0
? new Error(`${description} failed: ${err}`)
: new Error(`${description} failed: ${Buffer.concat(stderrBuffers).toString("utf8")}`);
newError.stack += (err.stack || '');
throw newError;
} finally {
this.logger.log(Buffer.concat(stderrBuffers).toString("utf8"));
// Remove the listeners we set up.
Expand Down Expand Up @@ -413,7 +414,7 @@ export class CodeQLCliServer implements Disposable {
const subcommandArgs = [
'--query', queryPath,
"--additional-packs",
workspaces.join(path.delimiter)
path.join(...workspaces)
];
return await this.runJsonCodeQlCliCommand<QuerySetup>(['resolve', 'library-path'], subcommandArgs, "Resolving library paths");
}
Expand Down Expand Up @@ -604,7 +605,7 @@ export class CodeQLCliServer implements Disposable {
resolveQlpacks(additionalPacks: string[], searchPath?: string[]): Promise<QlpacksInfo> {
const args = ['--additional-packs', additionalPacks.join(path.delimiter)];
if (searchPath !== undefined) {
args.push('--search-path', searchPath.join(path.delimiter));
args.push('--search-path', path.join(...searchPath));
}

return this.runJsonCodeQlCliCommand<QlpacksInfo>(
Expand Down
12 changes: 6 additions & 6 deletions extensions/ql-vscode/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,19 @@ const DEBUG_SETTING = new Setting('debug', RUNNING_QUERIES_SETTING);
const QUERY_SERVER_RESTARTING_SETTINGS = [NUMBER_OF_THREADS_SETTING, MEMORY_SETTING, DEBUG_SETTING];

export interface QueryServerConfig {
codeQlPath: string,
debug: boolean,
numThreads: number,
queryMemoryMb?: number,
timeoutSecs: number,
codeQlPath: string;
debug: boolean;
numThreads: number;
queryMemoryMb?: number;
timeoutSecs: number;
onDidChangeQueryServerConfiguration?: Event<void>;
}

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

export interface QueryHistoryConfig {
format: string,
format: string;
onDidChangeQueryHistoryConfiguration: Event<void>;
}

Expand Down
34 changes: 17 additions & 17 deletions extensions/ql-vscode/src/distribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@ import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-versi

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

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

/**
* Version constraint for the CLI.
*
*
* This applies to both extension-managed and CLI distributions.
*/
export const DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT: VersionConstraint = {
Expand All @@ -46,8 +46,8 @@ export const DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT: VersionConstraint = {
}

export interface DistributionProvider {
getCodeQlPathWithoutVersionCheck(): Promise<string | undefined>,
onDidChangeDistribution?: Event<void>
getCodeQlPathWithoutVersionCheck(): Promise<string | undefined>;
onDidChangeDistribution?: Event<void>;
}

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

/**
* Installs a release of the extension-managed distribution.
*
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public installExtensionManagedDistributionRelease(release: Release,
Expand Down Expand Up @@ -200,7 +200,7 @@ class ExtensionSpecificDistributionManager {
/**
* Check for updates to the extension-managed distribution. If one has not already been installed,
* this will return an update available result with the latest available release.
*
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async checkForUpdatesToDistribution(): Promise<DistributionUpdateCheckResult> {
Expand All @@ -216,7 +216,7 @@ class ExtensionSpecificDistributionManager {

/**
* Installs a release of the extension-managed distribution.
*
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async installDistributionRelease(release: Release,
Expand Down Expand Up @@ -247,8 +247,8 @@ class ExtensionSpecificDistributionManager {

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

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

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

/**
* Comparison of semantic versions.
*
*
* Returns a positive number if a is greater than b.
* Returns 0 if a equals b.
* Returns a negative number if a is less than b.
Expand Down Expand Up @@ -526,7 +526,7 @@ export type FindDistributionResult = CompatibleDistributionResult | UnknownCompa
interface CompatibleDistributionResult {
codeQlPath: string;
kind: FindDistributionResultKind.CompatibleDistribution;
version: Version
version: Version;
}

interface UnknownCompatibilityDistributionResult {
Expand Down Expand Up @@ -555,7 +555,7 @@ type DistributionUpdateCheckResult = AlreadyCheckedRecentlyResult | AlreadyUpToD
UpdateAvailableResult;

export interface AlreadyCheckedRecentlyResult {
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult
kind: DistributionUpdateCheckResultKind.AlreadyCheckedRecentlyResult;
}

export interface AlreadyUpToDateResult {
Expand Down
17 changes: 11 additions & 6 deletions extensions/ql-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command:
}

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

initializeLogging(ctx);

const distributionConfigListener = new DistributionConfigListener();
ctx.subscriptions.push(distributionConfigListener);
const distributionManager = new DistributionManager(ctx, distributionConfigListener, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT);
Expand Down Expand Up @@ -238,10 +238,6 @@ async function activateWithInstalledDistribution(ctx: ExtensionContext, distribu
const qlConfigurationListener = await QueryServerConfigListener.createQueryServerConfigListener(distributionManager);
ctx.subscriptions.push(qlConfigurationListener);

ctx.subscriptions.push(queryServerLogger);
ctx.subscriptions.push(ideServerLogger);


const cliServer = new CodeQLCliServer(distributionManager, logger);
ctx.subscriptions.push(cliServer);

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

function initializeLogging(ctx: ExtensionContext): void {
logger.init(ctx);
queryServerLogger.init(ctx);
ideServerLogger.init(ctx);
ctx.subscriptions.push(logger);
ctx.subscriptions.push(queryServerLogger);
ctx.subscriptions.push(ideServerLogger);
}

const checkForUpdatesCommand = 'codeQL.checkForUpdatesToCLI';
4 changes: 2 additions & 2 deletions extensions/ql-vscode/src/ide-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export async function spawnIdeServer(config: QueryServerConfig): Promise<StreamI
['execute', 'language-server'],
['--check-errors', 'ON_CHANGE'],
ideServerLogger,
data => ideServerLogger.logWithoutTrailingNewline(data.toString()),
data => ideServerLogger.logWithoutTrailingNewline(data.toString()),
data => ideServerLogger.log(data.toString(), { trailingNewline: false }),
data => ideServerLogger.log(data.toString(), { trailingNewline: false }),
progressReporter
);
return { writer: child.stdin!, reader: child.stdout! };
Expand Down
108 changes: 96 additions & 12 deletions extensions/ql-vscode/src/logging.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,124 @@
import { window as Window, OutputChannel, Progress } from 'vscode';
import { window as Window, OutputChannel, Progress, ExtensionContext, Disposable } from 'vscode';
import { DisposableObject } from 'semmle-vscode-utils';
import * as fs from 'fs-extra';
import * as path from 'path';

interface LogOptions {
/** If false, don't output a trailing newline for the log entry. Default true. */
trailingNewline?: boolean;

/** If specified, add this log entry to the log file at the specified location. */
additionalLogLocation?: string;
}

export interface Logger {
/** Writes the given log message, followed by a newline. */
log(message: string): void;
/** Writes the given log message, not followed by a newline. */
logWithoutTrailingNewline(message: string): void;
/** Writes the given log message, optionally followed by a newline. */
log(message: string, options?: LogOptions): Promise<void>;
/**
* Reveal this channel in the UI.
*
* @param preserveFocus When `true` the channel will not take focus.
*/
show(preserveFocus?: boolean): void;

/**
* Remove the log at the specified location
* @param location log to remove
*/
removeAdditionalLogLocation(location: string): Promise<void>;
}

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

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

constructor(title: string) {
constructor(private title: string) {
super();
this.outputChannel = Window.createOutputChannel(title);
this.push(this.outputChannel);
}

log(message: string) {
this.outputChannel.appendLine(message);
init(ctx: ExtensionContext): void {
this.additionalLogLocationPath = path.join(ctx.storagePath || ctx.globalStoragePath, this.title);

// clear out any old state from previous runs
fs.remove(this.additionalLogLocationPath);
}

logWithoutTrailingNewline(message: string) {
this.outputChannel.append(message);
/**
* This function is asynchronous and will only resolve once the message is written
* to the side log (if required). It is not necessary to await the results of this
* function if you don't need to guarantee that the log writing is complete before
* continuing.
*/
async log(message: string, options = { } as LogOptions): Promise<void> {
if (options.trailingNewline === undefined) {
options.trailingNewline = true;
}

if (options.trailingNewline) {
this.outputChannel.appendLine(message);
} else {
this.outputChannel.append(message);
}

if (this.additionalLogLocationPath && options.additionalLogLocation) {
const logPath = path.join(this.additionalLogLocationPath, options.additionalLogLocation);
let additional = this.additionalLocations.get(logPath);
if (!additional) {
const msg = `| Log being saved to ${logPath} |`;
const separator = new Array(msg.length).fill('-').join('');
this.outputChannel.appendLine(separator);
this.outputChannel.appendLine(msg);
this.outputChannel.appendLine(separator);
additional = new AdditionalLogLocation(logPath);
this.additionalLocations.set(logPath, additional);
this.track(additional);
}

await additional.log(message, options);
}
}

show(preserveFocus?: boolean) {
show(preserveFocus?: boolean): void {
this.outputChannel.show(preserveFocus);
}

async removeAdditionalLogLocation(location: string): Promise<void> {
if (this.additionalLogLocationPath) {
const logPath = path.join(this.additionalLogLocationPath, location);
const additional = this.additionalLocations.get(logPath);
if (additional) {
this.disposeAndStopTracking(additional);
this.additionalLocations.delete(logPath);
}
}
}
}

class AdditionalLogLocation extends Disposable {
constructor(private location: string) {
super(() => { /**/ });
}

async log(message: string, options = { } as LogOptions): Promise<void> {
if (options.trailingNewline === undefined) {
options.trailingNewline = true;
}
await fs.ensureFile(this.location);

await fs.appendFile(this.location, message + (options.trailingNewline ? '\n' : ''), {
encoding: 'utf8'
});
}

async dispose(): Promise<void> {
await fs.remove(this.location);
}
}

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

/** The logger for messages from the language server. */
export const ideServerLogger = new OutputChannelLogger('CodeQL Language Server');
export const ideServerLogger = new OutputChannelLogger(
'CodeQL Language Server'
);

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

0 comments on commit a6043f2

Please sign in to comment.