Path: blob/main/src/vs/workbench/contrib/files/browser/workspaceWatcher.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 { localize } from '../../../../nls.js';6import { IDisposable, Disposable, dispose, DisposableStore } from '../../../../base/common/lifecycle.js';7import { URI } from '../../../../base/common/uri.js';8import { IConfigurationService, IConfigurationChangeEvent } from '../../../../platform/configuration/common/configuration.js';9import { IFileService, IFilesConfiguration } from '../../../../platform/files/common/files.js';10import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from '../../../../platform/workspace/common/workspace.js';11import { ResourceMap } from '../../../../base/common/map.js';12import { INotificationService, Severity, NeverShowAgainScope, NotificationPriority } from '../../../../platform/notification/common/notification.js';13import { IOpenerService } from '../../../../platform/opener/common/opener.js';14import { isAbsolute } from '../../../../base/common/path.js';15import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';16import { IHostService } from '../../../services/host/browser/host.js';17import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';1819export class WorkspaceWatcher extends Disposable {2021static readonly ID = 'workbench.contrib.workspaceWatcher';2223private readonly watchedWorkspaces = new ResourceMap<IDisposable>(resource => this.uriIdentityService.extUri.getComparisonKey(resource));2425constructor(26@IFileService private readonly fileService: IFileService,27@IConfigurationService private readonly configurationService: IConfigurationService,28@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,29@INotificationService private readonly notificationService: INotificationService,30@IOpenerService private readonly openerService: IOpenerService,31@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,32@IHostService private readonly hostService: IHostService,33@ITelemetryService private readonly telemetryService: ITelemetryService34) {35super();3637this.registerListeners();3839this.refresh();40}4142private registerListeners(): void {43this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onDidChangeWorkspaceFolders(e)));44this._register(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState()));45this._register(this.configurationService.onDidChangeConfiguration(e => this.onDidChangeConfiguration(e)));46this._register(this.fileService.onDidWatchError(error => this.onDidWatchError(error)));47}4849private onDidChangeWorkspaceFolders(e: IWorkspaceFoldersChangeEvent): void {5051// Removed workspace: Unwatch52for (const removed of e.removed) {53this.unwatchWorkspace(removed);54}5556// Added workspace: Watch57for (const added of e.added) {58this.watchWorkspace(added);59}60}6162private onDidChangeWorkbenchState(): void {63this.refresh();64}6566private onDidChangeConfiguration(e: IConfigurationChangeEvent): void {67if (e.affectsConfiguration('files.watcherExclude') || e.affectsConfiguration('files.watcherInclude')) {68this.refresh();69}70}7172private onDidWatchError(error: Error): void {73const msg = error.toString();74let reason: 'ENOSPC' | 'EUNKNOWN' | 'ETERM' | undefined = undefined;7576// Detect if we run into ENOSPC issues77if (msg.indexOf('ENOSPC') >= 0) {78reason = 'ENOSPC';7980this.notificationService.prompt(81Severity.Warning,82localize('enospcError', "Unable to watch for file changes. Please follow the instructions link to resolve this issue."),83[{84label: localize('learnMore', "Instructions"),85run: () => this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=867693'))86}],87{88sticky: true,89neverShowAgain: { id: 'ignoreEnospcError', isSecondary: true, scope: NeverShowAgainScope.WORKSPACE }90}91);92}9394// Detect when the watcher throws an error unexpectedly95else if (msg.indexOf('EUNKNOWN') >= 0) {96reason = 'EUNKNOWN';9798this.notificationService.prompt(99Severity.Warning,100localize('eshutdownError', "File changes watcher stopped unexpectedly. A reload of the window may enable the watcher again unless the workspace cannot be watched for file changes."),101[{102label: localize('reload', "Reload"),103run: () => this.hostService.reload()104}],105{106sticky: true,107priority: NotificationPriority.SILENT // reduce potential spam since we don't really know how often this fires108}109);110}111112// Detect unexpected termination113else if (msg.indexOf('ETERM') >= 0) {114reason = 'ETERM';115}116117// Log telemetry if we gathered a reason (logging it from the renderer118// allows us to investigate this situation in context of experiments)119if (reason) {120type WatchErrorClassification = {121owner: 'bpasero';122comment: 'An event that fires when a watcher errors';123reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The watcher error reason.' };124};125type WatchErrorEvent = {126reason: string;127};128this.telemetryService.publicLog2<WatchErrorEvent, WatchErrorClassification>('fileWatcherError', { reason });129}130}131132private watchWorkspace(workspace: IWorkspaceFolder): void {133134// Compute the watcher exclude rules from configuration135const excludes: string[] = [];136const config = this.configurationService.getValue<IFilesConfiguration>({ resource: workspace.uri });137if (config.files?.watcherExclude) {138for (const key in config.files.watcherExclude) {139if (key && config.files.watcherExclude[key] === true) {140excludes.push(key);141}142}143}144145const pathsToWatch = new ResourceMap<URI>(uri => this.uriIdentityService.extUri.getComparisonKey(uri));146147// Add the workspace as path to watch148pathsToWatch.set(workspace.uri, workspace.uri);149150// Compute additional includes from configuration151if (config.files?.watcherInclude) {152for (const includePath of config.files.watcherInclude) {153if (!includePath) {154continue;155}156157// Absolute: verify a child of the workspace158if (isAbsolute(includePath)) {159const candidate = URI.file(includePath).with({ scheme: workspace.uri.scheme });160if (this.uriIdentityService.extUri.isEqualOrParent(candidate, workspace.uri)) {161pathsToWatch.set(candidate, candidate);162}163}164165// Relative: join against workspace folder166else {167const candidate = workspace.toResource(includePath);168pathsToWatch.set(candidate, candidate);169}170}171}172173// Watch all paths as instructed174const disposables = new DisposableStore();175for (const [, pathToWatch] of pathsToWatch) {176disposables.add(this.fileService.watch(pathToWatch, { recursive: true, excludes }));177}178this.watchedWorkspaces.set(workspace.uri, disposables);179}180181private unwatchWorkspace(workspace: IWorkspaceFolder): void {182if (this.watchedWorkspaces.has(workspace.uri)) {183dispose(this.watchedWorkspaces.get(workspace.uri));184this.watchedWorkspaces.delete(workspace.uri);185}186}187188private refresh(): void {189190// Unwatch all first191this.unwatchWorkspaces();192193// Watch each workspace folder194for (const folder of this.contextService.getWorkspace().folders) {195this.watchWorkspace(folder);196}197}198199private unwatchWorkspaces(): void {200for (const [, disposable] of this.watchedWorkspaces) {201disposable.dispose();202}203this.watchedWorkspaces.clear();204}205206override dispose(): void {207super.dispose();208209this.unwatchWorkspaces();210}211}212213214