From 29bde3176c7451e966797931aa8a1f7c3fce2983 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 22 Sep 2021 16:40:05 -0700 Subject: [PATCH 1/6] debug: initial work on memory support This implements the core memory "model" for debugging that reflects DAP, in `debugModel.ts`. It also implements a filesystem provider based on that in `debugMemory.ts`, for tentative application in the hex editor. Finally it adds context menu items for these. This works with changes in mock debug, but for some reason reopening the ".bin" file in the hex editor results in a blank editor. Still need to look at that. Ultimately though, as indicated in #126268, we'll probably want custom commands for the hex editor to call as low level read/write is not supported in the stable API. Also, the file API doesn't represent the "unreadable" ranges which DAP supports. --- src/vs/base/common/buffer.ts | 101 +++++++++ src/vs/base/test/common/buffer.test.ts | 51 ++++- src/vs/base/test/common/mock.ts | 6 +- .../debug/browser/debug.contribution.ts | 6 +- .../contrib/debug/browser/debugMemory.ts | 205 ++++++++++++++++++ .../contrib/debug/browser/debugService.ts | 4 +- .../contrib/debug/browser/debugSession.ts | 79 ++++--- .../contrib/debug/browser/rawDebugSession.ts | 16 ++ .../contrib/debug/browser/variablesView.ts | 24 +- .../workbench/contrib/debug/common/debug.ts | 108 +++++++-- .../contrib/debug/common/debugModel.ts | 182 +++++++++++++++- .../contrib/debug/common/debugViewModel.ts | 6 +- .../debug/test/browser/baseDebugView.test.ts | 4 +- .../debug/test/browser/debugHover.test.ts | 4 +- .../debug/test/browser/debugMemory.test.ts | 190 ++++++++++++++++ .../contrib/debug/test/browser/mockDebug.ts | 36 ++- .../view/renderers/backLayerWebView.ts | 10 +- .../test/browser/api/extHostTesting.test.ts | 4 +- 18 files changed, 949 insertions(+), 87 deletions(-) create mode 100644 src/vs/workbench/contrib/debug/browser/debugMemory.ts create mode 100644 src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index cebbbd3562081..546ecf27dcdf0 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -284,3 +284,104 @@ export function prefixedBufferReadable(prefix: VSBuffer, readable: VSBufferReada export function prefixedBufferStream(prefix: VSBuffer, stream: VSBufferReadableStream): VSBufferReadableStream { return streams.prefixedStream(prefix, stream, chunks => VSBuffer.concat(chunks)); } + +/** Decodes base64 to a uint8 array. URL-encoded and unpadded base64 is allowed. */ +export function decodeBase64(encoded: string) { + let building = 0; + let remainder = 0; + let bufi = 0; + + // The simpler way to do this is `Uint8Array.from(atob(str), c => c.charCodeAt(0))`, + // but that's about 10-20x slower than this function in current Chromium versions. + + const buffer = new Uint8Array(Math.floor(encoded.length / 4 * 3)); + const append = (value: number) => { + switch (remainder) { + case 3: + buffer[bufi++] = building | value; + remainder = 0; + break; + case 2: + buffer[bufi++] = building | (value >>> 2); + building = value << 6; + remainder = 3; + break; + case 1: + buffer[bufi++] = building | (value >>> 4); + building = value << 4; + remainder = 2; + break; + default: + building = value << 2; + remainder = 1; + } + }; + + for (let i = 0; i < encoded.length; i++) { + const code = encoded.charCodeAt(i); + // See https://datatracker.ietf.org/doc/html/rfc4648#section-4 + // This branchy code is about 3x faster than an indexOf on a base64 char string. + if (code >= 65 && code <= 90) { + append(code - 65); // A-Z starts ranges from char code 65 to 90 + } else if (code >= 97 && code <= 122) { + append(code - 97 + 26); // a-z starts ranges from char code 97 to 122, starting at byte 26 + } else if (code >= 48 && code <= 57) { + append(code - 48 + 52); // 0-9 starts ranges from char code 48 to 58, starting at byte 52 + } else if (code === 43 || code === 45) { + append(62); // "+" or "-" for URLS + } else if (code === 47 || code === 95) { + append(63); // "/" or "_" for URLS + } else if (code === 61) { + break; // "=" + } else { + throw new SyntaxError(`Unexpected base64 character ${encoded[i]}`); + } + } + + const unpadded = bufi; + while (remainder > 0) { + append(0); + } + + // slice is needed to account for overestimation due to padding + return VSBuffer.wrap(buffer).slice(0, unpadded); +} + +const base64Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +const base64UrlSafeAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + +/** Encodes a buffer to a base64 string. */ +export function encodeBase64({ buffer }: VSBuffer, padded = true, urlSafe = false) { + const dictionary = urlSafe ? base64UrlSafeAlphabet : base64Alphabet; + let output = ''; + + const remainder = buffer.byteLength % 3; + + let i = 0; + for (; i < buffer.byteLength - remainder; i += 3) { + const a = buffer[i + 0]; + const b = buffer[i + 1]; + const c = buffer[i + 2]; + + output += dictionary[a >>> 2]; + output += dictionary[(a << 4 | b >>> 4) & 0b111111]; + output += dictionary[(b << 2 | c >>> 6) & 0b111111]; + output += dictionary[c & 0b111111]; + } + + if (remainder === 1) { + const a = buffer[i + 0]; + output += dictionary[a >>> 2]; + output += dictionary[(a << 4) & 0b111111]; + if (padded) { output += '=='; } + } else if (remainder === 2) { + const a = buffer[i + 0]; + const b = buffer[i + 1]; + output += dictionary[a >>> 2]; + output += dictionary[(a << 4 | b >>> 4) & 0b111111]; + output += dictionary[(b << 2) & 0b111111]; + if (padded) { output += '='; } + } + + return output; +} diff --git a/src/vs/base/test/common/buffer.test.ts b/src/vs/base/test/common/buffer.test.ts index 461002be924cd..c2ff953d3c499 100644 --- a/src/vs/base/test/common/buffer.test.ts +++ b/src/vs/base/test/common/buffer.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { timeout } from 'vs/base/common/async'; -import { bufferedStreamToBuffer, bufferToReadable, bufferToStream, newWriteableBufferStream, readableToBuffer, streamToBuffer, VSBuffer } from 'vs/base/common/buffer'; +import { bufferedStreamToBuffer, bufferToReadable, bufferToStream, decodeBase64, encodeBase64, newWriteableBufferStream, readableToBuffer, streamToBuffer, VSBuffer } from 'vs/base/common/buffer'; import { peekStream } from 'vs/base/common/stream'; suite('Buffer', () => { @@ -412,4 +412,53 @@ suite('Buffer', () => { assert.strictEqual(u2[0], 17); } }); + + suite('base64', () => { + /* + Generated with: + + const crypto = require('crypto'); + + for (let i = 0; i < 16; i++) { + const buf = crypto.randomBytes(i); + console.log(`[new Uint8Array([${Array.from(buf).join(', ')}]), '${buf.toString('base64')}'],`) + } + + */ + + const testCases: [Uint8Array, string][] = [ + [new Uint8Array([]), ''], + [new Uint8Array([56]), 'OA=='], + [new Uint8Array([209, 4]), '0QQ='], + [new Uint8Array([19, 57, 119]), 'Ezl3'], + [new Uint8Array([199, 237, 207, 112]), 'x+3PcA=='], + [new Uint8Array([59, 193, 173, 26, 242]), 'O8GtGvI='], + [new Uint8Array([81, 226, 95, 231, 116, 126]), 'UeJf53R+'], + [new Uint8Array([11, 164, 253, 85, 8, 6, 56]), 'C6T9VQgGOA=='], + [new Uint8Array([164, 16, 88, 88, 224, 173, 144, 114]), 'pBBYWOCtkHI='], + [new Uint8Array([0, 196, 99, 12, 21, 229, 78, 101, 13]), 'AMRjDBXlTmUN'], + [new Uint8Array([167, 114, 225, 116, 226, 83, 51, 48, 88, 114]), 'p3LhdOJTMzBYcg=='], + [new Uint8Array([75, 33, 118, 10, 77, 5, 168, 194, 59, 47, 59]), 'SyF2Ck0FqMI7Lzs='], + [new Uint8Array([203, 182, 165, 51, 208, 27, 123, 223, 112, 198, 127, 147]), 'y7alM9Abe99wxn+T'], + [new Uint8Array([154, 93, 222, 41, 117, 234, 250, 85, 95, 144, 16, 94, 18]), 'ml3eKXXq+lVfkBBeEg=='], + [new Uint8Array([246, 186, 88, 105, 192, 57, 25, 168, 183, 164, 103, 162, 243, 56]), '9rpYacA5Gai3pGei8zg='], + [new Uint8Array([149, 240, 155, 96, 30, 55, 162, 172, 191, 187, 33, 124, 169, 183, 254]), 'lfCbYB43oqy/uyF8qbf+'], + ]; + + test('encodes', () => { + for (const [bytes, expected] of testCases) { + assert.strictEqual(encodeBase64(VSBuffer.wrap(bytes)), expected); + } + }); + + test('decodes', () => { + for (const [expected, encoded] of testCases) { + assert.deepStrictEqual(new Uint8Array(decodeBase64(encoded).buffer), expected); + } + }); + + test('throws error on invalid encoding', () => { + assert.throws(() => decodeBase64('invalid!')); + }); + }); }); diff --git a/src/vs/base/test/common/mock.ts b/src/vs/base/test/common/mock.ts index 3b31f0fb3d55f..7a0d9fbefd72f 100644 --- a/src/vs/base/test/common/mock.ts +++ b/src/vs/base/test/common/mock.ts @@ -13,11 +13,11 @@ export function mock(): Ctor { return function () { } as any; } -export type MockObject = { [K in keyof T]: K extends keyof TP ? TP[K] : SinonStub }; +export type MockObject = { [K in keyof T]: K extends ExceptProps ? T[K] : SinonStub }; // Creates an object object that returns sinon mocks for every property. Optionally // takes base properties. -export function mockObject>(properties?: TP): MockObject { +export const mockObject = () => = {}>(properties?: TP): MockObject => { return new Proxy({ ...properties } as any, { get(target, key) { if (!target.hasOwnProperty(key)) { @@ -31,4 +31,4 @@ export function mockObject>(properties?: return true; }, }); -} +}; diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 435bcf9adfe8d..367a3633517f6 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -16,7 +16,7 @@ import { CallStackView } from 'vs/workbench/contrib/debug/browser/callStackView' import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IDebugService, VIEWLET_ID, DEBUG_PANEL_ID, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA, - CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, getStateLabel, State, CONTEXT_WATCH_ITEM_TYPE, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, DISASSEMBLY_VIEW_ID, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY, + CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, getStateLabel, State, CONTEXT_WATCH_ITEM_TYPE, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, DISASSEMBLY_VIEW_ID, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_CAN_VIEW_MEMORY, } from 'vs/workbench/contrib/debug/common/debug'; import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import { DebugService } from 'vs/workbench/contrib/debug/browser/debugService'; @@ -32,7 +32,7 @@ import { launchSchemaId } from 'vs/workbench/services/configuration/common/confi import { LoadedScriptsView } from 'vs/workbench/contrib/debug/browser/loadedScriptsView'; import { RunToCursorAction } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; import { WatchExpressionsView, ADD_WATCH_LABEL, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, ADD_WATCH_ID } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; -import { VariablesView, SET_VARIABLE_ID, COPY_VALUE_ID, BREAK_WHEN_VALUE_CHANGES_ID, COPY_EVALUATE_PATH_ID, ADD_TO_WATCH_ID, BREAK_WHEN_VALUE_IS_ACCESSED_ID, BREAK_WHEN_VALUE_IS_READ_ID } from 'vs/workbench/contrib/debug/browser/variablesView'; +import { VariablesView, SET_VARIABLE_ID, COPY_VALUE_ID, BREAK_WHEN_VALUE_CHANGES_ID, COPY_EVALUATE_PATH_ID, ADD_TO_WATCH_ID, BREAK_WHEN_VALUE_IS_ACCESSED_ID, BREAK_WHEN_VALUE_IS_READ_ID, VIEW_MEMORY_ID } from 'vs/workbench/contrib/debug/browser/variablesView'; import { Repl } from 'vs/workbench/contrib/debug/browser/repl'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; @@ -143,6 +143,7 @@ registerDebugViewMenuItem(MenuId.DebugCallStackContext, RESTART_FRAME_ID, nls.lo registerDebugViewMenuItem(MenuId.DebugCallStackContext, COPY_STACK_TRACE_ID, nls.localize('copyStackTrace', "Copy Call Stack"), 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'), undefined, '3_modification'); registerDebugViewMenuItem(MenuId.DebugVariablesContext, SET_VARIABLE_ID, nls.localize('setValue', "Set Value"), 10, ContextKeyExpr.or(CONTEXT_SET_VARIABLE_SUPPORTED, ContextKeyExpr.and(CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, CONTEXT_SET_EXPRESSION_SUPPORTED)), CONTEXT_VARIABLE_IS_READONLY.toNegated(), '3_modification'); +registerDebugViewMenuItem(MenuId.DebugVariablesContext, VIEW_MEMORY_ID, nls.localize('viewMemory', "View Memory"), 15, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_IN_DEBUG_MODE, '3_modification'); registerDebugViewMenuItem(MenuId.DebugVariablesContext, COPY_VALUE_ID, nls.localize('copyValue', "Copy Value"), 10, undefined, undefined, '5_cutcopypaste'); registerDebugViewMenuItem(MenuId.DebugVariablesContext, COPY_EVALUATE_PATH_ID, nls.localize('copyAsExpression', "Copy as Expression"), 20, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, undefined, '5_cutcopypaste'); registerDebugViewMenuItem(MenuId.DebugVariablesContext, ADD_TO_WATCH_ID, nls.localize('addToWatchExpressions', "Add to Watch"), 100, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, undefined, 'z_commands'); @@ -154,6 +155,7 @@ registerDebugViewMenuItem(MenuId.DebugWatchContext, ADD_WATCH_ID, ADD_WATCH_LABE registerDebugViewMenuItem(MenuId.DebugWatchContext, EDIT_EXPRESSION_COMMAND_ID, nls.localize('editWatchExpression', "Edit Expression"), 20, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, '3_modification'); registerDebugViewMenuItem(MenuId.DebugWatchContext, SET_EXPRESSION_COMMAND_ID, nls.localize('setValue', "Set Value"), 30, ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), CONTEXT_SET_EXPRESSION_SUPPORTED), ContextKeyExpr.and(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('variable'), CONTEXT_SET_VARIABLE_SUPPORTED)), CONTEXT_VARIABLE_IS_READONLY.toNegated(), '3_modification'); registerDebugViewMenuItem(MenuId.DebugWatchContext, COPY_VALUE_ID, nls.localize('copyValue', "Copy Value"), 40, ContextKeyExpr.or(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), CONTEXT_WATCH_ITEM_TYPE.isEqualTo('variable')), CONTEXT_IN_DEBUG_MODE, '3_modification'); +registerDebugViewMenuItem(MenuId.DebugWatchContext, VIEW_MEMORY_ID, nls.localize('viewMemory', "View Memory"), 50, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_IN_DEBUG_MODE, '3_modification'); registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_EXPRESSION_COMMAND_ID, nls.localize('removeWatchExpression', "Remove Expression"), 10, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, 'z_commands'); registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, 20, undefined, undefined, 'z_commands'); diff --git a/src/vs/workbench/contrib/debug/browser/debugMemory.ts b/src/vs/workbench/contrib/debug/browser/debugMemory.ts new file mode 100644 index 0000000000000..421174cedd49b --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/debugMemory.ts @@ -0,0 +1,205 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { Emitter, Event } from 'vs/base/common/event'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { assertNever } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { FileChangeType, FileOpenOptions, FilePermission, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileChange, IFileSystemProvider, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { DEBUG_MEMORY_SCHEME, IDebugService, IDebugSession, IMemoryRegion, MemoryRangeType } from 'vs/workbench/contrib/debug/common/debug'; + +const rangeRe = /range=([0-9]+):([0-9]+)/; + +export class DebugMemoryFileSystemProvider implements IFileSystemProvider { + private memoryFdCounter = 0; + private readonly fdMemory = new Map(); + private readonly changeEmitter = new Emitter(); + + /** @inheritdoc */ + public readonly onDidChangeCapabilities = Event.None; + + /** @inheritdoc */ + public readonly onDidChangeFile = this.changeEmitter.event; + + /** @inheritdoc */ + public readonly capabilities = 0 + | FileSystemProviderCapabilities.PathCaseSensitive + | FileSystemProviderCapabilities.FileOpenReadWriteClose; + + constructor(private readonly debugService: IDebugService) { + debugService.onDidEndSession(session => { + for (const [fd, memory] of this.fdMemory) { + if (memory.session === session) { + this.close(fd); + } + } + }); + } + + public watch(resource: URI, opts: IWatchOptions) { + if (opts.recursive) { + return toDisposable(() => { }); + } + + const { session, memoryReference } = this.parseUri(resource); + return session.onDidInvalidateMemory(e => { + if (e.body.memoryReference === memoryReference) { + this.changeEmitter.fire([{ resource, type: FileChangeType.UPDATED }]); + } + }); + } + + /** @inheritdoc */ + public stat(file: URI): Promise { + const { readOnly } = this.parseUri(file); + return Promise.resolve({ + type: FileType.File, + mtime: 0, + ctime: 0, + size: 0, + permissions: readOnly ? FilePermission.Readonly : undefined, + }); + } + + /** @inheritdoc */ + public mkdir(): never { + throw new FileSystemProviderError(`Not allowed`, FileSystemProviderErrorCode.NoPermissions); + } + + /** @inheritdoc */ + public readdir(): never { + throw new FileSystemProviderError(`Not allowed`, FileSystemProviderErrorCode.NoPermissions); + } + + /** @inheritdoc */ + public delete(): never { + throw new FileSystemProviderError(`Not allowed`, FileSystemProviderErrorCode.NoPermissions); + } + + /** @inheritdoc */ + public rename(): never { + throw new FileSystemProviderError(`Not allowed`, FileSystemProviderErrorCode.NoPermissions); + } + + /** @inheritdoc */ + public open(resource: URI, _opts: FileOpenOptions): Promise { + const { session, memoryReference } = this.parseUri(resource); + const fd = this.memoryFdCounter++; + this.fdMemory.set(fd, { session, region: session.getMemory(memoryReference) }); + return Promise.resolve(fd); + } + + /** @inheritdoc */ + public close(fd: number) { + this.fdMemory.get(fd)?.region.dispose(); + this.fdMemory.delete(fd); + return Promise.resolve(); + } + + /** @inheritdoc */ + public async writeFile(resource: URI, content: Uint8Array) { + const { offset } = this.parseUri(resource); + if (!offset) { + throw new FileSystemProviderError(`Range must be present to read a file`, FileSystemProviderErrorCode.FileNotFound); + } + + const fd = await this.open(resource, { create: false }); + + try { + await this.write(fd, offset.fromOffset, content, 0, content.length); + } finally { + this.close(fd); + } + } + + /** @inheritdoc */ + public async readFile(resource: URI) { + const { offset } = this.parseUri(resource); + if (!offset) { + throw new FileSystemProviderError(`Range must be present to read a file`, FileSystemProviderErrorCode.FileNotFound); + } + + const data = new Uint8Array(offset.toOffset - offset.fromOffset); + const fd = await this.open(resource, { create: false }); + + try { + await this.read(fd, offset.fromOffset, data, 0, data.length); + return data; + } finally { + this.close(fd); + } + } + + /** @inheritdoc */ + public async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + const memory = this.fdMemory.get(fd); + if (!memory) { + throw new FileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable); + } + + const ranges = await memory.region.read(pos, length); + let readSoFar = 0; + for (const range of ranges) { + switch (range.type) { + case MemoryRangeType.Unreadable: + return readSoFar; + case MemoryRangeType.Error: + if (readSoFar > 0) { + return readSoFar; + } else { + throw new FileSystemProviderError(range.error, FileSystemProviderErrorCode.Unknown); + } + case MemoryRangeType.Valid: + const start = Math.max(0, pos - range.offset); + const toWrite = range.data.slice(start, Math.min(range.data.byteLength, start + (length - readSoFar))); + data.set(toWrite.buffer, offset + readSoFar); + readSoFar += toWrite.byteLength; + break; + default: + assertNever(range); + } + } + + return readSoFar; + } + + /** @inheritdoc */ + public write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { + const memory = this.fdMemory.get(fd); + if (!memory) { + throw new FileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable); + } + + return memory.region.write(pos, VSBuffer.wrap(data).slice(offset, offset + length)); + } + + protected parseUri(uri: URI) { + if (uri.scheme !== DEBUG_MEMORY_SCHEME) { + throw new FileSystemProviderError(`Cannot open file with scheme ${uri.scheme}`, FileSystemProviderErrorCode.FileNotFound); + } + + const session = this.debugService.getModel().getSession(uri.authority); + if (!session) { + throw new FileSystemProviderError(`Debug session not found`, FileSystemProviderErrorCode.FileNotFound); + } + + let offset: { fromOffset: number; toOffset: number } | undefined; + const rangeMatch = rangeRe.exec(uri.query); + if (rangeMatch) { + offset = { fromOffset: Number(rangeMatch[1]), toOffset: Number(rangeMatch[2]) }; + } + + const [, memoryReference] = uri.path.split('/'); + + return { + session, + offset, + readOnly: !!session.capabilities.supportsWriteMemoryRequest, + sessionId: uri.authority, + memoryReference, + }; + } +} diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 1868f0cf87598..8cc5722084218 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -34,9 +34,10 @@ import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs import { AdapterManager } from 'vs/workbench/contrib/debug/browser/debugAdapterManager'; import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ConfigurationManager } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; +import { DebugMemoryFileSystemProvider } from 'vs/workbench/contrib/debug/browser/debugMemory'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { DebugTaskRunner, TaskRunResult } from 'vs/workbench/contrib/debug/browser/debugTaskRunner'; -import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_MODE, debuggerDisabledMessage, getStateLabel, IAdapterManager, IBreakpoint, IBreakpointData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_MODE, debuggerDisabledMessage, DEBUG_MEMORY_SCHEME, getStateLabel, IAdapterManager, IBreakpoint, IBreakpointData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; @@ -138,6 +139,7 @@ export class DebugService implements IDebugService { this.viewModel = new ViewModel(contextKeyService); this.taskRunner = this.instantiationService.createInstance(DebugTaskRunner); + this.disposables.add(this.fileService.registerProvider(DEBUG_MEMORY_SCHEME, new DebugMemoryFileSystemProvider(this))); this.disposables.add(this.fileService.onDidFilesChange(e => this.onFileChanges(e))); this.disposables.add(this.lifecycleService.onWillShutdown(this.dispose, this)); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 429f9bde87f43..2b6cedacd13cf 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -3,41 +3,41 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; -import * as resources from 'vs/base/common/resources'; -import * as platform from 'vs/base/common/platform'; -import severity from 'vs/base/common/severity'; -import { Event, Emitter } from 'vs/base/common/event'; -import { Position, IPosition } from 'vs/editor/common/core/position'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { IDebugSession, IConfig, IThread, IRawModelUpdate, IDebugService, IRawStoppedDetails, State, LoadedSourceEvent, IFunctionBreakpoint, IExceptionBreakpoint, IBreakpoint, IExceptionInfo, AdapterEndEvent, IDebugger, VIEWLET_ID, IDebugConfiguration, IReplElement, IStackFrame, IExpression, IReplElementSource, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; -import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { distinct } from 'vs/base/common/arrays'; +import { Queue, RunOnceScheduler } from 'vs/base/common/async'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { canceled } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { normalizeDriveLetter } from 'vs/base/common/labels'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { mixin } from 'vs/base/common/objects'; -import { Thread, ExpressionContainer, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; -import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { IWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { RunOnceScheduler, Queue } from 'vs/base/common/async'; +import * as platform from 'vs/base/common/platform'; +import * as resources from 'vs/base/common/resources'; +import severity from 'vs/base/common/severity'; +import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { ICustomEndpointTelemetryService, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; -import { normalizeDriveLetter } from 'vs/base/common/labels'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; -import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; -import { distinct } from 'vs/base/common/arrays'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { localize } from 'vs/nls'; -import { canceled } from 'vs/base/common/errors'; -import { filterExceptionsFromTelemetry } from 'vs/workbench/contrib/debug/common/debugUtils'; -import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { ICustomEndpointTelemetryService, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { ViewContainerLocation } from 'vs/workbench/common/views'; +import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; +import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDebugConfiguration, IDebugger, IDebugService, IDebugSession, IDebugSessionOptions, IExceptionBreakpoint, IExceptionInfo, IExpression, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IReplElementSource, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; +import { DebugModel, ExpressionContainer, MemoryRegion, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { filterExceptionsFromTelemetry } from 'vs/workbench/contrib/debug/common/debugUtils'; +import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { ViewContainerLocation } from 'vs/workbench/common/views'; export class DebugSession implements IDebugSession { @@ -65,6 +65,7 @@ export class DebugSession implements IDebugSession { private readonly _onDidProgressStart = new Emitter(); private readonly _onDidProgressUpdate = new Emitter(); private readonly _onDidProgressEnd = new Emitter(); + private readonly _onDidInvalidMemory = new Emitter(); private readonly _onDidChangeREPLElements = new Emitter(); @@ -139,6 +140,10 @@ export class DebugSession implements IDebugSession { this._subId = subId; } + getMemory(memoryReference: string): IMemoryRegion { + return new MemoryRegion(memoryReference, this); + } + get subId(): string | undefined { return this._subId; } @@ -247,6 +252,10 @@ export class DebugSession implements IDebugSession { return this._onDidProgressEnd.event; } + get onDidInvalidateMemory(): Event { + return this._onDidInvalidMemory.event; + } + //---- DAP requests /** @@ -768,6 +777,22 @@ export class DebugSession implements IDebugSession { return response?.body?.instructions; } + readMemory(memoryReference: string, offset: number, count: number): Promise { + if (!this.raw) { + return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'readMemory'))); + } + + return this.raw.readMemory({ count, memoryReference, offset }); + } + + writeMemory(memoryReference: string, offset: number, data: string, allowPartial?: boolean): Promise { + if (!this.raw) { + return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'disassemble'))); + } + + return this.raw.writeMemory({ memoryReference, offset, allowPartial, data }); + } + //---- threads getThread(threadId: number): Thread | undefined { diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 24ffbc48e10da..2487937719abb 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -521,6 +521,22 @@ export class RawDebugSession implements IDisposable { return Promise.reject(new Error('disassemble is not supported')); } + async readMemory(args: DebugProtocol.ReadMemoryArguments): Promise { + if (this.capabilities.supportsReadMemoryRequest) { + return await this.send('readMemory', args); + } + + return Promise.reject(new Error('disassemble is not supported')); + } + + async writeMemory(args: DebugProtocol.WriteMemoryArguments): Promise { + if (this.capabilities.supportsWriteMemoryRequest) { + return await this.send('writeMemory', args); + } + + return Promise.reject(new Error('disassemble is not supported')); + } + cancel(args: DebugProtocol.CancelArguments): Promise { return this.send('cancel', args); } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 447971aefc307..70bdcd58d1058 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -6,8 +6,8 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, IDataBreakpointInfoResponse, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, VARIABLES_VIEW_ID, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY } from 'vs/workbench/contrib/debug/common/debug'; -import { Variable, Scope, ErrorScope, StackFrame, Expression } from 'vs/workbench/contrib/debug/common/debugModel'; +import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, IDataBreakpointInfoResponse, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, VARIABLES_VIEW_ID, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_CAN_VIEW_MEMORY } from 'vs/workbench/contrib/debug/common/debug'; +import { Variable, Scope, ErrorScope, StackFrame, Expression, getUriForDebugMemory } from 'vs/workbench/contrib/debug/common/debugModel'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { renderViewTree, renderVariable, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; @@ -45,6 +45,7 @@ let variableInternalContext: Variable | undefined; let dataBreakpointInfoResponse: IDataBreakpointInfoResponse | undefined; interface IVariablesContext { + sessionId: string | undefined; container: DebugProtocol.Variable | DebugProtocol.Scope; variable: DebugProtocol.Variable; } @@ -63,6 +64,7 @@ export class VariablesView extends ViewPane { private breakWhenValueIsReadSupported: IContextKey; private variableEvaluateName: IContextKey; private variableReadonly: IContextKey; + private viewMemorySupported: IContextKey; constructor( options: IViewletViewOptions, @@ -87,6 +89,7 @@ export class VariablesView extends ViewPane { this.breakWhenValueIsAccessedSupported = CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED.bindTo(contextKeyService); this.breakWhenValueIsReadSupported = CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED.bindTo(contextKeyService); this.variableEvaluateName = CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT.bindTo(contextKeyService); + this.viewMemorySupported = CONTEXT_CAN_VIEW_MEMORY.bindTo(contextKeyService); this.variableReadonly = CONTEXT_VARIABLE_IS_READONLY.bindTo(contextKeyService); // Use scheduler to prevent unnecessary flashing @@ -213,6 +216,7 @@ export class VariablesView extends ViewPane { variableInternalContext = variable; const session = this.debugService.getViewModel().focusedSession; this.variableEvaluateName.set(!!variable.evaluateName); + this.viewMemorySupported.set(!!session?.capabilities.supportsReadMemoryRequest && variable.memoryReference !== undefined); const attributes = variable.presentationHint?.attributes; this.variableReadonly.set(!!attributes && attributes.indexOf('readOnly') >= 0); this.breakWhenValueChangesSupported.reset(); @@ -243,6 +247,7 @@ export class VariablesView extends ViewPane { } const context: IVariablesContext = { + sessionId: variable.getSession()?.getId(), container: (variable.parent as (Variable | Scope)).toDebugProtocolObject(), variable: variable.toDebugProtocolObject() }; @@ -469,6 +474,21 @@ CommandsRegistry.registerCommand({ } }); +export const VIEW_MEMORY_ID = 'workbench.debug.viewlet.action.viewMemory'; +CommandsRegistry.registerCommand({ + id: VIEW_MEMORY_ID, + handler: async (accessor: ServicesAccessor, arg: IVariablesContext, ctx?: (Variable | Expression)[]) => { + if (!arg.sessionId || !arg.variable.memoryReference) { + return; + } + + accessor.get(IOpenerService).open(getUriForDebugMemory( + arg.sessionId, + arg.variable.memoryReference, + )); + } +}); + export const BREAK_WHEN_VALUE_CHANGES_ID = 'debug.breakWhenValueChanges'; CommandsRegistry.registerCommand({ id: BREAK_WHEN_VALUE_CHANGES_ID, diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 944941375f17d..4b691f887edf0 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -3,29 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { URI as uri } from 'vs/base/common/uri'; -import severity from 'vs/base/common/severity'; +import { IAction } from 'vs/base/common/actions'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import severity from 'vs/base/common/severity'; +import { URI as uri } from 'vs/base/common/uri'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ITextModel as EditorIModel } from 'vs/editor/common/model'; -import { IEditorPane } from 'vs/workbench/common/editor'; -import { Position, IPosition } from 'vs/editor/common/core/position'; -import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; -import { Range, IRange } from 'vs/editor/common/core/range'; +import * as nls from 'vs/nls'; +import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ITelemetryEndpoint } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; -import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; +import { IEditorPane } from 'vs/workbench/common/editor'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { IAction } from 'vs/base/common/actions'; -import { ITelemetryEndpoint } from 'vs/platform/telemetry/common/telemetry'; +import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export const VIEWLET_ID = 'workbench.view.debug'; @@ -58,6 +59,7 @@ export const CONTEXT_CALLSTACK_SESSION_IS_ATTACH = new RawContextKey('c export const CONTEXT_CALLSTACK_ITEM_STOPPED = new RawContextKey('callStackItemStopped', false, { type: 'boolean', description: nls.localize('callStackItemStopped', "True when the focused item in the CALL STACK is stopped. Used internaly for inline menus in the CALL STACK view.") }); export const CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD = new RawContextKey('callStackSessionHasOneThread', false, { type: 'boolean', description: nls.localize('callStackSessionHasOneThread', "True when the focused session in the CALL STACK view has exactly one thread. Used internally for inline menus in the CALL STACK view.") }); export const CONTEXT_WATCH_ITEM_TYPE = new RawContextKey('watchItemType', undefined, { type: 'string', description: nls.localize('watchItemType', "Represents the item type of the focused element in the WATCH view. For example: 'expression', 'variable'") }); +export const CONTEXT_CAN_VIEW_MEMORY = new RawContextKey('canViewMemory', undefined, { type: 'boolean', description: nls.localize('canViewMemory', "Indicates whether the item in the view has an associated memory refrence.") }); export const CONTEXT_BREAKPOINT_ITEM_TYPE = new RawContextKey('breakpointItemType', undefined, { type: 'string', description: nls.localize('breakpointItemType', "Represents the item type of the focused element in the BREAKPOINTS view. For example: 'breakpoint', 'exceptionBreakppint', 'functionBreakpoint', 'dataBreakpoint'") }); export const CONTEXT_BREAKPOINT_ACCESS_TYPE = new RawContextKey('breakpointAccessType', undefined, { type: 'string', description: nls.localize('breakpointAccessType', "Represents the access type of the focused data breakpoint in the BREAKPOINTS view. For example: 'read', 'readWrite', 'write'") }); export const CONTEXT_BREAKPOINT_SUPPORTS_CONDITION = new RawContextKey('breakpointSupportsCondition', false, { type: 'boolean', description: nls.localize('breakpointSupportsCondition', "True when the focused breakpoint supports conditions.") }); @@ -204,6 +206,77 @@ export interface IDataBreakpointInfoResponse { accessTypes?: DebugProtocol.DataBreakpointAccessType[]; } +export interface IMemoryInvalidationEvent { + fromOffset: number; + toOffset: number; +} + +export const enum MemoryRangeType { + Valid, + Unreadable, + Error, +} + +export interface IMemoryRange { + type: MemoryRangeType; + offset: number; + length: number; +} + +export interface IValidMemoryRange extends IMemoryRange { + type: MemoryRangeType.Valid; + offset: number; + length: number; + data: VSBuffer +} + +export interface IUnreadableMemoryRange extends IMemoryRange { + type: MemoryRangeType.Unreadable; +} + +export interface IErrorMemoryRange extends IMemoryRange { + type: MemoryRangeType.Error; + error: string; +} + +/** + * Union type of memory that can be returned from read(). Since a read request + * could encompass multiple previously-read ranges, multiple of these types + * are possible to return. + */ +export type MemoryRange = IValidMemoryRange | IUnreadableMemoryRange | IErrorMemoryRange; + +export const DEBUG_MEMORY_SCHEME = 'vscode-debug-memory'; + +/** + * An IMemoryRegion corresponds to a contiguous range of memory referred to + * by a DAP `memoryReference`. + */ +export interface IMemoryRegion extends IDisposable { + /** + * Event that fires when memory changes. Can be a result of memory events or + * `write` requests. + */ + readonly onDidInvalidate: Event; + + /** + * Whether writes are supported on this memory region. + */ + readonly writable: boolean; + + /** + * Requests memory ranges from the debug adapter. It returns a list of memory + * ranges that overlap (but may exceed!) the given offset. Use the `offset` + * and `length` of each range for display. + */ + read(fromOffset: number, toOffset: number): Promise; + + /** + * Writes memory to the debug adapter at the given offset. + */ + write(offset: number, data: VSBuffer): Promise; +} + export interface IDebugSession extends ITreeElement { readonly configuration: IConfig; @@ -219,6 +292,8 @@ export interface IDebugSession extends ITreeElement { setSubId(subId: string | undefined): void; + getMemory(memoryReference: string): IMemoryRegion; + setName(name: string): void; readonly onDidChangeName: Event; getLabel(): string; @@ -256,6 +331,7 @@ export interface IDebugSession extends ITreeElement { readonly onDidProgressStart: Event; readonly onDidProgressUpdate: Event; readonly onDidProgressEnd: Event; + readonly onDidInvalidateMemory: Event; // DAP request @@ -282,6 +358,8 @@ export interface IDebugSession extends ITreeElement { customRequest(request: string, args: any): Promise; cancel(progressId: string): Promise; disassemble(memoryReference: string, offset: number, instructionOffset: number, instructionCount: number): Promise; + readMemory(memoryReference: string, offset: number, count: number): Promise; + writeMemory(memoryReference: string, offset: number, data: string, allowPartial?: boolean): Promise; restartFrame(frameId: number, threadId: number): Promise; next(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 92018dd69a0ae..d05eca56c148c 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -4,17 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { URI as uri } from 'vs/base/common/uri'; +import { URI, URI as uri } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; import { Event, Emitter } from 'vs/base/common/event'; import { generateUuid } from 'vs/base/common/uuid'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { DeferredPromise, RunOnceScheduler } from 'vs/base/common/async'; import { isString, isUndefinedOrNull } from 'vs/base/common/types'; -import { distinct, lastIndex } from 'vs/base/common/arrays'; +import { binarySearch, distinct, flatten, lastIndex } from 'vs/base/common/arrays'; import { Range, IRange } from 'vs/editor/common/core/range'; import { ITreeElement, IExpression, IExpressionContainer, IDebugSession, IStackFrame, IExceptionBreakpoint, IBreakpoint, IFunctionBreakpoint, IDebugModel, - IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IBreakpointData, IExceptionInfo, IBreakpointsChangeEvent, IBreakpointUpdateData, IBaseBreakpoint, State, IDataBreakpoint, IInstructionBreakpoint + IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IBreakpointData, IExceptionInfo, IBreakpointsChangeEvent, IBreakpointUpdateData, IBaseBreakpoint, State, IDataBreakpoint, IInstructionBreakpoint, IMemoryRegion, IMemoryInvalidationEvent, MemoryRange, MemoryRangeType, DEBUG_MEMORY_SCHEME } from 'vs/workbench/contrib/debug/common/debug'; import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -25,6 +25,8 @@ import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; interface IDebugProtocolVariableWithContext extends DebugProtocol.Variable { __vscodeVariableMenuContext?: string; @@ -48,6 +50,7 @@ export class ExpressionContainer implements IExpressionContainer { private id: string, public namedVariables: number | undefined = 0, public indexedVariables: number | undefined = 0, + public memoryReference: string | undefined = undefined, private startOfVariables: number | undefined = 0 ) { } @@ -92,7 +95,7 @@ export class ExpressionContainer implements IExpressionContainer { for (let i = 0; i < numberOfChunks; i++) { const start = (this.startOfVariables || 0) + i * chunkSize; const count = Math.min(chunkSize, this.indexedVariables - i * chunkSize); - children.push(new Variable(this.session, this.threadId, this, this.reference, `[${start}..${start + count - 1}]`, '', '', undefined, count, { kind: 'virtual' }, undefined, undefined, true, start)); + children.push(new Variable(this.session, this.threadId, this, this.reference, `[${start}..${start + count - 1}]`, '', '', undefined, count, undefined, { kind: 'virtual' }, undefined, undefined, true, start)); } return children; @@ -132,12 +135,12 @@ export class ExpressionContainer implements IExpressionContainer { const count = nameCount.get(v.name) || 0; const idDuplicationIndex = count > 0 ? count.toString() : ''; nameCount.set(v.name, count + 1); - return new Variable(this.session, this.threadId, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type, v.__vscodeVariableMenuContext, true, 0, idDuplicationIndex); + return new Variable(this.session, this.threadId, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.memoryReference, v.presentationHint, v.type, v.__vscodeVariableMenuContext, true, 0, idDuplicationIndex); } - return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, { kind: 'virtual' }, undefined, undefined, false); + return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, undefined, { kind: 'virtual' }, undefined, undefined, false); }); } catch (e) { - return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, { kind: 'virtual' }, undefined, undefined, false)]; + return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, undefined, { kind: 'virtual' }, undefined, undefined, false)]; } } @@ -178,6 +181,7 @@ export class ExpressionContainer implements IExpressionContainer { this.reference = response.body.variablesReference; this.namedVariables = response.body.namedVariables; this.indexedVariables = response.body.indexedVariables; + this.memoryReference = response.body.memoryReference; this.type = response.body.type || this.type; return true; } @@ -197,6 +201,7 @@ function handleSetResponse(expression: ExpressionContainer, response: DebugProto expression.reference = response.body.variablesReference; expression.namedVariables = response.body.namedVariables; expression.indexedVariables = response.body.indexedVariables; + // todo @weinand: the set responses contain most properties, but not memory references. Should they? } } @@ -248,6 +253,7 @@ export class Variable extends ExpressionContainer implements IExpression { value: string | undefined, namedVariables: number | undefined, indexedVariables: number | undefined, + memoryReference: string | undefined, public presentationHint: DebugProtocol.VariablePresentationHint | undefined, type: string | undefined = undefined, public variableMenuContext: string | undefined = undefined, @@ -255,7 +261,7 @@ export class Variable extends ExpressionContainer implements IExpression { startOfVariables = 0, idDuplicationIndex = '', ) { - super(session, threadId, reference, `variable:${parent.getId()}:${name}:${idDuplicationIndex}`, namedVariables, indexedVariables, startOfVariables); + super(session, threadId, reference, `variable:${parent.getId()}:${name}:${idDuplicationIndex}`, namedVariables, indexedVariables, memoryReference, startOfVariables); this.value = value || ''; this.type = type; } @@ -296,6 +302,7 @@ export class Variable extends ExpressionContainer implements IExpression { return { name: this.name, variablesReference: this.reference || 0, + memoryReference: this.memoryReference, value: this.value, evaluateName: this.evaluateName }; @@ -589,6 +596,159 @@ export class Thread implements IThread { } } +interface IMemoryRangeWrapper { + fromOffset: number; + toOffset: number; + value: DeferredPromise +} + +/** + * Gets a URI to a memory in the given session ID. + */ +export const getUriForDebugMemory = ( + sessionId: string, + memoryReference: string, + range?: { fromOffset: number, toOffset: number }, + displayName = 'memory' +) => { + return URI.from({ + scheme: DEBUG_MEMORY_SCHEME, + authority: sessionId, + path: '/' + encodeURIComponent(memoryReference) + `/${encodeURIComponent(displayName)}.bin`, + query: range ? `?range=${range.fromOffset}:${range.toOffset}` : undefined, + }); +}; + +export class MemoryRegion extends Disposable implements IMemoryRegion { + protected readonly ranges: IMemoryRangeWrapper[] = []; + private readonly invalidateEmitter = this._register(new Emitter()); + + /** @inheritdoc */ + public readonly onDidInvalidate = this.invalidateEmitter.event; + + /** @inheritdoc */ + public readonly writable = !!this.session.capabilities.supportsWriteMemoryRequest; + + constructor(private readonly memoryReference: string, private readonly session: IDebugSession) { + super(); + this._register(session.onDidInvalidateMemory(e => { + if (e.body.memoryReference === memoryReference) { + this.invalidate(e.body.offset, e.body.count - e.body.offset); + } + })); + } + + public read(fromOffset: number, toOffset: number): Promise { + // here, we make requests for all ranges within the offset bounds which + // we've not already requested. + let startIndex = this.getInsertIndex(fromOffset); + + let index = startIndex; + for (let lastEnd = fromOffset; lastEnd < toOffset;) { + const next = this.ranges[index]; + if (!next) { + this.ranges.push(this.makeRangeRequest(lastEnd, toOffset)); + index++; + break; + } + + if (next.fromOffset > lastEnd) { + this.ranges.splice(index, 0, this.makeRangeRequest(lastEnd, next.fromOffset)); + index++; + } + + lastEnd = next.toOffset; + index++; + } + + return Promise.all(this.ranges.slice(startIndex, index).map(r => r.value.p)).then(flatten); + } + + public async write(offset: number, data: VSBuffer): Promise { + const result = await this.session.writeMemory(this.memoryReference, offset, encodeBase64(data), true); + const written = result?.body?.bytesWritten ?? data.byteLength; + this.invalidate(offset, offset + written); + return written; + } + + public override dispose() { + super.dispose(); + this.ranges.forEach(r => { + if (!r.value.isSettled) { + r.value.cancel(); + } + }); + } + + private invalidate(fromOffset: number, toOffset: number) { + // Here we want to remove any read ranges for invalidated data so they + // can be read again later. + + let startIndex = this.getInsertIndex(fromOffset); + const endIndex = this.getInsertIndex(toOffset) + 1; + + if (this.ranges[startIndex]?.toOffset === fromOffset) { + startIndex++; + } + + // no-op if there were no read ranges that got invalidated + if (endIndex - startIndex <= 0) { + return; + } + + this.ranges.splice(startIndex, endIndex - startIndex); + this.invalidateEmitter.fire({ fromOffset, toOffset }); + } + + private getInsertIndex(fromOffset: number) { + const searchIndex = binarySearch<{ toOffset: number }>(this.ranges, { toOffset: fromOffset }, (a, b) => a.toOffset - b.toOffset); + return searchIndex < 0 ? (~searchIndex) : searchIndex; + } + + private makeRangeRequest(fromOffset: number, toOffset: number): IMemoryRangeWrapper { + const length = toOffset - fromOffset; + const offset = fromOffset; + const promise = new DeferredPromise(); + + this.session.readMemory(this.memoryReference, fromOffset, toOffset - fromOffset).then( + (result): MemoryRange[] => { + if (result === undefined || !result.body?.data) { + return [{ type: MemoryRangeType.Unreadable, offset, length }]; + } + + let data: VSBuffer; + try { + data = decodeBase64(result.body.data); + } catch { + return [{ type: MemoryRangeType.Error, offset, length, error: 'Invalid base64 data from debug adapter' }]; + } + + const unreadable = result.body.unreadableBytes || 0; + const dataLength = length - unreadable; + if (data.byteLength < dataLength) { + const pad = VSBuffer.alloc(dataLength - data.byteLength); + pad.buffer.fill(0); + data = VSBuffer.concat([data, pad], dataLength); + } else if (data.byteLength > dataLength) { + data = data.slice(0, dataLength); + } + + if (!unreadable) { + return [{ type: MemoryRangeType.Valid, offset, length, data }]; + } + + return [ + { type: MemoryRangeType.Valid, offset, length: dataLength, data }, + { type: MemoryRangeType.Unreadable, offset: offset + dataLength, length: unreadable }, + ]; + }, + (error): MemoryRange[] => [{ type: MemoryRangeType.Error, offset, length, error: error.message }] + ).then(r => promise.complete(r)); + + return { fromOffset, toOffset, value: promise }; + } +} + export class Enablement implements IEnablement { constructor( public enabled: boolean, @@ -990,6 +1150,10 @@ export class ThreadAndSessionIds implements ITreeElement { } } +export class Memory { + +} + export class DebugModel implements IDebugModel { private sessions: IDebugSession[]; diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index ee0e186583749..68bd9a62171aa 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Emitter } from 'vs/base/common/event'; -import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_SET_EXPRESSION_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug'; -import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; export class ViewModel implements IViewModel { diff --git a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index 4ddd3bbb80c24..8e964a3d9310a 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -90,7 +90,7 @@ suite('Debug - Base Debug View', () => { const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', { startLineNumber: 1, startColumn: 1, endLineNumber: undefined!, endColumn: undefined! }, 0, true); const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); - let variable = new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined!, 0, 0, {}, 'string'); + let variable = new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined!, 0, 0, undefined, {}, 'string'); let expression = $('.'); let name = $('.'); let value = $('.'); @@ -118,7 +118,7 @@ suite('Debug - Base Debug View', () => { assert.ok(value.querySelector('a')); assert.strictEqual(value.querySelector('a')!.textContent, variable.value); - variable = new Variable(session, 1, scope, 2, 'console', 'console', '5', 0, 0, { kind: 'virtual' }); + variable = new Variable(session, 1, scope, 2, 'console', 'console', '5', 0, 0, undefined, { kind: 'virtual' }); expression = $('.'); name = $('.'); value = $('.'); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts index 19b009efad283..e88228789a1e7 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugHover.test.ts @@ -49,8 +49,8 @@ suite('Debug - Hover', () => { override getChildren(): Promise { return Promise.resolve([variableB]); } - }(session, 1, scope, 2, 'A', 'A', undefined!, 0, 0, {}, 'string'); - variableB = new Variable(session, 1, scope, 2, 'B', 'A.B', undefined!, 0, 0, {}, 'string'); + }(session, 1, scope, 2, 'A', 'A', undefined!, 0, 0, undefined, {}, 'string'); + variableB = new Variable(session, 1, scope, 2, 'B', 'A.B', undefined!, 0, 0, undefined, {}, 'string'); assert.strictEqual(await findExpressionInStackFrame(stackFrame, []), undefined); assert.strictEqual(await findExpressionInStackFrame(stackFrame, ['A']), variableA); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts new file mode 100644 index 0000000000000..00cb22c794ef5 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts @@ -0,0 +1,190 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; +import { Emitter } from 'vs/base/common/event'; +import { mockObject, MockObject } from 'vs/base/test/common/mock'; +import { MemoryRangeType } from 'vs/workbench/contrib/debug/common/debug'; +import { MemoryRegion } from 'vs/workbench/contrib/debug/common/debugModel'; +import { MockSession } from 'vs/workbench/contrib/debug/test/browser/mockDebug'; + +suite('Debug - Memory', () => { + const dapResponseCommon = { + command: 'someCommand', + type: 'response', + seq: 1, + request_seq: 1, + success: true, + }; + + suite('MemoryRegion', () => { + let memory: VSBuffer; + let unreadable: number; + let invalidateMemoryEmitter: Emitter; + let session: MockObject; + let region: TestMemoryRegion; + + class TestMemoryRegion extends MemoryRegion { + public assertNoOverlaps() { + for (const range of this.ranges) { + if (this.ranges.some(r => r !== range && r.toOffset > range.fromOffset && r.fromOffset < range.toOffset)) { + throw new Error(`Discovered overlapping ranges`); + } + } + } + } + + setup(() => { + const memoryBuf = new Uint8Array(1024); + for (let i = 0; i < memoryBuf.length; i++) { + memoryBuf[i] = i; // will be 0-255 + } + memory = VSBuffer.wrap(memoryBuf); + invalidateMemoryEmitter = new Emitter(); + unreadable = 0; + + session = mockObject()({ + onDidInvalidateMemory: invalidateMemoryEmitter.event + }); + + session.readMemory.callsFake((ref: string, fromOffset: number, count: number) => { + const res: DebugProtocol.ReadMemoryResponse = ({ + ...dapResponseCommon, + body: { + address: '0', + data: encodeBase64(memory.slice(fromOffset, fromOffset + Math.max(0, count - unreadable))), + unreadableBytes: unreadable + } + }); + + unreadable = 0; + + return Promise.resolve(res); + }); + + session.writeMemory.callsFake((ref: string, fromOffset: number, data: string): DebugProtocol.WriteMemoryResponse => { + const decoded = decodeBase64(data); + for (let i = 0; i < decoded.byteLength; i++) { + memory.buffer[fromOffset + i] = decoded.buffer[i]; + } + + return ({ + ...dapResponseCommon, + body: { + bytesWritten: decoded.byteLength, + offset: fromOffset, + } + }); + }); + + region = new TestMemoryRegion('ref', session as any); + }); + + teardown(() => { + region.assertNoOverlaps(); + region.dispose(); + }); + + test('reads a simple range', async () => { + assert.deepStrictEqual(await region.read(10, 14), [ + { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([10, 11, 12, 13])) } + ]); + }); + + test('reads an end-overlapping range', async () => { + await region.read(10, 14); + assert.deepStrictEqual(await region.read(12, 16), [ + { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([10, 11, 12, 13])) }, + { type: MemoryRangeType.Valid, offset: 14, length: 2, data: VSBuffer.wrap(new Uint8Array([14, 15])) }, + ]); + }); + + test('reads an start-overlapping range', async () => { + await region.read(10, 14); + assert.deepStrictEqual(await region.read(8, 12), [ + { type: MemoryRangeType.Valid, offset: 8, length: 2, data: VSBuffer.wrap(new Uint8Array([8, 9])) }, + { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([10, 11, 12, 13])) }, + ]); + }); + + test('reads an entirely-overlapping range', async () => { + await region.read(10, 14); + assert.deepStrictEqual(await region.read(8, 16), [ + { type: MemoryRangeType.Valid, offset: 8, length: 2, data: VSBuffer.wrap(new Uint8Array([8, 9])) }, + { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([10, 11, 12, 13])) }, + { type: MemoryRangeType.Valid, offset: 14, length: 2, data: VSBuffer.wrap(new Uint8Array([14, 15])) }, + ]); + }); + + test('reads an entirely-inset range', async () => { + await region.read(10, 14); + assert.deepStrictEqual(await region.read(11, 13), [ + { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([10, 11, 12, 13])) } + ]); + }); + + test('reads a non-contiguous range', async () => { + unreadable = 3; + assert.deepStrictEqual(await region.read(10, 14), [ + { type: MemoryRangeType.Valid, offset: 10, length: 1, data: VSBuffer.wrap(new Uint8Array([10])) }, + { type: MemoryRangeType.Unreadable, offset: 11, length: 3 }, + ]); + + assert.deepStrictEqual(await region.read(10, 16), [ + { type: MemoryRangeType.Valid, offset: 10, length: 1, data: VSBuffer.wrap(new Uint8Array([10])) }, + { type: MemoryRangeType.Unreadable, offset: 11, length: 3 }, + { type: MemoryRangeType.Valid, offset: 14, length: 2, data: VSBuffer.wrap(new Uint8Array([14, 15])) }, + ]); + }); + + test('writes memory when overlapping', async () => { + await region.read(10, 14); + await region.read(8, 10); + await region.read(15, 18); + + const readCalls = session.readMemory.callCount; + await region.write(12, VSBuffer.wrap(new Uint8Array([22, 23, 24, 25]))); + + assert.deepStrictEqual(await region.read(8, 18), [ + { type: MemoryRangeType.Valid, offset: 8, length: 2, data: VSBuffer.wrap(new Uint8Array([8, 9])) }, + { type: MemoryRangeType.Valid, offset: 10, length: 8, data: VSBuffer.wrap(new Uint8Array([10, 11, 22, 23, 24, 25, 16, 17])) }, + ]); + assert.strictEqual(session.readMemory.callCount, readCalls + 1); + }); + + test('writes memory when inset', async () => { + await region.read(10, 14); + await region.read(8, 10); + await region.read(14, 18); + + await region.write(12, VSBuffer.wrap(new Uint8Array([22]))); + + const readCalls = session.readMemory.callCount; + assert.deepStrictEqual(await region.read(8, 18), [ + { type: MemoryRangeType.Valid, offset: 8, length: 2, data: VSBuffer.wrap(new Uint8Array([8, 9])) }, + { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([10, 11, 22, 13])) }, + { type: MemoryRangeType.Valid, offset: 14, length: 4, data: VSBuffer.wrap(new Uint8Array([14, 15, 16, 17])) }, + ]); + assert.strictEqual(session.readMemory.callCount, readCalls + 1); + }); + + test('writes memory when exact', async () => { + await region.read(10, 14); + await region.read(8, 10); + await region.read(14, 18); + + await region.write(10, VSBuffer.wrap(new Uint8Array([20, 21, 22, 23]))); + + const readCalls = session.readMemory.callCount; + assert.deepStrictEqual(await region.read(8, 18), [ + { type: MemoryRangeType.Valid, offset: 8, length: 2, data: VSBuffer.wrap(new Uint8Array([8, 9])) }, + { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([20, 21, 22, 23])) }, + { type: MemoryRangeType.Valid, offset: 14, length: 4, data: VSBuffer.wrap(new Uint8Array([14, 15, 16, 17])) }, + ]); + assert.strictEqual(session.readMemory.callCount, readCalls + 1); + }); + }); +}); diff --git a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts index 60b8da2c38d48..af18ed46c5c30 100644 --- a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts @@ -3,21 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI as uri } from 'vs/base/common/uri'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; -import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { Position, IPosition } from 'vs/editor/common/core/position'; -import { ILaunch, IDebugService, State, IDebugSession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IDebugModel, IViewModel, IBreakpoint, LoadedSourceEvent, IThread, IRawModelUpdate, IFunctionBreakpoint, IExceptionBreakpoint, IDebugger, IExceptionInfo, AdapterEndEvent, IReplElement, IExpression, IReplElementSource, IDataBreakpoint, IDebugSessionOptions, IEvaluate, IAdapterManager, IRawStoppedDetails, IInstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; -import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import Severity from 'vs/base/common/severity'; +import { URI as uri } from 'vs/base/common/uri'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { ITextModel } from 'vs/editor/common/model'; +import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; -import { ExceptionBreakpoint, Expression, DataBreakpoint, FunctionBreakpoint, Breakpoint, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; +import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDebugger, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEvaluate, IExceptionBreakpoint, IExceptionInfo, IExpression, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IReplElementSource, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { Breakpoint, DataBreakpoint, DebugModel, ExceptionBreakpoint, Expression, FunctionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; +import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; -import { ITextModel } from 'vs/editor/common/model'; const fileService = new TestFileService(); export const mockUriIdentityService = new UriIdentityService(fileService); @@ -170,6 +170,22 @@ export class MockDebugService implements IDebugService { } export class MockSession implements IDebugSession { + getMemory(memoryReference: string): IMemoryRegion { + throw new Error('Method not implemented.'); + } + + get onDidInvalidateMemory(): Event { + throw new Error('Not implemented'); + } + + readMemory(memoryReference: string, offset: number, count: number): Promise { + throw new Error('Method not implemented.'); + } + + writeMemory(memoryReference: string, offset: number, data: string, allowPartial?: boolean): Promise { + throw new Error('Method not implemented.'); + } + get compoundRoot(): DebugCompoundRoot | undefined { return undefined; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 1400828370ce3..609d6eb83989e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -6,7 +6,7 @@ import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { IAction } from 'vs/base/common/actions'; import { coalesce } from 'vs/base/common/arrays'; -import { VSBuffer } from 'vs/base/common/buffer'; +import { decodeBase64 } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { getExtensionForMimeType } from 'vs/base/common/mime'; @@ -830,13 +830,7 @@ var requirejs = (function() { return; } - const decoded = atob(splitData); - const typedArray = new Uint8Array(decoded.length); - for (let i = 0; i < decoded.length; i++) { - typedArray[i] = decoded.charCodeAt(i); - } - - const buff = VSBuffer.wrap(typedArray); + const buff = decodeBase64(splitData); await this.fileService.writeFile(newFileUri, buff); await this.openerService.open(newFileUri); } diff --git a/src/vs/workbench/test/browser/api/extHostTesting.test.ts b/src/vs/workbench/test/browser/api/extHostTesting.test.ts index 7a5398a7eefb6..a5d8dc2bbf9e7 100644 --- a/src/vs/workbench/test/browser/api/extHostTesting.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTesting.test.ts @@ -462,11 +462,11 @@ suite('ExtHost Testing', () => { let dto: TestRunDto; setup(async () => { - proxy = mockObject(); + proxy = mockObject()(); cts = new CancellationTokenSource(); c = new TestRunCoordinator(proxy); - configuration = new TestRunProfileImpl(mockObject(), 'ctrlId', 42, 'Do Run', TestRunProfileKind.Run, () => { }, false); + configuration = new TestRunProfileImpl(mockObject()(), 'ctrlId', 42, 'Do Run', TestRunProfileKind.Run, () => { }, false); await single.expand(single.root.id, Infinity); single.collectDiff(); From 8538b4116e6ee4a6c65e3576529f4605d160b480 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 30 Dec 2021 14:10:15 -0800 Subject: [PATCH 2/6] debug: remove complex memory caching code for now --- .../contrib/debug/browser/debugMemory.ts | 3 +- .../contrib/debug/common/debugModel.ts | 159 +++++------------- .../debug/test/browser/debugMemory.test.ts | 100 +---------- 3 files changed, 49 insertions(+), 213 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugMemory.ts b/src/vs/workbench/contrib/debug/browser/debugMemory.ts index 421174cedd49b..b76e7d4d1bffa 100644 --- a/src/vs/workbench/contrib/debug/browser/debugMemory.ts +++ b/src/vs/workbench/contrib/debug/browser/debugMemory.ts @@ -152,12 +152,13 @@ export class DebugMemoryFileSystemProvider implements IFileSystemProvider { } else { throw new FileSystemProviderError(range.error, FileSystemProviderErrorCode.Unknown); } - case MemoryRangeType.Valid: + case MemoryRangeType.Valid: { const start = Math.max(0, pos - range.offset); const toWrite = range.data.slice(start, Math.min(range.data.byteLength, start + (length - readSoFar))); data.set(toWrite.buffer, offset + readSoFar); readSoFar += toWrite.byteLength; break; + } default: assertNever(range); } diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index d05eca56c148c..55ca8992510bd 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -3,30 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { URI, URI as uri } from 'vs/base/common/uri'; +import { distinct, lastIndex } from 'vs/base/common/arrays'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { mixin } from 'vs/base/common/objects'; import * as resources from 'vs/base/common/resources'; -import { Event, Emitter } from 'vs/base/common/event'; -import { generateUuid } from 'vs/base/common/uuid'; -import { DeferredPromise, RunOnceScheduler } from 'vs/base/common/async'; import { isString, isUndefinedOrNull } from 'vs/base/common/types'; -import { binarySearch, distinct, flatten, lastIndex } from 'vs/base/common/arrays'; -import { Range, IRange } from 'vs/editor/common/core/range'; -import { - ITreeElement, IExpression, IExpressionContainer, IDebugSession, IStackFrame, IExceptionBreakpoint, IBreakpoint, IFunctionBreakpoint, IDebugModel, - IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IBreakpointData, IExceptionInfo, IBreakpointsChangeEvent, IBreakpointUpdateData, IBaseBreakpoint, State, IDataBreakpoint, IInstructionBreakpoint, IMemoryRegion, IMemoryInvalidationEvent, MemoryRange, MemoryRangeType, DEBUG_MEMORY_SCHEME -} from 'vs/workbench/contrib/debug/common/debug'; -import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { URI, URI as uri } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import * as nls from 'vs/nls'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IEditorPane } from 'vs/workbench/common/editor'; -import { mixin } from 'vs/base/common/objects'; +import { DEBUG_MEMORY_SCHEME, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointsChangeEvent, IBreakpointUpdateData, IDataBreakpoint, IDebugModel, IDebugSession, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State } from 'vs/workbench/contrib/debug/common/debug'; +import { getUriFromSource, Source, UNKNOWN_SOURCE_LABEL } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; interface IDebugProtocolVariableWithContext extends DebugProtocol.Variable { __vscodeVariableMenuContext?: string; @@ -596,12 +593,6 @@ export class Thread implements IThread { } } -interface IMemoryRangeWrapper { - fromOffset: number; - toOffset: number; - value: DeferredPromise -} - /** * Gets a URI to a memory in the given session ID. */ @@ -620,7 +611,6 @@ export const getUriForDebugMemory = ( }; export class MemoryRegion extends Disposable implements IMemoryRegion { - protected readonly ranges: IMemoryRangeWrapper[] = []; private readonly invalidateEmitter = this._register(new Emitter()); /** @inheritdoc */ @@ -638,30 +628,40 @@ export class MemoryRegion extends Disposable implements IMemoryRegion { })); } - public read(fromOffset: number, toOffset: number): Promise { - // here, we make requests for all ranges within the offset bounds which - // we've not already requested. - let startIndex = this.getInsertIndex(fromOffset); + public async read(fromOffset: number, toOffset: number): Promise { + const length = toOffset - fromOffset; + const offset = fromOffset; + const result = await this.session.readMemory(this.memoryReference, offset, length); - let index = startIndex; - for (let lastEnd = fromOffset; lastEnd < toOffset;) { - const next = this.ranges[index]; - if (!next) { - this.ranges.push(this.makeRangeRequest(lastEnd, toOffset)); - index++; - break; - } + if (result === undefined || !result.body?.data) { + return [{ type: MemoryRangeType.Unreadable, offset, length }]; + } - if (next.fromOffset > lastEnd) { - this.ranges.splice(index, 0, this.makeRangeRequest(lastEnd, next.fromOffset)); - index++; - } + let data: VSBuffer; + try { + data = decodeBase64(result.body.data); + } catch { + return [{ type: MemoryRangeType.Error, offset, length, error: 'Invalid base64 data from debug adapter' }]; + } + + const unreadable = result.body.unreadableBytes || 0; + const dataLength = length - unreadable; + if (data.byteLength < dataLength) { + const pad = VSBuffer.alloc(dataLength - data.byteLength); + pad.buffer.fill(0); + data = VSBuffer.concat([data, pad], dataLength); + } else if (data.byteLength > dataLength) { + data = data.slice(0, dataLength); + } - lastEnd = next.toOffset; - index++; + if (!unreadable) { + return [{ type: MemoryRangeType.Valid, offset, length, data }]; } - return Promise.all(this.ranges.slice(startIndex, index).map(r => r.value.p)).then(flatten); + return [ + { type: MemoryRangeType.Valid, offset, length: dataLength, data }, + { type: MemoryRangeType.Unreadable, offset: offset + dataLength, length: unreadable }, + ]; } public async write(offset: number, data: VSBuffer): Promise { @@ -673,80 +673,11 @@ export class MemoryRegion extends Disposable implements IMemoryRegion { public override dispose() { super.dispose(); - this.ranges.forEach(r => { - if (!r.value.isSettled) { - r.value.cancel(); - } - }); } private invalidate(fromOffset: number, toOffset: number) { - // Here we want to remove any read ranges for invalidated data so they - // can be read again later. - - let startIndex = this.getInsertIndex(fromOffset); - const endIndex = this.getInsertIndex(toOffset) + 1; - - if (this.ranges[startIndex]?.toOffset === fromOffset) { - startIndex++; - } - - // no-op if there were no read ranges that got invalidated - if (endIndex - startIndex <= 0) { - return; - } - - this.ranges.splice(startIndex, endIndex - startIndex); this.invalidateEmitter.fire({ fromOffset, toOffset }); } - - private getInsertIndex(fromOffset: number) { - const searchIndex = binarySearch<{ toOffset: number }>(this.ranges, { toOffset: fromOffset }, (a, b) => a.toOffset - b.toOffset); - return searchIndex < 0 ? (~searchIndex) : searchIndex; - } - - private makeRangeRequest(fromOffset: number, toOffset: number): IMemoryRangeWrapper { - const length = toOffset - fromOffset; - const offset = fromOffset; - const promise = new DeferredPromise(); - - this.session.readMemory(this.memoryReference, fromOffset, toOffset - fromOffset).then( - (result): MemoryRange[] => { - if (result === undefined || !result.body?.data) { - return [{ type: MemoryRangeType.Unreadable, offset, length }]; - } - - let data: VSBuffer; - try { - data = decodeBase64(result.body.data); - } catch { - return [{ type: MemoryRangeType.Error, offset, length, error: 'Invalid base64 data from debug adapter' }]; - } - - const unreadable = result.body.unreadableBytes || 0; - const dataLength = length - unreadable; - if (data.byteLength < dataLength) { - const pad = VSBuffer.alloc(dataLength - data.byteLength); - pad.buffer.fill(0); - data = VSBuffer.concat([data, pad], dataLength); - } else if (data.byteLength > dataLength) { - data = data.slice(0, dataLength); - } - - if (!unreadable) { - return [{ type: MemoryRangeType.Valid, offset, length, data }]; - } - - return [ - { type: MemoryRangeType.Valid, offset, length: dataLength, data }, - { type: MemoryRangeType.Unreadable, offset: offset + dataLength, length: unreadable }, - ]; - }, - (error): MemoryRange[] => [{ type: MemoryRangeType.Error, offset, length, error: error.message }] - ).then(r => promise.complete(r)); - - return { fromOffset, toOffset, value: promise }; - } } export class Enablement implements IEnablement { diff --git a/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts index 00cb22c794ef5..7ee7788c6c673 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts @@ -25,17 +25,7 @@ suite('Debug - Memory', () => { let unreadable: number; let invalidateMemoryEmitter: Emitter; let session: MockObject; - let region: TestMemoryRegion; - - class TestMemoryRegion extends MemoryRegion { - public assertNoOverlaps() { - for (const range of this.ranges) { - if (this.ranges.some(r => r !== range && r.toOffset > range.fromOffset && r.fromOffset < range.toOffset)) { - throw new Error(`Discovered overlapping ranges`); - } - } - } - } + let region: MemoryRegion; setup(() => { const memoryBuf = new Uint8Array(1024); @@ -80,11 +70,10 @@ suite('Debug - Memory', () => { }); }); - region = new TestMemoryRegion('ref', session as any); + region = new MemoryRegion('ref', session as any); }); teardown(() => { - region.assertNoOverlaps(); region.dispose(); }); @@ -94,97 +83,12 @@ suite('Debug - Memory', () => { ]); }); - test('reads an end-overlapping range', async () => { - await region.read(10, 14); - assert.deepStrictEqual(await region.read(12, 16), [ - { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([10, 11, 12, 13])) }, - { type: MemoryRangeType.Valid, offset: 14, length: 2, data: VSBuffer.wrap(new Uint8Array([14, 15])) }, - ]); - }); - - test('reads an start-overlapping range', async () => { - await region.read(10, 14); - assert.deepStrictEqual(await region.read(8, 12), [ - { type: MemoryRangeType.Valid, offset: 8, length: 2, data: VSBuffer.wrap(new Uint8Array([8, 9])) }, - { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([10, 11, 12, 13])) }, - ]); - }); - - test('reads an entirely-overlapping range', async () => { - await region.read(10, 14); - assert.deepStrictEqual(await region.read(8, 16), [ - { type: MemoryRangeType.Valid, offset: 8, length: 2, data: VSBuffer.wrap(new Uint8Array([8, 9])) }, - { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([10, 11, 12, 13])) }, - { type: MemoryRangeType.Valid, offset: 14, length: 2, data: VSBuffer.wrap(new Uint8Array([14, 15])) }, - ]); - }); - - test('reads an entirely-inset range', async () => { - await region.read(10, 14); - assert.deepStrictEqual(await region.read(11, 13), [ - { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([10, 11, 12, 13])) } - ]); - }); - test('reads a non-contiguous range', async () => { unreadable = 3; assert.deepStrictEqual(await region.read(10, 14), [ { type: MemoryRangeType.Valid, offset: 10, length: 1, data: VSBuffer.wrap(new Uint8Array([10])) }, { type: MemoryRangeType.Unreadable, offset: 11, length: 3 }, ]); - - assert.deepStrictEqual(await region.read(10, 16), [ - { type: MemoryRangeType.Valid, offset: 10, length: 1, data: VSBuffer.wrap(new Uint8Array([10])) }, - { type: MemoryRangeType.Unreadable, offset: 11, length: 3 }, - { type: MemoryRangeType.Valid, offset: 14, length: 2, data: VSBuffer.wrap(new Uint8Array([14, 15])) }, - ]); - }); - - test('writes memory when overlapping', async () => { - await region.read(10, 14); - await region.read(8, 10); - await region.read(15, 18); - - const readCalls = session.readMemory.callCount; - await region.write(12, VSBuffer.wrap(new Uint8Array([22, 23, 24, 25]))); - - assert.deepStrictEqual(await region.read(8, 18), [ - { type: MemoryRangeType.Valid, offset: 8, length: 2, data: VSBuffer.wrap(new Uint8Array([8, 9])) }, - { type: MemoryRangeType.Valid, offset: 10, length: 8, data: VSBuffer.wrap(new Uint8Array([10, 11, 22, 23, 24, 25, 16, 17])) }, - ]); - assert.strictEqual(session.readMemory.callCount, readCalls + 1); - }); - - test('writes memory when inset', async () => { - await region.read(10, 14); - await region.read(8, 10); - await region.read(14, 18); - - await region.write(12, VSBuffer.wrap(new Uint8Array([22]))); - - const readCalls = session.readMemory.callCount; - assert.deepStrictEqual(await region.read(8, 18), [ - { type: MemoryRangeType.Valid, offset: 8, length: 2, data: VSBuffer.wrap(new Uint8Array([8, 9])) }, - { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([10, 11, 22, 13])) }, - { type: MemoryRangeType.Valid, offset: 14, length: 4, data: VSBuffer.wrap(new Uint8Array([14, 15, 16, 17])) }, - ]); - assert.strictEqual(session.readMemory.callCount, readCalls + 1); - }); - - test('writes memory when exact', async () => { - await region.read(10, 14); - await region.read(8, 10); - await region.read(14, 18); - - await region.write(10, VSBuffer.wrap(new Uint8Array([20, 21, 22, 23]))); - - const readCalls = session.readMemory.callCount; - assert.deepStrictEqual(await region.read(8, 18), [ - { type: MemoryRangeType.Valid, offset: 8, length: 2, data: VSBuffer.wrap(new Uint8Array([8, 9])) }, - { type: MemoryRangeType.Valid, offset: 10, length: 4, data: VSBuffer.wrap(new Uint8Array([20, 21, 22, 23])) }, - { type: MemoryRangeType.Valid, offset: 14, length: 4, data: VSBuffer.wrap(new Uint8Array([14, 15, 16, 17])) }, - ]); - assert.strictEqual(session.readMemory.callCount, readCalls + 1); }); }); }); From d5c15dd8425d0b52dc01151ded7b696787cdf491 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 30 Dec 2021 15:50:51 -0800 Subject: [PATCH 3/6] debug: open the hex editor when reading memory --- .../contrib/debug/browser/debugMemory.ts | 2 +- .../contrib/debug/browser/variablesView.ts | 25 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugMemory.ts b/src/vs/workbench/contrib/debug/browser/debugMemory.ts index b76e7d4d1bffa..b48f556a114ff 100644 --- a/src/vs/workbench/contrib/debug/browser/debugMemory.ts +++ b/src/vs/workbench/contrib/debug/browser/debugMemory.ts @@ -200,7 +200,7 @@ export class DebugMemoryFileSystemProvider implements IFileSystemProvider { offset, readOnly: !!session.capabilities.supportsWriteMemoryRequest, sessionId: uri.authority, - memoryReference, + memoryReference: decodeURIComponent(memoryReference), }; } } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 70bdcd58d1058..837a447ae9f10 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -32,11 +32,13 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { IMenuService, IMenu, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { localize } from 'vs/nls'; import { Codicon } from 'vs/base/common/codicons'; import { coalesce } from 'vs/base/common/arrays'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; const $ = dom.$; let forgetScopes = true; @@ -475,6 +477,10 @@ CommandsRegistry.registerCommand({ }); export const VIEW_MEMORY_ID = 'workbench.debug.viewlet.action.viewMemory'; + +const HEX_EDITOR_EXTENSION_ID = 'ms-vscode.hexeditor'; +const HEX_EDITOR_EDITOR_ID = 'hexEditor.hexedit'; + CommandsRegistry.registerCommand({ id: VIEW_MEMORY_ID, handler: async (accessor: ServicesAccessor, arg: IVariablesContext, ctx?: (Variable | Expression)[]) => { @@ -482,10 +488,19 @@ CommandsRegistry.registerCommand({ return; } - accessor.get(IOpenerService).open(getUriForDebugMemory( - arg.sessionId, - arg.variable.memoryReference, - )); + const commandService = accessor.get(ICommandService); + const editorService = accessor.get(IEditorService); + const ext = await accessor.get(IExtensionService).getExtension(HEX_EDITOR_EXTENSION_ID); + if (!ext) { + await commandService.executeCommand('workbench.extensions.search', `@id:${HEX_EDITOR_EXTENSION_ID}`); + } else { + await editorService.openEditor({ + resource: getUriForDebugMemory(arg.sessionId, arg.variable.memoryReference), + options: { + override: HEX_EDITOR_EDITOR_ID, + }, + }); + } } }); From a8501a073ca04d653f1b05f54d47764b055186b8 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 31 Dec 2021 11:09:08 -0800 Subject: [PATCH 4/6] debug: support writeMemory requests --- .../contrib/debug/browser/debugMemory.ts | 53 +++++++++++++++++-- .../contrib/debug/browser/rawDebugSession.ts | 4 +- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugMemory.ts b/src/vs/workbench/contrib/debug/browser/debugMemory.ts index b48f556a114ff..ea17e487be4b6 100644 --- a/src/vs/workbench/contrib/debug/browser/debugMemory.ts +++ b/src/vs/workbench/contrib/debug/browser/debugMemory.ts @@ -5,11 +5,12 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; -import { toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { clamp } from 'vs/base/common/numbers'; import { assertNever } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { FileChangeType, FileOpenOptions, FilePermission, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileChange, IFileSystemProvider, IStat, IWatchOptions } from 'vs/platform/files/common/files'; -import { DEBUG_MEMORY_SCHEME, IDebugService, IDebugSession, IMemoryRegion, MemoryRangeType } from 'vs/workbench/contrib/debug/common/debug'; +import { DEBUG_MEMORY_SCHEME, IDebugService, IDebugSession, IMemoryInvalidationEvent, IMemoryRegion, MemoryRange, MemoryRangeType } from 'vs/workbench/contrib/debug/common/debug'; const rangeRe = /range=([0-9]+):([0-9]+)/; @@ -86,9 +87,14 @@ export class DebugMemoryFileSystemProvider implements IFileSystemProvider { /** @inheritdoc */ public open(resource: URI, _opts: FileOpenOptions): Promise { - const { session, memoryReference } = this.parseUri(resource); + const { session, memoryReference, offset } = this.parseUri(resource); const fd = this.memoryFdCounter++; - this.fdMemory.set(fd, { session, region: session.getMemory(memoryReference) }); + let region = session.getMemory(memoryReference); + if (offset) { + region = new MemoryRegionView(region, offset); + } + + this.fdMemory.set(fd, { session, region }); return Promise.resolve(fd); } @@ -198,9 +204,46 @@ export class DebugMemoryFileSystemProvider implements IFileSystemProvider { return { session, offset, - readOnly: !!session.capabilities.supportsWriteMemoryRequest, + readOnly: !session.capabilities.supportsWriteMemoryRequest, sessionId: uri.authority, memoryReference: decodeURIComponent(memoryReference), }; } } + +/** A wrapper for a MemoryRegion that references a subset of data in another region. */ +class MemoryRegionView extends Disposable implements IMemoryRegion { + private readonly invalidateEmitter = new Emitter(); + + public readonly onDidInvalidate = this.invalidateEmitter.event; + public readonly writable: boolean; + private readonly width = this.range.toOffset - this.range.fromOffset; + + constructor(private readonly parent: IMemoryRegion, public readonly range: { fromOffset: number; toOffset: number }) { + super(); + this.writable = parent.writable; + + this._register(parent.onDidInvalidate(e => { + const fromOffset = clamp(e.fromOffset - range.fromOffset, 0, this.width); + const toOffset = clamp(e.toOffset - range.fromOffset, 0, this.width); + if (toOffset > fromOffset) { + this.invalidateEmitter.fire({ fromOffset, toOffset }); + } + })); + } + + public read(fromOffset: number, toOffset: number): Promise { + if (fromOffset < 0) { + throw new RangeError(`Invalid fromOffset: ${fromOffset}`); + } + + return this.parent.read( + this.range.fromOffset + fromOffset, + this.range.fromOffset + Math.min(toOffset, this.width), + ); + } + + public write(offset: number, data: VSBuffer): Promise { + return this.parent.write(this.range.fromOffset + offset, data); + } +} diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 2487937719abb..87fd439a7d2b6 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -526,7 +526,7 @@ export class RawDebugSession implements IDisposable { return await this.send('readMemory', args); } - return Promise.reject(new Error('disassemble is not supported')); + return Promise.reject(new Error('readMemory is not supported')); } async writeMemory(args: DebugProtocol.WriteMemoryArguments): Promise { @@ -534,7 +534,7 @@ export class RawDebugSession implements IDisposable { return await this.send('writeMemory', args); } - return Promise.reject(new Error('disassemble is not supported')); + return Promise.reject(new Error('writeMemory is not supported')); } cancel(args: DebugProtocol.CancelArguments): Promise { From 259c20e8d110af32925fb154c47c1d771ef1afa4 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 4 Jan 2022 14:29:31 -0800 Subject: [PATCH 5/6] debug: close memory view automatically when debug session ends --- .../contrib/debug/browser/variablesView.ts | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 837a447ae9f10..9eda83a19ba29 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -24,7 +24,7 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { dispose } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -488,19 +488,40 @@ CommandsRegistry.registerCommand({ return; } + const debugService = accessor.get(IDebugService); const commandService = accessor.get(ICommandService); const editorService = accessor.get(IEditorService); const ext = await accessor.get(IExtensionService).getExtension(HEX_EDITOR_EXTENSION_ID); if (!ext) { await commandService.executeCommand('workbench.extensions.search', `@id:${HEX_EDITOR_EXTENSION_ID}`); - } else { - await editorService.openEditor({ - resource: getUriForDebugMemory(arg.sessionId, arg.variable.memoryReference), - options: { - override: HEX_EDITOR_EDITOR_ID, - }, - }); + return; } + + const pane = await editorService.openEditor({ + resource: getUriForDebugMemory(arg.sessionId, arg.variable.memoryReference), + options: { + revealIfOpened: true, + override: HEX_EDITOR_EDITOR_ID, + }, + }); + + const editor = pane?.input; + if (!editor) { + return; + } + + const disposable = new DisposableStore(); + disposable.add(editor); + disposable.add(debugService.onDidEndSession(session => { + if (session.getId() === arg.sessionId) { + disposable.dispose(); + } + })); + disposable.add(editorService.onDidCloseEditor(e => { + if (e.editor === editor) { + disposable.dispose(); + } + })); } }); From 200a679b2308a5149e1f998be696f9a75a5cba66 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 4 Jan 2022 16:15:23 -0800 Subject: [PATCH 6/6] debug: support inline actions on variables, show data inspector inline --- .../contrib/debug/browser/baseDebugView.ts | 30 ++- .../debug/browser/debug.contribution.ts | 9 +- .../contrib/debug/browser/debugIcons.ts | 2 + .../debug/browser/media/debugViewlet.css | 17 +- .../contrib/debug/browser/variablesView.ts | 245 +++++++++--------- 5 files changed, 172 insertions(+), 131 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index 91e9625030dbc..48a01082eb1d8 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -9,7 +9,7 @@ import { Expression, Variable, ExpressionContainer } from 'vs/workbench/contrib/ import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInputValidationOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { ITreeRenderer, ITreeNode } from 'vs/base/browser/ui/tree/tree'; -import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -19,6 +19,7 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel'; import { once } from 'vs/base/common/functional'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; export const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; export const twistiePixels = 20; @@ -131,7 +132,8 @@ export interface IExpressionTemplateData { name: HTMLSpanElement; value: HTMLSpanElement; inputBoxContainer: HTMLElement; - toDispose: IDisposable; + actionBar?: ActionBar; + elementDisposable: IDisposable[]; label: HighlightedLabel; } @@ -153,20 +155,26 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer, index: number, data: IExpressionTemplateData): void { - data.toDispose.dispose(); - data.toDispose = Disposable.None; const { element } = node; this.renderExpression(element, data, createMatches(node.filterData)); + if (data.actionBar) { + this.renderActionBar!(data.actionBar, element, data); + } const selectedExpression = this.debugService.getViewModel().getSelectedExpression(); if (element === selectedExpression?.expression || (element instanceof Variable && element.errorMessage)) { const options = this.getInputBoxOptions(element, !!selectedExpression?.settingWatch); if (options) { - data.toDispose = this.renderInputBox(data.name, data.value, data.inputBoxContainer, options); - return; + data.elementDisposable.push(this.renderInputBox(data.name, data.value, data.inputBoxContainer, options)); } } } @@ -226,11 +234,15 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer, index: number, templateData: IExpressionTemplateData): void { - templateData.toDispose.dispose(); + dispose(templateData.elementDisposable); + templateData.elementDisposable = []; } disposeTemplate(templateData: IExpressionTemplateData): void { - templateData.toDispose.dispose(); + dispose(templateData.elementDisposable); + templateData.actionBar?.dispose(); } } diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 367a3633517f6..c779d9521595a 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/debug.contribution'; import 'vs/css!./media/debugHover'; import * as nls from 'vs/nls'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, Icon } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; @@ -118,14 +118,16 @@ registerDebugCommandPaletteItem(SELECT_AND_START_ID, SELECT_AND_START_LABEL, Con // Debug callstack context menu -const registerDebugViewMenuItem = (menuId: MenuId, id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation') => { +const registerDebugViewMenuItem = (menuId: MenuId, id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation', icon?: Icon) => { MenuRegistry.appendMenuItem(menuId, { group, when, order, + icon, command: { id, title, + icon, precondition } }); @@ -142,8 +144,9 @@ registerDebugViewMenuItem(MenuId.DebugCallStackContext, TERMINATE_THREAD_ID, nls registerDebugViewMenuItem(MenuId.DebugCallStackContext, RESTART_FRAME_ID, nls.localize('restartFrame', "Restart Frame"), 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'), CONTEXT_RESTART_FRAME_SUPPORTED), CONTEXT_STACK_FRAME_SUPPORTS_RESTART); registerDebugViewMenuItem(MenuId.DebugCallStackContext, COPY_STACK_TRACE_ID, nls.localize('copyStackTrace', "Copy Call Stack"), 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'), undefined, '3_modification'); +registerDebugViewMenuItem(MenuId.DebugVariablesContext, VIEW_MEMORY_ID, nls.localize('viewMemory', "View Memory"), 15, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_IN_DEBUG_MODE, 'inline', icons.debugInspectMemory); + registerDebugViewMenuItem(MenuId.DebugVariablesContext, SET_VARIABLE_ID, nls.localize('setValue', "Set Value"), 10, ContextKeyExpr.or(CONTEXT_SET_VARIABLE_SUPPORTED, ContextKeyExpr.and(CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, CONTEXT_SET_EXPRESSION_SUPPORTED)), CONTEXT_VARIABLE_IS_READONLY.toNegated(), '3_modification'); -registerDebugViewMenuItem(MenuId.DebugVariablesContext, VIEW_MEMORY_ID, nls.localize('viewMemory', "View Memory"), 15, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_IN_DEBUG_MODE, '3_modification'); registerDebugViewMenuItem(MenuId.DebugVariablesContext, COPY_VALUE_ID, nls.localize('copyValue', "Copy Value"), 10, undefined, undefined, '5_cutcopypaste'); registerDebugViewMenuItem(MenuId.DebugVariablesContext, COPY_EVALUATE_PATH_ID, nls.localize('copyAsExpression', "Copy as Expression"), 20, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, undefined, '5_cutcopypaste'); registerDebugViewMenuItem(MenuId.DebugVariablesContext, ADD_TO_WATCH_ID, nls.localize('addToWatchExpressions', "Add to Watch"), 100, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, undefined, 'z_commands'); diff --git a/src/vs/workbench/contrib/debug/browser/debugIcons.ts b/src/vs/workbench/contrib/debug/browser/debugIcons.ts index 50e3f86473393..ac91e22355ab4 100644 --- a/src/vs/workbench/contrib/debug/browser/debugIcons.ts +++ b/src/vs/workbench/contrib/debug/browser/debugIcons.ts @@ -83,3 +83,5 @@ export const breakpointsActivate = registerIcon('breakpoints-activate', Codicon. export const debugConsoleEvaluationInput = registerIcon('debug-console-evaluation-input', Codicon.arrowSmallRight, localize('debugConsoleEvaluationInput', 'Icon for the debug evaluation input marker.')); export const debugConsoleEvaluationPrompt = registerIcon('debug-console-evaluation-prompt', Codicon.chevronRight, localize('debugConsoleEvaluationPrompt', 'Icon for the debug evaluation prompt.')); + +export const debugInspectMemory = registerIcon('debug-inspect-memory', Codicon.fileBinary, localize('debugInspectMemory', 'Icon for the inspect memory action.')); diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 7cdd149a3d716..e32056677e8c2 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -216,9 +216,21 @@ font-size: 11px; } +.debug-pane .monaco-list-row .expression { + display: flex; +} + +.debug-pane .monaco-list-row .expression .actionbar-spacer { + flex-grow: 1; +} + +.debug-pane .monaco-list-row .expression .value { + overflow: hidden; + white-space: pre; + text-overflow: ellipsis; +} + .debug-pane .monaco-list-row .expression .value.changed { - padding: 2px; - margin: 4px; border-radius: 4px; } @@ -230,6 +242,7 @@ .debug-pane .inputBoxContainer { box-sizing: border-box; flex-grow: 1; + display: none; } .debug-pane .debug-watch .monaco-inputbox { diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 9eda83a19ba29..42f6f53c37b81 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -3,42 +3,43 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; -import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, IDataBreakpointInfoResponse, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, VARIABLES_VIEW_ID, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_CAN_VIEW_MEMORY } from 'vs/workbench/contrib/debug/common/debug'; -import { Variable, Scope, ErrorScope, StackFrame, Expression, getUriForDebugMemory } from 'vs/workbench/contrib/debug/common/debugModel'; -import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { renderViewTree, renderVariable, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; -import { IAction } from 'vs/base/common/actions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPane, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; -import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeRenderer, ITreeNode, ITreeMouseEvent, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; -import { FuzzyScore, createMatches } from 'vs/base/common/filters'; -import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IAsyncDataSource, ITreeContextMenuEvent, ITreeMouseEvent, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { IAction } from 'vs/base/common/actions'; +import { coalesce } from 'vs/base/common/arrays'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { Codicon } from 'vs/base/common/codicons'; +import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { withUndefinedAsNull } from 'vs/base/common/types'; -import { IMenuService, IMenu, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { localize } from 'vs/nls'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; -import { localize } from 'vs/nls'; -import { Codicon } from 'vs/base/common/codicons'; -import { coalesce } from 'vs/base/common/arrays'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderVariable, renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, CONTEXT_VARIABLES_FOCUSED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, CONTEXT_VARIABLE_IS_READONLY, IDataBreakpointInfoResponse, IDebugService, IExpression, IScope, IStackFrame, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { ErrorScope, Expression, getUriForDebugMemory, Scope, StackFrame, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; const $ = dom.$; let forgetScopes = true; @@ -59,14 +60,6 @@ export class VariablesView extends ViewPane { private tree!: WorkbenchAsyncDataTree; private savedViewState = new Map(); private autoExpandedScopes = new Set(); - private menu: IMenu; - private debugProtocolVariableMenuContext: IContextKey; - private breakWhenValueChangesSupported: IContextKey; - private breakWhenValueIsAccessedSupported: IContextKey; - private breakWhenValueIsReadSupported: IContextKey; - private variableEvaluateName: IContextKey; - private variableReadonly: IContextKey; - private viewMemorySupported: IContextKey; constructor( options: IViewletViewOptions, @@ -80,20 +73,10 @@ export class VariablesView extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, - @IMenuService menuService: IMenuService + @IMenuService private readonly menuService: IMenuService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this.menu = menuService.createMenu(MenuId.DebugVariablesContext, contextKeyService); - this._register(this.menu); - this.debugProtocolVariableMenuContext = CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT.bindTo(contextKeyService); - this.breakWhenValueChangesSupported = CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED.bindTo(contextKeyService); - this.breakWhenValueIsAccessedSupported = CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED.bindTo(contextKeyService); - this.breakWhenValueIsReadSupported = CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED.bindTo(contextKeyService); - this.variableEvaluateName = CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT.bindTo(contextKeyService); - this.viewMemorySupported = CONTEXT_CAN_VIEW_MEMORY.bindTo(contextKeyService); - this.variableReadonly = CONTEXT_VARIABLE_IS_READONLY.bindTo(contextKeyService); - // Use scheduler to prevent unnecessary flashing this.updateTreeScheduler = new RunOnceScheduler(async () => { const stackFrame = this.debugService.getViewModel().focusedStackFrame; @@ -213,57 +196,89 @@ export class VariablesView extends ViewPane { private async onContextMenu(e: ITreeContextMenuEvent): Promise { const variable = e.element; - if (variable instanceof Variable && !!variable.value) { - this.debugProtocolVariableMenuContext.set(variable.variableMenuContext || ''); - variableInternalContext = variable; - const session = this.debugService.getViewModel().focusedSession; - this.variableEvaluateName.set(!!variable.evaluateName); - this.viewMemorySupported.set(!!session?.capabilities.supportsReadMemoryRequest && variable.memoryReference !== undefined); - const attributes = variable.presentationHint?.attributes; - this.variableReadonly.set(!!attributes && attributes.indexOf('readOnly') >= 0); - this.breakWhenValueChangesSupported.reset(); - this.breakWhenValueIsAccessedSupported.reset(); - this.breakWhenValueIsReadSupported.reset(); - if (session && session.capabilities.supportsDataBreakpoints) { - dataBreakpointInfoResponse = await session.dataBreakpointInfo(variable.name, variable.parent.reference); - const dataBreakpointId = dataBreakpointInfoResponse?.dataId; - const dataBreakpointAccessTypes = dataBreakpointInfoResponse?.accessTypes; - if (!dataBreakpointAccessTypes) { - // Assumes default behaviour: Supports breakWhenValueChanges - this.breakWhenValueChangesSupported.set(!!dataBreakpointId); - } else { - dataBreakpointAccessTypes.forEach(accessType => { - switch (accessType) { - case 'read': - this.breakWhenValueIsReadSupported.set(!!dataBreakpointId); - break; - case 'write': - this.breakWhenValueChangesSupported.set(!!dataBreakpointId); - break; - case 'readWrite': - this.breakWhenValueIsAccessedSupported.set(!!dataBreakpointId); - break; - } - }); - } - } + if (!(variable instanceof Variable) || !variable.value) { + return; + } - const context: IVariablesContext = { - sessionId: variable.getSession()?.getId(), - container: (variable.parent as (Variable | Scope)).toDebugProtocolObject(), - variable: variable.toDebugProtocolObject() - }; - const actions: IAction[] = []; - const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: context, shouldForwardArgs: false }, actions); + const toDispose = new DisposableStore(); + + try { + const contextKeyService = toDispose.add(await getContextForVariableMenuWithDataAccess(this.contextKeyService, variable)); + const menu = toDispose.add(this.menuService.createMenu(MenuId.DebugVariablesContext, contextKeyService)); + + const context: IVariablesContext = getVariablesContext(variable); + const secondary: IAction[] = []; + const actionsDisposable = createAndFillInContextMenuActions(menu, { arg: context, shouldForwardArgs: false }, { primary: [], secondary }, 'inline'); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, - getActions: () => actions, + getActions: () => secondary, onHide: () => dispose(actionsDisposable) }); + } finally { + toDispose.dispose(); } } } +const getVariablesContext = (variable: Variable): IVariablesContext => ({ + sessionId: variable.getSession()?.getId(), + container: (variable.parent as (Variable | Scope)).toDebugProtocolObject(), + variable: variable.toDebugProtocolObject() +}); + +/** + * Gets a context key overlay that has context for the given variable, including data access info. + */ +async function getContextForVariableMenuWithDataAccess(parentContext: IContextKeyService, variable: Variable) { + const session = variable.getSession(); + if (!session || !session.capabilities.supportsDataBreakpoints) { + return getContextForVariableMenu(parentContext, variable); + } + + const contextKeys: [string, unknown][] = []; + dataBreakpointInfoResponse = await session.dataBreakpointInfo(variable.name, variable.parent.reference); + const dataBreakpointId = dataBreakpointInfoResponse?.dataId; + const dataBreakpointAccessTypes = dataBreakpointInfoResponse?.accessTypes; + + if (!dataBreakpointAccessTypes) { + contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED.key, !!dataBreakpointId]); + } else { + for (const accessType of dataBreakpointAccessTypes) { + switch (accessType) { + case 'read': + contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED.key, !!dataBreakpointId]); + break; + case 'write': + contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED.key, !!dataBreakpointId]); + break; + case 'readWrite': + contextKeys.push([CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED.key, !!dataBreakpointId]); + break; + } + } + } + + return getContextForVariableMenu(parentContext, variable, contextKeys); +} + +/** + * Gets a context key overlay that has context for the given variable. + */ +function getContextForVariableMenu(parentContext: IContextKeyService, variable: Variable, additionalContext: [string, unknown][] = []) { + const session = variable.getSession(); + const contextKeys: [string, unknown][] = [ + [CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT.key, variable.variableMenuContext || ''], + [CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT.key, !!variable.evaluateName], + [CONTEXT_CAN_VIEW_MEMORY.key, !!session?.capabilities.supportsReadMemoryRequest && variable.memoryReference !== undefined], + [CONTEXT_VARIABLE_IS_READONLY.key, !!variable.presentationHint?.attributes?.includes('readOnly')], + ...additionalContext, + ]; + + variableInternalContext = variable; + + return parentContext.createOverlay(contextKeys); +} + function isStackFrame(obj: any): obj is IStackFrame { return obj instanceof StackFrame; } @@ -371,6 +386,8 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { constructor( private readonly linkDetector: LinkDetector, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, @IDebugService debugService: IDebugService, @IContextViewService contextViewService: IContextViewService, @IThemeService themeService: IThemeService, @@ -409,6 +426,20 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { } }; } + + protected override renderActionBar(actionBar: ActionBar, expression: IExpression, data: IExpressionTemplateData) { + const variable = expression as Variable; + const contextKeyService = getContextForVariableMenu(this.contextKeyService, variable); + const menu = this.menuService.createMenu(MenuId.DebugVariablesContext, contextKeyService); + + const primary: IAction[] = []; + const context = getVariablesContext(variable); + data.elementDisposable.push(createAndFillInContextMenuActions(menu, { arg: context, shouldForwardArgs: false }, { primary, secondary: [] }, 'inline')); + + actionBar.clear(); + actionBar.context = context; + actionBar.push(primary, { icon: true, label: false }); + } } class VariablesAccessibilityProvider implements IListAccessibilityProvider { @@ -441,7 +472,7 @@ CommandsRegistry.registerCommand({ export const COPY_VALUE_ID = 'workbench.debug.viewlet.action.copyValue'; CommandsRegistry.registerCommand({ id: COPY_VALUE_ID, - handler: async (accessor: ServicesAccessor, arg: Variable | Expression | unknown, ctx?: (Variable | Expression)[]) => { + handler: async (accessor: ServicesAccessor, arg: Variable | Expression | IVariablesContext, ctx?: (Variable | Expression)[]) => { const debugService = accessor.get(IDebugService); const clipboardService = accessor.get(IClipboardService); let elementContext = ''; @@ -488,40 +519,20 @@ CommandsRegistry.registerCommand({ return; } - const debugService = accessor.get(IDebugService); const commandService = accessor.get(ICommandService); const editorService = accessor.get(IEditorService); const ext = await accessor.get(IExtensionService).getExtension(HEX_EDITOR_EXTENSION_ID); if (!ext) { await commandService.executeCommand('workbench.extensions.search', `@id:${HEX_EDITOR_EXTENSION_ID}`); - return; - } - - const pane = await editorService.openEditor({ - resource: getUriForDebugMemory(arg.sessionId, arg.variable.memoryReference), - options: { - revealIfOpened: true, - override: HEX_EDITOR_EDITOR_ID, - }, - }); - - const editor = pane?.input; - if (!editor) { - return; + } else { + await editorService.openEditor({ + resource: getUriForDebugMemory(arg.sessionId, arg.variable.memoryReference), + options: { + revealIfOpened: true, + override: HEX_EDITOR_EDITOR_ID, + }, + }); } - - const disposable = new DisposableStore(); - disposable.add(editor); - disposable.add(debugService.onDidEndSession(session => { - if (session.getId() === arg.sessionId) { - disposable.dispose(); - } - })); - disposable.add(editorService.onDidCloseEditor(e => { - if (e.editor === editor) { - disposable.dispose(); - } - })); } });