Merge from vscode a348d103d1256a06a2c9b3f9b406298a9fef6898 (#15681)

* Merge from vscode a348d103d1256a06a2c9b3f9b406298a9fef6898

* Fixes and cleanup

* Distro

* Fix hygiene yarn

* delete no yarn lock changes file

* Fix hygiene

* Fix layer check

* Fix CI

* Skip lib checks

* Remove tests deleted in vs code

* Fix tests

* Distro

* Fix tests and add removed extension point

* Skip failing notebook tests for now

* Disable broken tests and cleanup build folder

* Update yarn.lock and fix smoke tests

* Bump sqlite

* fix contributed actions and file spacing

* Fix user data path

* Update yarn.locks

Co-authored-by: ADS Merger <karlb@microsoft.com>
This commit is contained in:
Charles Gagnon
2021-06-17 08:17:11 -07:00
committed by GitHub
parent fdcb97c7f7
commit 3cb2f552a6
2582 changed files with 124827 additions and 87099 deletions

View File

@@ -0,0 +1,210 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions } from 'vs/platform/files/common/files';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { extUri } from 'vs/base/common/resources';
function split(path: string): [string, string] | undefined {
const match = /^(.*)\/([^/]+)$/.exec(path);
if (!match) {
return undefined;
}
const [, parentPath, name] = match;
return [parentPath, name];
}
function getRootUUID(uri: URI): string | undefined {
const match = /^\/([^/]+)\/[^/]+\/?$/.exec(uri.path);
if (!match) {
return undefined;
}
return match[1];
}
export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability {
private readonly files = new Map<string, FileSystemFileHandle>();
private readonly directories = new Map<string, FileSystemDirectoryHandle>();
readonly capabilities: FileSystemProviderCapabilities =
FileSystemProviderCapabilities.FileReadWrite
| FileSystemProviderCapabilities.PathCaseSensitive;
readonly onDidChangeCapabilities = Event.None;
private readonly _onDidChangeFile = new Emitter<readonly IFileChange[]>();
readonly onDidChangeFile = this._onDidChangeFile.event;
private readonly _onDidErrorOccur = new Emitter<string>();
readonly onDidErrorOccur = this._onDidErrorOccur.event;
async readFile(resource: URI): Promise<Uint8Array> {
const handle = await this.getFileHandle(resource);
if (!handle) {
throw new Error('File not found.');
}
const file = await handle.getFile();
return new Uint8Array(await file.arrayBuffer());
}
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
const handle = await this.getFileHandle(resource);
if (!handle) {
throw new Error('File not found.');
}
const writable = await handle.createWritable();
await writable.write(content);
await writable.close();
}
watch(resource: URI, opts: IWatchOptions): IDisposable {
return Disposable.None;
}
async stat(resource: URI): Promise<IStat> {
const rootUUID = getRootUUID(resource);
if (rootUUID) {
const fileHandle = this.files.get(rootUUID);
if (fileHandle) {
const file = await fileHandle.getFile();
return {
type: FileType.File,
mtime: file.lastModified,
ctime: 0,
size: file.size
};
}
const directoryHandle = this.directories.get(rootUUID);
if (directoryHandle) {
return {
type: FileType.Directory,
mtime: 0,
ctime: 0,
size: 0
};
}
}
const parent = await this.getParentDirectoryHandle(resource);
if (!parent) {
throw new Error('Stat error: no parent found');
}
const name = extUri.basename(resource);
for await (const [childName, child] of parent) {
if (childName === name) {
if (child.kind === 'file') {
const file = await child.getFile();
return {
type: FileType.File,
mtime: file.lastModified,
ctime: 0,
size: file.size
};
} else {
return {
type: FileType.Directory,
mtime: 0,
ctime: 0,
size: 0
};
}
}
}
throw new Error('Stat error: entry not found');
}
mkdir(resource: URI): Promise<void> {
throw new Error('Method not implemented.');
}
async readdir(resource: URI): Promise<[string, FileType][]> {
const parent = await this.getDirectoryHandle(resource);
if (!parent) {
throw new Error('Stat error: no parent found');
}
const result: [string, FileType][] = [];
for await (const [name, child] of parent) {
result.push([name, child.kind === 'file' ? FileType.File : FileType.Directory]);
}
return result;
}
delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
throw new Error('Method not implemented: delete');
}
rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
throw new Error('Method not implemented: rename');
}
private async getDirectoryHandle(uri: URI): Promise<FileSystemDirectoryHandle | undefined> {
const rootUUID = getRootUUID(uri);
if (rootUUID) {
return this.directories.get(rootUUID);
}
const splitResult = split(uri.path);
if (!splitResult) {
return undefined;
}
const parent = await this.getDirectoryHandle(URI.from({ ...uri, path: splitResult[0] }));
return await parent?.getDirectoryHandle(extUri.basename(uri));
}
private async getParentDirectoryHandle(uri: URI): Promise<FileSystemDirectoryHandle | undefined> {
return this.getDirectoryHandle(URI.from({ ...uri, path: extUri.dirname(uri).path }));
}
private async getFileHandle(uri: URI): Promise<FileSystemFileHandle | undefined> {
const rootUUID = getRootUUID(uri);
if (rootUUID) {
return this.files.get(rootUUID);
}
const parent = await this.getParentDirectoryHandle(uri);
const name = extUri.basename(uri);
return await parent?.getFileHandle(name);
}
registerFileHandle(uuid: string, handle: FileSystemFileHandle): void {
this.files.set(uuid, handle);
}
registerDirectoryHandle(uuid: string, handle: FileSystemDirectoryHandle): void {
this.directories.set(uuid, handle);
}
dispose(): void {
this._onDidChangeFile.dispose();
}
}

View File

@@ -10,7 +10,6 @@ import { Event, Emitter } from 'vs/base/common/event';
import { VSBuffer } from 'vs/base/common/buffer';
import { Throttler } from 'vs/base/common/async';
import { localize } from 'vs/nls';
import * as browser from 'vs/base/browser/browser';
import { joinPath } from 'vs/base/common/resources';
const INDEXEDDB_VSCODE_DB = 'vscode-web-db';
@@ -48,9 +47,6 @@ export class IndexedDB {
}
private openIndexedDB(name: string, version: number, stores: string[]): Promise<IDBDatabase | null> {
if (browser.isEdgeLegacy) {
return Promise.resolve(null);
}
return new Promise((c, e) => {
const request = window.indexedDB.open(name, version);
request.onerror = (err) => e(request.error);

View File

@@ -6,16 +6,16 @@
import { localize } from 'vs/nls';
import { mark } from 'vs/base/common/performance';
import { Disposable, IDisposable, toDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files';
import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent, IReadFileStreamOptions, FileDeleteOptions } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { Emitter } from 'vs/base/common/event';
import { IExtUri, extUri, extUriIgnorePathCase, isAbsolutePath } from 'vs/base/common/resources';
import { TernarySearchTree } from 'vs/base/common/map';
import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays';
import { ILogService } from 'vs/platform/log/common/log';
import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable, streamToBuffer, VSBufferReadableStream, VSBufferReadableBufferedStream, bufferedStreamToBuffer, newWriteableBufferStream } from 'vs/base/common/buffer';
import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream, newWriteableStream, IReadableStreamObservable, observe } from 'vs/base/common/stream';
import { Promises, Queue } from 'vs/base/common/async';
import { isReadableStream, transform, peekReadable, peekStream, isReadableBufferedStream, newWriteableStream, listenStream, consumeStream } from 'vs/base/common/stream';
import { Promises, ResourceQueue } from 'vs/base/common/async';
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
import { Schemas } from 'vs/base/common/network';
import { readFileIntoStream } from 'vs/platform/files/common/io';
@@ -71,6 +71,10 @@ export class FileService extends Disposable implements IFileService {
});
}
getProvider(scheme: string): IFileSystemProvider | undefined {
return this.provider.get(scheme);
}
async activateProvider(scheme: string): Promise<void> {
// Emit an event that we are about to activate a provider with the given scheme.
@@ -79,9 +83,7 @@ export class FileService extends Disposable implements IFileService {
this._onWillActivateFileSystemProvider.fire({
scheme,
join(promise) {
if (promise) {
joiners.push(promise);
}
joiners.push(promise);
},
});
@@ -364,12 +366,12 @@ export class FileService extends Disposable implements IFileService {
// write file: unbuffered (only if data to write is a buffer, or the provider has no buffered write capability)
if (!hasOpenReadWriteCloseCapability(provider) || (hasReadWriteCapability(provider) && bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer)) {
await this.doWriteUnbuffered(provider, resource, bufferOrReadableOrStreamOrBufferedStream);
await this.doWriteUnbuffered(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream);
}
// write file: buffered
else {
await this.doWriteBuffered(provider, resource, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
await this.doWriteBuffered(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
}
} catch (error) {
throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
@@ -379,6 +381,14 @@ export class FileService extends Disposable implements IFileService {
}
private async validateWriteFile(provider: IFileSystemProvider, resource: URI, options?: IWriteFileOptions): Promise<IStat | undefined> {
// Validate unlock support
const unlock = !!options?.unlock;
if (unlock && !(provider.capabilities & FileSystemProviderCapabilities.FileWriteUnlock)) {
throw new Error(localize('writeFailedUnlockUnsupported', "Unable to unlock file '{0}' because provider does not support it.", this.resourceForError(resource)));
}
// Validate via file stat meta data
let stat: IStat | undefined = undefined;
try {
stat = await provider.stat(resource);
@@ -386,7 +396,7 @@ export class FileService extends Disposable implements IFileService {
return undefined; // file might not exist
}
// file cannot be directory
// File cannot be directory
if ((stat.type & FileType.Directory) !== 0) {
throw new FileOperationError(localize('fileIsDirectoryWriteError', "Unable to write file '{0}' that is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options);
}
@@ -417,7 +427,28 @@ export class FileService extends Disposable implements IFileService {
async readFile(resource: URI, options?: IReadFileOptions): Promise<IFileContent> {
const provider = await this.withReadProvider(resource);
const stream = await this.doReadAsFileStream(provider, resource, {
if (options?.atomic) {
return this.doReadFileAtomic(provider, resource, options);
}
return this.doReadFile(provider, resource, options);
}
private async doReadFileAtomic(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions): Promise<IFileContent> {
return new Promise<IFileContent>((resolve, reject) => {
this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(async () => {
try {
const content = await this.doReadFile(provider, resource, options);
resolve(content);
} catch (error) {
reject(error);
}
});
});
}
private async doReadFile(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions): Promise<IFileContent> {
const stream = await this.doReadFileStream(provider, resource, {
...options,
// optimization: since we know that the caller does not
// care about buffering, we indicate this to the reader.
@@ -433,13 +464,13 @@ export class FileService extends Disposable implements IFileService {
};
}
async readFileStream(resource: URI, options?: IReadFileOptions): Promise<IFileStreamContent> {
async readFileStream(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStreamContent> {
const provider = await this.withReadProvider(resource);
return this.doReadAsFileStream(provider, resource, options);
return this.doReadFileStream(provider, resource, options);
}
private async doReadAsFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileOptions & { preferUnbuffered?: boolean; }): Promise<IFileStreamContent> {
private async doReadFileStream(provider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability | IFileSystemProviderWithFileReadStreamCapability, resource: URI, options?: IReadFileStreamOptions & { preferUnbuffered?: boolean; }): Promise<IFileStreamContent> {
// install a cancellation token that gets cancelled
// when any error occurs. this allows us to resolve
@@ -454,20 +485,17 @@ export class FileService extends Disposable implements IFileService {
throw error;
});
let fileStreamObserver: IReadableStreamObservable | undefined = undefined;
let fileStream: VSBufferReadableStream | undefined = undefined;
try {
// if the etag is provided, we await the result of the validation
// due to the likelyhood of hitting a NOT_MODIFIED_SINCE result.
// due to the likelihood of hitting a NOT_MODIFIED_SINCE result.
// otherwise, we let it run in parallel to the file reading for
// optimal startup performance.
if (options && typeof options.etag === 'string' && options.etag !== ETAG_DISABLED) {
await statPromise;
}
let fileStream: VSBufferReadableStream | undefined = undefined;
// read unbuffered (only if either preferred, or the provider has no buffered read capability)
if (!(hasOpenReadWriteCloseCapability(provider) || hasFileReadStreamCapability(provider)) || (hasReadWriteCapability(provider) && options?.preferUnbuffered)) {
fileStream = this.readFileUnbuffered(provider, resource, options);
@@ -483,9 +511,6 @@ export class FileService extends Disposable implements IFileService {
fileStream = this.readFileBuffered(provider, resource, cancellableSource.token, options);
}
// observe the stream for the error case below
fileStreamObserver = observe(fileStream);
const fileStat = await statPromise;
return {
@@ -497,15 +522,15 @@ export class FileService extends Disposable implements IFileService {
// Await the stream to finish so that we exit this method
// in a consistent state with file handles closed
// (https://github.com/microsoft/vscode/issues/114024)
if (fileStreamObserver) {
await fileStreamObserver.errorOrEnd();
if (fileStream) {
await consumeStream(fileStream);
}
throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options);
}
}
private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileOptions = Object.create(null)): VSBufferReadableStream {
private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
const fileStream = provider.readFileStream(resource, options, token);
return transform(fileStream, {
@@ -514,7 +539,7 @@ export class FileService extends Disposable implements IFileService {
}, data => VSBuffer.concat(data));
}
private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileOptions = Object.create(null)): VSBufferReadableStream {
private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileStreamOptions = Object.create(null)): VSBufferReadableStream {
const stream = newWriteableBufferStream();
readFileIntoStream(provider, resource, stream, data => data, {
@@ -526,7 +551,7 @@ export class FileService extends Disposable implements IFileService {
return stream;
}
private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileOptions): VSBufferReadableStream {
private readFileUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options?: IReadFileStreamOptions): VSBufferReadableStream {
const stream = newWriteableStream<VSBuffer>(data => VSBuffer.concat(data));
// Read the file into the stream async but do not wait for
@@ -552,13 +577,14 @@ export class FileService extends Disposable implements IFileService {
stream.end(VSBuffer.wrap(buffer));
} catch (err) {
stream.error(err);
stream.end();
}
})();
return stream;
}
private async validateReadFile(resource: URI, options?: IReadFileOptions): Promise<IFileStatWithMetadata> {
private async validateReadFile(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStatWithMetadata> {
const stat = await this.resolve(resource, { resolveMetadata: true });
// Throw if resource is a directory
@@ -577,7 +603,7 @@ export class FileService extends Disposable implements IFileService {
return stat;
}
private validateReadFileLimits(resource: URI, size: number, options?: IReadFileOptions): void {
private validateReadFileLimits(resource: URI, size: number, options?: IReadFileStreamOptions): void {
if (options?.limits) {
let tooLargeErrorResult: FileOperationResult | undefined = undefined;
@@ -866,7 +892,7 @@ export class FileService extends Disposable implements IFileService {
}
}
async canDelete(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<Error | true> {
async canDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<Error | true> {
try {
await this.doValidateDelete(resource, options);
} catch (error) {
@@ -876,7 +902,7 @@ export class FileService extends Disposable implements IFileService {
return true;
}
private async doValidateDelete(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<IFileSystemProvider> {
private async doValidateDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<IFileSystemProvider> {
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);
// Validate trash support
@@ -903,7 +929,7 @@ export class FileService extends Disposable implements IFileService {
return provider;
}
async del(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise<void> {
async del(resource: URI, options?: Partial<FileDeleteOptions>): Promise<void> {
const provider = await this.doValidateDelete(resource, options);
const useTrash = !!options?.useTrash;
@@ -978,7 +1004,7 @@ export class FileService extends Disposable implements IFileService {
].join();
}
dispose(): void {
override dispose(): void {
super.dispose();
this.activeWatchers.forEach(watcher => dispose(watcher.disposable));
@@ -989,35 +1015,13 @@ export class FileService extends Disposable implements IFileService {
//#region Helpers
private readonly writeQueues: Map<string, Queue<void>> = new Map();
private readonly writeQueue = this._register(new ResourceQueue());
private ensureWriteQueue(provider: IFileSystemProvider, resource: URI): Queue<void> {
const { providerExtUri } = this.getExtUri(provider);
const queueKey = providerExtUri.getComparisonKey(resource);
// ensure to never write to the same resource without finishing
// the one write. this ensures a write finishes consistently
// (even with error) before another write is done.
let writeQueue = this.writeQueues.get(queueKey);
if (!writeQueue) {
writeQueue = new Queue<void>();
this.writeQueues.set(queueKey, writeQueue);
const onFinish = Event.once(writeQueue.onFinished);
onFinish(() => {
this.writeQueues.delete(queueKey);
dispose(writeQueue);
});
}
return writeQueue;
}
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.ensureWriteQueue(provider, resource).queue(async () => {
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(async () => {
// open handle
const handle = await provider.open(resource, { create: true });
const handle = await provider.open(resource, { create: true, unlock: options?.unlock ?? false });
// write into handle until all bytes from buffer have been written
try {
@@ -1065,28 +1069,29 @@ export class FileService extends Disposable implements IFileService {
return new Promise(async (resolve, reject) => {
stream.on('data', async chunk => {
listenStream(stream, {
onData: async chunk => {
// pause stream to perform async write operation
stream.pause();
// pause stream to perform async write operation
stream.pause();
try {
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
} catch (error) {
return reject(error);
}
try {
await this.doWriteBuffer(provider, handle, chunk, chunk.byteLength, posInFile, 0);
} catch (error) {
return reject(error);
}
posInFile += chunk.byteLength;
posInFile += chunk.byteLength;
// resume stream now that we have successfully written
// run this on the next tick to prevent increasing the
// execution stack because resume() may call the event
// handler again before finishing.
setTimeout(() => stream.resume());
// resume stream now that we have successfully written
// run this on the next tick to prevent increasing the
// execution stack because resume() may call the event
// handler again before finishing.
setTimeout(() => stream.resume());
},
onError: error => reject(error),
onEnd: () => resolve()
});
stream.on('error', error => reject(error));
stream.on('end', () => resolve());
});
}
@@ -1111,11 +1116,11 @@ export class FileService extends Disposable implements IFileService {
}
}
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.ensureWriteQueue(provider, resource).queue(() => this.doWriteUnbufferedQueued(provider, resource, bufferOrReadableOrStreamOrBufferedStream));
private async doWriteUnbuffered(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(() => this.doWriteUnbufferedQueued(provider, resource, options, bufferOrReadableOrStreamOrBufferedStream));
}
private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
private async doWriteUnbufferedQueued(provider: IFileSystemProviderWithFileReadWriteCapability, resource: URI, options: IWriteFileOptions | undefined, bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
let buffer: VSBuffer;
if (bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) {
buffer = bufferOrReadableOrStreamOrBufferedStream;
@@ -1128,11 +1133,11 @@ export class FileService extends Disposable implements IFileService {
}
// Write through the provider
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true });
await provider.writeFile(resource, buffer.buffer, { create: true, overwrite: true, unlock: options?.unlock ?? false });
}
private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target));
return this.writeQueue.queueFor(target, this.getExtUri(targetProvider).providerExtUri).queue(() => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target));
}
private async doPipeBufferedQueued(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
@@ -1143,7 +1148,7 @@ export class FileService extends Disposable implements IFileService {
// Open handles
sourceHandle = await sourceProvider.open(source, { create: false });
targetHandle = await targetProvider.open(target, { create: true });
targetHandle = await targetProvider.open(target, { create: true, unlock: false });
const buffer = VSBuffer.alloc(this.BUFFER_SIZE);
@@ -1178,21 +1183,21 @@ export class FileService extends Disposable implements IFileService {
}
private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target));
return this.writeQueue.queueFor(target, this.getExtUri(targetProvider).providerExtUri).queue(() => this.doPipeUnbufferedQueued(sourceProvider, source, targetProvider, target));
}
private async doPipeUnbufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI): Promise<void> {
return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true });
return targetProvider.writeFile(target, await sourceProvider.readFile(source), { create: true, overwrite: true, unlock: false });
}
private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target));
return this.writeQueue.queueFor(target, this.getExtUri(targetProvider).providerExtUri).queue(() => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target));
}
private async doPipeUnbufferedToBufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
// Open handle
const targetHandle = await targetProvider.open(target, { create: true });
const targetHandle = await targetProvider.open(target, { create: true, unlock: false });
// Read entire buffer from source and write buffered
try {
@@ -1211,7 +1216,7 @@ export class FileService extends Disposable implements IFileService {
const buffer = await streamToBuffer(this.readFileBuffered(sourceProvider, source, CancellationToken.None));
// Write buffer into target at once
await this.doWriteUnbuffered(targetProvider, target, buffer);
await this.doWriteUnbuffered(targetProvider, target, undefined, buffer);
}
protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(provider: T, resource: URI): T {

View File

@@ -6,7 +6,7 @@
import { localize } from 'vs/nls';
import { sep } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import * as glob from 'vs/base/common/glob';
import { IExpression } from 'vs/base/common/glob';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { startsWithIgnoreCase } from 'vs/base/common/strings';
@@ -17,6 +17,8 @@ import { ReadableStreamEvents } from 'vs/base/common/stream';
import { CancellationToken } from 'vs/base/common/cancellation';
import { TernarySearchTree } from 'vs/base/common/map';
//#region file service & providers
export const IFileService = createDecorator<IFileService>('fileService');
export interface IFileService {
@@ -44,6 +46,11 @@ export interface IFileService {
*/
registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable;
/**
* Returns a file system provider for a certain scheme.
*/
getProvider(scheme: string): IFileSystemProvider | undefined;
/**
* Tries to activate a provider with the given scheme.
*/
@@ -112,7 +119,7 @@ export interface IFileService {
/**
* Read the contents of the provided resource buffered as stream.
*/
readFileStream(resource: URI, options?: IReadFileOptions): Promise<IFileStreamContent>;
readFileStream(resource: URI, options?: IReadFileStreamOptions): Promise<IFileStreamContent>;
/**
* Updates the content replacing its previous value.
@@ -170,13 +177,13 @@ export interface IFileService {
* move the file to trash. The optional recursive parameter allows to delete
* non-empty folders recursively.
*/
del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void>;
del(resource: URI, options?: Partial<FileDeleteOptions>): Promise<void>;
/**
* Find out if a delete operation is possible given the arguments. No changes on disk will
* be performed. Returns an Error if the operation cannot be done.
*/
canDelete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<Error | true>;
canDelete(resource: URI, options?: Partial<FileDeleteOptions>): Promise<Error | true>;
/**
* Allows to start a watcher that reports file/folder change events on the provided resource.
@@ -192,7 +199,22 @@ export interface IFileService {
}
export interface FileOverwriteOptions {
overwrite: boolean;
/**
* Set to `true` to overwrite a file if it exists. Will
* throw an error otherwise if the file does exist.
*/
readonly overwrite: boolean;
}
export interface FileUnlockOptions {
/**
* Set to `true` to try to remove any write locks the file might
* have. A file that is write locked will throw an error for any
* attempt to write to unless `unlock: true` is provided.
*/
readonly unlock: boolean;
}
export interface FileReadStreamOptions {
@@ -218,59 +240,159 @@ export interface FileReadStreamOptions {
};
}
export interface FileWriteOptions {
overwrite: boolean;
create: boolean;
export interface FileWriteOptions extends FileOverwriteOptions, FileUnlockOptions {
/**
* Set to `true` to create a file when it does not exist. Will
* throw an error otherwise if the file does not exist.
*/
readonly create: boolean;
}
export interface FileOpenOptions {
create: boolean;
export type FileOpenOptions = FileOpenForReadOptions | FileOpenForWriteOptions;
export function isFileOpenForWriteOptions(options: FileOpenOptions): options is FileOpenForWriteOptions {
return options.create === true;
}
export interface FileOpenForReadOptions {
/**
* A hint that the file should be opened for reading only.
*/
readonly create: false;
}
export interface FileOpenForWriteOptions extends FileUnlockOptions {
/**
* A hint that the file should be opened for reading and writing.
*/
readonly create: true;
}
export interface FileDeleteOptions {
recursive: boolean;
useTrash: boolean;
/**
* Set to `true` to recursively delete any children of the file. This
* only applies to folders and can lead to an error unless provided
* if the folder is not empty.
*/
readonly recursive: boolean;
/**
* Set to `true` to attempt to move the file to trash
* instead of deleting it permanently from disk. This
* option maybe not be supported on all providers.
*/
readonly useTrash: boolean;
}
export enum FileType {
/**
* File is unknown (neither file, directory nor symbolic link).
*/
Unknown = 0,
/**
* File is a normal file.
*/
File = 1,
/**
* File is a directory.
*/
Directory = 2,
/**
* File is a symbolic link.
*
* Note: even when the file is a symbolic link, you can test for
* `FileType.File` and `FileType.Directory` to know the type of
* the target the link points to.
*/
SymbolicLink = 64
}
export interface IStat {
type: FileType;
/**
* The file type.
*/
readonly type: FileType;
/**
* The last modification date represented as millis from unix epoch.
*/
mtime: number;
readonly mtime: number;
/**
* The creation date represented as millis from unix epoch.
*/
ctime: number;
readonly ctime: number;
/**
* The size of the file in bytes.
*/
size: number;
}
export interface IWatchOptions {
recursive: boolean;
/**
* Set to `true` to watch for changes recursively in a folder
* and all of its children.
*/
readonly recursive: boolean;
/**
* A set of paths to exclude from watching.
*/
excludes: string[];
}
export const enum FileSystemProviderCapabilities {
/**
* Provider supports unbuffered read/write.
*/
FileReadWrite = 1 << 1,
/**
* Provider supports open/read/write/close low level file operations.
*/
FileOpenReadWriteClose = 1 << 2,
/**
* Provider supports stream based reading.
*/
FileReadStream = 1 << 4,
/**
* Provider supports copy operation.
*/
FileFolderCopy = 1 << 3,
/**
* Provider is path case sensitive.
*/
PathCaseSensitive = 1 << 10,
/**
* All files of the provider are readonly.
*/
Readonly = 1 << 11,
Trash = 1 << 12
/**
* Provider supports to delete via trash.
*/
Trash = 1 << 12,
/**
* Provider support to unlock files for writing.
*/
FileWriteUnlock = 1 << 13
}
export interface IFileSystemProvider {
@@ -345,6 +467,7 @@ export enum FileSystemProviderErrorCode {
FileIsADirectory = 'EntryIsADirectory',
FileExceedsMemoryLimit = 'EntryExceedsMemoryLimit',
FileTooLarge = 'EntryTooLarge',
FileWriteLocked = 'EntryWriteLocked',
NoPermissions = 'NoPermissions',
Unavailable = 'Unavailable',
Unknown = 'Unknown'
@@ -404,6 +527,7 @@ export function toFileSystemProviderErrorCode(error: Error | undefined | null):
case FileSystemProviderErrorCode.FileNotFound: return FileSystemProviderErrorCode.FileNotFound;
case FileSystemProviderErrorCode.FileExceedsMemoryLimit: return FileSystemProviderErrorCode.FileExceedsMemoryLimit;
case FileSystemProviderErrorCode.FileTooLarge: return FileSystemProviderErrorCode.FileTooLarge;
case FileSystemProviderErrorCode.FileWriteLocked: return FileSystemProviderErrorCode.FileWriteLocked;
case FileSystemProviderErrorCode.NoPermissions: return FileSystemProviderErrorCode.NoPermissions;
case FileSystemProviderErrorCode.Unavailable: return FileSystemProviderErrorCode.Unavailable;
}
@@ -426,6 +550,8 @@ export function toFileOperationResult(error: Error): FileOperationResult {
return FileOperationResult.FILE_IS_DIRECTORY;
case FileSystemProviderErrorCode.FileNotADirectory:
return FileOperationResult.FILE_NOT_DIRECTORY;
case FileSystemProviderErrorCode.FileWriteLocked:
return FileOperationResult.FILE_WRITE_LOCKED;
case FileSystemProviderErrorCode.NoPermissions:
return FileOperationResult.FILE_PERMISSION_DENIED;
case FileSystemProviderErrorCode.FileExists:
@@ -440,18 +566,18 @@ export function toFileOperationResult(error: Error): FileOperationResult {
}
export interface IFileSystemProviderRegistrationEvent {
added: boolean;
scheme: string;
provider?: IFileSystemProvider;
readonly added: boolean;
readonly scheme: string;
readonly provider?: IFileSystemProvider;
}
export interface IFileSystemProviderCapabilitiesChangeEvent {
provider: IFileSystemProvider;
scheme: string;
readonly provider: IFileSystemProvider;
readonly scheme: string;
}
export interface IFileSystemProviderActivationEvent {
scheme: string;
readonly scheme: string;
join(promise: Promise<void>): void;
}
@@ -479,9 +605,9 @@ export class FileOperationEvent {
* Possible changes that can occur to a file.
*/
export const enum FileChangeType {
UPDATED = 0,
ADDED = 1,
DELETED = 2
UPDATED,
ADDED,
DELETED
}
/**
@@ -702,13 +828,13 @@ interface IBaseStat {
/**
* The unified resource identifier of this file or folder.
*/
resource: URI;
readonly resource: URI;
/**
* The name which is the last segment
* of the {{path}}.
*/
name: string;
readonly name: string;
/**
* The size of the file.
@@ -716,7 +842,7 @@ interface IBaseStat {
* The value may or may not be resolved as
* it is optional.
*/
size?: number;
readonly size?: number;
/**
* The last modification date represented as millis from unix epoch.
@@ -724,7 +850,7 @@ interface IBaseStat {
* The value may or may not be resolved as
* it is optional.
*/
mtime?: number;
readonly mtime?: number;
/**
* The creation date represented as millis from unix epoch.
@@ -732,7 +858,7 @@ interface IBaseStat {
* The value may or may not be resolved as
* it is optional.
*/
ctime?: number;
readonly ctime?: number;
/**
* A unique identifier thet represents the
@@ -741,15 +867,10 @@ interface IBaseStat {
* The value may or may not be resolved as
* it is optional.
*/
etag?: string;
readonly etag?: string;
}
export interface IBaseStatWithMetadata extends IBaseStat {
mtime: number;
ctime: number;
etag: string;
size: number;
}
export interface IBaseStatWithMetadata extends Required<IBaseStat> { }
/**
* A file resource with meta information.
@@ -759,17 +880,20 @@ export interface IFileStat extends IBaseStat {
/**
* The resource is a file.
*/
isFile: boolean;
readonly isFile: boolean;
/**
* The resource is a directory.
*/
isDirectory: boolean;
readonly isDirectory: boolean;
/**
* The resource is a symbolic link.
* The resource is a symbolic link. Note: even when the
* file is a symbolic link, you can test for `FileType.File`
* and `FileType.Directory` to know the type of the target
* the link points to.
*/
isSymbolicLink: boolean;
readonly isSymbolicLink: boolean;
/**
* The children of the file stat or undefined if none.
@@ -778,20 +902,20 @@ export interface IFileStat extends IBaseStat {
}
export interface IFileStatWithMetadata extends IFileStat, IBaseStatWithMetadata {
mtime: number;
ctime: number;
etag: string;
size: number;
children?: IFileStatWithMetadata[];
readonly mtime: number;
readonly ctime: number;
readonly etag: string;
readonly size: number;
readonly children?: IFileStatWithMetadata[];
}
export interface IResolveFileResult {
stat?: IFileStat;
success: boolean;
readonly stat?: IFileStat;
readonly success: boolean;
}
export interface IResolveFileResultWithMetadata extends IResolveFileResult {
stat?: IFileStatWithMetadata;
readonly stat?: IFileStatWithMetadata;
}
export interface IFileContent extends IBaseStatWithMetadata {
@@ -799,7 +923,7 @@ export interface IFileContent extends IBaseStatWithMetadata {
/**
* The content of a file as buffer.
*/
value: VSBuffer;
readonly value: VSBuffer;
}
export interface IFileStreamContent extends IBaseStatWithMetadata {
@@ -807,10 +931,10 @@ export interface IFileStreamContent extends IBaseStatWithMetadata {
/**
* The content of a file as stream.
*/
value: VSBufferReadableStream;
readonly value: VSBufferReadableStream;
}
export interface IReadFileOptions extends FileReadStreamOptions {
export interface IBaseReadFileOptions extends FileReadStreamOptions {
/**
* The optional etag parameter allows to return early from resolving the resource if
@@ -821,6 +945,28 @@ export interface IReadFileOptions extends FileReadStreamOptions {
readonly etag?: string;
}
export interface IReadFileStreamOptions extends IBaseReadFileOptions { }
export interface IReadFileOptions extends IBaseReadFileOptions {
/**
* The optional `atomic` flag can be used to make sure
* the `readFile` method is not running in parallel with
* any `write` operations in the same process.
*
* Typically you should not need to use this flag but if
* for example you are quickly reading a file right after
* a file event occured and the file changes a lot, there
* is a chance that a read returns an empty or partial file
* because a pending write has not finished yet.
*
* Note: this does not prevent the file from being written
* to from a different process. If you need such atomic
* operations, you better use a real database as storage.
*/
readonly atomic?: boolean;
}
export interface IWriteFileOptions {
/**
@@ -832,6 +978,11 @@ export interface IWriteFileOptions {
* The etag of the file. This can be used to prevent dirty writes.
*/
readonly etag?: string;
/**
* Whether to attempt to unlock a file before writing.
*/
readonly unlock?: boolean;
}
export interface IResolveFileOptions {
@@ -883,7 +1034,7 @@ export const enum FileOperationResult {
FILE_NOT_MODIFIED_SINCE,
FILE_MODIFIED_SINCE,
FILE_MOVE_CONFLICT,
FILE_READ_ONLY,
FILE_WRITE_LOCKED,
FILE_PERMISSION_DENIED,
FILE_TOO_LARGE,
FILE_INVALID_PATH,
@@ -892,6 +1043,10 @@ export const enum FileOperationResult {
FILE_OTHER_ERROR
}
//#endregion
//#region Settings
export const AutoSaveConfiguration = {
OFF: 'off',
AFTER_DELAY: 'afterDelay',
@@ -911,7 +1066,7 @@ export const FILES_EXCLUDE_CONFIG = 'files.exclude';
export interface IFilesConfiguration {
files: {
associations: { [filepattern: string]: string };
exclude: glob.IExpression;
exclude: IExpression;
watcherExclude: { [filepattern: string]: boolean };
encoding: string;
autoGuessEncoding: boolean;
@@ -926,6 +1081,10 @@ export interface IFilesConfiguration {
};
}
//#endregion
//#region Utilities
export enum FileKind {
FILE,
FOLDER,
@@ -972,6 +1131,7 @@ export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096;
* Helper to format a raw byte size into a human readable label.
*/
export class ByteSize {
static readonly KB = 1024;
static readonly MB = ByteSize.KB * ByteSize.KB;
static readonly GB = ByteSize.MB * ByteSize.KB;
@@ -1001,3 +1161,24 @@ export class ByteSize {
return localize('sizeTB', "{0}TB", (size / ByteSize.TB).toFixed(2));
}
}
// Native only: Arch limits
export interface IArchLimits {
readonly maxFileSize: number;
readonly maxHeapSize: number;
}
export const enum Arch {
IA32,
OTHER
}
export function getPlatformLimits(arch: Arch): IArchLimits {
return {
maxFileSize: arch === Arch.IA32 ? 300 * ByteSize.MB : 16 * ByteSize.GB, // https://github.com/microsoft/vscode/issues/30180
maxHeapSize: arch === Arch.IA32 ? 700 * ByteSize.MB : 2 * 700 * ByteSize.MB, // https://github.com/v8/v8/blob/5918a23a3d571b9625e5cce246bdd5b46ff7cd8b/src/heap/heap.cc#L149
};
}
//#endregion

View File

@@ -10,6 +10,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, createFileSystemProviderError, FileSystemProviderErrorCode, ensureFileSystemProviderError } from 'vs/platform/files/common/files';
import { canceled } from 'vs/base/common/errors';
import { IErrorTransformer, IDataTransformer, WriteableStream } from 'vs/base/common/stream';
import product from 'vs/platform/product/common/product';
export interface ICreateReadStreamOptions extends FileReadStreamOptions {
@@ -46,7 +47,11 @@ export async function readFileIntoStream<T>(
error = options.errorTransformer(error);
}
target.end(error);
if (typeof error !== 'undefined') {
target.error(error);
}
target.end();
}
}
@@ -123,7 +128,7 @@ function throwIfTooLarge(totalBytesRead: number, options: ICreateReadStreamOptio
// Return early if file is too large to load and we have configured limits
if (options?.limits) {
if (typeof options.limits.memory === 'number' && totalBytesRead > options.limits.memory) {
throw createFileSystemProviderError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), FileSystemProviderErrorCode.FileExceedsMemoryLimit);
throw createFileSystemProviderError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow {0} to use more memory", product.nameShort), FileSystemProviderErrorCode.FileExceedsMemoryLimit);
}
if (typeof options.limits.size === 'number' && totalBytesRead > options.limits.size) {

View File

@@ -0,0 +1,197 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
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 { FileChangeType, FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileType, IFileChange, IStat, IWatchOptions, FileOpenOptions, IFileSystemProviderWithFileReadWriteCapability, FileWriteOptions, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, FileReadStreamOptions, IFileSystemProviderWithOpenReadWriteCloseCapability } from 'vs/platform/files/common/files';
import { VSBuffer } from 'vs/base/common/buffer';
import { newWriteableStream, ReadableStreamEvents, ReadableStreamEventPayload } from 'vs/base/common/stream';
import { CancellationToken } from 'vs/base/common/cancellation';
import { canceled } from 'vs/base/common/errors';
import { toErrorMessage } from 'vs/base/common/errorMessage';
interface IFileChangeDto {
resource: UriComponents;
type: FileChangeType;
}
/**
* An abstract file system provider that delegates all calls to a provided
* `IChannel` via IPC communication.
*/
export abstract class IPCFileSystemProvider extends Disposable implements
IFileSystemProviderWithFileReadWriteCapability,
IFileSystemProviderWithOpenReadWriteCloseCapability,
IFileSystemProviderWithFileReadStreamCapability,
IFileSystemProviderWithFileFolderCopyCapability {
private readonly session: string = generateUuid();
private readonly _onDidChange = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile = this._onDidChange.event;
private _onDidWatchErrorOccur = this._register(new Emitter<string>());
readonly onDidErrorOccur = this._onDidWatchErrorOccur.event;
private readonly _onDidChangeCapabilities = this._register(new Emitter<void>());
readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;
private _capabilities = FileSystemProviderCapabilities.FileReadWrite
| FileSystemProviderCapabilities.FileOpenReadWriteClose
| FileSystemProviderCapabilities.FileReadStream
| FileSystemProviderCapabilities.FileFolderCopy
| FileSystemProviderCapabilities.FileWriteUnlock;
get capabilities(): FileSystemProviderCapabilities { return this._capabilities; }
constructor(private readonly channel: IChannel) {
super();
this.registerListeners();
}
private registerListeners(): void {
this._register(this.channel.listen<IFileChangeDto[] | string>('filechange', [this.session])(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._onDidWatchErrorOccur.fire(error);
}
}));
}
protected setCaseSensitive(isCaseSensitive: boolean) {
if (isCaseSensitive) {
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
} else {
this._capabilities &= ~FileSystemProviderCapabilities.PathCaseSensitive;
}
this._onDidChangeCapabilities.fire(undefined);
}
// --- forwarding calls
stat(resource: URI): Promise<IStat> {
return this.channel.call('stat', [resource]);
}
open(resource: URI, opts: FileOpenOptions): Promise<number> {
return this.channel.call('open', [resource, opts]);
}
close(fd: number): Promise<void> {
return this.channel.call('close', [fd]);
}
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
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;
}
async readFile(resource: URI): Promise<Uint8Array> {
const buff = <VSBuffer>await this.channel.call('readFile', [resource]);
return buff.buffer;
}
readFileStream(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);
// Reading as file stream goes through an event to the remote side
const listener = this.channel.listen<ReadableStreamEventPayload<VSBuffer>>('readFileStream', [resource, opts])(dataOrErrorOrEnd => {
// data
if (dataOrErrorOrEnd instanceof VSBuffer) {
stream.write(dataOrErrorOrEnd.buffer);
}
// end or error
else {
if (dataOrErrorOrEnd === 'end') {
stream.end();
} else {
// Since we receive data through a IPC channel, it is likely
// that the error was not serialized, or only partially. To
// ensure our API use is correct, we convert the data to an
// error here to forward it properly.
let error = dataOrErrorOrEnd;
if (!(error instanceof Error)) {
error = new Error(toErrorMessage(error));
}
stream.error(error);
stream.end();
}
// Signal to the remote side that we no longer listen
listener.dispose();
}
});
// Support cancellation
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.
listener.dispose();
});
return stream;
}
write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]);
}
writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]);
}
delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
return this.channel.call('delete', [resource, opts]);
}
mkdir(resource: URI): Promise<void> {
return this.channel.call('mkdir', [resource]);
}
readdir(resource: URI): Promise<[string, FileType][]> {
return this.channel.call('readdir', [resource]);
}
rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
return this.channel.call('rename', [resource, target, opts]);
}
copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
return this.channel.call('copy', [resource, target, opts]);
}
watch(resource: URI, opts: IWatchOptions): IDisposable {
const req = Math.random();
this.channel.call('watch', [this.session, req, resource, opts]);
return toDisposable(() => this.channel.call('unwatch', [this.session, req]));
}
}

View File

@@ -21,7 +21,7 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
super(logService, options);
}
get capabilities(): FileSystemProviderCapabilities {
override get capabilities(): FileSystemProviderCapabilities {
if (!this._capabilities) {
this._capabilities = super.capabilities | FileSystemProviderCapabilities.Trash;
}
@@ -29,7 +29,7 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
return this._capabilities;
}
protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
protected override async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
if (!opts.useTrash) {
return super.doDelete(filePath, opts);
}

View File

@@ -6,7 +6,7 @@
import { open, close, read, write, fdatasync, Stats, promises } from 'fs';
import { promisify } from 'util';
import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability } from 'vs/platform/files/common/files';
import { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability, isFileOpenForWriteOptions } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { isLinux, isWindows } from 'vs/base/common/platform';
@@ -30,7 +30,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
export interface IWatcherOptions {
pollingInterval?: number;
usePolling: boolean;
usePolling: boolean | string[];
}
export interface IDiskFileSystemProviderOptions {
@@ -64,7 +64,8 @@ export class DiskFileSystemProvider extends Disposable implements
FileSystemProviderCapabilities.FileReadWrite |
FileSystemProviderCapabilities.FileOpenReadWriteClose |
FileSystemProviderCapabilities.FileReadStream |
FileSystemProviderCapabilities.FileFolderCopy;
FileSystemProviderCapabilities.FileFolderCopy |
FileSystemProviderCapabilities.FileWriteUnlock;
if (isLinux) {
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
@@ -188,12 +189,12 @@ export class DiskFileSystemProvider extends Disposable implements
}
// Open
handle = await this.open(resource, { create: true });
handle = await this.open(resource, { create: true, unlock: opts.unlock });
// Write content at once
await this.write(handle, 0, content, 0, content.byteLength);
} catch (error) {
throw this.toFileSystemProviderError(error);
throw await this.toFileSystemProviderWriteError(resource, error);
} finally {
if (typeof handle === 'number') {
await this.close(handle);
@@ -203,15 +204,28 @@ export class DiskFileSystemProvider extends Disposable implements
private readonly mapHandleToPos: Map<number, number> = new Map();
private readonly writeHandles: Set<number> = new Set();
private readonly writeHandles = new Map<number, URI>();
private canFlush: boolean = true;
async open(resource: URI, opts: FileOpenOptions): Promise<number> {
try {
const filePath = this.toFilePath(resource);
// Determine wether to unlock the file (write only)
if (isFileOpenForWriteOptions(opts) && opts.unlock) {
try {
const { stat } = await SymlinkSupport.stat(filePath);
if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {
await promises.chmod(filePath, stat.mode | 0o200);
}
} catch (error) {
this.logService.trace(error); // ignore any errors here and try to just write
}
}
// Determine file flags for opening (read vs write)
let flags: string | undefined = undefined;
if (opts.create) {
if (isFileOpenForWriteOptions(opts)) {
if (isWindows) {
try {
// On Windows and if the file exists, we use a different strategy of saving the file
@@ -252,13 +266,17 @@ export class DiskFileSystemProvider extends Disposable implements
this.mapHandleToPos.set(handle, 0);
// remember that this handle was used for writing
if (opts.create) {
this.writeHandles.add(handle);
if (isFileOpenForWriteOptions(opts)) {
this.writeHandles.set(handle, resource);
}
return handle;
} catch (error) {
throw this.toFileSystemProviderError(error);
if (isFileOpenForWriteOptions(opts)) {
throw await this.toFileSystemProviderWriteError(resource, error);
} else {
throw this.toFileSystemProviderError(error);
}
}
}
@@ -388,7 +406,7 @@ export class DiskFileSystemProvider extends Disposable implements
return bytesWritten;
} catch (error) {
throw this.toFileSystemProviderError(error);
throw await this.toFileSystemProviderWriteError(this.writeHandles.get(fd), error);
} finally {
this.updatePos(fd, normalizedPos, bytesWritten);
}
@@ -690,9 +708,29 @@ export class DiskFileSystemProvider extends Disposable implements
return createFileSystemProviderError(error, code);
}
private async toFileSystemProviderWriteError(resource: URI | undefined, error: NodeJS.ErrnoException): Promise<FileSystemProviderError> {
let fileSystemProviderWriteError = this.toFileSystemProviderError(error);
// If the write error signals permission issues, we try
// to read the file's mode to see if the file is write
// locked.
if (resource && fileSystemProviderWriteError.code === FileSystemProviderErrorCode.NoPermissions) {
try {
const { stat } = await SymlinkSupport.stat(this.toFilePath(resource));
if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {
fileSystemProviderWriteError = createFileSystemProviderError(error, FileSystemProviderErrorCode.FileWriteLocked);
}
} catch (error) {
this.logService.trace(error); // ignore - return original error
}
}
return fileSystemProviderWriteError;
}
//#endregion
dispose(): void {
override dispose(): void {
super.dispose();
dispose(this.recursiveWatcher);

View File

@@ -124,7 +124,7 @@ export class FileWatcher extends Disposable {
}
}
dispose(): void {
override dispose(): void {
this.isDisposed = true;
super.dispose();

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nsfw from 'vscode-nsfw';
import * as nsfw from 'nsfw';
import * as glob from 'vs/base/common/glob';
import { join } from 'vs/base/common/path';
import { isMacintosh } from 'vs/base/common/platform';
@@ -61,9 +61,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
});
// Logging
if (this.verboseLogging) {
this.log(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
}
this.debug(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
// Stop watching some roots
rootsToStopWatching.forEach(root => {
@@ -133,9 +131,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
}
}
if (this.verboseLogging) {
this.log(`Start watching with nsfw: ${request.path}`);
}
this.debug(`Start watching with nsfw: ${request.path}`);
nsfw(request.path, events => {
for (const e of events) {
@@ -249,4 +245,8 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
private error(message: string) {
this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (nsfw)] ` + message });
}
private debug(message: string) {
this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (nsfw)] ` + message });
}
}

View File

@@ -10,7 +10,7 @@ import { IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
suite('NSFW Watcher Service', async () => {
// Load `nsfwWatcherService` within the suite to prevent all tests
// from failing to start if `vscode-nsfw` was not properly installed
// from failing to start if `nsfw` was not properly installed
const { NsfwWatcherService } = await import('vs/platform/files/node/watcher/nsfw/nsfwWatcherService');
class TestNsfwWatcherService extends NsfwWatcherService {

View File

@@ -5,8 +5,8 @@
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
const server = new Server('watcher');
const service = new NsfwWatcherService();
server.registerChannel('watcher', createChannelReceiver(service));
server.registerChannel('watcher', ProxyChannel.fromService(service));

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createChannelSender, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { ProxyChannel, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
@@ -61,7 +61,7 @@ export class FileWatcher extends Disposable {
}));
// Initialize watcher
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service = ProxyChannel.toService<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service.setVerboseLogging(this.verboseLogging);
@@ -91,7 +91,7 @@ export class FileWatcher extends Disposable {
}
}
dispose(): void {
override dispose(): void {
this.isDisposed = true;
super.dispose();

View File

@@ -49,7 +49,7 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
get wacherCount() { return this._watcherCount; }
private pollingInterval?: number;
private usePolling?: boolean;
private usePolling?: boolean | string[];
private verboseLogging: boolean | undefined;
private spamCheckStartTime: number | undefined;
@@ -101,7 +101,11 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
private watch(basePath: string, requests: IWatcherRequest[]): IWatcher {
const pollingInterval = this.pollingInterval || 5000;
const usePolling = this.usePolling;
let usePolling = this.usePolling; // boolean or a list of path patterns
if (Array.isArray(usePolling)) {
// switch to polling if one of the paths matches with a watched path
usePolling = usePolling.some(pattern => requests.some(r => glob.match(pattern, r.path)));
}
const watcherOpts: chokidar.WatchOptions = {
ignoreInitial: true,
@@ -142,9 +146,7 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
this.warn(`Watcher basePath does not match version on disk and was corrected (original: ${basePath}, real: ${realBasePath})`);
}
if (this.verboseLogging) {
this.log(`Start watching with chokidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`);
}
this.debug(`Start watching with chokidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`);
let chokidarWatcher: chokidar.FSWatcher | null = chokidar.watch(realBasePath, watcherOpts);
this._watcherCount++;
@@ -297,6 +299,10 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (chokidar)] ` + message });
}
private debug(message: string) {
this._onDidLogMessage.fire({ type: 'debug', message: `[File Watcher (chokidar)] ` + message });
}
private warn(message: string) {
this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (chokidar)] ` + message });
}

View File

@@ -13,7 +13,7 @@ export interface IWatcherRequest {
export interface IWatcherOptions {
pollingInterval?: number;
usePolling?: boolean;
usePolling?: boolean | string[]; // boolean or a set of glob patterns matching folders that need polling
verboseLogging?: boolean;
}

View File

@@ -5,8 +5,8 @@
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { ChokidarWatcherService } from 'vs/platform/files/node/watcher/unix/chokidarWatcherService';
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
const server = new Server('watcher');
const service = new ChokidarWatcherService();
server.registerChannel('watcher', createChannelReceiver(service));
server.registerChannel('watcher', ProxyChannel.fromService(service));

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createChannelSender, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { ProxyChannel, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
@@ -62,7 +62,7 @@ export class FileWatcher extends Disposable {
}));
// Initialize watcher
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service = ProxyChannel.toService<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service.init({ ...this.watcherOptions, verboseLogging: this.verboseLogging });
this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e)));
@@ -92,7 +92,7 @@ export class FileWatcher extends Disposable {
}
}
dispose(): void {
override dispose(): void {
this.isDisposed = true;
super.dispose();

View File

@@ -13,7 +13,7 @@ export interface IDiskFileChange {
}
export interface ILogMessage {
type: 'trace' | 'warn' | 'error';
type: 'trace' | 'warn' | 'error' | 'info' | 'debug';
message: string;
}

View File

@@ -6,11 +6,13 @@
import * as assert from 'assert';
import { FileService } from 'vs/platform/files/common/fileService';
import { URI } from 'vs/base/common/uri';
import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files';
import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities, IFileSystemProviderCapabilitiesChangeEvent, FileOpenOptions, FileReadStreamOptions, IStat, FileType } from 'vs/platform/files/common/files';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { NullLogService } from 'vs/platform/log/common/log';
import { timeout } from 'vs/base/common/async';
import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider';
import { consumeStream, newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream';
import { CancellationToken } from 'vs/base/common/cancellation';
suite('File Service', () => {
@@ -20,6 +22,7 @@ suite('File Service', () => {
const provider = new NullFileSystemProvider();
assert.strictEqual(service.canHandleResource(resource), false);
assert.strictEqual(service.getProvider(resource.scheme), undefined);
const registrations: IFileSystemProviderRegistrationEvent[] = [];
service.onDidChangeFileSystemProviderRegistrations(e => {
@@ -31,7 +34,7 @@ suite('File Service', () => {
capabilityChanges.push(e);
});
let registrationDisposable: IDisposable | undefined = undefined;
let registrationDisposable: IDisposable | undefined;
let callCount = 0;
service.onWillActivateFileSystemProvider(e => {
callCount++;
@@ -48,6 +51,7 @@ suite('File Service', () => {
await service.activateProvider('test');
assert.strictEqual(service.canHandleResource(resource), true);
assert.strictEqual(service.getProvider(resource.scheme), provider);
assert.strictEqual(registrations.length, 1);
assert.strictEqual(registrations[0].scheme, 'test');
@@ -126,4 +130,82 @@ suite('File Service', () => {
service.dispose();
});
test('error from readFile bubbles through (https://github.com/microsoft/vscode/issues/118060) - async', async () => {
testReadErrorBubbles(true);
});
test('error from readFile bubbles through (https://github.com/microsoft/vscode/issues/118060)', async () => {
testReadErrorBubbles(false);
});
async function testReadErrorBubbles(async: boolean) {
const service = new FileService(new NullLogService());
const provider = new class extends NullFileSystemProvider {
override async stat(resource: URI): Promise<IStat> {
return {
mtime: Date.now(),
ctime: Date.now(),
size: 100,
type: FileType.File
};
}
override readFile(resource: URI): Promise<Uint8Array> {
if (async) {
return timeout(5).then(() => { throw new Error('failed'); });
}
throw new Error('failed');
}
override open(resource: URI, opts: FileOpenOptions): Promise<number> {
if (async) {
return timeout(5).then(() => { throw new Error('failed'); });
}
throw new Error('failed');
}
readFileStream(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
if (async) {
const stream = newWriteableStream<Uint8Array>(chunk => chunk[0]);
timeout(5).then(() => stream.error(new Error('failed')));
return stream;
}
throw new Error('failed');
}
};
const disposable = service.registerProvider('test', provider);
for (const capabilities of [FileSystemProviderCapabilities.FileReadWrite, FileSystemProviderCapabilities.FileReadStream, FileSystemProviderCapabilities.FileOpenReadWriteClose]) {
provider.setCapabilities(capabilities);
let e1;
try {
await service.readFile(URI.parse('test://foo/bar'));
} catch (error) {
e1 = error;
}
assert.ok(e1);
let e2;
try {
const stream = await service.readFileStream(URI.parse('test://foo/bar'));
await consumeStream(stream.value, chunk => chunk[0]);
} catch (error) {
e2 = error;
}
assert.ok(e2);
}
disposable.dispose();
}
});

View File

@@ -78,7 +78,8 @@ suite('IndexedDB File Service', function () {
disposables.add(userdataFileProvider);
};
setup(async () => {
setup(async function () {
this.timeout(15000);
await reload();
});

View File

@@ -8,13 +8,12 @@ import { tmpdir } from 'os';
import { FileService } from 'vs/platform/files/common/fileService';
import { Schemas } from 'vs/base/common/network';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { getRandomTestPath, getPathFromAmdModule } from 'vs/base/test/node/testUtils';
import { join, basename, dirname, posix } from 'vs/base/common/path';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { copy, rimraf, rimrafSync } from 'vs/base/node/pfs';
import { URI } from 'vs/base/common/uri';
import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync, createReadStream, promises } from 'fs';
import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities, FileChangeType, IFileChange, FileChangesEvent, FileOperationError, etag, IStat, IFileStatWithMetadata, IReadFileOptions } from 'vs/platform/files/common/files';
import { NullLogService } from 'vs/platform/log/common/log';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { DisposableStore } from 'vs/base/common/lifecycle';
@@ -59,13 +58,14 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
private smallStatSize: boolean = false;
private _testCapabilities!: FileSystemProviderCapabilities;
get capabilities(): FileSystemProviderCapabilities {
override get capabilities(): FileSystemProviderCapabilities {
if (!this._testCapabilities) {
this._testCapabilities =
FileSystemProviderCapabilities.FileReadWrite |
FileSystemProviderCapabilities.FileOpenReadWriteClose |
FileSystemProviderCapabilities.FileReadStream |
FileSystemProviderCapabilities.Trash |
FileSystemProviderCapabilities.FileWriteUnlock |
FileSystemProviderCapabilities.FileFolderCopy;
if (isLinux) {
@@ -76,7 +76,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
return this._testCapabilities;
}
set capabilities(capabilities: FileSystemProviderCapabilities) {
override set capabilities(capabilities: FileSystemProviderCapabilities) {
this._testCapabilities = capabilities;
}
@@ -88,7 +88,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
this.smallStatSize = enabled;
}
async stat(resource: URI): Promise<IStat> {
override async stat(resource: URI): Promise<IStat> {
const res = await super.stat(resource);
if (this.invalidStatSize) {
@@ -100,7 +100,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
return res;
}
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
override async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
const bytesRead = await super.read(fd, pos, data, offset, length);
this.totalBytesRead += bytesRead;
@@ -108,7 +108,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
return bytesRead;
}
async readFile(resource: URI): Promise<Uint8Array> {
override async readFile(resource: URI): Promise<Uint8Array> {
const res = await super.readFile(resource);
this.totalBytesRead += res.byteLength;
@@ -1181,8 +1181,14 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
return testReadFile(URI.file(join(testDir, 'lorem.txt')));
});
async function testReadFile(resource: URI): Promise<void> {
const content = await service.readFile(resource);
test('readFile - atomic', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadStream);
return testReadFile(URI.file(join(testDir, 'lorem.txt')), { atomic: true });
});
async function testReadFile(resource: URI, options?: IReadFileOptions): Promise<void> {
const content = await service.readFile(resource, options);
assert.strictEqual(content.value.toString(), readFileSync(resource.fsPath).toString());
}
@@ -1584,6 +1590,20 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
assert.strictEqual(error!.fileOperationResult, FileOperationResult.FILE_TOO_LARGE);
}
(isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('readFile - dangling symbolic link - https://github.com/microsoft/vscode/issues/116049', async () => {
const link = URI.file(join(testDir, 'small.js-link'));
await promises.symlink(join(testDir, 'small.js'), link.fsPath);
let error: FileOperationError | undefined = undefined;
try {
await service.readFile(link);
} catch (err) {
error = err;
}
assert.ok(error);
});
test('createFile', async () => {
return assertCreateFile(contents => VSBuffer.fromString(contents));
});
@@ -1740,19 +1760,23 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
assert.ok(error!);
}
test('writeFile (large file) - multiple parallel writes queue up', async () => {
test('writeFile (large file) - multiple parallel writes queue up and atomic read support', async () => {
const resource = URI.file(join(testDir, 'lorem.txt'));
const content = readFileSync(resource.fsPath);
const newContent = content.toString() + content.toString();
await Promise.all(['0', '00', '000', '0000', '00000'].map(async offset => {
const writePromises = Promise.all(['0', '00', '000', '0000', '00000'].map(async offset => {
const fileStat = await service.writeFile(resource, VSBuffer.fromString(offset + newContent));
assert.strictEqual(fileStat.name, 'lorem.txt');
}));
const fileContent = readFileSync(resource.fsPath).toString();
assert.ok(['0', '00', '000', '0000', '00000'].some(offset => fileContent === offset + newContent));
const readPromises = Promise.all(['0', '00', '000', '0000', '00000'].map(async () => {
const fileContent = await service.readFile(resource, { atomic: true });
assert.ok(fileContent.value.byteLength > 0); // `atomic: true` ensures we never read a truncated file
}));
await Promise.all([writePromises, readPromises]);
});
test('writeFile (readable) - default', async () => {
@@ -1875,6 +1899,63 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
assert.strictEqual(readFileSync(resource.fsPath).toString(), content);
});
test('writeFile - locked files and unlocking', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileWriteUnlock);
return testLockedFiles(false);
});
test('writeFile (stream) - locked files and unlocking', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileWriteUnlock);
return testLockedFiles(false);
});
test('writeFile - locked files and unlocking throws error when missing capability', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite);
return testLockedFiles(true);
});
test('writeFile (stream) - locked files and unlocking throws error when missing capability', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose);
return testLockedFiles(true);
});
async function testLockedFiles(expectError: boolean) {
const lockedFile = URI.file(join(testDir, 'my-locked-file'));
await service.writeFile(lockedFile, VSBuffer.fromString('Locked File'));
const stats = await promises.stat(lockedFile.fsPath);
await promises.chmod(lockedFile.fsPath, stats.mode & ~0o200);
let error;
const newContent = 'Updates to locked file';
try {
await service.writeFile(lockedFile, VSBuffer.fromString(newContent));
} catch (e) {
error = e;
}
assert.ok(error);
error = undefined;
if (expectError) {
try {
await service.writeFile(lockedFile, VSBuffer.fromString(newContent), { unlock: true });
} catch (e) {
error = e;
}
assert.ok(error);
} else {
await service.writeFile(lockedFile, VSBuffer.fromString(newContent), { unlock: true });
assert.strictEqual(readFileSync(lockedFile.fsPath).toString(), newContent);
}
}
test('writeFile (error when folder is encountered)', async () => {
const resource = URI.file(testDir);
@@ -2272,7 +2353,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
const resource = URI.file(join(testDir, 'lorem.txt'));
const buffer = VSBuffer.alloc(1024);
const fdWrite = await fileProvider.open(resource, { create: true });
const fdWrite = await fileProvider.open(resource, { create: true, unlock: false });
const fdRead = await fileProvider.open(resource, { create: false });
let posInFileWrite = 0;