nsfwWatcherService.ts 9.3 KB
Newer Older
D
Daniel Imms 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

D
Daniel Imms 已提交
6
import * as glob from 'vs/base/common/glob';
B
Benjamin Pasero 已提交
7
import * as extpath from 'vs/base/common/extpath';
8
import * as path from 'vs/base/common/path';
B
Benjamin Pasero 已提交
9
import * as platform from 'vs/base/common/platform';
10
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
R
Robo 已提交
11
import * as nsfw from 'nsfw';
12
import { IWatcherService, IWatcherRequest, IWatcherOptions } from 'vs/platform/files/node/watcher/nsfw/watcher';
13 14
import { ThrottledDelayer } from 'vs/base/common/async';
import { FileChangeType } from 'vs/platform/files/common/files';
15
import { normalizeNFC } from 'vs/base/common/normalization';
16
import { Event, Emitter } from 'vs/base/common/event';
B
Benjamin Pasero 已提交
17
import { realcaseSync, realpathSync } from 'vs/base/node/extpath';
18

19 20 21 22
const nsfwActionToRawChangeType: { [key: number]: number } = [];
nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED;
nsfwActionToRawChangeType[nsfw.actions.MODIFIED] = FileChangeType.UPDATED;
nsfwActionToRawChangeType[nsfw.actions.DELETED] = FileChangeType.DELETED;
D
Daniel Imms 已提交
23

24
interface IWatcherObjet {
25 26
	start(): any;
	stop(): any;
27
}
D
Daniel Imms 已提交
28 29

interface IPathWatcher {
J
Johannes Rieken 已提交
30
	ready: Promise<IWatcherObjet>;
31
	watcher?: IWatcherObjet;
32
	ignored: glob.ParsedPattern[];
D
Daniel Imms 已提交
33 34
}

D
Daniel Imms 已提交
35
export class NsfwWatcherService implements IWatcherService {
36
	private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
37

D
Daniel Imms 已提交
38
	private _pathWatchers: { [watchPath: string]: IPathWatcher } = {};
39
	private _verboseLogging: boolean;
B
Benjamin Pasero 已提交
40
	private enospcErrorLogged: boolean;
41

42
	private _onWatchEvent = new Emitter<IDiskFileChange[]>();
43
	readonly onWatchEvent = this._onWatchEvent.event;
B
Benjamin Pasero 已提交
44

45 46 47 48
	private _onLogMessage = new Emitter<ILogMessage>();
	readonly onLogMessage: Event<ILogMessage> = this._onLogMessage.event;

	watch(options: IWatcherOptions): Event<IDiskFileChange[]> {
49
		return this.onWatchEvent;
50
	}
D
Daniel Imms 已提交
51

52
	private _watch(request: IWatcherRequest): void {
53
		let undeliveredFileEvents: IDiskFileChange[] = [];
54
		const fileEventDelayer = new ThrottledDelayer<void>(NsfwWatcherService.FS_EVENT_DELAY);
55

B
Benjamin Pasero 已提交
56
		let readyPromiseResolve: (watcher: IWatcherObjet) => void;
57
		this._pathWatchers[request.path] = {
B
Benjamin Pasero 已提交
58
			ready: new Promise<IWatcherObjet>(resolve => readyPromiseResolve = resolve),
59
			ignored: Array.isArray(request.excludes) ? request.excludes.map(ignored => glob.parse(ignored)) : []
60
		};
D
Daniel Imms 已提交
61

62
		process.on('uncaughtException', (e: Error | string) => {
B
Benjamin Pasero 已提交
63 64 65 66 67 68 69 70

			// 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 (e === 'Inotify limit reached' && !this.enospcErrorLogged) {
				this.enospcErrorLogged = true;
71
				this.error('Inotify limit reached (ENOSPC)');
B
Benjamin Pasero 已提交
72 73 74
			}
		});

B
Benjamin Pasero 已提交
75 76 77 78 79
		// NSFW does not report file changes in the path provided on macOS if
		// - the path uses wrong casing
		// - the path is a symbolic link
		// We have to detect this case and massage the events to correct this.
		let realBasePathDiffers = false;
80
		let realBasePathLength = request.path.length;
B
Benjamin Pasero 已提交
81 82 83 84
		if (platform.isMacintosh) {
			try {

				// First check for symbolic link
85
				let realBasePath = realpathSync(request.path);
B
Benjamin Pasero 已提交
86 87

				// Second check for casing difference
88 89
				if (request.path === realBasePath) {
					realBasePath = (realcaseSync(request.path) || request.path);
B
Benjamin Pasero 已提交
90 91
				}

92
				if (request.path !== realBasePath) {
B
Benjamin Pasero 已提交
93 94 95
					realBasePathLength = realBasePath.length;
					realBasePathDiffers = true;

96
					this.warn(`Watcher basePath does not match version on disk and will be corrected (original: ${request.path}, real: ${realBasePath})`);
B
Benjamin Pasero 已提交
97 98 99 100 101 102
				}
			} catch (error) {
				// ignore
			}
		}

103
		nsfw(request.path, events => {
104
			for (const e of events) {
105 106
				// Logging
				if (this._verboseLogging) {
107
					const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : path.join(e.directory, e.file || '');
108
					this.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`);
109
				}
D
Daniel Imms 已提交
110

111 112 113 114
				// Convert nsfw event to IRawFileChange and add to queue
				let absolutePath: string;
				if (e.action === nsfw.actions.RENAMED) {
					// Rename fires when a file's name changes within a single directory
115
					absolutePath = path.join(e.directory, e.oldFile || '');
116
					if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
117
						undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath });
118
					} else if (this._verboseLogging) {
119
						this.log(` >> ignored ${absolutePath}`);
120
					}
R
Robo 已提交
121
					absolutePath = path.join(e.newDirectory || e.directory, e.newFile || '');
122
					if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
123
						undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath });
124
					} else if (this._verboseLogging) {
125
						this.log(` >> ignored ${absolutePath}`);
126 127
					}
				} else {
128
					absolutePath = path.join(e.directory, e.file || '');
129
					if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.path].ignored)) {
130 131 132 133
						undeliveredFileEvents.push({
							type: nsfwActionToRawChangeType[e.action],
							path: absolutePath
						});
134
					} else if (this._verboseLogging) {
135
						this.log(` >> ignored ${absolutePath}`);
136 137
					}
				}
138
			}
139

140 141 142 143
			// Delay and send buffer
			fileEventDelayer.trigger(() => {
				const events = undeliveredFileEvents;
				undeliveredFileEvents = [];
144

145
				if (platform.isMacintosh) {
B
Benjamin Pasero 已提交
146 147 148 149 150 151 152
					events.forEach(e => {

						// Mac uses NFD unicode form on disk, but we want NFC
						e.path = normalizeNFC(e.path);

						// Convert paths back to original form in case it differs
						if (realBasePathDiffers) {
153
							e.path = request.path + e.path.substr(realBasePathLength);
B
Benjamin Pasero 已提交
154 155
						}
					});
156 157
				}

158
				// Broadcast to clients normalized
159
				const res = normalizeFileChanges(events);
160
				this._onWatchEvent.fire(res);
161

162 163 164
				// Logging
				if (this._verboseLogging) {
					res.forEach(r => {
165
						this.log(` >> normalized ${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`);
166 167
					});
				}
168

R
Rob Lourens 已提交
169
				return Promise.resolve(undefined);
170
			});
171
		}).then(watcher => {
172
			this._pathWatchers[request.path].watcher = watcher;
173
			const startPromise = watcher.start();
B
Benjamin Pasero 已提交
174
			startPromise.then(() => readyPromiseResolve(watcher));
175
			return startPromise;
176
		});
177
	}
D
Daniel Imms 已提交
178

J
Johannes Rieken 已提交
179 180
	public setRoots(roots: IWatcherRequest[]): Promise<void> {
		const promises: Promise<void>[] = [];
181
		const normalizedRoots = this._normalizeRoots(roots);
D
Daniel Imms 已提交
182

183
		// Gather roots that are not currently being watched
184
		const rootsToStartWatching = normalizedRoots.filter(r => {
185
			return !(r.path in this._pathWatchers);
186 187
		});

188
		// Gather current roots that don't exist in the new roots array
189
		const rootsToStopWatching = Object.keys(this._pathWatchers).filter(r => {
190
			return normalizedRoots.every(normalizedRoot => normalizedRoot.path !== r);
191 192
		});

D
Daniel Imms 已提交
193
		// Logging
194
		if (this._verboseLogging) {
195
			this.log(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
D
Daniel Imms 已提交
196 197
		}

198 199 200 201 202 203 204
		// Stop watching some roots
		rootsToStopWatching.forEach(root => {
			this._pathWatchers[root].ready.then(watcher => watcher.stop());
			delete this._pathWatchers[root];
		});

		// Start watching some roots
205
		rootsToStartWatching.forEach(root => this._watch(root));
D
Daniel Imms 已提交
206

207 208
		// Refresh ignored arrays in case they changed
		roots.forEach(root => {
209 210
			if (root.path in this._pathWatchers) {
				this._pathWatchers[root.path].ignored = Array.isArray(root.excludes) ? root.excludes.map(ignored => glob.parse(ignored)) : [];
211 212 213
			}
		});

R
Rob Lourens 已提交
214
		return Promise.all(promises).then(() => undefined);
215 216
	}

J
Johannes Rieken 已提交
217
	public setVerboseLogging(enabled: boolean): Promise<void> {
218
		this._verboseLogging = enabled;
R
Rob Lourens 已提交
219
		return Promise.resolve(undefined);
220 221
	}

J
Johannes Rieken 已提交
222
	public stop(): Promise<void> {
223 224 225 226 227 228
		for (let path in this._pathWatchers) {
			let watcher = this._pathWatchers[path];
			watcher.ready.then(watcher => watcher.stop());
			delete this._pathWatchers[path];
		}
		this._pathWatchers = Object.create(null);
B
Benjamin Pasero 已提交
229
		return Promise.resolve();
230 231
	}

232
	/**
233 234
	 * Normalizes a set of root paths by removing any root paths that are
	 * sub-paths of other roots.
235
	 */
236
	protected _normalizeRoots(roots: IWatcherRequest[]): IWatcherRequest[] {
237
		return roots.filter(r => roots.every(other => {
238
			return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path));
239 240 241
		}));
	}

242 243
	private _isPathIgnored(absolutePath: string, ignored: glob.ParsedPattern[]): boolean {
		return ignored && ignored.some(i => i(absolutePath));
D
Daniel Imms 已提交
244
	}
245 246 247 248 249 250 251 252 253 254 255 256

	private log(message: string) {
		this._onLogMessage.fire({ type: 'trace', message: `[File Watcher (nswf)] ` + message });
	}

	private warn(message: string) {
		this._onLogMessage.fire({ type: 'warn', message: `[File Watcher (nswf)] ` + message });
	}

	private error(message: string) {
		this._onLogMessage.fire({ type: 'error', message: `[File Watcher (nswf)] ` + message });
	}
D
Daniel Imms 已提交
257
}