Path: blob/main/src/vs/workbench/contrib/debug/browser/debugMemory.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { VSBuffer } from '../../../../base/common/buffer.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';8import { clamp } from '../../../../base/common/numbers.js';9import { assertNever } from '../../../../base/common/assert.js';10import { URI } from '../../../../base/common/uri.js';11import { FileChangeType, IFileOpenOptions, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileChange, IFileSystemProvider, IStat, IWatchOptions, createFileSystemProviderError } from '../../../../platform/files/common/files.js';12import { DEBUG_MEMORY_SCHEME, IDebugService, IDebugSession, IMemoryInvalidationEvent, IMemoryRegion, MemoryRange, MemoryRangeType, State } from '../common/debug.js';1314const rangeRe = /range=([0-9]+):([0-9]+)/;1516export class DebugMemoryFileSystemProvider extends Disposable implements IFileSystemProvider {17private memoryFdCounter = 0;18private readonly fdMemory = new Map<number, { session: IDebugSession; region: IMemoryRegion }>();19private readonly changeEmitter = new Emitter<readonly IFileChange[]>();2021/** @inheritdoc */22public readonly onDidChangeCapabilities = Event.None;2324/** @inheritdoc */25public readonly onDidChangeFile = this.changeEmitter.event;2627/** @inheritdoc */28public readonly capabilities = 029| FileSystemProviderCapabilities.PathCaseSensitive30| FileSystemProviderCapabilities.FileOpenReadWriteClose;3132constructor(private readonly debugService: IDebugService) {33super();3435this._register(debugService.onDidEndSession(({ session }) => {36for (const [fd, memory] of this.fdMemory) {37if (memory.session === session) {38this.close(fd);39}40}41}));42}4344public watch(resource: URI, opts: IWatchOptions) {45if (opts.recursive) {46return toDisposable(() => { });47}4849const { session, memoryReference, offset } = this.parseUri(resource);50const disposable = new DisposableStore();5152disposable.add(session.onDidChangeState(() => {53if (session.state === State.Running || session.state === State.Inactive) {54this.changeEmitter.fire([{ type: FileChangeType.DELETED, resource }]);55}56}));5758disposable.add(session.onDidInvalidateMemory(e => {59if (e.body.memoryReference !== memoryReference) {60return;61}6263if (offset && (e.body.offset >= offset.toOffset || e.body.offset + e.body.count < offset.fromOffset)) {64return;65}6667this.changeEmitter.fire([{ resource, type: FileChangeType.UPDATED }]);68}));6970return disposable;71}7273/** @inheritdoc */74public stat(file: URI): Promise<IStat> {75const { readOnly } = this.parseUri(file);76return Promise.resolve({77type: FileType.File,78mtime: 0,79ctime: 0,80size: 0,81permissions: readOnly ? FilePermission.Readonly : undefined,82});83}8485/** @inheritdoc */86public mkdir(): never {87throw createFileSystemProviderError(`Not allowed`, FileSystemProviderErrorCode.NoPermissions);88}8990/** @inheritdoc */91public readdir(): never {92throw createFileSystemProviderError(`Not allowed`, FileSystemProviderErrorCode.NoPermissions);93}9495/** @inheritdoc */96public delete(): never {97throw createFileSystemProviderError(`Not allowed`, FileSystemProviderErrorCode.NoPermissions);98}99100/** @inheritdoc */101public rename(): never {102throw createFileSystemProviderError(`Not allowed`, FileSystemProviderErrorCode.NoPermissions);103}104105/** @inheritdoc */106public open(resource: URI, _opts: IFileOpenOptions): Promise<number> {107const { session, memoryReference, offset } = this.parseUri(resource);108const fd = this.memoryFdCounter++;109let region = session.getMemory(memoryReference);110if (offset) {111region = new MemoryRegionView(region, offset);112}113114this.fdMemory.set(fd, { session, region });115return Promise.resolve(fd);116}117118/** @inheritdoc */119public close(fd: number) {120this.fdMemory.get(fd)?.region.dispose();121this.fdMemory.delete(fd);122return Promise.resolve();123}124125/** @inheritdoc */126public async writeFile(resource: URI, content: Uint8Array) {127const { offset } = this.parseUri(resource);128if (!offset) {129throw createFileSystemProviderError(`Range must be present to read a file`, FileSystemProviderErrorCode.FileNotFound);130}131132const fd = await this.open(resource, { create: false });133134try {135await this.write(fd, offset.fromOffset, content, 0, content.length);136} finally {137this.close(fd);138}139}140141/** @inheritdoc */142public async readFile(resource: URI) {143const { offset } = this.parseUri(resource);144if (!offset) {145throw createFileSystemProviderError(`Range must be present to read a file`, FileSystemProviderErrorCode.FileNotFound);146}147148const data = new Uint8Array(offset.toOffset - offset.fromOffset);149const fd = await this.open(resource, { create: false });150151try {152await this.read(fd, offset.fromOffset, data, 0, data.length);153return data;154} finally {155this.close(fd);156}157}158159/** @inheritdoc */160public async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {161const memory = this.fdMemory.get(fd);162if (!memory) {163throw createFileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable);164}165166const ranges = await memory.region.read(pos, length);167let readSoFar = 0;168for (const range of ranges) {169switch (range.type) {170case MemoryRangeType.Unreadable:171return readSoFar;172case MemoryRangeType.Error:173if (readSoFar > 0) {174return readSoFar;175} else {176throw createFileSystemProviderError(range.error, FileSystemProviderErrorCode.Unknown);177}178case MemoryRangeType.Valid: {179const start = Math.max(0, pos - range.offset);180const toWrite = range.data.slice(start, Math.min(range.data.byteLength, start + (length - readSoFar)));181data.set(toWrite.buffer, offset + readSoFar);182readSoFar += toWrite.byteLength;183break;184}185default:186assertNever(range);187}188}189190return readSoFar;191}192193/** @inheritdoc */194public write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {195const memory = this.fdMemory.get(fd);196if (!memory) {197throw createFileSystemProviderError(`No file with that descriptor open`, FileSystemProviderErrorCode.Unavailable);198}199200return memory.region.write(pos, VSBuffer.wrap(data).slice(offset, offset + length));201}202203protected parseUri(uri: URI) {204if (uri.scheme !== DEBUG_MEMORY_SCHEME) {205throw createFileSystemProviderError(`Cannot open file with scheme ${uri.scheme}`, FileSystemProviderErrorCode.FileNotFound);206}207208const session = this.debugService.getModel().getSession(uri.authority);209if (!session) {210throw createFileSystemProviderError(`Debug session not found`, FileSystemProviderErrorCode.FileNotFound);211}212213let offset: { fromOffset: number; toOffset: number } | undefined;214const rangeMatch = rangeRe.exec(uri.query);215if (rangeMatch) {216offset = { fromOffset: Number(rangeMatch[1]), toOffset: Number(rangeMatch[2]) };217}218219const [, memoryReference] = uri.path.split('/');220221return {222session,223offset,224readOnly: !session.capabilities.supportsWriteMemoryRequest,225sessionId: uri.authority,226memoryReference: decodeURIComponent(memoryReference),227};228}229}230231/** A wrapper for a MemoryRegion that references a subset of data in another region. */232class MemoryRegionView extends Disposable implements IMemoryRegion {233private readonly invalidateEmitter = new Emitter<IMemoryInvalidationEvent>();234235public readonly onDidInvalidate = this.invalidateEmitter.event;236public readonly writable: boolean;237private readonly width: number;238239constructor(private readonly parent: IMemoryRegion, public readonly range: { fromOffset: number; toOffset: number }) {240super();241this.writable = parent.writable;242this.width = range.toOffset - range.fromOffset;243244this._register(parent);245this._register(parent.onDidInvalidate(e => {246const fromOffset = clamp(e.fromOffset - range.fromOffset, 0, this.width);247const toOffset = clamp(e.toOffset - range.fromOffset, 0, this.width);248if (toOffset > fromOffset) {249this.invalidateEmitter.fire({ fromOffset, toOffset });250}251}));252}253254public read(fromOffset: number, toOffset: number): Promise<MemoryRange[]> {255if (fromOffset < 0) {256throw new RangeError(`Invalid fromOffset: ${fromOffset}`);257}258259return this.parent.read(260this.range.fromOffset + fromOffset,261this.range.fromOffset + Math.min(toOffset, this.width),262);263}264265public write(offset: number, data: VSBuffer): Promise<number> {266return this.parent.write(this.range.fromOffset + offset, data);267}268}269270271