Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update nsfw and watcher service #133826

Merged
merged 2 commits into from Sep 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -78,7 +78,7 @@
"sudo-prompt": "9.2.1",
"tas-client-umd": "0.1.4",
"v8-inspect-profiler": "^0.0.22",
"vscode-nsfw": "2.1.4",
"vscode-nsfw": "2.1.6",
"vscode-oniguruma": "1.5.1",
"vscode-proxy-agent": "^0.11.0",
"vscode-regexpp": "^3.1.0",
Expand Down
2 changes: 1 addition & 1 deletion remote/package.json
Expand Up @@ -18,7 +18,7 @@
"node-pty": "0.11.0-beta7",
"spdlog": "^0.13.0",
"tas-client-umd": "0.1.4",
"vscode-nsfw": "2.1.4",
"vscode-nsfw": "2.1.6",
"vscode-oniguruma": "1.5.1",
"vscode-proxy-agent": "^0.11.0",
"vscode-regexpp": "^3.1.0",
Expand Down
8 changes: 4 additions & 4 deletions remote/yarn.lock
Expand Up @@ -556,10 +556,10 @@ universalify@^0.1.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==

vscode-nsfw@2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/vscode-nsfw/-/vscode-nsfw-2.1.4.tgz#8965413e41a7099e36813921d3b468c12d1b6e59"
integrity sha512-EsYWpACwojUjLgJFPhtnk/omId+oyuHpctWQiCLblf53eOxzYC7HT30NljEigl/sFmuTZM0IEMrsonj3GKDYPw==
vscode-nsfw@2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/vscode-nsfw/-/vscode-nsfw-2.1.6.tgz#51148fad0e7ff3b72fe0b3500903ab24af00f308"
integrity sha512-3RCPQ1/yBvwJuaUG6bJMOcmlRKuDHFn2xx04u8hWYOkNQODK7LncP5qXwU/NNN32/Vgp/cDy4P1o5HoGAHFOfg==
dependencies:
node-addon-api "*"

Expand Down
83 changes: 35 additions & 48 deletions src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts
Expand Up @@ -5,7 +5,7 @@

import * as nsfw from 'vscode-nsfw';
import { existsSync } from 'fs';
import { RunOnceScheduler, ThrottledDelayer } from 'vs/base/common/async';
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';
Expand Down Expand Up @@ -52,8 +52,6 @@ interface IWatcher extends IDisposable {

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 static readonly MAX_RESTARTS = 5; // number of restarts we allow before giving up in case of unexpected shutdown

private static readonly MAP_NSFW_ACTION_TO_FILE_CHANGE = new Map<number, number>(
Expand Down Expand Up @@ -134,9 +132,6 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
private startWatching(request: IWatchRequest, restarts = 0): void {
const cts = new CancellationTokenSource();

let undeliveredFileEvents: IDiskFileChange[] = [];
const fileEventDelayer = new ThrottledDelayer<void>(NsfwWatcherService.FS_EVENT_DELAY);

let nsfwPromiseResolve: (watcher: nsfw.NSFW) => void;
const instance = new Promise<nsfw.NSFW>(resolve => nsfwPromiseResolve = resolve);

Expand All @@ -149,7 +144,6 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
token: cts.token,
dispose: () => {
cts.dispose(true);
fileEventDelayer.dispose();
instance.then(instance => instance.stop());
}
};
Expand All @@ -158,7 +152,9 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
// Path checks for symbolic links / wrong casing
const { realBasePathDiffers, realBasePathLength } = this.checkRequest(request);

const onFileEvent = (path: string, type: FileChangeType) => {
let undeliveredFileEvents: IDiskFileChange[] = [];

const onRawFileEvent = (path: string, type: FileChangeType) => {
if (!this.isPathIgnored(path, watcher.ignored)) {
undeliveredFileEvents.push({ type, path });
} else if (this.verboseLogging) {
Expand All @@ -167,43 +163,37 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
};

nsfw(request.path, events => {
if (watcher.token.isCancellationRequested) {
return; // return early when disposed
}

for (const event of events) {
if (watcher.token.isCancellationRequested) {
break; // return early when disposed
}

// Logging
// 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}`);
}

// Rename: convert into DELETE & ADD
if (event.action === nsfw.actions.RENAMED) {
onFileEvent(join(event.directory, event.oldFile || ''), FileChangeType.DELETED); // Rename fires when a file's name changes within a single directory
onFileEvent(join(event.newDirectory || event.directory, event.newFile || ''), FileChangeType.ADDED);
onRawFileEvent(join(event.directory, event.oldFile || ''), FileChangeType.DELETED); // Rename fires when a file's name changes within a single directory
onRawFileEvent(join(event.newDirectory || event.directory, event.newFile || ''), FileChangeType.ADDED);
}

// Created, modified, deleted: taks as is
// Created, modified, deleted: take as is
else {
onFileEvent(join(event.directory, event.file || ''), NsfwWatcherService.MAP_NSFW_ACTION_TO_FILE_CHANGE.get(event.action)!);
onRawFileEvent(join(event.directory, event.file || ''), NsfwWatcherService.MAP_NSFW_ACTION_TO_FILE_CHANGE.get(event.action)!);
}
}

// Send events delayed and normalized
fileEventDelayer.trigger(async () => {
if (watcher.token.isCancellationRequested) {
return; // return early when disposed
}
// Reset undelivered events array
const undeliveredFileEventsToEmit = undeliveredFileEvents;
undeliveredFileEvents = [];

// Remember as delivered
const events = undeliveredFileEvents;
undeliveredFileEvents = [];

// Broadcast to clients normalized
const normalizedEvents = normalizeFileChanges(this.normalizeEvents(events, request, realBasePathDiffers, realBasePathLength));
this.emitEvents(normalizedEvents);
});
// Broadcast to clients normalized
const normalizedEvents = normalizeFileChanges(this.normalizeEvents(undeliveredFileEventsToEmit, request, realBasePathDiffers, realBasePathLength));
this.emitEvents(normalizedEvents);
}, this.getOptions(watcher)).then(async nsfwWatcher => {

// Begin watching unless disposed already
Expand All @@ -230,9 +220,9 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
}
},

// The default delay of NSFW is 500 but we already do some
// debouncing in our code so we reduce the delay slightly
debounceMS: 100
// The default delay of NSFW is 500 but we want to
// react a bit faster than that.
debounceMS: 250
};
}

Expand Down Expand Up @@ -309,36 +299,33 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
// See https://github.com/microsoft/vscode/issues/7950
if (msg.indexOf('Inotify limit reached') !== -1) {
if (!this.enospcErrorLogged) {
this.enospcErrorLogged = true; // only log this error once to protect against log spam
this.error('Inotify limit reached (ENOSPC)', watcher);
}
}

// Specially handle this error that indicates the watcher
// has stopped and we need to restart it.
else if (msg.indexOf('Service shutdown unexpectedly') !== -1) {
const handled = this.onUnexpectedShutdown(watcher);
if (!handled) {
this.error('Watcher service shutdown unexpectedly (ESHUTDOWN)', watcher);
this.enospcErrorLogged = true;
}
}

// Log any other error
// Any other error is unexpected and we should try to
// restart the watcher as a result to get into healthy
// state again.
else {
this.error(msg, watcher);
const handled = this.onUnexpectedError(msg, watcher);
if (!handled) {
this.error(`Unexpected error: ${msg} (ESHUTDOWN)`, watcher);
}
}
}

private onUnexpectedShutdown(watcher?: IWatcher): boolean {
private onUnexpectedError(error: string, watcher?: IWatcher): boolean {
if (!watcher || watcher.restarts >= NsfwWatcherService.MAX_RESTARTS) {
return false; // we need a watcher that has not been restarted 5 times already
return false; // we need a watcher that has not been restarted MAX_RESTARTS times already
}

let handled = false;

// Just try to restart watcher now if the path still exists
if (existsSync(watcher.request.path)) {
this.warn('Watcher service shutdown unexpectedly and will be restarted', watcher);
this.warn(`Watcher will be restarted due to unexpected error: ${error}`, watcher);
this.restartWatching(watcher);

handled = true;
Expand All @@ -354,7 +341,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {
}

private onWatchedPathDeleted(watcher: IWatcher): boolean {
this.warn('Watcher service shutdown unexpectedly because watched path got deleted', watcher);
this.warn('Watcher shutdown because watched path got deleted', watcher);

// Send a manual event given we know the root got deleted
this.emitEvents([{ path: watcher.request.path, type: FileChangeType.DELETED }]);
Expand All @@ -368,7 +355,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService {

// Watcher path came back! Restart watching...
if (path === watcher.request.path && (type === 'added' || type === 'changed')) {
this.warn('Watcher service restarts for watched path got created again', watcher);
this.warn('Watcher restarts because watched path got created again', watcher);

// Stop watching that parent folder
disposable.dispose();
Expand Down
28 changes: 18 additions & 10 deletions src/vs/platform/files/test/node/recursiveWatcher.test.ts
Expand Up @@ -5,6 +5,7 @@

import * as assert from 'assert';
import { tmpdir } from 'os';
import { timeout } from 'vs/base/common/async';
import { join } from 'vs/base/common/path';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { Promises } from 'vs/base/node/pfs';
Expand Down Expand Up @@ -38,25 +39,31 @@ flakySuite('Recursive Watcher', () => {
protected override getOptions(watcher: any) {
return {
...super.getOptions(watcher),

// required to let tests pass properly. with a smaller value
// somehow `fsevents` on macOS seems to report events from
// the past so that the event listening will assume wrongly
debounceMS: 500
debounceMS: 1
};
}
}

let testDir: string;
let service: TestNsfwWatcherService;
let enableLogging = false;

let loggingEnabled = false;

function enableLogging(enable: boolean) {
loggingEnabled = enable;
service?.setVerboseLogging(enable);
}

enableLogging(false);

setup(async () => {
service = new TestNsfwWatcherService();

if (enableLogging) {
service.onDidLogMessage(e => console.log(`[recursive watcher test message] ${e.message}`));
}
service.onDidLogMessage(e => {
if (loggingEnabled) {
console.log(`[recursive watcher test message] ${e.message}`);
}
});

testDir = getRandomTestPath(tmpdir(), 'vsctests', 'filewatcher');

Expand Down Expand Up @@ -120,7 +127,7 @@ flakySuite('Recursive Watcher', () => {
await Promises.rename(newFolderPath, renamedFolderPath);
await changeFuture;

// TODO: case rename is currently broken on macOS (https://github.com/Axosoft/nsfw/issues/146)
// Case rename is currently broken on macOS (https://github.com/Axosoft/nsfw/issues/146)
if (isWindows) {

// Rename file (same name, different case)
Expand Down Expand Up @@ -175,6 +182,7 @@ 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;
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Expand Up @@ -10408,10 +10408,10 @@ vscode-nls-dev@^3.3.1:
xml2js "^0.4.19"
yargs "^13.2.4"

vscode-nsfw@2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/vscode-nsfw/-/vscode-nsfw-2.1.4.tgz#8965413e41a7099e36813921d3b468c12d1b6e59"
integrity sha512-EsYWpACwojUjLgJFPhtnk/omId+oyuHpctWQiCLblf53eOxzYC7HT30NljEigl/sFmuTZM0IEMrsonj3GKDYPw==
vscode-nsfw@2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/vscode-nsfw/-/vscode-nsfw-2.1.6.tgz#51148fad0e7ff3b72fe0b3500903ab24af00f308"
integrity sha512-3RCPQ1/yBvwJuaUG6bJMOcmlRKuDHFn2xx04u8hWYOkNQODK7LncP5qXwU/NNN32/Vgp/cDy4P1o5HoGAHFOfg==
dependencies:
node-addon-api "*"

Expand Down