From 281c3740ff37bb2ccaec7cb79b795a7a07153ef3 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Wed, 28 Feb 2024 19:21:03 +0100 Subject: [PATCH 1/7] Support storing and applying of memory content Add commands to store and apply memory content as Intel HEX file - Encapsulate behavior in new MemoryStorage class - Trigger 'store' from Memory view, Variables view and command palette - Trigger 'apply' from Memory view, Explorer view and command palette - Use nrf-intel-hex library for read/write file licensed under BSD-3 Use quick inputs to guide user through necessary input - Initialize as much of the input as possible through command args Communicate with webview through messenger requests and notifications -- Request to trigger store and apply from webview -- Notify webview about any written memory so it can update properly Minor improvements - Move some common types and functionality into 'common' area - Avoid bleeding Debug Adapter types into webview, use messaging types - Common style: 'getVariables' -> 'getVariablesType' - Provide utility functions and types for debug requests - Fix 'Enter' handling for numpad by checking key value of event Closes #50 --- media/options-widget.css | 7 +- package.json | 40 +++- src/common/debug-requests.ts | 39 ++++ src/common/intel-hex.ts | 43 ++++ src/common/memory-range.ts | 10 +- src/common/memory.ts | 74 +++++++ src/common/messaging.ts | 37 +++- src/entry-points/browser/extension.ts | 3 + src/entry-points/desktop/extension.ts | 3 + src/plugin/adapter-registry/c-tracker.ts | 7 +- src/plugin/memory-provider.ts | 35 +++- src/plugin/memory-storage.ts | 194 ++++++++++++++++++ src/plugin/memory-webview-main.ts | 88 ++++---- src/webview/columns/address-column.tsx | 2 +- src/webview/columns/ascii-column.ts | 2 +- .../columns/column-contribution-service.ts | 7 +- src/webview/columns/data-column.tsx | 3 +- src/webview/components/memory-table.tsx | 8 +- src/webview/components/memory-widget.tsx | 11 +- src/webview/components/options-widget.tsx | 86 ++++---- src/webview/memory-webview-view.tsx | 104 +++++++--- src/webview/utils/view-types.ts | 14 +- src/webview/variables/variable-decorations.ts | 7 +- yarn.lock | 10 + 24 files changed, 672 insertions(+), 162 deletions(-) create mode 100644 src/common/debug-requests.ts create mode 100644 src/common/intel-hex.ts create mode 100644 src/common/memory.ts create mode 100644 src/plugin/memory-storage.ts diff --git a/media/options-widget.css b/media/options-widget.css index 5275d33..73538a7 100644 --- a/media/options-widget.css +++ b/media/options-widget.css @@ -35,10 +35,11 @@ flex-grow: 1; } +.memory-options-widget .p-button { + align-self: end; +} + .memory-options-widget .edit-label-toggle { - position: absolute; - right: 24px; - top: 8px; opacity: 0; transition: opacity 0.2s; } diff --git a/package.json b/package.json index 98525a6..4b04c96 100644 --- a/package.json +++ b/package.json @@ -37,13 +37,15 @@ "formik": "^2.4.5", "lodash": "^4.17.21", "memoize-one": "^6.0.0", + "nrf-intel-hex": "^1.4.0", "primeflex": "^3.3.1", "primereact": "^10.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", "vscode-messenger": "^0.4.3", "vscode-messenger-common": "^0.4.3", - "vscode-messenger-webview": "^0.4.3" + "vscode-messenger-webview": "^0.4.3", + "vscode-uri": "^3.0.8" }, "devDependencies": { "@types/lodash": "^4.14.202", @@ -107,6 +109,16 @@ "title": "Advanced Display Options", "category": "Memory", "enablement": "webviewId === memory-inspector.memory" + }, + { + "command": "memory-inspector.store-file", + "title": "Store Memory as File", + "category": "Memory" + }, + { + "command": "memory-inspector.apply-file", + "title": "Apply Memory from File", + "category": "Memory" } ], "menus": { @@ -118,12 +130,24 @@ { "command": "memory-inspector.show-variable", "when": "false" + }, + { + "command": "memory-inspector.store-file", + "when": "memory-inspector.canRead" + }, + { + "command": "memory-inspector.apply-file", + "when": "memory-inspector.canWrite" } ], "debug/variables/context": [ { "command": "memory-inspector.show-variable", "when": "canViewMemory && memory-inspector.canRead" + }, + { + "command": "memory-inspector.store-file", + "when": "canViewMemory && memory-inspector.canRead" } ], "view/item/context": [ @@ -132,6 +156,20 @@ "when": "canViewMemory && memory-inspector.canRead" } ], + "explorer/context": [ + { + "command": "memory-inspector.apply-file", + "group": "debug", + "when": "memory-inspector.canWrite && resourceExtname === .hex" + } + ], + "editor/context": [ + { + "command": "memory-inspector.apply-file", + "group": "debug", + "when": "memory-inspector.canWrite && resourceExtname === .hex" + } + ], "webview/context": [ { "command": "memory-inspector.toggle-variables-column", diff --git a/src/common/debug-requests.ts b/src/common/debug-requests.ts new file mode 100644 index 0000000..7dbb8e1 --- /dev/null +++ b/src/common/debug-requests.ts @@ -0,0 +1,39 @@ +/******************************************************************************** + * Copyright (C) 2018 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +// inspired by https://github.com/eclipse-theia/theia/blob/master/packages/debug/src/browser/debug-session-connection.ts + +import type { DebugProtocol } from '@vscode/debugprotocol'; +import type { DebugSession } from 'vscode'; + +export interface DebugRequestTypes { + 'evaluate': [DebugProtocol.EvaluateArguments, DebugProtocol.EvaluateResponse['body']] + 'readMemory': [DebugProtocol.ReadMemoryArguments, DebugProtocol.ReadMemoryResponse['body']] + 'writeMemory': [DebugProtocol.WriteMemoryArguments, DebugProtocol.WriteMemoryResponse['body']] +} + +export async function sendRequest(session: DebugSession, + command: K, args: DebugRequestTypes[K][0]): Promise { + return session.customRequest(command, args); +} + +export namespace EvaluateExpression { + export function sizeOf(expression: string): string { + return `sizeof(${expression})`; + } + export function addressOf(expression: string): string { + return `&(${expression})`; + } +}; diff --git a/src/common/intel-hex.ts b/src/common/intel-hex.ts new file mode 100644 index 0000000..3ea4a45 --- /dev/null +++ b/src/common/intel-hex.ts @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (C) 2024 EclipseSource. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { URI, Utils } from 'vscode-uri'; + +export namespace IntelHEX { + export namespace FileExtensions { + export const All = [ + // General + 'hex', 'mcs', 'int', 'ihex', 'ihe', 'ihx', + // Platform-specific + 'h80', 'h86', 'a43', 'a90', + // Binary or Intel hex + 'obj', 'obl', 'obh', 'rom', 'eep' + ]; + export const Default = 'hex'; + + export function applyIfMissing(file: URI): URI { + const extWithDot = Utils.extname(file); + if (extWithDot.length === 0 || !IntelHEX.FileExtensions.All.includes(extWithDot.slice(1))) { + return URI.file(file.fsPath + '.' + IntelHEX.FileExtensions.Default); + } + return file; + }; + }; + export const DialogFilters = { + 'Intel HEX Files': IntelHEX.FileExtensions.All, + 'All Files': ['*'] + }; +}; diff --git a/src/common/memory-range.ts b/src/common/memory-range.ts index 8205ede..02d119c 100644 --- a/src/common/memory-range.ts +++ b/src/common/memory-range.ts @@ -27,6 +27,12 @@ export interface MemoryRange { endAddress?: string; } +export interface WrittenMemory { + memoryReference: string; + offset?: number; + count?: number +} + /** Suitable for arithemetic */ export interface BigIntMemoryRange { startAddress: bigint; @@ -93,8 +99,8 @@ export function getAddressLength(padding: number, radix: Radix): number { return Math.ceil(padding / Math.log2(radix)); } -export function toHexStringWithRadixMarker(target: bigint): string { - return `${getRadixMarker(Radix.Hexadecimal)}${getAddressString(target, Radix.Hexadecimal)}`; +export function toHexStringWithRadixMarker(target: bigint, paddedLength: number = 0): string { + return `${getRadixMarker(Radix.Hexadecimal)}${getAddressString(target, Radix.Hexadecimal, paddedLength)}`; } export interface VariableMetadata { diff --git a/src/common/memory.ts b/src/common/memory.ts new file mode 100644 index 0000000..d3a79ea --- /dev/null +++ b/src/common/memory.ts @@ -0,0 +1,74 @@ +/******************************************************************************** + * Copyright (C) 2024 EclipseSource. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import type { DebugProtocol } from '@vscode/debugprotocol'; +import { ReadMemoryResult } from './messaging'; + +export interface Memory { + address: bigint; + bytes: Uint8Array; +} + +export function createMemoryFromRead(result: ReadMemoryResult): Memory { + if (!result?.data) { throw new Error('No memory provided!'); } + const address = BigInt(result.address); + const bytes = stringToBytesMemory(result.data); + return { bytes, address }; +} + +export function stringToBytesMemory(data: string): Uint8Array { + return Uint8Array.from(Buffer.from(data, 'base64')); +} + +export function bytesToStringMemory(data: Uint8Array): string { + return Buffer.from(data).toString('base64'); +} + +export function validateMemoryReference(reference: string): string | undefined { + const asNumber = Number(reference); + // we allow an address that is not a number, e.g., an expression, but if it is a number it must be >= 0 + return !isNaN(asNumber) && asNumber < 0 ? 'Value needs to be >= 0' : undefined; +} + +export function validateOffset(offset: string): string | undefined { + const asNumber = Number(offset); + return isNaN(asNumber) ? 'No number provided' : undefined; +} + +export function validateCount(count: string): string | undefined { + const asNumber = Number(count); + if (isNaN(asNumber)) { + return 'No number provided'; + } else if (asNumber <= 0) { + return 'Value needs to be > 0'; + } +} + +export interface MemoryVariable extends DebugProtocol.Variable { + memoryReference: string; +} + +export const isMemoryVariable = (variable: unknown): variable is MemoryVariable => !!variable && !!(variable as MemoryVariable).memoryReference; + +export interface MemoryVariableNode { + variable: MemoryVariable; + sessionId: string; +} + +export const isMemoryVariableNode = (node: unknown): node is MemoryVariableNode => + !!node + && isMemoryVariable((node as MemoryVariableNode).variable) + && typeof (node as MemoryVariableNode).sessionId === 'string'; diff --git a/src/common/messaging.ts b/src/common/messaging.ts index 9ac79eb..1b4607e 100644 --- a/src/common/messaging.ts +++ b/src/common/messaging.ts @@ -17,20 +17,41 @@ import type { DebugProtocol } from '@vscode/debugprotocol'; import type { NotificationType, RequestType } from 'vscode-messenger-common'; import { MemoryViewSettings } from '../webview/utils/view-types'; -import type { VariableRange } from './memory-range'; +import type { VariableRange, WrittenMemory } from './memory-range'; +import { DebugRequestTypes } from './debug-requests'; +import { MemoryVariableNode } from './memory'; +import { URI } from 'vscode-uri'; -export type MemoryReadResult = DebugProtocol.ReadMemoryResponse['body']; -export type MemoryWriteResult = DebugProtocol.WriteMemoryResponse['body']; +// convenience types for easier readability and better semantics +export type MemoryOptions = Partial; +export type ReadMemoryArguments = DebugRequestTypes['readMemory'][0]; +export type ReadMemoryResult = DebugRequestTypes['readMemory'][1]; + +export type WriteMemoryArguments = DebugRequestTypes['writeMemory'][0] & { count?: number }; +export type WriteMemoryResult = DebugRequestTypes['writeMemory'][1]; + +export type StoreMemoryArguments = MemoryOptions | MemoryVariableNode; +export type StoreMemoryResult = void; + +export type ApplyMemoryArguments = URI | undefined; +export type ApplyMemoryResult = MemoryOptions; + +// Notifications export const readyType: NotificationType = { method: 'ready' }; -export const logMessageType: RequestType = { method: 'logMessage' }; export const setMemoryViewSettingsType: NotificationType> = { method: 'setMemoryViewSettings' }; export const resetMemoryViewSettingsType: NotificationType = { method: 'resetMemoryViewSettings' }; export const setTitleType: NotificationType = { method: 'setTitle' }; -export const setOptionsType: RequestType, void> = { method: 'setOptions' }; -export const readMemoryType: RequestType = { method: 'readMemory' }; -export const writeMemoryType: RequestType = { method: 'writeMemory' }; -export const getVariables: RequestType = { method: 'getVariables' }; +export const memoryWrittenType: NotificationType = { method: 'memoryWritten' }; + +// Requests +export const setOptionsType: RequestType = { method: 'setOptions' }; +export const logMessageType: RequestType = { method: 'logMessage' }; +export const readMemoryType: RequestType = { method: 'readMemory' }; +export const writeMemoryType: RequestType = { method: 'writeMemory' }; +export const getVariablesType: RequestType = { method: 'getVariables' }; +export const storeMemoryType: RequestType = { method: 'storeMemory' }; +export const applyMemoryType: RequestType = { method: 'applyMemory' }; export const showAdvancedOptionsType: NotificationType = { method: 'showAdvancedOptions' }; export const getWebviewSelectionType: RequestType = { method: 'getWebviewSelection' }; diff --git a/src/entry-points/browser/extension.ts b/src/entry-points/browser/extension.ts index 64c62e7..da20df5 100644 --- a/src/entry-points/browser/extension.ts +++ b/src/entry-points/browser/extension.ts @@ -19,16 +19,19 @@ import { AdapterRegistry } from '../../plugin/adapter-registry/adapter-registry' import { MemoryProvider } from '../../plugin/memory-provider'; import { MemoryWebview } from '../../plugin/memory-webview-main'; import { CAdapter } from '../../plugin/adapter-registry/c-adapter'; +import { MemoryStorage } from '../../plugin/memory-storage'; export const activate = async (context: vscode.ExtensionContext): Promise => { const registry = new AdapterRegistry(); const memoryProvider = new MemoryProvider(registry); const memoryView = new MemoryWebview(context.extensionUri, memoryProvider); + const memoryStorage = new MemoryStorage(memoryProvider); const cAdapter = new CAdapter(registry); registry.activate(context); memoryProvider.activate(context); memoryView.activate(context); + memoryStorage.activate(context); cAdapter.activate(context); return registry; diff --git a/src/entry-points/desktop/extension.ts b/src/entry-points/desktop/extension.ts index d4cb368..52c6a12 100644 --- a/src/entry-points/desktop/extension.ts +++ b/src/entry-points/desktop/extension.ts @@ -19,16 +19,19 @@ import { AdapterRegistry } from '../../plugin/adapter-registry/adapter-registry' import { MemoryProvider } from '../../plugin/memory-provider'; import { MemoryWebview } from '../../plugin/memory-webview-main'; import { CAdapter } from '../../plugin/adapter-registry/c-adapter'; +import { MemoryStorage } from '../../plugin/memory-storage'; export const activate = async (context: vscode.ExtensionContext): Promise => { const registry = new AdapterRegistry(); const memoryProvider = new MemoryProvider(registry); const memoryView = new MemoryWebview(context.extensionUri, memoryProvider); + const memoryStorage = new MemoryStorage(memoryProvider); const cAdapter = new CAdapter(registry); memoryProvider.activate(context); registry.activate(context); memoryView.activate(context); + memoryStorage.activate(context); cAdapter.activate(context); return registry; diff --git a/src/plugin/adapter-registry/c-tracker.ts b/src/plugin/adapter-registry/c-tracker.ts index 2d709e0..f71c946 100644 --- a/src/plugin/adapter-registry/c-tracker.ts +++ b/src/plugin/adapter-registry/c-tracker.ts @@ -18,6 +18,7 @@ import * as vscode from 'vscode'; import { DebugProtocol } from '@vscode/debugprotocol'; import { AdapterVariableTracker, hexAddress, notADigit } from './adapter-capabilities'; import { toHexStringWithRadixMarker, VariableRange } from '../../common/memory-range'; +import { sendRequest, EvaluateExpression } from '../../common/debug-requests'; export class CTracker extends AdapterVariableTracker { /** @@ -32,9 +33,9 @@ export class CTracker extends AdapterVariableTracker { } try { const [addressResponse, sizeResponse] = await Promise.all([ - session.customRequest('evaluate', { expression: `&(${variable.name})`, context: 'watch', frameId: this.currentFrame }), - session.customRequest('evaluate', { expression: `sizeof(${variable.name})`, context: 'watch', frameId: this.currentFrame }), - ]) as DebugProtocol.EvaluateResponse['body'][]; + sendRequest(session, 'evaluate', { expression: EvaluateExpression.addressOf(variable.name), context: 'watch', frameId: this.currentFrame }), + sendRequest(session, 'evaluate', { expression: EvaluateExpression.sizeOf(variable.name), context: 'watch', frameId: this.currentFrame }) + ]); const addressPart = hexAddress.exec(addressResponse.result); if (!addressPart) { return undefined; } const startAddress = BigInt(addressPart[0]); diff --git a/src/plugin/memory-provider.ts b/src/plugin/memory-provider.ts index f6ba15f..12cb3ee 100644 --- a/src/plugin/memory-provider.ts +++ b/src/plugin/memory-provider.ts @@ -14,12 +14,14 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import * as vscode from 'vscode'; -import * as manifest from './manifest'; import { DebugProtocol } from '@vscode/debugprotocol'; -import { MemoryReadResult, MemoryWriteResult } from '../common/messaging'; +import * as vscode from 'vscode'; +import { VariableRange, WrittenMemory } from '../common/memory-range'; +import { ReadMemoryResult, WriteMemoryResult } from '../common/messaging'; import { AdapterRegistry } from './adapter-registry/adapter-registry'; -import { VariableRange } from '../common/memory-range'; +import * as manifest from './manifest'; +import { sendRequest } from '../common/debug-requests'; +import { stringToBytesMemory } from '../common/memory'; export interface LabeledUint8Array extends Uint8Array { label?: string; @@ -34,8 +36,11 @@ export class MemoryProvider { public static ReadKey = `${manifest.PACKAGE_NAME}.canRead`; public static WriteKey = `${manifest.PACKAGE_NAME}.canWrite`; - private _onDidStopDebug: vscode.EventEmitter = new vscode.EventEmitter(); - public readonly onDidStopDebug: vscode.Event = this._onDidStopDebug.event; + private _onDidStopDebug = new vscode.EventEmitter(); + public readonly onDidStopDebug = this._onDidStopDebug.event; + + private _onDidWriteMemory = new vscode.EventEmitter(); + public readonly onDidWriteMemory = this._onDidWriteMemory.event; protected readonly sessions = new Map(); @@ -113,12 +118,22 @@ export class MemoryProvider { return vscode.debug.activeDebugSession; } - public async readMemory(readMemoryArguments: DebugProtocol.ReadMemoryArguments): Promise { - return this.assertCapability('supportsReadMemoryRequest', 'read memory').customRequest('readMemory', readMemoryArguments); + public async readMemory(args: DebugProtocol.ReadMemoryArguments): Promise { + return sendRequest(this.assertCapability('supportsReadMemoryRequest', 'read memory'), 'readMemory', args); + } + + public async writeMemory(args: DebugProtocol.WriteMemoryArguments & { count?: number }): Promise { + return sendRequest(this.assertCapability('supportsWriteMemoryRequest', 'write memory'), 'writeMemory', args).then(response => { + const offset = response?.offset ? (args.offset ?? 0) + response.offset : args.offset; + // we accept count as an additional argument so we can skip the memory length calculation + const count = response?.bytesWritten ?? args.count ?? stringToBytesMemory(args.data).length; + this._onDidWriteMemory.fire({ memoryReference: args.memoryReference, offset, count }); + return response; + }); } - public async writeMemory(writeMemoryArguments: DebugProtocol.WriteMemoryArguments): Promise { - return this.assertCapability('supportsWriteMemoryRequest', 'write memory').customRequest('writeMemory', writeMemoryArguments); + public async evaluate(args: DebugProtocol.EvaluateArguments): Promise { + return sendRequest(this.assertActiveSession('evaluate'), 'evaluate', args); } public async getVariables(variableArguments: DebugProtocol.ReadMemoryArguments): Promise { diff --git a/src/plugin/memory-storage.ts b/src/plugin/memory-storage.ts new file mode 100644 index 0000000..aad73b4 --- /dev/null +++ b/src/plugin/memory-storage.ts @@ -0,0 +1,194 @@ +/******************************************************************************** + * Copyright (C) 2024 EclipseSource. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import MemoryMap from 'nrf-intel-hex'; +import * as vscode from 'vscode'; +import { URI, Utils } from 'vscode-uri'; +import { IntelHEX } from '../common/intel-hex'; +import { + bytesToStringMemory, createMemoryFromRead, + isMemoryVariableNode, validateCount, validateMemoryReference, validateOffset +} from '../common/memory'; +import { toHexStringWithRadixMarker } from '../common/memory-range'; +import * as manifest from './manifest'; +import { MemoryProvider } from './memory-provider'; +import { EvaluateExpression } from '../common/debug-requests'; +import { ApplyMemoryArguments, ApplyMemoryResult, MemoryOptions, StoreMemoryArguments } from '../common/messaging'; + +export const StoreCommandType = `${manifest.PACKAGE_NAME}.store-file`; +export const ApplyCommandType = `${manifest.PACKAGE_NAME}.apply-file`; + +const VALID_FILE_NAME_CHARS = /[^a-zA-Z0-9 _-]/g; + +type StoreMemoryOptions = Required & { + outputFile: vscode.Uri; +}; + +const DEFAULT_STORE_OPTIONS: Omit = { + memoryReference: toHexStringWithRadixMarker(0n, 8), + offset: 0, + count: 256 +}; + +interface ApplyMemoryOptions { + uri: vscode.Uri; +} + +export class MemoryStorage { + constructor(protected memoryProvider: MemoryProvider) { + } + + public activate(context: vscode.ExtensionContext): void { + context.subscriptions.push( + vscode.commands.registerCommand(StoreCommandType, args => this.storeMemory(args)), + vscode.commands.registerCommand(ApplyCommandType, args => this.applyMemory(args)) + ); + } + + public async storeMemory(args?: StoreMemoryArguments): Promise { + const providedDefaultOptions = await this.storeArgsToOptions(args); + const options = await this.getStoreMemoryOptions(providedDefaultOptions); + if (!options) { + // user aborted process + return; + } + + const { outputFile, ...readArgs } = options; + try { + const memoryResponse = await this.memoryProvider.readMemory(readArgs); + const memory = createMemoryFromRead(memoryResponse); + const memoryMap = new MemoryMap({ [Number(memory.address)]: memory.bytes }); + await vscode.workspace.fs.writeFile(outputFile, new TextEncoder().encode(memoryMap.asHexString())); + } catch (error) { + if (error instanceof Error) { + vscode.window.showErrorMessage(`Could not write memory to '${vscode.workspace.asRelativePath(outputFile)}': ${error.message}`); + } else { + vscode.window.showErrorMessage(`Could not write memory to '${vscode.workspace.asRelativePath(outputFile)}': ${error}`); + } + return; + } + + const option = await vscode.window.showInformationMessage(`File '${vscode.workspace.asRelativePath(outputFile)}' saved.`, 'Open File'); + if (option === 'Open File') { + await vscode.window.showTextDocument(outputFile); + } + } + + protected async storeArgsToOptions(args?: StoreMemoryArguments): Promise> { + if (!args) { + return {}; + } + if (isMemoryVariableNode(args)) { + try { + const variableName = args.variable.evaluateName ?? args.variable.name; + const { result } = await this.memoryProvider.evaluate({ expression: EvaluateExpression.sizeOf(variableName), context: 'watch' }); + const count = validateCount(result) === undefined ? Number(result) : undefined; + return { count, memoryReference: EvaluateExpression.addressOf(variableName), offset: 0 }; + } catch (error) { + // ignore, we are just using them as default values + return { memoryReference: args.variable.memoryReference, offset: 0 }; + } + } + return args; + } + + protected async getStoreMemoryOptions(providedDefault?: Partial): Promise { + const memoryReference = await vscode.window.showInputBox({ + title: 'Store Memory as File (1/3)', + prompt: 'Start Memory Address', + placeHolder: 'Hex address or expression', + value: providedDefault?.memoryReference ?? DEFAULT_STORE_OPTIONS.memoryReference, + validateInput: validateMemoryReference + }); + if (!memoryReference) { + return; + } + const offset = await vscode.window.showInputBox({ + title: 'Store Memory as File (2/3)', + prompt: 'Memory Address Offset', + placeHolder: 'Positive or negative offset in bytes', + value: providedDefault?.offset?.toString() ?? DEFAULT_STORE_OPTIONS.offset.toString(), + validateInput: validateOffset + }); + if (!offset) { + return; + } + const count = await vscode.window.showInputBox({ + title: 'Store Memory as File (3/3)', + prompt: 'Length', + placeHolder: 'Number of bytes to read', + value: providedDefault?.count?.toString() ?? DEFAULT_STORE_OPTIONS.count.toString(), + validateInput: validateCount + }); + if (!count) { + return; + } + const workspaceUri = vscode.workspace.workspaceFolders?.[0]?.uri; + const defaultUri = workspaceUri ? Utils.joinPath(workspaceUri, memoryReference.replace(VALID_FILE_NAME_CHARS, '') + '_' + count) : workspaceUri; + const saveFile = await vscode.window.showSaveDialog({ title: 'Store Memory', defaultUri, filters: IntelHEX.DialogFilters }); + if (!saveFile) { + return; + } + const outputFile = IntelHEX.FileExtensions.applyIfMissing(saveFile); + return { memoryReference, offset: Number(offset), count: Number(count), outputFile }; + } + + public async applyMemory(args?: ApplyMemoryArguments): Promise { + const providedDefaultOptions = await this.applyArgsToOptions(args); + const options = await this.getApplyMemoryOptions(providedDefaultOptions); + if (!options) { + // user aborted process + return {}; + } + try { + const byteContent = await vscode.workspace.fs.readFile(options.uri); + const memoryMap = MemoryMap.fromHex(new TextDecoder().decode(byteContent)); + let memoryReference: string | undefined; + let count: number | undefined; + for (const [address, memory] of memoryMap) { + memoryReference = toHexStringWithRadixMarker(BigInt(address)); + count = memory.length; + const data = bytesToStringMemory(memory); + await this.memoryProvider.writeMemory({ memoryReference, data, count }); + } + await vscode.window.showInformationMessage(`Memory from '${vscode.workspace.asRelativePath(options.uri)}' applied.`); + return { memoryReference, count, offset: 0 }; + } catch (error) { + if (error instanceof Error) { + vscode.window.showErrorMessage(`Could not apply memory from '${vscode.workspace.asRelativePath(options.uri)}': ${error.message}`); + } else { + vscode.window.showErrorMessage(`Could not apply memory from '${vscode.workspace.asRelativePath(options.uri)}': ${error}`); + } + return {}; + } + } + + protected async applyArgsToOptions(args?: ApplyMemoryArguments): Promise> { + return URI.isUri(args) ? { uri: args } : {}; + } + + protected async getApplyMemoryOptions(providedDefault?: Partial): Promise { + if (providedDefault?.uri) { + // if we are already given a URI, let's not bother the user and simply use it + return { uri: providedDefault.uri }; + } + const selectedUris = await vscode.window.showOpenDialog({ title: 'Apply Memory', filters: IntelHEX.DialogFilters }); + if (selectedUris && selectedUris?.length > 0) { + return { uri: selectedUris[0] }; + } + return undefined; + } +} diff --git a/src/plugin/memory-webview-main.ts b/src/plugin/memory-webview-main.ts index 21c6e3b..ebd9fbe 100644 --- a/src/plugin/memory-webview-main.ts +++ b/src/plugin/memory-webview-main.ts @@ -15,45 +15,45 @@ ********************************************************************************/ import * as vscode from 'vscode'; -import type { DebugProtocol } from '@vscode/debugprotocol'; -import * as manifest from './manifest'; import { Messenger } from 'vscode-messenger'; import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; +import { isMemoryVariableNode } from '../common/memory'; +import { Endianness, VariableRange } from '../common/memory-range'; import { - readyType, + MemoryOptions, + ReadMemoryArguments, + ReadMemoryResult, + StoreMemoryArguments, + WebviewSelection, + WriteMemoryArguments, + WriteMemoryResult, + applyMemoryType, + getVariablesType, + getWebviewSelectionType, logMessageType, - setOptionsType, + memoryWrittenType, readMemoryType, - writeMemoryType, - MemoryReadResult, - MemoryWriteResult, - getVariables, - setMemoryViewSettingsType, + readyType, resetMemoryViewSettingsType, + setMemoryViewSettingsType, + setOptionsType, setTitleType, showAdvancedOptionsType, - getWebviewSelectionType, - WebviewSelection, + storeMemoryType, + writeMemoryType, } from '../common/messaging'; -import { MemoryProvider } from './memory-provider'; -import { outputChannelLogger } from './logger'; -import { Endianness, VariableRange } from '../common/memory-range'; -import { AddressPaddingOptions, MemoryViewSettings, ScrollingBehavior } from '../webview/utils/view-types'; import { WebviewContext, getVisibleColumns } from '../common/webview-context'; - -interface Variable { - name: string; - value: string; - variablesReference: number; - memoryReference: number; -} +import { AddressPaddingOptions, MemoryViewSettings, ScrollingBehavior } from '../webview/utils/view-types'; +import { outputChannelLogger } from './logger'; +import * as manifest from './manifest'; +import { MemoryProvider } from './memory-provider'; +import { ApplyCommandType, StoreCommandType } from './memory-storage'; enum RefreshEnum { off = 0, on = 1 } -const isMemoryVariable = (variable: Variable): variable is Variable => variable && !!(variable as Variable).memoryReference; const CONFIGURABLE_COLUMNS = [ manifest.CONFIG_SHOW_ASCII_COLUMN, manifest.CONFIG_SHOW_VARIABLES_COLUMN, @@ -90,9 +90,8 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { vscode.window.registerCustomEditorProvider(manifest.EDITOR_NAME, this), vscode.commands.registerCommand(MemoryWebview.ShowCommandType, () => this.show()), vscode.commands.registerCommand(MemoryWebview.VariableCommandType, node => { - const variable = node.variable; - if (isMemoryVariable(variable)) { - this.show({ memoryReference: variable.memoryReference.toString() }); + if (isMemoryVariableNode(node)) { + this.show({ memoryReference: node.variable.memoryReference.toString() }); } }), vscode.commands.registerCommand(MemoryWebview.ToggleVariablesColumnCommandType, (ctx: WebviewContext) => { @@ -138,7 +137,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { await this.show({ memoryReference }, webviewPanel); } - public async show(initialMemory?: Partial, panel?: vscode.WebviewPanel): Promise { + public async show(initialMemory?: MemoryOptions, panel?: vscode.WebviewPanel): Promise { const distPathUri = vscode.Uri.joinPath(this.extensionUri, 'dist', 'views'); const mediaPathUri = vscode.Uri.joinPath(this.extensionUri, 'media'); const codiconPathUri = vscode.Uri.joinPath(this.extensionUri, 'node_modules', '@vscode', 'codicons', 'dist'); @@ -198,7 +197,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { `; } - protected setWebviewMessageListener(panel: vscode.WebviewPanel, options?: Partial): void { + protected setWebviewMessageListener(panel: vscode.WebviewPanel, options?: MemoryOptions): void { const participant = this.messenger.registerWebviewPanel(panel); const disposables = [ @@ -212,15 +211,18 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.messenger.onRequest(logMessageType, message => outputChannelLogger.info('[webview]:', message), { sender: participant }), this.messenger.onRequest(readMemoryType, request => this.readMemory(request), { sender: participant }), this.messenger.onRequest(writeMemoryType, request => this.writeMemory(request), { sender: participant }), - this.messenger.onRequest(getVariables, request => this.getVariables(request), { sender: participant }), + this.messenger.onRequest(getVariablesType, request => this.getVariables(request), { sender: participant }), this.messenger.onNotification(resetMemoryViewSettingsType, () => this.setInitialSettings(participant, panel.title), { sender: participant }), this.messenger.onNotification(setTitleType, title => { panel.title = title; }, { sender: participant }), + this.messenger.onRequest(storeMemoryType, args => this.storeMemory(args), { sender: participant }), + this.messenger.onRequest(applyMemoryType, () => this.applyMemory(), { sender: participant }), this.memoryProvider.onDidStopDebug(() => { if (this.refreshOnStop === RefreshEnum.on) { this.refresh(participant); } }), + this.memoryProvider.onDidWriteMemory(writtenMemory => this.messenger.sendNotification(memoryWrittenType, participant, writtenMemory)) ]; panel.onDidChangeViewState(newState => { if (newState.webviewPanel.visible) { @@ -230,7 +232,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { panel.onDidDispose(() => disposables.forEach(disposable => disposable.dispose())); } - protected async refresh(participant: WebviewIdMessageParticipant, options?: Partial): Promise { + protected async refresh(participant: WebviewIdMessageParticipant, options: MemoryOptions = {}): Promise { this.messenger.sendRequest(setOptionsType, participant, options); } @@ -261,27 +263,27 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { }; } - protected async readMemory(request: DebugProtocol.ReadMemoryArguments): Promise { + protected async readMemory(request: ReadMemoryArguments): Promise { try { return await this.memoryProvider.readMemory(request); } catch (err) { - outputChannelLogger.error('Error fetching memory', err instanceof Error ? `: ${err.message}\n${err.stack}` : ''); + this.logError('Error fetching memory', err); } } - protected async writeMemory(request: DebugProtocol.WriteMemoryArguments): Promise { + protected async writeMemory(request: WriteMemoryArguments): Promise { try { - return await this.memoryProvider.writeMemory(request); + return this.memoryProvider.writeMemory(request); } catch (err) { - outputChannelLogger.error('Error writing memory', err instanceof Error ? `: ${err.message}\n${err.stack}` : ''); + this.logError('Error writing memory', err); } } - protected async getVariables(request: DebugProtocol.ReadMemoryArguments): Promise { + protected async getVariables(request: ReadMemoryArguments): Promise { try { return await this.memoryProvider.getVariables(request); } catch (err) { - outputChannelLogger.error('Error fetching variables', err instanceof Error ? `: ${err.message}\n${err.stack}` : ''); + this.logError('Error fetching variables', err); return []; } } @@ -301,4 +303,16 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.setMemoryViewSettings(ctx.messageParticipant, { visibleColumns }); } + + protected async storeMemory(storeArguments: StoreMemoryArguments): Promise { + return vscode.commands.executeCommand(StoreCommandType, storeArguments); + } + + protected async applyMemory(): Promise { + return vscode.commands.executeCommand(ApplyCommandType); + } + + protected logError(msg: string, err: unknown): void { + outputChannelLogger.error(msg, err instanceof Error ? `: ${err.message}\n${err.stack}` : ''); + } } diff --git a/src/webview/columns/address-column.tsx b/src/webview/columns/address-column.tsx index ccbc3fd..e9d8b51 100644 --- a/src/webview/columns/address-column.tsx +++ b/src/webview/columns/address-column.tsx @@ -17,7 +17,7 @@ import React, { ReactNode } from 'react'; import { BigIntMemoryRange, getAddressString, getRadixMarker } from '../../common/memory-range'; import { ColumnContribution, ColumnFittingType, TableRenderOptions } from './column-contribution-service'; -import { Memory } from '../utils/view-types'; +import { Memory } from '../../common/memory'; export class AddressColumn implements ColumnContribution { static ID = 'address'; diff --git a/src/webview/columns/ascii-column.ts b/src/webview/columns/ascii-column.ts index 866b510..cb522bd 100644 --- a/src/webview/columns/ascii-column.ts +++ b/src/webview/columns/ascii-column.ts @@ -17,7 +17,7 @@ import { ReactNode } from 'react'; import { BigIntMemoryRange, toOffset } from '../../common/memory-range'; import { ColumnContribution, TableRenderOptions } from './column-contribution-service'; -import { Memory } from '../utils/view-types'; +import { Memory } from '../../common/memory'; function isPrintableAsAscii(input: number): boolean { return input >= 32 && input < (128 - 1); diff --git a/src/webview/columns/column-contribution-service.ts b/src/webview/columns/column-contribution-service.ts index 42fa494..1d4ada6 100644 --- a/src/webview/columns/column-contribution-service.ts +++ b/src/webview/columns/column-contribution-service.ts @@ -14,10 +14,11 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { DebugProtocol } from '@vscode/debugprotocol'; import type * as React from 'react'; import { BigIntMemoryRange } from '../../common/memory-range'; -import type { Disposable, Memory, MemoryState, SerializedTableRenderOptions, UpdateExecutor } from '../utils/view-types'; +import type { Disposable, MemoryState, SerializedTableRenderOptions, UpdateExecutor } from '../utils/view-types'; +import { Memory } from '../../common/memory'; +import { ReadMemoryArguments } from '../../common/messaging'; export type ColumnFittingType = 'content-width'; @@ -31,7 +32,7 @@ export interface ColumnContribution { priority?: number; render(range: BigIntMemoryRange, memory: Memory, options: TableRenderOptions): React.ReactNode /** Called when fetching new memory or when activating the column. */ - fetchData?(currentViewParameters: DebugProtocol.ReadMemoryArguments): Promise; + fetchData?(currentViewParameters: ReadMemoryArguments): Promise; /** Called when the user reveals the column */ activate?(memory: MemoryState): Promise; /** Called when the user hides the column */ diff --git a/src/webview/columns/data-column.tsx b/src/webview/columns/data-column.tsx index 0389b3c..816705b 100644 --- a/src/webview/columns/data-column.tsx +++ b/src/webview/columns/data-column.tsx @@ -16,11 +16,12 @@ import * as React from 'react'; import { BigIntMemoryRange, Endianness, toOffset } from '../../common/memory-range'; -import { FullNodeAttributes, Memory } from '../utils/view-types'; +import { FullNodeAttributes } from '../utils/view-types'; import { ColumnContribution, TableRenderOptions } from './column-contribution-service'; import { decorationService } from '../decorations/decoration-service'; import type { MemorySizeOptions } from '../components/memory-table'; import { elementInnerWidth, characterWidthInContainer } from '../utils/window'; +import { Memory } from '../../common/memory'; export class DataColumn implements ColumnContribution { static CLASS_NAME = 'column-data'; diff --git a/src/webview/components/memory-table.tsx b/src/webview/components/memory-table.tsx index 908d2f2..63cf32b 100644 --- a/src/webview/components/memory-table.tsx +++ b/src/webview/components/memory-table.tsx @@ -22,13 +22,15 @@ import { ProgressSpinner } from 'primereact/progressspinner'; import { Tooltip } from 'primereact/tooltip'; import React from 'react'; import { TableRenderOptions } from '../columns/column-contribution-service'; -import { Decoration, Memory, MemoryDisplayConfiguration, ScrollingBehavior, isTrigger } from '../utils/view-types'; +import { Decoration, MemoryDisplayConfiguration, ScrollingBehavior, isTrigger } from '../utils/view-types'; import isDeepEqual from 'fast-deep-equal'; import { classNames } from 'primereact/utils'; import { tryToNumber } from '../../common/typescript'; import { DataColumn } from '../columns/data-column'; import { createColumnVscodeContext, createSectionVscodeContext } from '../utils/vscode-contexts'; import { WebviewSelection } from '../../common/messaging'; +import { ReadMemoryArguments } from '../../common/messaging'; +import { Memory } from '../../common/memory'; import { debounce } from 'lodash'; import type { HoverService } from '../hovers/hover-service'; import { TooltipEvent } from 'primereact/tooltip/tooltipoptions'; @@ -37,7 +39,7 @@ export interface MoreMemorySelectProps { activeReadArguments: Required; options: number[]; direction: 'above' | 'below'; - fetchMemory(partialOptions?: Partial): Promise; + fetchMemory(partialOptions?: Partial): Promise; disabled: boolean } @@ -134,7 +136,7 @@ interface MemoryTableProps extends TableRenderOptions, MemoryDisplayConfiguratio decorations: Decoration[]; effectiveAddressLength: number; hoverService: HoverService; - fetchMemory(partialOptions?: Partial): Promise; + fetchMemory(partialOptions?: Partial): Promise; isMemoryFetching: boolean; isFrozen: boolean; } diff --git a/src/webview/components/memory-widget.tsx b/src/webview/components/memory-widget.tsx index 71f4c7d..820e6de 100644 --- a/src/webview/components/memory-widget.tsx +++ b/src/webview/components/memory-widget.tsx @@ -17,12 +17,13 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import React from 'react'; import { ColumnStatus } from '../columns/column-contribution-service'; -import { Decoration, Memory, MemoryDisplayConfiguration, MemoryState } from '../utils/view-types'; +import { Decoration, MemoryDisplayConfiguration, MemoryState } from '../utils/view-types'; import { MemoryTable } from './memory-table'; import { OptionsWidget } from './options-widget'; import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; import { VscodeContext, createAppVscodeContext } from '../utils/vscode-contexts'; -import { WebviewSelection } from '../../common/messaging'; +import { WebviewSelection, MemoryOptions } from '../../common/messaging'; +import { Memory } from '../../common/memory'; import { HoverService } from '../hovers/hover-service'; interface MemoryWidgetProps extends MemoryDisplayConfiguration { @@ -43,7 +44,9 @@ interface MemoryWidgetProps extends MemoryDisplayConfiguration { updateMemoryDisplayConfiguration: (memoryArguments: Partial) => void; resetMemoryDisplayConfiguration: () => void; updateTitle: (title: string) => void; - fetchMemory(partialOptions?: Partial): Promise + fetchMemory(partialOptions?: MemoryOptions): Promise; + triggerStoreMemory(): void; + triggerApplyMemory(): void; } interface MemoryWidgetState { @@ -94,6 +97,8 @@ export class MemoryWidget extends React.Component { - configuredReadArguments: Required; - activeReadArguments: Required; + configuredReadArguments: Required; + activeReadArguments: Required; title: string; updateRenderOptions: (options: Partial) => void; resetRenderOptions: () => void; @@ -44,6 +46,8 @@ export interface OptionsWidgetProps toggleColumn(id: string, isVisible: boolean): void; toggleFrozen: () => void; isFrozen: boolean; + triggerStoreMemory(): void; + triggerApplyMemory(): void; } interface OptionsWidgetState { @@ -101,36 +105,18 @@ export class OptionsWidget extends React.Component { const errors: FormikErrors = {}; - - if (values.address.trim().length === 0) { - errors.address = 'Required'; - } else { - const address = +values.address; - if (!isNaN(address) && address < 0) { - errors.address = 'Value needs to be >= 0'; - } + const addressError = values.address.trim().length === 0 ? 'Required' : validateMemoryReference(values.address); + if (addressError) { + errors.address = addressError; } - - if (values.offset.trim().length === 0) { - errors.offset = 'Required'; - } else { - const offset = +values.offset; - if (isNaN(offset)) { - errors.offset = 'No number provided'; - } + const offsetError = values.offset.trim().length === 0 ? 'Required' : validateOffset(values.offset); + if (offsetError) { + errors.offset = offsetError; } - - if (values.count.trim().length === 0) { - errors.count = 'Required'; - } else { - const count = +values.count; - if (isNaN(count)) { - errors.count = 'No number provided'; - } else if (count <= 0) { - errors.count = 'Value needs to be > 0'; - } + const countError = values.count.trim().length === 0 ? 'Required' : validateCount(values.count); + if (countError) { + errors.count = countError; } - return errors; }; @@ -176,16 +162,38 @@ export class OptionsWidget extends React.Component{this.props.title} )} {!isLabelEditing && ( - diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index c485f35..7277d2e 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -37,6 +37,8 @@ import { setTitleType, showAdvancedOptionsType, storeMemoryType, + sessionContextChangedType, + SessionContext, } from '../common/messaging'; import { AddressColumn } from './columns/address-column'; import { AsciiColumn } from './columns/ascii-column'; @@ -55,6 +57,7 @@ import { VariableHover } from './hovers/variable-hover'; export interface MemoryAppState extends MemoryState, MemoryDisplayConfiguration { messageParticipant: WebviewIdMessageParticipant; title: string; + sessionContext: SessionContext; effectiveAddressLength: number; decorations: Decoration[]; hoverService: HoverService; @@ -62,6 +65,11 @@ export interface MemoryAppState extends MemoryState, MemoryDisplayConfiguration isFrozen: boolean; } +const DEFAULT_SESSION_CONTEXT: SessionContext = { + canRead: false, + canWrite: false +}; + const MEMORY_DISPLAY_CONFIGURATION_DEFAULTS: MemoryDisplayConfiguration = { bytesPerWord: 1, wordsPerGroup: 1, @@ -72,6 +80,7 @@ const MEMORY_DISPLAY_CONFIGURATION_DEFAULTS: MemoryDisplayConfiguration = { addressRadix: 16, showRadixPrefix: true, }; + const DEFAULT_READ_ARGUMENTS: Required = { memoryReference: '', offset: 0, @@ -94,6 +103,7 @@ class App extends React.Component<{}, MemoryAppState> { this.state = { messageParticipant: { type: 'webview', webviewId: '' }, title: 'Memory', + sessionContext: DEFAULT_SESSION_CONTEXT, memory: undefined, effectiveAddressLength: 0, configuredReadArguments: DEFAULT_READ_ARGUMENTS, @@ -109,7 +119,8 @@ class App extends React.Component<{}, MemoryAppState> { public componentDidMount(): void { messenger.onRequest(setOptionsType, options => this.setOptions(options)); - messenger.onNotification(memoryWrittenType, writtenMemory => this.handleWrittenMemory(writtenMemory)); + messenger.onNotification(memoryWrittenType, writtenMemory => this.memoryWritten(writtenMemory)); + messenger.onNotification(sessionContextChangedType, sessionContext => this.sessionContextChanged(sessionContext)); messenger.onNotification(setMemoryViewSettingsType, config => { if (config.visibleColumns) { for (const column of columnContributionService.getColumns()) { @@ -138,7 +149,7 @@ class App extends React.Component<{}, MemoryAppState> { hoverService.setMemoryState(this.state); } - protected handleWrittenMemory(writtenMemory: WrittenMemory): void { + protected memoryWritten(writtenMemory: WrittenMemory): void { if (!this.state.memory) { return; } @@ -171,11 +182,16 @@ class App extends React.Component<{}, MemoryAppState> { this.updateMemoryState(); } + protected sessionContextChanged(sessionContext: SessionContext): void { + this.setState({ sessionContext }); + } + public render(): React.ReactNode { return Date: Tue, 12 Mar 2024 10:25:50 +0100 Subject: [PATCH 4/7] Further enhancements: Context menu, memory event, PR feedback - Integrate commands with context menu - React to 'memory' event from debug adapter - Use 'Store Memory to File' as proper storage label - Use workspace folder as default save location - Minor fix for numpad key enter on memory options --- package.json | 12 ++++- src/common/debug-requests.ts | 27 ++++++++++ src/common/messaging.ts | 3 +- src/common/webview-context.ts | 10 ++++ .../adapter-registry/adapter-capabilities.ts | 29 ++--------- src/plugin/memory-provider.ts | 50 ++++++++++++------- src/plugin/memory-storage.ts | 16 ++++-- src/webview/components/memory-table.tsx | 2 +- src/webview/components/memory-widget.tsx | 1 + src/webview/components/options-widget.tsx | 2 +- src/webview/memory-webview-view.tsx | 13 +++-- 11 files changed, 111 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 887479e..d9d0453 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ }, { "command": "memory-inspector.store-file", - "title": "Store Memory as File", + "title": "Store Memory to File", "enablement": "memory-inspector.canRead", "category": "Memory" }, @@ -190,6 +190,16 @@ "command": "memory-inspector.show-advanced-display-options", "group": "display@4", "when": "webviewId === memory-inspector.memory" + }, + { + "command": "memory-inspector.store-file", + "group": "display@5", + "when": "webviewId === memory-inspector.memory" + }, + { + "command": "memory-inspector.apply-file", + "group": "display@6", + "when": "webviewId === memory-inspector.memory" } ] }, diff --git a/src/common/debug-requests.ts b/src/common/debug-requests.ts index 4313a3a..1e2f54f 100644 --- a/src/common/debug-requests.ts +++ b/src/common/debug-requests.ts @@ -20,10 +20,22 @@ import type { DebugSession } from 'vscode'; export interface DebugRequestTypes { 'evaluate': [DebugProtocol.EvaluateArguments, DebugProtocol.EvaluateResponse['body']] + 'initialize': [DebugProtocol.InitializeRequestArguments, DebugProtocol.InitializeResponse['body']] 'readMemory': [DebugProtocol.ReadMemoryArguments, DebugProtocol.ReadMemoryResponse['body']] + 'scopes': [DebugProtocol.ScopesArguments, DebugProtocol.ScopesResponse['body']] + 'variables': [DebugProtocol.VariablesArguments, DebugProtocol.VariablesResponse['body']] 'writeMemory': [DebugProtocol.WriteMemoryArguments, DebugProtocol.WriteMemoryResponse['body']] } +export interface DebugEvents { + 'memory': DebugProtocol.MemoryEvent, + 'stopped': DebugProtocol.StoppedEvent +} + +export type DebugRequest = Omit & { command: COMMAND, arguments: ARGS }; +export type DebugResponse = Omit & { command: COMMAND, body: BODY }; +export type DebugEvent = DebugProtocol.Event & { body: T }; + export async function sendRequest(session: DebugSession, command: K, args: DebugRequestTypes[K][0]): Promise { return session.customRequest(command, args); @@ -43,3 +55,18 @@ export function isDebugEvaluateArguments(args: DebugProtocol.EvaluateArguments | const assumed = args ? args as DebugProtocol.EvaluateArguments : undefined; return typeof assumed?.expression === 'string'; } + +export function isDebugRequest(command: K, message: unknown): message is DebugRequest { + const assumed = message ? message as DebugProtocol.Request : undefined; + return !!assumed && assumed.type === 'request' && assumed.command === command; +} + +export function isDebugResponse(command: K, message: unknown): message is DebugResponse { + const assumed = message ? message as DebugProtocol.Response : undefined; + return !!assumed && assumed.type === 'response' && assumed.command === command; +} + +export function isDebugEvent(event: K, message: unknown): message is DebugEvents[K] { + const assumed = message ? message as DebugProtocol.Event : undefined; + return !!assumed && assumed.type === 'event' && assumed.event === event; +} diff --git a/src/common/messaging.ts b/src/common/messaging.ts index d4836d5..2b71c6f 100644 --- a/src/common/messaging.ts +++ b/src/common/messaging.ts @@ -21,6 +21,7 @@ import type { VariableRange, WrittenMemory } from './memory-range'; import { DebugRequestTypes } from './debug-requests'; import { URI } from 'vscode-uri'; import { VariablesView } from '../plugin/external-views'; +import { WebviewContext } from './webview-context'; // convenience types for easier readability and better semantics export type MemoryOptions = Partial; @@ -31,7 +32,7 @@ export type ReadMemoryResult = DebugRequestTypes['readMemory'][1]; export type WriteMemoryArguments = DebugRequestTypes['writeMemory'][0] & { count?: number }; export type WriteMemoryResult = DebugRequestTypes['writeMemory'][1]; -export type StoreMemoryArguments = MemoryOptions & { proposedOutputName?: string } | VariablesView.IVariablesContext; +export type StoreMemoryArguments = MemoryOptions & { proposedOutputName?: string } | VariablesView.IVariablesContext | WebviewContext; export type StoreMemoryResult = void; export type ApplyMemoryArguments = URI | undefined; diff --git a/src/common/webview-context.ts b/src/common/webview-context.ts index 044c442..264b11a 100644 --- a/src/common/webview-context.ts +++ b/src/common/webview-context.ts @@ -16,6 +16,7 @@ import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; import { VariableMetadata } from './memory-range'; +import { ReadMemoryArguments } from './messaging'; export interface WebviewContext { messageParticipant: WebviewIdMessageParticipant, @@ -23,6 +24,7 @@ export interface WebviewContext { showAsciiColumn: boolean showVariablesColumn: boolean, showRadixPrefix: boolean, + activeReadArguments: Required } export interface WebviewCellContext extends WebviewContext { @@ -48,3 +50,11 @@ export function getVisibleColumns(context: WebviewContext): string[] { } return columns; } + +export function isWebviewContext(args: WebviewContext | unknown): args is WebviewContext { + const assumed = args ? args as WebviewContext : undefined; + return typeof assumed?.messageParticipant?.type === 'string' && assumed.messageParticipant.type === 'webview' && typeof assumed.messageParticipant.webviewId === 'string' + && typeof assumed.webviewSection === 'string' && typeof assumed.showAsciiColumn === 'boolean' && typeof assumed.showVariablesColumn === 'boolean' + && typeof assumed.showRadixPrefix === 'boolean' && typeof assumed.activeReadArguments?.count === 'number' && typeof assumed.activeReadArguments?.offset === 'number' + && typeof assumed.activeReadArguments?.memoryReference === 'string'; +} diff --git a/src/plugin/adapter-registry/adapter-capabilities.ts b/src/plugin/adapter-registry/adapter-capabilities.ts index d0512aa..6d029fe 100644 --- a/src/plugin/adapter-registry/adapter-capabilities.ts +++ b/src/plugin/adapter-registry/adapter-capabilities.ts @@ -18,6 +18,7 @@ import * as vscode from 'vscode'; import { DebugProtocol } from '@vscode/debugprotocol'; import { VariableRange } from '../../common/memory-range'; import { Logger } from '../logger'; +import { isDebugRequest, isDebugResponse } from '../../common/debug-requests'; /** Represents capabilities that may be achieved with particular debug adapters but are not part of the DAP */ export interface AdapterCapabilities { @@ -46,9 +47,9 @@ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { constructor(protected readonly onEnd: vscode.Disposable, protected logger: Logger) { } onWillReceiveMessage(message: unknown): void { - if (isScopesRequest(message)) { + if (isDebugRequest('scopes', message)) { this.currentFrame = message.arguments.frameId; - } else if (isVariableRequest(message)) { + } else if (isDebugRequest('variables', message)) { if (message.arguments.variablesReference in this.variablesTree) { this.pendingMessages.set(message.seq, message.arguments.variablesReference); } @@ -57,7 +58,7 @@ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { /** Produces a two-level tree of scopes and their immediate children. Does not handle expansion of complex variables. */ onDidSendMessage(message: unknown): void { - if (isScopesResponse(message)) { + if (isDebugResponse('scopes', message)) { this.variablesTree = {}; // Scopes request implies that all scopes will be queried again. for (const scope of message.body.scopes) { if (this.isDesiredScope(scope)) { @@ -66,7 +67,7 @@ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { } } } - } else if (isVariableResponse(message)) { + } else if (isDebugResponse('variables', message)) { if (this.pendingMessages.has(message.request_seq)) { const parentReference = this.pendingMessages.get(message.request_seq)!; this.pendingMessages.delete(message.request_seq); @@ -149,23 +150,3 @@ export class VariableTracker implements AdapterCapabilities { return this.sessions.get(session.id)?.getSizeOfVariable?.(variableName, session); } } - -export function isScopesRequest(message: unknown): message is DebugProtocol.ScopesRequest { - const candidate = message as DebugProtocol.ScopesRequest; - return !!candidate && candidate.command === 'scopes'; -} - -export function isVariableRequest(message: unknown): message is DebugProtocol.VariablesRequest { - const candidate = message as DebugProtocol.VariablesRequest; - return !!candidate && candidate.command === 'variables'; -} - -export function isScopesResponse(message: unknown): message is DebugProtocol.ScopesResponse { - const candidate = message as DebugProtocol.ScopesResponse; - return !!candidate && candidate.command === 'scopes' && Array.isArray(candidate.body.scopes); -} - -export function isVariableResponse(message: unknown): message is DebugProtocol.VariablesResponse { - const candidate = message as DebugProtocol.VariablesResponse; - return !!candidate && candidate.command === 'variables' && Array.isArray(candidate.body.variables); -} diff --git a/src/plugin/memory-provider.ts b/src/plugin/memory-provider.ts index 690cbae..7bacae7 100644 --- a/src/plugin/memory-provider.ts +++ b/src/plugin/memory-provider.ts @@ -20,18 +20,13 @@ import { VariableRange, WrittenMemory } from '../common/memory-range'; import { ReadMemoryResult, SessionContext, WriteMemoryResult } from '../common/messaging'; import { AdapterRegistry } from './adapter-registry/adapter-registry'; import * as manifest from './manifest'; -import { sendRequest } from '../common/debug-requests'; +import { isDebugEvent, isDebugRequest, isDebugResponse, sendRequest } from '../common/debug-requests'; import { stringToBytesMemory } from '../common/memory'; export interface LabeledUint8Array extends Uint8Array { label?: string; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isInitializeMessage = (message: any): message is DebugProtocol.InitializeResponse => message.command === 'initialize' && message.type === 'response'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isStoppedEvent = (message: any): boolean => message.type === 'event' && message.event === 'stopped'; - export class MemoryProvider { public static ReadKey = `${manifest.PACKAGE_NAME}.canRead`; public static WriteKey = `${manifest.PACKAGE_NAME}.canWrite`; @@ -46,7 +41,8 @@ export class MemoryProvider { private _onDidChangeSessionContext = new vscode.EventEmitter(); public readonly onDidChangeSessionContext = this._onDidChangeSessionContext.event; - protected readonly sessions = new Map(); + protected readonly sessionDebugCapabilities = new Map(); + protected readonly sessionClientCapabilities = new Map(); constructor(protected adapterRegistry: AdapterRegistry) { } @@ -70,21 +66,27 @@ export class MemoryProvider { contributedTracker?.onWillStopSession?.(); }, onDidSendMessage: message => { - if (isInitializeMessage(message)) { + if (isDebugResponse('initialize', message)) { // Check for right capabilities in the adapter - this.sessions.set(session.id, message.body); + this.sessionDebugCapabilities.set(session.id, message.body); if (vscode.debug.activeDebugSession?.id === session.id) { this.setContext(session); } - } - if (isStoppedEvent(message)) { + } else if (isDebugEvent('stopped', message)) { this._onDidStopDebug.fire(session); + } else if (isDebugEvent('memory', message)) { + this._onDidWriteMemory.fire(message.body); } contributedTracker?.onDidSendMessage?.(message); }, onError: error => { contributedTracker?.onError?.(error); }, onExit: (code, signal) => { contributedTracker?.onExit?.(code, signal); }, - onWillReceiveMessage: message => { contributedTracker?.onWillReceiveMessage?.(message); } + onWillReceiveMessage: message => { + if (isDebugRequest('initialize', message)) { + this.sessionClientCapabilities.set(session.id, message.arguments); + } + contributedTracker?.onWillReceiveMessage?.(message); + } }); }; @@ -99,11 +101,12 @@ export class MemoryProvider { } protected debugSessionTerminated(session: vscode.DebugSession): void { - this.sessions.delete(session.id); + this.sessionDebugCapabilities.delete(session.id); + this.sessionClientCapabilities.delete(session.id); } protected setContext(session?: vscode.DebugSession): void { - const capabilities = session && this.sessions.get(session.id); + const capabilities = session && this.sessionDebugCapabilities.get(session.id); this._sessionContext = { sessionId: session?.id, canRead: !!capabilities?.supportsReadMemoryRequest, @@ -117,12 +120,20 @@ export class MemoryProvider { /** Returns the session if the capability is present, otherwise throws. */ protected assertCapability(capability: keyof DebugProtocol.Capabilities, action: string): vscode.DebugSession { const session = this.assertActiveSession(action); - if (!this.sessions.get(session.id)?.[capability]) { + if (!this.hasDebugCapabilitiy(session, capability)) { throw new Error(`Cannot ${action}. Session does not have capability ${capability}.`); } return session; } + protected hasDebugCapabilitiy(session: vscode.DebugSession, capability: keyof DebugProtocol.Capabilities): boolean { + return !!this.sessionDebugCapabilities.get(session.id)?.[capability]; + } + + protected hasClientCapabilitiy(session: vscode.DebugSession, capability: keyof DebugProtocol.InitializeRequestArguments): boolean { + return !!this.sessionClientCapabilities.get(session.id)?.[capability]; + } + protected assertActiveSession(action: string): vscode.DebugSession { if (!vscode.debug.activeDebugSession) { throw new Error(`Cannot ${action}. No active debug session.`); @@ -135,11 +146,16 @@ export class MemoryProvider { } public async writeMemory(args: DebugProtocol.WriteMemoryArguments & { count?: number }): Promise { - return sendRequest(this.assertCapability('supportsWriteMemoryRequest', 'write memory'), 'writeMemory', args).then(response => { + const session = this.assertCapability('supportsWriteMemoryRequest', 'write memory'); + return sendRequest(session, 'writeMemory', args).then(response => { const offset = response?.offset ? (args.offset ?? 0) + response.offset : args.offset; // we accept count as an additional argument so we can skip the memory length calculation const count = response?.bytesWritten ?? args.count ?? stringToBytesMemory(args.data).length; - this._onDidWriteMemory.fire({ memoryReference: args.memoryReference, offset, count }); + if (!this.hasClientCapabilitiy(session, 'supportsMemoryEvent')) { + // we only send out a custom event if we don't expect the client to handle the memory event + // since our client is VS Code we can assume that they will always support this but better to be safe + this._onDidWriteMemory.fire({ memoryReference: args.memoryReference, offset, count }); + } return response; }); } diff --git a/src/plugin/memory-storage.ts b/src/plugin/memory-storage.ts index 0bb1d33..84e55a9 100644 --- a/src/plugin/memory-storage.ts +++ b/src/plugin/memory-storage.ts @@ -27,6 +27,7 @@ import * as manifest from './manifest'; import { MemoryProvider } from './memory-provider'; import { ApplyMemoryArguments, ApplyMemoryResult, MemoryOptions, StoreMemoryArguments } from '../common/messaging'; import { isVariablesContext } from './external-views'; +import { isWebviewContext } from '../common/webview-context'; export const StoreCommandType = `${manifest.PACKAGE_NAME}.store-file`; export const ApplyCommandType = `${manifest.PACKAGE_NAME}.apply-file`; @@ -92,6 +93,9 @@ export class MemoryStorage { if (!args) { return {}; } + if (isWebviewContext(args)) { + return { ...args.activeReadArguments }; + } if (isVariablesContext(args)) { try { const variableName = args.variable.evaluateName ?? args.variable.name; @@ -108,7 +112,7 @@ export class MemoryStorage { protected async getStoreMemoryOptions(providedDefault?: Partial): Promise { const memoryReference = await vscode.window.showInputBox({ - title: 'Store Memory as File (1/3)', + title: 'Store Memory to File (1/3)', prompt: 'Start Memory Address', placeHolder: 'Hex address or expression', value: providedDefault?.memoryReference ?? DEFAULT_STORE_OPTIONS.memoryReference, @@ -118,7 +122,7 @@ export class MemoryStorage { return; } const offset = await vscode.window.showInputBox({ - title: 'Store Memory as File (2/3)', + title: 'Store Memory to File (2/3)', prompt: 'Memory Address Offset', placeHolder: 'Positive or negative offset in bytes', value: providedDefault?.offset?.toString() ?? DEFAULT_STORE_OPTIONS.offset.toString(), @@ -128,7 +132,7 @@ export class MemoryStorage { return; } const count = await vscode.window.showInputBox({ - title: 'Store Memory as File (3/3)', + title: 'Store Memory to File (3/3)', prompt: 'Length', placeHolder: 'Number of bytes to read', value: providedDefault?.count?.toString() ?? DEFAULT_STORE_OPTIONS.count.toString(), @@ -188,7 +192,11 @@ export class MemoryStorage { // if we are already given a URI, let's not bother the user and simply use it return { uri: providedDefault.uri }; } - const selectedUris = await vscode.window.showOpenDialog({ title: 'Apply Memory', filters: IntelHEX.DialogFilters }); + const selectedUris = await vscode.window.showOpenDialog({ + title: 'Apply Memory', + filters: IntelHEX.DialogFilters, + defaultUri: vscode.workspace.workspaceFolders?.[0]?.uri + }); if (selectedUris && selectedUris?.length > 0) { return { uri: selectedUris[0] }; } diff --git a/src/webview/components/memory-table.tsx b/src/webview/components/memory-table.tsx index 8533ea7..a57f76c 100644 --- a/src/webview/components/memory-table.tsx +++ b/src/webview/components/memory-table.tsx @@ -442,7 +442,7 @@ export class MemoryTable extends React.PureComponent) => void = e => this.doHandleKeyDown(e); protected doHandleKeyDown(event: KeyboardEvent): void { - if (event.code === 'Enter') { + if (event.key === 'Enter') { const id = event.currentTarget.id as InputId; const value = event.currentTarget.value; diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index 7277d2e..679c2b2 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -53,6 +53,7 @@ import { hoverService, HoverService } from './hovers/hover-service'; import { AddressHover } from './hovers/address-hover'; import { DataHover } from './hovers/data-hover'; import { VariableHover } from './hovers/variable-hover'; +import { debounce } from 'lodash'; export interface MemoryAppState extends MemoryState, MemoryDisplayConfiguration { messageParticipant: WebviewIdMessageParticipant; @@ -149,13 +150,14 @@ class App extends React.Component<{}, MemoryAppState> { hoverService.setMemoryState(this.state); } - protected memoryWritten(writtenMemory: WrittenMemory): void { + // use a slight debounce as the same event may come in short succession + protected memoryWritten = debounce((writtenMemory: WrittenMemory): void => { if (!this.state.memory) { return; } if (this.state.activeReadArguments.memoryReference === writtenMemory.memoryReference) { // catch simple case - this.updateMemoryState(); + this.fetchMemory(); return; } try { @@ -171,7 +173,8 @@ class App extends React.Component<{}, MemoryAppState> { endAddress: this.state.memory.address + BigInt(this.state.memory.bytes.length) }; if (doOverlap(written, shown)) { - this.updateMemoryState(); + this.fetchMemory(); + return; } } catch (error) { // ignore and fall through @@ -179,8 +182,8 @@ class App extends React.Component<{}, MemoryAppState> { // we could try to convert any expression we may have to an address by sending an evaluation request to the DA // but for now we just go with a pessimistic approach: if we are unsure, we refresh the memory - this.updateMemoryState(); - } + this.fetchMemory(); + }, 100); protected sessionContextChanged(sessionContext: SessionContext): void { this.setState({ sessionContext }); From 83c51b5fc421391a1a50d6da1abcc67882cbf1f9 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Wed, 13 Mar 2024 09:48:34 +0100 Subject: [PATCH 5/7] Add further PR feedback - Use short (PascalCase) names for generic arguments - Adapt copyright header in debug-requests - Remove optional 'count' parameter in messaging and memory-provider - Remove caching of session context and simply re-create if necessary - Execute variable queries in parallel instead of sequentially - Fix typo in comment --- src/common/debug-requests.ts | 6 ++-- src/common/messaging.ts | 2 +- .../adapter-registry/adapter-capabilities.ts | 2 +- src/plugin/adapter-registry/c-tracker.ts | 9 +++--- src/plugin/memory-provider.ts | 31 +++++++++---------- src/plugin/memory-storage.ts | 2 +- src/plugin/memory-webview-main.ts | 6 ++-- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/common/debug-requests.ts b/src/common/debug-requests.ts index 1e2f54f..dd3d78a 100644 --- a/src/common/debug-requests.ts +++ b/src/common/debug-requests.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2018 Red Hat, Inc. and others. + * Copyright (C) 2024 EclipseSource. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -32,8 +32,8 @@ export interface DebugEvents { 'stopped': DebugProtocol.StoppedEvent } -export type DebugRequest = Omit & { command: COMMAND, arguments: ARGS }; -export type DebugResponse = Omit & { command: COMMAND, body: BODY }; +export type DebugRequest = Omit & { command: C, arguments: A }; +export type DebugResponse = Omit & { command: C, body: B }; export type DebugEvent = DebugProtocol.Event & { body: T }; export async function sendRequest(session: DebugSession, diff --git a/src/common/messaging.ts b/src/common/messaging.ts index 2b71c6f..0fe2e43 100644 --- a/src/common/messaging.ts +++ b/src/common/messaging.ts @@ -29,7 +29,7 @@ export type MemoryOptions = Partial; export type ReadMemoryArguments = DebugRequestTypes['readMemory'][0]; export type ReadMemoryResult = DebugRequestTypes['readMemory'][1]; -export type WriteMemoryArguments = DebugRequestTypes['writeMemory'][0] & { count?: number }; +export type WriteMemoryArguments = DebugRequestTypes['writeMemory'][0]; export type WriteMemoryResult = DebugRequestTypes['writeMemory'][1]; export type StoreMemoryArguments = MemoryOptions & { proposedOutputName?: string } | VariablesView.IVariablesContext | WebviewContext; diff --git a/src/plugin/adapter-registry/adapter-capabilities.ts b/src/plugin/adapter-registry/adapter-capabilities.ts index 6d029fe..b8e1940 100644 --- a/src/plugin/adapter-registry/adapter-capabilities.ts +++ b/src/plugin/adapter-registry/adapter-capabilities.ts @@ -112,7 +112,7 @@ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { throw new Error('To be implemented by derived classes!'); } - /** Resolves the address of a given variable in bytes withthe current context. */ + /** Resolves the address of a given variable in bytes within the current context. */ getAddressOfVariable?(variableName: string, session: vscode.DebugSession): Promise; /** Resolves the size of a given variable in bytes within the current context. */ diff --git a/src/plugin/adapter-registry/c-tracker.ts b/src/plugin/adapter-registry/c-tracker.ts index adc2817..8c3caa8 100644 --- a/src/plugin/adapter-registry/c-tracker.ts +++ b/src/plugin/adapter-registry/c-tracker.ts @@ -42,13 +42,14 @@ export class CTracker extends AdapterVariableTracker { return undefined; } try { - const variableAddress = await this.getAddressOfVariable(variable.name, session); + const [variableAddress, variableSize] = await Promise.all([ + variable.memoryReference && hexAddress.test(variable.memoryReference) ? variable.memoryReference : this.getAddressOfVariable(variable.name, session), + this.getSizeOfVariable(variable.name, session) + ]); if (!variableAddress) { return undefined; } - const variableSize = await this.getSizeOfVariable(variable.name, session); - if (!variableSize) { return undefined; } const startAddress = BigInt(variableAddress); - const endAddress = BigInt(variableAddress) + variableSize; + const endAddress = variableSize !== undefined ? startAddress + variableSize : undefined; this.logger.debug('Resolved', variable.name, { start: variableAddress, size: variableSize }); return { name: variable.name, diff --git a/src/plugin/memory-provider.ts b/src/plugin/memory-provider.ts index 7bacae7..14640e6 100644 --- a/src/plugin/memory-provider.ts +++ b/src/plugin/memory-provider.ts @@ -37,7 +37,6 @@ export class MemoryProvider { private _onDidWriteMemory = new vscode.EventEmitter(); public readonly onDidWriteMemory = this._onDidWriteMemory.event; - private _sessionContext: SessionContext = { canRead: false, canWrite: false }; private _onDidChangeSessionContext = new vscode.EventEmitter(); public readonly onDidChangeSessionContext = this._onDidChangeSessionContext.event; @@ -47,10 +46,6 @@ export class MemoryProvider { constructor(protected adapterRegistry: AdapterRegistry) { } - get sessionContext(): SessionContext { - return this._sessionContext; - } - public activate(context: vscode.ExtensionContext): void { const createDebugAdapterTracker = (session: vscode.DebugSession): Required => { const handlerForSession = this.adapterRegistry.getHandlerForSession(session.type); @@ -105,16 +100,21 @@ export class MemoryProvider { this.sessionClientCapabilities.delete(session.id); } - protected setContext(session?: vscode.DebugSession): void { - const capabilities = session && this.sessionDebugCapabilities.get(session.id); - this._sessionContext = { - sessionId: session?.id, + createContext(session = vscode.debug.activeDebugSession): SessionContext { + const sessionId = session?.id; + const capabilities = sessionId ? this.sessionDebugCapabilities.get(sessionId) : undefined; + return { + sessionId, canRead: !!capabilities?.supportsReadMemoryRequest, canWrite: !!capabilities?.supportsWriteMemoryRequest }; - vscode.commands.executeCommand('setContext', MemoryProvider.ReadKey, this.sessionContext.canRead); - vscode.commands.executeCommand('setContext', MemoryProvider.WriteKey, this.sessionContext.canWrite); - this._onDidChangeSessionContext.fire(this.sessionContext); + } + + protected setContext(session?: vscode.DebugSession): void { + const newContext = this.createContext(session); + vscode.commands.executeCommand('setContext', MemoryProvider.ReadKey, newContext.canRead); + vscode.commands.executeCommand('setContext', MemoryProvider.WriteKey, newContext.canWrite); + this._onDidChangeSessionContext.fire(newContext); } /** Returns the session if the capability is present, otherwise throws. */ @@ -145,15 +145,14 @@ export class MemoryProvider { return sendRequest(this.assertCapability('supportsReadMemoryRequest', 'read memory'), 'readMemory', args); } - public async writeMemory(args: DebugProtocol.WriteMemoryArguments & { count?: number }): Promise { + public async writeMemory(args: DebugProtocol.WriteMemoryArguments): Promise { const session = this.assertCapability('supportsWriteMemoryRequest', 'write memory'); return sendRequest(session, 'writeMemory', args).then(response => { - const offset = response?.offset ? (args.offset ?? 0) + response.offset : args.offset; - // we accept count as an additional argument so we can skip the memory length calculation - const count = response?.bytesWritten ?? args.count ?? stringToBytesMemory(args.data).length; if (!this.hasClientCapabilitiy(session, 'supportsMemoryEvent')) { // we only send out a custom event if we don't expect the client to handle the memory event // since our client is VS Code we can assume that they will always support this but better to be safe + const offset = response?.offset ? (args.offset ?? 0) + response.offset : args.offset; + const count = response?.bytesWritten ?? stringToBytesMemory(args.data).length; this._onDidWriteMemory.fire({ memoryReference: args.memoryReference, offset, count }); } return response; diff --git a/src/plugin/memory-storage.ts b/src/plugin/memory-storage.ts index 84e55a9..65915bb 100644 --- a/src/plugin/memory-storage.ts +++ b/src/plugin/memory-storage.ts @@ -169,7 +169,7 @@ export class MemoryStorage { memoryReference = toHexStringWithRadixMarker(address); count = memory.length; const data = bytesToStringMemory(memory); - await this.memoryProvider.writeMemory({ memoryReference, data, count }); + await this.memoryProvider.writeMemory({ memoryReference, data }); } await vscode.window.showInformationMessage(`Memory from '${vscode.workspace.asRelativePath(options.uri)}' applied.`); return { memoryReference, count, offset: 0 }; diff --git a/src/plugin/memory-webview-main.ts b/src/plugin/memory-webview-main.ts index 3f4d8cf..6462148 100644 --- a/src/plugin/memory-webview-main.ts +++ b/src/plugin/memory-webview-main.ts @@ -206,7 +206,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { const disposables = [ this.messenger.onNotification(readyType, () => { this.setInitialSettings(participant, panel.title); - this.setSessionContext(participant, this.memoryProvider.sessionContext); + this.setSessionContext(participant, this.memoryProvider.createContext()); this.refresh(participant, options); }, { sender: participant }), this.messenger.onRequest(setOptionsType, o => { @@ -316,7 +316,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { protected async storeMemory(storeArguments: StoreMemoryArguments): Promise { // Even if we disable the command in VS Code through enablement or when condition, programmatic execution is still possible. // However, we want to fail early in case the user tries to execute a disabled command - if (!this.memoryProvider.sessionContext.canRead) { + if (!this.memoryProvider.createContext().canRead) { throw new Error('Cannot read memory, no valid debug session.'); } return vscode.commands.executeCommand(StoreCommandType, storeArguments); @@ -325,7 +325,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { protected async applyMemory(): Promise { // Even if we disable the command in VS Code through enablement or when condition, programmatic execution is still possible. // However, we want to fail early in case the user tries to execute a disabled command - if (!this.memoryProvider.sessionContext.canWrite) { + if (!this.memoryProvider.createContext().canWrite) { throw new Error('Cannot write memory, no valid debug session.'); } return vscode.commands.executeCommand(ApplyCommandType); From 175a3308d266c3fe590d073140a0155336ea0fa5 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Wed, 13 Mar 2024 12:07:25 +0100 Subject: [PATCH 6/7] Unify usage of hex address conversion --- src/plugin/adapter-registry/adapter-capabilities.ts | 4 ++++ src/plugin/adapter-registry/c-tracker.ts | 7 +++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/plugin/adapter-registry/adapter-capabilities.ts b/src/plugin/adapter-registry/adapter-capabilities.ts index b8e1940..06088e4 100644 --- a/src/plugin/adapter-registry/adapter-capabilities.ts +++ b/src/plugin/adapter-registry/adapter-capabilities.ts @@ -38,6 +38,10 @@ export type VariablesTree = Record { const response = await sendRequest(session, 'evaluate', { expression: CEvaluateExpression.addressOf(variableName), context: 'watch', frameId: this.currentFrame }); - const addressPart = hexAddress.exec(response.result); - return addressPart ? addressPart[0] : undefined; + return findHexAddress(response.result); } async getSizeOfVariable(variableName: string, session: vscode.DebugSession): Promise { From edf986d3d26e07c9687a348640de68b5f6a36207 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Wed, 13 Mar 2024 20:26:54 +0100 Subject: [PATCH 7/7] Improve address handling and support decimal addresses - Provide decimal address regex and function to extract address - Ensure we can properly serialize bigints when logging data - Ensure we return a proper variable range if we only have the address --- src/common/typescript.ts | 5 +++ .../adapter-registry/adapter-capabilities.ts | 12 ++++++- src/plugin/adapter-registry/c-tracker.ts | 35 ++++++++++--------- src/plugin/logger.ts | 3 +- src/webview/variables/variable-decorations.ts | 6 +--- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/src/common/typescript.ts b/src/common/typescript.ts index e1bef53..d8ef137 100644 --- a/src/common/typescript.ts +++ b/src/common/typescript.ts @@ -19,3 +19,8 @@ export function tryToNumber(value?: string | number): number | undefined { if (value === '' || isNaN(asNumber)) { return undefined; } return asNumber; } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function stringifyWithBigInts(object: any, space?: string | number): any { + return JSON.stringify(object, (_key, value) => typeof value === 'bigint' ? value.toString() : value, space); +} diff --git a/src/plugin/adapter-registry/adapter-capabilities.ts b/src/plugin/adapter-registry/adapter-capabilities.ts index 06088e4..63bc225 100644 --- a/src/plugin/adapter-registry/adapter-capabilities.ts +++ b/src/plugin/adapter-registry/adapter-capabilities.ts @@ -36,12 +36,22 @@ export interface AdapterCapabilities { export type WithChildren = Original & { children?: Array> }; export type VariablesTree = Record>; export const hexAddress = /0x[0-9a-f]+/i; +export const decimalAddress = /[0-9]+/i; export const notADigit = /[^0-9]/; -export function findHexAddress(text?: string): string | undefined { +export function extractHexAddress(text?: string): string | undefined { return text ? hexAddress.exec(text)?.[0] : undefined; } +export function extractDecimalAddress(text?: string): string | undefined { + return text ? decimalAddress.exec(text)?.[0] : undefined; +} + +export function extractAddress(text?: string): string | undefined { + // search for hex address first as a hex adress (0x12345678) also matches an integer address (12345678) + return text ? extractHexAddress(text) ?? extractDecimalAddress(text) : undefined; +} + /** This class implements some of the basic elements of tracking adapter sessions in order to maintain a list of variables. */ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { protected currentFrame?: number; diff --git a/src/plugin/adapter-registry/c-tracker.ts b/src/plugin/adapter-registry/c-tracker.ts index 825c16e..a3377d9 100644 --- a/src/plugin/adapter-registry/c-tracker.ts +++ b/src/plugin/adapter-registry/c-tracker.ts @@ -16,7 +16,7 @@ import * as vscode from 'vscode'; import { DebugProtocol } from '@vscode/debugprotocol'; -import { AdapterVariableTracker, findHexAddress, notADigit } from './adapter-capabilities'; +import { AdapterVariableTracker, extractAddress, notADigit } from './adapter-capabilities'; import { toHexStringWithRadixMarker, VariableRange } from '../../common/memory-range'; import { sendRequest } from '../../common/debug-requests'; @@ -41,32 +41,35 @@ export class CTracker extends AdapterVariableTracker { { noName: !variable.name, noFrame: this.currentFrame === undefined }); return undefined; } + let variableAddress = extractAddress(variable.memoryReference); + let variableSize: bigint | undefined = undefined; try { - const [variableAddress, variableSize] = await Promise.all([ - findHexAddress(variable.memoryReference) ?? this.getAddressOfVariable(variable.name, session), + [variableAddress, variableSize] = await Promise.all([ + variableAddress ?? this.getAddressOfVariable(variable.name, session), this.getSizeOfVariable(variable.name, session) ]); - if (!variableAddress) { return undefined; } - - const startAddress = BigInt(variableAddress); - const endAddress = variableSize !== undefined ? startAddress + variableSize : undefined; - this.logger.debug('Resolved', variable.name, { start: variableAddress, size: variableSize }); - return { - name: variable.name, - startAddress: toHexStringWithRadixMarker(startAddress), - endAddress: endAddress === undefined ? undefined : toHexStringWithRadixMarker(endAddress), - value: variable.value, - type: variable.type, - }; } catch (err) { this.logger.warn('Unable to resolve location and size of', variable.name + (err instanceof Error ? ':\n\t' + err.message : '')); + // fall through as we may still have a valid variable address that we can use + } + if (!variableAddress) { return undefined; } + this.logger.debug('Resolved', variable.name, { start: variableAddress, size: variableSize }); + const address = BigInt(variableAddress); + const variableRange: VariableRange = { + name: variable.name, + startAddress: toHexStringWithRadixMarker(address), + endAddress: variableSize === undefined ? undefined : toHexStringWithRadixMarker(address + variableSize), + value: variable.value, + type: variable.type, + }; + return variableRange; } async getAddressOfVariable(variableName: string, session: vscode.DebugSession): Promise { const response = await sendRequest(session, 'evaluate', { expression: CEvaluateExpression.addressOf(variableName), context: 'watch', frameId: this.currentFrame }); - return findHexAddress(response.result); + return extractAddress(response.result); } async getSizeOfVariable(variableName: string, session: vscode.DebugSession): Promise { diff --git a/src/plugin/logger.ts b/src/plugin/logger.ts index bd0271e..aaf9542 100644 --- a/src/plugin/logger.ts +++ b/src/plugin/logger.ts @@ -16,6 +16,7 @@ import * as vscode from 'vscode'; import * as manifest from './manifest'; +import { stringifyWithBigInts } from '../common/typescript'; export enum Verbosity { off = 0, @@ -50,7 +51,7 @@ export abstract class Logger { return; } - const result = messages.map(message => typeof message === 'string' ? message : JSON.stringify(message, undefined, '\t')).join(' '); + const result = messages.map(message => typeof message === 'string' ? message : stringifyWithBigInts(message, '\t')).join(' '); this.logMessage(result); } diff --git a/src/webview/variables/variable-decorations.ts b/src/webview/variables/variable-decorations.ts index 93ee20a..226ae8c 100644 --- a/src/webview/variables/variable-decorations.ts +++ b/src/webview/variables/variable-decorations.ts @@ -25,6 +25,7 @@ import { ReactNode } from 'react'; import { areVariablesEqual, compareBigInt, BigIntMemoryRange, BigIntVariableRange, doOverlap } from '../../common/memory-range'; import * as React from 'react'; import { createVariableVscodeContext } from '../utils/vscode-contexts'; +import { stringifyWithBigInts } from '../../common/typescript'; const NON_HC_COLORS = [ 'var(--vscode-terminal-ansiBlue)', @@ -137,9 +138,4 @@ export class VariableDecorator implements ColumnContribution, Decorator { } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function stringifyWithBigInts(object: any): any { - return JSON.stringify(object, (_key, value) => typeof value === 'bigint' ? value.toString() : value); -} - export const variableDecorator = new VariableDecorator();