提交 50657045 编写于 作者: D Daniel Imms 提交者: GitHub

Merge pull request #28948 from Microsoft/tyriar/nsfw

Add opt-in multi-root aware file watcher based on Axosoft/nsfw
......@@ -302,6 +302,11 @@
"from": "normalize-path@>=2.0.1 <3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.0.1.tgz"
},
"nsfw": {
"version": "1.0.15",
"from": "nsfw@1.0.15",
"resolved": "https://registry.npmjs.org/nsfw/-/nsfw-1.0.15.tgz"
},
"object.omit": {
"version": "2.0.0",
"from": "object.omit@>=2.0.0 <3.0.0",
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'nsfw' {
interface NsfwWatcher {
start(): void;
stop(): void;
}
interface NsfwWatchingPromise {
then(): void;
}
interface NsfwStartWatchingPromise {
then(fn: (watcher: NsfwWatcher) => void): NsfwWatchingPromise;
}
interface NsfwEvent {
action: number;
directory: string;
file?: string;
newFile?: string;
oldFile?: string;
}
interface NsfwFunction {
(dir: string, eventHandler: (events: NsfwEvent[]) => void, options?: any): NsfwStartWatchingPromise;
actions: {
CREATED: number;
DELETED: number;
MODIFIED: number;
RENAMED: number;
}
}
var nsfw: NsfwFunction;
export = nsfw;
}
......@@ -538,6 +538,7 @@ export interface IFilesConfiguration {
autoSaveDelay: number;
eol: string;
hotExit: string;
useNsfwFileWatcher: boolean;
};
}
......
......@@ -24,6 +24,7 @@ exports.collectModules = function (excludes) {
createModuleDescription('vs/workbench/services/search/node/searchApp', []),
createModuleDescription('vs/workbench/services/search/node/worker/searchWorkerApp', []),
createModuleDescription('vs/workbench/services/files/node/watcher/unix/watcherApp', []),
createModuleDescription('vs/workbench/services/files/node/watcher/nsfw/watcherApp', []),
createModuleDescription('vs/workbench/node/extensionHostProcess', []),
......
......@@ -275,6 +275,11 @@ configurationRegistry.registerConfiguration({
],
'description': nls.localize('hotExit', "Controls whether unsaved files are remembered between sessions, allowing the save prompt when exiting the editor to be skipped.", HotExitConfiguration.ON_EXIT, HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE)
},
'files.useNsfwFileWatcher': {
'type': 'boolean',
'default': false,
'description': nls.localize('useNsfwFileWatcher', "Use the new experimental file watcher utilizing the nsfw library.")
},
'files.defaultLanguage': {
'type': 'string',
'description': nls.localize('defaultLanguage', "The default language mode that is assigned to new files.")
......
......@@ -19,6 +19,7 @@ import { FileService } from 'vs/workbench/services/files/node/fileService';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import { parseArgs } from 'vs/platform/environment/node/argv';
import { RawTextSource } from 'vs/editor/common/model/textSource';
import { TestContextService } from 'vs/workbench/test/workbenchTestServices';
class TestEnvironmentService extends EnvironmentService {
......@@ -47,7 +48,7 @@ const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', crypto.cre
class TestBackupFileService extends BackupFileService {
constructor(workspace: Uri, backupHome: string, workspacesJsonPath: string) {
const fileService = new FileService(workspace.fsPath, { disableWatcher: true });
const fileService = new FileService(workspace.fsPath, { disableWatcher: true }, new TestContextService());
super(workspaceBackupPath, fileService);
}
......
......@@ -124,7 +124,7 @@ suite('ConfigurationEditingService', () => {
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IModeService, ModeServiceImpl);
instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl));
instantiationService.stub(IFileService, new FileService(workspaceDir, { disableWatcher: true }));
instantiationService.stub(IFileService, instantiationService.createInstance(FileService, workspaceDir, { disableWatcher: true }));
instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService));
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
......
......@@ -85,11 +85,12 @@ export class FileService implements IFileService {
encodingOverride,
watcherIgnoredPatterns,
verboseLogging: environmentService.verbose,
useNsfwFileWatcher: configuration.files.useNsfwFileWatcher
};
// create service
const workspace = this.contextService.getWorkspace();
this.raw = new NodeFileService(workspace ? workspace.resource.fsPath : void 0, fileServiceConfig);
this.raw = new NodeFileService(workspace ? workspace.resource.fsPath : void 0, fileServiceConfig, contextService);
// Listeners
this.registerListeners();
......
......@@ -26,6 +26,7 @@ import uri from 'vs/base/common/uri';
import nls = require('vs/nls');
import { isWindows, isLinux } from 'vs/base/common/platform';
import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import pfs = require('vs/base/node/pfs');
import encoding = require('vs/base/node/encoding');
......@@ -35,6 +36,7 @@ import { FileWatcher as UnixWatcherService } from 'vs/workbench/services/files/n
import { FileWatcher as WindowsWatcherService } from 'vs/workbench/services/files/node/watcher/win32/watcherService';
import { toFileChangesEvent, normalize, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import Event, { Emitter } from 'vs/base/common/event';
import { FileWatcher as NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/watcherService';
export interface IEncodingOverride {
resource: uri;
......@@ -51,6 +53,7 @@ export interface IFileServiceOptions {
watcherIgnoredPatterns?: string[];
disableWatcher?: boolean;
verboseLogging?: boolean;
useNsfwFileWatcher?: boolean;
}
function etag(stat: fs.Stats): string;
......@@ -90,7 +93,11 @@ export class FileService implements IFileService {
private fileChangesWatchDelayer: ThrottledDelayer<void>;
private undeliveredRawFileChangesEvents: IRawFileChange[];
constructor(basePath: string, options: IFileServiceOptions) {
constructor(
basePath: string,
options: IFileServiceOptions,
private contextService: IWorkspaceContextService
) {
this.toDispose = [];
this.basePath = basePath ? paths.normalize(basePath) : void 0;
......@@ -120,10 +127,14 @@ export class FileService implements IFileService {
}
if (this.basePath && !this.options.disableWatcher) {
if (isWindows) {
this.setupWin32WorkspaceWatching();
if (this.options.useNsfwFileWatcher) {
this.setupNsfwWorkspceWatching();
} else {
this.setupUnixWorkspaceWatching();
if (isWindows) {
this.setupWin32WorkspaceWatching();
} else {
this.setupUnixWorkspaceWatching();
}
}
}
......@@ -154,6 +165,10 @@ export class FileService implements IFileService {
this.toDispose.push(toDisposable(new UnixWatcherService(this.basePath, this.options.watcherIgnoredPatterns, e => this._onFileChanges.fire(e), this.options.errorLogger, this.options.verboseLogging).startWatching()));
}
private setupNsfwWorkspceWatching(): void {
this.toDispose.push(toDisposable(new NsfwWatcherService(this.basePath, this.options.watcherIgnoredPatterns, e => this._onFileChanges.fire(e), this.options.errorLogger, this.options.verboseLogging, this.contextService).startWatching()));
}
public resolveFile(resource: uri, options?: IResolveFileOptions): TPromise<IFileStat> {
return this.resolve(resource, options);
}
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as glob from 'vs/base/common/glob';
import * as path from 'path';
import * as watcher from 'vs/workbench/services/files/node/watcher/common';
import * as nsfw from 'nsfw';
import { IWatcherService, IWatcherRequest } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
import { TPromise } from 'vs/base/common/winjs.base';
import { ThrottledDelayer } from 'vs/base/common/async';
import { FileChangeType } from 'vs/platform/files/common/files';
const nsfwActionToRawChangeType: { [key: number]: number } = [];
nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED;
nsfwActionToRawChangeType[nsfw.actions.MODIFIED] = FileChangeType.UPDATED;
nsfwActionToRawChangeType[nsfw.actions.DELETED] = FileChangeType.DELETED;
interface IPathWatcher {
watcher?: {
start(): void;
stop(): void;
};
}
export class NsfwWatcherService implements IWatcherService {
private static FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
private _pathWatchers: { [watchPath: string]: IPathWatcher } = {};
public watch(request: IWatcherRequest): TPromise<void> {
if (request.verboseLogging) {
console.log('request', request);
}
let undeliveredFileEvents: watcher.IRawFileChange[] = [];
const fileEventDelayer = new ThrottledDelayer(NsfwWatcherService.FS_EVENT_DELAY);
console.log('starting to watch ' + request.basePath);
this._pathWatchers[request.basePath] = {};
const promise = new TPromise<void>((c, e, p) => {
nsfw(request.basePath, events => {
for (let i = 0; i < events.length; i++) {
const e = events[i];
// Logging
if (request.verboseLogging) {
const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile) + ' -> ' + e.newFile : path.join(e.directory, e.file);
console.log(e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]', logPath);
}
// Convert nsfw event to IRawFileChange and add to queue
let absolutePath: string;
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, request.ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath });
}
absolutePath = path.join(e.directory, e.newFile);
if (!this._isPathIgnored(absolutePath, request.ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath });
}
} else {
absolutePath = path.join(e.directory, e.file);
if (!this._isPathIgnored(absolutePath, request.ignored)) {
undeliveredFileEvents.push({
type: nsfwActionToRawChangeType[e.action],
path: absolutePath
});
}
}
}
// Delay and send buffer
fileEventDelayer.trigger(() => {
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
// Broadcast to clients normalized
const res = watcher.normalize(events);
p(res);
// Logging
if (request.verboseLogging) {
res.forEach(r => {
console.log(' >> normalized', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path);
});
}
return TPromise.as(null);
});
}).then(watcher => {
console.log('watcher ready ' + request.basePath);
this._pathWatchers[request.basePath].watcher = watcher;
return watcher.start();
});
});
return promise;
}
public setRoots(roots: string[]): TPromise<void> {
const rootsToStartWatching = roots.filter(r => !(r in this._pathWatchers));
const rootsToStopWatching = Object.keys(this._pathWatchers).filter(r => roots.indexOf(r) === -1);
// TODO: Don't watch inner folders
// TODO: Move verboseLogging to constructor
// Logging
if (true) {
console.log(`Set watch roots: start: [${rootsToStartWatching.join(',')}], stop: [${rootsToStopWatching.join(',')}]`);
}
const promises: TPromise<void>[] = [];
if (rootsToStartWatching.length) {
rootsToStartWatching.forEach(root => {
promises.push(this.watch({
basePath: root,
ignored: [],
// TODO: Inherit from initial request
verboseLogging: true
}));
});
}
if (rootsToStopWatching.length) {
rootsToStopWatching.forEach(root => {
this._pathWatchers[root].watcher.stop();
delete this._pathWatchers[root];
});
}
// TODO: Don't watch sub-folders of folders
return TPromise.join(promises).then(() => void 0);
}
private _isPathIgnored(absolutePath: string, ignored: string[]): boolean {
return ignored && ignored.some(ignore => glob.match(ignore, absolutePath));
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
export interface IWatcherRequest {
basePath: string;
ignored: string[];
verboseLogging: boolean;
}
export interface IWatcherService {
setRoots(roots: string[]): TPromise<void>;
watch(request: IWatcherRequest): TPromise<void>;
}
export interface IFileWatcher {
startWatching(): () => void;
addFolder(folder: string): void;
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { WatcherChannel } from 'vs/workbench/services/files/node/watcher/nsfw/watcherIpc';
import { NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService';
const server = new Server();
const service = new NsfwWatcherService();
const channel = new WatcherChannel(service);
server.registerChannel('watcher', channel);
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IWatcherRequest, IWatcherService } from './watcher';
export interface IWatcherChannel extends IChannel {
call(command: 'watch', request: IWatcherRequest): TPromise<void>;
call(command: string, arg: any): TPromise<any>;
}
export class WatcherChannel implements IWatcherChannel {
constructor(private service: IWatcherService) { }
call(command: string, arg: any): TPromise<any> {
switch (command) {
case 'setRoots': return this.service.setRoots(arg);
case 'watch': return this.service.watch(arg);
}
return undefined;
}
}
export class WatcherChannelClient implements IWatcherService {
constructor(private channel: IWatcherChannel) { }
setRoots(roots: string[]): TPromise<void> {
return this.channel.call('setRoots', roots);
}
watch(request: IWatcherRequest): TPromise<void> {
return this.channel.call('watch', request);
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import uri from 'vs/base/common/uri';
import { toFileChangesEvent, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { IWatcherChannel, WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/nsfw/watcherIpc';
import { FileChangesEvent } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
export class FileWatcher {
private static MAX_RESTARTS = 5;
private isDisposed: boolean;
private restartCounter: number;
constructor(
private basePath: string,
private ignored: string[],
private onFileChanges: (changes: FileChangesEvent) => void,
private errorLogger: (msg: string) => void,
private verboseLogging: boolean,
private contextService: IWorkspaceContextService
) {
this.isDisposed = false;
this.restartCounter = 0;
}
public startWatching(): () => void {
const args = ['--type=watcherService'];
const client = new Client(
uri.parse(require.toUrl('bootstrap')).fsPath,
{
serverName: 'Watcher',
args,
env: {
AMD_ENTRYPOINT: 'vs/workbench/services/files/node/watcher/nsfw/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: this.verboseLogging
}
}
);
const channel = getNextTickChannel(client.getChannel<IWatcherChannel>('watcher'));
const service = new WatcherChannelClient(channel);
// Start watching
service.watch({ basePath: this.basePath, ignored: this.ignored, verboseLogging: this.verboseLogging }).then(null, (err) => {
if (!(err instanceof Error && err.name === 'Canceled' && err.message === 'Canceled')) {
return TPromise.wrapError(err); // the service lib uses the promise cancel error to indicate the process died, we do not want to bubble this up
}
return undefined;
}, (events: IRawFileChange[]) => this.onRawFileEvents(events)).done(() => {
// our watcher app should never be completed because it keeps on watching. being in here indicates
// that the watcher process died and we want to restart it here. we only do it a max number of times
if (!this.isDisposed) {
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
this.errorLogger('[FileWatcher] terminated unexpectedly and is restarted again...');
this.restartCounter++;
this.startWatching();
} else {
this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}, this.errorLogger);
this.contextService.onDidChangeWorkspaceRoots(roots => {
service.setRoots(roots.map(r => r.fsPath));
console.log('roots changed', roots);
});
return () => {
client.dispose();
this.isDisposed = true;
};
}
private onRawFileEvents(events: IRawFileChange[]): void {
// Emit through broadcast service
if (events.length > 0) {
this.onFileChanges(toFileChangesEvent(events));
}
}
}
\ No newline at end of file
......@@ -63,6 +63,7 @@ export class FileWatcher {
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
this.errorLogger('[FileWatcher] terminated unexpectedly and is restarted again...');
this.restartCounter++;
// TODO: What do we do for multi-root here?
this.startWatching();
} else {
this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!');
......
......@@ -19,6 +19,7 @@ import extfs = require('vs/base/node/extfs');
import encodingLib = require('vs/base/node/encoding');
import utils = require('vs/workbench/services/files/test/node/utils');
import { onError } from 'vs/base/test/common/utils';
import { TestContextService } from "vs/workbench/test/workbenchTestServices";
suite('FileService', () => {
let service: FileService;
......@@ -35,7 +36,7 @@ suite('FileService', () => {
return onError(error, done);
}
service = new FileService(testDir, { disableWatcher: true });
service = new FileService(testDir, { disableWatcher: true }, new TestContextService());
done();
});
});
......@@ -735,7 +736,7 @@ suite('FileService', () => {
encoding: 'windows1252',
encodingOverride: encodingOverride,
disableWatcher: true
});
}, new TestContextService());
_service.resolveContent(uri.file(path.join(testDir, 'index.html'))).done(c => {
assert.equal(c.encoding, 'windows1252');
......@@ -761,7 +762,7 @@ suite('FileService', () => {
let _service = new FileService(_testDir, {
disableWatcher: true
});
}, new TestContextService());
extfs.copy(_sourceDir, _testDir, () => {
fs.readFile(resource.fsPath, (error, data) => {
......
......@@ -73,7 +73,7 @@ suite('Keybindings Editing', () => {
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IModeService, ModeServiceImpl);
instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl));
instantiationService.stub(IFileService, new FileService(testDir, { disableWatcher: true }));
instantiationService.stub(IFileService, new FileService(testDir, { disableWatcher: true }, new TestContextService()));
instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService));
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService));
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册