mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
Merge from vscode 2c306f762bf9c3db82dc06c7afaa56ef46d72f79 (#14050)
* Merge from vscode 2c306f762bf9c3db82dc06c7afaa56ef46d72f79 * Fix breaks * Extension management fixes * Fix breaks in windows bundling * Fix/skip failing tests * Update distro * Add clear to nuget.config * Add hygiene task * Bump distro * Fix hygiene issue * Add build to hygiene exclusion * Update distro * Update hygiene * Hygiene exclusions * Update tsconfig * Bump distro for server breaks * Update build config * Update darwin path * Add done calls to notebook tests * Skip failing tests * Disable smoke tests
This commit is contained in:
@@ -3,9 +3,14 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyValueFileSystemProvider } from 'vs/platform/files/common/keyValueFileSystemProvider';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions, FileChangeType, createFileSystemProviderError, FileSystemProviderErrorCode } from 'vs/platform/files/common/files';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { joinPath, extUri, dirname } from 'vs/base/common/resources';
|
||||
import { localize } from 'vs/nls';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import { IFileSystemProvider } from 'vs/platform/files/common/files';
|
||||
|
||||
const INDEXEDDB_VSCODE_DB = 'vscode-web-db';
|
||||
export const INDEXEDDB_USERDATA_OBJECT_STORE = 'vscode-userdata-store';
|
||||
@@ -19,8 +24,8 @@ export class IndexedDB {
|
||||
this.indexedDBPromise = this.openIndexedDB(INDEXEDDB_VSCODE_DB, 2, [INDEXEDDB_USERDATA_OBJECT_STORE, INDEXEDDB_LOGS_OBJECT_STORE]);
|
||||
}
|
||||
|
||||
async createFileSystemProvider(scheme: string, store: string): Promise<IFileSystemProvider | null> {
|
||||
let fsp: IFileSystemProvider | null = null;
|
||||
async createFileSystemProvider(scheme: string, store: string): Promise<IIndexedDBFileSystemProvider | null> {
|
||||
let fsp: IIndexedDBFileSystemProvider | null = null;
|
||||
const indexedDB = await this.indexedDBPromise;
|
||||
if (indexedDB) {
|
||||
if (indexedDB.objectStoreNames.contains(store)) {
|
||||
@@ -63,13 +68,147 @@ export class IndexedDB {
|
||||
|
||||
}
|
||||
|
||||
class IndexedDBFileSystemProvider extends KeyValueFileSystemProvider {
|
||||
export interface IIndexedDBFileSystemProvider extends Disposable, IFileSystemProviderWithFileReadWriteCapability {
|
||||
reset(): Promise<void>;
|
||||
}
|
||||
|
||||
constructor(scheme: string, private readonly database: IDBDatabase, private readonly store: string) {
|
||||
super(scheme);
|
||||
class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSystemProvider {
|
||||
|
||||
readonly capabilities: FileSystemProviderCapabilities =
|
||||
FileSystemProviderCapabilities.FileReadWrite
|
||||
| FileSystemProviderCapabilities.PathCaseSensitive;
|
||||
readonly onDidChangeCapabilities: Event<void> = Event.None;
|
||||
|
||||
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
|
||||
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
|
||||
|
||||
private readonly versions: Map<string, number> = new Map<string, number>();
|
||||
private readonly dirs: Set<string> = new Set<string>();
|
||||
|
||||
constructor(private readonly scheme: string, private readonly database: IDBDatabase, private readonly store: string) {
|
||||
super();
|
||||
this.dirs.add('/');
|
||||
}
|
||||
|
||||
protected async getAllKeys(): Promise<string[]> {
|
||||
watch(resource: URI, opts: IWatchOptions): IDisposable {
|
||||
return Disposable.None;
|
||||
}
|
||||
|
||||
async mkdir(resource: URI): Promise<void> {
|
||||
try {
|
||||
const resourceStat = await this.stat(resource);
|
||||
if (resourceStat.type === FileType.File) {
|
||||
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
|
||||
}
|
||||
} catch (error) { /* Ignore */ }
|
||||
|
||||
// Make sure parent dir exists
|
||||
await this.stat(dirname(resource));
|
||||
|
||||
this.dirs.add(resource.path);
|
||||
}
|
||||
|
||||
async stat(resource: URI): Promise<IStat> {
|
||||
try {
|
||||
const content = await this.readFile(resource);
|
||||
return {
|
||||
type: FileType.File,
|
||||
ctime: 0,
|
||||
mtime: this.versions.get(resource.toString()) || 0,
|
||||
size: content.byteLength
|
||||
};
|
||||
} catch (e) {
|
||||
}
|
||||
const files = await this.readdir(resource);
|
||||
if (files.length) {
|
||||
return {
|
||||
type: FileType.Directory,
|
||||
ctime: 0,
|
||||
mtime: 0,
|
||||
size: 0
|
||||
};
|
||||
}
|
||||
if (this.dirs.has(resource.path)) {
|
||||
return {
|
||||
type: FileType.Directory,
|
||||
ctime: 0,
|
||||
mtime: 0,
|
||||
size: 0
|
||||
};
|
||||
}
|
||||
throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);
|
||||
}
|
||||
|
||||
async readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (hasKey) {
|
||||
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
|
||||
}
|
||||
const keys = await this.getAllKeys();
|
||||
const files: Map<string, [string, FileType]> = new Map<string, [string, FileType]>();
|
||||
for (const key of keys) {
|
||||
const keyResource = this.toResource(key);
|
||||
if (extUri.isEqualOrParent(keyResource, resource)) {
|
||||
const path = extUri.relativePath(resource, keyResource);
|
||||
if (path) {
|
||||
const keySegments = path.split('/');
|
||||
files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...files.values()];
|
||||
}
|
||||
|
||||
async readFile(resource: URI): Promise<Uint8Array> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (!hasKey) {
|
||||
throw createFileSystemProviderError(localize('fileNotFound', "File not found"), FileSystemProviderErrorCode.FileNotFound);
|
||||
}
|
||||
const value = await this.getValue(resource.path);
|
||||
if (typeof value === 'string') {
|
||||
return VSBuffer.fromString(value).buffer;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (!hasKey) {
|
||||
const files = await this.readdir(resource);
|
||||
if (files.length) {
|
||||
throw createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory);
|
||||
}
|
||||
}
|
||||
await this.setValue(resource.path, content);
|
||||
this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1);
|
||||
this._onDidChangeFile.fire([{ resource, type: FileChangeType.UPDATED }]);
|
||||
}
|
||||
|
||||
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (hasKey) {
|
||||
await this.deleteKey(resource.path);
|
||||
this.versions.delete(resource.path);
|
||||
this._onDidChangeFile.fire([{ resource, type: FileChangeType.DELETED }]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.recursive) {
|
||||
const files = await this.readdir(resource);
|
||||
await Promise.all(files.map(([key]) => this.delete(joinPath(resource, key), opts)));
|
||||
}
|
||||
}
|
||||
|
||||
rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return Promise.reject(new Error('Not Supported'));
|
||||
}
|
||||
|
||||
private toResource(key: string): URI {
|
||||
return URI.file(key).with({ scheme: this.scheme });
|
||||
}
|
||||
|
||||
async getAllKeys(): Promise<string[]> {
|
||||
return new Promise(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store]);
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
@@ -79,7 +218,7 @@ class IndexedDBFileSystemProvider extends KeyValueFileSystemProvider {
|
||||
});
|
||||
}
|
||||
|
||||
protected hasKey(key: string): Promise<boolean> {
|
||||
hasKey(key: string): Promise<boolean> {
|
||||
return new Promise<boolean>(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store]);
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
@@ -91,7 +230,7 @@ class IndexedDBFileSystemProvider extends KeyValueFileSystemProvider {
|
||||
});
|
||||
}
|
||||
|
||||
protected getValue(key: string): Promise<string> {
|
||||
getValue(key: string): Promise<Uint8Array | string> {
|
||||
return new Promise(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store]);
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
@@ -101,7 +240,7 @@ class IndexedDBFileSystemProvider extends KeyValueFileSystemProvider {
|
||||
});
|
||||
}
|
||||
|
||||
protected setValue(key: string, value: string): Promise<void> {
|
||||
setValue(key: string, value: Uint8Array): Promise<void> {
|
||||
return new Promise(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store], 'readwrite');
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
@@ -111,7 +250,7 @@ class IndexedDBFileSystemProvider extends KeyValueFileSystemProvider {
|
||||
});
|
||||
}
|
||||
|
||||
protected deleteKey(key: string): Promise<void> {
|
||||
deleteKey(key: string): Promise<void> {
|
||||
return new Promise(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store], 'readwrite');
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
@@ -120,4 +259,14 @@ class IndexedDBFileSystemProvider extends KeyValueFileSystemProvider {
|
||||
request.onsuccess = () => c();
|
||||
});
|
||||
}
|
||||
|
||||
reset(): Promise<void> {
|
||||
return new Promise(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store], 'readwrite');
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
const request = objectStore.clear();
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => c();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Queue } 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';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
|
||||
export class FileService extends Disposable implements IFileService {
|
||||
|
||||
@@ -54,7 +55,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
|
||||
// Forward events from provider
|
||||
const providerDisposables = new DisposableStore();
|
||||
providerDisposables.add(provider.onDidChangeFile(changes => this._onDidFilesChange.fire(new FileChangesEvent(changes, this.getExtUri(provider).extUri))));
|
||||
providerDisposables.add(provider.onDidChangeFile(changes => this._onDidFilesChange.fire(new FileChangesEvent(changes, !this.isPathCaseSensitive(provider)))));
|
||||
providerDisposables.add(provider.onDidChangeCapabilities(() => this._onDidChangeFileSystemProviderCapabilities.fire({ provider, scheme })));
|
||||
if (typeof provider.onDidErrorOccur === 'function') {
|
||||
providerDisposables.add(provider.onDidErrorOccur(error => this._onError.fire(new Error(error))));
|
||||
@@ -101,6 +102,10 @@ export class FileService extends Disposable implements IFileService {
|
||||
return !!(provider && (provider.capabilities & capability));
|
||||
}
|
||||
|
||||
listCapabilities(): Iterable<{ scheme: string, capabilities: FileSystemProviderCapabilities }> {
|
||||
return Iterable.map(this.provider, ([scheme, provider]) => ({ scheme, capabilities: provider.capabilities }));
|
||||
}
|
||||
|
||||
protected async withProvider(resource: URI): Promise<IFileSystemProvider> {
|
||||
|
||||
// Assert path is absolute
|
||||
@@ -175,6 +180,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat>;
|
||||
private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
|
||||
const provider = await this.withProvider(resource);
|
||||
const isPathCaseSensitive = this.isPathCaseSensitive(provider);
|
||||
|
||||
const resolveTo = options?.resolveTo;
|
||||
const resolveSingleChildDescendants = options?.resolveSingleChildDescendants;
|
||||
@@ -188,7 +194,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
|
||||
// lazy trie to check for recursive resolving
|
||||
if (!trie) {
|
||||
trie = TernarySearchTree.forUris<true>();
|
||||
trie = TernarySearchTree.forUris<true>(() => !isPathCaseSensitive);
|
||||
trie.set(resource, true);
|
||||
if (isNonEmptyArray(resolveTo)) {
|
||||
resolveTo.forEach(uri => trie!.set(uri, true));
|
||||
@@ -756,7 +762,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
|
||||
private getExtUri(provider: IFileSystemProvider): { extUri: IExtUri, isPathCaseSensitive: boolean } {
|
||||
const isPathCaseSensitive = !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
|
||||
const isPathCaseSensitive = this.isPathCaseSensitive(provider);
|
||||
|
||||
return {
|
||||
extUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase,
|
||||
@@ -764,6 +770,10 @@ export class FileService extends Disposable implements IFileService {
|
||||
};
|
||||
}
|
||||
|
||||
private isPathCaseSensitive(provider: IFileSystemProvider): boolean {
|
||||
return !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
|
||||
}
|
||||
|
||||
async createFolder(resource: URI): Promise<IFileStatWithMetadata> {
|
||||
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { startsWithIgnoreCase } from 'vs/base/common/strings';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IExtUri } from 'vs/base/common/resources';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { isNumber, isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
import { ReadableStreamEvents } from 'vs/base/common/stream';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { TernarySearchTree } from 'vs/base/common/map';
|
||||
|
||||
export const IFileService = createDecorator<IFileService>('fileService');
|
||||
|
||||
@@ -59,6 +59,11 @@ export interface IFileService {
|
||||
*/
|
||||
hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean;
|
||||
|
||||
/**
|
||||
* List the schemes and capabilies for registered file system providers
|
||||
*/
|
||||
listCapabilities(): Iterable<{ scheme: string, capabilities: FileSystemProviderCapabilities }>
|
||||
|
||||
/**
|
||||
* Allows to listen for file changes. The event will fire for every file within the opened workspace
|
||||
* (if any) as well as all files that have been watched explicitly using the #watch() API.
|
||||
@@ -361,7 +366,7 @@ export function createFileSystemProviderError(error: Error | string, code: FileS
|
||||
|
||||
export function ensureFileSystemProviderError(error?: Error): Error {
|
||||
if (!error) {
|
||||
return createFileSystemProviderError(localize('unknownError', "Unknown Error"), FileSystemProviderErrorCode.Unknown); // https://github.com/Microsoft/vscode/issues/72798
|
||||
return createFileSystemProviderError(localize('unknownError', "Unknown Error"), FileSystemProviderErrorCode.Unknown); // https://github.com/microsoft/vscode/issues/72798
|
||||
}
|
||||
|
||||
return error;
|
||||
@@ -497,36 +502,111 @@ export interface IFileChange {
|
||||
|
||||
export class FileChangesEvent {
|
||||
|
||||
constructor(public readonly changes: readonly IFileChange[], private readonly extUri: IExtUri) { }
|
||||
/**
|
||||
* @deprecated use the `contains()` or `affects` method to efficiently find
|
||||
* out if the event relates to a given resource. these methods ensure:
|
||||
* - that there is no expensive lookup needed (by using a `TernarySearchTree`)
|
||||
* - correctly handles `FileChangeType.DELETED` events
|
||||
*/
|
||||
readonly changes: readonly IFileChange[];
|
||||
|
||||
private readonly added: TernarySearchTree<URI, IFileChange> | undefined = undefined;
|
||||
private readonly updated: TernarySearchTree<URI, IFileChange> | undefined = undefined;
|
||||
private readonly deleted: TernarySearchTree<URI, IFileChange> | undefined = undefined;
|
||||
|
||||
constructor(changes: readonly IFileChange[], private readonly ignorePathCasing: boolean) {
|
||||
this.changes = changes;
|
||||
|
||||
for (const change of changes) {
|
||||
switch (change.type) {
|
||||
case FileChangeType.ADDED:
|
||||
if (!this.added) {
|
||||
this.added = TernarySearchTree.forUris<IFileChange>(() => this.ignorePathCasing);
|
||||
}
|
||||
this.added.set(change.resource, change);
|
||||
break;
|
||||
case FileChangeType.UPDATED:
|
||||
if (!this.updated) {
|
||||
this.updated = TernarySearchTree.forUris<IFileChange>(() => this.ignorePathCasing);
|
||||
}
|
||||
this.updated.set(change.resource, change);
|
||||
break;
|
||||
case FileChangeType.DELETED:
|
||||
if (!this.deleted) {
|
||||
this.deleted = TernarySearchTree.forUris<IFileChange>(() => this.ignorePathCasing);
|
||||
}
|
||||
this.deleted.set(change.resource, change);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this change event contains the provided file with the given change type (if provided). In case of
|
||||
* type DELETED, this method will also return true if a folder got deleted that is the parent of the
|
||||
* provided file path.
|
||||
* Find out if the file change events match the provided resource.
|
||||
*
|
||||
* Note: when passing `FileChangeType.DELETED`, we consider a match
|
||||
* also when the parent of the resource got deleted.
|
||||
*/
|
||||
contains(resource: URI, type?: FileChangeType): boolean {
|
||||
contains(resource: URI, ...types: FileChangeType[]): boolean {
|
||||
return this.doContains(resource, { includeChildren: false }, ...types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out if the file change events either match the provided
|
||||
* resource, or contain a child of this resource.
|
||||
*/
|
||||
affects(resource: URI, ...types: FileChangeType[]): boolean {
|
||||
return this.doContains(resource, { includeChildren: true }, ...types);
|
||||
}
|
||||
|
||||
private doContains(resource: URI, options: { includeChildren: boolean }, ...types: FileChangeType[]): boolean {
|
||||
if (!resource) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const checkForChangeType = !isUndefinedOrNull(type);
|
||||
const hasTypesFilter = types.length > 0;
|
||||
|
||||
return this.changes.some(change => {
|
||||
if (checkForChangeType && change.type !== type) {
|
||||
return false;
|
||||
// Added
|
||||
if (!hasTypesFilter || types.includes(FileChangeType.ADDED)) {
|
||||
if (this.added?.get(resource)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For deleted also return true when deleted folder is parent of target path
|
||||
if (change.type === FileChangeType.DELETED) {
|
||||
return this.extUri.isEqualOrParent(resource, change.resource);
|
||||
if (options.includeChildren && this.added?.findSuperstr(resource)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Updated
|
||||
if (!hasTypesFilter || types.includes(FileChangeType.UPDATED)) {
|
||||
if (this.updated?.get(resource)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.extUri.isEqual(resource, change.resource);
|
||||
});
|
||||
if (options.includeChildren && this.updated?.findSuperstr(resource)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Deleted
|
||||
if (!hasTypesFilter || types.includes(FileChangeType.DELETED)) {
|
||||
if (this.deleted?.findSubstr(resource) /* deleted also considers parent folders */) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.includeChildren && this.deleted?.findSuperstr(resource)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the changes that describe added files.
|
||||
* @deprecated use the `contains()` method to efficiently find out if the event
|
||||
* relates to a given resource. this method ensures:
|
||||
* - that there is no expensive lookup needed by using a `TernarySearchTree`
|
||||
* - correctly handles `FileChangeType.DELETED` events
|
||||
*/
|
||||
getAdded(): IFileChange[] {
|
||||
return this.getOfType(FileChangeType.ADDED);
|
||||
@@ -536,11 +616,14 @@ export class FileChangesEvent {
|
||||
* Returns if this event contains added files.
|
||||
*/
|
||||
gotAdded(): boolean {
|
||||
return this.hasType(FileChangeType.ADDED);
|
||||
return !!this.added;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the changes that describe deleted files.
|
||||
* @deprecated use the `contains()` method to efficiently find out if the event
|
||||
* relates to a given resource. this method ensures:
|
||||
* - that there is no expensive lookup needed by using a `TernarySearchTree`
|
||||
* - correctly handles `FileChangeType.DELETED` events
|
||||
*/
|
||||
getDeleted(): IFileChange[] {
|
||||
return this.getOfType(FileChangeType.DELETED);
|
||||
@@ -550,11 +633,14 @@ export class FileChangesEvent {
|
||||
* Returns if this event contains deleted files.
|
||||
*/
|
||||
gotDeleted(): boolean {
|
||||
return this.hasType(FileChangeType.DELETED);
|
||||
return !!this.deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the changes that describe updated files.
|
||||
* @deprecated use the `contains()` method to efficiently find out if the event
|
||||
* relates to a given resource. this method ensures:
|
||||
* - that there is no expensive lookup needed by using a `TernarySearchTree`
|
||||
* - correctly handles `FileChangeType.DELETED` events
|
||||
*/
|
||||
getUpdated(): IFileChange[] {
|
||||
return this.getOfType(FileChangeType.UPDATED);
|
||||
@@ -564,21 +650,30 @@ export class FileChangesEvent {
|
||||
* Returns if this event contains updated files.
|
||||
*/
|
||||
gotUpdated(): boolean {
|
||||
return this.hasType(FileChangeType.UPDATED);
|
||||
return !!this.updated;
|
||||
}
|
||||
|
||||
private getOfType(type: FileChangeType): IFileChange[] {
|
||||
return this.changes.filter(change => change.type === type);
|
||||
}
|
||||
|
||||
private hasType(type: FileChangeType): boolean {
|
||||
return this.changes.some(change => {
|
||||
return change.type === type;
|
||||
});
|
||||
const changes: IFileChange[] = [];
|
||||
|
||||
const eventsForType = type === FileChangeType.ADDED ? this.added : type === FileChangeType.UPDATED ? this.updated : this.deleted;
|
||||
if (eventsForType) {
|
||||
for (const [, change] of eventsForType) {
|
||||
changes.push(change);
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use the `contains()` method to efficiently find out if the event
|
||||
* relates to a given resource. this method ensures:
|
||||
* - that there is no expensive lookup needed by using a `TernarySearchTree`
|
||||
* - correctly handles `FileChangeType.DELETED` events
|
||||
*/
|
||||
filter(filterFn: (change: IFileChange) => boolean): FileChangesEvent {
|
||||
return new FileChangesEvent(this.changes.filter(change => filterFn(change)), this.extUri);
|
||||
return new FileChangesEvent(this.changes.filter(change => filterFn(change)), this.ignorePathCasing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,11 +952,11 @@ export function whenProviderRegistered(file: URI, fileService: IFileService): Pr
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((c, e) => {
|
||||
return new Promise(resolve => {
|
||||
const disposable = fileService.onDidChangeFileSystemProviderRegistrations(e => {
|
||||
if (e.scheme === file.scheme && e.added) {
|
||||
disposable.dispose();
|
||||
c();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -876,29 +971,33 @@ export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096;
|
||||
/**
|
||||
* Helper to format a raw byte size into a human readable label.
|
||||
*/
|
||||
export class BinarySize {
|
||||
export class ByteSize {
|
||||
static readonly KB = 1024;
|
||||
static readonly MB = BinarySize.KB * BinarySize.KB;
|
||||
static readonly GB = BinarySize.MB * BinarySize.KB;
|
||||
static readonly TB = BinarySize.GB * BinarySize.KB;
|
||||
static readonly MB = ByteSize.KB * ByteSize.KB;
|
||||
static readonly GB = ByteSize.MB * ByteSize.KB;
|
||||
static readonly TB = ByteSize.GB * ByteSize.KB;
|
||||
|
||||
static formatSize(size: number): string {
|
||||
if (size < BinarySize.KB) {
|
||||
return localize('sizeB', "{0}B", size);
|
||||
if (!isNumber(size)) {
|
||||
size = 0;
|
||||
}
|
||||
|
||||
if (size < BinarySize.MB) {
|
||||
return localize('sizeKB', "{0}KB", (size / BinarySize.KB).toFixed(2));
|
||||
if (size < ByteSize.KB) {
|
||||
return localize('sizeB', "{0}B", size.toFixed(0));
|
||||
}
|
||||
|
||||
if (size < BinarySize.GB) {
|
||||
return localize('sizeMB', "{0}MB", (size / BinarySize.MB).toFixed(2));
|
||||
if (size < ByteSize.MB) {
|
||||
return localize('sizeKB', "{0}KB", (size / ByteSize.KB).toFixed(2));
|
||||
}
|
||||
|
||||
if (size < BinarySize.TB) {
|
||||
return localize('sizeGB', "{0}GB", (size / BinarySize.GB).toFixed(2));
|
||||
if (size < ByteSize.GB) {
|
||||
return localize('sizeMB', "{0}MB", (size / ByteSize.MB).toFixed(2));
|
||||
}
|
||||
|
||||
return localize('sizeTB', "{0}TB", (size / BinarySize.TB).toFixed(2));
|
||||
if (size < ByteSize.TB) {
|
||||
return localize('sizeGB', "{0}GB", (size / ByteSize.GB).toFixed(2));
|
||||
}
|
||||
|
||||
return localize('sizeTB', "{0}TB", (size / ByteSize.TB).toFixed(2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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, FileChangeType, createFileSystemProviderError, FileSystemProviderErrorCode } from 'vs/platform/files/common/files';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { joinPath, extUri, dirname } from 'vs/base/common/resources';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export abstract class KeyValueFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability {
|
||||
|
||||
readonly capabilities: FileSystemProviderCapabilities =
|
||||
FileSystemProviderCapabilities.FileReadWrite
|
||||
| FileSystemProviderCapabilities.PathCaseSensitive;
|
||||
readonly onDidChangeCapabilities: Event<void> = Event.None;
|
||||
|
||||
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
|
||||
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
|
||||
|
||||
private readonly versions: Map<string, number> = new Map<string, number>();
|
||||
private readonly dirs: Set<string> = new Set<string>();
|
||||
|
||||
constructor(private readonly scheme: string) {
|
||||
super();
|
||||
// Add root directory by default
|
||||
this.dirs.add('/');
|
||||
}
|
||||
|
||||
watch(resource: URI, opts: IWatchOptions): IDisposable {
|
||||
return Disposable.None;
|
||||
}
|
||||
|
||||
async mkdir(resource: URI): Promise<void> {
|
||||
try {
|
||||
const resourceStat = await this.stat(resource);
|
||||
if (resourceStat.type === FileType.File) {
|
||||
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
|
||||
}
|
||||
} catch (error) { /* Ignore */ }
|
||||
|
||||
// Make sure parent dir exists
|
||||
await this.stat(dirname(resource));
|
||||
|
||||
this.dirs.add(resource.path);
|
||||
}
|
||||
|
||||
async stat(resource: URI): Promise<IStat> {
|
||||
try {
|
||||
const content = await this.readFile(resource);
|
||||
return {
|
||||
type: FileType.File,
|
||||
ctime: 0,
|
||||
mtime: this.versions.get(resource.toString()) || 0,
|
||||
size: content.byteLength
|
||||
};
|
||||
} catch (e) {
|
||||
}
|
||||
const files = await this.readdir(resource);
|
||||
if (files.length) {
|
||||
return {
|
||||
type: FileType.Directory,
|
||||
ctime: 0,
|
||||
mtime: 0,
|
||||
size: 0
|
||||
};
|
||||
}
|
||||
if (this.dirs.has(resource.path)) {
|
||||
return {
|
||||
type: FileType.Directory,
|
||||
ctime: 0,
|
||||
mtime: 0,
|
||||
size: 0
|
||||
};
|
||||
}
|
||||
throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);
|
||||
}
|
||||
|
||||
async readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (hasKey) {
|
||||
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
|
||||
}
|
||||
const keys = await this.getAllKeys();
|
||||
const files: Map<string, [string, FileType]> = new Map<string, [string, FileType]>();
|
||||
for (const key of keys) {
|
||||
const keyResource = this.toResource(key);
|
||||
if (extUri.isEqualOrParent(keyResource, resource)) {
|
||||
const path = extUri.relativePath(resource, keyResource);
|
||||
if (path) {
|
||||
const keySegments = path.split('/');
|
||||
files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...files.values()];
|
||||
}
|
||||
|
||||
async readFile(resource: URI): Promise<Uint8Array> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (!hasKey) {
|
||||
throw createFileSystemProviderError(localize('fileNotFound', "File not found"), FileSystemProviderErrorCode.FileNotFound);
|
||||
}
|
||||
const value = await this.getValue(resource.path);
|
||||
return VSBuffer.fromString(value).buffer;
|
||||
}
|
||||
|
||||
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (!hasKey) {
|
||||
const files = await this.readdir(resource);
|
||||
if (files.length) {
|
||||
throw createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory);
|
||||
}
|
||||
}
|
||||
await this.setValue(resource.path, VSBuffer.wrap(content).toString());
|
||||
this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1);
|
||||
this._onDidChangeFile.fire([{ resource, type: FileChangeType.UPDATED }]);
|
||||
}
|
||||
|
||||
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (hasKey) {
|
||||
await this.deleteKey(resource.path);
|
||||
this.versions.delete(resource.path);
|
||||
this._onDidChangeFile.fire([{ resource, type: FileChangeType.DELETED }]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.recursive) {
|
||||
const files = await this.readdir(resource);
|
||||
await Promise.all(files.map(([key]) => this.delete(joinPath(resource, key), opts)));
|
||||
}
|
||||
}
|
||||
|
||||
rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return Promise.reject(new Error('Not Supported'));
|
||||
}
|
||||
|
||||
private toResource(key: string): URI {
|
||||
return URI.file(key).with({ scheme: this.scheme });
|
||||
}
|
||||
|
||||
protected abstract getAllKeys(): Promise<string[]>;
|
||||
protected abstract hasKey(key: string): Promise<boolean>;
|
||||
protected abstract getValue(key: string): Promise<string>;
|
||||
protected abstract setValue(key: string, value: string): Promise<void>;
|
||||
protected abstract deleteKey(key: string): Promise<void>;
|
||||
}
|
||||
@@ -9,13 +9,13 @@ import { isWindows } from 'vs/base/common/platform';
|
||||
import { localize } from 'vs/nls';
|
||||
import { basename } from 'vs/base/common/path';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
|
||||
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
|
||||
|
||||
export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
|
||||
|
||||
constructor(
|
||||
logService: ILogService,
|
||||
private readonly electronService: IElectronService,
|
||||
private readonly nativeHostService: INativeHostService,
|
||||
options?: IDiskFileSystemProviderOptions
|
||||
) {
|
||||
super(logService, options);
|
||||
@@ -34,7 +34,7 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
|
||||
return super.doDelete(filePath, opts);
|
||||
}
|
||||
|
||||
const result = await this.electronService.moveItemToTrash(filePath);
|
||||
const result = await this.nativeHostService.moveItemToTrash(filePath);
|
||||
if (!result) {
|
||||
throw new Error(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin", basename(filePath)) : localize('trashFailed', "Failed to move '{0}' to the trash", basename(filePath)));
|
||||
}
|
||||
|
||||
@@ -216,8 +216,8 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
try {
|
||||
// On Windows and if the file exists, we use a different strategy of saving the file
|
||||
// by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows
|
||||
// (see https://github.com/Microsoft/vscode/issues/931) and prevent removing alternate data streams
|
||||
// (see https://github.com/Microsoft/vscode/issues/6363)
|
||||
// (see https://github.com/microsoft/vscode/issues/931) and prevent removing alternate data streams
|
||||
// (see https://github.com/microsoft/vscode/issues/6363)
|
||||
await truncate(filePath, 0);
|
||||
|
||||
// After a successful truncate() the flag can be set to 'r+' which will not truncate.
|
||||
|
||||
@@ -9,12 +9,13 @@ import * as path from 'vs/base/common/path';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
|
||||
import * as nsfw from 'vscode-nsfw';
|
||||
import { IWatcherService, IWatcherRequest, IWatcherOptions } from 'vs/platform/files/node/watcher/nsfw/watcher';
|
||||
import { IWatcherService, IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { FileChangeType } from 'vs/platform/files/common/files';
|
||||
import { normalizeNFC } from 'vs/base/common/normalization';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { realcaseSync, realpathSync } from 'vs/base/node/extpath';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
const nsfwActionToRawChangeType: { [key: number]: number } = [];
|
||||
nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED;
|
||||
@@ -32,29 +33,61 @@ interface IPathWatcher {
|
||||
ignored: glob.ParsedPattern[];
|
||||
}
|
||||
|
||||
export class NsfwWatcherService implements IWatcherService {
|
||||
export class NsfwWatcherService extends Disposable implements IWatcherService {
|
||||
|
||||
private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
|
||||
|
||||
private _pathWatchers: { [watchPath: string]: IPathWatcher } = {};
|
||||
private _verboseLogging: boolean | undefined;
|
||||
private readonly _onDidChangeFile = this._register(new Emitter<IDiskFileChange[]>());
|
||||
readonly onDidChangeFile = this._onDidChangeFile.event;
|
||||
|
||||
private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>());
|
||||
readonly onDidLogMessage: Event<ILogMessage> = this._onDidLogMessage.event;
|
||||
|
||||
private pathWatchers: { [watchPath: string]: IPathWatcher } = {};
|
||||
private verboseLogging: boolean | undefined;
|
||||
private enospcErrorLogged: boolean | undefined;
|
||||
|
||||
private readonly _onWatchEvent = new Emitter<IDiskFileChange[]>();
|
||||
readonly onWatchEvent = this._onWatchEvent.event;
|
||||
async setRoots(roots: IWatcherRequest[]): Promise<void> {
|
||||
const normalizedRoots = this._normalizeRoots(roots);
|
||||
|
||||
private readonly _onLogMessage = new Emitter<ILogMessage>();
|
||||
readonly onLogMessage: Event<ILogMessage> = this._onLogMessage.event;
|
||||
// Gather roots that are not currently being watched
|
||||
const rootsToStartWatching = normalizedRoots.filter(r => {
|
||||
return !(r.path in this.pathWatchers);
|
||||
});
|
||||
|
||||
watch(options: IWatcherOptions): Event<IDiskFileChange[]> {
|
||||
return this.onWatchEvent;
|
||||
// Gather current roots that don't exist in the new roots array
|
||||
const rootsToStopWatching = Object.keys(this.pathWatchers).filter(r => {
|
||||
return normalizedRoots.every(normalizedRoot => normalizedRoot.path !== r);
|
||||
});
|
||||
|
||||
// Logging
|
||||
if (this.verboseLogging) {
|
||||
this.log(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
|
||||
}
|
||||
|
||||
// Stop watching some roots
|
||||
rootsToStopWatching.forEach(root => {
|
||||
this.pathWatchers[root].ready.then(watcher => watcher.stop());
|
||||
delete this.pathWatchers[root];
|
||||
});
|
||||
|
||||
// Start watching some roots
|
||||
rootsToStartWatching.forEach(root => this.doWatch(root));
|
||||
|
||||
// Refresh ignored arrays in case they changed
|
||||
roots.forEach(root => {
|
||||
if (root.path in this.pathWatchers) {
|
||||
this.pathWatchers[root.path].ignored = Array.isArray(root.excludes) ? root.excludes.map(ignored => glob.parse(ignored)) : [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _watch(request: IWatcherRequest): void {
|
||||
private doWatch(request: IWatcherRequest): void {
|
||||
let undeliveredFileEvents: IDiskFileChange[] = [];
|
||||
const fileEventDelayer = new ThrottledDelayer<void>(NsfwWatcherService.FS_EVENT_DELAY);
|
||||
|
||||
let readyPromiseResolve: (watcher: IWatcherObjet) => void;
|
||||
this._pathWatchers[request.path] = {
|
||||
this.pathWatchers[request.path] = {
|
||||
ready: new Promise<IWatcherObjet>(resolve => readyPromiseResolve = resolve),
|
||||
ignored: Array.isArray(request.excludes) ? request.excludes.map(ignored => glob.parse(ignored)) : []
|
||||
};
|
||||
@@ -65,7 +98,7 @@ export class NsfwWatcherService implements IWatcherService {
|
||||
// the watcher consumes so many file descriptors that
|
||||
// we are running into a limit. We only want to warn
|
||||
// once in this case to avoid log spam.
|
||||
// See https://github.com/Microsoft/vscode/issues/7950
|
||||
// See https://github.com/microsoft/vscode/issues/7950
|
||||
if (e === 'Inotify limit reached' && !this.enospcErrorLogged) {
|
||||
this.enospcErrorLogged = true;
|
||||
this.error('Inotify limit reached (ENOSPC)');
|
||||
@@ -100,14 +133,14 @@ export class NsfwWatcherService implements IWatcherService {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._verboseLogging) {
|
||||
if (this.verboseLogging) {
|
||||
this.log(`Start watching with nsfw: ${request.path}`);
|
||||
}
|
||||
|
||||
nsfw(request.path, events => {
|
||||
for (const e of events) {
|
||||
// Logging
|
||||
if (this._verboseLogging) {
|
||||
if (this.verboseLogging) {
|
||||
const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : path.join(e.directory, e.file || '');
|
||||
this.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`);
|
||||
}
|
||||
@@ -117,25 +150,25 @@ export class NsfwWatcherService implements IWatcherService {
|
||||
if (e.action === nsfw.actions.RENAMED) {
|
||||
// Rename fires when a file's name changes within a single directory
|
||||
absolutePath = path.join(e.directory, e.oldFile || '');
|
||||
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
|
||||
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
|
||||
undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath });
|
||||
} else if (this._verboseLogging) {
|
||||
} else if (this.verboseLogging) {
|
||||
this.log(` >> ignored ${absolutePath}`);
|
||||
}
|
||||
absolutePath = path.join(e.newDirectory || e.directory, e.newFile || '');
|
||||
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
|
||||
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
|
||||
undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath });
|
||||
} else if (this._verboseLogging) {
|
||||
} else if (this.verboseLogging) {
|
||||
this.log(` >> ignored ${absolutePath}`);
|
||||
}
|
||||
} else {
|
||||
absolutePath = path.join(e.directory, e.file || '');
|
||||
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
|
||||
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
|
||||
undeliveredFileEvents.push({
|
||||
type: nsfwActionToRawChangeType[e.action],
|
||||
path: absolutePath
|
||||
});
|
||||
} else if (this._verboseLogging) {
|
||||
} else if (this.verboseLogging) {
|
||||
this.log(` >> ignored ${absolutePath}`);
|
||||
}
|
||||
}
|
||||
@@ -161,94 +194,59 @@ export class NsfwWatcherService implements IWatcherService {
|
||||
|
||||
// Broadcast to clients normalized
|
||||
const res = normalizeFileChanges(events);
|
||||
this._onWatchEvent.fire(res);
|
||||
this._onDidChangeFile.fire(res);
|
||||
|
||||
// Logging
|
||||
if (this._verboseLogging) {
|
||||
if (this.verboseLogging) {
|
||||
res.forEach(r => {
|
||||
this.log(` >> normalized ${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}).then(watcher => {
|
||||
this._pathWatchers[request.path].watcher = watcher;
|
||||
this.pathWatchers[request.path].watcher = watcher;
|
||||
const startPromise = watcher.start();
|
||||
startPromise.then(() => readyPromiseResolve(watcher));
|
||||
|
||||
return startPromise;
|
||||
});
|
||||
}
|
||||
|
||||
async setRoots(roots: IWatcherRequest[]): Promise<void> {
|
||||
const normalizedRoots = this._normalizeRoots(roots);
|
||||
|
||||
// Gather roots that are not currently being watched
|
||||
const rootsToStartWatching = normalizedRoots.filter(r => {
|
||||
return !(r.path in this._pathWatchers);
|
||||
});
|
||||
|
||||
// Gather current roots that don't exist in the new roots array
|
||||
const rootsToStopWatching = Object.keys(this._pathWatchers).filter(r => {
|
||||
return normalizedRoots.every(normalizedRoot => normalizedRoot.path !== r);
|
||||
});
|
||||
|
||||
// Logging
|
||||
if (this._verboseLogging) {
|
||||
this.log(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
|
||||
}
|
||||
|
||||
// Stop watching some roots
|
||||
rootsToStopWatching.forEach(root => {
|
||||
this._pathWatchers[root].ready.then(watcher => watcher.stop());
|
||||
delete this._pathWatchers[root];
|
||||
});
|
||||
|
||||
// Start watching some roots
|
||||
rootsToStartWatching.forEach(root => this._watch(root));
|
||||
|
||||
// Refresh ignored arrays in case they changed
|
||||
roots.forEach(root => {
|
||||
if (root.path in this._pathWatchers) {
|
||||
this._pathWatchers[root.path].ignored = Array.isArray(root.excludes) ? root.excludes.map(ignored => glob.parse(ignored)) : [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setVerboseLogging(enabled: boolean): Promise<void> {
|
||||
this._verboseLogging = enabled;
|
||||
this.verboseLogging = enabled;
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
for (let path in this._pathWatchers) {
|
||||
let watcher = this._pathWatchers[path];
|
||||
for (let path in this.pathWatchers) {
|
||||
let watcher = this.pathWatchers[path];
|
||||
watcher.ready.then(watcher => watcher.stop());
|
||||
delete this._pathWatchers[path];
|
||||
delete this.pathWatchers[path];
|
||||
}
|
||||
this._pathWatchers = Object.create(null);
|
||||
|
||||
this.pathWatchers = Object.create(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a set of root paths by removing any root paths that are
|
||||
* sub-paths of other roots.
|
||||
*/
|
||||
protected _normalizeRoots(roots: IWatcherRequest[]): IWatcherRequest[] {
|
||||
// Normalizes a set of root paths by removing any root paths that are
|
||||
// sub-paths of other roots.
|
||||
return roots.filter(r => roots.every(other => {
|
||||
return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path));
|
||||
}));
|
||||
}
|
||||
|
||||
private _isPathIgnored(absolutePath: string, ignored: glob.ParsedPattern[]): boolean {
|
||||
private isPathIgnored(absolutePath: string, ignored: glob.ParsedPattern[]): boolean {
|
||||
return ignored && ignored.some(i => i(absolutePath));
|
||||
}
|
||||
|
||||
private log(message: string) {
|
||||
this._onLogMessage.fire({ type: 'trace', message: `[File Watcher (nsfw)] ` + message });
|
||||
this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (nsfw)] ` + message });
|
||||
}
|
||||
|
||||
private warn(message: string) {
|
||||
this._onLogMessage.fire({ type: 'warn', message: `[File Watcher (nsfw)] ` + message });
|
||||
this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (nsfw)] ` + message });
|
||||
}
|
||||
|
||||
private error(message: string) {
|
||||
this._onLogMessage.fire({ type: 'error', message: `[File Watcher (nsfw)] ` + message });
|
||||
this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (nsfw)] ` + message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,24 +5,27 @@
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
|
||||
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
|
||||
import { IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
|
||||
|
||||
class TestNsfwWatcherService extends NsfwWatcherService {
|
||||
suite('NSFW Watcher Service', async () => {
|
||||
|
||||
normalizeRoots(roots: string[]): string[] {
|
||||
// Load `nsfwWatcherService` within the suite to prevent all tests
|
||||
// from failing to start if `vscode-nsfw` was not properly installed
|
||||
const { NsfwWatcherService } = await import('vs/platform/files/node/watcher/nsfw/nsfwWatcherService');
|
||||
|
||||
// Work with strings as paths to simplify testing
|
||||
const requests: IWatcherRequest[] = roots.map(r => {
|
||||
return { path: r, excludes: [] };
|
||||
});
|
||||
class TestNsfwWatcherService extends NsfwWatcherService {
|
||||
|
||||
return this._normalizeRoots(requests).map(r => r.path);
|
||||
normalizeRoots(roots: string[]): string[] {
|
||||
|
||||
// Work with strings as paths to simplify testing
|
||||
const requests: IWatcherRequest[] = roots.map(r => {
|
||||
return { path: r, excludes: [] };
|
||||
});
|
||||
|
||||
return this._normalizeRoots(requests).map(r => r.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suite('NSFW Watcher Service', () => {
|
||||
suite('_normalizeRoots', () => {
|
||||
test('should not impacts roots that don\'t overlap', () => {
|
||||
const service = new TestNsfwWatcherService();
|
||||
|
||||
@@ -11,13 +11,13 @@ export interface IWatcherRequest {
|
||||
excludes: string[];
|
||||
}
|
||||
|
||||
export interface IWatcherOptions {
|
||||
}
|
||||
|
||||
export interface IWatcherService {
|
||||
watch(options: IWatcherOptions): Event<IDiskFileChange[]>;
|
||||
|
||||
readonly onDidChangeFile: Event<IDiskFileChange[]>;
|
||||
readonly onDidLogMessage: Event<ILogMessage>;
|
||||
|
||||
setRoots(roots: IWatcherRequest[]): Promise<void>;
|
||||
setVerboseLogging(enabled: boolean): Promise<void>;
|
||||
onLogMessage: Event<ILogMessage>;
|
||||
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { WatcherChannel } from 'vs/platform/files/node/watcher/nsfw/watcherIpc';
|
||||
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
|
||||
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
|
||||
|
||||
const server = new Server('watcher');
|
||||
const service = new NsfwWatcherService();
|
||||
const channel = new WatcherChannel(service);
|
||||
server.registerChannel('watcher', channel);
|
||||
server.registerChannel('watcher', createChannelReceiver(service));
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IWatcherRequest, IWatcherService, IWatcherOptions } from './watcher';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
|
||||
|
||||
export class WatcherChannel implements IServerChannel {
|
||||
|
||||
constructor(private service: IWatcherService) { }
|
||||
|
||||
listen(_: unknown, event: string, arg?: any): Event<any> {
|
||||
switch (event) {
|
||||
case 'watch': return this.service.watch(arg);
|
||||
case 'onLogMessage': return this.service.onLogMessage;
|
||||
}
|
||||
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(_: unknown, command: string, arg?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'setRoots': return this.service.setRoots(arg);
|
||||
case 'setVerboseLogging': return this.service.setVerboseLogging(arg);
|
||||
case 'stop': return this.service.stop();
|
||||
}
|
||||
|
||||
throw new Error(`Call not found: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class WatcherChannelClient implements IWatcherService {
|
||||
|
||||
constructor(private channel: IChannel) { }
|
||||
|
||||
watch(options: IWatcherOptions): Event<IDiskFileChange[]> {
|
||||
return this.channel.listen('watch', options);
|
||||
}
|
||||
|
||||
setVerboseLogging(enable: boolean): Promise<void> {
|
||||
return this.channel.call('setVerboseLogging', enable);
|
||||
}
|
||||
|
||||
setRoots(roots: IWatcherRequest[]): Promise<void> {
|
||||
return this.channel.call('setRoots', roots);
|
||||
}
|
||||
|
||||
get onLogMessage(): Event<ILogMessage> {
|
||||
return this.channel.listen('onLogMessage');
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
return this.channel.call('stop');
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,18 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { createChannelSender, 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 { WatcherChannelClient } from 'vs/platform/files/node/watcher/nsfw/watcherIpc';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { IWatcherRequest, IWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcher';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
|
||||
export class FileWatcher extends Disposable {
|
||||
|
||||
private static readonly MAX_RESTARTS = 5;
|
||||
|
||||
private service: WatcherChannelClient | undefined;
|
||||
private service: IWatcherService | undefined;
|
||||
private isDisposed: boolean;
|
||||
private restartCounter: number;
|
||||
|
||||
@@ -35,7 +34,7 @@ export class FileWatcher extends Disposable {
|
||||
|
||||
private startWatching(): void {
|
||||
const client = this._register(new Client(
|
||||
getPathFromAmdModule(require, 'bootstrap-fork'),
|
||||
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
|
||||
{
|
||||
serverName: 'File Watcher (nsfw)',
|
||||
args: ['--type=watcherService'],
|
||||
@@ -62,15 +61,12 @@ export class FileWatcher extends Disposable {
|
||||
}));
|
||||
|
||||
// Initialize watcher
|
||||
const channel = getNextTickChannel(client.getChannel('watcher'));
|
||||
this.service = new WatcherChannelClient(channel);
|
||||
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
|
||||
|
||||
this.service.setVerboseLogging(this.verboseLogging);
|
||||
|
||||
const options = {};
|
||||
this._register(this.service.watch(options)(e => !this.isDisposed && this.onDidFilesChange(e)));
|
||||
|
||||
this._register(this.service.onLogMessage(m => this.onLogMessage(m)));
|
||||
this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e)));
|
||||
this._register(this.service.onDidLogMessage(m => this.onLogMessage(m)));
|
||||
|
||||
// Start watching
|
||||
this.setFolders(this.folders);
|
||||
|
||||
@@ -18,6 +18,7 @@ import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/
|
||||
import { IWatcherRequest, IWatcherService, IWatcherOptions } from 'vs/platform/files/node/watcher/unix/watcher';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
process.noAsar = true; // disable ASAR support in watcher process
|
||||
|
||||
@@ -30,81 +31,76 @@ interface ExtendedWatcherRequest extends IWatcherRequest {
|
||||
parsedPattern?: glob.ParsedPattern;
|
||||
}
|
||||
|
||||
export class ChokidarWatcherService implements IWatcherService {
|
||||
export class ChokidarWatcherService extends Disposable implements IWatcherService {
|
||||
|
||||
private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
|
||||
private static readonly EVENT_SPAM_WARNING_THRESHOLD = 60 * 1000; // warn after certain time span of event spam
|
||||
|
||||
private _watchers: { [watchPath: string]: IWatcher } = Object.create(null);
|
||||
private _watcherCount = 0;
|
||||
private readonly _onDidChangeFile = this._register(new Emitter<IDiskFileChange[]>());
|
||||
readonly onDidChangeFile = this._onDidChangeFile.event;
|
||||
|
||||
private _pollingInterval?: number;
|
||||
private _usePolling?: boolean;
|
||||
private _verboseLogging: boolean | undefined;
|
||||
private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>());
|
||||
readonly onDidLogMessage: Event<ILogMessage> = this._onDidLogMessage.event;
|
||||
|
||||
private watchers = new Map<string, IWatcher>();
|
||||
|
||||
private _watcherCount = 0;
|
||||
get wacherCount() { return this._watcherCount; }
|
||||
|
||||
private pollingInterval?: number;
|
||||
private usePolling?: boolean;
|
||||
private verboseLogging: boolean | undefined;
|
||||
|
||||
private spamCheckStartTime: number | undefined;
|
||||
private spamWarningLogged: boolean | undefined;
|
||||
private enospcErrorLogged: boolean | undefined;
|
||||
|
||||
private readonly _onWatchEvent = new Emitter<IDiskFileChange[]>();
|
||||
readonly onWatchEvent = this._onWatchEvent.event;
|
||||
|
||||
private readonly _onLogMessage = new Emitter<ILogMessage>();
|
||||
readonly onLogMessage: Event<ILogMessage> = this._onLogMessage.event;
|
||||
|
||||
watch(options: IWatcherOptions): Event<IDiskFileChange[]> {
|
||||
this._pollingInterval = options.pollingInterval;
|
||||
this._usePolling = options.usePolling;
|
||||
this._watchers = Object.create(null);
|
||||
async init(options: IWatcherOptions): Promise<void> {
|
||||
this.pollingInterval = options.pollingInterval;
|
||||
this.usePolling = options.usePolling;
|
||||
this.watchers.clear();
|
||||
this._watcherCount = 0;
|
||||
|
||||
return this.onWatchEvent;
|
||||
this.verboseLogging = options.verboseLogging;
|
||||
}
|
||||
|
||||
async setVerboseLogging(enabled: boolean): Promise<void> {
|
||||
this._verboseLogging = enabled;
|
||||
this.verboseLogging = enabled;
|
||||
}
|
||||
|
||||
async setRoots(requests: IWatcherRequest[]): Promise<void> {
|
||||
const watchers = Object.create(null);
|
||||
const watchers = new Map<string, IWatcher>();
|
||||
const newRequests: string[] = [];
|
||||
|
||||
const requestsByBasePath = normalizeRoots(requests);
|
||||
|
||||
// evaluate new & remaining watchers
|
||||
for (const basePath in requestsByBasePath) {
|
||||
const watcher = this._watchers[basePath];
|
||||
const watcher = this.watchers.get(basePath);
|
||||
if (watcher && isEqualRequests(watcher.requests, requestsByBasePath[basePath])) {
|
||||
watchers[basePath] = watcher;
|
||||
delete this._watchers[basePath];
|
||||
watchers.set(basePath, watcher);
|
||||
this.watchers.delete(basePath);
|
||||
} else {
|
||||
newRequests.push(basePath);
|
||||
}
|
||||
}
|
||||
|
||||
// stop all old watchers
|
||||
for (const path in this._watchers) {
|
||||
await this._watchers[path].stop();
|
||||
for (const [, watcher] of this.watchers) {
|
||||
await watcher.stop();
|
||||
}
|
||||
|
||||
// start all new watchers
|
||||
for (const basePath of newRequests) {
|
||||
const requests = requestsByBasePath[basePath];
|
||||
watchers[basePath] = this._watch(basePath, requests);
|
||||
watchers.set(basePath, this.watch(basePath, requests));
|
||||
}
|
||||
|
||||
this._watchers = watchers;
|
||||
this.watchers = watchers;
|
||||
}
|
||||
|
||||
// for test purposes
|
||||
get wacherCount() {
|
||||
return this._watcherCount;
|
||||
}
|
||||
|
||||
private _watch(basePath: string, requests: IWatcherRequest[]): IWatcher {
|
||||
|
||||
const pollingInterval = this._pollingInterval || 5000;
|
||||
const usePolling = this._usePolling;
|
||||
private watch(basePath: string, requests: IWatcherRequest[]): IWatcher {
|
||||
const pollingInterval = this.pollingInterval || 5000;
|
||||
const usePolling = this.usePolling;
|
||||
|
||||
const watcherOpts: chokidar.WatchOptions = {
|
||||
ignoreInitial: true,
|
||||
@@ -113,15 +109,14 @@ export class ChokidarWatcherService implements IWatcherService {
|
||||
interval: pollingInterval, // while not used in normal cases, if any error causes chokidar to fallback to polling, increase its intervals
|
||||
binaryInterval: pollingInterval,
|
||||
usePolling: usePolling,
|
||||
disableGlobbing: true // fix https://github.com/Microsoft/vscode/issues/4586
|
||||
disableGlobbing: true // fix https://github.com/microsoft/vscode/issues/4586
|
||||
};
|
||||
|
||||
const excludes: string[] = [];
|
||||
|
||||
const isSingleFolder = requests.length === 1;
|
||||
if (isSingleFolder) {
|
||||
// if there's only one request, use the built-in ignore-filterering
|
||||
excludes.push(...requests[0].excludes);
|
||||
excludes.push(...requests[0].excludes); // if there's only one request, use the built-in ignore-filterering
|
||||
}
|
||||
|
||||
if ((isMacintosh || isLinux) && (basePath.length === 0 || basePath === '/')) {
|
||||
@@ -146,8 +141,8 @@ export class ChokidarWatcherService implements IWatcherService {
|
||||
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 chockidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`);
|
||||
if (this.verboseLogging) {
|
||||
this.log(`Start watching with chokidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`);
|
||||
}
|
||||
|
||||
let chokidarWatcher: chokidar.FSWatcher | null = chokidar.watch(realBasePath, watcherOpts);
|
||||
@@ -165,7 +160,7 @@ export class ChokidarWatcherService implements IWatcherService {
|
||||
requests,
|
||||
stop: async () => {
|
||||
try {
|
||||
if (this._verboseLogging) {
|
||||
if (this.verboseLogging) {
|
||||
this.log(`Stop watching: ${basePath}]`);
|
||||
}
|
||||
if (chokidarWatcher) {
|
||||
@@ -227,7 +222,7 @@ export class ChokidarWatcherService implements IWatcherService {
|
||||
const event = { type: eventType, path };
|
||||
|
||||
// Logging
|
||||
if (this._verboseLogging) {
|
||||
if (this.verboseLogging) {
|
||||
this.log(`${eventType === FileChangeType.ADDED ? '[ADDED]' : eventType === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`);
|
||||
}
|
||||
|
||||
@@ -253,10 +248,10 @@ export class ChokidarWatcherService implements IWatcherService {
|
||||
|
||||
// Broadcast to clients normalized
|
||||
const res = normalizeFileChanges(events);
|
||||
this._onWatchEvent.fire(res);
|
||||
this._onDidChangeFile.fire(res);
|
||||
|
||||
// Logging
|
||||
if (this._verboseLogging) {
|
||||
if (this.verboseLogging) {
|
||||
res.forEach(r => {
|
||||
this.log(` >> normalized ${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`);
|
||||
});
|
||||
@@ -274,7 +269,7 @@ export class ChokidarWatcherService implements IWatcherService {
|
||||
// the watcher consumes so many file descriptors that
|
||||
// we are running into a limit. We only want to warn
|
||||
// once in this case to avoid log spam.
|
||||
// See https://github.com/Microsoft/vscode/issues/7950
|
||||
// See https://github.com/microsoft/vscode/issues/7950
|
||||
if (error.code === 'ENOSPC') {
|
||||
if (!this.enospcErrorLogged) {
|
||||
this.enospcErrorLogged = true;
|
||||
@@ -290,24 +285,23 @@ export class ChokidarWatcherService implements IWatcherService {
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
for (const path in this._watchers) {
|
||||
const watcher = this._watchers[path];
|
||||
for (const [, watcher] of this.watchers) {
|
||||
await watcher.stop();
|
||||
}
|
||||
|
||||
this._watchers = Object.create(null);
|
||||
this.watchers.clear();
|
||||
}
|
||||
|
||||
private log(message: string) {
|
||||
this._onLogMessage.fire({ type: 'trace', message: `[File Watcher (chokidar)] ` + message });
|
||||
this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (chokidar)] ` + message });
|
||||
}
|
||||
|
||||
private warn(message: string) {
|
||||
this._onLogMessage.fire({ type: 'warn', message: `[File Watcher (chokidar)] ` + message });
|
||||
this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (chokidar)] ` + message });
|
||||
}
|
||||
|
||||
private error(message: string) {
|
||||
this._onLogMessage.fire({ type: 'error', message: `[File Watcher (chokidar)] ` + message });
|
||||
this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (chokidar)] ` + message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,58 +4,37 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as os from 'os';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { normalizeRoots, ChokidarWatcherService } from '../chokidarWatcherService';
|
||||
import { IWatcherRequest } from '../watcher';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { IDiskFileChange } from 'vs/platform/files/node/watcher/watcher';
|
||||
import { FileChangeType } from 'vs/platform/files/common/files';
|
||||
import { IWatcherRequest } from 'vs/platform/files/node/watcher/unix/watcher';
|
||||
|
||||
function newRequest(basePath: string, ignored: string[] = []): IWatcherRequest {
|
||||
return { path: basePath, excludes: ignored };
|
||||
}
|
||||
suite('Chokidar normalizeRoots', async () => {
|
||||
|
||||
function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) {
|
||||
const requests = inputPaths.map(path => newRequest(path));
|
||||
const actual = normalizeRoots(requests);
|
||||
assert.deepEqual(Object.keys(actual).sort(), expectedPaths);
|
||||
}
|
||||
// Load `chokidarWatcherService` within the suite to prevent all tests
|
||||
// from failing to start if `chokidar` was not properly installed
|
||||
const { normalizeRoots } = await import('vs/platform/files/node/watcher/unix/chokidarWatcherService');
|
||||
|
||||
function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) {
|
||||
const actual = normalizeRoots(inputRequests);
|
||||
const actualPath = Object.keys(actual).sort();
|
||||
const expectedPaths = Object.keys(expectedRequests).sort();
|
||||
assert.deepEqual(actualPath, expectedPaths);
|
||||
for (let path of actualPath) {
|
||||
let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
|
||||
let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
|
||||
assert.deepEqual(a, e);
|
||||
function newRequest(basePath: string, ignored: string[] = []): IWatcherRequest {
|
||||
return { path: basePath, excludes: ignored };
|
||||
}
|
||||
}
|
||||
|
||||
function sort(changes: IDiskFileChange[]) {
|
||||
return changes.sort((c1, c2) => {
|
||||
return c1.path.localeCompare(c2.path);
|
||||
});
|
||||
}
|
||||
|
||||
function wait(time: number) {
|
||||
return new Delayer<void>(time).trigger(() => { });
|
||||
}
|
||||
|
||||
async function assertFileEvents(actuals: IDiskFileChange[], expected: IDiskFileChange[]) {
|
||||
let repeats = 40;
|
||||
while ((actuals.length < expected.length) && repeats-- > 0) {
|
||||
await wait(50);
|
||||
function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) {
|
||||
const requests = inputPaths.map(path => newRequest(path));
|
||||
const actual = normalizeRoots(requests);
|
||||
assert.deepEqual(Object.keys(actual).sort(), expectedPaths);
|
||||
}
|
||||
|
||||
function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) {
|
||||
const actual = normalizeRoots(inputRequests);
|
||||
const actualPath = Object.keys(actual).sort();
|
||||
const expectedPaths = Object.keys(expectedRequests).sort();
|
||||
assert.deepEqual(actualPath, expectedPaths);
|
||||
for (let path of actualPath) {
|
||||
let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
|
||||
let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
|
||||
assert.deepEqual(a, e);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(sort(actuals), sort(expected));
|
||||
actuals.length = 0;
|
||||
}
|
||||
|
||||
suite('Chokidar normalizeRoots', () => {
|
||||
test('should not impacts roots that don\'t overlap', () => {
|
||||
if (platform.isWindows) {
|
||||
assertNormalizedRootPath(['C:\\a'], ['C:\\a']);
|
||||
@@ -115,208 +94,3 @@ suite('Chokidar normalizeRoots', () => {
|
||||
assertNormalizedRequests([r2, r3], { [p2]: [r2, r3] });
|
||||
});
|
||||
});
|
||||
|
||||
suite.skip('Chokidar watching', () => {
|
||||
const tmpdir = os.tmpdir();
|
||||
const testDir = path.join(tmpdir, 'chokidartest-' + Date.now());
|
||||
const aFolder = path.join(testDir, 'a');
|
||||
const bFolder = path.join(testDir, 'b');
|
||||
const b2Folder = path.join(bFolder, 'b2');
|
||||
|
||||
const service = new ChokidarWatcherService();
|
||||
const result: IDiskFileChange[] = [];
|
||||
let error: string | null = null;
|
||||
|
||||
suiteSetup(async () => {
|
||||
await pfs.mkdirp(testDir);
|
||||
await pfs.mkdirp(aFolder);
|
||||
await pfs.mkdirp(bFolder);
|
||||
await pfs.mkdirp(b2Folder);
|
||||
|
||||
const opts = { verboseLogging: false, pollingInterval: 200 };
|
||||
service.watch(opts)(e => {
|
||||
if (Array.isArray(e)) {
|
||||
result.push(...e);
|
||||
}
|
||||
});
|
||||
service.onLogMessage(msg => {
|
||||
if (msg.type === 'error') {
|
||||
console.log('set error', msg.message);
|
||||
error = msg.message;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
suiteTeardown(async () => {
|
||||
await pfs.rimraf(testDir, pfs.RimRafMode.MOVE);
|
||||
await service.stop();
|
||||
});
|
||||
|
||||
setup(() => {
|
||||
result.length = 0;
|
||||
assert.equal(error, null);
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
assert.equal(error, null);
|
||||
});
|
||||
|
||||
test('simple file operations, single root, no ignore', async () => {
|
||||
let request: IWatcherRequest = { path: testDir, excludes: [] };
|
||||
service.setRoots([request]);
|
||||
await wait(300);
|
||||
|
||||
assert.equal(service.wacherCount, 1);
|
||||
|
||||
// create a file
|
||||
let testFilePath = path.join(testDir, 'file.txt');
|
||||
await pfs.writeFile(testFilePath, '');
|
||||
await assertFileEvents(result, [{ path: testFilePath, type: FileChangeType.ADDED }]);
|
||||
|
||||
// modify a file
|
||||
await pfs.writeFile(testFilePath, 'Hello');
|
||||
await assertFileEvents(result, [{ path: testFilePath, type: FileChangeType.UPDATED }]);
|
||||
|
||||
// create a folder
|
||||
let testFolderPath = path.join(testDir, 'newFolder');
|
||||
await pfs.mkdirp(testFolderPath);
|
||||
// copy a file
|
||||
let copiedFilePath = path.join(testFolderPath, 'file2.txt');
|
||||
await pfs.copy(testFilePath, copiedFilePath);
|
||||
await assertFileEvents(result, [{ path: copiedFilePath, type: FileChangeType.ADDED }, { path: testFolderPath, type: FileChangeType.ADDED }]);
|
||||
|
||||
// delete a file
|
||||
await pfs.rimraf(copiedFilePath, pfs.RimRafMode.MOVE);
|
||||
let renamedFilePath = path.join(testFolderPath, 'file3.txt');
|
||||
// move a file
|
||||
await pfs.rename(testFilePath, renamedFilePath);
|
||||
await assertFileEvents(result, [{ path: copiedFilePath, type: FileChangeType.DELETED }, { path: testFilePath, type: FileChangeType.DELETED }, { path: renamedFilePath, type: FileChangeType.ADDED }]);
|
||||
|
||||
// delete a folder
|
||||
await pfs.rimraf(testFolderPath, pfs.RimRafMode.MOVE);
|
||||
await assertFileEvents(result, [{ path: testFolderPath, type: FileChangeType.DELETED }, { path: renamedFilePath, type: FileChangeType.DELETED }]);
|
||||
});
|
||||
|
||||
test('simple file operations, ignore', async () => {
|
||||
let request: IWatcherRequest = { path: testDir, excludes: ['**/b/**', '**/*.js', '.git/**'] };
|
||||
service.setRoots([request]);
|
||||
await wait(300);
|
||||
|
||||
assert.equal(service.wacherCount, 1);
|
||||
|
||||
// create various ignored files
|
||||
let file1 = path.join(bFolder, 'file1.txt'); // hidden
|
||||
await pfs.writeFile(file1, 'Hello');
|
||||
let file2 = path.join(b2Folder, 'file2.txt'); // hidden
|
||||
await pfs.writeFile(file2, 'Hello');
|
||||
let folder1 = path.join(bFolder, 'folder1'); // hidden
|
||||
await pfs.mkdirp(folder1);
|
||||
let folder2 = path.join(aFolder, 'b'); // hidden
|
||||
await pfs.mkdirp(folder2);
|
||||
let folder3 = path.join(testDir, '.git'); // hidden
|
||||
await pfs.mkdirp(folder3);
|
||||
let folder4 = path.join(testDir, '.git1');
|
||||
await pfs.mkdirp(folder4);
|
||||
let folder5 = path.join(aFolder, '.git');
|
||||
await pfs.mkdirp(folder5);
|
||||
let file3 = path.join(aFolder, 'file3.js'); // hidden
|
||||
await pfs.writeFile(file3, 'var x;');
|
||||
let file4 = path.join(aFolder, 'file4.txt');
|
||||
await pfs.writeFile(file4, 'Hello');
|
||||
await assertFileEvents(result, [{ path: file4, type: FileChangeType.ADDED }, { path: folder4, type: FileChangeType.ADDED }, { path: folder5, type: FileChangeType.ADDED }]);
|
||||
|
||||
// move some files
|
||||
let movedFile1 = path.join(folder2, 'file1.txt'); // from ignored to ignored
|
||||
await pfs.rename(file1, movedFile1);
|
||||
let movedFile2 = path.join(aFolder, 'file2.txt'); // from ignored to visible
|
||||
await pfs.rename(file2, movedFile2);
|
||||
let movedFile3 = path.join(aFolder, 'file3.txt'); // from ignored file ext to visible
|
||||
await pfs.rename(file3, movedFile3);
|
||||
await assertFileEvents(result, [{ path: movedFile2, type: FileChangeType.ADDED }, { path: movedFile3, type: FileChangeType.ADDED }]);
|
||||
|
||||
// delete all files
|
||||
await pfs.rimraf(movedFile1); // hidden
|
||||
await pfs.rimraf(movedFile2, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(movedFile3, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(folder1); // hidden
|
||||
await pfs.rimraf(folder2); // hidden
|
||||
await pfs.rimraf(folder3); // hidden
|
||||
await pfs.rimraf(folder4, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(folder5, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(file4, pfs.RimRafMode.MOVE);
|
||||
await assertFileEvents(result, [{ path: movedFile2, type: FileChangeType.DELETED }, { path: movedFile3, type: FileChangeType.DELETED }, { path: file4, type: FileChangeType.DELETED }, { path: folder4, type: FileChangeType.DELETED }, { path: folder5, type: FileChangeType.DELETED }]);
|
||||
});
|
||||
|
||||
test('simple file operations, multiple roots', async () => {
|
||||
let request1: IWatcherRequest = { path: aFolder, excludes: ['**/*.js'] };
|
||||
let request2: IWatcherRequest = { path: b2Folder, excludes: ['**/*.ts'] };
|
||||
service.setRoots([request1, request2]);
|
||||
await wait(300);
|
||||
|
||||
assert.equal(service.wacherCount, 2);
|
||||
|
||||
// create some files
|
||||
let folderPath1 = path.join(aFolder, 'folder1');
|
||||
await pfs.mkdirp(folderPath1);
|
||||
let filePath1 = path.join(folderPath1, 'file1.json');
|
||||
await pfs.writeFile(filePath1, '');
|
||||
let filePath2 = path.join(folderPath1, 'file2.js'); // filtered
|
||||
await pfs.writeFile(filePath2, '');
|
||||
let folderPath2 = path.join(b2Folder, 'folder2');
|
||||
await pfs.mkdirp(folderPath2);
|
||||
let filePath3 = path.join(folderPath2, 'file3.ts'); // filtered
|
||||
await pfs.writeFile(filePath3, '');
|
||||
let filePath4 = path.join(testDir, 'file4.json'); // outside roots
|
||||
await pfs.writeFile(filePath4, '');
|
||||
|
||||
await assertFileEvents(result, [{ path: folderPath1, type: FileChangeType.ADDED }, { path: filePath1, type: FileChangeType.ADDED }, { path: folderPath2, type: FileChangeType.ADDED }]);
|
||||
|
||||
// change roots
|
||||
let request3: IWatcherRequest = { path: aFolder, excludes: ['**/*.json'] };
|
||||
service.setRoots([request3]);
|
||||
await wait(300);
|
||||
|
||||
assert.equal(service.wacherCount, 1);
|
||||
|
||||
// delete all
|
||||
await pfs.rimraf(folderPath1, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(folderPath2, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(filePath4, pfs.RimRafMode.MOVE);
|
||||
|
||||
await assertFileEvents(result, [{ path: folderPath1, type: FileChangeType.DELETED }, { path: filePath2, type: FileChangeType.DELETED }]);
|
||||
});
|
||||
|
||||
test('simple file operations, nested roots', async () => {
|
||||
let request1: IWatcherRequest = { path: testDir, excludes: ['**/b2/**'] };
|
||||
let request2: IWatcherRequest = { path: bFolder, excludes: ['**/b3/**'] };
|
||||
service.setRoots([request1, request2]);
|
||||
await wait(300);
|
||||
|
||||
assert.equal(service.wacherCount, 1);
|
||||
|
||||
// create files
|
||||
let filePath1 = path.join(bFolder, 'file1.xml'); // visible by both
|
||||
await pfs.writeFile(filePath1, '');
|
||||
let filePath2 = path.join(b2Folder, 'file2.xml'); // filtered by root1, but visible by root2
|
||||
await pfs.writeFile(filePath2, '');
|
||||
let folderPath1 = path.join(b2Folder, 'b3'); // filtered
|
||||
await pfs.mkdirp(folderPath1);
|
||||
let filePath3 = path.join(folderPath1, 'file3.xml'); // filtered
|
||||
await pfs.writeFile(filePath3, '');
|
||||
|
||||
await assertFileEvents(result, [{ path: filePath1, type: FileChangeType.ADDED }, { path: filePath2, type: FileChangeType.ADDED }]);
|
||||
|
||||
let renamedFilePath2 = path.join(folderPath1, 'file2.xml');
|
||||
// move a file
|
||||
await pfs.rename(filePath2, renamedFilePath2);
|
||||
await assertFileEvents(result, [{ path: filePath2, type: FileChangeType.DELETED }]);
|
||||
|
||||
// delete all
|
||||
await pfs.rimraf(folderPath1, pfs.RimRafMode.MOVE);
|
||||
await pfs.rimraf(filePath1, pfs.RimRafMode.MOVE);
|
||||
|
||||
await assertFileEvents(result, [{ path: filePath1, type: FileChangeType.DELETED }]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -14,12 +14,18 @@ export interface IWatcherRequest {
|
||||
export interface IWatcherOptions {
|
||||
pollingInterval?: number;
|
||||
usePolling?: boolean;
|
||||
verboseLogging?: boolean;
|
||||
}
|
||||
|
||||
export interface IWatcherService {
|
||||
watch(options: IWatcherOptions): Event<IDiskFileChange[]>;
|
||||
|
||||
readonly onDidChangeFile: Event<IDiskFileChange[]>;
|
||||
readonly onDidLogMessage: Event<ILogMessage>;
|
||||
|
||||
init(options: IWatcherOptions): Promise<void>;
|
||||
|
||||
setRoots(roots: IWatcherRequest[]): Promise<void>;
|
||||
setVerboseLogging(enabled: boolean): Promise<void>;
|
||||
onLogMessage: Event<ILogMessage>;
|
||||
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { WatcherChannel } from 'vs/platform/files/node/watcher/unix/watcherIpc';
|
||||
import { ChokidarWatcherService } from 'vs/platform/files/node/watcher/unix/chokidarWatcherService';
|
||||
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
|
||||
|
||||
const server = new Server('watcher');
|
||||
const service = new ChokidarWatcherService();
|
||||
const channel = new WatcherChannel(service);
|
||||
server.registerChannel('watcher', channel);
|
||||
server.registerChannel('watcher', createChannelReceiver(service));
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IWatcherRequest, IWatcherService, IWatcherOptions } from './watcher';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
|
||||
|
||||
export class WatcherChannel implements IServerChannel {
|
||||
|
||||
constructor(private service: IWatcherService) { }
|
||||
|
||||
listen(_: unknown, event: string, arg?: any): Event<any> {
|
||||
switch (event) {
|
||||
case 'watch': return this.service.watch(arg);
|
||||
case 'onLogMessage': return this.service.onLogMessage;
|
||||
}
|
||||
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(_: unknown, command: string, arg?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'setRoots': return this.service.setRoots(arg);
|
||||
case 'setVerboseLogging': return this.service.setVerboseLogging(arg);
|
||||
case 'stop': return this.service.stop();
|
||||
}
|
||||
|
||||
throw new Error(`Call not found: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class WatcherChannelClient implements IWatcherService {
|
||||
|
||||
constructor(private channel: IChannel) { }
|
||||
|
||||
watch(options: IWatcherOptions): Event<IDiskFileChange[]> {
|
||||
return this.channel.listen('watch', options);
|
||||
}
|
||||
|
||||
setVerboseLogging(enable: boolean): Promise<void> {
|
||||
return this.channel.call('setVerboseLogging', enable);
|
||||
}
|
||||
|
||||
get onLogMessage(): Event<ILogMessage> {
|
||||
return this.channel.listen('onLogMessage');
|
||||
}
|
||||
|
||||
setRoots(roots: IWatcherRequest[]): Promise<void> {
|
||||
return this.channel.call('setRoots', roots);
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
return this.channel.call('stop');
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,20 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { createChannelSender, 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 { WatcherChannelClient } from 'vs/platform/files/node/watcher/unix/watcherIpc';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWatcherRequest, IWatcherOptions } from 'vs/platform/files/node/watcher/unix/watcher';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { IWatcherRequest, IWatcherOptions, IWatcherService } from 'vs/platform/files/node/watcher/unix/watcher';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
|
||||
export class FileWatcher extends Disposable {
|
||||
|
||||
private static readonly MAX_RESTARTS = 5;
|
||||
|
||||
private isDisposed: boolean;
|
||||
private restartCounter: number;
|
||||
private service: WatcherChannelClient | undefined;
|
||||
private service: IWatcherService | undefined;
|
||||
|
||||
constructor(
|
||||
private folders: IWatcherRequest[],
|
||||
@@ -35,7 +35,7 @@ export class FileWatcher extends Disposable {
|
||||
|
||||
private startWatching(): void {
|
||||
const client = this._register(new Client(
|
||||
getPathFromAmdModule(require, 'bootstrap-fork'),
|
||||
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
|
||||
{
|
||||
serverName: 'File Watcher (chokidar)',
|
||||
args: ['--type=watcherService'],
|
||||
@@ -62,14 +62,11 @@ export class FileWatcher extends Disposable {
|
||||
}));
|
||||
|
||||
// Initialize watcher
|
||||
const channel = getNextTickChannel(client.getChannel('watcher'));
|
||||
this.service = new WatcherChannelClient(channel);
|
||||
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
|
||||
this.service.init({ ...this.watcherOptions, verboseLogging: this.verboseLogging });
|
||||
|
||||
this.service.setVerboseLogging(this.verboseLogging);
|
||||
|
||||
this._register(this.service.watch(this.watcherOptions)(e => !this.isDisposed && this.onDidFilesChange(e)));
|
||||
|
||||
this._register(this.service.onLogMessage(m => this.onLogMessage(m)));
|
||||
this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e)));
|
||||
this._register(this.service.onDidLogMessage(m => this.onLogMessage(m)));
|
||||
|
||||
// Start watching
|
||||
this.service.setRoots(this.folders);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Native File Watching for Windows using C# FileSystemWatcher
|
||||
|
||||
- Repository: https://github.com/Microsoft/vscode-filewatcher-windows
|
||||
- Repository: https://github.com/microsoft/vscode-filewatcher-windows
|
||||
|
||||
# Build
|
||||
|
||||
- Build in "Release" config
|
||||
- Copy CodeHelper.exe over into this folder
|
||||
- Copy CodeHelper.exe over into this folder
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FileChangeType } from 'vs/platform/files/common/files';
|
||||
import * as decoder from 'vs/base/node/decoder';
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
|
||||
export class OutOfProcessWin32FolderWatcher {
|
||||
|
||||
@@ -50,7 +50,7 @@ export class OutOfProcessWin32FolderWatcher {
|
||||
args.push('-verbose');
|
||||
}
|
||||
|
||||
this.handle = cp.spawn(getPathFromAmdModule(require, 'vs/platform/files/node/watcher/win32/CodeHelper.exe'), args);
|
||||
this.handle = cp.spawn(FileAccess.asFileUri('vs/platform/files/node/watcher/win32/CodeHelper.exe', require).fsPath, args);
|
||||
|
||||
const stdoutLineDecoder = new decoder.LineDecoder();
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { posix } from 'vs/base/common/path';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { FileOperation, FileOperationEvent } from 'vs/platform/files/common/files';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IIndexedDBFileSystemProvider, IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, INDEXEDDB_USERDATA_OBJECT_STORE } from 'vs/platform/files/browser/indexedDBFileSystemProvider';
|
||||
import { assertIsDefined } from 'vs/base/common/types';
|
||||
|
||||
// FileService doesn't work with \ leading a path. Windows join swaps /'s for \'s,
|
||||
// making /-style absolute paths fail isAbsolute checks.
|
||||
const join = posix.join;
|
||||
|
||||
suite('IndexedDB File Service', function () {
|
||||
|
||||
const logSchema = 'logs';
|
||||
|
||||
let service: FileService;
|
||||
let logFileProvider: IIndexedDBFileSystemProvider;
|
||||
let userdataFileProvider: IIndexedDBFileSystemProvider;
|
||||
const testDir = '/';
|
||||
|
||||
const makeLogfileURI = (path: string) => URI.from({ scheme: logSchema, path });
|
||||
const makeUserdataURI = (path: string) => URI.from({ scheme: Schemas.userData, path });
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
setup(async () => {
|
||||
const logService = new NullLogService();
|
||||
|
||||
service = new FileService(logService);
|
||||
disposables.add(service);
|
||||
|
||||
logFileProvider = assertIsDefined(await new IndexedDB().createFileSystemProvider(Schemas.file, INDEXEDDB_LOGS_OBJECT_STORE));
|
||||
disposables.add(service.registerProvider(logSchema, logFileProvider));
|
||||
disposables.add(logFileProvider);
|
||||
|
||||
userdataFileProvider = assertIsDefined(await new IndexedDB().createFileSystemProvider(logSchema, INDEXEDDB_USERDATA_OBJECT_STORE));
|
||||
disposables.add(service.registerProvider(Schemas.userData, userdataFileProvider));
|
||||
disposables.add(userdataFileProvider);
|
||||
});
|
||||
|
||||
teardown(async () => {
|
||||
disposables.clear();
|
||||
|
||||
await logFileProvider.delete(makeLogfileURI(testDir), { recursive: true, useTrash: false });
|
||||
await userdataFileProvider.delete(makeUserdataURI(testDir), { recursive: true, useTrash: false });
|
||||
});
|
||||
|
||||
test('createFolder', async () => {
|
||||
let event: FileOperationEvent | undefined;
|
||||
disposables.add(service.onDidRunOperation(e => event = e));
|
||||
|
||||
const parent = await service.resolve(makeUserdataURI(testDir));
|
||||
|
||||
const newFolderResource = makeUserdataURI(join(parent.resource.path, 'newFolder'));
|
||||
|
||||
assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 0);
|
||||
const newFolder = await service.createFolder(newFolderResource);
|
||||
assert.equal(newFolder.name, 'newFolder');
|
||||
// Invalid.. dirs dont exist in our IDBFSB.
|
||||
// assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 1);
|
||||
|
||||
assert.ok(event);
|
||||
assert.equal(event!.resource.path, newFolderResource.path);
|
||||
assert.equal(event!.operation, FileOperation.CREATE);
|
||||
assert.equal(event!.target!.resource.path, newFolderResource.path);
|
||||
assert.equal(event!.target!.isDirectory, true);
|
||||
});
|
||||
});
|
||||
@@ -9,38 +9,108 @@ import { isEqual, isEqualOrParent } from 'vs/base/common/extpath';
|
||||
import { FileChangeType, FileChangesEvent, isParent } from 'vs/platform/files/common/files';
|
||||
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
|
||||
import { toResource } from 'vs/base/test/common/utils';
|
||||
import { extUri } from 'vs/base/common/resources';
|
||||
|
||||
suite('Files', () => {
|
||||
|
||||
test('FileChangesEvent', function () {
|
||||
let changes = [
|
||||
test('FileChangesEvent - basics', function () {
|
||||
const changes = [
|
||||
{ resource: toResource.call(this, '/foo/updated.txt'), type: FileChangeType.UPDATED },
|
||||
{ resource: toResource.call(this, '/foo/otherupdated.txt'), type: FileChangeType.UPDATED },
|
||||
{ resource: toResource.call(this, '/added.txt'), type: FileChangeType.ADDED },
|
||||
{ resource: toResource.call(this, '/bar/deleted.txt'), type: FileChangeType.DELETED },
|
||||
{ resource: toResource.call(this, '/bar/folder'), type: FileChangeType.DELETED }
|
||||
{ resource: toResource.call(this, '/bar/folder'), type: FileChangeType.DELETED },
|
||||
{ resource: toResource.call(this, '/BAR/FOLDER'), type: FileChangeType.DELETED }
|
||||
];
|
||||
|
||||
let r1 = new FileChangesEvent(changes, extUri);
|
||||
for (const ignorePathCasing of [false, true]) {
|
||||
const event = new FileChangesEvent(changes, ignorePathCasing);
|
||||
|
||||
assert(!r1.contains(toResource.call(this, '/foo'), FileChangeType.UPDATED));
|
||||
assert(r1.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED));
|
||||
assert(!r1.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.ADDED));
|
||||
assert(!r1.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.DELETED));
|
||||
assert(!event.contains(toResource.call(this, '/foo'), FileChangeType.UPDATED));
|
||||
assert(event.affects(toResource.call(this, '/foo'), FileChangeType.UPDATED));
|
||||
assert(event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED));
|
||||
assert(event.affects(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED));
|
||||
assert(event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED, FileChangeType.ADDED));
|
||||
assert(event.affects(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED, FileChangeType.ADDED));
|
||||
assert(event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED, FileChangeType.ADDED, FileChangeType.DELETED));
|
||||
assert(!event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.ADDED, FileChangeType.DELETED));
|
||||
assert(!event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.ADDED));
|
||||
assert(!event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.DELETED));
|
||||
assert(!event.affects(toResource.call(this, '/foo/updated.txt'), FileChangeType.DELETED));
|
||||
|
||||
assert(r1.contains(toResource.call(this, '/bar/folder'), FileChangeType.DELETED));
|
||||
assert(r1.contains(toResource.call(this, '/bar/folder/somefile'), FileChangeType.DELETED));
|
||||
assert(r1.contains(toResource.call(this, '/bar/folder/somefile/test.txt'), FileChangeType.DELETED));
|
||||
assert(!r1.contains(toResource.call(this, '/bar/folder2/somefile'), FileChangeType.DELETED));
|
||||
assert(event.contains(toResource.call(this, '/bar/folder'), FileChangeType.DELETED));
|
||||
assert(event.contains(toResource.call(this, '/BAR/FOLDER'), FileChangeType.DELETED));
|
||||
assert(event.affects(toResource.call(this, '/BAR'), FileChangeType.DELETED));
|
||||
if (ignorePathCasing) {
|
||||
assert(event.contains(toResource.call(this, '/BAR/folder'), FileChangeType.DELETED));
|
||||
assert(event.affects(toResource.call(this, '/bar'), FileChangeType.DELETED));
|
||||
} else {
|
||||
assert(!event.contains(toResource.call(this, '/BAR/folder'), FileChangeType.DELETED));
|
||||
assert(event.affects(toResource.call(this, '/bar'), FileChangeType.DELETED));
|
||||
}
|
||||
assert(event.contains(toResource.call(this, '/bar/folder/somefile'), FileChangeType.DELETED));
|
||||
assert(event.contains(toResource.call(this, '/bar/folder/somefile/test.txt'), FileChangeType.DELETED));
|
||||
assert(event.contains(toResource.call(this, '/BAR/FOLDER/somefile/test.txt'), FileChangeType.DELETED));
|
||||
if (ignorePathCasing) {
|
||||
assert(event.contains(toResource.call(this, '/BAR/folder/somefile/test.txt'), FileChangeType.DELETED));
|
||||
} else {
|
||||
assert(!event.contains(toResource.call(this, '/BAR/folder/somefile/test.txt'), FileChangeType.DELETED));
|
||||
}
|
||||
assert(!event.contains(toResource.call(this, '/bar/folder2/somefile'), FileChangeType.DELETED));
|
||||
|
||||
assert.strictEqual(5, r1.changes.length);
|
||||
assert.strictEqual(1, r1.getAdded().length);
|
||||
assert.strictEqual(true, r1.gotAdded());
|
||||
assert.strictEqual(2, r1.getUpdated().length);
|
||||
assert.strictEqual(true, r1.gotUpdated());
|
||||
assert.strictEqual(2, r1.getDeleted().length);
|
||||
assert.strictEqual(true, r1.gotDeleted());
|
||||
assert.strictEqual(6, event.changes.length);
|
||||
assert.strictEqual(1, event.getAdded().length);
|
||||
assert.strictEqual(true, event.gotAdded());
|
||||
assert.strictEqual(2, event.getUpdated().length);
|
||||
assert.strictEqual(true, event.gotUpdated());
|
||||
assert.strictEqual(ignorePathCasing ? 2 : 3, event.getDeleted().length);
|
||||
assert.strictEqual(true, event.gotDeleted());
|
||||
}
|
||||
});
|
||||
|
||||
test('FileChangesEvent - supports multiple changes on file tree', function () {
|
||||
for (const type of [FileChangeType.ADDED, FileChangeType.UPDATED, FileChangeType.DELETED]) {
|
||||
const changes = [
|
||||
{ resource: toResource.call(this, '/foo/bar/updated.txt'), type },
|
||||
{ resource: toResource.call(this, '/foo/bar/otherupdated.txt'), type },
|
||||
{ resource: toResource.call(this, '/foo/bar'), type },
|
||||
{ resource: toResource.call(this, '/foo'), type },
|
||||
{ resource: toResource.call(this, '/bar'), type },
|
||||
{ resource: toResource.call(this, '/bar/foo'), type },
|
||||
{ resource: toResource.call(this, '/bar/foo/updated.txt'), type },
|
||||
{ resource: toResource.call(this, '/bar/foo/otherupdated.txt'), type }
|
||||
];
|
||||
|
||||
for (const ignorePathCasing of [false, true]) {
|
||||
const event = new FileChangesEvent(changes, ignorePathCasing);
|
||||
|
||||
for (const change of changes) {
|
||||
assert(event.contains(change.resource, type));
|
||||
assert(event.affects(change.resource, type));
|
||||
}
|
||||
|
||||
assert(event.affects(toResource.call(this, '/foo'), type));
|
||||
assert(event.affects(toResource.call(this, '/bar'), type));
|
||||
assert(event.affects(toResource.call(this, '/'), type));
|
||||
assert(!event.affects(toResource.call(this, '/foobar'), type));
|
||||
|
||||
assert(!event.contains(toResource.call(this, '/some/foo/bar'), type));
|
||||
assert(!event.affects(toResource.call(this, '/some/foo/bar'), type));
|
||||
assert(!event.contains(toResource.call(this, '/some/bar'), type));
|
||||
assert(!event.affects(toResource.call(this, '/some/bar'), type));
|
||||
|
||||
switch (type) {
|
||||
case FileChangeType.ADDED:
|
||||
assert.strictEqual(8, event.getAdded().length);
|
||||
break;
|
||||
case FileChangeType.UPDATED:
|
||||
assert.strictEqual(8, event.getUpdated().length);
|
||||
break;
|
||||
case FileChangeType.DELETED:
|
||||
assert.strictEqual(8, event.getDeleted().length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function testIsEqual(testMethod: (pA: string, pB: string, ignoreCase: boolean) => boolean): void {
|
||||
|
||||
@@ -93,7 +93,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
|
||||
const res = await super.stat(resource);
|
||||
|
||||
if (this.invalidStatSize) {
|
||||
res.size = String(res.size) as any; // for https://github.com/Microsoft/vscode/issues/72909
|
||||
res.size = String(res.size) as any; // for https://github.com/microsoft/vscode/issues/72909
|
||||
} else if (this.smallStatSize) {
|
||||
res.size = 1;
|
||||
}
|
||||
@@ -1488,7 +1488,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
|
||||
assert.equal(fileProvider.totalBytesRead, 0);
|
||||
}
|
||||
|
||||
test('readFile - FILE_NOT_MODIFIED_SINCE does not fire wrongly - https://github.com/Microsoft/vscode/issues/72909', async () => {
|
||||
test('readFile - FILE_NOT_MODIFIED_SINCE does not fire wrongly - https://github.com/microsoft/vscode/issues/72909', async () => {
|
||||
fileProvider.setInvalidStatSize(true);
|
||||
|
||||
const resource = URI.file(join(testDir, 'index.html'));
|
||||
|
||||
@@ -9,10 +9,9 @@ import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files
|
||||
import { URI as uri } from 'vs/base/common/uri';
|
||||
import { IDiskFileChange, normalizeFileChanges, toFileChanges } from 'vs/platform/files/node/watcher/watcher';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { ExtUri } from 'vs/base/common/resources';
|
||||
|
||||
function toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent {
|
||||
return new FileChangesEvent(toFileChanges(changes), new ExtUri(() => !platform.isLinux));
|
||||
return new FileChangesEvent(toFileChanges(changes), !platform.isLinux);
|
||||
}
|
||||
|
||||
class TestFileWatcher {
|
||||
|
||||
Reference in New Issue
Block a user