Skip to content

Commit

Permalink
sandbox - implement non-recursive watch support (#132282)
Browse files Browse the repository at this point in the history
  • Loading branch information
bpasero committed Oct 20, 2021
1 parent 2ae9352 commit 2f795d3
Show file tree
Hide file tree
Showing 8 changed files with 431 additions and 249 deletions.
4 changes: 2 additions & 2 deletions src/vs/code/electron-main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,8 +564,8 @@ export class CodeApplication extends Disposable {
// Local Files
const diskFileSystemProvider = this.fileService.getProvider(Schemas.file);
assertType(diskFileSystemProvider instanceof DiskFileSystemProvider);
const fileSystemProviderChannel = new DiskFileSystemProviderChannel(diskFileSystemProvider);
mainProcessElectronServer.registerChannel('localFiles', fileSystemProviderChannel);
const fileSystemProviderChannel = new DiskFileSystemProviderChannel(diskFileSystemProvider, this.logService);
mainProcessElectronServer.registerChannel('diskFiles', fileSystemProviderChannel);

// Configuration
mainProcessElectronServer.registerChannel(UserConfigurationFileServiceId, ProxyChannel.fromService(new UserConfigurationFileService(this.environmentMainService, this.fileService, this.logService)));
Expand Down
142 changes: 142 additions & 0 deletions src/vs/platform/files/common/diskFileSystemProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { insert } from 'vs/base/common/arrays';
import { ThrottledDelayer } from 'vs/base/common/async';
import { Emitter } from 'vs/base/common/event';
import { combinedDisposable, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { normalize } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import { IFileChange, IWatchOptions } from 'vs/platform/files/common/files';
import { IDiskFileChange, ILogMessage, IWatchRequest, toFileChanges, WatcherService } from 'vs/platform/files/common/watcher';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';

export interface IWatcherOptions {
pollingInterval?: number;
usePolling: boolean | string[];
}

export abstract class AbstractDiskFileSystemProvider extends Disposable {

constructor(
protected readonly logService: ILogService
) {
super();
}

//#region File Watching

protected readonly _onDidErrorOccur = this._register(new Emitter<string>());
readonly onDidErrorOccur = this._onDidErrorOccur.event;

protected readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event;

private recursiveWatcher: WatcherService | undefined;
private readonly recursiveFoldersToWatch: IWatchRequest[] = [];
private recursiveWatchRequestDelayer = this._register(new ThrottledDelayer<void>(0));

watch(resource: URI, opts: IWatchOptions): IDisposable {
if (opts.recursive) {
return this.watchRecursive(resource, opts);
}

return this.watchNonRecursive(resource);
}

private watchRecursive(resource: URI, opts: IWatchOptions): IDisposable {

// Add to list of folders to watch recursively
const folderToWatch: IWatchRequest = { path: this.toFilePath(resource), excludes: opts.excludes };
const remove = insert(this.recursiveFoldersToWatch, folderToWatch);

// Trigger update
this.refreshRecursiveWatchers();

return toDisposable(() => {

// Remove from list of folders to watch recursively
remove();

// Trigger update
this.refreshRecursiveWatchers();
});
}

private refreshRecursiveWatchers(): void {

// Buffer requests for recursive watching to decide on right watcher
// that supports potentially watching more than one folder at once
this.recursiveWatchRequestDelayer.trigger(async () => {
this.doRefreshRecursiveWatchers();
});
}

private doRefreshRecursiveWatchers(): void {

// Reuse existing
if (this.recursiveWatcher) {
this.recursiveWatcher.watch(this.recursiveFoldersToWatch);
}

// Otherwise, create new if we have folders to watch
else if (this.recursiveFoldersToWatch.length > 0) {
this.recursiveWatcher = this._register(this.createRecursiveWatcher(
this.recursiveFoldersToWatch,
changes => this._onDidChangeFile.fire(toFileChanges(changes)),
msg => this.onWatcherLogMessage(msg),
this.logService.getLevel() === LogLevel.Trace
));

// Apply log levels dynamically
this._register(this.logService.onDidChangeLogLevel(() => {
this.recursiveWatcher?.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);
}));
}
}

protected abstract createRecursiveWatcher(
folders: IWatchRequest[],
onChange: (changes: IDiskFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean
): WatcherService;

private watchNonRecursive(resource: URI): IDisposable {
const watcherService = this.createNonRecursiveWatcher(
this.toFilePath(resource),
changes => this._onDidChangeFile.fire(toFileChanges(changes)),
msg => this.onWatcherLogMessage(msg),
this.logService.getLevel() === LogLevel.Trace
);

const logLevelListener = this.logService.onDidChangeLogLevel(() => {
watcherService.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);
});

return combinedDisposable(watcherService, logLevelListener);
}

private onWatcherLogMessage(msg: ILogMessage): void {
if (msg.type === 'error') {
this._onDidErrorOccur.fire(msg.message);
}

this.logService[msg.type](msg.message);
}

protected abstract createNonRecursiveWatcher(
path: string,
onChange: (changes: IDiskFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean
): IDisposable & { setVerboseLogging: (verboseLogging: boolean) => void };

protected toFilePath(resource: URI): string {
return normalize(resource.fsPath);
}

//#endregion
}
140 changes: 84 additions & 56 deletions src/vs/platform/files/common/ipcFileSystemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,19 @@ interface IFileChangeDto {
type: FileChangeType;
}

/**
* An abstract file system provider that delegates all calls to a provided
* `IChannel` via IPC communication.
*/
export abstract class IPCFileSystemProvider extends Disposable implements
IFileSystemProviderWithFileReadWriteCapability,
IFileSystemProviderWithOpenReadWriteCloseCapability,
IFileSystemProviderWithFileReadStreamCapability,
IFileSystemProviderWithFileFolderCopyCapability {

private readonly session: string = generateUuid();
constructor(private readonly channel: IChannel) {
super();

private readonly _onDidChange = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile = this._onDidChange.event;
this.registerFileChangeListeners();
}

private _onDidWatchErrorOccur = this._register(new Emitter<string>());
readonly onDidErrorOccur = this._onDidWatchErrorOccur.event;
//#region File Capabilities

private readonly _onDidChangeCapabilities = this._register(new Emitter<void>());
readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event;
Expand All @@ -48,59 +44,31 @@ export abstract class IPCFileSystemProvider extends Disposable implements
| FileSystemProviderCapabilities.FileWriteUnlock;
get capabilities(): FileSystemProviderCapabilities { return this._capabilities; }

constructor(private readonly channel: IChannel) {
super();

this.registerListeners();
}

private registerListeners(): void {
this._register(this.channel.listen<IFileChangeDto[] | string>('filechange', [this.session])(eventsOrError => {
if (Array.isArray(eventsOrError)) {
const events = eventsOrError;
this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type })));
} else {
const error = eventsOrError;
this._onDidWatchErrorOccur.fire(error);
}
}));
}

protected setCaseSensitive(isCaseSensitive: boolean) {
if (isCaseSensitive) {
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
} else {
this._capabilities &= ~FileSystemProviderCapabilities.PathCaseSensitive;
}

this._onDidChangeCapabilities.fire(undefined);
this._onDidChangeCapabilities.fire();
}

// --- forwarding calls
//#endregion

//#region File Metadata Resolving

stat(resource: URI): Promise<IStat> {
return this.channel.call('stat', [resource]);
}

open(resource: URI, opts: FileOpenOptions): Promise<number> {
return this.channel.call('open', [resource, opts]);
}

close(fd: number): Promise<void> {
return this.channel.call('close', [fd]);
readdir(resource: URI): Promise<[string, FileType][]> {
return this.channel.call('readdir', [resource]);
}

async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]);
//#endregion

// copy back the data that was written into the buffer on the remote
// side. we need to do this because buffers are not referenced by
// pointer, but only by value and as such cannot be directly written
// to from the other process.
data.set(bytes.buffer.slice(0, bytesRead), offset);

return bytesRead;
}
//#region File Reading/Writing

async readFile(resource: URI): Promise<Uint8Array> {
const buff = <VSBuffer>await this.channel.call('readFile', [resource]);
Expand Down Expand Up @@ -160,24 +128,44 @@ export abstract class IPCFileSystemProvider extends Disposable implements
return stream;
}

write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]);
}

writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
return this.channel.call('writeFile', [resource, VSBuffer.wrap(content), opts]);
}

delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
return this.channel.call('delete', [resource, opts]);
open(resource: URI, opts: FileOpenOptions): Promise<number> {
return this.channel.call('open', [resource, opts]);
}

close(fd: number): Promise<void> {
return this.channel.call('close', [fd]);
}

async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
const [bytes, bytesRead]: [VSBuffer, number] = await this.channel.call('read', [fd, pos, length]);

// copy back the data that was written into the buffer on the remote
// side. we need to do this because buffers are not referenced by
// pointer, but only by value and as such cannot be directly written
// to from the other process.
data.set(bytes.buffer.slice(0, bytesRead), offset);

return bytesRead;
}

write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
return this.channel.call('write', [fd, pos, VSBuffer.wrap(data), offset, length]);
}

//#endregion

//#region Move/Copy/Delete/Create Folder

mkdir(resource: URI): Promise<void> {
return this.channel.call('mkdir', [resource]);
}

readdir(resource: URI): Promise<[string, FileType][]> {
return this.channel.call('readdir', [resource]);
delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
return this.channel.call('delete', [resource, opts]);
}

rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
Expand All @@ -188,10 +176,50 @@ export abstract class IPCFileSystemProvider extends Disposable implements
return this.channel.call('copy', [resource, target, opts]);
}

//#endregion

//#region File Watching

private readonly _onDidChange = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile = this._onDidChange.event;

private _onDidErrorOccur = this._register(new Emitter<string>());
readonly onDidErrorOccur = this._onDidErrorOccur.event;

// The contract for file watching via remote is to identify us
// via a unique but readonly session ID. Since the remote is
// managing potentially many watchers from different clients,
// this helps the server to properly partition events to the right
// clients.
private readonly sessionId = generateUuid();

private registerFileChangeListeners(): void {

// The contract for file changes is that there is one listener
// for both events and errors from the watcher. So we need to
// unwrap the event from the remote and emit through the proper
// emitter.
this._register(this.channel.listen<IFileChangeDto[] | string>('filechange', [this.sessionId])(eventsOrError => {
if (Array.isArray(eventsOrError)) {
const events = eventsOrError;
this._onDidChange.fire(events.map(event => ({ resource: URI.revive(event.resource), type: event.type })));
} else {
const error = eventsOrError;
this._onDidErrorOccur.fire(error);
}
}));
}

watch(resource: URI, opts: IWatchOptions): IDisposable {
const req = Math.random();
this.channel.call('watch', [this.session, req, resource, opts]);

return toDisposable(() => this.channel.call('unwatch', [this.session, req]));
// Generate a request UUID to correlate the watcher
// back to us when we ask to dispose the watcher later.
const req = generateUuid();

this.channel.call('watch', [this.sessionId, req, resource, opts]);

return toDisposable(() => this.channel.call('unwatch', [this.sessionId, req]));
}

//#endregion
}
Loading

0 comments on commit 2f795d3

Please sign in to comment.