From fe52357c235576e6001aa640f2a099eb9bb199f2 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 22 Sep 2021 16:40:05 -0700 Subject: [PATCH] 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 f685828119371..3ba15e53f1088 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -236,3 +236,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 d9572ef3095e6..d240cf6db0c1b 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'; @@ -144,6 +144,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'); @@ -155,6 +156,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 59b30f000126d..76fc95aeae4c1 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, 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, 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 f5aa6f6dbc99b..7acd033350845 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 { IProductService } from 'vs/platform/product/common/productService'; +import { ICustomEndpointTelemetryService, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; +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 { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +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'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; 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 8fe9ecba59db2..e2b28743a7765 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 024bad8f81099..09b92023ddb7d 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 cab51887ee35f..9a2df38485362 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.") }); @@ -201,6 +203,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; @@ -216,6 +289,8 @@ export interface IDebugSession extends ITreeElement { setSubId(subId: string | undefined): void; + getMemory(memoryReference: string): IMemoryRegion; + setName(name: string): void; readonly onDidChangeName: Event; getLabel(): string; @@ -253,6 +328,7 @@ export interface IDebugSession extends ITreeElement { readonly onDidProgressStart: Event; readonly onDidProgressUpdate: Event; readonly onDidProgressEnd: Event; + readonly onDidInvalidateMemory: Event; // DAP request @@ -279,6 +355,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 2b98db67b13ac..cab2369b45c29 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/workbench/services/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 d631b6739e2d8..3c61e5e78a472 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -83,7 +83,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 = $('.'); @@ -111,7 +111,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 13f3370aafc85..39d14772ec545 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 { 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 { TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +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 { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService'; -import { ITextModel } from 'vs/editor/common/model'; +import { TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; 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 40b3a0f96db73..45e5c785a8c7e 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'; @@ -707,13 +707,7 @@ export class BackLayerWebView extends Disposable { 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 486c2537128a0..fa9b4c1b40afc 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();