Files
pioarduino-vscode-debug/src/frontend/diagnostics.ts
T
2026-05-01 22:03:05 +02:00

497 lines
16 KiB
TypeScript

import * as vscode from 'vscode'
/**
* Represents an action that can be taken when an error occurs.
*/
export interface ErrorAction {
label: string
callback: () => void
}
/**
* Log entry for diagnostic output.
*/
export interface LogEntry {
timestamp: Date
level: 'debug' | 'info' | 'warn' | 'error'
source: string
message: string
details?: string
}
/**
* Error patterns for common GDB/MI errors with suggested actions.
*/
interface ErrorPattern {
pattern: RegExp
message: string
actions: ErrorAction[]
severity: 'error' | 'warning'
}
/**
* Centralized diagnostics and error handling for the debug extension.
* Provides structured error messages, logging, and actionable error recovery.
*/
export class DiagnosticsManager {
private outputChannel: vscode.OutputChannel
private logEntries: LogEntry[] = []
private maxLogEntries: number = 1000
private errorPatterns: ErrorPattern[] = []
private showDevDebugOutput: boolean = false
constructor() {
this.outputChannel = vscode.window.createOutputChannel('PlatformIO Debug Diagnostics')
this.initializeErrorPatterns()
}
/**
* Sets whether to show detailed debug output.
*/
setShowDevDebugOutput(enabled: boolean): void {
this.showDevDebugOutput = enabled
}
/**
* Logs a message to the diagnostic output.
*/
log(level: LogEntry['level'], source: string, message: string, details?: string): void {
const entry: LogEntry = {
timestamp: new Date(),
level,
source,
message,
details
}
this.logEntries.push(entry)
// Maintain log size limit
if (this.logEntries.length > this.maxLogEntries) {
this.logEntries.shift()
}
// Always log errors and warnings; log info in normal mode; log debug only if dev output enabled
if (level === 'error' || level === 'warn' || level === 'info' || this.showDevDebugOutput) {
const timestamp = entry.timestamp.toISOString().split('T')[1].split('.')[0]
const logLine = `[${timestamp}] [${level.toUpperCase()}] [${source}] ${message}`
this.outputChannel.appendLine(logLine)
if (details && this.showDevDebugOutput) {
this.outputChannel.appendLine(` Details: ${details}`)
}
}
}
/**
* Logs a debug message.
*/
debug(source: string, message: string, details?: string): void {
this.log('debug', source, message, details)
}
/**
* Logs an informational message.
*/
info(source: string, message: string, details?: string): void {
this.log('info', source, message, details)
}
/**
* Logs a warning message.
*/
warn(source: string, message: string, details?: string): void {
this.log('warn', source, message, details)
}
/**
* Logs an error message.
*/
error(source: string, message: string, details?: string): void {
this.log('error', source, message, details)
}
/**
* Shows the diagnostic output channel.
*/
showOutputChannel(): void {
this.outputChannel.show()
}
/**
* Clears the diagnostic log.
*/
clearLog(): void {
this.logEntries = []
this.outputChannel.clear()
}
/**
* Gets all log entries, optionally filtered by level.
*/
getLogEntries(level?: LogEntry['level']): LogEntry[] {
if (level) {
return this.logEntries.filter(entry => entry.level === level)
}
return [...this.logEntries]
}
/**
* Exports the diagnostic log to a string for bug reports.
*/
exportLog(): string {
const lines: string[] = [
'PlatformIO Debug Diagnostic Log',
`Generated: ${new Date().toISOString()}`,
`Total Entries: ${this.logEntries.length}`,
'---'
]
for (const entry of this.logEntries) {
const timestamp = entry.timestamp.toISOString()
lines.push(`[${timestamp}] [${entry.level.toUpperCase()}] [${entry.source}] ${entry.message}`)
if (entry.details) {
lines.push(` Details: ${entry.details}`)
}
}
return lines.join('\n')
}
/**
* Shows an error message with optional actions.
*/
showError(message: string, actions?: ErrorAction[]): void {
this.error('DiagnosticsManager', `Showing error: ${message}`)
if (actions && actions.length > 0) {
const actionLabels = actions.map(a => a.label)
Promise.resolve(vscode.window.showErrorMessage(message, ...actionLabels))
.then(selected => {
if (selected) {
const action = actions.find(a => a.label === selected)
if (action) {
this.info('DiagnosticsManager', `Executing action: ${action.label}`)
try { action.callback() } catch (err) {
this.error('DiagnosticsManager', `Error dialog action failed: ${err}`)
}
}
}
})
.catch(err => this.error('DiagnosticsManager', `Error dialog promise failed: ${err}`))
} else {
vscode.window.showErrorMessage(message)
}
}
/**
* Shows a warning message with optional actions.
*/
showWarning(message: string, actions?: ErrorAction[]): void {
this.warn('DiagnosticsManager', `Showing warning: ${message}`)
if (actions && actions.length > 0) {
const actionLabels = actions.map(a => a.label)
Promise.resolve(vscode.window.showWarningMessage(message, ...actionLabels))
.then(selected => {
if (selected) {
const action = actions.find(a => a.label === selected)
if (action) {
try { action.callback() } catch (err) {
this.error('DiagnosticsManager', `Warning dialog action failed: ${err}`)
}
}
}
})
.catch(err => this.error('DiagnosticsManager', `Warning dialog promise failed: ${err}`))
} else {
vscode.window.showWarningMessage(message)
}
}
/**
* Shows an informational message with optional actions.
*/
showInfo(message: string, actions?: ErrorAction[]): void {
this.info('DiagnosticsManager', `Showing info: ${message}`)
if (actions && actions.length > 0) {
const actionLabels = actions.map(a => a.label)
Promise.resolve(vscode.window.showInformationMessage(message, ...actionLabels))
.then(selected => {
if (selected) {
const action = actions.find(a => a.label === selected)
if (action) {
try { action.callback() } catch (err) {
this.error('DiagnosticsManager', `Info dialog action failed: ${err}`)
}
}
}
})
.catch(err => this.error('DiagnosticsManager', `Info dialog promise failed: ${err}`))
} else {
vscode.window.showInformationMessage(message)
}
}
/**
* Handles a GDB/MI error by matching against known patterns.
* Returns true if the error was handled by a pattern, false otherwise.
*/
handleGDBError(errorMessage: string): boolean {
this.error('GDB', 'Error received', errorMessage)
for (const pattern of this.errorPatterns) {
if (pattern.pattern.test(errorMessage)) {
this.info('DiagnosticsManager', `Matched error pattern: ${pattern.message}`)
if (pattern.severity === 'error') {
this.showError(pattern.message, pattern.actions)
} else {
this.showWarning(pattern.message, pattern.actions)
}
return true
}
}
// No pattern matched, show generic error
this.showError(`Debug error: ${errorMessage}`, [
{
label: 'Show Diagnostics',
callback: () => this.showOutputChannel()
},
{
label: 'Copy Error',
callback: () => {
vscode.env.clipboard.writeText(errorMessage)
this.showInfo('Error message copied to clipboard')
}
}
])
return false
}
/**
* Handles connection errors with specific troubleshooting steps.
*/
handleConnectionError(error: string, host?: string, port?: number): void {
this.error('Connection', `Connection error to ${host}:${port}`, error)
this.showError(
`Failed to connect to debug server${host && port ? ` at ${host}:${port}` : ''}.`,
[
{
label: 'Check Connection',
callback: () => {
this.showInfo(
'Troubleshooting:\n' +
'1. Verify target device is connected\n' +
'2. Check OpenOCD/GDB server is running\n' +
'3. Verify port and host settings\n' +
'4. Check firewall settings'
)
}
},
{
label: 'Restart Debug',
callback: () => {
vscode.commands.executeCommand('workbench.action.debug.restart')
}
},
{
label: 'Show Diagnostics',
callback: () => this.showOutputChannel()
}
]
)
}
/**
* Handles SVD file loading errors.
*/
handleSVDError(error: string, svdPath?: string): void {
this.error('SVD', `SVD file error${svdPath ? ` for ${svdPath}` : ''}`, error)
const message = svdPath
? `Failed to load SVD file: ${svdPath}`
: 'Failed to load SVD file'
this.showError(message, [
{
label: 'Locate SVD File',
callback: () => {
vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
filters: {
'SVD Files': ['svd'],
'All Files': ['*']
}
}).then(uri => {
if (uri && uri[0]) {
// Notify extension to reload with new SVD path
vscode.commands.executeCommand('platformio-debug.reloadSVD', uri[0].fsPath)
}
})
}
},
{
label: 'Skip SVD Load',
callback: () => {
this.showInfo('SVD loading skipped. Peripheral view will not be available.')
}
}
])
}
/**
* Handles memory access errors.
*/
handleMemoryError(error: string, address?: number): void {
this.error('Memory', `Memory access error${address !== undefined ? ` at 0x${address.toString(16)}` : ''}`, error)
const message = address !== undefined
? `Cannot access memory at address 0x${address.toString(16)}`
: 'Cannot access memory'
this.showError(message, [
{
label: 'Target May Not Be Halted',
callback: () => {
this.showInfo(
'To access memory, the target must be halted.\n' +
'Try pausing execution first (F6) or setting a breakpoint.'
)
}
},
{
label: 'Check Address',
callback: () => {
this.showInfo(
'Verify the memory address is valid:\n' +
'1. Check device memory map\n' +
'2. Verify address is accessible\n' +
'3. Check if region requires special permissions'
)
}
}
])
}
/**
* Initializes the error pattern matchers.
*/
private initializeErrorPatterns(): void {
this.errorPatterns = [
{
pattern: /connection\s+(refused|failed|timed\s+out)/i,
message: 'Debug server connection failed. The target may not be connected.',
severity: 'error',
actions: [
{
label: 'Check Connection',
callback: () => this.showConnectionTroubleshooting()
},
{
label: 'Retry',
callback: () => vscode.commands.executeCommand('workbench.action.debug.restart')
}
]
},
{
pattern: /no\s+such\s+file\s+or\s+directory/i,
message: 'Required file not found. Check your project configuration.',
severity: 'error',
actions: [
{
label: 'Open Settings',
callback: () => vscode.commands.executeCommand('workbench.action.openSettings', 'platformio')
}
]
},
{
pattern: /cannot\s+access\s+memory/i,
message: 'Cannot access target memory. The target may not be halted.',
severity: 'error',
actions: [
{
label: 'Pause Target',
callback: () => vscode.commands.executeCommand('workbench.action.debug.pause')
},
{
label: 'Set Breakpoint',
callback: () => vscode.commands.executeCommand('editor.debug.action.toggleBreakpoint')
}
]
},
{
pattern: /remote\s+replied\s+with\s+error/i,
message: 'GDB server protocol error. There may be a version mismatch.',
severity: 'warning',
actions: [
{
label: 'Show Diagnostics',
callback: () => this.showOutputChannel()
}
]
},
{
pattern: /unrecognized\s+command/i,
message: 'Unrecognized GDB command. Check GDB server compatibility.',
severity: 'warning',
actions: [
{
label: 'Check GDB Version',
callback: () => this.showInfo('Ensure GDB version is compatible with your target.')
}
]
}
]
}
/**
* Shows connection troubleshooting information.
*/
private showConnectionTroubleshooting(): void {
this.showInfo(
'Connection Troubleshooting:\n\n' +
'1. Verify target device is connected via USB/JTAG\n' +
'2. Check OpenOCD or GDB server is running\n' +
'3. Verify port settings in launch.json\n' +
'4. Check for driver issues (Zadig for Windows)\n' +
'5. Ensure correct permissions (Linux: udev rules)',
[
{
label: 'Open Documentation',
callback: () => {
vscode.env.openExternal(vscode.Uri.parse('https://docs.platformio.org/en/latest/plus/debugging.html'))
}
}
]
)
}
}
/**
* Singleton instance of the diagnostics manager.
*/
let diagnosticsManager: DiagnosticsManager | null = null
/**
* Gets the singleton diagnostics manager instance.
* Creates the instance on first call.
*/
export function getDiagnosticsManager(): DiagnosticsManager {
if (!diagnosticsManager) {
diagnosticsManager = new DiagnosticsManager()
}
return diagnosticsManager
}
/**
* Resets the singleton instance (useful for testing).
*/
export function resetDiagnosticsManager(): void {
diagnosticsManager = null
}