@theia/filesystem 1.73.0-next.2 → 1.73.0-next.24

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 (41) hide show
  1. package/lib/browser/file-service.d.ts +2 -0
  2. package/lib/browser/file-service.d.ts.map +1 -1
  3. package/lib/browser/file-service.js +10 -5
  4. package/lib/browser/file-service.js.map +1 -1
  5. package/lib/browser/file-tree/file-tree-widget.d.ts +2 -0
  6. package/lib/browser/file-tree/file-tree-widget.d.ts.map +1 -1
  7. package/lib/browser/file-tree/file-tree-widget.js +6 -1
  8. package/lib/browser/file-tree/file-tree-widget.js.map +1 -1
  9. package/lib/browser/filesystem-frontend-contribution.d.ts +2 -1
  10. package/lib/browser/filesystem-frontend-contribution.d.ts.map +1 -1
  11. package/lib/browser/filesystem-frontend-contribution.js +7 -2
  12. package/lib/browser/filesystem-frontend-contribution.js.map +1 -1
  13. package/lib/node/disk-file-system-provider.d.ts +2 -0
  14. package/lib/node/disk-file-system-provider.d.ts.map +1 -1
  15. package/lib/node/disk-file-system-provider.js +9 -3
  16. package/lib/node/disk-file-system-provider.js.map +1 -1
  17. package/lib/node/parcel-watcher/parcel-filesystem-service.d.ts +8 -0
  18. package/lib/node/parcel-watcher/parcel-filesystem-service.d.ts.map +1 -1
  19. package/lib/node/parcel-watcher/parcel-filesystem-service.js +38 -2
  20. package/lib/node/parcel-watcher/parcel-filesystem-service.js.map +1 -1
  21. package/lib/node/parcel-watcher/parcel-watcher-exclude.spec.d.ts +2 -0
  22. package/lib/node/parcel-watcher/parcel-watcher-exclude.spec.d.ts.map +1 -0
  23. package/lib/node/parcel-watcher/parcel-watcher-exclude.spec.js +79 -0
  24. package/lib/node/parcel-watcher/parcel-watcher-exclude.spec.js.map +1 -0
  25. package/lib/node/parcel-watcher/parcel-watcher-retry.spec.d.ts +2 -0
  26. package/lib/node/parcel-watcher/parcel-watcher-retry.spec.d.ts.map +1 -0
  27. package/lib/node/parcel-watcher/parcel-watcher-retry.spec.js +102 -0
  28. package/lib/node/parcel-watcher/parcel-watcher-retry.spec.js.map +1 -0
  29. package/lib/node/upload/node-file-upload-service.d.ts +2 -0
  30. package/lib/node/upload/node-file-upload-service.d.ts.map +1 -1
  31. package/lib/node/upload/node-file-upload-service.js +9 -3
  32. package/lib/node/upload/node-file-upload-service.js.map +1 -1
  33. package/package.json +3 -3
  34. package/src/browser/file-service.ts +9 -6
  35. package/src/browser/file-tree/file-tree-widget.tsx +6 -3
  36. package/src/browser/filesystem-frontend-contribution.ts +7 -4
  37. package/src/node/disk-file-system-provider.ts +8 -4
  38. package/src/node/parcel-watcher/parcel-filesystem-service.ts +45 -2
  39. package/src/node/parcel-watcher/parcel-watcher-exclude.spec.ts +91 -0
  40. package/src/node/parcel-watcher/parcel-watcher-retry.spec.ts +117 -0
  41. package/src/node/upload/node-file-upload-service.ts +9 -4
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ // *****************************************************************************
3
+ // Copyright (C) 2026 EclipseSource and others.
4
+ //
5
+ // This program and the accompanying materials are made available under the
6
+ // terms of the Eclipse Public License v. 2.0 which is available at
7
+ // http://www.eclipse.org/legal/epl-2.0.
8
+ //
9
+ // This Source Code may also be made available under the following Secondary
10
+ // Licenses when the conditions for such availability set forth in the Eclipse
11
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
12
+ // with the GNU Classpath Exception which is available at
13
+ // https://www.gnu.org/software/classpath/license.html.
14
+ //
15
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
16
+ // *****************************************************************************
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ const chai = require("chai");
19
+ const sinon = require("sinon");
20
+ const temp = require("temp");
21
+ const fs = require("@theia/core/shared/fs-extra");
22
+ const node_1 = require("@theia/core/lib/node");
23
+ const parcel_filesystem_service_1 = require("./parcel-filesystem-service");
24
+ // We require the *same* module object that the production code imports from, so that
25
+ // stubbing its `subscribe` export is observed by `ParcelWatcher`. The `@theia/core/shared`
26
+ // shim simply re-exports `require('@parcel/watcher')`, so this is the identical reference.
27
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
28
+ const parcel = require('@theia/core/shared/@parcel/watcher');
29
+ const expect = chai.expect;
30
+ const track = temp.track();
31
+ /**
32
+ * Covers the inotify-tree-race fix in `ParcelWatcher.start()`:
33
+ *
34
+ * parcel-watcher walks the directory tree and only then calls `inotify_add_watch`
35
+ * on each subdirectory. If a subdirectory disappears between the walk and the add
36
+ * (common when watching dirs that contain rotated logs/temp folders), the syscall
37
+ * returns ENOENT and parcel-watcher fails the *entire* subscribe. The fix retries
38
+ * `createWatcher` a few times, but only when (a) the underlying error indicates a
39
+ * missing path and (b) the watched root itself still exists.
40
+ */
41
+ describe('parcel-filesystem-watcher transient ENOENT handling', function () {
42
+ this.timeout(20000);
43
+ let root;
44
+ let service;
45
+ let subscribeStub;
46
+ let consoleErrorStub;
47
+ let onError;
48
+ beforeEach(() => {
49
+ const tempPath = temp.mkdirSync('parcel-enoent-root');
50
+ root = node_1.FileUri.create(fs.realpathSync(tempPath));
51
+ // start() now logs the underlying error to stderr on failure; silence it
52
+ // so the test output stays readable.
53
+ consoleErrorStub = sinon.stub(console, 'error');
54
+ service = new parcel_filesystem_service_1.ParcelFileSystemWatcherService({ verbose: false });
55
+ onError = sinon.stub();
56
+ service.setClient({
57
+ onDidFilesChanged: () => undefined,
58
+ onError,
59
+ });
60
+ });
61
+ afterEach(() => {
62
+ subscribeStub?.restore();
63
+ consoleErrorStub.restore();
64
+ track.cleanupSync();
65
+ });
66
+ it('retries when subscribe throws a transient ENOENT and the watched root still exists', async () => {
67
+ let attempts = 0;
68
+ subscribeStub = sinon.stub(parcel, 'subscribe').callsFake(async () => {
69
+ attempts++;
70
+ if (attempts < 3) {
71
+ throw new Error('No such file or directory at /tmp/rotated-log');
72
+ }
73
+ return { unsubscribe: async () => undefined };
74
+ });
75
+ await service.watchFileChanges(0, root.toString());
76
+ // Backoff schedule for two retries: 100 + 200 = 300ms. Leave generous margin.
77
+ await new Promise(resolve => setTimeout(resolve, 800));
78
+ expect(attempts, 'subscribe should have been retried until it succeeded').to.equal(3);
79
+ expect(onError.called, 'no error should surface to the client once the retry recovered').to.equal(false);
80
+ });
81
+ it('does not retry on non-ENOENT errors and surfaces the failure immediately', async () => {
82
+ subscribeStub = sinon.stub(parcel, 'subscribe').callsFake(async () => {
83
+ throw new Error('EACCES: permission denied');
84
+ });
85
+ await service.watchFileChanges(0, root.toString());
86
+ await new Promise(resolve => setTimeout(resolve, 200));
87
+ expect(subscribeStub.callCount, 'non-transient errors must not trigger any retry').to.equal(1);
88
+ expect(onError.called, 'error must be reported to the client').to.equal(true);
89
+ });
90
+ it('gives up after the retry budget is exhausted on persistent ENOENT', async () => {
91
+ subscribeStub = sinon.stub(parcel, 'subscribe').callsFake(async () => {
92
+ throw new Error('No such file or directory at /tmp/rotated-log');
93
+ });
94
+ await service.watchFileChanges(0, root.toString());
95
+ // Total backoff is 100+200+300+400 = 1000ms; leave a margin for scheduling.
96
+ await new Promise(resolve => setTimeout(resolve, 1700));
97
+ // Initial attempt + 4 retries = 5 total subscribe calls.
98
+ expect(subscribeStub.callCount, 'should retry up to the budget then give up').to.equal(5);
99
+ expect(onError.called, 'error must surface once the retry budget is exhausted').to.equal(true);
100
+ });
101
+ });
102
+ //# sourceMappingURL=parcel-watcher-retry.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parcel-watcher-retry.spec.js","sourceRoot":"","sources":["../../../src/node/parcel-watcher/parcel-watcher-retry.spec.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,8 @@
1
1
  import express = require('@theia/core/shared/express');
2
2
  import { BackendApplicationContribution } from '@theia/core/lib/node';
3
+ import { ILogger } from '@theia/core';
3
4
  export declare class NodeFileUploadService implements BackendApplicationContribution {
5
+ protected readonly logger: ILogger;
4
6
  private static readonly UPLOAD_DIR;
5
7
  configure(app: express.Application): Promise<void>;
6
8
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"node-file-upload-service.d.ts","sourceRoot":"","sources":["../../../src/node/upload/node-file-upload-service.ts"],"names":[],"mappings":"AAmBA,OAAO,OAAO,GAAG,QAAQ,4BAA4B,CAAC,CAAC;AAEvD,OAAO,EAAE,8BAA8B,EAAW,MAAM,sBAAsB,CAAC;AAI/E,qBACa,qBAAsB,YAAW,8BAA8B;IACxE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAkB;IAE9C,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAexD;;OAEG;cACa,qBAAqB,IAAI,OAAO,CAAC,MAAM,CAAC;IAIxD;;OAEG;cACa,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;cAIzC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CA2BxG"}
1
+ {"version":3,"file":"node-file-upload-service.d.ts","sourceRoot":"","sources":["../../../src/node/upload/node-file-upload-service.ts"],"names":[],"mappings":"AAmBA,OAAO,OAAO,GAAG,QAAQ,4BAA4B,CAAC,CAAC;AAEvD,OAAO,EAAE,8BAA8B,EAAW,MAAM,sBAAsB,CAAC;AAG/E,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEtC,qBACa,qBAAsB,YAAW,8BAA8B;IAGxE,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAEnC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAkB;IAE9C,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAexD;;OAEG;cACa,qBAAqB,IAAI,OAAO,CAAC,MAAM,CAAC;IAIxD;;OAEG;cACa,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC;cAIzC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CA2BxG"}
@@ -25,6 +25,7 @@ const fs = require("@theia/core/shared/fs-extra");
25
25
  const node_1 = require("@theia/core/lib/node");
26
26
  const inversify_1 = require("@theia/core/shared/inversify");
27
27
  const file_upload_1 = require("../../common/file-upload");
28
+ const core_1 = require("@theia/core");
28
29
  let NodeFileUploadService = class NodeFileUploadService {
29
30
  static { NodeFileUploadService_1 = this; }
30
31
  static { this.UPLOAD_DIR = 'theia_upload'; }
@@ -33,8 +34,8 @@ let NodeFileUploadService = class NodeFileUploadService {
33
34
  this.getTemporaryUploadDest(),
34
35
  this.getHttpFileUploadPath()
35
36
  ]);
36
- console.debug(`HTTP file upload URL path: ${http_path}`);
37
- console.debug(`Backend file upload cache path: ${dest}`);
37
+ this.logger.debug(`HTTP file upload URL path: ${http_path}`);
38
+ this.logger.debug(`Backend file upload cache path: ${dest}`);
38
39
  app.post(http_path,
39
40
  // `multer` handles `multipart/form-data` containing our file to upload.
40
41
  multer({ dest }).single('file'), (request, response, next) => this.handleFileUpload(request, response));
@@ -69,7 +70,7 @@ let NodeFileUploadService = class NodeFileUploadService {
69
70
  response.status(200).send(target); // ok
70
71
  }
71
72
  catch (error) {
72
- console.error(error);
73
+ this.logger.error(error);
73
74
  if (error.message) {
74
75
  // internal server error with error message as response
75
76
  response.status(500).send(error.message);
@@ -82,6 +83,11 @@ let NodeFileUploadService = class NodeFileUploadService {
82
83
  }
83
84
  };
84
85
  exports.NodeFileUploadService = NodeFileUploadService;
86
+ tslib_1.__decorate([
87
+ (0, inversify_1.inject)(core_1.ILogger),
88
+ (0, inversify_1.named)('filesystem:NodeFileUploadService'),
89
+ tslib_1.__metadata("design:type", Object)
90
+ ], NodeFileUploadService.prototype, "logger", void 0);
85
91
  exports.NodeFileUploadService = NodeFileUploadService = NodeFileUploadService_1 = tslib_1.__decorate([
86
92
  (0, inversify_1.injectable)()
87
93
  ], NodeFileUploadService);
@@ -1 +1 @@
1
- {"version":3,"file":"node-file-upload-service.js","sourceRoot":"","sources":["../../../src/node/upload/node-file-upload-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"node-file-upload-service.js","sourceRoot":"","sources":["../../../src/node/upload/node-file-upload-service.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@theia/filesystem",
3
- "version": "1.73.0-next.2+8ed51a5e94d",
3
+ "version": "1.73.0-next.24+ed85d4937",
4
4
  "description": "Theia - FileSystem Extension",
5
5
  "dependencies": {
6
- "@theia/core": "1.73.0-next.2+8ed51a5e94d",
6
+ "@theia/core": "1.73.0-next.24+ed85d4937",
7
7
  "@types/body-parser": "^1.19.6",
8
8
  "@types/multer": "^1.4.13",
9
9
  "@types/tar-fs": "^1.16.3",
@@ -82,5 +82,5 @@
82
82
  "nyc": {
83
83
  "extends": "../../configs/nyc.json"
84
84
  },
85
- "gitHead": "8ed51a5e94d15a6003725683a14773c6f8d64ac6"
85
+ "gitHead": "ed85d49379279b6472543df174249476e1619bac"
86
86
  }
@@ -68,7 +68,7 @@ import { Mutable } from '@theia/core/lib/common/types';
68
68
  import { readFileIntoStream } from '../common/io';
69
69
  import { FileSystemWatcherErrorHandler } from './filesystem-watcher-error-handler';
70
70
  import { FileSystemUtils } from '../common/filesystem-utils';
71
- import { nls } from '@theia/core';
71
+ import { nls, ILogger } from '@theia/core';
72
72
  import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
73
73
 
74
74
  export interface FileOperationParticipant {
@@ -305,6 +305,9 @@ export class FileService {
305
305
  @inject(FileSystemWatcherErrorHandler)
306
306
  protected readonly watcherErrorHandler: FileSystemWatcherErrorHandler;
307
307
 
308
+ @inject(ILogger) @named('filesystem:FileService')
309
+ protected readonly logger: ILogger;
310
+
308
311
  @postConstruct()
309
312
  protected init(): void {
310
313
  for (const contribution of this.contributions.getContributions()) {
@@ -581,7 +584,7 @@ export class FileService {
581
584
 
582
585
  return await this.toFileStat(provider, childResource, childStat, entries.length, resolveMetadata, recurse);
583
586
  } catch (error) {
584
- console.trace(error);
587
+ this.logger.error(error);
585
588
 
586
589
  return null; // can happen e.g. due to permission errors
587
590
  }
@@ -590,7 +593,7 @@ export class FileService {
590
593
  // make sure to get rid of null values that signal a failure to resolve a particular entry
591
594
  fileStat.children = resolvedEntries.filter(e => !!e) as FileStat[];
592
595
  } catch (error) {
593
- console.trace(error);
596
+ this.logger.error(error);
594
597
 
595
598
  fileStat.children = []; // gracefully handle errors, we may not have permissions to read
596
599
  }
@@ -615,7 +618,7 @@ export class FileService {
615
618
  try {
616
619
  return { stat: await this.doResolveFile(entry.resource, entry.options), success: true };
617
620
  } catch (error) {
618
- console.trace(error);
621
+ this.logger.error(error);
619
622
 
620
623
  return { stat: undefined, success: false };
621
624
  }
@@ -1454,7 +1457,7 @@ export class FileService {
1454
1457
  } else {
1455
1458
  watchDisposable = disposable;
1456
1459
  }
1457
- }, error => console.error(error));
1460
+ }, error => this.logger.error(error));
1458
1461
 
1459
1462
  return Disposable.create(() => watchDisposable.dispose());
1460
1463
  }
@@ -2030,7 +2033,7 @@ export class FileService {
2030
2033
  timeout(participantsTimeout, cancellationTokenSource.token).then(() => cancellationTokenSource.dispose(), () => { /* no-op if cancelled */ })
2031
2034
  ]);
2032
2035
  } catch (err) {
2033
- console.warn(err);
2036
+ this.logger.warn(err);
2034
2037
  }
2035
2038
  }
2036
2039
  },
@@ -15,7 +15,7 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import * as React from '@theia/core/shared/react';
18
- import { injectable, inject } from '@theia/core/shared/inversify';
18
+ import { injectable, inject, named } from '@theia/core/shared/inversify';
19
19
  import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
20
20
  import URI from '@theia/core/lib/common/uri';
21
21
  import { UriSelection } from '@theia/core/lib/common/selection';
@@ -26,7 +26,7 @@ import { FileTreeModel } from './file-tree-model';
26
26
  import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service';
27
27
  import { ApplicationShell } from '@theia/core/lib/browser/shell';
28
28
  import { FileStat, FileType } from '../../common/files';
29
- import { isOSX } from '@theia/core';
29
+ import { isOSX, ILogger } from '@theia/core';
30
30
  import { FileUploadService } from '../../common/upload/file-upload';
31
31
 
32
32
  export const FILE_TREE_CLASS = 'theia-FileTree';
@@ -45,6 +45,9 @@ export class FileTreeWidget extends CompressedTreeWidget {
45
45
  @inject(IconThemeService)
46
46
  protected readonly iconThemeService: IconThemeService;
47
47
 
48
+ @inject(ILogger) @named('filesystem:FileTreeWidget')
49
+ protected readonly logger: ILogger;
50
+
48
51
  constructor(
49
52
  @inject(TreeProps) props: TreeProps,
50
53
  @inject(FileTreeModel) override readonly model: FileTreeModel,
@@ -201,7 +204,7 @@ export class FileTreeWidget extends CompressedTreeWidget {
201
204
  }
202
205
  } catch (e) {
203
206
  if (!isCancelled(e)) {
204
- console.error(e);
207
+ this.logger.error(e);
205
208
  }
206
209
  }
207
210
  }
@@ -14,7 +14,7 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { CorePreferences, nls } from '@theia/core';
17
+ import { CorePreferences, nls, ILogger } from '@theia/core';
18
18
  import {
19
19
  ApplicationShell,
20
20
  CommonCommands,
@@ -35,7 +35,7 @@ import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/c
35
35
  import { Deferred } from '@theia/core/lib/common/promise-util';
36
36
  import URI from '@theia/core/lib/common/uri';
37
37
  import { environment } from '@theia/core/shared/@theia/application-package/lib/environment';
38
- import { inject, injectable } from '@theia/core/shared/inversify';
38
+ import { inject, injectable, named } from '@theia/core/shared/inversify';
39
39
  import { UserWorkingDirectoryProvider } from '@theia/core/lib/browser/user-working-directory-provider';
40
40
  import { FileChangeType, FileChangesEvent, FileOperation } from '../common/files';
41
41
  import { FileDialogService, SaveFileDialogProps } from './file-dialog';
@@ -95,6 +95,9 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri
95
95
  @inject(UserWorkingDirectoryProvider)
96
96
  protected readonly workingDirectory: UserWorkingDirectoryProvider;
97
97
 
98
+ @inject(ILogger) @named('filesystem:FileSystemFrontendContribution')
99
+ protected readonly logger: ILogger;
100
+
98
101
  protected onDidChangeEditorFileEmitter = new Emitter<{ editor: NavigatableWidget, type: FileChangeType }>();
99
102
  readonly onDidChangeEditorFile = this.onDidChangeEditorFileEmitter.event;
100
103
 
@@ -171,7 +174,7 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri
171
174
  return fileUploadResult;
172
175
  } catch (e) {
173
176
  if (!isCancelled(e)) {
174
- console.error(e);
177
+ this.logger.error(e);
175
178
  }
176
179
  }
177
180
  }
@@ -227,7 +230,7 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri
227
230
  try {
228
231
  await operation();
229
232
  } catch (e) {
230
- console.error(e);
233
+ this.logger.error(e);
231
234
  }
232
235
  });
233
236
  }
@@ -22,7 +22,8 @@
22
22
  /* eslint-disable no-null/no-null */
23
23
  /* eslint-disable @typescript-eslint/no-shadow */
24
24
 
25
- import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
25
+ import { injectable, inject, postConstruct, named } from '@theia/core/shared/inversify';
26
+ import { ILogger } from '@theia/core';
26
27
  import { basename, dirname, normalize, join } from 'path';
27
28
  import { generateUuid } from '@theia/core/lib/common/uuid';
28
29
  import * as os from 'os';
@@ -109,6 +110,9 @@ export class DiskFileSystemProvider implements Disposable,
109
110
  @inject(EncodingService)
110
111
  protected readonly encodingService: EncodingService;
111
112
 
113
+ @inject(ILogger) @named('filesystem:DiskFileSystemProvider')
114
+ protected readonly logger: ILogger;
115
+
112
116
  @postConstruct()
113
117
  protected init(): void {
114
118
  this.toDispose.push(this.watcher);
@@ -220,7 +224,7 @@ export class DiskFileSystemProvider implements Disposable,
220
224
  const stat = await this.stat(resource.resolve(child));
221
225
  result.push([child, stat.type]);
222
226
  } catch (error) {
223
- console.trace(error); // ignore errors for individual entries that can arise from permission denied
227
+ this.logger.error(error); // ignore errors for individual entries that can arise from permission denied
224
228
  }
225
229
  }));
226
230
 
@@ -332,7 +336,7 @@ export class DiskFileSystemProvider implements Disposable,
332
336
  // After a successful truncate() the flag can be set to 'r+' which will not truncate.
333
337
  flags = 'r+';
334
338
  } catch (error) {
335
- console.trace(error);
339
+ this.logger.error(error);
336
340
  }
337
341
  }
338
342
 
@@ -384,7 +388,7 @@ export class DiskFileSystemProvider implements Disposable,
384
388
  // In some exotic setups it is well possible that node fails to sync
385
389
  // In that case we disable flushing and log the error to our logger
386
390
  this.canFlush = false;
387
- console.error(error);
391
+ this.logger.error(error);
388
392
  }
389
393
  }
390
394
 
@@ -27,7 +27,15 @@ import { subscribe, Options, AsyncSubscription, Event } from '@theia/core/shared
27
27
  import { isOSX, isWindows } from '@theia/core';
28
28
 
29
29
  export interface ParcelWatcherOptions {
30
+ /** Compiled exclude patterns, used to filter events after they arrive. */
30
31
  ignored: Minimatch[]
32
+ /**
33
+ * Raw exclude patterns, passed to the native parcel `ignore` option so that excluded
34
+ * directories are never crawled or watched in the first place (rather than only having
35
+ * their events filtered out afterwards). This is what actually keeps the number of OS
36
+ * file watches (e.g. inotify watches on Linux) bounded on large workspaces.
37
+ */
38
+ ignorePatterns: string[]
31
39
  }
32
40
 
33
41
  export const ParcelFileSystemWatcherServerOptions = Symbol('ParcelFileSystemWatcherServerOptions');
@@ -132,6 +140,7 @@ export class ParcelWatcher {
132
140
  if (error === WatcherDisposal) {
133
141
  return false;
134
142
  }
143
+ console.error(`Watcher failed to start at "${this.fsPath}":`, error);
135
144
  this._dispose();
136
145
  this.fireError();
137
146
  throw error;
@@ -219,7 +228,33 @@ export class ParcelWatcher {
219
228
  this.assertNotDisposed();
220
229
  }
221
230
  this.assertNotDisposed();
222
- const watcher = await this.createWatcher();
231
+ // This race is specific to Linux/inotify: parcel-watcher's inotify backend walks
232
+ // the tree and then calls inotify_add_watch on every subdirectory. If a subdirectory
233
+ // disappears between the walk and the add (common when watching dirs that contain
234
+ // auto-rotated log/temp folders), the syscall returns ENOENT and parcel-watcher fails
235
+ // the entire subscribe. Retry a few times: by the next walk the gone-but-not-forgotten
236
+ // dir is no longer present. Windows (ReadDirectoryChangesW) and macOS (FSEvents) watch
237
+ // the whole subtree from a single handle on the root and never register per-subdirectory
238
+ // watches, so they cannot hit this race; the retry is simply a no-op there.
239
+ let watcher: AsyncSubscription | undefined;
240
+ let attempt = 0;
241
+ while (true) {
242
+ try {
243
+ watcher = await this.createWatcher();
244
+ break;
245
+ } catch (error) {
246
+ const message: string = (error && error.message) || '';
247
+ const isTransientEnoent = message.includes('No such file or directory')
248
+ && await fsp.stat(this.fsPath).then(() => true, () => false);
249
+ if (!isTransientEnoent || attempt >= 4) {
250
+ throw error;
251
+ }
252
+ attempt++;
253
+ this.assertNotDisposed();
254
+ await timeout(100 * attempt);
255
+ this.assertNotDisposed();
256
+ }
257
+ }
223
258
  this.assertNotDisposed();
224
259
  this.debug('STARTED', `disposed=${this.disposed}`);
225
260
  // The watcher could be disposed while it was starting, make sure to check for this:
@@ -260,7 +295,14 @@ export class ParcelWatcher {
260
295
  }
261
296
  }, {
262
297
  backend: ParcelWatcher.PARCEL_WATCHER_BACKEND,
263
- ...this.parcelFileSystemWatchServerOptions.parcelOptions
298
+ ...this.parcelFileSystemWatchServerOptions.parcelOptions,
299
+ // Pass the excludes to parcel's native `ignore` so excluded directories are pruned
300
+ // from the watch tree (no OS watch is placed), not merely filtered out of the event
301
+ // stream. Mirrors VS Code's parcel watcher (`ignore: <request excludes>`).
302
+ ignore: [
303
+ ...(this.parcelFileSystemWatchServerOptions.parcelOptions.ignore ?? []),
304
+ ...this.watcherOptions.ignorePatterns
305
+ ]
264
306
  });
265
307
  }
266
308
 
@@ -435,6 +477,7 @@ export class ParcelFileSystemWatcherService implements FileSystemWatcherService
435
477
  const watcherOptions: ParcelWatcherOptions = {
436
478
  ignored: options.ignored
437
479
  .map(pattern => new Minimatch(pattern, { dot: true })),
480
+ ignorePatterns: options.ignored,
438
481
  };
439
482
  return new ParcelWatcher(clientId, fsPath, watcherOptions, this.options, this.maybeClient);
440
483
  }
@@ -0,0 +1,91 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 Safi Seid-Ahmad, K2view 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 chai from 'chai';
18
+ import * as sinon from 'sinon';
19
+ import * as temp from 'temp';
20
+ import * as fs from '@theia/core/shared/fs-extra';
21
+ import URI from '@theia/core/lib/common/uri';
22
+ import { FileUri } from '@theia/core/lib/node';
23
+ import { Options } from '@theia/core/shared/@parcel/watcher';
24
+ import { ParcelFileSystemWatcherService } from './parcel-filesystem-service';
25
+
26
+ // We require the *same* module object that the production code imports from, so that
27
+ // stubbing its `subscribe` export is observed by `ParcelWatcher`. The `@theia/core/shared`
28
+ // shim simply re-exports `require('@parcel/watcher')`, so this is the identical reference.
29
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
30
+ const parcel = require('@theia/core/shared/@parcel/watcher');
31
+
32
+ const expect = chai.expect;
33
+ const track = temp.track();
34
+
35
+ /**
36
+ * Reproduces the `files.watcherExclude` / exclude leak:
37
+ *
38
+ * Excludes flow `files.watcherExclude` -> `WatchOptions.excludes`
39
+ * -> `disk-file-system-provider` (`{ ignored }`) -> `ParcelWatcherOptions.ignored`.
40
+ * However `ParcelWatcher.createWatcher()` only uses `ignored` to *filter events after they
41
+ * arrive* (`isIgnored`/`pushFileChange`). It never passes them to the native `@parcel/watcher`
42
+ * `subscribe(..., { ignore })` option, so excluded directories are STILL crawled and given
43
+ * inotify watches. On large workspaces this exhausts `fs.inotify.max_user_watches`
44
+ * ("Unable to watch for file changes in this large workspace.") even though the user
45
+ * configured excludes that should have pruned those directories.
46
+ */
47
+ describe('parcel-filesystem-watcher exclude handling', function (): void {
48
+
49
+ this.timeout(20000);
50
+
51
+ let root: URI;
52
+ let service: ParcelFileSystemWatcherService;
53
+ let subscribeStub: sinon.SinonStub;
54
+ let capturedOptions: Options[];
55
+
56
+ beforeEach(() => {
57
+ const tempPath = temp.mkdirSync('parcel-exclude-root');
58
+ root = FileUri.create(fs.realpathSync(tempPath));
59
+ capturedOptions = [];
60
+ // Stub the native parcel `subscribe` so we can inspect the options Theia passes to it
61
+ // (and so the test does not place real OS watches).
62
+ subscribeStub = sinon.stub(parcel, 'subscribe').callsFake(
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ async (_dir: string, _cb: any, opts: Options) => {
65
+ capturedOptions.push(opts);
66
+ return { unsubscribe: async () => undefined };
67
+ });
68
+ service = new ParcelFileSystemWatcherService({ verbose: false });
69
+ });
70
+
71
+ afterEach(() => {
72
+ subscribeStub.restore();
73
+ service.dispose();
74
+ track.cleanupSync();
75
+ });
76
+
77
+ it('passes the configured excludes to the native parcel `ignore` option so excluded directories are not watched', async () => {
78
+ const excludePattern = '**/node_modules/**';
79
+
80
+ await service.watchFileChanges(0, root.toString(), { ignored: [excludePattern] });
81
+ // Allow ParcelWatcher.start() (stat + subscribe) to run.
82
+ await new Promise(resolve => setTimeout(resolve, 300));
83
+
84
+ expect(subscribeStub.called, 'native parcel subscribe should have been called').to.equal(true);
85
+
86
+ const opts = capturedOptions[0] ?? {};
87
+ expect(opts.ignore, 'parcel subscribe options should carry an `ignore` list built from the excludes').to.not.equal(undefined);
88
+ expect(opts.ignore, 'the configured exclude pattern should be passed to parcel `ignore`').to.include(excludePattern);
89
+ });
90
+
91
+ });