diff --git a/CHANGELOG.md b/CHANGELOG.md index 256d1e69..f9557fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ ChangeLog +# V0.4.2 + +* Now you can toggle Hex mode for Registers and Variables independently from the Debug Panel. + * The title bar of the Registers View contains a button to toggle between Hex and Natural modes + + * In the Variable Window, you can right click on any variable and it allows you to toggle the Hex mode. This setting applies also applies to Watch variables. Too bad we don't have access to the title bar and there is no good way of implementing a per-variable format + + +* There is now a Refresh button in the title bars of the Registers and Peripheral Windows. The registers window, when refreshed will use the current stack/frame to retrieve the values. +* You can hover over a scalar variable name and get a tool-tip that gives you decimal, hex, octal and binary forms of the same. + +* Terminal input + * Support for `Paste` in RTT in RTT terminals (See Issue #463) + * Input entered into RTT Terminals can now have thier encoding be use using the `iencoding` option for the `console` and `binary` encoders +* Global variables are now sorted in the Variables Windows. But all variables starting with double underscores `__` are pushed to the bottom. # V0.4.1 + Minor bug fix. The `launch.json` option `clearSearch` was not working for `rttConfig`. Only affected OpenOCD users. # V0.4.0 diff --git a/images/hex-off-dark.svg b/images/hex-off-dark.svg new file mode 100644 index 00000000..07e8a355 --- /dev/null +++ b/images/hex-off-dark.svg @@ -0,0 +1,10 @@ + + + + 0x + diff --git a/images/hex-off-light.svg b/images/hex-off-light.svg new file mode 100644 index 00000000..7f07659d --- /dev/null +++ b/images/hex-off-light.svg @@ -0,0 +1,10 @@ + + + + 0x + diff --git a/images/hex-on-dark.svg b/images/hex-on-dark.svg new file mode 100644 index 00000000..9b92275c --- /dev/null +++ b/images/hex-on-dark.svg @@ -0,0 +1,10 @@ + + + + n + diff --git a/images/hex-on-light.svg b/images/hex-on-light.svg new file mode 100644 index 00000000..bb562189 --- /dev/null +++ b/images/hex-on-light.svg @@ -0,0 +1,10 @@ + + + + n + diff --git a/options-doc.py b/options-doc.py index 3f0dca04..eaf6a75c 100755 --- a/options-doc.py +++ b/options-doc.py @@ -52,7 +52,7 @@ def get_properties(pkg, type): # pp.pprint(categories) MISSING_ATTRIBUTES = ['extensionPath', - 'flattenAnonymous', 'registerUseNaturalFormat', 'toolchainPath'] + 'flattenAnonymous', 'registerUseNaturalFormat', 'variableUseNaturalFormat', 'toolchainPath'] all_properties = {**attach_properties, **launch_properties} diff --git a/package-lock.json b/package-lock.json index 55d964a9..3116ef67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cortex-debug", - "version": "0.4.1", + "version": "0.4.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cortex-debug", - "version": "0.4.1", + "version": "0.4.2", "dependencies": { "binary-parser": "^1.7.0", "bindings": "^1.5.0", diff --git a/package.json b/package.json index a9a4eb2b..1bef7e7d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.4.1", + "version": "0.4.2", "activationEvents": [ "onDebugResolve:cortex-debug" ], @@ -105,6 +105,11 @@ "type": "boolean", "default": true, "description": "If true, display registers in their natural format as specified by ARM. Hex otherwise." + }, + "cortex-debug.variableUseNaturalFormat": { + "type": "boolean", + "default": true, + "description": "If true, display variables in their natural format as specified by ARM. Hex otherwise." } } }, @@ -124,13 +129,51 @@ "title": "Copy Value", "icon": "$(files)" }, + { + "command": "cortex-debug.registers.refresh", + "title": "Refresh", + "icon": "$(refresh)" + }, + { + "command": "cortex-debug.registers.regHexModeTurnOff", + "title": "Enable Natural mode", + "icon": { + "light": "images/hex-on-light.svg", + "dark": "images/hex-on-dark.svg" + } + }, + { + "command": "cortex-debug.registers.regHexModeTurnOn", + "title": "Enable Hex mode", + "icon": { + "light": "images/hex-off-light.svg", + "dark": "images/hex-off-dark.svg" + } + }, + { + "command": "cortex-debug.registers.varHexModeTurnOff", + "title": "Enable Natural mode", + "icon": { + "light": "images/hex-on-light.svg", + "dark": "images/hex-on-dark.svg" + } + }, + { + "command": "cortex-debug.registers.varHexModeTurnOn", + "title": "Enable Hex mode", + "icon": { + "light": "images/hex-off-light.svg", + "dark": "images/hex-off-dark.svg" + } + }, { "command": "cortex-debug.peripherals.setFormat", "title": "Set Value Format" }, { "command": "cortex-debug.peripherals.forceRefresh", - "title": "Refresh" + "title": "Refresh", + "icon": "$(refresh)" }, { "command": "cortex-debug.peripherals.pin", @@ -614,19 +657,26 @@ }, "encoding": { "type": "string", - "description": "ascii, utf8, ucs2, utf16le are for 'console', rest are for 'binary' type data", - "default": "", + "description": "How binary data bytes are converted into a number. All little-endian", + "default": "unsigned", "enum": [ - "ascii", - "utf8", - "ucs2", - "utf16le", "unsigned", "signed", "Q16.16", "float" ] }, + "iencoding": { + "type": "string", + "description": "How keyoard input is encoded. Cooked mode only", + "default": "utf8", + "enum": [ + "ascii", + "utf8", + "ucs2", + "utf16le" + ] + }, "scale": { "default": 1, "description": "Binary only: This setting will scale the raw value from the ITM port by the specified value. Can be used, for example, to scale a raw n-bit ADC reading to a voltage value. (e.g to scale a 12-bit ADC reading to a 3.3v scale you would need a scale value of 3.3/4096 = 0.0008056640625", @@ -1428,19 +1478,26 @@ }, "encoding": { "type": "string", - "description": "ascii, utf8, ucs2, utf16le are for 'console', rest are for 'binary' type data", - "default": "", + "description": "How binary data bytes are converted into a number. All little-endian", + "default": "unsigned", "enum": [ - "ascii", - "utf8", - "ucs2", - "utf16le", "unsigned", "signed", "Q16.16", "float" ] }, + "iencoding": { + "type": "string", + "description": "How keyoard input is encoded Cooked mode only", + "default": "utf8", + "enum": [ + "ascii", + "utf8", + "ucs2", + "utf16le" + ] + }, "scale": { "default": 1, "description": "Binary only: This setting will scale the raw value from the ITM port by the specified value. Can be used, for example, to scale a raw n-bit ADC reading to a voltage value. (e.g to scale a 12-bit ADC reading to a 3.3v scale you would need a scale value of 3.3/4096 = 0.0008056640625", @@ -2044,7 +2101,40 @@ "when": "view == cortex-debug.peripherals" } ], - "view/title": [] + "view/title": [ + { + "command": "cortex-debug.registers.regHexModeTurnOn", + "when": "view == cortex-debug.registers && debugState == stopped && cortex-debug:registerUseNaturalFormat", + "group": "navigation" + }, + { + "command": "cortex-debug.registers.regHexModeTurnOff", + "when": "view == cortex-debug.registers && debugState == stopped && !cortex-debug:registerUseNaturalFormat", + "group": "navigation" + }, + { + "command": "cortex-debug.registers.refresh", + "when": "view == cortex-debug.registers && debugState == stopped", + "group": "navigation" + }, + { + "command": "cortex-debug.peripherals.forceRefresh", + "when": "view == cortex-debug.peripherals && debugState == stopped", + "group": "navigation" + } + ], + "debug/variables/context": [ + { + "command": "cortex-debug.registers.varHexModeTurnOn", + "when": "inDebugMode && debugType == 'cortex-debug' && debugState == stopped && cortex-debug:variableUseNaturalFormat", + "group": "navigation" + }, + { + "command": "cortex-debug.registers.varHexModeTurnOff", + "when": "inDebugMode && debugType == 'cortex-debug' && debugState == stopped && !cortex-debug:variableUseNaturalFormat", + "group": "navigation" + } + ] }, "views": { "debug": [ diff --git a/src/backend/backend.ts b/src/backend/backend.ts index 55205ab6..8ef580bb 100644 --- a/src/backend/backend.ts +++ b/src/backend/backend.ts @@ -1,6 +1,5 @@ import { MINode } from './mi_parse'; import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; - export interface Breakpoint { file?: string; line?: number; @@ -74,6 +73,42 @@ export class VariableObject { this.hasMore = !!MINode.valueOf(node, 'has_more'); } + public createToolTip(name: string, value: string): string { + let ret = this.type; + if (this.isCompound()) { + return ret; + } + + let val = 0; + if ((/^0[xX][0-9A-Fa-f]+/.test(value)) || /^[-]?[0-9]+/.test(value)) { + val = parseInt(value.toLowerCase()); + + ret += ' ' + name + ';' ; + if (this.value.startsWith('<')) { + console.log(this); + } + + ret += `\ndec: ${val}`; + if (this.value.startsWith('-')) { + val = val < 0 ? -val : val; + val = ((val ^ 0xffffffff) + 1) >>> 0; + } + let str = val.toString(16); + str = '0x' + '0'.repeat(8 - str.length) + str; + ret += `\nhex: ${str}`; + + str = val.toString(8); + str = '0'.repeat(12 - str.length) + str; + ret += `\noct: ${str}`; + + str = val.toString(2); + str = '0'.repeat(32 - str.length) + str; + str = str.substr(0, 8) + ' ' + str.substr(8, 8) + ' ' + str.substr(16, 8) + ' ' + str.substr(24, 8); + ret += `\nbin: ${str}`; + } + return ret; + } + public applyChanges(node: MINode) { this.value = MINode.valueOf(node, 'value'); if (!!MINode.valueOf(node, 'type_changed')) { @@ -101,9 +136,8 @@ export class VariableObject { }, variablesReference: this.id }; - if (this.displayhint) { - // res.kind = this.displayhint; - } + + res.type = this.createToolTip(res.name, res.value); // This ends up becoming a tool-tip return res; } } diff --git a/src/backend/mi2/mi2.ts b/src/backend/mi2/mi2.ts index 17e95c9d..0ec118a9 100644 --- a/src/backend/mi2/mi2.ts +++ b/src/backend/mi2/mi2.ts @@ -157,7 +157,7 @@ export class MI2 extends EventEmitter implements IBackend { this.emit('running', parsed); } else if (record.asyncClass === 'stopped') { - let reason = parsed.record('reason'); + const reason = parsed.record('reason'); if (trace) { this.log('stderr', 'stop: ' + reason); } @@ -181,7 +181,6 @@ export class MI2 extends EventEmitter implements IBackend { this.emit('exited-normally', parsed); } else { - let msg = 'Not implemented stop reason (assuming exception): '; if ((reason === undefined) && this.firstStop) { this.log('console', 'Program stopped, probably due to a reset and/or halt issued by debugger'); this.emit('stopped', parsed, 'entry'); @@ -323,18 +322,18 @@ export class MI2 extends EventEmitter implements IBackend { } public goto(filename: string, line: number): Thenable { - if (trace) { - this.log('stderr', 'goto'); - } - return new Promise((resolve, reject) => { - const target: string = '"' + (filename ? escape(filename) + ":" : "") + line.toString() + '"'; - this.sendCommand("break-insert -t " + target).then(() => { - this.sendCommand("exec-jump " + target).then((info) => { - resolve(info.resultRecords.resultClass === 'running'); - }, reject); - }, reject); - }); - } + if (trace) { + this.log('stderr', 'goto'); + } + return new Promise((resolve, reject) => { + const target: string = '"' + (filename ? escape(filename) + ':' : '') + line.toString() + '"'; + this.sendCommand('break-insert -t ' + target).then(() => { + this.sendCommand('exec-jump ' + target).then((info) => { + resolve(info.resultRecords.resultClass === 'running'); + }, reject); + }, reject); + }); + } public restart(commands: string[]): Thenable { if (trace) { diff --git a/src/backend/server.ts b/src/backend/server.ts index 28bed0c9..ef673370 100644 --- a/src/backend/server.ts +++ b/src/backend/server.ts @@ -74,7 +74,7 @@ export class GDBServer extends EventEmitter { }); } else { // For servers like BMP that are always running directly on the probe - this.connectToConsole(); + this.connectToConsole(); resolve(true); } }); @@ -173,7 +173,7 @@ export class GDBServer extends EventEmitter { if (this.consoleSocket) { this.consoleSocket.write(data); } else { - console.error('sendToConsole: console not open. How did this happen?') + console.error('sendToConsole: console not open. How did this happen?'); } } diff --git a/src/backend/symbols.ts b/src/backend/symbols.ts index f64c47de..d9471345 100644 --- a/src/backend/symbols.ts +++ b/src/backend/symbols.ts @@ -84,7 +84,7 @@ export class SymbolTable { const restored = this.deSerializeFileMaps(this.executable); const options = ['--syms']; if (!restored) { - options.push('-Wi'); // WARING! Creates super large output + options.push('-Wi'); // WARNING! Creates super large output } if (this.demangle) { options.push('-C'); @@ -137,6 +137,8 @@ export class SymbolTable { this.allSymbols.push(sym); } this.categorizeSymbols(); + this.sortGlobalVars(); + if (!restored) { this.serializeFileMaps(this.executable); } @@ -144,6 +146,22 @@ export class SymbolTable { catch (e) { } } + private sortGlobalVars() { + // We only sort globalVars. Want to preserve statics original order though. + this.globalVars.sort((a, b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'})); + + // double underscore variables are less interesting. Push it down to the bottom + const doubleUScores: SymbolInformation[] = []; + while (this.globalVars.length > 0) { + if (this.globalVars[0].name.startsWith('__')) { + doubleUScores.push(this.globalVars.shift()); + } else { + break; + } + } + this.globalVars = this.globalVars.concat(doubleUScores); + } + private categorizeSymbols() { for (const sym of this.allSymbols) { const scope = sym.scope; @@ -408,7 +426,7 @@ export class SymbolTable { } return null; - } + } public static NormalizePath(pathName: string): string { if (!pathName) { return pathName; } diff --git a/src/common.ts b/src/common.ts index 7668c3dd..f0e11637 100644 --- a/src/common.ts +++ b/src/common.ts @@ -4,6 +4,11 @@ import { EventEmitter } from 'events'; import { TcpPortScanner } from './tcpportscanner'; import { GDBServer } from './backend/server'; +export enum CortexDebugKeys { + REGISTER_DISPLAY_MODE = 'registerUseNaturalFormat', + VARIABLE_DISPLAY_MODE = 'variableUseNaturalFormat' +} + export enum NumberFormat { Auto = 0, Hexidecimal, @@ -78,17 +83,31 @@ export interface RTTCommonDecoderOpts { ports: number[]; } +export enum TextEncoding { + UTF8 = 'utf8', + UTF16LE = 'utf16le', + ASCII = 'ascii', + UCS2 = 'ucs2' +} + +export enum BinaryEncoding { + UNSIGNED = 'unsigned', + SIGNED = 'signed', + Q1616 = 'Q16.16', + FLOAT = 'float' +} export interface RTTConsoleDecoderOpts extends RTTCommonDecoderOpts { // Console options - encoding: string; // 'utf8', 'ascii', etc. - label: string; // label for window - prompt: string; // Prompt to use - noprompt: boolean;// disable prompt - noclear: boolean; // do not vlear screen buffer on connect - logfile: string; // log IO to file + label: string; // label for window + prompt: string; // Prompt to use + noprompt: boolean; // disable prompt + noclear: boolean; // do not vlear screen buffer on connect + logfile: string; // log IO to file inputmode: TerminalInputMode; + iencoding: TextEncoding; // Encoding used for input // Binary only options scale: number; + encoding: BinaryEncoding; } export class RTTConfigureEvent extends Event implements DebugProtocol.Event { @@ -141,7 +160,7 @@ export interface RTTConfiguration { } export interface ConfigurationArguments extends DebugProtocol.LaunchRequestArguments { - request: string, + request: string; toolchainPath: string; toolchainPrefix: string; executable: string; @@ -179,6 +198,7 @@ export interface ConfigurationArguments extends DebugProtocol.LaunchRequestArgum runToEntryPoint: string; flattenAnonymous: boolean; registerUseNaturalFormat: boolean; + variableUseNaturalFormat: boolean; numberOfProcessors: number; targetProcessor: number; @@ -256,7 +276,7 @@ export class RTTServerHelper { // a multple clients connect to the same channel. Perhaps in the future // it wil public rttPortsPending: number = 0; - public allocateRTTPorts(cfg: RTTConfiguration, startPort:number = 60000) { + public allocateRTTPorts(cfg: RTTConfiguration, startPort: number = 60000) { if (!cfg || !cfg.enabled || !cfg.decoders || cfg.decoders.length === 0) { return; } @@ -279,8 +299,8 @@ export class RTTServerHelper { this.rttPortsPending = Object.keys(this.rttLocalPortMap).length; const portFinderOpts = { min: startPort, max: startPort + 2000, retrieve: this.rttPortsPending, consecutive: false }; - TcpPortScanner.findFreePorts(portFinderOpts, GDBServer.LOCALHOST).then((ports) => { - this.rttPortsPending = 0; + TcpPortScanner.findFreePorts(portFinderOpts, GDBServer.LOCALHOST).then((ports) => { + this.rttPortsPending = 0; for (const dec of cfg.decoders) { if (dec.ports && (dec.ports.length > 0)) { this.rttPortsPending = this.rttPortsPending + dec.ports.length; @@ -296,7 +316,7 @@ export class RTTServerHelper { } else { let str = this.rttLocalPortMap[dec.port]; if (str === dummy) { - str = ports.shift().toString(); + str = ports.shift().toString(); this.rttLocalPortMap[dec.port] = str; } dec.tcpPort = str; @@ -315,7 +335,7 @@ export class RTTServerHelper { })); } } - } + } } } @@ -348,7 +368,7 @@ export function getAnyFreePort(preferred: number): Promise { resolve(ports[0]); }).catch((e) => { reject(e); - }); + }); } if (preferred > 0) { @@ -397,7 +417,7 @@ export class ResettableInterval { protected intervalId: NodeJS.Timeout; protected args: any[]; - constructor(protected cb: (...args) => void, protected interval:number, runNow: boolean = false, ...args) { + constructor(protected cb: (...args) => void, protected interval: number, runNow: boolean = false, ...args) { this.args = args; if (runNow) { this.cb(...this.args); @@ -427,7 +447,7 @@ export class ResettableTimeout { protected timeoutId: NodeJS.Timeout = null; protected args: any[]; - constructor(protected cb: (...args: any) => void, protected interval:number, ...args: any[]) { + constructor(protected cb: (...args: any) => void, protected interval: number, ...args: any[]) { this.args = args; this.timeoutId = setTimeout((...args) => { this.timeoutId = null; diff --git a/src/frontend/configprovider.ts b/src/frontend/configprovider.ts index 1bcbef8e..83da37b6 100644 --- a/src/frontend/configprovider.ts +++ b/src/frontend/configprovider.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import * as os from 'os'; import { STLinkServerController } from './../stlink'; import { GDBServerConsole } from './server_console'; +import { CortexDebugKeys } from '../common'; const OPENOCD_VALID_RTOS: string[] = ['eCos', 'ThreadX', 'FreeRTOS', 'ChibiOS', 'embKernel', 'mqx', 'uCOS-III', 'nuttx', 'auto']; const JLINK_VALID_RTOS: string[] = ['FreeRTOS', 'embOS', 'ChibiOS', 'Zephyr']; @@ -17,7 +18,7 @@ export class CortexDebugConfigurationProvider implements vscode.DebugConfigurati ): vscode.ProviderResult { if (GDBServerConsole.BackendPort <= 0) { vscode.window.showErrorMessage('GDB server console not yet ready. Please try again. Report this problem'); - return undefined; + return undefined; } config.gdbServerConsolePort = GDBServerConsole.BackendPort; @@ -141,7 +142,8 @@ export class CortexDebugConfigurationProvider implements vscode.DebugConfigurati } config.flattenAnonymous = configuration.flattenAnonymous; - config.registerUseNaturalFormat = configuration.registerUseNaturalFormat; + config.registerUseNaturalFormat = configuration.get(CortexDebugKeys.REGISTER_DISPLAY_MODE, true); + config.variableUseNaturalFormat = configuration.get(CortexDebugKeys.VARIABLE_DISPLAY_MODE, true); if (validationResponse) { vscode.window.showErrorMessage(validationResponse); @@ -343,7 +345,7 @@ export class CortexDebugConfigurationProvider implements vscode.DebugConfigurati else if (config.swoConfig.source !== 'socket' && !config.swoConfig.swoPath) { vscode.window.showWarningMessage(`SWO source type "${config.swoConfig.source}" requires a "swoPath". Disabling SWO support.`); config.swoConfig = { enabled: false }; - config.graphConfig = []; + config.graphConfig = []; } } diff --git a/src/frontend/extension.ts b/src/frontend/extension.ts index adeda6fa..381e393a 100644 --- a/src/frontend/extension.ts +++ b/src/frontend/extension.ts @@ -8,7 +8,7 @@ import { BaseNode, PeripheralBaseNode } from './views/nodes/basenode'; import { RTTCore, SWOCore } from './swo/core'; import { SWORTTSource } from './swo/sources/common'; -import { NumberFormat, ConfigurationArguments, RTTCommonDecoderOpts, RTTConsoleDecoderOpts } from '../common'; +import { NumberFormat, ConfigurationArguments, RTTCommonDecoderOpts, RTTConsoleDecoderOpts, CortexDebugKeys } from '../common'; import { MemoryContentProvider } from './memory_content_provider'; import Reporting from '../reporting'; @@ -36,7 +36,7 @@ export class CortexDebugExtension { private swoSource: SWORTTSource = null; private rttTerminals: RTTTerminal[] = []; private rttPortMap: { [channel: number]: SocketRTTSource} = {}; - private gdbServerConsole : GDBServerConsole = null; + private gdbServerConsole: GDBServerConsole = null; private finishedTerminalSetup = false; private peripheralProvider: PeripheralTreeProvider; @@ -48,6 +48,7 @@ export class CortexDebugExtension { private SVDDirectory: SVDInfo[] = []; private functionSymbols: SymbolInformation[] = null; + private debuggerStatus: 'started' | 'stopped' | 'running' | 'none'; constructor(private context: vscode.ExtensionContext) { this.startServerConsole(context); // Make this the first thing we do so it is ready for the session @@ -55,6 +56,7 @@ export class CortexDebugExtension { this.registerProvider = new RegisterTreeProvider(); this.memoryProvider = new MemoryContentProvider(); + this.debuggerStatus = 'none'; let tmp = []; try { const dirPath = path.join(context.extensionPath, 'data', 'SVDMap.json'); @@ -72,6 +74,12 @@ export class CortexDebugExtension { treeDataProvider: this.registerProvider }); + const config = vscode.workspace.getConfiguration('cortex-debug'); + vscode.commands.executeCommand('setContext', `cortex-debug:${CortexDebugKeys.REGISTER_DISPLAY_MODE}`, + config.get(CortexDebugKeys.REGISTER_DISPLAY_MODE, true)); + vscode.commands.executeCommand('setContext', `cortex-debug:${CortexDebugKeys.VARIABLE_DISPLAY_MODE}`, + config.get(CortexDebugKeys.VARIABLE_DISPLAY_MODE, true)); + context.subscriptions.push( vscode.workspace.registerTextDocumentContentProvider('examinememory', this.memoryProvider), vscode.workspace.registerTextDocumentContentProvider('disassembly', new DisassemblyContentProvider()), @@ -84,11 +92,17 @@ export class CortexDebugExtension { vscode.commands.registerCommand('cortex-debug.peripherals.unpin', this.peripheralsTogglePin.bind(this)), vscode.commands.registerCommand('cortex-debug.registers.copyValue', this.registersCopyValue.bind(this)), - + vscode.commands.registerCommand('cortex-debug.registers.refresh', this.registersRefresh.bind(this)), + vscode.commands.registerCommand('cortex-debug.registers.regHexModeTurnOn', this.registersNaturalMode.bind(this, false)), + vscode.commands.registerCommand('cortex-debug.registers.regHexModeTurnOff', this.registersNaturalMode.bind(this, true)), + vscode.commands.registerCommand('cortex-debug.registers.varHexModeTurnOn', this.variablesNaturalMode.bind(this, false)), + vscode.commands.registerCommand('cortex-debug.registers.varHexModeTurnOff', this.variablesNaturalMode.bind(this, true)), + vscode.commands.registerCommand('cortex-debug.examineMemory', this.examineMemory.bind(this)), vscode.commands.registerCommand('cortex-debug.viewDisassembly', this.showDisassembly.bind(this)), vscode.commands.registerCommand('cortex-debug.setForceDisassembly', this.setForceDisassembly.bind(this)), + vscode.workspace.onDidChangeConfiguration(this.settingsChanged.bind(this)), vscode.debug.onDidReceiveDebugSessionCustomEvent(this.receivedCustomEvent.bind(this)), vscode.debug.onDidStartDebugSession(this.debugSessionStarted.bind(this)), vscode.debug.onDidTerminateDebugSession(this.debugSessionTerminated.bind(this)), @@ -134,6 +148,29 @@ export class CortexDebugExtension { }); }); } + + private settingsChanged(e: vscode.ConfigurationChangeEvent) { + const msg = 'New format will take effect next time the program pauses'; + if (e.affectsConfiguration(`cortex-debug.${CortexDebugKeys.REGISTER_DISPLAY_MODE}`)) { + if (vscode.debug.activeDebugSession) { + if (this.debuggerStatus === 'running') { + vscode.window.showInformationMessage(msg); + } else { + this.registerProvider.refresh(); + } + } + } + if (e.affectsConfiguration(`cortex-debug.${CortexDebugKeys.VARIABLE_DISPLAY_MODE}`)) { + if (vscode.debug.activeDebugSession) { + const config = vscode.workspace.getConfiguration('cortex-debug'); + const isHex = config.get(CortexDebugKeys.VARIABLE_DISPLAY_MODE, true) ? false : true; + vscode.debug.activeDebugSession.customRequest('set-var-format', { hex: isHex }); + if (this.debuggerStatus === 'running') { + vscode.window.showInformationMessage(msg); + } + } + } + } private getSVDFile(device: string): string { const entry = this.SVDDirectory.find((de) => de.expression.test(device)); @@ -359,8 +396,9 @@ export class CortexDebugExtension { { label: 'Decimal', description: 'Format value in decimal', value: NumberFormat.Decimal }, { label: 'Binary', description: 'Format value in binary', value: NumberFormat.Binary } ]); - if (result === undefined) + if (result === undefined) { return; + } node.format = result.value; this.peripheralProvider.refresh(); @@ -368,9 +406,13 @@ export class CortexDebugExtension { } private async peripheralsForceRefresh(node: PeripheralBaseNode): Promise { - node.getPeripheral().updateData().then((e) => { + if (node) { + node.getPeripheral().updateData().then((e) => { + this.peripheralProvider.refresh(); + }); + } else { this.peripheralProvider.refresh(); - }); + } } private async peripheralsTogglePin(node: PeripheralBaseNode): Promise { @@ -388,6 +430,37 @@ export class CortexDebugExtension { } } + private registersRefresh(): void { + this.registerProvider.refresh(); + } + + // Settings changes + private registersNaturalMode(newVal: any) { + const config = vscode.workspace.getConfiguration('cortex-debug'); + + vscode.commands.executeCommand('setContext', `cortex-debug:${CortexDebugKeys.REGISTER_DISPLAY_MODE}`, newVal); + try { + config.update(CortexDebugKeys.REGISTER_DISPLAY_MODE, newVal); + } + catch (e) { + console.error(e); + } + } + + private variablesNaturalMode(newVal: boolean, cxt?: any) { + // 'cxt' contains the treeItem on which this menu was invoked. Maybe we can do something + // with it later + const config = vscode.workspace.getConfiguration('cortex-debug'); + + vscode.commands.executeCommand('setContext', `cortex-debug:${CortexDebugKeys.VARIABLE_DISPLAY_MODE}`, newVal); + try { + config.update(CortexDebugKeys.VARIABLE_DISPLAY_MODE, newVal); + } + catch (e) { + console.error(e); + } + } + // Debug Events private debugSessionStarted(session: vscode.DebugSession) { if (session.type !== 'cortex-debug') { return; } @@ -403,6 +476,7 @@ export class CortexDebugExtension { } this.functionSymbols = null; + this.debuggerStatus = 'started'; session.customRequest('get-arguments').then((args) => { let svdfile = args.svdFile; @@ -413,7 +487,7 @@ export class CortexDebugExtension { Reporting.beginSession(args as ConfigurationArguments); if (this.swoSource) { this.initializeSWO(args); } - if (Object.keys(this.rttPortMap).length > 0) { this.initializeRTT(args); } + if (Object.keys(this.rttPortMap).length > 0) { this.initializeRTT(args); } this.registerProvider.debugSessionStarted(); this.peripheralProvider.debugSessionStarted(svdfile ? svdfile : null); @@ -429,6 +503,7 @@ export class CortexDebugExtension { try { Reporting.endSession(); + this.debuggerStatus = 'none'; this.registerProvider.debugSessionTerminated(); this.peripheralProvider.debugSessionTerminated(); if (this.swo) { @@ -446,6 +521,7 @@ export class CortexDebugExtension { // if a connection never happened. term.inUse = false; } + // tslint:disable-next-line: forin for (const ch in this.rttPortMap) { this.rttPortMap[ch].dispose(); } @@ -486,6 +562,7 @@ export class CortexDebugExtension { } private receivedStopEvent(e) { + this.debuggerStatus = 'stopped'; this.peripheralProvider.debugStopped(); this.registerProvider.debugStopped(); vscode.workspace.textDocuments.filter((td) => td.fileName.endsWith('.cdmem')) @@ -495,6 +572,7 @@ export class CortexDebugExtension { } private receivedContinuedEvent(e) { + this.debuggerStatus = 'running'; this.peripheralProvider.debugContinued(); this.registerProvider.debugContinued(); if (this.swo) { this.swo.debugContinued(); } @@ -547,7 +625,7 @@ export class CortexDebugExtension { if (!decoder.ports) { this.createRTTSource(decoder.tcpPort, decoder.port); } else { - for (var ix = 0; ix < decoder.ports.length; ix = ix + 1) { + for (let ix = 0; ix < decoder.ports.length; ix = ix + 1) { // Hopefully ports and tcpPorts are a matched set this.createRTTSource(decoder.tcpPorts[ix], decoder.ports[ix]); } @@ -615,8 +693,8 @@ export class CortexDebugExtension { if (!output.endsWith('\n')) { output += '\n'; } if (!this.adapterOutputChannel) { this.adapterOutputChannel = vscode.window.createOutputChannel('Adapter Output'); - this.adapterOutputChannel.appendLine("DEPRECATED: Please check the 'TERMINALS' tab for 'gdb-server' output. " + - "This 'Adapter Output' will not appear in future releases"); + this.adapterOutputChannel.appendLine('DEPRECATED: Please check the \'TERMINALS\' tab for \'gdb-server\' output. ' + + 'This \'Adapter Output\' will not appear in future releases'); this.adapterOutputChannel.show(); } else if (this.clearAdapterOutputChannel) { this.adapterOutputChannel.clear(); diff --git a/src/frontend/pty.ts b/src/frontend/pty.ts index 276e7fc6..b6445605 100644 --- a/src/frontend/pty.ts +++ b/src/frontend/pty.ts @@ -6,7 +6,7 @@ import { ResettableTimeout, TerminalInputMode } from '../common'; export interface IPtyTerminalOptions { name: string; // Name of the terminal prompt: string; // Prompt to be used - inputMode: TerminalInputMode + inputMode: TerminalInputMode; } export const ESC = '\x1b'; // ASCII escape character @@ -28,17 +28,19 @@ for (let ix = zero; ix <= 'Z'.charCodeAt(0); ix++) { } class ACTIONS { - static cursorUp(n=1) { return CSI + n.toString() + 'A'; } - static cursorDown(n=1) { return CSI + n.toString() + 'B'; } - static cursorForward(n=1) { return CSI + n.toString() + 'C'; } - static cursorBack(n=1) { return CSI + n.toString() + 'D'; } - static clearAll() { return CSI + '2J' + CSI + '3J' + CSI + ';H'; } // Kill entire buffer and set cursor postion to 1,1 - static deleteChar() { return CSI + 'P'; } - static deletePrevChar() { return ACTIONS.cursorBack() + ACTIONS.deleteChar(); } - static deleteCurrChar() { return ACTIONS.deleteChar(); } - static killLineForward() { return CSI + 'K'; } - static killLine(n=0) { return (n ? ACTIONS.cursorBack(n) : '') + ACTIONS.killLineForward(); } -}; + public static cursorUp(n = 1) { return n > 0 ? CSI + n.toString() + 'A' : ''; } + public static cursorDown(n = 1) { return n > 0 ? CSI + n.toString() + 'B' : ''; } + public static cursorForward(n = 1) { return n > 0 ? CSI + n.toString() + 'C' : ''; } + public static cursorBack(n = 1) { return n > 0 ? CSI + n.toString() + 'D' : ''; } + public static clearAll() { return CSI + '2J' + CSI + '3J' + CSI + ';H'; } // Kill entire buffer and set cursor postion to 1,1 + public static clearScreen() { return CSI + '2J' + CSI + ';H'; } // Kill the visible part of the screen + public static deleteChar() { return CSI + 'P'; } + public static deletePrevChar() { return ACTIONS.cursorBack() + ACTIONS.deleteChar(); } + public static deleteCurrChar() { return ACTIONS.deleteChar(); } + public static killLineForward() { return CSI + 'K'; } + public static killLine(n = 0) { return ACTIONS.cursorBack(n) + ACTIONS.killLineForward(); } + public static killEntireLine() { return CSI + '2K'; } +} export function magentaWrite(msg: string, pty: PtyTerminal) { if (pty) { @@ -46,7 +48,6 @@ export function magentaWrite(msg: string, pty: PtyTerminal) { } } - /* ** The following events generated by this class ** @@ -71,10 +72,11 @@ export class PtyTerminal extends EventEmitter { protected promptTimer: ResettableTimeout = null; public isReady = false; protected pendingWrites: any[] = []; + private suspendPrompting: boolean = false; - static oldOnes: { [name: string]: PtyTerminal } = {}; + private static oldOnes: { [name: string]: PtyTerminal } = {}; - readonly pty: vscode.Pseudoterminal = { + private readonly pty: vscode.Pseudoterminal = { onDidWrite: this.writeEmitter.event, // onDidOverrideDimensions?: vscode.Event; // onDidClose?: vscode.Event; @@ -85,13 +87,13 @@ export class PtyTerminal extends EventEmitter { throw new Error('Method not implemented.'); } */ - handleInput: (data:string) => { this.handleInput(data); } + handleInput: (data: string) => { this.handleInput(data); } /* setDimensions?(dimensions: vscode.TerminalDimensions): void { throw new Error('Method not implemented.'); } */ - } + }; constructor(protected options: IPtyTerminalOptions) { super(); @@ -128,7 +130,7 @@ export class PtyTerminal extends EventEmitter { this.terminal = null; } - static findExisting(name: string): PtyTerminal { + public static findExisting(name: string): PtyTerminal { return PtyTerminal.oldOnes[name]; } @@ -156,36 +158,28 @@ export class PtyTerminal extends EventEmitter { this.write('\n'); // This will write also prompt } - protected handleInput(chr: string): void { + protected handleInput(chars: string): void { if (this.isPaused || (this.options.inputMode === TerminalInputMode.DISABLED)) { return; } try { - switch (chr) { + switch (chars) { case KEYS.enter: - this.handleReturn(chr); + this.handleReturn(chars); break; case KEYS.del: if (this.options.inputMode === TerminalInputMode.COOKED) { this.killPrevChar(); } else { - this.emit('data', chr); + this.emit('data', chars); } break; default: - if (!this.handleSpecialChar(chr) && (chr.length === 1)) { - // Handle special chars and leave the rest. If the char.length is not 1 - // it is something special - this.curLine = PtyTerminal.insertCharsAt(this.curLine, chr, this.cursorPos-1); - this.writeEmitter.fire(ACTIONS.killLineForward()); - const tail = this.curLine.slice(this.cursorPos-1); - this.writeEmitter.fire(tail); - if (tail.length > 1) { - this.writeEmitter.fire(ACTIONS.cursorBack(tail.length-1)); - } - this.cursorPos += chr.length; + if (!this.handleSpecialChars(chars)) { + // Handle special chars and leave the rest. + this.handleOtherChars(chars); } - break + break; } } catch (e) { @@ -196,31 +190,110 @@ export class PtyTerminal extends EventEmitter { private handleReturn(chr: string) { if (this.options.inputMode === TerminalInputMode.COOKED) { this.emit('data', this.curLine + os.EOL); - this.curLine = ''; - this.cursorPos = 1; - this.write('\r\n'); } else { this.emit('data', chr); - if (this.options.inputMode === TerminalInputMode.RAWECHO) { - this.write('\r\n'); + } + + if (this.options.inputMode !== TerminalInputMode.RAW) { + this.writeEmitter.fire('\r\n'); + this.cursorPos = 1; + this.curLine = ''; + this.didPrompt = false; + this.doPrompt(); + } + } + + /* + ** Most special chars are already handled. This function handles the rest. At this point + ** We have keystrokes or a flood of characters from a Paste operation. This can get complicated + ** as what is pasted can also have unprintable characters. We have a choice + ** + ** 1. Reject the entire stream of chars + ** 2. Filter the chars + ** 3. Just pass it on to the program and let the string to Buffer translation deal. No loss of data + ** + ** For now, we will go chose Option #3. This is far from perfect and we will revisit. + ** + */ + private handleOtherChars(str: string) { + // We are only expecting COOKED mode here. Everything else should have already been handled + const lines = str.split(/\r\n|\r|\n/); + if (lines.length > 1) { + if (this.promptTimer) { + this.promptTimer.kill(); + this.promptTimer = null; } + // The last line decides if there will be a prompt or not. Otherwise, no prompt + // emitted until all the lines are consumed + this.didPrompt = false; + this.suspendPrompting = true; } + for (let ix = 0; ix < lines.length; ix++ ) { + const line = lines[ix]; + const isLastLine = (ix === (lines.length - 1)); + if ((line === '') && isLastLine) { + // Last line will be empty if the input string ended with a newline + this.suspendPrompting = false; + this.doPrompt(); + break; + } + let tail = this.curLine.slice(this.cursorPos - 1); + this.curLine = PtyTerminal.insertCharsAt(this.curLine, line, this.cursorPos - 1); + this.writeEmitter.fire(ACTIONS.killLineForward()); + if (!isLastLine) { + this.writeEmitter.fire(line); + this.cursorPos += line.length; + this.curLine = (tail.length > 0) ? this.curLine.slice(0, -tail.length) : this.curLine; + this.handleReturn(KEYS.enter); + if (tail.length > 0) { + // We carry the tail with us to the next line but cursor position does not change + // Which at this point should be at the beginning of the next line. + this.writeEmitter.fire(tail + ACTIONS.cursorBack(tail.length)); + this.curLine = tail; + } + } else { + tail = this.curLine.slice(this.cursorPos - 1); + this.writeEmitter.fire(tail); + const count = tail.length - line.length; + this.writeEmitter.fire(ACTIONS.cursorBack(count)); + this.cursorPos += line.length; + } + } + this.suspendPrompting = false; } - protected handleSpecialChar(chr: string): boolean { + /* + ** Handle character key sequences. Most terminals are variations of xterm which in turn + ** are variations of a VT100. Here we are focussed only on termnial input and not what + ** are supposed to be ouput ANSI sequences. + ** + ** Handle commin CSI + Char codes + ** Hanele (most) keys that are less than 0x20 (SPACE) + ** + ** In the above, we ignore what we have not implemented but pretend that it has been handled + ** Insterad of passing it on to the program as normal input. Example. we don't handle a TAB (yet) + ** Neither do we handle Page Up/Down but they have a CSI prefix so again we ignore such things + ** as they do not make sense for line editing. + ** + ** See https://www.xfree86.org/current/ctlseqs.html + ** + ** Note that Enter and Del have already been handled + */ + protected handleSpecialChars(chars: string): boolean { if (this.options.inputMode !== TerminalInputMode.COOKED){ if (this.options.inputMode === TerminalInputMode.RAWECHO) { - this.writeEmitter.fire(chr); + this.writeEmitter.fire(chars); } - this.emit('data', chr); + this.emit('data', chars); return true; } - let code = chr.charCodeAt(0); + let code = chars.charCodeAt(0); if (code === 27) { // Esc character - if ((chr[1] !== '[') || (chr.length !== 3)) { - return true; + if ((chars[1] !== '[') || (chars.length !== 3)) { + // Function keys and some others fall into this category + return false; } - switch (chr[2]) { + switch (chars[2]) { case 'A': { // UP: TODO: use for history break; } @@ -235,11 +308,19 @@ export class PtyTerminal extends EventEmitter { this.moveLeft(); break; } + case 'H': { // Home + this.moveToBeg(); + break; + } + case 'E': { // End + this.moveToEnd(); + break; + } } return true; - } else if ((chr.length === 1) && (code < 0x20)) { - chr = String.fromCharCode(code += 0x40); - switch (chr) { + } else if ((chars.length === 1) && (code < 0x20)) { + chars = String.fromCharCode(code += 0x40); + switch (chars) { case 'C': { this.emit('break'); break; @@ -288,20 +369,30 @@ export class PtyTerminal extends EventEmitter { this.killEntireLine(); break; } + case 'L': { + this.clearScreen(); + break; + } } return true; } else { return false; - } + } + } + + private clearScreen() { + this.writeEmitter.fire(ACTIONS.clearScreen()); + this.curLine = ''; + this.cursorPos = 1; + this.didPrompt = false; + this.doPrompt(); } private killEntireLine() { const n = this.cursorPos - 1; - if (n > 0) { - this.writeEmitter.fire(ACTIONS.killLine(n)); - this.cursorPos = 1; - this.curLine = ''; - } + this.writeEmitter.fire(ACTIONS.killLine(n)); + this.cursorPos = 1; + this.curLine = ''; } private killLineFromCursor() { @@ -357,20 +448,20 @@ export class PtyTerminal extends EventEmitter { } } - static removeCharAt(str: string, ix:number): string { + private static removeCharAt(str: string, ix: number): string { if (ix === 0) { return str.slice(1); } else if (ix >= (str.length - 1)) { - return str.slice(0,-1); + return str.slice(0, -1); } else { - return str.slice(0, ix) + str.slice(ix+1); + return str.slice(0, ix) + str.slice(ix + 1); } } - static insertCharsAt(str: string, chr: string, ix:number): string { + private static insertCharsAt(str: string, chr: string, ix: number): string { if (ix === 0) { return chr + str; - } else if (ix >= (str.length - 1)) { + } else if (ix >= str.length) { return str + chr; } else { return str.slice(0, ix) + chr + str.slice(ix); @@ -393,7 +484,6 @@ export class PtyTerminal extends EventEmitter { if ((typeof data !== 'string') && !(data instanceof String)) { data = data.toString('utf8'); } - const endsWithNl = data.endsWith('\n'); data = data.replace(/[\r]?\n/g, '\r\n'); this.writeEmitter.fire(data); if (data.endsWith('\n')) { @@ -409,13 +499,14 @@ export class PtyTerminal extends EventEmitter { // When we prompt, we not only write the prompt but also any remaining input protected doPrompt() { - if (!this.didPrompt) { + if (!this.didPrompt && !this.suspendPrompting) { if (this.promptTimer === null) { this.promptTimer = new ResettableTimeout(() => { const str = this.options.prompt + this.curLine; if (str.length) { this.writeEmitter.fire(str); } + this.cursorPos = this.curLine.length + 1; this.didPrompt = true; }, 100); } else { @@ -427,8 +518,10 @@ export class PtyTerminal extends EventEmitter { // When we unPrompt, we not only erase the prompt but any remaining input protected unPrompt() { if (this.didPrompt) { - const len = this.options.prompt.length + this.curLine.length; - this.writeEmitter.fire(ACTIONS.killLine(len)); + const len = this.options.prompt.length + this.cursorPos - 1; + this.writeEmitter.fire(ACTIONS.killEntireLine()); + this.writeEmitter.fire(ACTIONS.cursorBack(len)); + this.writeEmitter.fire(this.curLine); this.didPrompt = false; } } diff --git a/src/frontend/rtt_terminal.ts b/src/frontend/rtt_terminal.ts index c8691b2b..56a7241c 100644 --- a/src/frontend/rtt_terminal.ts +++ b/src/frontend/rtt_terminal.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; -import { RTTConsoleDecoderOpts, TerminalInputMode } from '../common'; +import { RTTConsoleDecoderOpts, TerminalInputMode, TextEncoding, BinaryEncoding } from '../common'; import { IPtyTerminalOptions, magentaWrite, PtyTerminal, RESET } from './pty'; import { decoders as DECODER_MAP } from './swo/decoders/utils'; import { SocketRTTSource } from './swo/sources/socket'; @@ -22,10 +22,10 @@ export class RTTTerminal { src: SocketRTTSource) { this.ptyOptions = this.createTermOptions(null); this.createTerminal(); + this.sanitizeEncodings(this.options); this.binaryFormatter = new BinaryFormatter(this.ptyTerm, this.options.encoding, this.options.scale); this.connectToSource(src); this.openLogFile(); - setTimeout(() => this.terminal.show(), 100); } private connectToSource(src: SocketRTTSource) { @@ -34,7 +34,7 @@ export class RTTTerminal { const code: string = (e as any).code; if (code === 'ECONNRESET') { // Server closed the connection. We are done with this session - this.source = null; + this.source = null; } else if (code === 'ECONNREFUSED') { // We expect 'ECONNREFUSED' if the server has not yet started. magentaWrite(`${e.message}\nPlease report this problem.`, this.ptyTerm); @@ -62,7 +62,7 @@ export class RTTTerminal { this.source = null; this.inUse = false; if (!this.options.noclear && (this.logFd >= 0)) { - try { fs.closeSync(this.logFd); } catch { }; + try { fs.closeSync(this.logFd); } catch { } } this.logFd = -1; this.ptyTerm.write(RESET + '\n'); @@ -94,7 +94,7 @@ export class RTTTerminal { catch (e) { const msg = `Could not open file ${this.options.logfile} for writing. ${e.toString()}`; console.error(msg); - magentaWrite(msg, this.ptyTerm) + magentaWrite(msg, this.ptyTerm); } } } @@ -102,14 +102,14 @@ export class RTTTerminal { private writeNonBinary(buf: Buffer) { let start = 0; for (let ix = 1; ix < buf.length; ix++ ) { - if (buf[ix-1] !== 0xff) { continue; } + if (buf[ix - 1] !== 0xff) { continue; } const chr = buf[ix]; if (((chr >= 48) && (chr <= 57)) || ((chr >= 65) && (chr <= 90))) { if (ix >= 1) { - this.ptyTerm.write(buf.slice(start, ix-1)); + this.ptyTerm.write(buf.slice(start, ix - 1)); } this.ptyTerm.write(`\n`); - buf = buf.slice(ix+1); + buf = buf.slice(ix + 1); ix = 0; start = 0; } @@ -124,7 +124,7 @@ export class RTTTerminal { name: RTTTerminal.createTermName(this.options, existing), prompt: this.createPrompt(), inputMode: this.options.inputmode || TerminalInputMode.COOKED - } + }; return ret; } @@ -135,11 +135,11 @@ export class RTTTerminal { } protected createPrompt(): string { - return this.options.noprompt ? '' : this.options.prompt || `RTT:${this.options.port}> ` + return this.options.noprompt ? '' : this.options.prompt || `RTT:${this.options.port}> `; } - static createTermName(options: RTTConsoleDecoderOpts, existing: string | null): string { - const suffix = options.type === 'binary' ? `enc:${getEncoding(options.encoding)}` : options.type; + protected static createTermName(options: RTTConsoleDecoderOpts, existing: string | null): string { + const suffix = options.type === 'binary' ? `enc:${getBinaryEncoding(options.encoding)}` : options.type; const orig = options.label || `RTT Ch:${options.port} ${suffix}`; let ret = orig; let count = 1; @@ -157,9 +157,13 @@ export class RTTTerminal { this.dispose(); } - public sendData(str: string) { + public sendData(str: string | Buffer) { if (this.source) { try { + if (((typeof str === 'string') || (str instanceof String)) && + (this.options.inputmode === TerminalInputMode.COOKED)) { + str = Buffer.from(str as string, this.options.iencoding); + } this.source.write(str); } catch (e) { @@ -168,10 +172,16 @@ export class RTTTerminal { } } + private sanitizeEncodings(obj: RTTConsoleDecoderOpts) { + obj.encoding = getBinaryEncoding(obj.encoding); + obj.iencoding = getTextEncoding(obj.iencoding); + } + // If all goes well, this will reset the terminal options. Label for the VSCode terminal has to match // since there no way to rename it. If successful, tt will reset the Terminal options and mark it as // used (inUse = true) as well public tryReuse(options: RTTConsoleDecoderOpts, src: SocketRTTSource): boolean { + this.sanitizeEncodings(this.options); const newTermName = RTTTerminal.createTermName(options, this.ptyOptions.name); if (newTermName === this.ptyOptions.name) { this.inUse = true; @@ -198,10 +208,10 @@ export class RTTTerminal { return false; } - dispose() { + public dispose() { this.ptyTerm.dispose(); if (this.logFd >= 0) { - try { fs.closeSync(this.logFd); } catch {}; + try { fs.closeSync(this.logFd); } catch {} this.logFd = -1; } } @@ -219,14 +229,21 @@ function padLeft(str: string, len: number, chr = ' '): string { return str; } -function getEncoding(enc: string): string { - const encodings: string[] = ['signed', 'unsigned', 'Q16.16', 'float']; - if (!enc || this.encodings.indexOf(enc) < 0) { - enc = 'unsigned'; +function getBinaryEncoding(enc: string): BinaryEncoding { + enc = enc ? enc.toLowerCase() : ''; + if (!(enc in BinaryEncoding)) { + enc = BinaryEncoding.UNSIGNED; } - return enc; + return enc as BinaryEncoding; } +function getTextEncoding(enc: string): TextEncoding { + enc = enc ? enc.toLowerCase() : ''; + if (!(enc in TextEncoding)) { + return TextEncoding.UTF8; + } + return enc as TextEncoding; +} class BinaryFormatter { private readonly bytesNeeded = 4; private buffer = Buffer.alloc(4); @@ -237,15 +254,15 @@ class BinaryFormatter { protected encoding: string, protected scale: number) { this.bytesRead = 0; - this.encoding = getEncoding(encoding); + this.encoding = getBinaryEncoding(encoding); this.scale = scale || 1; } public writeBinary(input: string | Buffer) { - let data: Buffer = Buffer.from(input); + const data: Buffer = Buffer.from(input); const date = new Date(); - for (let ix = 0; ix < data.length; ix = ix + 1) { - this.buffer[this.bytesRead] = data[ix]; + for (const chr of data) { + this.buffer[this.bytesRead] = chr; this.bytesRead = this.bytesRead + 1; if (this.bytesRead === this.bytesNeeded) { let chars = ''; @@ -253,8 +270,8 @@ class BinaryFormatter { if (byte <= 32 || (byte >= 127 && byte <= 159)) { chars += '.'; } else { - chars += String.fromCharCode(byte); - } + chars += String.fromCharCode(byte); + } } const blah = this.buffer.toString(); const hexvalue = padLeft(this.buffer.toString('hex'), 8, '0'); diff --git a/src/frontend/server_console.ts b/src/frontend/server_console.ts index 972eba1e..7ede2c63 100644 --- a/src/frontend/server_console.ts +++ b/src/frontend/server_console.ts @@ -13,7 +13,7 @@ export class GDBServerConsole { public ptyTerm: PtyTerminal = null; protected ptyOptions: IPtyTerminalOptions; - static BackendPort: number = -1; + public static BackendPort: number = -1; constructor(public context: vscode.ExtensionContext) { this.ptyOptions = { @@ -21,7 +21,7 @@ export class GDBServerConsole { prompt : '', // Can't have a prompt since the gdb-server or semihosting may have one inputMode : TerminalInputMode.COOKED }; - this.ptyOptions.name = GDBServerConsole.createTermName(this.ptyOptions.name, null) + this.ptyOptions.name = GDBServerConsole.createTermName(this.ptyOptions.name, null); this.setupTerminal(); setTimeout(() => { this.ptyTerm.terminal.show(); @@ -60,7 +60,7 @@ export class GDBServerConsole { if (true) { try { msg = 'SERVER CONSOLE DEBUG: ' + msg; - console.log(msg) + console.log(msg); if (this.ptyTerm) { msg += msg.endsWith('\n') ? '' : '\n'; magentaWrite(msg, this.ptyTerm); @@ -73,7 +73,7 @@ export class GDBServerConsole { // Create a server for the GDBServer running in the adapter process. Any data // from the gdb-server (like OpenOCD) is sent here and sent to the terminal // and any usr input in the terminal is sent back (like semi-hosting) - public startServer() : Promise { + public startServer(): Promise { return new Promise((resolve, reject) => { getAnyFreePort(55878).then((p) => { this.toBackendPort = p; @@ -119,7 +119,7 @@ export class GDBServerConsole { fs.writeFileSync(this.logFd, data.toString()); } } - catch (_e) { + catch (e) { this.logFd = -1; } }); @@ -139,7 +139,7 @@ export class GDBServerConsole { this.ptyTerm.clearTerminalBuffer(); } - static createTermName(want: string, existing: string | null): string { + private static createTermName(want: string, existing: string | null): string { let ret = want; let count = 1; while (vscode.window.terminals.findIndex((t) => t.name === ret) >= 0) { diff --git a/src/frontend/swo/decoders/binary.ts b/src/frontend/swo/decoders/binary.ts index 79ee84bf..005bb444 100644 --- a/src/frontend/swo/decoders/binary.ts +++ b/src/frontend/swo/decoders/binary.ts @@ -33,7 +33,7 @@ export class SWOBinaryProcessor implements SWORTTDecoder { } private createVSCodeTerminal(config: SWOBinaryDecoderConfig) { - const options : IPtyTerminalOptions = { + const options: IPtyTerminalOptions = { name: this.createName(config), prompt: '', inputMode: TerminalInputMode.DISABLED diff --git a/src/frontend/swo/decoders/console.ts b/src/frontend/swo/decoders/console.ts index af67e486..6205b10f 100644 --- a/src/frontend/swo/decoders/console.ts +++ b/src/frontend/swo/decoders/console.ts @@ -41,7 +41,7 @@ export class SWOConsoleProcessor implements SWORTTDecoder { } private createVSCodeTerminal(config: SWOConsoleDecoderConfig) { - const options : IPtyTerminalOptions = { + const options: IPtyTerminalOptions = { name: this.createName(config), prompt: '', inputMode: TerminalInputMode.DISABLED diff --git a/src/frontend/views/registers.ts b/src/frontend/views/registers.ts index 3c551d22..a82e1295 100644 --- a/src/frontend/views/registers.ts +++ b/src/frontend/views/registers.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import { NodeSetting } from '../../common'; +import { CortexDebugKeys, NodeSetting } from '../../common'; import { RegisterNode, RegisterValue } from './nodes/registernode'; import { MessageNode } from './nodes/messagenode'; import { BaseNode } from './nodes/basenode'; @@ -38,7 +38,10 @@ export class RegisterTreeProvider implements TreeDataProvider { } public _refreshRegisterValues() { - debug.activeDebugSession.customRequest('read-registers').then((data) => { + const config = vscode.workspace.getConfiguration('cortex-debug'); + const val = config.get(CortexDebugKeys.REGISTER_DISPLAY_MODE); + const args = { hex: !val }; + debug.activeDebugSession.customRequest('read-registers', args).then((data) => { data.forEach((reg) => { const index = parseInt(reg.number, 10); const regNode = this.registerMap[index]; diff --git a/src/gdb.ts b/src/gdb.ts index 0bdbb982..a9116b70 100644 --- a/src/gdb.ts +++ b/src/gdb.ts @@ -7,7 +7,7 @@ import { DebugProtocol } from 'vscode-debugprotocol'; import { MI2 } from './backend/mi2/mi2'; import { hexFormat } from './frontend/utils'; import { Breakpoint, Variable, VariableObject, MIError } from './backend/backend'; -import { TelemetryEvent, ConfigurationArguments, StoppedEvent, GDBServerController, AdapterOutputEvent, DisassemblyInstruction, createPortName } from './common'; +import { TelemetryEvent, ConfigurationArguments, StoppedEvent, GDBServerController, AdapterOutputEvent, DisassemblyInstruction, createPortName, CortexDebugKeys } from './common'; import { GDBServer } from './backend/server'; import { MINode } from './backend/mi_parse'; import { expandValue, isExpandable } from './backend/gdb_expansion'; @@ -33,6 +33,7 @@ import { ExternalServerController } from './external'; import { SymbolTable } from './backend/symbols'; import { SymbolInformation, SymbolScope, SymbolType } from './symbols'; import { TcpPortScanner } from './tcpportscanner'; +import { nextTick } from 'process'; const SERVER_TYPE_MAP = { jlink: JLinkServerController, @@ -170,7 +171,7 @@ export class GDBDebugSession extends DebugSession { response.body.supportsEvaluateForHovers = true; response.body.supportsSetVariable = true; response.body.supportsRestartRequest = true; - response.body.supportsGotoTargetsRequest = true; + response.body.supportsGotoTargetsRequest = true; this.sendResponse(response); } @@ -190,8 +191,8 @@ export class GDBDebugSession extends DebugSession { const rttSym = this.symbolTable.getGlobalOrStaticVarByName(symName); if (!rttSym) { this.args.rttConfig.enabled = false; - this.handleMsg('stderr', `Could not find symbol '${symName}' in executable. ` + - `Make sure you complile/link with debug ON or you can specify your own RTT address\n`); + this.handleMsg('stderr', `Could not find symbol '${symName}' in executable. ` + + 'Make sure you complile/link with debug ON or you can specify your own RTT address\n'); } else { const searchStr = this.args.rttConfig.searchId || 'SEGGER RTT'; this.args.rttConfig.address = '0x' + rttSym.address.toString(16); @@ -348,7 +349,7 @@ export class GDBDebugSession extends DebugSession { this.sendErrorResponse( response, 107, - `GDB Server Console tcp port is undefined.` + 'GDB Server Console tcp port is undefined.' ); return; } @@ -404,6 +405,10 @@ export class GDBDebugSession extends DebugSession { commands.push('interpreter-exec console "set print asm-demangle on"'); } + if (!this.args.variableUseNaturalFormat) { + commands.push(...this.formatRadixGdbCommand()); + } + try { commands.push(...this.serverController.initCommands()); @@ -620,8 +625,14 @@ export class GDBDebugSession extends DebugSession { if (this.stopped === false) { return ; } this.writeMemoryRequestCustom(response, args['address'], args['data']); break; + case 'set-var-format': + // if (this.stopped === false) { return ; } + this.args.variableUseNaturalFormat = (args && args.hex) ? false : true; + this.setGdbOutputRadix(); + break; case 'read-registers': if (this.stopped === false) { return ; } + this.args.registerUseNaturalFormat = (args && args.hex) ? false : true; this.readRegistersRequest(response); break; case 'read-register-list': @@ -650,6 +661,30 @@ export class GDBDebugSession extends DebugSession { } } + protected setGdbOutputRadix() { + for (const cmd of this.formatRadixGdbCommand()) { + this.miDebugger.sendCommand(cmd); + } + if (this.stopped) { + // We area already stopped but this fakes a stop again which referhes all debugger windows + // We don't have a way to only referesh portions. It is all or nothing, there is a bit + // of screen flashing and causes changes in GUI contexts (stack for instance) + this.sendEvent(new StoppedEvent(this.stoppedReason, this.currentThreadId, true)); + } + } + + private formatRadixGdbCommand(forced: string | null = null): string[] { + // radix setting affects future inerpretations of values, so format it unambigiously with hex values + const radix = forced || (this.args.variableUseNaturalFormat ? '0xa' : '0x10'); + // If we set just the output radix, it will affect setting values. Always leave input radix in decimal + // Also, don't understand why setting the output-radix modifies the input radix as well + const cmds = [ + `interpreter-exec console "set output-radix ${radix}"`, + 'interpreter-exec console "set input-radix 0xa"' + ]; + return cmds; + } + protected async disassembleRequest(response: DebugProtocol.Response, args: any): Promise { if (args.function) { try { @@ -787,6 +822,14 @@ export class GDBDebugSession extends DebugSession { } protected readRegistersRequest(response: DebugProtocol.Response) { + if (!this.args.variableUseNaturalFormat) { + // requesting a radix on the register-values does not work unless the output radix is + // decimal. bug in gdb I think. We temporarily force to decimal and then restore later + for (const cmd of this.formatRadixGdbCommand('0xa')) { + this.miDebugger.sendCommand(cmd); + } + } + const fmt = this.args.registerUseNaturalFormat ? 'N' : 'x'; this.miDebugger.sendCommand(`data-list-register-values ${fmt}`).then((node) => { if (node.resultRecords.resultClass === 'done') { @@ -810,6 +853,12 @@ export class GDBDebugSession extends DebugSession { this.sendErrorResponse(response, 115, `Unable to read registers: ${error.toString()}`); this.sendEvent(new TelemetryEvent('Error', 'Reading Registers', '')); }); + + if (!this.args.variableUseNaturalFormat) { + for (const cmd of this.formatRadixGdbCommand()) { + this.miDebugger.sendCommand(cmd); + } + } } protected readRegisterListRequest(response: DebugProtocol.Response) { @@ -1064,7 +1113,7 @@ export class GDBDebugSession extends DebugSession { this.activeThreadIds.clear(); } - protected stopEvent(info: MINode, reason:string = 'exception') { + protected stopEvent(info: MINode, reason: string = 'exception') { if (!this.started) { this.crashed = true; } if (!this.quit) { this.stopped = true; @@ -2133,20 +2182,20 @@ export class GDBDebugSession extends DebugSession { } protected gotoTargetsRequest(response: DebugProtocol.GotoTargetsResponse, args: DebugProtocol.GotoTargetsArguments): void { - this.miDebugger.goto(args.source.path, args.line).then(done => { - response.body = { - targets: [{ - id: 1, - label: args.source.name, - column: args.column, - line : args.line - }] - }; - this.sendResponse(response); - }, msg => { - this.sendErrorResponse(response, 16, `Could not jump to: ${msg}`); - }); - } + this.miDebugger.goto(args.source.path, args.line).then((done) => { + response.body = { + targets: [{ + id: 1, + label: args.source.name, + column: args.column, + line: args.line + }] + }; + this.sendResponse(response); + }, (msg) => { + this.sendErrorResponse(response, 16, `Could not jump to: ${msg}`); + }); + } } function prettyStringArray(strings) {