UNPKG

@theia/filesystem

Version:
402 lines • 15.7 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2017-2018 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** Object.defineProperty(exports, "__esModule", { value: true }); exports.ParcelFileSystemWatcherService = exports.ParcelWatcher = exports.WatcherDisposal = exports.ParcelFileSystemWatcherServerOptions = void 0; const path = require("path"); const fs_1 = require("fs"); const minimatch_1 = require("minimatch"); const file_uri_1 = require("@theia/core/lib/common/file-uri"); const file_change_collection_1 = require("../file-change-collection"); const promise_util_1 = require("@theia/core/lib/common/promise-util"); const watcher_1 = require("@theia/core/shared/@parcel/watcher"); exports.ParcelFileSystemWatcherServerOptions = Symbol('ParcelFileSystemWatcherServerOptions'); /** * This is a flag value passed around upon disposal. */ exports.WatcherDisposal = Symbol('WatcherDisposal'); /** * Because URIs can be watched by different clients, we'll track * how many are listening for a given URI. * * This component wraps the whole start/stop process given some * reference count. * * Once there are no more references the handle * will wait for some time before destroying its resources. */ class ParcelWatcher { constructor( /** Initial reference to this handle. */ initialClientId, /** Filesystem path to be watched. */ fsPath, /** Watcher-specific options */ watcherOptions, /** Logging and parcel watcher options */ parcelFileSystemWatchServerOptions, /** The client to forward events to. */ fileSystemWatcherClient, /** Amount of time in ms to wait once this handle is not referenced anymore. */ deferredDisposalTimeout = 10000) { this.fsPath = fsPath; this.watcherOptions = watcherOptions; this.parcelFileSystemWatchServerOptions = parcelFileSystemWatchServerOptions; this.fileSystemWatcherClient = fileSystemWatcherClient; this.deferredDisposalTimeout = deferredDisposalTimeout; this.disposed = false; /** * Used for debugging to keep track of the watchers. */ this.debugId = ParcelWatcher.debugIdSequence++; /** * This deferred only rejects with `WatcherDisposal` and never resolves. */ this.deferredDisposalDeferred = new promise_util_1.Deferred(); /** * We count each reference made to this watcher, per client. * * We do this to know where to send events via the network. * * An entry should be removed when its value hits zero. */ this.refsPerClient = new Map(); /** * Ensures that events are processed in the order they are emitted, * despite being processed async. */ this.parcelEventProcessingQueue = Promise.resolve(); /** * Resolves once this handle disposed itself and its resources. Never throws. */ this.whenDisposed = this.deferredDisposalDeferred.promise.catch(() => undefined); this.refsPerClient.set(initialClientId, { value: 1 }); this.whenStarted = this.start().then(() => true, error => { if (error === exports.WatcherDisposal) { return false; } this._dispose(); this.fireError(); throw error; }); this.debug('NEW', `initialClientId=${initialClientId}`); } addRef(clientId) { let refs = this.refsPerClient.get(clientId); if (typeof refs === 'undefined') { this.refsPerClient.set(clientId, refs = { value: 1 }); } else { refs.value += 1; } const totalRefs = this.getTotalReferences(); // If it was zero before, 1 means we were revived: const revived = totalRefs === 1; if (revived) { this.onRefsRevive(); } this.debug('REF++', `clientId=${clientId}, clientRefs=${refs.value}, totalRefs=${totalRefs}. revived=${revived}`); } removeRef(clientId) { const refs = this.refsPerClient.get(clientId); if (typeof refs === 'undefined') { this.info('WARN REF--', `removed one too many reference: clientId=${clientId}`); return; } refs.value -= 1; // We must remove the key from `this.clientReferences` because // we list active clients by reading the keys of this map. if (refs.value === 0) { this.refsPerClient.delete(clientId); } const totalRefs = this.getTotalReferences(); const dead = totalRefs === 0; if (dead) { this.onRefsReachZero(); } this.debug('REF--', `clientId=${clientId}, clientRefs=${refs.value}, totalRefs=${totalRefs}, dead=${dead}`); } /** * All clients with at least one active reference. */ getClientIds() { return Array.from(this.refsPerClient.keys()); } /** * Add the references for each client together. */ getTotalReferences() { let total = 0; for (const refs of this.refsPerClient.values()) { total += refs.value; } return total; } /** * Returns true if at least one client listens to this handle. */ isInUse() { return this.refsPerClient.size > 0; } /** * @throws with {@link WatcherDisposal} if this instance is disposed. */ assertNotDisposed() { if (this.disposed) { throw exports.WatcherDisposal; } } /** * When starting a watcher, we'll first check and wait for the path to exists * before running a parcel watcher. */ async start() { while (await fs_1.promises.stat(this.fsPath).then(() => false, () => true)) { await (0, promise_util_1.timeout)(500); this.assertNotDisposed(); } this.assertNotDisposed(); const watcher = await this.createWatcher(); this.assertNotDisposed(); this.debug('STARTED', `disposed=${this.disposed}`); // The watcher could be disposed while it was starting, make sure to check for this: if (this.disposed) { await this.stopWatcher(watcher); throw exports.WatcherDisposal; } this.watcher = watcher; } /** * Given a started parcel watcher instance, gracefully shut it down. */ async stopWatcher(watcher) { await watcher.unsubscribe() .then(() => 'success=true', error => error) .then(status => this.debug('STOPPED', status)); } async createWatcher() { let fsPath = await fs_1.promises.realpath(this.fsPath); if ((await fs_1.promises.stat(fsPath)).isFile()) { fsPath = path.dirname(fsPath); } return (0, watcher_1.subscribe)(fsPath, (err, events) => { if (err) { if (err.message && err.message.includes('File system must be re-scanned')) { console.log(`FS Events were dropped on watcher ${fs_1.promises}`); } else { console.error(`Watcher service error on "${fsPath}":`, err); this._dispose(); this.fireError(); return; } } if (events) { this.handleWatcherEvents(events); } }, { ...this.parcelFileSystemWatchServerOptions.parcelOptions }); } handleWatcherEvents(events) { // Only process events if someone is listening. if (this.isInUse()) { // This callback is async, but parcel won't wait for it to finish before firing the next one. // We will use a lock/queue to make sure everything is processed in the order it arrives. this.parcelEventProcessingQueue = this.parcelEventProcessingQueue.then(async () => { const fileChangeCollection = new file_change_collection_1.FileChangeCollection(); for (const event of events) { const filePath = event.path; if (event.type === 'create') { this.pushFileChange(fileChangeCollection, 1 /* FileChangeType.ADDED */, filePath); } else if (event.type === 'delete') { this.pushFileChange(fileChangeCollection, 2 /* FileChangeType.DELETED */, filePath); } else if (event.type === 'update') { this.pushFileChange(fileChangeCollection, 0 /* FileChangeType.UPDATED */, filePath); } } const changes = fileChangeCollection.values(); // If all changes are part of the ignored files, the collection will be empty. if (changes.length > 0) { this.fileSystemWatcherClient.onDidFilesChanged({ clients: this.getClientIds(), changes, }); } }, console.error); } } async resolveEventPath(directory, file) { // parcel already resolves symlinks, the paths should be clean already: return path.resolve(directory, file); } pushFileChange(changes, type, filePath) { if (!this.isIgnored(filePath)) { const uri = file_uri_1.FileUri.create(filePath).toString(); changes.push({ type, uri }); } } fireError() { this.fileSystemWatcherClient.onError({ clients: this.getClientIds(), uri: this.fsPath, }); } /** * When references hit zero, we'll schedule disposal for a bit later. * * This allows new references to reuse this watcher instead of creating a new one. * * e.g. A frontend disconnects for a few milliseconds before reconnecting again. */ onRefsReachZero() { this.deferredDisposalTimer = setTimeout(() => this._dispose(), this.deferredDisposalTimeout); } /** * If we get new references after hitting zero, let's unschedule our disposal and keep watching. */ onRefsRevive() { if (this.deferredDisposalTimer) { clearTimeout(this.deferredDisposalTimer); this.deferredDisposalTimer = undefined; } } isIgnored(filePath) { return this.watcherOptions.ignored.length > 0 && this.watcherOptions.ignored.some(m => m.match(filePath)); } /** * Internal disposal mechanism. */ async _dispose() { if (!this.disposed) { this.disposed = true; this.deferredDisposalDeferred.reject(exports.WatcherDisposal); if (this.watcher) { this.stopWatcher(this.watcher); this.watcher = undefined; } this.debug('DISPOSED'); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any info(prefix, ...params) { this.parcelFileSystemWatchServerOptions.info(`${prefix} ParcelWatcher(${this.debugId} at "${this.fsPath}"):`, ...params); } // eslint-disable-next-line @typescript-eslint/no-explicit-any debug(prefix, ...params) { if (this.parcelFileSystemWatchServerOptions.verbose) { this.info(prefix, ...params); } } } exports.ParcelWatcher = ParcelWatcher; ParcelWatcher.debugIdSequence = 0; class ParcelFileSystemWatcherService { constructor(options) { this.watcherId = 0; this.watchers = new Map(); this.watcherHandles = new Map(); /** * `this.client` is undefined until someone sets it. */ this.maybeClient = { onDidFilesChanged: event => { var _a; return (_a = this.client) === null || _a === void 0 ? void 0 : _a.onDidFilesChanged(event); }, onError: event => { var _a; return (_a = this.client) === null || _a === void 0 ? void 0 : _a.onError(event); }, }; this.options = { parcelOptions: {}, verbose: false, info: (message, ...args) => console.info(message, ...args), error: (message, ...args) => console.error(message, ...args), ...options }; } setClient(client) { this.client = client; } /** * A specific client requests us to watch a given `uri` according to some `options`. * * We internally re-use all the same `(uri, options)` pairs. */ async watchFileChanges(clientId, uri, options) { const resolvedOptions = this.resolveWatchOptions(options); const watcherKey = this.getWatcherKey(uri, resolvedOptions); let watcher = this.watchers.get(watcherKey); if (watcher === undefined) { const fsPath = file_uri_1.FileUri.fsPath(uri); watcher = this.createWatcher(clientId, fsPath, resolvedOptions); watcher.whenDisposed.then(() => this.watchers.delete(watcherKey)); this.watchers.set(watcherKey, watcher); } else { watcher.addRef(clientId); } const watcherId = this.watcherId++; this.watcherHandles.set(watcherId, { clientId, watcher }); watcher.whenDisposed.then(() => this.watcherHandles.delete(watcherId)); return watcherId; } createWatcher(clientId, fsPath, options) { const watcherOptions = { ignored: options.ignored .map(pattern => new minimatch_1.Minimatch(pattern, { dot: true })), }; return new ParcelWatcher(clientId, fsPath, watcherOptions, this.options, this.maybeClient); } async unwatchFileChanges(watcherId) { const handle = this.watcherHandles.get(watcherId); if (handle === undefined) { console.warn(`tried to de-allocate a disposed watcher: watcherId=${watcherId}`); } else { this.watcherHandles.delete(watcherId); handle.watcher.removeRef(handle.clientId); } } /** * Given some `URI` and some `WatchOptions`, generate a unique key. */ getWatcherKey(uri, options) { return [ uri, options.ignored.slice(0).sort().join() // use a **sorted copy** of `ignored` as part of the key ].join(); } /** * Return fully qualified options. */ resolveWatchOptions(options) { return { ignored: [], ...options, }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any debug(message, ...params) { if (this.options.verbose) { this.options.info(message, ...params); } } dispose() { // Singletons shouldn't be disposed... } } exports.ParcelFileSystemWatcherService = ParcelFileSystemWatcherService; //# sourceMappingURL=parcel-filesystem-service.js.map