Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support storing and applying of memory content #96

Merged
merged 7 commits into from Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}