Enable parcel-watcher as new default watcher (#132483)

This commit is contained in:
Benjamin Pasero
2021-10-11 11:16:56 +02:00
parent 5dcc08052c
commit 85c5eb7281
24 changed files with 1215 additions and 186 deletions
+24 -24
View File
@@ -132,7 +132,7 @@
"restrictions": [
"vs/nls",
"**/vs/base/{common,node}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -170,7 +170,7 @@
"vs/nls",
"**/vs/base/{common,node}/**",
"**/vs/base/parts/*/{common,node}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -189,7 +189,7 @@
"vs/css!./**/*",
"**/vs/base/{common,browser,node,electron-sandbox,electron-browser}/**",
"**/vs/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -198,7 +198,7 @@
"vs/nls",
"**/vs/base/{common,node,electron-main}/**",
"**/vs/base/parts/*/{common,node,electron-main}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -255,7 +255,7 @@
"**/vs/base/{common,node}/**",
"**/vs/base/parts/*/{common,node}/**",
"**/vs/platform/*/{common,node}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -276,7 +276,7 @@
"**/vs/base/{common,browser,node,electron-sandbox,electron-browser}/**",
"**/vs/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**",
"**/vs/platform/*/{common,browser,node,electron-sandbox,electron-browser}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -286,7 +286,7 @@
"**/vs/base/{common,node,electron-main}/**",
"**/vs/base/parts/*/{common,node,electron-main}/**",
"**/vs/platform/*/{common,node,electron-main}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -513,7 +513,7 @@
"**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**",
"**/vs/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**",
"**/vs/workbench/services/*/{common,browser,node,electron-sandbox,electron-browser}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -528,7 +528,7 @@
"vs/workbench/contrib/files/browser/editors/fileEditorInput",
"**/vs/workbench/services/**",
"**/vs/workbench/test/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -592,7 +592,7 @@
"**/vs/workbench/{common,node}/**",
"**/vs/workbench/api/{common,node}/**",
"**/vs/workbench/services/**/{common,node}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -623,7 +623,7 @@
"**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**",
"**/vs/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**",
"**/vs/workbench/services/**/{common,browser,node,electron-sandbox,electron-browser}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -742,7 +742,7 @@
"**/vs/workbench/api/{common,node}/**",
"**/vs/workbench/services/**/{common,node}/**",
"**/vs/workbench/contrib/**/{common,node}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -775,7 +775,7 @@
"**/vs/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**",
"**/vs/workbench/services/**/{common,browser,node,electron-sandbox,electron-browser}/**",
"**/vs/workbench/contrib/**/{common,browser,node,electron-sandbox,electron-browser}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -798,7 +798,7 @@
"**/vs/base/parts/**/{common,node}/**",
"**/vs/platform/**/{common,node}/**",
"**/vs/code/**/{common,node}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -810,7 +810,7 @@
"**/vs/base/parts/**/{common,browser,node,electron-sandbox,electron-browser}/**",
"**/vs/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**",
"**/vs/code/**/{common,browser,node,electron-sandbox,electron-browser}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -821,7 +821,7 @@
"**/vs/base/parts/**/{common,node,electron-main}/**",
"**/vs/platform/**/{common,node,electron-main}/**",
"**/vs/code/**/{common,node,electron-main}/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -833,7 +833,7 @@
"**/vs/platform/**/{common,node}/**",
"**/vs/workbench/**/{common,node}/**",
"**/vs/server/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -904,28 +904,28 @@
"target": "**/test/smoke/**",
"restrictions": [
"**/test/smoke/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
"target": "**/test/automation/**",
"restrictions": [
"**/test/automation/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
"target": "**/test/integration/**",
"restrictions": [
"**/test/integration/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
"target": "**/test/monaco/**",
"restrictions": [
"**/test/monaco/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
@@ -943,21 +943,21 @@
"target": "**/{node,electron-browser,electron-main}/**/*.test.ts",
"restrictions": [
"**/vs/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
"target": "**/{node,electron-browser,electron-main}/**/test/**",
"restrictions": [
"**/vs/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
"target": "**/test/{node,electron-browser,electron-main}/**",
"restrictions": [
"**/vs/**",
"@vscode/*", "*" // node modules
"@vscode/*", "@parcel/*", "*" // node modules
]
},
{
+6
View File
@@ -86,6 +86,12 @@ vscode-nsfw/src/**
vscode-nsfw/includes/**
!vscode-nsfw/build/Release/*.node
@parcel/watcher/binding.gyp
@parcel/watcher/build/**
@parcel/watcher/prebuilds/**
@parcel/watcher/src/**
!@parcel/watcher/build/Release/*.node
vsda/build/**
vsda/ci/**
vsda/src/**
@@ -91,7 +91,7 @@ steps:
# Set compiler toolchain
export CC=$PWD/.build/CR_Clang/bin/clang
export CXX=$PWD/.build/CR_Clang/bin/clang++
export CXXFLAGS="-nostdinc++ -D_LIBCPP_HAS_NO_VENDOR_AVAILABILITY_ANNOTATIONS -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit"
export CXXFLAGS="-nostdinc++ -D_LIBCPP_HAS_NO_VENDOR_AVAILABILITY_ANNOTATIONS -D__NO_INLINE__ -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit"
export LDFLAGS="-stdlib=libc++ -fuse-ld=lld -flto=thin -fsplit-lto-unit -L$PWD/.build/libcxx-objects -lc++abi"
fi
+1
View File
@@ -59,6 +59,7 @@
},
"dependencies": {
"@microsoft/applicationinsights-web": "^2.6.4",
"@parcel/watcher": "2.0.0",
"@vscode/sqlite3": "4.0.12",
"@vscode/vscode-languagedetection": "1.0.21",
"applicationinsights": "1.0.8",
+1
View File
@@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@microsoft/applicationinsights-web": "^2.6.4",
"@parcel/watcher": "2.0.0",
"@vscode/vscode-languagedetection": "1.0.21",
"applicationinsights": "1.0.8",
"chokidar": "3.5.1",
+18
View File
@@ -83,6 +83,14 @@
resolved "https://registry.yarnpkg.com/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.4.tgz#40e1c0ad20743fcee1604a7df2c57faf0aa1af87"
integrity sha512-Ot53G927ykMF8cQ3/zq4foZtdk+Tt1YpX7aUTHxBU7UHNdkEiBvBfZSq+rnlUmKCJ19VatwPG4mNzvcGpBj4og==
"@parcel/watcher@2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.0.tgz#ebe992a4838b35c3da9a568eb95a71cb26ddf551"
integrity sha512-ByalKmRRXNNAhwZ0X1r0XeIhh1jG8zgdlvjgHk9ZV3YxiersEGNQkwew+RfqJbIL4gOJfvC2ey6lg5kaeRainw==
dependencies:
node-addon-api "^3.2.1"
node-gyp-build "^4.3.0"
"@tootallnate/once@1", "@tootallnate/once@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@@ -444,11 +452,21 @@ node-addon-api@^3.0.2:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239"
integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==
node-addon-api@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
node-addon-api@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.2.0.tgz#117cbb5a959dff0992e1c586ae0393573e4d2a87"
integrity sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q==
node-gyp-build@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3"
integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==
node-pty@0.11.0-beta7:
version "0.11.0-beta7"
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.11.0-beta7.tgz#aed0888b5032d96c54d8473455e6adfae3bbebbe"
@@ -42,6 +42,11 @@ suite('Native Modules (all platforms)', () => {
assert.ok(typeof nsfWatcher === 'function', testErrorMessage('nsfw'));
});
test('parcel', async () => {
const parcelWatcher = await import('@parcel/watcher');
assert.ok(typeof parcelWatcher.subscribe === 'function', testErrorMessage('parcel'));
});
test('sqlite3', async () => {
const sqlite3 = await import('@vscode/sqlite3');
assert.ok(typeof sqlite3.Database === 'function', testErrorMessage('@vscode/sqlite3'));
@@ -22,6 +22,7 @@ import { createFileSystemProviderError, FileDeleteOptions, FileOpenOptions, File
import { readFileIntoStream } from 'vs/platform/files/common/io';
import { FileWatcher as NodeJSWatcherService } from 'vs/platform/files/node/watcher/nodejs/watcherService';
import { FileWatcher as NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcherService';
import { FileWatcher as ParcelWatcherService } from 'vs/platform/files/node/watcher/parcel/watcherService';
import { FileWatcher as UnixWatcherService } from 'vs/platform/files/node/watcher/unix/watcherService';
import { IDiskFileChange, ILogMessage, IWatchRequest, toFileChanges } from 'vs/platform/files/node/watcher/watcher';
import { FileWatcher as WindowsWatcherService } from 'vs/platform/files/node/watcher/win32/watcherService';
@@ -36,7 +37,7 @@ export interface IWatcherOptions {
export interface IDiskFileSystemProviderOptions {
bufferSize?: number;
watcher?: IWatcherOptions;
enableLegacyRecursiveWatcher?: boolean;
legacyWatcher?: string;
}
export class DiskFileSystemProvider extends Disposable implements
@@ -532,7 +533,7 @@ export class DiskFileSystemProvider extends Disposable implements
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event;
private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService | undefined;
private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService | ParcelWatcherService | undefined;
private readonly recursiveFoldersToWatch: IWatchRequest[] = [];
private recursiveWatchRequestDelayer = this._register(new ThrottledDelayer<void>(0));
@@ -577,7 +578,7 @@ export class DiskFileSystemProvider extends Disposable implements
private doRefreshRecursiveWatchers(): void {
// Reuse existing
if (this.recursiveWatcher instanceof NsfwWatcherService) {
if (this.recursiveWatcher instanceof NsfwWatcherService || this.recursiveWatcher instanceof ParcelWatcherService) {
this.recursiveWatcher.watch(this.recursiveFoldersToWatch);
}
@@ -597,7 +598,7 @@ export class DiskFileSystemProvider extends Disposable implements
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean,
watcherOptions?: IWatcherOptions
): WindowsWatcherService | UnixWatcherService | NsfwWatcherService
): WindowsWatcherService | UnixWatcherService | NsfwWatcherService | ParcelWatcherService
};
let watcherOptions: IWatcherOptions | undefined = undefined;
@@ -608,30 +609,27 @@ export class DiskFileSystemProvider extends Disposable implements
watcherOptions = this.options?.watcher;
}
// can use efficient watcher
else {
// Conditionally fallback to our legacy file watcher:
// - If provided as option from the outside (i.e. via settings)
// - Linux: until we support ignore patterns (unless insiders)
let enableLegacyWatcher: boolean;
if (this.options?.enableLegacyRecursiveWatcher) {
enableLegacyWatcher = true;
let enableLegacyWatcher = false;
if (this.options?.legacyWatcher === 'on' || this.options?.legacyWatcher === 'off') {
enableLegacyWatcher = this.options.legacyWatcher === 'on'; // setting always wins
} else {
enableLegacyWatcher = product.quality === 'stable' && isLinux;
}
// Legacy Watcher
if (enableLegacyWatcher && this.recursiveFoldersToWatch.length === 1) {
if (isWindows) {
watcherImpl = WindowsWatcherService;
} else {
watcherImpl = UnixWatcherService;
if (product.quality === 'stable') {
// in stable use legacy for single folder workspaces
// TODO@bpasero remove me eventually
enableLegacyWatcher = this.recursiveFoldersToWatch.length === 1;
}
}
// Standard Watcher
else {
watcherImpl = NsfwWatcherService;
if (enableLegacyWatcher) {
if (isLinux) {
watcherImpl = UnixWatcherService;
} else {
watcherImpl = NsfwWatcherService;
}
} else {
watcherImpl = ParcelWatcherService;
}
}
@@ -172,7 +172,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
// Log the raw event before normalization or checking for ignore patterns
if (this.verboseLogging) {
const logPath = event.action === nsfw.actions.RENAMED ? `${join(event.directory, event.oldFile || '')} -> ${event.newFile}` : join(event.directory, event.file || '');
this.log(`${event.action === nsfw.actions.CREATED ? '[CREATED]' : event.action === nsfw.actions.DELETED ? '[DELETED]' : event.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`);
this.log(`${event.action === nsfw.actions.CREATED ? '[ADDED]' : event.action === nsfw.actions.DELETED ? '[DELETED]' : event.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`);
}
// Rename: convert into DELETE & ADD
@@ -311,7 +311,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
else {
const handled = this.onUnexpectedError(msg, watcher);
if (!handled) {
this.error(`Unexpected error: ${msg} (ESHUTDOWN)`, watcher);
this.error(`Unexpected error: ${msg} (EUNKNOWN)`, watcher);
}
}
}
@@ -0,0 +1,567 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as parcelWatcher from '@parcel/watcher';
import { existsSync } from 'fs';
import { RunOnceScheduler } from 'vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { Emitter } from 'vs/base/common/event';
import { isEqualOrParent } from 'vs/base/common/extpath';
import { parse, ParsedPattern } from 'vs/base/common/glob';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { TernarySearchTree } from 'vs/base/common/map';
import { normalizeNFC } from 'vs/base/common/normalization';
import { dirname, isAbsolute, join, normalize, sep } from 'vs/base/common/path';
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { rtrim } from 'vs/base/common/strings';
import { realcaseSync, realpathSync } from 'vs/base/node/extpath';
import { watchFolder } from 'vs/base/node/watcher';
import { FileChangeType } from 'vs/platform/files/common/files';
import { IWatcherService } from 'vs/platform/files/node/watcher/parcel/watcher';
import { IDiskFileChange, ILogMessage, normalizeFileChanges, IWatchRequest } from 'vs/platform/files/node/watcher/watcher';
export interface IWatcher extends IDisposable {
/**
* The Parcel watcher instance is resolved when the watching has started.
*/
readonly instance: Promise<parcelWatcher.AsyncSubscription | undefined>;
/**
* The watch request associated to the watcher.
*/
request: IWatchRequest;
/**
* Associated ignored patterns for the watcher that can be updated.
*/
ignored: ParsedPattern[];
/**
* How often this watcher has been restarted in case of an unexpected
* shutdown.
*/
restarts: number;
/**
* The cancellation token associated with the lifecycle of the watcher.
*/
token: CancellationToken;
/**
* Stops and disposes the watcher. Same as `dispose` but allows to await
* the watcher getting unsubscribed.
*/
stop(): Promise<void>;
}
export class ParcelWatcherService extends Disposable implements IWatcherService {
private static readonly MAX_RESTARTS = 5; // number of restarts we allow before giving up in case of unexpected errors
private static readonly MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE = new Map<parcelWatcher.EventType, number>(
[
['create', FileChangeType.ADDED],
['update', FileChangeType.UPDATED],
['delete', FileChangeType.DELETED]
]
);
private static readonly GLOB_MARKERS = {
Star: '*',
GlobStar: '**',
GlobStarPathStartPosix: '**/',
GlobStarPathEndPosix: '/**',
StarPathEndPosix: '/*',
GlobStarPathStartWindows: '**\\',
GlobStarPathEndWindows: '\\**'
};
private static readonly PARCEL_WATCHER_BACKEND = isWindows ? 'windows' : isLinux ? 'inotify' : 'fs-events';
private readonly _onDidChangeFile = this._register(new Emitter<IDiskFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event;
private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>());
readonly onDidLogMessage = this._onDidLogMessage.event;
protected readonly watchers = new Map<string, IWatcher>();
private verboseLogging = false;
private enospcErrorLogged = false;
constructor() {
super();
this.registerListeners();
}
private registerListeners(): void {
// Error handling on process
process.on('uncaughtException', error => this.onUnexpectedError(error));
process.on('unhandledRejection', error => this.onUnexpectedError(error));
}
async watch(requests: IWatchRequest[]): Promise<void> {
// Figure out duplicates to remove from the requests
const normalizedRequests = this.normalizeRequests(requests);
// Gather paths that we should start watching
const requestsToStartWatching = normalizedRequests.filter(request => {
const watcher = this.watchers.get(request.path);
if (!watcher) {
return true; // not yet watching that path
}
// Re-watch path if excludes have changed
return watcher.request.excludes !== request.excludes;
});
// Gather paths that we should stop watching
const pathsToStopWatching = Array.from(this.watchers.values()).filter(({ request }) => {
return !normalizedRequests.find(normalizedRequest => normalizedRequest.path === request.path && normalizedRequest.excludes === request.excludes);
}).map(({ request }) => request.path);
// Logging
this.debug(`Request to start watching: ${requestsToStartWatching.map(request => `${request.path} (excludes: ${request.excludes})`).join(',')}`);
this.debug(`Request to stop watching: ${pathsToStopWatching.join(',')}`);
// Stop watching as instructed
for (const pathToStopWatching of pathsToStopWatching) {
await this.stopWatching(pathToStopWatching);
}
// Start watching as instructed
for (const request of requestsToStartWatching) {
this.startWatching(request);
}
}
private toExcludePatterns(excludes: string[] | undefined): ParsedPattern[] {
return Array.isArray(excludes) ? excludes.map(exclude => parse(exclude)) : [];
}
protected toExcludePaths(path: string, excludes: string[] | undefined): string[] | undefined {
if (!Array.isArray(excludes)) {
return undefined;
}
const excludePaths = new Set<string>();
// Parcel watcher currently does not support glob patterns
// for native exclusions. As long as that is the case, try
// to convert exclude patterns into absolute paths that the
// watcher supports natively to reduce the overhead at the
// level of the file watcher as much as possible.
// Refs: https://github.com/parcel-bundler/watcher/issues/64
for (const exclude of excludes) {
const isGlob = exclude.includes(ParcelWatcherService.GLOB_MARKERS.Star);
// Glob pattern: check for typical patterns and convert
let normalizedExclude: string | undefined = undefined;
if (isGlob) {
// Examples: **
if (exclude === ParcelWatcherService.GLOB_MARKERS.GlobStar) {
normalizedExclude = path;
}
// Examples:
// - **/node_modules/**
// - **/.git/objects/**
// - **/build-folder
// - output/**
else {
const startsWithGlobStar = exclude.startsWith(ParcelWatcherService.GLOB_MARKERS.GlobStarPathStartPosix) || exclude.startsWith(ParcelWatcherService.GLOB_MARKERS.GlobStarPathStartWindows);
const endsWithGlobStar = exclude.endsWith(ParcelWatcherService.GLOB_MARKERS.GlobStarPathEndPosix) || exclude.endsWith(ParcelWatcherService.GLOB_MARKERS.GlobStarPathEndWindows);
if (startsWithGlobStar || endsWithGlobStar) {
if (startsWithGlobStar && endsWithGlobStar) {
normalizedExclude = exclude.substring(ParcelWatcherService.GLOB_MARKERS.GlobStarPathStartPosix.length, exclude.length - ParcelWatcherService.GLOB_MARKERS.GlobStarPathEndPosix.length);
} else if (startsWithGlobStar) {
normalizedExclude = exclude.substring(ParcelWatcherService.GLOB_MARKERS.GlobStarPathStartPosix.length);
} else {
normalizedExclude = exclude.substring(0, exclude.length - ParcelWatcherService.GLOB_MARKERS.GlobStarPathEndPosix.length);
}
}
// Support even more glob patterns on Linux where we know
// that each folder requires a file handle to watch.
// Examples:
// - node_modules/* (full form: **/node_modules/*/**)
if (isLinux && normalizedExclude) {
const endsWithStar = normalizedExclude?.endsWith(ParcelWatcherService.GLOB_MARKERS.StarPathEndPosix);
if (endsWithStar) {
normalizedExclude = normalizedExclude.substring(0, normalizedExclude.length - ParcelWatcherService.GLOB_MARKERS.StarPathEndPosix.length);
}
}
}
}
// Not a glob pattern, take as is
else {
normalizedExclude = exclude;
}
if (!normalizedExclude || normalizedExclude.includes(ParcelWatcherService.GLOB_MARKERS.Star)) {
continue; // skip for parcel (will be applied later by our glob matching)
}
// Absolute path: normalize to watched path and
// exclude if not a parent of it otherwise.
if (isAbsolute(normalizedExclude)) {
if (!isEqualOrParent(normalizedExclude, path, !isLinux)) {
continue; // exclude points to path outside of watched folder, ignore
}
// convert to relative path to ensure we
// get the correct path casing going forward
normalizedExclude = normalizedExclude.substr(path.length);
}
// Finally take as relative path joined to watched path
excludePaths.add(rtrim(join(path, normalizedExclude), sep));
}
if (excludePaths.size > 0) {
return Array.from(excludePaths);
}
return undefined;
}
private startWatching(request: IWatchRequest, restarts = 0): void {
const cts = new CancellationTokenSource();
let parcelWatcherPromiseResolve: (watcher: parcelWatcher.AsyncSubscription | undefined) => void;
const instance = new Promise<parcelWatcher.AsyncSubscription | undefined>(resolve => parcelWatcherPromiseResolve = resolve);
// Remember as watcher instance
const watcher: IWatcher = {
request,
instance,
ignored: this.toExcludePatterns(request.excludes),
restarts,
token: cts.token,
stop: async () => {
cts.dispose(true);
(await instance)?.unsubscribe();
},
dispose: () => {
watcher.stop();
}
};
this.watchers.set(request.path, watcher);
// Path checks for symbolic links / wrong casing
const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request);
let undeliveredFileEvents: IDiskFileChange[] = [];
const onRawFileEvent = (path: string, type: FileChangeType) => {
if (this.verboseLogging) {
this.log(`${type === FileChangeType.ADDED ? '[ADDED]' : type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`);
}
if (!this.isPathIgnored(path, watcher.ignored)) {
undeliveredFileEvents.push({ type, path });
} else {
if (this.verboseLogging) {
this.log(` >> ignored ${path}`);
}
}
};
const ignore = this.toExcludePaths(realPath, watcher.request.excludes);
parcelWatcher.subscribe(realPath, (error, events) => {
if (watcher.token.isCancellationRequested) {
return; // return early when disposed
}
if (error) {
this.error(`Unexpected error in event callback: ${toErrorMessage(error)}`, watcher);
}
if (events.length === 0) {
return; // assume this can happen if we had an error before
}
for (const event of events) {
onRawFileEvent(event.path, ParcelWatcherService.MAP_PARCEL_WATCHER_ACTION_TO_FILE_CHANGE.get(event.type)!);
}
// Reset undelivered events array
const undeliveredFileEventsToEmit = undeliveredFileEvents;
undeliveredFileEvents = [];
// Normalize and detect root path deletes
const { events: normalizedEvents, rootDeleted } = this.normalizeEvents(undeliveredFileEventsToEmit, request, realPathDiffers, realPathLength);
// Broadcast to clients coalesced
const coalescedEvents = normalizeFileChanges(normalizedEvents);
this.emitEvents(coalescedEvents);
// Handle root path delete if confirmed from coalseced events
if (rootDeleted && coalescedEvents.some(event => event.path === watcher.request.path && event.type === FileChangeType.DELETED)) {
this.onWatchedPathDeleted(watcher);
}
}, {
backend: ParcelWatcherService.PARCEL_WATCHER_BACKEND,
ignore
}).then(parcelWatcher => {
this.debug(`Started watching: '${realPath}' with backend '${ParcelWatcherService.PARCEL_WATCHER_BACKEND}' and native excludes '${ignore?.join(', ')}'`);
parcelWatcherPromiseResolve(parcelWatcher);
}).catch(error => {
this.onUnexpectedError(error, watcher);
parcelWatcherPromiseResolve(undefined);
});
}
private emitEvents(events: IDiskFileChange[]): void {
// Send outside
this._onDidChangeFile.fire(events);
// Logging
if (this.verboseLogging) {
for (const event of events) {
this.log(` >> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`);
}
}
}
private normalizePath(request: IWatchRequest): { realPath: string, realPathDiffers: boolean, realPathLength: number } {
let realPath = request.path;
let realPathDiffers = false;
let realPathLength = request.path.length;
try {
// First check for symbolic link
realPath = realpathSync(request.path);
// Second check for casing difference
if (request.path === realPath) {
realPath = realcaseSync(request.path) ?? request.path;
}
// Correct watch path as needed
if (request.path !== realPath) {
realPathLength = realPath.length;
realPathDiffers = true;
this.warn(`correcting a path to watch that seems to be a symbolic link (original: ${request.path}, real: ${realPath})`);
}
} catch (error) {
// ignore
}
return { realPath, realPathDiffers, realPathLength };
}
private normalizeEvents(events: IDiskFileChange[], request: IWatchRequest, realPathDiffers: boolean, realPathLength: number): { events: IDiskFileChange[], rootDeleted: boolean } {
let rootDeleted = false;
for (const event of events) {
// Mac uses NFD unicode form on disk, but we want NFC
if (isMacintosh) {
event.path = normalizeNFC(event.path);
}
// TODO@bpasero workaround for https://github.com/parcel-bundler/watcher/issues/68
// where watching root drive letter adds extra backslashes.
if (isWindows) {
if (request.path.length <= 3) { // for ex. c:, C:\
event.path = normalize(event.path);
}
}
// Convert paths back to original form in case it differs
if (realPathDiffers) {
event.path = request.path + event.path.substr(realPathLength);
}
// Check for root deleted
if (event.path === request.path && event.type === FileChangeType.DELETED) {
rootDeleted = true;
}
}
return { events, rootDeleted };
}
private onWatchedPathDeleted(watcher: IWatcher): void {
this.warn('Watcher shutdown because watched path got deleted', watcher);
const parentPath = dirname(watcher.request.path);
if (existsSync(parentPath)) {
const disposable = watchFolder(parentPath, (type, path) => {
if (watcher.token.isCancellationRequested) {
return; // return early when disposed
}
// Watcher path came back! Restart watching...
if (path === watcher.request.path && (type === 'added' || type === 'changed')) {
this.warn('Watcher restarts because watched path got created again', watcher);
// Stop watching that parent folder
disposable.dispose();
// Send a manual event given we know the root got added again
this.emitEvents([{ path: watcher.request.path, type: FileChangeType.ADDED }]);
// Restart the file watching
this.restartWatching(watcher);
}
}, error => {
// Ignore
});
// Make sure to stop watching when the watcher is disposed
watcher.token.onCancellationRequested(() => disposable.dispose());
}
}
private onUnexpectedError(error: unknown, watcher?: IWatcher): void {
const msg = toErrorMessage(error);
// Specially handle ENOSPC errors that can happen when
// 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
if (msg.indexOf('No space left on device') !== -1) {
if (!this.enospcErrorLogged) {
this.error('Inotify limit reached (ENOSPC)', watcher);
this.enospcErrorLogged = true;
}
}
// Any other error is unexpected and we should try to
// restart the watcher as a result to get into healthy
// state again if possible and if not attempted too much
else {
if (watcher && watcher.restarts < ParcelWatcherService.MAX_RESTARTS) {
if (existsSync(watcher.request.path)) {
this.warn(`Watcher will be restarted due to unexpected error: ${error}`, watcher);
this.restartWatching(watcher);
} else {
this.error(`Unexpected error: ${msg} (EUNKNOWN: path ${watcher.request.path} no longer exists)`, watcher);
}
} else {
this.error(`Unexpected error: ${msg} (EUNKNOWN)`, watcher);
}
}
}
async stop(): Promise<void> {
for (const [path] of this.watchers) {
await this.stopWatching(path);
}
this.watchers.clear();
}
protected restartWatching(watcher: IWatcher, delay = 800): void {
// Restart watcher delayed to accomodate for
// changes on disk that have triggered the
// need for a restart in the first place.
const scheduler = new RunOnceScheduler(async () => {
if (watcher.token.isCancellationRequested) {
return; // return early when disposed
}
// Await the watcher having stopped, as this is
// needed to properly re-watch the same path
await this.stopWatching(watcher.request.path);
// Start watcher again counting the restarts
this.startWatching(watcher.request, watcher.restarts + 1);
}, delay);
scheduler.schedule();
watcher.token.onCancellationRequested(() => scheduler.dispose());
}
private async stopWatching(path: string): Promise<void> {
const watcher = this.watchers.get(path);
if (watcher) {
this.watchers.delete(path);
await watcher.stop();
}
}
protected normalizeRequests(requests: IWatchRequest[]): IWatchRequest[] {
const requestTrie = TernarySearchTree.forPaths<IWatchRequest>();
// Sort requests by path length to have shortest first
// to have a way to prevent children to be watched if
// parents exist.
requests.sort((requestA, requestB) => requestA.path.length - requestB.path.length);
// Only consider requests for watching that are not
// a child of an existing request path to prevent
// duplication.
//
// However, allow explicit requests to watch folders
// that are symbolic links because the Parcel watcher
// does not allow to recursively watch symbolic links.
for (const request of requests) {
if (requestTrie.findSubstr(request.path)) {
try {
const realpath = realpathSync(request.path);
if (realpath === request.path) {
this.warn(`ignoring a path for watching who's parent is already watched: ${request.path}`);
continue; // path is not a symbolic link or similar
}
} catch (error) {
continue; // invalid path - ignore from watching
}
}
requestTrie.set(request.path, request);
}
return Array.from(requestTrie).map(([, request]) => request);
}
private isPathIgnored(absolutePath: string, ignored: ParsedPattern[]): boolean {
return ignored.some(ignore => ignore(absolutePath));
}
async setVerboseLogging(enabled: boolean): Promise<void> {
this.verboseLogging = enabled;
}
private log(message: string) {
this._onDidLogMessage.fire({ type: 'trace', message: this.toMessage(message) });
}
private warn(message: string, watcher?: IWatcher) {
this._onDidLogMessage.fire({ type: 'warn', message: this.toMessage(message, watcher) });
}
private error(message: string, watcher: IWatcher | undefined) {
this._onDidLogMessage.fire({ type: 'error', message: this.toMessage(message, watcher) });
}
private debug(message: string): void {
this._onDidLogMessage.fire({ type: 'debug', message: this.toMessage(message) });
}
private toMessage(message: string, watcher?: IWatcher): string {
return watcher ? `[File Watcher (parcel)] ${message} (path: ${watcher.request.path})` : `[File Watcher (parcel)] ${message}`;
}
}
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IDiskFileChange, ILogMessage, IWatchRequest } from 'vs/platform/files/node/watcher/watcher';
export interface IWatcherService {
/**
* A normalized file change event from the raw events
* the watcher emits.
*/
readonly onDidChangeFile: Event<IDiskFileChange[]>;
/**
* An event to indicate a message that should get logged.
*/
readonly onDidLogMessage: Event<ILogMessage>;
/**
* Configures the watcher service to watch according
* to the requests. Any existing watched path that
* is not in the array, will be removed from watching
* and any new path will be added to watching.
*/
watch(requests: IWatchRequest[]): Promise<void>;
/**
* Enable verbose logging in the watcher.
*/
setVerboseLogging(enabled: boolean): Promise<void>;
/**
* Stop all watchers.
*/
stop(): Promise<void>;
}
@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { ParcelWatcherService } from 'vs/platform/files/node/watcher/parcel/parcelWatcherService';
const server = new Server('watcher');
const service = new ParcelWatcherService();
server.registerChannel('watcher', ProxyChannel.fromService(service));
@@ -0,0 +1,96 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { FileAccess } from 'vs/base/common/network';
import { getNextTickChannel, ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IWatcherService } from 'vs/platform/files/node/watcher/parcel/watcher';
import { IDiskFileChange, ILogMessage, IWatchRequest } from 'vs/platform/files/node/watcher/watcher';
export class FileWatcher extends Disposable {
private static readonly MAX_RESTARTS = 5;
private service: IWatcherService | undefined;
private isDisposed = false;
private restartCounter = 0;
constructor(
private requests: IWatchRequest[],
private readonly onDidFilesChange: (changes: IDiskFileChange[]) => void,
private readonly onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean,
) {
super();
this.startWatching();
}
private startWatching(): void {
const client = this._register(new Client(
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
{
serverName: 'File Watcher (parcel)',
args: ['--type=watcherService'],
env: {
VSCODE_AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/parcel/watcherApp',
VSCODE_PIPE_LOGGING: 'true',
VSCODE_VERBOSE_LOGGING: 'true' // transmit console logs from server to client
}
}
));
this._register(client.onDidProcessExit(() => {
// 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.error('terminated unexpectedly and is restarted again...');
this.restartCounter++;
this.startWatching();
} else {
this.error('failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}));
// Initialize watcher
this.service = ProxyChannel.toService<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service.setVerboseLogging(this.verboseLogging);
// Wire in event handlers
this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e)));
this._register(this.service.onDidLogMessage(e => this.onLogMessage(e)));
// Start watching
this.watch(this.requests);
}
setVerboseLogging(verboseLogging: boolean): void {
this.verboseLogging = verboseLogging;
if (!this.isDisposed) {
this.service?.setVerboseLogging(verboseLogging);
}
}
error(message: string) {
this.onLogMessage({ type: 'error', message: `[File Watcher (parcel)] ${message}` });
}
watch(requests: IWatchRequest[]): void {
this.requests = requests;
this.service?.watch(requests);
}
override dispose(): void {
this.isDisposed = true;
super.dispose();
}
}
@@ -72,10 +72,12 @@ class EventNormalizer {
const newChangeType = event.type;
// macOS/Windows: track renames to different case but
// same name by changing previous event to DELETED
// whenever new event with different case occurs
if (existingEvent.path !== event.path && (newChangeType === FileChangeType.ADDED || newChangeType === FileChangeType.UPDATED)) {
existingEvent.type = FileChangeType.DELETED;
// same name by changing current event to DELETED
// this encodes some underlying knowledge about the
// file watcher being used by assuming we first get
// an event for the CREATE and then an event that we
// consider as DELETE if same name / different case.
if (existingEvent.path !== event.path && event.type === FileChangeType.DELETED) {
keepEvent = true;
}
@@ -4,19 +4,20 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { realpathSync } from 'fs';
import { tmpdir } from 'os';
import { timeout } from 'vs/base/common/async';
import { join } from 'vs/base/common/path';
import { dirname, join, sep } from 'vs/base/common/path';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { Promises } from 'vs/base/node/pfs';
import { Promises, RimRafMode } from 'vs/base/node/pfs';
import { flakySuite, getPathFromAmdModule, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { FileChangeType } from 'vs/platform/files/common/files';
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
import { IWatcher, ParcelWatcherService } from 'vs/platform/files/node/watcher/parcel/parcelWatcherService';
import { IWatchRequest } from 'vs/platform/files/node/watcher/watcher';
flakySuite('Recursive Watcher', () => {
flakySuite('Recursive Watcher (parcel)', () => {
class TestNsfwWatcherService extends NsfwWatcherService {
class TestParcelWatcherService extends ParcelWatcherService {
testNormalizePaths(paths: string[]): string[] {
@@ -30,22 +31,26 @@ flakySuite('Recursive Watcher', () => {
override async watch(requests: IWatchRequest[]): Promise<void> {
await super.watch(requests);
await this.whenReady();
}
async whenReady(): Promise<void> {
for (const [, watcher] of this.watchers) {
await watcher.instance;
}
}
protected override getOptions(watcher: any) {
return {
...super.getOptions(watcher),
debounceMS: 1
};
override toExcludePaths(path: string, excludes: string[] | undefined): string[] | undefined {
return super.toExcludePaths(path, excludes);
}
override restartWatching(watcher: IWatcher, delay = 10): void {
return super.restartWatching(watcher, delay);
}
}
let testDir: string;
let service: TestNsfwWatcherService;
let service: TestParcelWatcherService;
let loggingEnabled = false;
@@ -57,7 +62,7 @@ flakySuite('Recursive Watcher', () => {
enableLogging(false);
setup(async () => {
service = new TestNsfwWatcherService();
service = new TestParcelWatcherService();
service.onDidLogMessage(e => {
if (loggingEnabled) {
@@ -78,23 +83,43 @@ flakySuite('Recursive Watcher', () => {
return Promises.rm(testDir);
});
function awaitEvent(service: TestNsfwWatcherService, path: string, type: FileChangeType): Promise<void> {
return new Promise(resolve => {
function toMsg(type: FileChangeType): string {
switch (type) {
case FileChangeType.ADDED: return 'added';
case FileChangeType.DELETED: return 'deleted';
default: return 'changed';
}
}
function awaitEvent(service: TestParcelWatcherService, path: string, type: FileChangeType, failOnEvent?: boolean): Promise<void> {
if (loggingEnabled) {
console.log(`Awaiting change type '${toMsg(type)}' on file '${path}'`);
}
return new Promise<void>((resolve, reject) => {
const disposable = service.onDidChangeFile(events => {
for (const event of events) {
if (event.path === path && event.type === type) {
disposable.dispose();
resolve();
if (failOnEvent) {
reject(new Error('Unexpected file event'));
} else {
resolve();
}
break;
}
}
});
}).then(() => {
// Unwind a bit to avoid calling watcher methods directly
// after a file event was send. At least one test was seen
// to crash when immediately re-watching the same folder
// from within the event callback due to a mutex lock issue.
return timeout(5);
});
}
const runWatchTests = process.env['BUILD_SOURCEVERSION'] || process.env['CI'] || !!process.env['VSCODE_RUN_RECURSIVE_WATCH_TESTS'];
(runWatchTests ? test : test.skip)('basics', async function () {
test('basics', async function () {
await service.watch([{ path: testDir, excludes: [] }]);
// New file
@@ -127,29 +152,25 @@ flakySuite('Recursive Watcher', () => {
await Promises.rename(newFolderPath, renamedFolderPath);
await changeFuture;
// Case rename is currently broken on macOS (https://github.com/Axosoft/nsfw/issues/146)
if (isWindows) {
// Rename file (same name, different case)
const caseRenamedFilePath = join(testDir, 'deep', 'RenamedFile.txt');
changeFuture = Promise.all([
awaitEvent(service, renamedFilePath, FileChangeType.DELETED),
awaitEvent(service, caseRenamedFilePath, FileChangeType.ADDED)
]);
await Promises.rename(renamedFilePath, caseRenamedFilePath);
await changeFuture;
renamedFilePath = caseRenamedFilePath;
// Rename file (same name, different case)
const caseRenamedFilePath = join(testDir, 'deep', 'RenamedFile.txt');
changeFuture = Promise.all([
awaitEvent(service, renamedFilePath, FileChangeType.DELETED),
awaitEvent(service, caseRenamedFilePath, FileChangeType.ADDED)
]);
await Promises.rename(renamedFilePath, caseRenamedFilePath);
await changeFuture;
renamedFilePath = caseRenamedFilePath;
// Rename folder (same name, different case)
const caseRenamedFolderPath = join(testDir, 'deep', 'REnamed Folder');
changeFuture = Promise.all([
awaitEvent(service, renamedFolderPath, FileChangeType.DELETED),
awaitEvent(service, caseRenamedFolderPath, FileChangeType.ADDED)
]);
await Promises.rename(renamedFolderPath, caseRenamedFolderPath);
await changeFuture;
renamedFolderPath = caseRenamedFolderPath;
}
// Rename folder (same name, different case)
const caseRenamedFolderPath = join(testDir, 'deep', 'REnamed Folder');
changeFuture = Promise.all([
awaitEvent(service, renamedFolderPath, FileChangeType.DELETED),
awaitEvent(service, caseRenamedFolderPath, FileChangeType.ADDED)
]);
await Promises.rename(renamedFolderPath, caseRenamedFolderPath);
await changeFuture;
renamedFolderPath = caseRenamedFolderPath;
// Move file
const movedFilepath = join(testDir, 'movedFile.txt');
@@ -182,11 +203,20 @@ flakySuite('Recursive Watcher', () => {
await changeFuture;
// Change file
await timeout(1100); // nsfw cannot distinguish a create from a change when time period from create to change is <1s
changeFuture = awaitEvent(service, copiedFilepath, FileChangeType.UPDATED);
await Promises.writeFile(copiedFilepath, 'Hello Change');
await changeFuture;
// Read file does not emit event
changeFuture = awaitEvent(service, copiedFilepath, FileChangeType.UPDATED, true /* unexpected */);
await Promises.readFile(copiedFilepath);
await Promise.race([timeout(100), changeFuture]);
// Stat file does not emit event
changeFuture = awaitEvent(service, copiedFilepath, FileChangeType.UPDATED, true /* unexpected */);
await Promises.stat(copiedFilepath);
await Promise.race([timeout(100), changeFuture]);
// Delete file
changeFuture = awaitEvent(service, copiedFilepath, FileChangeType.DELETED);
await Promises.unlink(copiedFilepath);
@@ -198,8 +228,100 @@ flakySuite('Recursive Watcher', () => {
await changeFuture;
});
(runWatchTests ? test : test.skip)('subsequent watch updates watchers', async function () {
await service.watch([{ path: testDir, excludes: ['**/*.js'] }]);
test('multiple events', async function () {
await service.watch([{ path: testDir, excludes: [] }]);
await Promises.mkdir(join(testDir, 'deep-multiple'));
// multiple add
const newFilePath1 = join(testDir, 'newFile-1.txt');
const newFilePath2 = join(testDir, 'newFile-2.txt');
const newFilePath3 = join(testDir, 'newFile-3.txt');
const newFilePath4 = join(testDir, 'deep-multiple', 'newFile-1.txt');
const newFilePath5 = join(testDir, 'deep-multiple', 'newFile-2.txt');
const newFilePath6 = join(testDir, 'deep-multiple', 'newFile-3.txt');
const addedFuture1: Promise<unknown> = awaitEvent(service, newFilePath1, FileChangeType.ADDED);
const addedFuture2: Promise<unknown> = awaitEvent(service, newFilePath2, FileChangeType.ADDED);
const addedFuture3: Promise<unknown> = awaitEvent(service, newFilePath3, FileChangeType.ADDED);
const addedFuture4: Promise<unknown> = awaitEvent(service, newFilePath4, FileChangeType.ADDED);
const addedFuture5: Promise<unknown> = awaitEvent(service, newFilePath5, FileChangeType.ADDED);
const addedFuture6: Promise<unknown> = awaitEvent(service, newFilePath6, FileChangeType.ADDED);
await Promise.all([
await Promises.writeFile(newFilePath1, 'Hello World 1'),
await Promises.writeFile(newFilePath2, 'Hello World 2'),
await Promises.writeFile(newFilePath3, 'Hello World 3'),
await Promises.writeFile(newFilePath4, 'Hello World 4'),
await Promises.writeFile(newFilePath5, 'Hello World 5'),
await Promises.writeFile(newFilePath6, 'Hello World 6')
]);
await Promise.all([addedFuture1, addedFuture2, addedFuture3, addedFuture4, addedFuture5, addedFuture6]);
// multiple change
const changeFuture1: Promise<unknown> = awaitEvent(service, newFilePath1, FileChangeType.UPDATED);
const changeFuture2: Promise<unknown> = awaitEvent(service, newFilePath2, FileChangeType.UPDATED);
const changeFuture3: Promise<unknown> = awaitEvent(service, newFilePath3, FileChangeType.UPDATED);
const changeFuture4: Promise<unknown> = awaitEvent(service, newFilePath4, FileChangeType.UPDATED);
const changeFuture5: Promise<unknown> = awaitEvent(service, newFilePath5, FileChangeType.UPDATED);
const changeFuture6: Promise<unknown> = awaitEvent(service, newFilePath6, FileChangeType.UPDATED);
await Promise.all([
await Promises.writeFile(newFilePath1, 'Hello Update 1'),
await Promises.writeFile(newFilePath2, 'Hello Update 2'),
await Promises.writeFile(newFilePath3, 'Hello Update 3'),
await Promises.writeFile(newFilePath4, 'Hello Update 4'),
await Promises.writeFile(newFilePath5, 'Hello Update 5'),
await Promises.writeFile(newFilePath6, 'Hello Update 6')
]);
await Promise.all([changeFuture1, changeFuture2, changeFuture3, changeFuture4, changeFuture5, changeFuture6]);
// copy with multiple files
const copyFuture1: Promise<unknown> = awaitEvent(service, join(testDir, 'deep-multiple-copy', 'newFile-1.txt'), FileChangeType.ADDED);
const copyFuture2: Promise<unknown> = awaitEvent(service, join(testDir, 'deep-multiple-copy', 'newFile-2.txt'), FileChangeType.ADDED);
const copyFuture3: Promise<unknown> = awaitEvent(service, join(testDir, 'deep-multiple-copy', 'newFile-3.txt'), FileChangeType.ADDED);
const copyFuture4: Promise<unknown> = awaitEvent(service, join(testDir, 'deep-multiple-copy'), FileChangeType.ADDED);
await Promises.copy(join(testDir, 'deep-multiple'), join(testDir, 'deep-multiple-copy'), { preserveSymlinks: false });
await Promise.all([copyFuture1, copyFuture2, copyFuture3, copyFuture4]);
// multiple delete (single files)
const deleteFuture1: Promise<unknown> = awaitEvent(service, newFilePath1, FileChangeType.DELETED);
const deleteFuture2: Promise<unknown> = awaitEvent(service, newFilePath2, FileChangeType.DELETED);
const deleteFuture3: Promise<unknown> = awaitEvent(service, newFilePath3, FileChangeType.DELETED);
const deleteFuture4: Promise<unknown> = awaitEvent(service, newFilePath4, FileChangeType.DELETED);
const deleteFuture5: Promise<unknown> = awaitEvent(service, newFilePath5, FileChangeType.DELETED);
const deleteFuture6: Promise<unknown> = awaitEvent(service, newFilePath6, FileChangeType.DELETED);
await Promise.all([
await Promises.unlink(newFilePath1),
await Promises.unlink(newFilePath2),
await Promises.unlink(newFilePath3),
await Promises.unlink(newFilePath4),
await Promises.unlink(newFilePath5),
await Promises.unlink(newFilePath6)
]);
await Promise.all([deleteFuture1, deleteFuture2, deleteFuture3, deleteFuture4, deleteFuture5, deleteFuture6]);
// multiple delete (folder)
const deleteFolderFuture1: Promise<unknown> = awaitEvent(service, join(testDir, 'deep-multiple'), FileChangeType.DELETED);
const deleteFolderFuture2: Promise<unknown> = awaitEvent(service, join(testDir, 'deep-multiple-copy'), FileChangeType.DELETED);
await Promise.all([Promises.rm(join(testDir, 'deep-multiple'), RimRafMode.UNLINK), Promises.rm(join(testDir, 'deep-multiple-copy'), RimRafMode.UNLINK)]);
await Promise.all([deleteFolderFuture1, deleteFolderFuture2]);
});
test('subsequent watch updates watchers (path)', async function () {
await service.watch([{ path: testDir, excludes: [join(realpathSync(testDir), 'unrelated')] }]);
// New file (*.txt)
let newTextFilePath = join(testDir, 'deep', 'newFile.txt');
@@ -207,13 +329,13 @@ flakySuite('Recursive Watcher', () => {
await Promises.writeFile(newTextFilePath, 'Hello World');
await changeFuture;
await service.watch([{ path: join(testDir, 'deep'), excludes: ['**/*.js'] }]);
await service.watch([{ path: join(testDir, 'deep'), excludes: [join(realpathSync(testDir), 'unrelated')] }]);
newTextFilePath = join(testDir, 'deep', 'newFile2.txt');
changeFuture = awaitEvent(service, newTextFilePath, FileChangeType.ADDED);
await Promises.writeFile(newTextFilePath, 'Hello World');
await changeFuture;
await service.watch([{ path: join(testDir, 'deep'), excludes: ['**/*.txt'] }]);
await service.watch([{ path: join(testDir, 'deep'), excludes: [realpathSync(testDir)] }]);
await service.watch([{ path: join(testDir, 'deep'), excludes: [] }]);
newTextFilePath = join(testDir, 'deep', 'newFile3.txt');
changeFuture = awaitEvent(service, newTextFilePath, FileChangeType.ADDED);
@@ -223,7 +345,20 @@ flakySuite('Recursive Watcher', () => {
return service.stop();
});
((isWindows /* windows: cannot create file symbolic link without elevated context */ || !runWatchTests) ? test.skip : test)('symlink support (root)', async function () {
test('subsequent watch updates watchers (excludes)', async function () {
await service.watch([{ path: testDir, excludes: [realpathSync(testDir)] }]);
await service.watch([{ path: testDir, excludes: [] }]);
// New file (*.txt)
let newTextFilePath = join(testDir, 'deep', 'newFile.txt');
let changeFuture: Promise<unknown> = awaitEvent(service, newTextFilePath, FileChangeType.ADDED);
await Promises.writeFile(newTextFilePath, 'Hello World');
await changeFuture;
return service.stop();
});
(isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (root)', async function () {
const link = join(testDir, 'deep-linked');
const linkTarget = join(testDir, 'deep');
await Promises.symlink(linkTarget, link);
@@ -237,7 +372,7 @@ flakySuite('Recursive Watcher', () => {
await changeFuture;
});
((isWindows /* windows: cannot create file symbolic link without elevated context */ || !runWatchTests) ? test.skip : test)('symlink support (via extra watch)', async function () {
(isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (via extra watch)', async function () {
const link = join(testDir, 'deep-linked');
const linkTarget = join(testDir, 'deep');
await Promises.symlink(linkTarget, link);
@@ -251,7 +386,7 @@ flakySuite('Recursive Watcher', () => {
await changeFuture;
});
((isLinux /* linux: is case sensitive */ || !runWatchTests) ? test.skip : test)('wrong casing', async function () {
(isLinux /* linux: is case sensitive */ ? test.skip : test)('wrong casing', async function () {
const deepWrongCasedPath = join(testDir, 'DEEP');
await service.watch([{ path: deepWrongCasedPath, excludes: [] }]);
@@ -263,6 +398,37 @@ flakySuite('Recursive Watcher', () => {
await changeFuture;
});
test('invalid folder does not explode', async function () {
const invalidPath = join(testDir, 'invalid');
await service.watch([{ path: invalidPath, excludes: [] }]);
});
test('deleting watched path is handled properly', async function () {
const watchedPath = join(testDir, 'deep');
await service.watch([{ path: watchedPath, excludes: [] }]);
// Delete watched path
let changeFuture: Promise<unknown> = awaitEvent(service, watchedPath, FileChangeType.DELETED);
await Promises.rm(watchedPath, RimRafMode.UNLINK);
await changeFuture;
// Restore watched path
changeFuture = awaitEvent(service, watchedPath, FileChangeType.ADDED);
await Promises.mkdir(watchedPath);
await changeFuture;
await timeout(20); // restart is delayed
await service.whenReady();
// Verify events come in again
const newFilePath = join(watchedPath, 'newFile.txt');
changeFuture = awaitEvent(service, newFilePath, FileChangeType.ADDED);
await Promises.writeFile(newFilePath, 'Hello World');
await changeFuture;
});
test('should not exclude roots that do not overlap', () => {
if (isWindows) {
assert.deepStrictEqual(service.testNormalizePaths(['C:\\a']), ['C:\\a']);
@@ -288,4 +454,105 @@ flakySuite('Recursive Watcher', () => {
assert.deepStrictEqual(service.testNormalizePaths(['/a', '/a/b', '/a/c/d']), ['/a']);
}
});
test('excludes are converted to absolute paths', () => {
// undefined / empty
assert.strictEqual(service.toExcludePaths(testDir, undefined), undefined);
assert.strictEqual(service.toExcludePaths(testDir, []), undefined);
// absolute paths
let excludes = service.toExcludePaths(testDir, [testDir]);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], testDir);
excludes = service.toExcludePaths(testDir, [`${testDir}${sep}`, join(testDir, 'foo', 'bar'), `${join(testDir, 'other', 'deep')}${sep}`]);
assert.strictEqual(excludes?.length, 3);
assert.strictEqual(excludes[0], testDir);
assert.strictEqual(excludes[1], join(testDir, 'foo', 'bar'));
assert.strictEqual(excludes[2], join(testDir, 'other', 'deep'));
// wrong casing is normalized for root
excludes = service.toExcludePaths(testDir, [join(testDir.toUpperCase(), 'node_modules', '**')]);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, 'node_modules'));
// exclude ignored if not parent of watched dir
excludes = service.toExcludePaths(testDir, [join(dirname(testDir), 'node_modules', '**')]);
assert.strictEqual(excludes, undefined);
// relative paths
excludes = service.toExcludePaths(testDir, ['.']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], testDir);
excludes = service.toExcludePaths(testDir, ['foo', `bar${sep}`, join('foo', 'bar'), `${join('other', 'deep')}${sep}`]);
assert.strictEqual(excludes?.length, 4);
assert.strictEqual(excludes[0], join(testDir, 'foo'));
assert.strictEqual(excludes[1], join(testDir, 'bar'));
assert.strictEqual(excludes[2], join(testDir, 'foo', 'bar'));
assert.strictEqual(excludes[3], join(testDir, 'other', 'deep'));
// simple globs (relative)
excludes = service.toExcludePaths(testDir, ['**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], testDir);
excludes = service.toExcludePaths(testDir, ['**/node_modules/**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, 'node_modules'));
excludes = service.toExcludePaths(testDir, ['**/.git/objects/**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, '.git', 'objects'));
excludes = service.toExcludePaths(testDir, ['**/node_modules']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, 'node_modules'));
excludes = service.toExcludePaths(testDir, ['**/.git/objects']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, '.git', 'objects'));
excludes = service.toExcludePaths(testDir, ['node_modules/**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, 'node_modules'));
excludes = service.toExcludePaths(testDir, ['.git/objects/**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, '.git', 'objects'));
// simple globs (absolute)
excludes = service.toExcludePaths(testDir, [join(testDir, 'node_modules', '**')]);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, 'node_modules'));
// Linux: more restrictive glob treatment
if (isLinux) {
excludes = service.toExcludePaths(testDir, ['**/node_modules/*/**']);
assert.strictEqual(excludes?.length, 1);
assert.strictEqual(excludes[0], join(testDir, 'node_modules'));
}
// unsupported globs
else {
excludes = service.toExcludePaths(testDir, ['**/node_modules/*/**']);
assert.strictEqual(excludes, undefined);
}
excludes = service.toExcludePaths(testDir, ['**/*.js']);
assert.strictEqual(excludes, undefined);
excludes = service.toExcludePaths(testDir, ['*.js']);
assert.strictEqual(excludes, undefined);
excludes = service.toExcludePaths(testDir, ['*']);
assert.strictEqual(excludes, undefined);
});
});
@@ -11,10 +11,6 @@ import { URI as uri } from 'vs/base/common/uri';
import { FileChangesEvent, FileChangeType, IFileChange } from 'vs/platform/files/common/files';
import { IDiskFileChange, normalizeFileChanges, toFileChanges } from 'vs/platform/files/node/watcher/watcher';
function toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent {
return new FileChangesEvent(toFileChanges(changes), !isLinux);
}
class TestFileWatcher {
private readonly _onDidFilesChange: Emitter<{ raw: IFileChange[], event: FileChangesEvent }>;
@@ -37,9 +33,13 @@ class TestFileWatcher {
// Emit through event emitter
if (normalizedEvents.length > 0) {
this._onDidFilesChange.fire({ raw: toFileChanges(normalizedEvents), event: toFileChangesEvent(normalizedEvents) });
this._onDidFilesChange.fire({ raw: toFileChanges(normalizedEvents), event: this.toFileChangesEvent(normalizedEvents) });
}
}
private toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent {
return new FileChangesEvent(toFileChanges(changes), !isLinux);
}
}
enum Path {
@@ -50,7 +50,7 @@ enum Path {
suite('Watcher Events Normalizer', () => {
test('simple add/update/delete', function (done: () => void) {
test('simple add/update/delete', done => {
const watch = new TestFileWatcher();
const added = uri.file('/users/data/src/added.txt');
@@ -63,12 +63,12 @@ suite('Watcher Events Normalizer', () => {
{ path: deleted.fsPath, type: FileChangeType.DELETED },
];
watch.onDidFilesChange(({ event: e, raw }) => {
assert.ok(e);
watch.onDidFilesChange(({ event, raw }) => {
assert.ok(event);
assert.strictEqual(raw.length, 3);
assert.ok(e.contains(added, FileChangeType.ADDED));
assert.ok(e.contains(updated, FileChangeType.UPDATED));
assert.ok(e.contains(deleted, FileChangeType.DELETED));
assert.ok(event.contains(added, FileChangeType.ADDED));
assert.ok(event.contains(updated, FileChangeType.UPDATED));
assert.ok(event.contains(deleted, FileChangeType.DELETED));
done();
});
@@ -76,20 +76,19 @@ suite('Watcher Events Normalizer', () => {
watch.report(raw);
});
let pathSpecs = isWindows ? [Path.WINDOWS, Path.UNC] : [Path.UNIX];
pathSpecs.forEach((p) => {
test('delete only reported for top level folder (' + p + ')', function (done: () => void) {
(isWindows ? [Path.WINDOWS, Path.UNC] : [Path.UNIX]).forEach(path => {
test(`delete only reported for top level folder (${path})`, done => {
const watch = new TestFileWatcher();
const deletedFolderA = uri.file(p === Path.UNIX ? '/users/data/src/todelete1' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete1' : '\\\\localhost\\users\\data\\src\\todelete1');
const deletedFolderB = uri.file(p === Path.UNIX ? '/users/data/src/todelete2' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2' : '\\\\localhost\\users\\data\\src\\todelete2');
const deletedFolderBF1 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/file.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\file.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\file.txt');
const deletedFolderBF2 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/more/test.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\more\\test.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\more\\test.txt');
const deletedFolderBF3 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/super/bar/foo.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\super\\bar\\foo.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\super\\bar\\foo.txt');
const deletedFileA = uri.file(p === Path.UNIX ? '/users/data/src/deleteme.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\deleteme.txt' : '\\\\localhost\\users\\data\\src\\deleteme.txt');
const deletedFolderA = uri.file(path === Path.UNIX ? '/users/data/src/todelete1' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete1' : '\\\\localhost\\users\\data\\src\\todelete1');
const deletedFolderB = uri.file(path === Path.UNIX ? '/users/data/src/todelete2' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2' : '\\\\localhost\\users\\data\\src\\todelete2');
const deletedFolderBF1 = uri.file(path === Path.UNIX ? '/users/data/src/todelete2/file.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\file.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\file.txt');
const deletedFolderBF2 = uri.file(path === Path.UNIX ? '/users/data/src/todelete2/more/test.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\more\\test.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\more\\test.txt');
const deletedFolderBF3 = uri.file(path === Path.UNIX ? '/users/data/src/todelete2/super/bar/foo.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\super\\bar\\foo.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\super\\bar\\foo.txt');
const deletedFileA = uri.file(path === Path.UNIX ? '/users/data/src/deleteme.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\deleteme.txt' : '\\\\localhost\\users\\data\\src\\deleteme.txt');
const addedFile = uri.file(p === Path.UNIX ? '/users/data/src/added.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\added.txt' : '\\\\localhost\\users\\data\\src\\added.txt');
const updatedFile = uri.file(p === Path.UNIX ? '/users/data/src/updated.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\updated.txt' : '\\\\localhost\\users\\data\\src\\updated.txt');
const addedFile = uri.file(path === Path.UNIX ? '/users/data/src/added.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\added.txt' : '\\\\localhost\\users\\data\\src\\added.txt');
const updatedFile = uri.file(path === Path.UNIX ? '/users/data/src/updated.txt' : path === Path.WINDOWS ? 'C:\\users\\data\\src\\updated.txt' : '\\\\localhost\\users\\data\\src\\updated.txt');
const raw: IDiskFileChange[] = [
{ path: deletedFolderA.fsPath, type: FileChangeType.DELETED },
@@ -102,15 +101,15 @@ suite('Watcher Events Normalizer', () => {
{ path: updatedFile.fsPath, type: FileChangeType.UPDATED }
];
watch.onDidFilesChange(({ event: e, raw }) => {
assert.ok(e);
watch.onDidFilesChange(({ event, raw }) => {
assert.ok(event);
assert.strictEqual(raw.length, 5);
assert.ok(e.contains(deletedFolderA, FileChangeType.DELETED));
assert.ok(e.contains(deletedFolderB, FileChangeType.DELETED));
assert.ok(e.contains(deletedFileA, FileChangeType.DELETED));
assert.ok(e.contains(addedFile, FileChangeType.ADDED));
assert.ok(e.contains(updatedFile, FileChangeType.UPDATED));
assert.ok(event.contains(deletedFolderA, FileChangeType.DELETED));
assert.ok(event.contains(deletedFolderB, FileChangeType.DELETED));
assert.ok(event.contains(deletedFileA, FileChangeType.DELETED));
assert.ok(event.contains(addedFile, FileChangeType.ADDED));
assert.ok(event.contains(updatedFile, FileChangeType.UPDATED));
done();
});
@@ -119,7 +118,7 @@ suite('Watcher Events Normalizer', () => {
});
});
test('event normalization: ignore CREATE followed by DELETE', function (done: () => void) {
test('event normalization: ignore CREATE followed by DELETE', done => {
const watch = new TestFileWatcher();
const created = uri.file('/users/data/src/related');
@@ -132,11 +131,11 @@ suite('Watcher Events Normalizer', () => {
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
];
watch.onDidFilesChange(({ event: e, raw }) => {
assert.ok(e);
watch.onDidFilesChange(({ event, raw }) => {
assert.ok(event);
assert.strictEqual(raw.length, 1);
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
assert.ok(event.contains(unrelated, FileChangeType.UPDATED));
done();
});
@@ -144,7 +143,7 @@ suite('Watcher Events Normalizer', () => {
watch.report(raw);
});
test('event normalization: flatten DELETE followed by CREATE into CHANGE', function (done: () => void) {
test('event normalization: flatten DELETE followed by CREATE into CHANGE', done => {
const watch = new TestFileWatcher();
const deleted = uri.file('/users/data/src/related');
@@ -157,12 +156,12 @@ suite('Watcher Events Normalizer', () => {
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
];
watch.onDidFilesChange(({ event: e, raw }) => {
assert.ok(e);
watch.onDidFilesChange(({ event, raw }) => {
assert.ok(event);
assert.strictEqual(raw.length, 2);
assert.ok(e.contains(deleted, FileChangeType.UPDATED));
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
assert.ok(event.contains(deleted, FileChangeType.UPDATED));
assert.ok(event.contains(unrelated, FileChangeType.UPDATED));
done();
});
@@ -170,7 +169,7 @@ suite('Watcher Events Normalizer', () => {
watch.report(raw);
});
test('event normalization: ignore UPDATE when CREATE received', function (done: () => void) {
test('event normalization: ignore UPDATE when CREATE received', done => {
const watch = new TestFileWatcher();
const created = uri.file('/users/data/src/related');
@@ -183,13 +182,13 @@ suite('Watcher Events Normalizer', () => {
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
];
watch.onDidFilesChange(({ event: e, raw }) => {
assert.ok(e);
watch.onDidFilesChange(({ event, raw }) => {
assert.ok(event);
assert.strictEqual(raw.length, 2);
assert.ok(e.contains(created, FileChangeType.ADDED));
assert.ok(!e.contains(created, FileChangeType.UPDATED));
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
assert.ok(event.contains(created, FileChangeType.ADDED));
assert.ok(!event.contains(created, FileChangeType.UPDATED));
assert.ok(event.contains(unrelated, FileChangeType.UPDATED));
done();
});
@@ -197,7 +196,7 @@ suite('Watcher Events Normalizer', () => {
watch.report(raw);
});
test('event normalization: apply DELETE', function (done: () => void) {
test('event normalization: apply DELETE', done => {
const watch = new TestFileWatcher();
const updated = uri.file('/users/data/src/related');
@@ -212,13 +211,13 @@ suite('Watcher Events Normalizer', () => {
{ path: updated.fsPath, type: FileChangeType.DELETED }
];
watch.onDidFilesChange(({ event: e, raw }) => {
assert.ok(e);
watch.onDidFilesChange(({ event, raw }) => {
assert.ok(event);
assert.strictEqual(raw.length, 2);
assert.ok(e.contains(deleted, FileChangeType.DELETED));
assert.ok(!e.contains(updated, FileChangeType.UPDATED));
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
assert.ok(event.contains(deleted, FileChangeType.DELETED));
assert.ok(!event.contains(updated, FileChangeType.UPDATED));
assert.ok(event.contains(unrelated, FileChangeType.UPDATED));
done();
});
@@ -226,35 +225,25 @@ suite('Watcher Events Normalizer', () => {
watch.report(raw);
});
test('event normalization: track case renames', function (done: () => void) {
test('event normalization: track case renames', done => {
const watch = new TestFileWatcher();
const updated = uri.file('/users/data/src/related');
const updated2 = uri.file('/users/data/src/Related');
const added = uri.file('/users/data/src/added');
const added2 = uri.file('/users/data/src/ADDED');
const oldPath = uri.file('/users/data/src/added');
const newPath = uri.file('/users/data/src/ADDED');
const raw: IDiskFileChange[] = [
{ path: updated.fsPath, type: FileChangeType.UPDATED },
{ path: updated2.fsPath, type: FileChangeType.UPDATED },
{ path: added.fsPath, type: FileChangeType.ADDED },
{ path: added2.fsPath, type: FileChangeType.ADDED },
{ path: newPath.fsPath, type: FileChangeType.ADDED },
{ path: oldPath.fsPath, type: FileChangeType.DELETED }
];
watch.onDidFilesChange(({ event: e, raw }) => {
assert.ok(e);
assert.strictEqual(raw.length, 4);
watch.onDidFilesChange(({ event, raw }) => {
assert.ok(event);
assert.strictEqual(raw.length, 2);
for (const r of raw) {
if (isEqual(r.resource, updated)) {
assert.strictEqual(r.type, isLinux ? FileChangeType.UPDATED : FileChangeType.DELETED);
} else if (isEqual(r.resource, updated2)) {
assert.strictEqual(r.type, FileChangeType.UPDATED);
} else if (isEqual(r.resource, added)) {
if (isEqual(r.resource, oldPath)) {
assert.strictEqual(r.type, isLinux ? FileChangeType.ADDED : FileChangeType.DELETED);
} else if (isEqual(r.resource, added2)) {
} else if (isEqual(r.resource, newPath)) {
assert.strictEqual(r.type, FileChangeType.ADDED);
} else {
assert.fail();
+1 -1
View File
@@ -289,7 +289,7 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native
maximized?: boolean;
accessibilitySupport?: boolean;
enableLegacyRecursiveWatcher?: boolean; // TODO@bpasero remove me once watcher is settled
legacyWatcher?: string; // TODO@bpasero remove me once watcher is settled
perfMarks: PerformanceMark[];
@@ -1270,7 +1270,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
os: { release: release(), hostname: hostname() },
zoomLevel: typeof windowConfig?.zoomLevel === 'number' ? windowConfig.zoomLevel : undefined,
enableLegacyRecursiveWatcher: this.configurationService.getValue('files.legacyWatcher'),
legacyWatcher: this.configurationService.getValue('files.legacyWatcher'),
autoDetectHighContrast: windowConfig?.autoDetectHighContrast ?? true,
accessibilitySupport: app.accessibilitySupportEnabled,
colorScheme: {
+1
View File
@@ -16,6 +16,7 @@ exports.collectModules = function () {
createModuleDescription('vs/platform/files/node/watcher/unix/watcherApp'),
createModuleDescription('vs/platform/files/node/watcher/nsfw/watcherApp'),
createModuleDescription('vs/platform/files/node/watcher/parcel/watcherApp'),
createModuleDescription('vs/platform/terminal/node/ptyHostMain'),
@@ -17,7 +17,7 @@ import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/file
import { BinaryFileEditor } from 'vs/workbench/contrib/files/browser/editors/binaryFileEditor';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { isLinux, isNative, isWeb, isWindows } from 'vs/base/common/platform';
import { isNative, isWeb, isWindows } from 'vs/base/common/platform';
import { ExplorerViewletViewsContribution } from 'vs/workbench/contrib/files/browser/explorerViewlet';
import { IEditorPaneRegistry, EditorPaneDescriptor } from 'vs/workbench/browser/editor';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
@@ -34,7 +34,6 @@ import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { IExplorerService } from 'vs/workbench/contrib/files/browser/files';
import { FileEditorInputSerializer, FileEditorWorkingCopyEditorHandler } from 'vs/workbench/contrib/files/browser/editors/fileEditorHandler';
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
import product from 'vs/platform/product/common/product';
class FileUriLabelContribution implements IWorkbenchContribution {
@@ -245,7 +244,7 @@ configurationRegistry.registerConfiguration({
'files.watcherExclude': {
'type': 'object',
'default': { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true },
'markdownDescription': nls.localize('watcherExclude', "Configure glob patterns of file paths to exclude from file watching. Patterns must match on absolute paths, i.e. prefix with `**/` or the full path to match properly and suffix with `/**` to match files within a path (for example `**/build/output/**` or `/Users/name/workspaces/project/build/output/**`). When you experience Code consuming lots of CPU time on startup, you can exclude large folders to reduce the initial load."),
'markdownDescription': nls.localize('watcherExclude', "Configure paths or glob patterns to exclude from file watching. Paths that are relative (for example `build/output`) will be resolved to an absolute path using the currently opened workspace. Glob patterns must match on absolute paths (i.e. prefix with `**/` or the full path and suffix with `/**` to match files within a path) to match properly (for example `**/build/output/**` or `/Users/name/workspaces/project/build/output/**`). When you experience the file watcher process consuming a lot of CPU, make sure to exclude large folders that are of less interest (such as build output folders)."),
'scope': ConfigurationScope.RESOURCE
},
'files.watcherInclude': {
@@ -254,12 +253,22 @@ configurationRegistry.registerConfiguration({
'type': 'string'
},
'default': [],
'description': nls.localize('watcherInclude', "Configure extra paths to watch for changes inside the workspace. By default, all workspace folders will be watched recursively, except for folders that are symbolic links. You can explicitly add absolute or relative paths to support watching folders that are symbolic links. Relative paths will be resolved against the workspace folder to form an absolute path."),
'description': nls.localize('watcherInclude', "Configure extra paths to watch for changes inside the workspace. By default, all workspace folders will be watched recursively, except for folders that are symbolic links. You can explicitly add absolute or relative paths to support watching folders that are symbolic links. Relative paths will be resolved to an absolute path using the currently opened workspace."),
'scope': ConfigurationScope.RESOURCE
},
'files.legacyWatcher': {
'type': 'boolean',
'default': product.quality === 'stable' && isLinux,
'type': 'string',
'enum': [
'on',
'off',
'default',
],
'markdownEnumDescriptions': [
nls.localize('files.legacyWatcher.on', "Enable the legacy file watcher in case you see issues with the new file watcher."),
nls.localize('files.legacyWatcher.off', "Disable the legacy file watcher and enable the new file watcher to benefit from its capabilities."),
nls.localize('files.legacyWatcher.default', "The new file watcher will be enabled if you are using insiders version or whenever you open multi-root workspaces."),
],
'default': 'default',
'description': nls.localize('legacyWatcher', "Controls the mechanism used for file watching. Only change this when you see issues related to file watching."),
},
'files.hotExit': hotExitConfiguration,
@@ -86,8 +86,8 @@ export class WorkspaceWatcher extends Disposable {
);
}
// Detect when the watcher shutsdown unexpectedly
else if (msg.indexOf('ESHUTDOWN') >= 0) {
// Detect when the watcher throws an error unexpectedly
else if (msg.indexOf('EUNKNOWN') >= 0) {
this.notificationService.prompt(
Severity.Warning,
localize('eshutdownError', "File changes watcher stopped unexpectedly. Please reload the window to enable the watcher again."),
@@ -26,7 +26,7 @@ interface IConfiguration extends IWindowsConfiguration {
debug?: { console?: { wordWrap?: boolean } };
editor?: { accessibilitySupport?: 'on' | 'off' | 'auto' };
security?: { workspace?: { trust?: { enabled?: boolean } } };
files?: { legacyWatcher?: boolean };
files?: { legacyWatcher?: string };
}
export class SettingsChangeRelauncher extends Disposable implements IWorkbenchContribution {
@@ -38,7 +38,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo
private updateMode: string | undefined;
private accessibilitySupport: 'on' | 'off' | 'auto' | undefined;
private workspaceTrustEnabled: boolean | undefined;
private legacyFileWatcher: boolean | undefined = undefined;
private legacyFileWatcher: string | undefined = undefined;
constructor(
@IHostService private readonly hostService: IHostService,
@@ -102,7 +102,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo
}
// Legacy File Watcher
if (typeof config.files?.legacyWatcher === 'boolean' && config.files.legacyWatcher !== this.legacyFileWatcher) {
if (typeof config.files?.legacyWatcher === 'string' && config.files.legacyWatcher !== this.legacyFileWatcher) {
this.legacyFileWatcher = config.files.legacyWatcher;
changed = true;
}
@@ -26,7 +26,7 @@ class DesktopMain extends SharedDesktopMain {
protected registerFileSystemProviders(environmentService: INativeWorkbenchEnvironmentService, fileService: IFileService, logService: ILogService, nativeHostService: INativeHostService): void {
// Local Files
const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService, nativeHostService, { enableLegacyRecursiveWatcher: this.configuration.enableLegacyRecursiveWatcher }));
const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService, nativeHostService, { legacyWatcher: this.configuration.legacyWatcher }));
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
// User Data Provider
+18
View File
@@ -417,6 +417,14 @@
dependencies:
"@octokit/openapi-types" "^10.2.2"
"@parcel/watcher@2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.0.tgz#ebe992a4838b35c3da9a568eb95a71cb26ddf551"
integrity sha512-ByalKmRRXNNAhwZ0X1r0XeIhh1jG8zgdlvjgHk9ZV3YxiersEGNQkwew+RfqJbIL4gOJfvC2ey6lg5kaeRainw==
dependencies:
node-addon-api "^3.2.1"
node-gyp-build "^4.3.0"
"@sindresorhus/is@^0.14.0":
version "0.14.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
@@ -6935,6 +6943,11 @@ node-addon-api@^3.0.0, node-addon-api@^3.0.2:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239"
integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==
node-addon-api@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
node-addon-api@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.2.0.tgz#117cbb5a959dff0992e1c586ae0393573e4d2a87"
@@ -6945,6 +6958,11 @@ node-fetch@^2.6.1:
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.2.tgz#986996818b73785e47b1965cc34eb093a1d464d0"
integrity sha512-aLoxToI6RfZ+0NOjmWAgn9+LEd30YCkJKFSyWacNZdEKTit/ZMcKjGkTRo8uWEsnIb/hfKecNPEbln02PdWbcA==
node-gyp-build@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3"
integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==
node-libs-browser@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"