Skip to content

Commit

Permalink
Support storing and applying of memory content (#96)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
martin-fleck-at committed Mar 13, 2024
1 parent 3cd4184 commit bbf3e9d
Show file tree
Hide file tree
Showing 29 changed files with 965 additions and 246 deletions.
7 changes: 4 additions & 3 deletions media/options-widget.css
Expand Up @@ -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;
}
Expand Down
50 changes: 49 additions & 1 deletion package.json
Expand Up @@ -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",
Expand Down Expand Up @@ -107,6 +109,18 @@
"title": "Advanced Display Options",
"category": "Memory",
"enablement": "webviewId === memory-inspector.memory"
},
{
"command": "memory-inspector.store-file",
"title": "Store Memory to File",
"enablement": "memory-inspector.canRead",
"category": "Memory"
},
{
"command": "memory-inspector.apply-file",
"title": "Apply Memory from File",
"enablement": "memory-inspector.canWrite",
"category": "Memory"
}
],
"menus": {
Expand All @@ -118,12 +132,22 @@
{
"command": "memory-inspector.show-variable",
"when": "false"
},
{
"command": "memory-inspector.store-file"
},
{
"command": "memory-inspector.apply-file"
}
],
"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": [
Expand All @@ -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",
Expand All @@ -152,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"
}
]
},
Expand Down
72 changes: 72 additions & 0 deletions src/common/debug-requests.ts
@@ -0,0 +1,72 @@
/********************************************************************************
* 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
********************************************************************************/
// 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']]
'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<C, A> = Omit<DebugProtocol.Request, 'command' | 'arguments'> & { command: C, arguments: A };
export type DebugResponse<C, B> = Omit<DebugProtocol.Response, 'command' | 'body'> & { command: C, body: B };
export type DebugEvent<T> = DebugProtocol.Event & { body: T };

export async function sendRequest<K extends keyof DebugRequestTypes>(session: DebugSession,
command: K, args: DebugRequestTypes[K][0]): Promise<DebugRequestTypes[K][1]> {
return session.customRequest(command, args);
}

export function isDebugVariable(variable: DebugProtocol.Variable | unknown): variable is DebugProtocol.Variable {
const assumed = variable ? variable as DebugProtocol.Variable : undefined;
return typeof assumed?.name === 'string' && typeof assumed?.value === 'string';
}

export function isDebugScope(scope: DebugProtocol.Scope | unknown): scope is DebugProtocol.Scope {
const assumed = scope ? scope as DebugProtocol.Scope : undefined;
return typeof assumed?.name === 'string' && typeof assumed?.variablesReference === 'number';
}

export function isDebugEvaluateArguments(args: DebugProtocol.EvaluateArguments | unknown): args is DebugProtocol.EvaluateArguments {
const assumed = args ? args as DebugProtocol.EvaluateArguments : undefined;
return typeof assumed?.expression === 'string';
}

export function isDebugRequest<K extends keyof DebugRequestTypes>(command: K, message: unknown): message is DebugRequest<K, DebugRequestTypes[K][0]> {
const assumed = message ? message as DebugProtocol.Request : undefined;
return !!assumed && assumed.type === 'request' && assumed.command === command;
}

export function isDebugResponse<K extends keyof DebugRequestTypes>(command: K, message: unknown): message is DebugResponse<K, DebugRequestTypes[K][1]> {
const assumed = message ? message as DebugProtocol.Response : undefined;
return !!assumed && assumed.type === 'response' && assumed.command === command;
}

export function isDebugEvent<K extends keyof DebugEvents>(event: K, message: unknown): message is DebugEvents[K] {
const assumed = message ? message as DebugProtocol.Event : undefined;
return !!assumed && assumed.type === 'event' && assumed.event === event;
}
43 changes: 43 additions & 0 deletions 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': ['*']
};
};
12 changes: 9 additions & 3 deletions src/common/memory-range.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -85,16 +91,16 @@ export function getRadixMarker(radix: Radix): string {
return radixPrefixMap[radix];
}

export function getAddressString(address: bigint, radix: Radix, paddedLength: number = 0): string {
export function getAddressString(address: bigint | number, radix: Radix, paddedLength: number = 0): string {
return address.toString(radix).padStart(paddedLength, '0');
}

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 | number, paddedLength: number = 0): string {
return `${getRadixMarker(Radix.Hexadecimal)}${getAddressString(target, Radix.Hexadecimal, paddedLength)}`;
}

export interface VariableMetadata {
Expand Down
61 changes: 61 additions & 0 deletions src/common/memory.ts
@@ -0,0 +1,61 @@
/********************************************************************************
* 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 { ReadMemoryArguments, ReadMemoryResult } from './messaging';

export interface Memory {
address: bigint;
bytes: Uint8Array;
}

export function createMemoryFromRead(result: ReadMemoryResult, request?: ReadMemoryArguments): Memory {
if (!result?.data) {
const message = request ? `No memory provided for address ${request.memoryReference}`
+ `, offset ${request.offset} and count ${request.count}!` : 'No memory provided.';
throw new Error(message);
}
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 must be >= 0' : undefined;
}

export function validateOffset(offset: string): string | undefined {
const asNumber = Number(offset);
return isNaN(asNumber) ? 'Must be number' : undefined;
}

export function validateCount(count: string): string | undefined {
const asNumber = Number(count);
if (isNaN(asNumber)) {
return 'Must be number';
} else if (asNumber <= 0) {
return 'Value must be > 0';
}
}
45 changes: 37 additions & 8 deletions src/common/messaging.ts
Expand Up @@ -17,20 +17,49 @@
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 { URI } from 'vscode-uri';
import { VariablesView } from '../plugin/external-views';
import { WebviewContext } from './webview-context';

export type MemoryReadResult = DebugProtocol.ReadMemoryResponse['body'];
export type MemoryWriteResult = DebugProtocol.WriteMemoryResponse['body'];
// convenience types for easier readability and better semantics
export type MemoryOptions = Partial<DebugProtocol.ReadMemoryArguments>;

export type ReadMemoryArguments = DebugRequestTypes['readMemory'][0];
export type ReadMemoryResult = DebugRequestTypes['readMemory'][1];

export type WriteMemoryArguments = DebugRequestTypes['writeMemory'][0];
export type WriteMemoryResult = DebugRequestTypes['writeMemory'][1];

export type StoreMemoryArguments = MemoryOptions & { proposedOutputName?: string } | VariablesView.IVariablesContext | WebviewContext;
export type StoreMemoryResult = void;

export type ApplyMemoryArguments = URI | undefined;
export type ApplyMemoryResult = MemoryOptions;

export interface SessionContext {
sessionId?: string;
canRead: boolean;
canWrite: boolean;
}

// Notifications
export const readyType: NotificationType<void> = { method: 'ready' };
export const logMessageType: RequestType<string, void> = { method: 'logMessage' };
export const setMemoryViewSettingsType: NotificationType<Partial<MemoryViewSettings>> = { method: 'setMemoryViewSettings' };
export const resetMemoryViewSettingsType: NotificationType<void> = { method: 'resetMemoryViewSettings' };
export const setTitleType: NotificationType<string> = { method: 'setTitle' };
export const setOptionsType: RequestType<Partial<DebugProtocol.ReadMemoryArguments | undefined>, void> = { method: 'setOptions' };
export const readMemoryType: RequestType<DebugProtocol.ReadMemoryArguments, MemoryReadResult> = { method: 'readMemory' };
export const writeMemoryType: RequestType<DebugProtocol.WriteMemoryArguments, MemoryWriteResult> = { method: 'writeMemory' };
export const getVariables: RequestType<DebugProtocol.ReadMemoryArguments, VariableRange[]> = { method: 'getVariables' };
export const memoryWrittenType: NotificationType<WrittenMemory> = { method: 'memoryWritten' };
export const sessionContextChangedType: NotificationType<SessionContext> = { method: 'sessionContextChanged' };

// Requests
export const setOptionsType: RequestType<MemoryOptions, void> = { method: 'setOptions' };
export const logMessageType: RequestType<string, void> = { method: 'logMessage' };
export const readMemoryType: RequestType<ReadMemoryArguments, ReadMemoryResult> = { method: 'readMemory' };
export const writeMemoryType: RequestType<WriteMemoryArguments, WriteMemoryResult> = { method: 'writeMemory' };
export const getVariablesType: RequestType<ReadMemoryArguments, VariableRange[]> = { method: 'getVariables' };
export const storeMemoryType: RequestType<StoreMemoryArguments, void> = { method: 'storeMemory' };
export const applyMemoryType: RequestType<ApplyMemoryArguments, ApplyMemoryResult> = { method: 'applyMemory' };

export const showAdvancedOptionsType: NotificationType<void> = { method: 'showAdvancedOptions' };
export const getWebviewSelectionType: RequestType<void, WebviewSelection> = { method: 'getWebviewSelection' };
Expand Down
5 changes: 5 additions & 0 deletions src/common/typescript.ts
Expand Up @@ -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);
}

0 comments on commit bbf3e9d

Please sign in to comment.