Skip to content

Commit 3843201

Browse files
authored
Add Swift: Capture VSCode Swift Diagnostic Logs command (#830)
* 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 a6b6fee commit 3843201

File tree

10 files changed

+510
-32
lines changed

10 files changed

+510
-32
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@
170170
"title": "Attach to Process...",
171171
"category": "Swift"
172172
},
173+
{
174+
"command": "swift.captureDiagnostics",
175+
"title": "Capture VS Code Swift Diagnostic Bundle",
176+
"category": "Swift"
177+
},
173178
{
174179
"command": "swift.clearDiagnosticsCollection",
175180
"title": "Clear Diagnostics Collection",

src/TestExplorer/TestRunner.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -555,11 +555,6 @@ export class TestRunner {
555555
) {
556556
return new Promise<void>((resolve, reject) => {
557557
const args = testBuildConfig.args ?? [];
558-
this.folderContext?.workspaceContext.outputChannel.logDiagnostic(
559-
`Exec: ${testBuildConfig.program} ${args.join(" ")}`,
560-
this.folderContext.name
561-
);
562-
563558
let kindLabel: string;
564559
switch (testKind) {
565560
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 VS Code Swift open source project
44
//
5-
// Copyright (c) 2021-2023 the VS Code Swift project authors
5+
// Copyright (c) 2021-2024 the VS Code 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:
@@ -848,5 +849,6 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] {
848849
vscode.commands.registerCommand("swift.clearDiagnosticsCollection", () =>
849850
ctx.diagnostics.clear()
850851
),
852+
vscode.commands.registerCommand("swift.captureDiagnostics", () => captureDiagnostics(ctx)),
851853
];
852854
}

src/commands/captureDiagnostics.ts

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2021-2024 the VS Code 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 VS Code 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 { Writable } from "stream";
21+
import { WorkspaceContext } from "../WorkspaceContext";
22+
import { Version } from "../utilities/version";
23+
import { execFileStreamOutput } from "../utilities/utilities";
24+
import configuration from "../configuration";
25+
26+
export async function captureDiagnostics(
27+
ctx: WorkspaceContext,
28+
allowMinimalCapture: boolean = true
29+
) {
30+
try {
31+
const captureMode = await captureDiagnosticsMode(ctx, allowMinimalCapture);
32+
33+
// dialog was cancelled
34+
if (!captureMode) {
35+
return;
36+
}
37+
38+
const diagnosticsDir = path.join(
39+
tmpdir(),
40+
`vscode-diagnostics-${formatDateString(new Date())}`
41+
);
42+
43+
await fs.mkdir(diagnosticsDir);
44+
await writeLogFile(diagnosticsDir, "extension-logs.txt", extensionLogs(ctx));
45+
await writeLogFile(diagnosticsDir, "settings.txt", settingsLogs(ctx));
46+
47+
if (captureMode === "Full") {
48+
await writeLogFile(diagnosticsDir, "source-code-diagnostics.txt", diagnosticLogs());
49+
50+
// The `sourcekit-lsp diagnose` command is only available in 6.0 and higher.
51+
if (ctx.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0))) {
52+
await sourcekitDiagnose(ctx, diagnosticsDir);
53+
} else {
54+
await writeLogFile(diagnosticsDir, "sourcekit-lsp.txt", sourceKitLogs(ctx));
55+
}
56+
}
57+
58+
ctx.outputChannel.log(`Saved diagnostics to ${diagnosticsDir}`);
59+
await showCapturedDiagnosticsResults(diagnosticsDir);
60+
} catch (error) {
61+
vscode.window.showErrorMessage(`Unable to capture diagnostic logs: ${error}`);
62+
}
63+
}
64+
65+
export async function promptForDiagnostics(ctx: WorkspaceContext) {
66+
const ok = "OK";
67+
const cancel = "Cancel";
68+
const result = await vscode.window.showInformationMessage(
69+
"SourceKit-LSP has been restored. Would you like to capture a diagnostic bundle to file an issue?",
70+
ok,
71+
cancel
72+
);
73+
74+
if (!result || result === cancel) {
75+
return;
76+
}
77+
78+
return await captureDiagnostics(ctx, false);
79+
}
80+
81+
async function captureDiagnosticsMode(
82+
ctx: WorkspaceContext,
83+
allowMinimalCapture: boolean
84+
): Promise<"Minimal" | "Full" | undefined> {
85+
if (
86+
ctx.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) ||
87+
vscode.workspace.getConfiguration("sourcekit-lsp").get<string>("trace.server", "off") !==
88+
"off"
89+
) {
90+
const fullButton = allowMinimalCapture ? "Capture Full Diagnostics" : "Capture Diagnostics";
91+
const minimalButton = "Capture Minimal Diagnostics";
92+
const buttons = allowMinimalCapture ? [fullButton, minimalButton] : [fullButton];
93+
const fullCaptureResult = await vscode.window.showInformationMessage(
94+
`A Diagnostic Bundle collects information that helps the developers of the Swift for VS Code extension diagnose and fix issues.
95+
96+
This information contains:
97+
- Extension logs
98+
- Versions of Swift installed on your system
99+
- Crash logs from SourceKit
100+
- Log messages emitted by SourceKit
101+
- If possible, a minimized project that caused SourceKit to crash
102+
- If possible, a minimized project that caused the Swift compiler to crash
103+
104+
All information is collected locally and you can inspect the diagnose bundle before sharing it with developers of the Swift for VS Code extension.
105+
106+
Please file an issue with a description of the problem you are seeing at https://github.com/swiftlang/vscode-swift, and attach this diagnose bundle.`,
107+
{
108+
modal: true,
109+
detail: allowMinimalCapture
110+
? `If you wish to omit potentially sensitive information choose "${minimalButton}"`
111+
: undefined,
112+
},
113+
...buttons
114+
);
115+
if (!fullCaptureResult) {
116+
return undefined;
117+
}
118+
119+
return fullCaptureResult === fullButton ? "Full" : "Minimal";
120+
} else {
121+
return "Minimal";
122+
}
123+
}
124+
125+
async function showCapturedDiagnosticsResults(diagnosticsDir: string) {
126+
const showInFinderButton = `Show In ${showCommandType()}`;
127+
const copyPath = "Copy Path to Clipboard";
128+
const result = await vscode.window.showInformationMessage(
129+
`Saved diagnostic logs to ${diagnosticsDir}`,
130+
showInFinderButton,
131+
copyPath
132+
);
133+
if (result === copyPath) {
134+
vscode.env.clipboard.writeText(diagnosticsDir);
135+
} else if (result === showInFinderButton) {
136+
exec(showDirectoryCommand(diagnosticsDir), error => {
137+
// Opening the explorer on windows returns an exit code of 1 despite opening successfully.
138+
if (error && process.platform !== "win32") {
139+
vscode.window.showErrorMessage(
140+
`Failed to open ${showCommandType()}: ${error.message}`
141+
);
142+
}
143+
});
144+
}
145+
}
146+
147+
async function writeLogFile(dir: string, name: string, logs: string) {
148+
if (logs.length === 0) {
149+
return;
150+
}
151+
await fs.writeFile(path.join(dir, name), logs);
152+
}
153+
154+
function extensionLogs(ctx: WorkspaceContext): string {
155+
return ctx.outputChannel.logs.join("\n");
156+
}
157+
158+
function settingsLogs(ctx: WorkspaceContext): string {
159+
const settings = JSON.stringify(vscode.workspace.getConfiguration("swift"), null, 2);
160+
return `${ctx.toolchain.diagnostics}\nSettings:\n${settings}`;
161+
}
162+
163+
function diagnosticLogs(): string {
164+
const diagnosticToString = (diagnostic: vscode.Diagnostic) => {
165+
return `${severityToString(diagnostic.severity)} - ${diagnostic.message} [Ln ${diagnostic.range.start.line}, Col ${diagnostic.range.start.character}]`;
166+
};
167+
168+
return vscode.languages
169+
.getDiagnostics()
170+
.map(
171+
([uri, diagnostics]) => `${uri}\n\t${diagnostics.map(diagnosticToString).join("\n\t")}`
172+
)
173+
.join("\n");
174+
}
175+
176+
function sourceKitLogs(ctx: WorkspaceContext) {
177+
return (ctx.languageClientManager.languageClientOutputChannel?.logs ?? []).join("\n");
178+
}
179+
180+
async function sourcekitDiagnose(ctx: WorkspaceContext, dir: string) {
181+
const sourcekitDiagnosticDir = path.join(dir, "sourcekit-lsp");
182+
await fs.mkdir(sourcekitDiagnosticDir);
183+
184+
const toolchainSourceKitLSP = ctx.toolchain.getToolchainExecutable("sourcekit-lsp");
185+
const lspConfig = configuration.lsp;
186+
const serverPathConfig = lspConfig.serverPath;
187+
const serverPath = serverPathConfig.length > 0 ? serverPathConfig : toolchainSourceKitLSP;
188+
189+
await vscode.window.withProgress(
190+
{
191+
location: vscode.ProgressLocation.Notification,
192+
},
193+
async progress => {
194+
progress.report({ message: "Diagnosing SourceKit-LSP..." });
195+
const writableStream = progressUpdatingWritable(percent =>
196+
progress.report({ message: `Diagnosing SourceKit-LSP: ${percent}%` })
197+
);
198+
199+
await execFileStreamOutput(
200+
serverPath,
201+
[
202+
"diagnose",
203+
"--bundle-output-path",
204+
sourcekitDiagnosticDir,
205+
"--toolchain",
206+
ctx.toolchain.toolchainPath,
207+
],
208+
writableStream,
209+
writableStream,
210+
null,
211+
{
212+
env: { ...process.env, ...configuration.swiftEnvironmentVariables },
213+
maxBuffer: 16 * 1024 * 1024,
214+
},
215+
ctx.currentFolder ?? undefined
216+
);
217+
}
218+
);
219+
}
220+
221+
function progressUpdatingWritable(updateProgress: (str: string) => void): Writable {
222+
return new Writable({
223+
write(chunk, encoding, callback) {
224+
const str = (chunk as Buffer).toString("utf8").trim();
225+
const percent = /^([0-9])+%/.exec(str);
226+
if (percent && percent[1]) {
227+
updateProgress(percent[1]);
228+
}
229+
230+
callback();
231+
},
232+
});
233+
}
234+
235+
function showDirectoryCommand(dir: string): string {
236+
switch (process.platform) {
237+
case "win32":
238+
return `explorer ${dir}`;
239+
case "darwin":
240+
return `open ${dir}`;
241+
default:
242+
return `xdg-open ${dir}`;
243+
}
244+
}
245+
246+
function showCommandType(): string {
247+
switch (process.platform) {
248+
case "win32":
249+
return "Explorer";
250+
case "darwin":
251+
return "Finder";
252+
default:
253+
return "File Manager";
254+
}
255+
}
256+
257+
function severityToString(severity: vscode.DiagnosticSeverity): string {
258+
switch (severity) {
259+
case vscode.DiagnosticSeverity.Error:
260+
return "Error";
261+
case vscode.DiagnosticSeverity.Warning:
262+
return "Warning";
263+
case vscode.DiagnosticSeverity.Information:
264+
return "Information";
265+
case vscode.DiagnosticSeverity.Hint:
266+
return "Hint";
267+
}
268+
}
269+
270+
function formatDateString(date: Date): string {
271+
const padZero = (num: number, length: number = 2) => num.toString().padStart(length, "0");
272+
273+
const year = date.getFullYear();
274+
const month = padZero(date.getMonth() + 1);
275+
const day = padZero(date.getDate());
276+
const hours = padZero(date.getHours());
277+
const minutes = padZero(date.getMinutes());
278+
const seconds = padZero(date.getSeconds());
279+
const timezoneOffset = -date.getTimezoneOffset();
280+
const timezoneSign = timezoneOffset >= 0 ? "+" : "-";
281+
const timezoneHours = padZero(Math.floor(Math.abs(timezoneOffset) / 60));
282+
const timezoneMinutes = padZero(Math.abs(timezoneOffset) % 60);
283+
return `${year}-${month}-${day}T${hours}-${minutes}-${seconds}${timezoneSign}${timezoneHours}-${timezoneMinutes}`;
284+
}

src/extension.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,12 @@ export interface Api {
5050
export async function activate(context: vscode.ExtensionContext): Promise<Api | undefined> {
5151
try {
5252
console.debug("Activating Swift for Visual Studio Code...");
53-
const outputChannel = new SwiftOutputChannel();
53+
const outputChannel = new SwiftOutputChannel("Swift");
5454

5555
checkAndWarnAboutWindowsSymlinks(outputChannel);
5656

5757
const toolchain: SwiftToolchain | undefined = await SwiftToolchain.create()
5858
.then(toolchain => {
59-
outputChannel.log(toolchain.swiftVersionString);
6059
toolchain.logDiagnostics(outputChannel);
6160
contextKeys.createNewProjectAvailable = toolchain.swiftVersion.isGreaterThanOrEqual(
6261
new Version(5, 8, 0)

0 commit comments

Comments
 (0)