@theia/filesystem 1.77.0-next.57 → 1.77.0-next.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/lib/common/filesystem-watcher-protocol.d.ts +4 -0
  2. package/lib/common/filesystem-watcher-protocol.d.ts.map +1 -1
  3. package/lib/node/disk-file-system-provider.d.ts.map +1 -1
  4. package/lib/node/disk-file-system-provider.js +2 -1
  5. package/lib/node/disk-file-system-provider.js.map +1 -1
  6. package/lib/node/filesystem-backend-module.d.ts +2 -2
  7. package/lib/node/filesystem-backend-module.d.ts.map +1 -1
  8. package/lib/node/filesystem-backend-module.js +5 -4
  9. package/lib/node/filesystem-backend-module.js.map +1 -1
  10. package/lib/node/filesystem-watcher-service-impl.d.ts +30 -0
  11. package/lib/node/filesystem-watcher-service-impl.d.ts.map +1 -0
  12. package/lib/node/filesystem-watcher-service-impl.js +92 -0
  13. package/lib/node/filesystem-watcher-service-impl.js.map +1 -0
  14. package/lib/node/filesystem-watcher-service-impl.spec.d.ts +2 -0
  15. package/lib/node/filesystem-watcher-service-impl.spec.d.ts.map +1 -0
  16. package/lib/node/filesystem-watcher-service-impl.spec.js +202 -0
  17. package/lib/node/filesystem-watcher-service-impl.spec.js.map +1 -0
  18. package/lib/node/filesystem-watcher.d.ts +93 -0
  19. package/lib/node/filesystem-watcher.d.ts.map +1 -0
  20. package/lib/node/filesystem-watcher.js +134 -0
  21. package/lib/node/filesystem-watcher.js.map +1 -0
  22. package/lib/node/nodejs-watcher/node-directory-watcher.d.ts +175 -0
  23. package/lib/node/nodejs-watcher/node-directory-watcher.d.ts.map +1 -0
  24. package/lib/node/nodejs-watcher/node-directory-watcher.js +580 -0
  25. package/lib/node/nodejs-watcher/node-directory-watcher.js.map +1 -0
  26. package/lib/node/nodejs-watcher/node-directory-watcher.spec.d.ts +2 -0
  27. package/lib/node/nodejs-watcher/node-directory-watcher.spec.d.ts.map +1 -0
  28. package/lib/node/nodejs-watcher/node-directory-watcher.spec.js +1284 -0
  29. package/lib/node/nodejs-watcher/node-directory-watcher.spec.js.map +1 -0
  30. package/lib/node/nodejs-watcher/watch-request-router.d.ts +63 -0
  31. package/lib/node/nodejs-watcher/watch-request-router.d.ts.map +1 -0
  32. package/lib/node/nodejs-watcher/watch-request-router.js +183 -0
  33. package/lib/node/nodejs-watcher/watch-request-router.js.map +1 -0
  34. package/lib/node/nodejs-watcher/watcher-host.d.ts +50 -0
  35. package/lib/node/nodejs-watcher/watcher-host.d.ts.map +1 -0
  36. package/lib/node/nodejs-watcher/watcher-host.js +99 -0
  37. package/lib/node/nodejs-watcher/watcher-host.js.map +1 -0
  38. package/lib/node/parcel-watcher/index.js +2 -2
  39. package/lib/node/parcel-watcher/index.js.map +1 -1
  40. package/lib/node/parcel-watcher/parcel-filesystem-service.d.ts +14 -194
  41. package/lib/node/parcel-watcher/parcel-filesystem-service.d.ts.map +1 -1
  42. package/lib/node/parcel-watcher/parcel-filesystem-service.js +7 -424
  43. package/lib/node/parcel-watcher/parcel-filesystem-service.js.map +1 -1
  44. package/lib/node/parcel-watcher/parcel-filesystem-watcher.spec.js +2 -2
  45. package/lib/node/parcel-watcher/parcel-filesystem-watcher.spec.js.map +1 -1
  46. package/lib/node/parcel-watcher/parcel-watcher-exclude.spec.js +3 -3
  47. package/lib/node/parcel-watcher/parcel-watcher-exclude.spec.js.map +1 -1
  48. package/lib/node/parcel-watcher/parcel-watcher-retry.spec.js +2 -2
  49. package/lib/node/parcel-watcher/parcel-watcher-retry.spec.js.map +1 -1
  50. package/lib/node/parcel-watcher/parcel-watcher.d.ts +165 -0
  51. package/lib/node/parcel-watcher/parcel-watcher.d.ts.map +1 -0
  52. package/lib/node/parcel-watcher/parcel-watcher.js +370 -0
  53. package/lib/node/parcel-watcher/parcel-watcher.js.map +1 -0
  54. package/lib/node/test/watcher-test-helper.d.ts +21 -0
  55. package/lib/node/test/watcher-test-helper.d.ts.map +1 -0
  56. package/lib/node/test/watcher-test-helper.js +56 -0
  57. package/lib/node/test/watcher-test-helper.js.map +1 -0
  58. package/package.json +3 -3
  59. package/src/common/filesystem-watcher-protocol.ts +4 -0
  60. package/src/node/disk-file-system-provider.ts +2 -1
  61. package/src/node/filesystem-backend-module.ts +5 -4
  62. package/src/node/filesystem-watcher-service-impl.spec.ts +272 -0
  63. package/src/node/filesystem-watcher-service-impl.ts +109 -0
  64. package/src/node/filesystem-watcher.ts +197 -0
  65. package/src/node/nodejs-watcher/node-directory-watcher.spec.ts +1594 -0
  66. package/src/node/nodejs-watcher/node-directory-watcher.ts +664 -0
  67. package/src/node/nodejs-watcher/watch-request-router.ts +224 -0
  68. package/src/node/nodejs-watcher/watcher-host.ts +126 -0
  69. package/src/node/parcel-watcher/index.ts +2 -2
  70. package/src/node/parcel-watcher/parcel-filesystem-service.ts +14 -500
  71. package/src/node/parcel-watcher/parcel-filesystem-watcher.spec.ts +4 -4
  72. package/src/node/parcel-watcher/parcel-watcher-exclude.spec.ts +4 -4
  73. package/src/node/parcel-watcher/parcel-watcher-retry.spec.ts +3 -3
  74. package/src/node/parcel-watcher/parcel-watcher.ts +432 -0
  75. package/src/node/test/watcher-test-helper.ts +58 -0
@@ -0,0 +1,272 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Ehab Younes and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import * as assert from 'assert';
18
+ import { wait } from '@theia/core/lib/common/promise-util';
19
+ import * as temp from 'temp';
20
+ import * as fs from '@theia/core/shared/fs-extra';
21
+ import { isWindows } from '@theia/core';
22
+ import { FileUri } from '@theia/core/lib/node';
23
+ import { DirectoryWatcherProvider, NodeDirectoryWatcher } from './nodejs-watcher/node-directory-watcher';
24
+ import { NO_LOGGING, TempDir, WATCHER_TIMINGS as TIMINGS } from './test/watcher-test-helper';
25
+ import { FileSystemWatcher, WatcherProvider } from './filesystem-watcher';
26
+ import {
27
+ FileSystemWatcherServiceImpl, ParcelFileSystemWatcherServerOptions, ParcelFileSystemWatcherService, ParcelWatcher, RecursiveWatcherProvider
28
+ } from './parcel-watcher/parcel-filesystem-service';
29
+ import { WatchOptions } from '../common/filesystem-watcher-protocol';
30
+
31
+ const track = temp.track();
32
+
33
+ const RECURSIVE: Readonly<WatchOptions> = { ignored: [], recursive: true };
34
+ const NON_RECURSIVE: Readonly<WatchOptions> = { ignored: [], recursive: false };
35
+
36
+ /** Exposes what a provider allocated and who serves what, so a test can tell sharing from double-allocation. */
37
+ interface TestProvider extends WatcherProvider {
38
+ readonly allocated: readonly FileSystemWatcher[];
39
+ watcherOf(watcherId: number): FileSystemWatcher | undefined;
40
+ }
41
+
42
+ class TestDirectoryProvider extends DirectoryWatcherProvider implements TestProvider {
43
+ get allocated(): readonly FileSystemWatcher[] {
44
+ return this.activeWatchers;
45
+ }
46
+
47
+ watcherOf(watcherId: number): FileSystemWatcher | undefined {
48
+ return this.watcherFor(watcherId);
49
+ }
50
+ }
51
+
52
+ class TestRecursiveProvider extends RecursiveWatcherProvider implements TestProvider {
53
+ get allocated(): readonly FileSystemWatcher[] {
54
+ return this.activeWatchers;
55
+ }
56
+
57
+ watcherOf(watcherId: number): FileSystemWatcher | undefined {
58
+ return this.watcherFor(watcherId);
59
+ }
60
+ }
61
+
62
+ function isTestProvider(provider: WatcherProvider): provider is TestProvider {
63
+ return provider instanceof TestDirectoryProvider || provider instanceof TestRecursiveProvider;
64
+ }
65
+
66
+ /** A temporary directory and a watcher service on it, one per test. */
67
+ class Sandbox extends FileSystemWatcherServiceImpl {
68
+
69
+ /** The directory the requests of this test point into. */
70
+ readonly files = new TempDir(fs.realpathSync.native(temp.mkdirSync('filesystem-watcher-service')));
71
+
72
+ protected readonly requested: number[] = [];
73
+
74
+ constructor() {
75
+ super(NO_LOGGING);
76
+ }
77
+
78
+ get root(): string {
79
+ return this.files.root;
80
+ }
81
+
82
+ watch(fsPath: string, options: WatchOptions): Promise<number> {
83
+ return this.watchFileChanges(1, FileUri.create(fsPath).toString(), options)
84
+ .then(watcherId => (this.requested.push(watcherId), watcherId));
85
+ }
86
+
87
+ /** The watchers currently allocated, one per distinct key, across both providers. */
88
+ get allocated(): readonly FileSystemWatcher[] {
89
+ return this.testProviders.flatMap(provider => provider.allocated);
90
+ }
91
+
92
+ protected get testProviders(): TestProvider[] {
93
+ return this.providers.filter(isTestProvider);
94
+ }
95
+
96
+ /** The watcher serving a request, asked of whichever provider holds it. */
97
+ watcherOf(watcherId: number): FileSystemWatcher | undefined {
98
+ return this.testProviders.map(provider => provider.watcherOf(watcherId)).find(Boolean);
99
+ }
100
+
101
+ async release(): Promise<void> {
102
+ const outstanding = this.requested.splice(0).filter(watcherId => this.watcherOf(watcherId));
103
+ const allocated = this.allocated;
104
+ await Promise.all(outstanding.map(watcherId => this.unwatchFileChanges(watcherId)));
105
+ await Promise.all(allocated.map(watcher => watcher.whenDisposed));
106
+ }
107
+
108
+ protected override createProviders(options: ParcelFileSystemWatcherServerOptions): WatcherProvider[] {
109
+ return [
110
+ new TestRecursiveProvider(options, this.maybeClient, TIMINGS.deferredDisposalTimeout),
111
+ new TestDirectoryProvider(options, this.maybeClient, TIMINGS)
112
+ ];
113
+ }
114
+ }
115
+
116
+ describe('filesystem-watcher-service', function (): void {
117
+
118
+ this.timeout(20000);
119
+
120
+ let box: Sandbox;
121
+
122
+ beforeEach(() => {
123
+ box = new Sandbox();
124
+ });
125
+
126
+ afterEach(async () => {
127
+ await box.release();
128
+ track.cleanupSync();
129
+ });
130
+
131
+ describe('routing', () => {
132
+
133
+ it('keeps the deprecated service name usable by adopters', () => {
134
+ assert.strictEqual(ParcelFileSystemWatcherService, FileSystemWatcherServiceImpl);
135
+ });
136
+
137
+ it('serves a recursive request, and one that does not say, with the parcel watcher', async () => {
138
+
139
+ const recursive = await box.watch(box.root, RECURSIVE);
140
+ const unspecified = await box.watch(box.files.mkdir('other'), { ignored: [] });
141
+
142
+ assert.ok(box.watcherOf(recursive) instanceof ParcelWatcher);
143
+ assert.ok(box.watcherOf(unspecified) instanceof ParcelWatcher);
144
+ });
145
+
146
+ it('serves a non-recursive request with a directory watcher', async () => {
147
+
148
+ const watcherId = await box.watch(box.root, NON_RECURSIVE);
149
+
150
+ assert.ok(box.watcherOf(watcherId) instanceof NodeDirectoryWatcher);
151
+ });
152
+
153
+ it('keeps a recursive and a non-recursive request on the same path apart', async () => {
154
+
155
+ const recursive = await box.watch(box.root, RECURSIVE);
156
+ const nonRecursive = await box.watch(box.root, NON_RECURSIVE);
157
+
158
+ assert.notStrictEqual(box.watcherOf(recursive), box.watcherOf(nonRecursive));
159
+ assert.strictEqual(box.allocated.length, 2);
160
+ });
161
+ });
162
+
163
+ describe('sharing', () => {
164
+
165
+ it('shares one watcher between a directory and the files inside it', async () => {
166
+
167
+ const directory = await box.watch(box.root, NON_RECURSIVE);
168
+ const first = await box.watch(box.files.write('a.txt'), NON_RECURSIVE);
169
+ const second = await box.watch(box.files.write('b.txt'), NON_RECURSIVE);
170
+
171
+ assert.strictEqual(box.allocated.length, 1);
172
+ assert.strictEqual(box.watcherOf(directory), box.watcherOf(first));
173
+ assert.strictEqual(box.watcherOf(first), box.watcherOf(second));
174
+ });
175
+
176
+ it('shares one watcher between requests with different excludes', async () => {
177
+
178
+ await box.watch(box.root, { ignored: ['**/node_modules'], recursive: false });
179
+ await box.watch(box.root, NON_RECURSIVE);
180
+
181
+ assert.strictEqual(box.allocated.length, 1);
182
+ });
183
+
184
+ it('shares one watcher between a directory and a symbolic link to it', async () => {
185
+ const real = box.files.mkdir('real');
186
+ fs.symlinkSync(real, box.files.path('link'), isWindows ? 'junction' : 'dir');
187
+
188
+ const direct = await box.watch(real, NON_RECURSIVE);
189
+ const linked = await box.watch(box.files.path('link'), NON_RECURSIVE);
190
+
191
+ assert.strictEqual(box.allocated.length, 1);
192
+ assert.strictEqual(box.watcherOf(direct), box.watcherOf(linked));
193
+ });
194
+
195
+ it('shares one watcher between concurrent requests for the same directory', async () => {
196
+ const [first, second] = [box.files.write('a.txt'), box.files.write('b.txt')];
197
+
198
+ const [one, two] = await Promise.all([box.watch(first, NON_RECURSIVE), box.watch(second, NON_RECURSIVE)]);
199
+
200
+ assert.strictEqual(box.allocated.length, 1);
201
+ assert.strictEqual(box.watcherOf(one), box.watcherOf(two));
202
+ });
203
+
204
+ it('moves a watcher to the parent once its path turns out to be a file', async () => {
205
+ const pending = await box.watch(box.files.path('later.txt'), NON_RECURSIVE);
206
+ box.files.write('later.txt');
207
+ await (box.watcherOf(pending) as NodeDirectoryWatcher).whenStarted;
208
+
209
+ const directory = await box.watch(box.root, NON_RECURSIVE);
210
+
211
+ assert.strictEqual(box.allocated.length, 1);
212
+ assert.strictEqual(box.watcherOf(pending), box.watcherOf(directory));
213
+ });
214
+
215
+ it('merges two watchers whose paths turn out to be files in one directory', async () => {
216
+ const [one, two] = await Promise.all([
217
+ box.watch(box.files.path('a.txt'), NON_RECURSIVE),
218
+ box.watch(box.files.path('b.txt'), NON_RECURSIVE)
219
+ ]);
220
+ box.files.write('a.txt');
221
+ box.files.write('b.txt');
222
+ await Promise.all([one, two].map(watcherId => (box.watcherOf(watcherId) as NodeDirectoryWatcher).whenStarted));
223
+
224
+ assert.strictEqual(box.allocated.length, 1);
225
+ assert.strictEqual(box.watcherOf(one), box.watcherOf(two));
226
+
227
+ // The handles have to follow the merge, or the moved request would never be released.
228
+ const survivor = box.watcherOf(one) as NodeDirectoryWatcher;
229
+ await Promise.all([one, two].map(watcherId => box.unwatchFileChanges(watcherId)));
230
+ await survivor.whenDisposed;
231
+ assert.strictEqual(box.allocated.length, 0);
232
+ });
233
+
234
+ it('does not attach a request to a watcher that is already disposed', async () => {
235
+ const first = await box.watch(box.root, NON_RECURSIVE);
236
+ const disposed = box.watcherOf(first) as NodeDirectoryWatcher;
237
+ disposed.dispose();
238
+
239
+ const second = await box.watch(box.root, NON_RECURSIVE);
240
+
241
+ assert.notStrictEqual(box.watcherOf(second), disposed);
242
+ assert.strictEqual(box.allocated.length, 1);
243
+ });
244
+ });
245
+
246
+ describe('releasing', () => {
247
+
248
+ it('disposes a directory watcher once its last request is unwatched', async () => {
249
+ const first = await box.watch(box.root, NON_RECURSIVE);
250
+ const second = await box.watch(box.root, NON_RECURSIVE);
251
+ const watcher = box.watcherOf(first) as NodeDirectoryWatcher;
252
+
253
+ await box.unwatchFileChanges(first);
254
+ await wait(TIMINGS.deferredDisposalTimeout * 2);
255
+ assert.strictEqual(watcher.isDisposed, false, 'the remaining request must keep the watcher alive');
256
+
257
+ await box.unwatchFileChanges(second);
258
+ await watcher.whenDisposed;
259
+ assert.strictEqual(box.allocated.length, 0);
260
+ });
261
+
262
+ it('releases the client reference of a recursive watcher', async () => {
263
+ const watcherId = await box.watch(box.root, RECURSIVE);
264
+ const watcher = box.watcherOf(watcherId) as ParcelWatcher;
265
+
266
+ await box.unwatchFileChanges(watcherId);
267
+ await watcher.whenDisposed;
268
+
269
+ assert.strictEqual(box.allocated.length, 0);
270
+ });
271
+ });
272
+ });
@@ -0,0 +1,109 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2017-2018 TypeFox and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { FileUri } from '@theia/core/lib/common/file-uri';
18
+ import { FileSystemWatcherService, FileSystemWatcherServiceClient, WatchOptions } from '../common/filesystem-watcher-protocol';
19
+ import { ResolvedWatchOptions, WatcherLogger, WatcherProvider } from './filesystem-watcher';
20
+ import { DirectoryWatcherProvider } from './nodejs-watcher/node-directory-watcher';
21
+ import { ParcelFileSystemWatcherServerOptions, RecursiveWatcherProvider } from './parcel-watcher/parcel-watcher';
22
+
23
+ /** Routes each request to the first provider that can handle it. The provider picks the watcher that serves it. */
24
+ export class FileSystemWatcherServiceImpl implements FileSystemWatcherService {
25
+
26
+ protected client: FileSystemWatcherServiceClient | undefined;
27
+
28
+ protected watcherId = 0;
29
+
30
+ protected readonly logger: WatcherLogger;
31
+
32
+ /** Checked in order. The first one whose `canHandle` returns `true` serves the request. */
33
+ protected readonly providers: readonly WatcherProvider[];
34
+
35
+ /**
36
+ * `this.client` is undefined until someone sets it.
37
+ */
38
+ protected readonly maybeClient: FileSystemWatcherServiceClient = {
39
+ onDidFilesChanged: event => this.client?.onDidFilesChanged(event),
40
+ onError: event => this.client?.onError(event),
41
+ };
42
+
43
+ constructor(options?: Partial<ParcelFileSystemWatcherServerOptions>) {
44
+ const resolved: ParcelFileSystemWatcherServerOptions = {
45
+ parcelOptions: {},
46
+ verbose: false,
47
+ info: (message, ...args) => console.info(message, ...args),
48
+ error: (message, ...args) => console.error(message, ...args),
49
+ ...options
50
+ };
51
+ this.logger = resolved;
52
+ this.providers = this.createProviders(resolved);
53
+ }
54
+
55
+ setClient(client: FileSystemWatcherServiceClient | undefined): void {
56
+ this.client = client;
57
+ }
58
+
59
+ /** Requests resolving to the same target share a watcher. */
60
+ async watchFileChanges(clientId: number, uri: string, options?: WatchOptions): Promise<number> {
61
+ const resolvedOptions = this.resolveWatchOptions(options);
62
+ const provider = this.providerFor(resolvedOptions);
63
+ const watcherId = this.watcherId++;
64
+ await provider.watch(watcherId, { clientId, path: FileUri.fsPath(uri), ignored: resolvedOptions.ignored });
65
+ return watcherId;
66
+ }
67
+
68
+ async unwatchFileChanges(watcherId: number): Promise<void> {
69
+ if (!this.providers.some(provider => provider.unwatch(watcherId))) {
70
+ console.warn(`tried to de-allocate a disposed watcher: watcherId=${watcherId}`);
71
+ }
72
+ }
73
+
74
+ /** `options` keeps the parcel shape that adopters already bind. Only the recursive provider reads `parcelOptions`. */
75
+ protected createProviders(options: ParcelFileSystemWatcherServerOptions): WatcherProvider[] {
76
+ return [
77
+ new RecursiveWatcherProvider(options, this.maybeClient),
78
+ new DirectoryWatcherProvider(options, this.maybeClient)
79
+ ];
80
+ }
81
+
82
+ protected providerFor(options: ResolvedWatchOptions): WatcherProvider {
83
+ const provider = this.providers.find(candidate => candidate.canHandle(options));
84
+ if (!provider) {
85
+ throw new Error(`No watcher provider takes options: ${JSON.stringify(options)}`);
86
+ }
87
+ return provider;
88
+ }
89
+
90
+ /**
91
+ * Return fully qualified options. Watchers created before `recursive` existed were always recursive.
92
+ */
93
+ protected resolveWatchOptions(options?: WatchOptions): ResolvedWatchOptions {
94
+ return {
95
+ ignored: options?.ignored ?? [],
96
+ recursive: options?.recursive ?? true
97
+ };
98
+ }
99
+
100
+ protected debug(message: string, ...params: unknown[]): void {
101
+ if (this.logger.verbose) {
102
+ this.logger.info(message, ...params);
103
+ }
104
+ }
105
+
106
+ dispose(): void {
107
+ // Singletons shouldn't be disposed...
108
+ }
109
+ }
@@ -0,0 +1,197 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Ehab Younes and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { Deferred } from '@theia/core/lib/common/promise-util';
18
+ import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
19
+ import { WatchOptions } from '../common/filesystem-watcher-protocol';
20
+
21
+ /** How a watcher reports what it is doing. */
22
+ export interface WatcherLogger {
23
+ verbose: boolean;
24
+ info: (message: string, ...args: unknown[]) => void;
25
+ error: (message: string, ...args: unknown[]) => void;
26
+ }
27
+
28
+ /** A single client request served by a {@link FileSystemWatcher}. */
29
+ export interface WatchRequest {
30
+ /** Client to route the changes of this request to. */
31
+ readonly clientId: number;
32
+ /** Path as requested by the client. Change URIs are built from it, never from the real path. */
33
+ readonly path: string;
34
+ /** Exclude patterns of this request alone. */
35
+ readonly ignored: readonly string[];
36
+ }
37
+
38
+ /** A watch shared by every request that resolves to the same target. Implementations decide whether it recurses. */
39
+ export interface FileSystemWatcher {
40
+ /** A disposed watcher released its resources and must not be given further requests. */
41
+ readonly isDisposed: boolean;
42
+ /** Resolves once this watcher disposed itself. Never rejects. */
43
+ readonly whenDisposed: Promise<void>;
44
+ /** Serves `request` until {@link removeRequest} is called with the same `watcherId`. */
45
+ addRequest(watcherId: number, request: WatchRequest): void;
46
+ /** Releases a request. The watcher disposes itself once it has none left. */
47
+ removeRequest(watcherId: number): void;
48
+ }
49
+
50
+ /** Handles disposal and logging for a watcher. Subclasses decide how to observe the file system. */
51
+ export abstract class AbstractFileSystemWatcher implements FileSystemWatcher {
52
+
53
+ private static debugIdSequence = 0;
54
+
55
+ private readonly debugId = AbstractFileSystemWatcher.debugIdSequence++;
56
+ private readonly disposalDeferred = new Deferred<void>();
57
+ private readonly disposal = new CancellationTokenSource();
58
+
59
+ readonly whenDisposed = this.disposalDeferred.promise;
60
+
61
+ constructor(
62
+ /** Path this watcher was asked about; the subclass decides what it resolves to. */
63
+ protected readonly target: string,
64
+ private readonly logger: WatcherLogger
65
+ ) { }
66
+
67
+ get isDisposed(): boolean {
68
+ return this.disposal.token.isCancellationRequested;
69
+ }
70
+
71
+ abstract addRequest(watcherId: number, request: WatchRequest): void;
72
+ abstract removeRequest(watcherId: number): void;
73
+
74
+ /**
75
+ * Marks this watcher disposed.
76
+ * @returns `false` if it already was, in which case the caller must not clean up again.
77
+ */
78
+ protected markDisposed(): boolean {
79
+ if (this.isDisposed) {
80
+ return false;
81
+ }
82
+ this.disposal.cancel();
83
+ this.disposalDeferred.resolve();
84
+ return true;
85
+ }
86
+
87
+ /** Logs as-is. Unlike {@link info} it is not prefixed, since these messages name their own path. */
88
+ protected error(message: string, ...params: unknown[]): void {
89
+ this.logger.error(message, ...params);
90
+ }
91
+
92
+ protected info(prefix: string, ...params: unknown[]): void {
93
+ this.logger.info(`${prefix} ${this.constructor.name}(${this.debugId} at "${this.target}"):`, ...params);
94
+ }
95
+
96
+ protected debug(prefix: string, ...params: unknown[]): void {
97
+ if (this.logger.verbose) {
98
+ this.info(prefix, ...params);
99
+ }
100
+ }
101
+ }
102
+
103
+ export type ResolvedWatchOptions = Required<WatchOptions>;
104
+
105
+ /** Serves either recursive or non-recursive requests, and decides which watcher serves each one. */
106
+ export interface WatcherProvider {
107
+ /** @returns `true` if this provider serves requests made with these options. */
108
+ canHandle(options: ResolvedWatchOptions): boolean;
109
+ /** Serves `request` under `watcherId`, on a watcher shared with requests resolving to the same target. */
110
+ watch(watcherId: number, request: WatchRequest): Promise<void>;
111
+ /**
112
+ * Releases the request registered under `watcherId`.
113
+ * @returns `true` if this provider served the request.
114
+ */
115
+ unwatch(watcherId: number): boolean;
116
+ }
117
+
118
+ /** A {@link WatcherProvider} that keeps one watcher per target. Subclasses decide which key a target maps to. */
119
+ export abstract class AbstractWatcherProvider implements WatcherProvider {
120
+
121
+ private readonly watchersByTarget = new Map<string, FileSystemWatcher>();
122
+ /** The watcher that serves each request, by `watcherId`. */
123
+ private readonly watchersByRequest = new Map<number, FileSystemWatcher>();
124
+
125
+ /** Every watcher this provider currently has open. */
126
+ protected get activeWatchers(): readonly FileSystemWatcher[] {
127
+ return Array.from(this.watchersByTarget.values());
128
+ }
129
+
130
+ abstract canHandle(options: ResolvedWatchOptions): boolean;
131
+ abstract watch(watcherId: number, request: WatchRequest): Promise<void>;
132
+
133
+ unwatch(watcherId: number): boolean {
134
+ const watcher = this.watchersByRequest.get(watcherId);
135
+ if (!watcher) {
136
+ return false;
137
+ }
138
+ this.watchersByRequest.delete(watcherId);
139
+ watcher.removeRequest(watcherId);
140
+ return true;
141
+ }
142
+
143
+ /** The watcher serving a request, if this provider has one. */
144
+ protected watcherFor(watcherId: number): FileSystemWatcher | undefined {
145
+ return this.watchersByRequest.get(watcherId);
146
+ }
147
+
148
+ /** Puts a request on a watcher and records which one, so {@link unwatch} finds it again. */
149
+ protected serve(watcherId: number, watcher: FileSystemWatcher, request: WatchRequest): void {
150
+ watcher.addRequest(watcherId, request);
151
+ this.assign(watcherId, watcher);
152
+ }
153
+
154
+ /** Records the watcher of a request that reached it without {@link serve}, such as through a re-key. */
155
+ protected assign(watcherId: number, watcher: FileSystemWatcher): void {
156
+ this.watchersByRequest.set(watcherId, watcher);
157
+ }
158
+
159
+ /**
160
+ * Returns the watcher under `watcherKey`, or creates one. A disposed watcher stays in the map until a later
161
+ * promise callback removes it, so it is replaced here rather than handed a new request.
162
+ */
163
+ protected getOrCreateWatcher<T extends FileSystemWatcher>(watcherKey: string, create: () => T): T {
164
+ const existing = this.watchersByTarget.get(watcherKey);
165
+ if (existing && !existing.isDisposed) {
166
+ return existing as T;
167
+ }
168
+ const watcher = create();
169
+ this.watchersByTarget.set(watcherKey, watcher);
170
+ watcher.whenDisposed.then(() => {
171
+ if (this.watchersByTarget.get(watcherKey) === watcher) {
172
+ this.watchersByTarget.delete(watcherKey);
173
+ }
174
+ // Requests it handed to another watcher already point there, so drop only the ones still on it.
175
+ for (const [watcherId, serving] of this.watchersByRequest) {
176
+ if (serving === watcher) {
177
+ this.watchersByRequest.delete(watcherId);
178
+ }
179
+ }
180
+ });
181
+ return watcher;
182
+ }
183
+
184
+ /** @returns `true` if `watcher` is the one registered under `watcherKey`. */
185
+ protected isRegisteredAs(watcher: FileSystemWatcher, watcherKey: string): boolean {
186
+ return this.watchersByTarget.get(watcherKey) === watcher;
187
+ }
188
+
189
+ /** Forgets every key a watcher is registered under, so that it can be registered under another. */
190
+ protected unregisterWatcher(watcher: FileSystemWatcher): void {
191
+ for (const [watcherKey, registered] of this.watchersByTarget) {
192
+ if (registered === watcher) {
193
+ this.watchersByTarget.delete(watcherKey);
194
+ }
195
+ }
196
+ }
197
+ }