Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/files/browser/workspaceWatcher.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { localize } from '../../../../nls.js';
7
import { IDisposable, Disposable, dispose, DisposableStore } from '../../../../base/common/lifecycle.js';
8
import { URI } from '../../../../base/common/uri.js';
9
import { IConfigurationService, IConfigurationChangeEvent } from '../../../../platform/configuration/common/configuration.js';
10
import { IFileService, IFilesConfiguration } from '../../../../platform/files/common/files.js';
11
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from '../../../../platform/workspace/common/workspace.js';
12
import { ResourceMap } from '../../../../base/common/map.js';
13
import { INotificationService, Severity, NeverShowAgainScope, NotificationPriority } from '../../../../platform/notification/common/notification.js';
14
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
15
import { isAbsolute } from '../../../../base/common/path.js';
16
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
17
import { IHostService } from '../../../services/host/browser/host.js';
18
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
19
20
export class WorkspaceWatcher extends Disposable {
21
22
static readonly ID = 'workbench.contrib.workspaceWatcher';
23
24
private readonly watchedWorkspaces = new ResourceMap<IDisposable>(resource => this.uriIdentityService.extUri.getComparisonKey(resource));
25
26
constructor(
27
@IFileService private readonly fileService: IFileService,
28
@IConfigurationService private readonly configurationService: IConfigurationService,
29
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
30
@INotificationService private readonly notificationService: INotificationService,
31
@IOpenerService private readonly openerService: IOpenerService,
32
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
33
@IHostService private readonly hostService: IHostService,
34
@ITelemetryService private readonly telemetryService: ITelemetryService
35
) {
36
super();
37
38
this.registerListeners();
39
40
this.refresh();
41
}
42
43
private registerListeners(): void {
44
this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onDidChangeWorkspaceFolders(e)));
45
this._register(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState()));
46
this._register(this.configurationService.onDidChangeConfiguration(e => this.onDidChangeConfiguration(e)));
47
this._register(this.fileService.onDidWatchError(error => this.onDidWatchError(error)));
48
}
49
50
private onDidChangeWorkspaceFolders(e: IWorkspaceFoldersChangeEvent): void {
51
52
// Removed workspace: Unwatch
53
for (const removed of e.removed) {
54
this.unwatchWorkspace(removed);
55
}
56
57
// Added workspace: Watch
58
for (const added of e.added) {
59
this.watchWorkspace(added);
60
}
61
}
62
63
private onDidChangeWorkbenchState(): void {
64
this.refresh();
65
}
66
67
private onDidChangeConfiguration(e: IConfigurationChangeEvent): void {
68
if (e.affectsConfiguration('files.watcherExclude') || e.affectsConfiguration('files.watcherInclude')) {
69
this.refresh();
70
}
71
}
72
73
private onDidWatchError(error: Error): void {
74
const msg = error.toString();
75
let reason: 'ENOSPC' | 'EUNKNOWN' | 'ETERM' | undefined = undefined;
76
77
// Detect if we run into ENOSPC issues
78
if (msg.indexOf('ENOSPC') >= 0) {
79
reason = 'ENOSPC';
80
81
this.notificationService.prompt(
82
Severity.Warning,
83
localize('enospcError', "Unable to watch for file changes. Please follow the instructions link to resolve this issue."),
84
[{
85
label: localize('learnMore', "Instructions"),
86
run: () => this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=867693'))
87
}],
88
{
89
sticky: true,
90
neverShowAgain: { id: 'ignoreEnospcError', isSecondary: true, scope: NeverShowAgainScope.WORKSPACE }
91
}
92
);
93
}
94
95
// Detect when the watcher throws an error unexpectedly
96
else if (msg.indexOf('EUNKNOWN') >= 0) {
97
reason = 'EUNKNOWN';
98
99
this.notificationService.prompt(
100
Severity.Warning,
101
localize('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."),
102
[{
103
label: localize('reload', "Reload"),
104
run: () => this.hostService.reload()
105
}],
106
{
107
sticky: true,
108
priority: NotificationPriority.SILENT // reduce potential spam since we don't really know how often this fires
109
}
110
);
111
}
112
113
// Detect unexpected termination
114
else if (msg.indexOf('ETERM') >= 0) {
115
reason = 'ETERM';
116
}
117
118
// Log telemetry if we gathered a reason (logging it from the renderer
119
// allows us to investigate this situation in context of experiments)
120
if (reason) {
121
type WatchErrorClassification = {
122
owner: 'bpasero';
123
comment: 'An event that fires when a watcher errors';
124
reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The watcher error reason.' };
125
};
126
type WatchErrorEvent = {
127
reason: string;
128
};
129
this.telemetryService.publicLog2<WatchErrorEvent, WatchErrorClassification>('fileWatcherError', { reason });
130
}
131
}
132
133
private watchWorkspace(workspace: IWorkspaceFolder): void {
134
135
// Compute the watcher exclude rules from configuration
136
const excludes: string[] = [];
137
const config = this.configurationService.getValue<IFilesConfiguration>({ resource: workspace.uri });
138
if (config.files?.watcherExclude) {
139
for (const key in config.files.watcherExclude) {
140
if (key && config.files.watcherExclude[key] === true) {
141
excludes.push(key);
142
}
143
}
144
}
145
146
const pathsToWatch = new ResourceMap<URI>(uri => this.uriIdentityService.extUri.getComparisonKey(uri));
147
148
// Add the workspace as path to watch
149
pathsToWatch.set(workspace.uri, workspace.uri);
150
151
// Compute additional includes from configuration
152
if (config.files?.watcherInclude) {
153
for (const includePath of config.files.watcherInclude) {
154
if (!includePath) {
155
continue;
156
}
157
158
// Absolute: verify a child of the workspace
159
if (isAbsolute(includePath)) {
160
const candidate = URI.file(includePath).with({ scheme: workspace.uri.scheme });
161
if (this.uriIdentityService.extUri.isEqualOrParent(candidate, workspace.uri)) {
162
pathsToWatch.set(candidate, candidate);
163
}
164
}
165
166
// Relative: join against workspace folder
167
else {
168
const candidate = workspace.toResource(includePath);
169
pathsToWatch.set(candidate, candidate);
170
}
171
}
172
}
173
174
// Watch all paths as instructed
175
const disposables = new DisposableStore();
176
for (const [, pathToWatch] of pathsToWatch) {
177
disposables.add(this.fileService.watch(pathToWatch, { recursive: true, excludes }));
178
}
179
this.watchedWorkspaces.set(workspace.uri, disposables);
180
}
181
182
private unwatchWorkspace(workspace: IWorkspaceFolder): void {
183
if (this.watchedWorkspaces.has(workspace.uri)) {
184
dispose(this.watchedWorkspaces.get(workspace.uri));
185
this.watchedWorkspaces.delete(workspace.uri);
186
}
187
}
188
189
private refresh(): void {
190
191
// Unwatch all first
192
this.unwatchWorkspaces();
193
194
// Watch each workspace folder
195
for (const folder of this.contextService.getWorkspace().folders) {
196
this.watchWorkspace(folder);
197
}
198
}
199
200
private unwatchWorkspaces(): void {
201
for (const [, disposable] of this.watchedWorkspaces) {
202
disposable.dispose();
203
}
204
this.watchedWorkspaces.clear();
205
}
206
207
override dispose(): void {
208
super.dispose();
209
210
this.unwatchWorkspaces();
211
}
212
}
213
214