Path: blob/main/src/vs/platform/files/common/diskFileSystemProvider.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { insert } from '../../../base/common/arrays.js';6import { ThrottledDelayer } from '../../../base/common/async.js';7import { onUnexpectedError } from '../../../base/common/errors.js';8import { Emitter } from '../../../base/common/event.js';9import { removeTrailingPathSeparator } from '../../../base/common/extpath.js';10import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';11import { normalize } from '../../../base/common/path.js';12import { URI } from '../../../base/common/uri.js';13import { IFileChange, IFileSystemProvider, IWatchOptions } from './files.js';14import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, ILogMessage, INonRecursiveWatchRequest, IRecursiveWatcherOptions, isRecursiveWatchRequest, IUniversalWatchRequest, reviveFileChanges } from './watcher.js';15import { ILogService, LogLevel } from '../../log/common/log.js';1617export interface IDiskFileSystemProviderOptions {18watcher?: {1920/**21* Extra options for the recursive file watching.22*/23recursive?: IRecursiveWatcherOptions;2425/**26* Forces all file watch requests to run through a27* single universal file watcher, both recursive28* and non-recursively.29*30* Enabling this option might cause some overhead,31* specifically the universal file watcher will run32* in a separate process given its complexity. Only33* enable it when you understand the consequences.34*/35forceUniversal?: boolean;36};37}3839export abstract class AbstractDiskFileSystemProvider extends Disposable implements40Pick<IFileSystemProvider, 'watch'>,41Pick<IFileSystemProvider, 'onDidChangeFile'>,42Pick<IFileSystemProvider, 'onDidWatchError'> {4344constructor(45protected readonly logService: ILogService,46private readonly options?: IDiskFileSystemProviderOptions47) {48super();49}5051protected readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());52readonly onDidChangeFile = this._onDidChangeFile.event;5354protected readonly _onDidWatchError = this._register(new Emitter<string>());55readonly onDidWatchError = this._onDidWatchError.event;5657watch(resource: URI, opts: IWatchOptions): IDisposable {58if (opts.recursive || this.options?.watcher?.forceUniversal) {59return this.watchUniversal(resource, opts);60}6162return this.watchNonRecursive(resource, opts);63}6465private getRefreshWatchersDelay(count: number): number {66if (count > 200) {67// If there are many requests to refresh, start to throttle68// the refresh to reduce pressure. We see potentially thousands69// of requests coming in on startup repeatedly so we take it easy.70return 500;71}7273// By default, use a short delay to keep watchers updating fast but still74// with a delay so that we can efficiently deduplicate requests or reuse75// existing watchers.76return 0;77}7879//#region File Watching (universal)8081private universalWatcher: AbstractUniversalWatcherClient | undefined;8283private readonly universalWatchRequests: IUniversalWatchRequest[] = [];84private readonly universalWatchRequestDelayer = this._register(new ThrottledDelayer<void>(this.getRefreshWatchersDelay(this.universalWatchRequests.length)));8586private watchUniversal(resource: URI, opts: IWatchOptions): IDisposable {87const request = this.toWatchRequest(resource, opts);88const remove = insert(this.universalWatchRequests, request);8990// Trigger update91this.refreshUniversalWatchers();9293return toDisposable(() => {9495// Remove from list of paths to watch universally96remove();9798// Trigger update99this.refreshUniversalWatchers();100});101}102103private toWatchRequest(resource: URI, opts: IWatchOptions): IUniversalWatchRequest {104const request: IUniversalWatchRequest = {105path: this.toWatchPath(resource),106excludes: opts.excludes,107includes: opts.includes,108recursive: opts.recursive,109filter: opts.filter,110correlationId: opts.correlationId111};112113if (isRecursiveWatchRequest(request)) {114115// Adjust for polling116const usePolling = this.options?.watcher?.recursive?.usePolling;117if (usePolling === true) {118request.pollingInterval = this.options?.watcher?.recursive?.pollingInterval ?? 5000;119} else if (Array.isArray(usePolling)) {120if (usePolling.includes(request.path)) {121request.pollingInterval = this.options?.watcher?.recursive?.pollingInterval ?? 5000;122}123}124}125126return request;127}128129private refreshUniversalWatchers(): void {130this.universalWatchRequestDelayer.trigger(() => {131return this.doRefreshUniversalWatchers();132}, this.getRefreshWatchersDelay(this.universalWatchRequests.length)).catch(error => onUnexpectedError(error));133}134135private doRefreshUniversalWatchers(): Promise<void> {136137// Create watcher if this is the first time138if (!this.universalWatcher) {139this.universalWatcher = this._register(this.createUniversalWatcher(140changes => this._onDidChangeFile.fire(reviveFileChanges(changes)),141msg => this.onWatcherLogMessage(msg),142this.logService.getLevel() === LogLevel.Trace143));144145// Apply log levels dynamically146this._register(this.logService.onDidChangeLogLevel(() => {147this.universalWatcher?.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);148}));149}150151// Ask to watch the provided paths152return this.universalWatcher.watch(this.universalWatchRequests);153}154155protected abstract createUniversalWatcher(156onChange: (changes: IFileChange[]) => void,157onLogMessage: (msg: ILogMessage) => void,158verboseLogging: boolean159): AbstractUniversalWatcherClient;160161//#endregion162163//#region File Watching (non-recursive)164165private nonRecursiveWatcher: AbstractNonRecursiveWatcherClient | undefined;166167private readonly nonRecursiveWatchRequests: INonRecursiveWatchRequest[] = [];168private readonly nonRecursiveWatchRequestDelayer = this._register(new ThrottledDelayer<void>(this.getRefreshWatchersDelay(this.nonRecursiveWatchRequests.length)));169170private watchNonRecursive(resource: URI, opts: IWatchOptions): IDisposable {171172// Add to list of paths to watch non-recursively173const request: INonRecursiveWatchRequest = {174path: this.toWatchPath(resource),175excludes: opts.excludes,176includes: opts.includes,177recursive: false,178filter: opts.filter,179correlationId: opts.correlationId180};181const remove = insert(this.nonRecursiveWatchRequests, request);182183// Trigger update184this.refreshNonRecursiveWatchers();185186return toDisposable(() => {187188// Remove from list of paths to watch non-recursively189remove();190191// Trigger update192this.refreshNonRecursiveWatchers();193});194}195196private refreshNonRecursiveWatchers(): void {197this.nonRecursiveWatchRequestDelayer.trigger(() => {198return this.doRefreshNonRecursiveWatchers();199}, this.getRefreshWatchersDelay(this.nonRecursiveWatchRequests.length)).catch(error => onUnexpectedError(error));200}201202private doRefreshNonRecursiveWatchers(): Promise<void> {203204// Create watcher if this is the first time205if (!this.nonRecursiveWatcher) {206this.nonRecursiveWatcher = this._register(this.createNonRecursiveWatcher(207changes => this._onDidChangeFile.fire(reviveFileChanges(changes)),208msg => this.onWatcherLogMessage(msg),209this.logService.getLevel() === LogLevel.Trace210));211212// Apply log levels dynamically213this._register(this.logService.onDidChangeLogLevel(() => {214this.nonRecursiveWatcher?.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);215}));216}217218// Ask to watch the provided paths219return this.nonRecursiveWatcher.watch(this.nonRecursiveWatchRequests);220}221222protected abstract createNonRecursiveWatcher(223onChange: (changes: IFileChange[]) => void,224onLogMessage: (msg: ILogMessage) => void,225verboseLogging: boolean226): AbstractNonRecursiveWatcherClient;227228//#endregion229230private onWatcherLogMessage(msg: ILogMessage): void {231if (msg.type === 'error') {232this._onDidWatchError.fire(msg.message);233}234235this.logWatcherMessage(msg);236}237238protected logWatcherMessage(msg: ILogMessage): void {239this.logService[msg.type](msg.message);240}241242protected toFilePath(resource: URI): string {243return normalize(resource.fsPath);244}245246private toWatchPath(resource: URI): string {247const filePath = this.toFilePath(resource);248249// Ensure to have any trailing path separators removed, otherwise250// we may believe the path is not "real" and will convert every251// event back to this form, which is not warranted.252// See also https://github.com/microsoft/vscode/issues/210517253return removeTrailingPathSeparator(filePath);254}255}256257258