Skip to content

Commit e1a0d24

Browse files
committed
Add Swift: Capture VSCode Swift Diagnostic Logs command
Adds a new command that users can use to help generate bug reports for the extension itself. The Capture VSCode Swift Diagnostic Logs command will create a new folder in a temporary directory that contains: - Swift version and path information - Extension logs - Any diagnostics in the Problems pane Users can copy the folder path or, if they're on macOS, open it in Finder. From there they can zip and attach these files to a GitHub issue. We'll want to update the New Issue Template with instructions on how to capture and attach these logs.
1 parent 52db2ae commit e1a0d24

File tree

8 files changed

+167
-11
lines changed

8 files changed

+167
-11
lines changed

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,11 @@
164164
"command": "swift.attachDebugger",
165165
"title": "Attach to Process...",
166166
"category": "Swift"
167+
},
168+
{
169+
"command": "swift.captureDiagnostics",
170+
"title": "Capture Diagnostic Logs",
171+
"category": "Swift"
167172
}
168173
],
169174
"configuration": [
@@ -1152,4 +1157,4 @@
11521157
"vscode-languageclient": "^9.0.1",
11531158
"xml2js": "^0.6.2"
11541159
}
1155-
}
1160+
}

src/TestExplorer/TestRunner.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -523,11 +523,6 @@ export class TestRunner {
523523
) {
524524
return new Promise<void>((resolve, reject) => {
525525
const args = testBuildConfig.args ?? [];
526-
this.folderContext?.workspaceContext.outputChannel.logDiagnostic(
527-
`Exec: ${testBuildConfig.program} ${args.join(" ")}`,
528-
this.folderContext.name
529-
);
530-
531526
let kindLabel: string;
532527
switch (testKind) {
533528
case TestKind.coverage:

src/commands.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the VSCode Swift open source project
44
//
5-
// Copyright (c) 2021-2023 the VSCode Swift project authors
5+
// Copyright (c) 2021-2024 the VSCode Swift project authors
66
// Licensed under Apache License v2.0
77
//
88
// See LICENSE.txt for license information
@@ -31,6 +31,7 @@ import { execFile } from "./utilities/utilities";
3131
import { SwiftExecOperation, TaskOperation } from "./tasks/TaskQueue";
3232
import { SwiftProjectTemplate } from "./toolchain/toolchain";
3333
import { showToolchainSelectionQuickPick, showToolchainError } from "./ui/ToolchainSelection";
34+
import { captureDiagnostics } from "./commands/captureDiagnostics";
3435

3536
/**
3637
* References:
@@ -845,5 +846,6 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] {
845846
}
846847
}),
847848
vscode.commands.registerCommand("swift.attachDebugger", () => attachDebugger(ctx)),
849+
vscode.commands.registerCommand("swift.captureDiagnostics", () => captureDiagnostics(ctx)),
848850
];
849851
}

src/commands/captureDiagnostics.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VSCode Swift open source project
4+
//
5+
// Copyright (c) 2021-2024 the VSCode Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VSCode Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import * as fs from "fs/promises";
16+
import * as path from "path";
17+
import * as vscode from "vscode";
18+
import { tmpdir } from "os";
19+
import { exec } from "child_process";
20+
import { SwiftOutputChannel } from "../ui/SwiftOutputChannel";
21+
import { WorkspaceContext } from "../WorkspaceContext";
22+
23+
export async function captureDiagnostics(ctx: WorkspaceContext) {
24+
const diagnosticsDir = path.join(
25+
tmpdir(),
26+
`vscode-diagnostics-${formatDateString(new Date())}`
27+
);
28+
29+
const versionOutputChannel = new SwiftOutputChannel();
30+
ctx.toolchain.logDiagnostics(versionOutputChannel);
31+
32+
const logs = ctx.outputChannel.logs.join("\n");
33+
const versionLogs = versionOutputChannel.logs.join("\n");
34+
const diagnosticLogs = buildDiagnostics();
35+
36+
try {
37+
await fs.mkdir(diagnosticsDir);
38+
await fs.writeFile(path.join(diagnosticsDir, "logs.txt"), logs);
39+
await fs.writeFile(path.join(diagnosticsDir, "version.txt"), versionLogs);
40+
await fs.writeFile(path.join(diagnosticsDir, "diagnostics.txt"), diagnosticLogs);
41+
42+
ctx.outputChannel.log(`Saved diagnostics to ${diagnosticsDir}`);
43+
44+
const showInFinderButton = "Show In Finder";
45+
const copyPath = "Copy Path to Clipboard";
46+
const infoDialogButtons = [
47+
...(process.platform === "darwin" ? [showInFinderButton] : []),
48+
copyPath,
49+
];
50+
const result = await vscode.window.showInformationMessage(
51+
`Saved diagnostic logs to ${diagnosticsDir}`,
52+
...infoDialogButtons
53+
);
54+
if (result === copyPath) {
55+
vscode.env.clipboard.writeText(diagnosticsDir);
56+
} else if (result === showInFinderButton) {
57+
exec(`open ${diagnosticsDir}`, error => {
58+
if (error) {
59+
vscode.window.showErrorMessage(`Failed to open Finder: ${error.message}`);
60+
return;
61+
}
62+
});
63+
}
64+
} catch (error) {
65+
vscode.window.showErrorMessage(`Unable to captrure diagnostics logs: ${error}`);
66+
}
67+
}
68+
69+
function buildDiagnostics(): string {
70+
const diagnosticToString = (diagnostic: vscode.Diagnostic) => {
71+
return `${severityToString(diagnostic.severity)} - ${diagnostic.message} [Ln ${diagnostic.range.start.line}, Col ${diagnostic.range.start.character}]`;
72+
};
73+
74+
return vscode.languages
75+
.getDiagnostics()
76+
.map(
77+
([uri, diagnostics]) => `${uri}\n\t${diagnostics.map(diagnosticToString).join("\n\t")}`
78+
)
79+
.join("\n");
80+
}
81+
82+
function severityToString(severity: vscode.DiagnosticSeverity): string {
83+
switch (severity) {
84+
case vscode.DiagnosticSeverity.Error:
85+
return "Error";
86+
case vscode.DiagnosticSeverity.Warning:
87+
return "Warning";
88+
case vscode.DiagnosticSeverity.Information:
89+
return "Information";
90+
case vscode.DiagnosticSeverity.Hint:
91+
return "Hint";
92+
}
93+
}
94+
95+
function padZero(num: number, length: number = 2): string {
96+
return num.toString().padStart(length, "0");
97+
}
98+
99+
function formatDateString(date: Date): string {
100+
const year = date.getFullYear();
101+
const month = padZero(date.getMonth() + 1);
102+
const day = padZero(date.getDate());
103+
const hours = padZero(date.getHours());
104+
const minutes = padZero(date.getMinutes());
105+
const seconds = padZero(date.getSeconds());
106+
const timezoneOffset = -date.getTimezoneOffset();
107+
const timezoneSign = timezoneOffset >= 0 ? "+" : "-";
108+
const timezoneHours = padZero(Math.floor(Math.abs(timezoneOffset) / 60));
109+
const timezoneMinutes = padZero(Math.abs(timezoneOffset) % 60);
110+
return `${year}-${month}-${day}T${hours}-${minutes}-${seconds}${timezoneSign}${timezoneHours}-${timezoneMinutes}`;
111+
}

src/extension.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api |
5353
const outputChannel = new SwiftOutputChannel();
5454
const toolchain: SwiftToolchain | undefined = await SwiftToolchain.create()
5555
.then(toolchain => {
56-
outputChannel.log(toolchain.swiftVersionString);
5756
toolchain.logDiagnostics(outputChannel);
5857
contextKeys.createNewProjectAvailable = toolchain.swiftVersion.isGreaterThanOrEqual(
5958
new Version(5, 8, 0)

src/tasks/TaskQueue.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,22 @@ export class TaskQueue {
253253
if (operation.log) {
254254
switch (result) {
255255
case 0:
256-
this.workspaceContext.outputChannel.logEnd("done.");
256+
this.workspaceContext.outputChannel.logEnd(
257+
"done.",
258+
this.folderContext.name
259+
);
257260
break;
258261
case undefined:
259-
this.workspaceContext.outputChannel.logEnd("cancelled.");
262+
this.workspaceContext.outputChannel.logEnd(
263+
"cancelled.",
264+
this.folderContext.name
265+
);
260266
break;
261267
default:
262-
this.workspaceContext.outputChannel.logEnd("failed.");
268+
this.workspaceContext.outputChannel.logEnd(
269+
"failed.",
270+
this.folderContext.name
271+
);
263272
break;
264273
}
265274
}

src/toolchain/toolchain.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ export class SwiftToolchain {
389389
}
390390

391391
logDiagnostics(channel: SwiftOutputChannel) {
392+
channel.logDiagnostic(this.swiftVersionString);
392393
channel.logDiagnostic(`Swift Path: ${this.swiftFolderPath}`);
393394
channel.logDiagnostic(`Toolchain Path: ${this.toolchainPath}`);
394395
if (this.runtimePath) {

src/ui/SwiftOutputChannel.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import configuration from "../configuration";
1717

1818
export class SwiftOutputChannel {
1919
private channel: vscode.OutputChannel;
20+
private logStore = new RollingLog(1024 * 1024 * 5);
2021

2122
constructor() {
2223
this.channel = vscode.window.createOutputChannel("Swift");
@@ -65,6 +66,7 @@ export class SwiftOutputChannel {
6566

6667
private sendLog(line: string) {
6768
this.channel.append(line);
69+
this.logStore.append(line);
6870

6971
if (process.env["CI"] !== "1") {
7072
console.log(line);
@@ -79,4 +81,36 @@ export class SwiftOutputChannel {
7981
second: "numeric",
8082
});
8183
}
84+
85+
get logs(): string[] {
86+
return this.logStore.logs;
87+
}
88+
}
89+
90+
class RollingLog {
91+
private _logs: string[] = [];
92+
private currentLogLength: number = 0;
93+
94+
constructor(private maxSizeCharacters: number) {}
95+
96+
public get logs(): string[] {
97+
return [...this._logs];
98+
}
99+
100+
append(log: string) {
101+
// It can be costly to calculate the actual memory size of a string in Node so just
102+
// use the total number of characters in the logs as a huristic for total size.
103+
const logSize = log.length;
104+
105+
while (this.currentLogLength + logSize > this.maxSizeCharacters && this.logs.length > 0) {
106+
const oldestLog = this.logs.shift();
107+
if (oldestLog) {
108+
this.currentLogLength -= oldestLog.length;
109+
}
110+
}
111+
112+
this._logs.push(log);
113+
114+
this.currentLogLength += logSize;
115+
}
82116
}

0 commit comments

Comments
 (0)