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:
Karl Burtram
2021-02-09 16:15:05 -08:00
committed by GitHub
parent 6f192f9af5
commit ce612a3d96
1929 changed files with 68012 additions and 34564 deletions

View File

@@ -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.

View File

@@ -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 });
}
}

View File

@@ -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();

View File

@@ -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>;
}
}

View File

@@ -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));

View File

@@ -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');
}
}

View File

@@ -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);

View File

@@ -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 });
}
}

View File

@@ -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 }]);
});
});

View File

@@ -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>;
}
}

View File

@@ -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));

View File

@@ -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');
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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();