Skip to content

Commit b53b33e

Browse files
committed
Use file handle in TeeLogger
This switches the `TeeLogger` from using the `appendFile` function to manually opening and closing a file handle and calling `appendFile` on the file handle. This results in a more efficient implementation as the file handle is only opened once and closed once, instead of being opened and closed for every log message. This should result in less "too many open files" errors.
1 parent 38e862c commit b53b33e

File tree

6 files changed

+71
-27
lines changed

6 files changed

+71
-27
lines changed

extensions/ql-vscode/src/common/disposable-object.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,7 @@ export class DisposableObject implements Disposable {
8888
}
8989
}
9090
}
91+
92+
export function isDisposable(obj: unknown): obj is Disposable {
93+
return obj !== undefined && typeof (obj as Disposable).dispose === "function";
94+
}

extensions/ql-vscode/src/common/logging/tee-logger.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { appendFile, ensureFile } from "fs-extra";
1+
import { ensureFile } from "fs-extra";
2+
import { open } from "node:fs/promises";
3+
import type { FileHandle } from "node:fs/promises";
24
import { isAbsolute } from "path";
35
import { getErrorMessage } from "../helpers-pure";
46
import type { Logger, LogOptions } from "./logger";
7+
import type { Disposable } from "../disposable-object";
58

69
/**
710
* An implementation of {@link Logger} that sends the output both to another {@link Logger}
@@ -10,9 +13,10 @@ import type { Logger, LogOptions } from "./logger";
1013
* The first time a message is written, an additional banner is written to the underlying logger
1114
* pointing the user to the "side log" file.
1215
*/
13-
export class TeeLogger implements Logger {
16+
export class TeeLogger implements Logger, Disposable {
1417
private emittedRedirectMessage = false;
1518
private error = false;
19+
private fileHandle: FileHandle | undefined = undefined;
1620

1721
public constructor(
1822
private readonly logger: Logger,
@@ -37,11 +41,15 @@ export class TeeLogger implements Logger {
3741

3842
if (!this.error) {
3943
try {
44+
if (!this.fileHandle) {
45+
await ensureFile(this.location);
46+
47+
this.fileHandle = await open(this.location, "a");
48+
}
49+
4050
const trailingNewline = options.trailingNewline ?? true;
41-
await ensureFile(this.location);
4251

43-
await appendFile(
44-
this.location,
52+
await this.fileHandle.appendFile(
4553
message + (trailingNewline ? "\n" : ""),
4654
{
4755
encoding: "utf8",
@@ -50,6 +58,7 @@ export class TeeLogger implements Logger {
5058
} catch (e) {
5159
// Write an error message to the primary log, and stop trying to write to the side log.
5260
this.error = true;
61+
this.fileHandle = undefined;
5362
const errorMessage = getErrorMessage(e);
5463
await this.logger.log(
5564
`Error writing to additional log file: ${errorMessage}`,
@@ -65,4 +74,9 @@ export class TeeLogger implements Logger {
6574
show(preserveFocus?: boolean): void {
6675
this.logger.show(preserveFocus);
6776
}
77+
78+
dispose(): void {
79+
void this.fileHandle?.close();
80+
this.fileHandle = undefined;
81+
}
6882
}

extensions/ql-vscode/src/language-support/contextual/query-resolver.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,12 @@ export async function runContextualQuery(
9292
void extLogger.log(
9393
`Running contextual query ${query}; results will be stored in ${queryRun.outputDir.querySaveDir}`,
9494
);
95-
const results = await queryRun.evaluate(
96-
progress,
97-
token,
98-
new TeeLogger(qs.logger, queryRun.outputDir.logPath),
99-
);
100-
await cleanup?.();
101-
return results;
95+
const teeLogger = new TeeLogger(qs.logger, queryRun.outputDir.logPath);
96+
97+
try {
98+
return await queryRun.evaluate(progress, token, teeLogger);
99+
} finally {
100+
await cleanup?.();
101+
teeLogger.dispose();
102+
}
102103
}

extensions/ql-vscode/src/local-queries/local-query-run.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { redactableError } from "../common/errors";
2525
import type { LocalQueries } from "./local-queries";
2626
import { tryGetQueryMetadata } from "../codeql-cli/query-metadata";
2727
import { telemetryListener } from "../common/vscode/telemetry";
28+
import { isDisposable } from "../common/disposable-object";
2829

2930
function formatResultMessage(result: CoreQueryResults): string {
3031
switch (result.resultType) {
@@ -61,6 +62,10 @@ export class LocalQueryRun {
6162
private readonly localQueries: LocalQueries,
6263
private readonly queryInfo: LocalQueryInfo,
6364
private readonly dbItem: DatabaseItem,
65+
/**
66+
* The logger is only available while the query is running and will be disposed of when the
67+
* query completes.
68+
*/
6469
public readonly logger: Logger, // Public so that other clients, like the debug adapter, know where to send log output
6570
private readonly queryHistoryManager: QueryHistoryManager,
6671
private readonly cliServer: CodeQLCliServer,
@@ -92,6 +97,10 @@ export class LocalQueryRun {
9297
// Note we must update the query history view after showing results as the
9398
// display and sorting might depend on the number of results
9499
await this.queryHistoryManager.refreshTreeView();
100+
101+
if (isDisposable(this.logger)) {
102+
this.logger.dispose();
103+
}
95104
}
96105

97106
/**
@@ -110,6 +119,10 @@ export class LocalQueryRun {
110119
err.message = `Error running query: ${err.message}`;
111120
this.queryInfo.failureReason = err.message;
112121
await this.queryHistoryManager.refreshTreeView();
122+
123+
if (isDisposable(this.logger)) {
124+
this.logger.dispose();
125+
}
113126
}
114127

115128
/**

extensions/ql-vscode/src/local-queries/run-query.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,21 +47,27 @@ export async function runQuery({
4747
undefined,
4848
);
4949

50-
const completedQuery = await queryRun.evaluate(
51-
progress,
52-
token,
53-
new TeeLogger(queryRunner.logger, queryRun.outputDir.logPath),
50+
const teeLogger = new TeeLogger(
51+
queryRunner.logger,
52+
queryRun.outputDir.logPath,
5453
);
5554

56-
if (completedQuery.resultType !== QueryResultType.SUCCESS) {
57-
void showAndLogExceptionWithTelemetry(
58-
extLogger,
59-
telemetryListener,
60-
redactableError`Failed to run ${basename(queryPath)} query: ${
61-
completedQuery.message ?? "No message"
62-
}`,
63-
);
64-
return;
55+
try {
56+
const completedQuery = await queryRun.evaluate(progress, token, teeLogger);
57+
58+
if (completedQuery.resultType !== QueryResultType.SUCCESS) {
59+
void showAndLogExceptionWithTelemetry(
60+
extLogger,
61+
telemetryListener,
62+
redactableError`Failed to run ${basename(queryPath)} query: ${
63+
completedQuery.message ?? "No message"
64+
}`,
65+
);
66+
return;
67+
}
68+
69+
return completedQuery;
70+
} finally {
71+
teeLogger.dispose();
6572
}
66-
return completedQuery;
6773
}

extensions/ql-vscode/test/common/logging/output-channel-logger.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { dirSync } from "tmp";
55
import type { BaseLogger, Logger } from "../../../src/common/logging";
66
import { TeeLogger } from "../../../src/common/logging";
77
import { OutputChannelLogger } from "../../../src/common/logging/vscode";
8+
import type { Disposable } from "../../../src/common/disposable-object";
89

910
jest.setTimeout(999999);
1011

@@ -66,6 +67,8 @@ describe("OutputChannelLogger tests", function () {
6667

6768
// should have created 1 side log
6869
expect(readdirSync(tempFolders.storagePath.name)).toEqual(["hucairz"]);
70+
71+
hucairz.dispose();
6972
});
7073

7174
it("should create a side log", async () => {
@@ -86,12 +89,15 @@ describe("OutputChannelLogger tests", function () {
8689
expect(
8790
readFileSync(join(tempFolders.storagePath.name, "second"), "utf8"),
8891
).toBe("yyy\n");
92+
93+
first.dispose();
94+
second.dispose();
8995
});
9096

9197
function createSideLogger(
9298
logger: Logger,
9399
additionalLogLocation: string,
94-
): BaseLogger {
100+
): BaseLogger & Disposable {
95101
return new TeeLogger(
96102
logger,
97103
join(tempFolders.storagePath.name, additionalLogLocation),

0 commit comments

Comments
 (0)