-
Notifications
You must be signed in to change notification settings - Fork 79
Add Swift: Capture VSCode Swift Diagnostic Logs command #830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
8ce3f59
Add Swift: Capture VSCode Swift Diagnostic Logs command
plemarquand 4bdc0b5
Add open diagnostic folder button to windows/linux
plemarquand eb80a0d
Capture swift settings in diagnostics
plemarquand 63aea21
Capture sourcekit-lsp diagnostics
plemarquand a9a8461
Fixup test after rebase
plemarquand a1a0a31
Dont print sourcekit-lsp logs to console
plemarquand e69024f
Use sourcekit-lsp diagnose if available
plemarquand 21f2c8c
Address comments
plemarquand 3dbbfb4
Refactor RollingLog
plemarquand a023fa2
Handle dialog cancellation
plemarquand 4d9533f
Add extension settings back to bundle
plemarquand 5c59e6b
Rework Diagnosing SourceKit-LSP message
plemarquand c5c6529
Prompt to capture diagnostics if sourcekit-lsp crashes
plemarquand 7059eb9
Address comments
plemarquand 4abee57
Only capture source code diagnostics in full capture mode
plemarquand 3df51ce
Fix licence header
plemarquand 7d21e9f
More robust check for sourcekit crash notification
plemarquand 60979e2
Prompt for diagnostics at most once an hour
plemarquand File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,284 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the VS Code Swift open source project | ||
// | ||
// Copyright (c) 2021-2024 the VS Code Swift project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import * as fs from "fs/promises"; | ||
import * as path from "path"; | ||
import * as vscode from "vscode"; | ||
import { tmpdir } from "os"; | ||
import { exec } from "child_process"; | ||
import { Writable } from "stream"; | ||
import { WorkspaceContext } from "../WorkspaceContext"; | ||
import { Version } from "../utilities/version"; | ||
import { execFileStreamOutput } from "../utilities/utilities"; | ||
import configuration from "../configuration"; | ||
|
||
export async function captureDiagnostics( | ||
ctx: WorkspaceContext, | ||
allowMinimalCapture: boolean = true | ||
) { | ||
try { | ||
const captureMode = await captureDiagnosticsMode(ctx, allowMinimalCapture); | ||
|
||
// dialog was cancelled | ||
if (!captureMode) { | ||
return; | ||
} | ||
|
||
const diagnosticsDir = path.join( | ||
tmpdir(), | ||
`vscode-diagnostics-${formatDateString(new Date())}` | ||
); | ||
|
||
await fs.mkdir(diagnosticsDir); | ||
await writeLogFile(diagnosticsDir, "extension-logs.txt", extensionLogs(ctx)); | ||
await writeLogFile(diagnosticsDir, "settings.txt", settingsLogs(ctx)); | ||
|
||
if (captureMode === "Full") { | ||
await writeLogFile(diagnosticsDir, "source-code-diagnostics.txt", diagnosticLogs()); | ||
|
||
// The `sourcekit-lsp diagnose` command is only available in 6.0 and higher. | ||
if (ctx.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0))) { | ||
await sourcekitDiagnose(ctx, diagnosticsDir); | ||
} else { | ||
await writeLogFile(diagnosticsDir, "sourcekit-lsp.txt", sourceKitLogs(ctx)); | ||
} | ||
} | ||
|
||
ctx.outputChannel.log(`Saved diagnostics to ${diagnosticsDir}`); | ||
await showCapturedDiagnosticsResults(diagnosticsDir); | ||
} catch (error) { | ||
vscode.window.showErrorMessage(`Unable to capture diagnostic logs: ${error}`); | ||
} | ||
} | ||
|
||
export async function promptForDiagnostics(ctx: WorkspaceContext) { | ||
const ok = "OK"; | ||
const cancel = "Cancel"; | ||
const result = await vscode.window.showInformationMessage( | ||
"SourceKit-LSP has been restored. Would you like to capture a diagnostic bundle to file an issue?", | ||
ok, | ||
cancel | ||
); | ||
|
||
if (!result || result === cancel) { | ||
return; | ||
} | ||
|
||
return await captureDiagnostics(ctx, false); | ||
} | ||
|
||
async function captureDiagnosticsMode( | ||
ctx: WorkspaceContext, | ||
allowMinimalCapture: boolean | ||
): Promise<"Minimal" | "Full" | undefined> { | ||
if ( | ||
ctx.swiftVersion.isGreaterThanOrEqual(new Version(6, 0, 0)) || | ||
vscode.workspace.getConfiguration("sourcekit-lsp").get<string>("trace.server", "off") !== | ||
"off" | ||
) { | ||
const fullButton = allowMinimalCapture ? "Capture Full Diagnostics" : "Capture Diagnostics"; | ||
const minimalButton = "Capture Minimal Diagnostics"; | ||
const buttons = allowMinimalCapture ? [fullButton, minimalButton] : [fullButton]; | ||
const fullCaptureResult = await vscode.window.showInformationMessage( | ||
`A Diagnostic Bundle collects information that helps the developers of the Swift for VS Code extension diagnose and fix issues. | ||
|
||
This information contains: | ||
- Extension logs | ||
- Versions of Swift installed on your system | ||
- Crash logs from SourceKit | ||
- Log messages emitted by SourceKit | ||
- If possible, a minimized project that caused SourceKit to crash | ||
- If possible, a minimized project that caused the Swift compiler to crash | ||
|
||
All information is collected locally and you can inspect the diagnose bundle before sharing it with developers of the Swift for VS Code extension. | ||
|
||
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.`, | ||
{ | ||
modal: true, | ||
detail: allowMinimalCapture | ||
? `If you wish to omit potentially sensitive information choose "${minimalButton}"` | ||
: undefined, | ||
}, | ||
...buttons | ||
); | ||
if (!fullCaptureResult) { | ||
return undefined; | ||
} | ||
|
||
return fullCaptureResult === fullButton ? "Full" : "Minimal"; | ||
} else { | ||
return "Minimal"; | ||
} | ||
} | ||
|
||
async function showCapturedDiagnosticsResults(diagnosticsDir: string) { | ||
const showInFinderButton = `Show In ${showCommandType()}`; | ||
const copyPath = "Copy Path to Clipboard"; | ||
const result = await vscode.window.showInformationMessage( | ||
`Saved diagnostic logs to ${diagnosticsDir}`, | ||
showInFinderButton, | ||
copyPath | ||
); | ||
if (result === copyPath) { | ||
vscode.env.clipboard.writeText(diagnosticsDir); | ||
} else if (result === showInFinderButton) { | ||
exec(showDirectoryCommand(diagnosticsDir), error => { | ||
// Opening the explorer on windows returns an exit code of 1 despite opening successfully. | ||
if (error && process.platform !== "win32") { | ||
vscode.window.showErrorMessage( | ||
`Failed to open ${showCommandType()}: ${error.message}` | ||
); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
async function writeLogFile(dir: string, name: string, logs: string) { | ||
if (logs.length === 0) { | ||
return; | ||
} | ||
await fs.writeFile(path.join(dir, name), logs); | ||
} | ||
|
||
function extensionLogs(ctx: WorkspaceContext): string { | ||
return ctx.outputChannel.logs.join("\n"); | ||
} | ||
|
||
function settingsLogs(ctx: WorkspaceContext): string { | ||
const settings = JSON.stringify(vscode.workspace.getConfiguration("swift"), null, 2); | ||
return `${ctx.toolchain.diagnostics}\nSettings:\n${settings}`; | ||
} | ||
|
||
function diagnosticLogs(): string { | ||
const diagnosticToString = (diagnostic: vscode.Diagnostic) => { | ||
return `${severityToString(diagnostic.severity)} - ${diagnostic.message} [Ln ${diagnostic.range.start.line}, Col ${diagnostic.range.start.character}]`; | ||
}; | ||
|
||
return vscode.languages | ||
.getDiagnostics() | ||
.map( | ||
([uri, diagnostics]) => `${uri}\n\t${diagnostics.map(diagnosticToString).join("\n\t")}` | ||
) | ||
.join("\n"); | ||
} | ||
|
||
function sourceKitLogs(ctx: WorkspaceContext) { | ||
return (ctx.languageClientManager.languageClientOutputChannel?.logs ?? []).join("\n"); | ||
} | ||
|
||
async function sourcekitDiagnose(ctx: WorkspaceContext, dir: string) { | ||
const sourcekitDiagnosticDir = path.join(dir, "sourcekit-lsp"); | ||
await fs.mkdir(sourcekitDiagnosticDir); | ||
|
||
const toolchainSourceKitLSP = ctx.toolchain.getToolchainExecutable("sourcekit-lsp"); | ||
const lspConfig = configuration.lsp; | ||
const serverPathConfig = lspConfig.serverPath; | ||
const serverPath = serverPathConfig.length > 0 ? serverPathConfig : toolchainSourceKitLSP; | ||
plemarquand marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
await vscode.window.withProgress( | ||
{ | ||
location: vscode.ProgressLocation.Notification, | ||
}, | ||
async progress => { | ||
progress.report({ message: "Diagnosing SourceKit-LSP..." }); | ||
const writableStream = progressUpdatingWritable(percent => | ||
progress.report({ message: `Diagnosing SourceKit-LSP: ${percent}%` }) | ||
); | ||
|
||
await execFileStreamOutput( | ||
serverPath, | ||
[ | ||
"diagnose", | ||
"--bundle-output-path", | ||
sourcekitDiagnosticDir, | ||
"--toolchain", | ||
ctx.toolchain.toolchainPath, | ||
], | ||
writableStream, | ||
writableStream, | ||
null, | ||
{ | ||
env: { ...process.env, ...configuration.swiftEnvironmentVariables }, | ||
maxBuffer: 16 * 1024 * 1024, | ||
}, | ||
ctx.currentFolder ?? undefined | ||
); | ||
} | ||
); | ||
} | ||
|
||
function progressUpdatingWritable(updateProgress: (str: string) => void): Writable { | ||
return new Writable({ | ||
write(chunk, encoding, callback) { | ||
const str = (chunk as Buffer).toString("utf8").trim(); | ||
const percent = /^([0-9])+%/.exec(str); | ||
if (percent && percent[1]) { | ||
updateProgress(percent[1]); | ||
} | ||
|
||
callback(); | ||
}, | ||
}); | ||
} | ||
|
||
function showDirectoryCommand(dir: string): string { | ||
switch (process.platform) { | ||
case "win32": | ||
return `explorer ${dir}`; | ||
case "darwin": | ||
return `open ${dir}`; | ||
default: | ||
return `xdg-open ${dir}`; | ||
} | ||
} | ||
|
||
function showCommandType(): string { | ||
switch (process.platform) { | ||
case "win32": | ||
return "Explorer"; | ||
case "darwin": | ||
return "Finder"; | ||
default: | ||
return "File Manager"; | ||
} | ||
} | ||
|
||
function severityToString(severity: vscode.DiagnosticSeverity): string { | ||
switch (severity) { | ||
case vscode.DiagnosticSeverity.Error: | ||
return "Error"; | ||
case vscode.DiagnosticSeverity.Warning: | ||
return "Warning"; | ||
case vscode.DiagnosticSeverity.Information: | ||
return "Information"; | ||
case vscode.DiagnosticSeverity.Hint: | ||
return "Hint"; | ||
} | ||
} | ||
|
||
function formatDateString(date: Date): string { | ||
const padZero = (num: number, length: number = 2) => num.toString().padStart(length, "0"); | ||
|
||
const year = date.getFullYear(); | ||
const month = padZero(date.getMonth() + 1); | ||
const day = padZero(date.getDate()); | ||
const hours = padZero(date.getHours()); | ||
const minutes = padZero(date.getMinutes()); | ||
const seconds = padZero(date.getSeconds()); | ||
const timezoneOffset = -date.getTimezoneOffset(); | ||
const timezoneSign = timezoneOffset >= 0 ? "+" : "-"; | ||
const timezoneHours = padZero(Math.floor(Math.abs(timezoneOffset) / 60)); | ||
const timezoneMinutes = padZero(Math.abs(timezoneOffset) % 60); | ||
return `${year}-${month}-${day}T${hours}-${minutes}-${seconds}${timezoneSign}${timezoneHours}-${timezoneMinutes}`; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.