/*--------------------------------------------------------------------------------------------- * 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 { CancellationToken } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { canceled } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { newWriteableStream, ReadableStreamEventPayload, ReadableStreamEvents } from 'vs/base/common/stream'; import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { createFileSystemProviderError, IFileAtomicReadOptions, FileChangeType, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileChange, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IStat, IWatchOptions, IFileSystemProviderError } from 'vs/platform/files/common/files'; export const LOCAL_FILE_SYSTEM_CHANNEL_NAME = 'localFilesystem'; /** * An implementation of a local disk file system provider * that is backed by a `IChannel` and thus implemented via * IPC on a different process. */ export class DiskFileSystemProviderClient extends Disposable implements IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability { constructor( private readonly channel: IChannel, private readonly extraCapabilities: { trash?: boolean; pathCaseSensitive?: boolean } ) { super(); this.registerFileChangeListeners(); } //#region File Capabilities readonly onDidChangeCapabilities: Event = Event.None; private _capabilities: FileSystemProviderCapabilities | undefined; get capabilities(): FileSystemProviderCapabilities { if (!this._capabilities) { this._capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileReadStream | FileSystemProviderCapabilities.FileFolderCopy | FileSystemProviderCapabilities.FileWriteUnlock | FileSystemProviderCapabilities.FileAtomicRead | FileSystemProviderCapabilities.FileAtomicWrite | FileSystemProviderCapabilities.FileAtomicDelete | FileSystemProviderCapabilities.FileClone; if (this.extraCapabilities.pathCaseSensitive) { this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive; } if (this.extraCapabilities.trash) { this._capabilities |= FileSystemProviderCapabilities.Trash; } } return this._capabilities; } //#endregion //#region File Metadata Resolving stat(resource: URI): Promise { return this.channel.call('stat', [resource]); } readdir(resource: URI): Promise<[string, FileType][]> { return this.channel.call('readdir', [resource]); } //#endregion //#region File Reading/Writing async readFile(resource: URI, opts?: IFileAtomicReadOptions): Promise { const { buffer } = await this.channel.call('readFile', [resource, opts]) as VSBuffer; return buffer; } readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents { const stream = newWriteableStream(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer); const disposables = new DisposableStore(); // Reading as file stream goes through an event to the remote side disposables.add(this.channel.listen>('readFileStream', [resource, opts])(dataOrErrorOrEnd => { // data if (dataOrErrorOrEnd instanceof VSBuffer) { stream.write(dataOrErrorOrEnd.buffer); } // end or error else { if (dataOrErrorOrEnd === 'end') { stream.end(); } else { let error: Error; // Take Error as is if type matches if (dataOrErrorOrEnd instanceof Error) { error = dataOrErrorOrEnd; } // Otherwise, try to deserialize into an error. // Since we communicate via IPC, we cannot be sure // that Error objects are properly serialized. else { const errorCandidate = dataOrErrorOrEnd as IFileSystemProviderError; error = createFileSystemProviderError(errorCandidate.message ?? toErrorMessage(errorCandidate), errorCandidate.code ?? FileSystemProviderErrorCode.Unknown); } stream.error(error); stream.end(); } // Signal to the remote side that we no longer listen disposables.dispose(); } })); // Support cancellation disposables.add(token.onCancellationRequested(() => { // Ensure to end the stream properly with an error // to indicate the cancellation. stream.error(canceled()); stream.end(); // Ensure to dispose the listener upon cancellation. This will // bubble through the remote side as event and allows to stop // reading the file. disposables.dispose(); })); return stream; } writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise { return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]); } open(resource: URI, opts: IFileOpenOptions): Promise { return this.channel.call('open', [resource, opts]); } close(fd: number): Promise { return this.channel.call('close', [fd]); } async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]); // copy back the data that was written into the buffer on the remote // side. we need to do this because buffers are not referenced by // pointer, but only by value and as such cannot be directly written // to from the other process. data.set(bytes.buffer.slice(0, bytesRead), offset); return bytesRead; } write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]); } //#endregion //#region Move/Copy/Delete/Create Folder mkdir(resource: URI): Promise { return this.channel.call('mkdir', [resource]); } delete(resource: URI, opts: IFileDeleteOptions): Promise { return this.channel.call('delete', [resource, opts]); } rename(resource: URI, target: URI, opts: IFileOverwriteOptions): Promise { return this.channel.call('rename', [resource, target, opts]); } copy(resource: URI, target: URI, opts: IFileOverwriteOptions): Promise { return this.channel.call('copy', [resource, target, opts]); } //#endregion //#region Clone File cloneFile(resource: URI, target: URI): Promise { return this.channel.call('cloneFile', [resource, target]); } //#endregion //#region File Watching private readonly _onDidChange = this._register(new Emitter()); readonly onDidChangeFile = this._onDidChange.event; private readonly _onDidWatchError = this._register(new Emitter()); readonly onDidWatchError = this._onDidWatchError.event; // The contract for file watching via remote is to identify us // via a unique but readonly session ID. Since the remote is // managing potentially many watchers from different clients, // this helps the server to properly partition events to the right // clients. private readonly sessionId = generateUuid(); private registerFileChangeListeners(): void { // The contract for file changes is that there is one listener // for both events and errors from the watcher. So we need to // unwrap the event from the remote and emit through the proper // emitter. this._register(this.channel.listen<{ resource: UriComponents; type: FileChangeType }[] | string>('fileChange', [this.sessionId])(eventsOrError => { if (Array.isArray(eventsOrError)) { const events = eventsOrError; this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type }))); } else { const error = eventsOrError; this._onDidWatchError.fire(error); } })); } watch(resource: URI, opts: IWatchOptions): IDisposable { // Generate a request UUID to correlate the watcher // back to us when we ask to dispose the watcher later. const req = generateUuid(); this.channel.call('watch', [this.sessionId, req, resource, opts]); return toDisposable(() => this.channel.call('unwatch', [this.sessionId, req])); } //#endregion }