From 7ea3cdf9f3d50a14da4ab54d9b63a9230021bd09 Mon Sep 17 00:00:00 2001 From: Nigel Westbury Date: Wed, 2 Sep 2020 09:13:42 +0100 Subject: [PATCH] Show message if insufficient file watcher handles Signed-off-by: Nigel Westbury --- CHANGELOG.md | 2 +- .../filesystem/src/browser/file-service.ts | 8 +++ .../src/browser/filesystem-frontend-module.ts | 3 + .../filesystem-watcher-error-handler.ts | 60 +++++++++++++++++++ .../common/delegating-file-system-provider.ts | 5 ++ packages/filesystem/src/common/files.ts | 1 + .../src/common/filesystem-watcher-protocol.ts | 5 ++ .../src/common/remote-file-system-provider.ts | 12 ++++ .../src/node/disk-file-system-provider.ts | 6 +- .../src/node/filesystem-watcher-client.ts | 5 ++ .../nsfw-filesystem-watcher.spec.ts | 4 ++ .../nsfw-watcher/nsfw-filesystem-watcher.ts | 5 ++ .../src/main/browser/file-system-main-impl.ts | 1 + 13 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 packages/filesystem/src/browser/filesystem-watcher-error-handler.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d3e1e5a7d22ef..19c53c01c841c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - This change triggers the context menu for a given shell tab-bar without the need to activate it - While registering a command, `Event` should be passed down, if not passed, then the commands will not work correctly as they no longer rely on the activation of tab-bar - [core] Moved `findTitle()` and `findTabBar()` from `common-frontend-contribution.ts` to `application-shell.ts` [#6965](https://github.com/eclipse-theia/theia/pull/6965) - +- [filesystem] show Linux users a warning when Inotify handles have been exhausted, with link to instructions on how to fix [#8458](https://github.com/eclipse-theia/theia/pull/8458) ## v1.5.0 - 27/08/2020 diff --git a/packages/filesystem/src/browser/file-service.ts b/packages/filesystem/src/browser/file-service.ts index 3483c3819ff70..de52a82846939 100644 --- a/packages/filesystem/src/browser/file-service.ts +++ b/packages/filesystem/src/browser/file-service.ts @@ -65,6 +65,7 @@ import { UTF8, UTF8_with_bom } from '@theia/core/lib/common/encodings'; import { EncodingService, ResourceEncoding, DecodeStreamResult } from '@theia/core/lib/common/encoding-service'; import { Mutable } from '@theia/core/lib/common/types'; import { readFileIntoStream } from '../common/io'; +import { FileSystemWatcherErrorHandler } from './filesystem-watcher-error-handler'; export interface FileOperationParticipant { @@ -247,6 +248,9 @@ export class FileService { @inject(ContributionProvider) @named(FileServiceContribution) protected readonly contributions: ContributionProvider; + @inject(FileSystemWatcherErrorHandler) + protected readonly watcherErrorHandler: FileSystemWatcherErrorHandler; + @postConstruct() protected init(): void { for (const contribution of this.contributions.getContributions()) { @@ -308,6 +312,7 @@ export class FileService { const providerDisposables = new DisposableCollection(); providerDisposables.push(provider.onDidChangeFile(changes => this.onDidFilesChangeEmitter.fire(new FileChangesEvent(changes)))); + providerDisposables.push(provider.onFileWatchError(() => this.handleFileWatchError())); providerDisposables.push(provider.onDidChangeCapabilities(() => this.onDidChangeFileSystemProviderCapabilitiesEmitter.fire({ provider, scheme }))); return Disposable.create(() => { @@ -1675,4 +1680,7 @@ export class FileService { // #endregion + protected handleFileWatchError(): void { + this.watcherErrorHandler.handleError(); + } } diff --git a/packages/filesystem/src/browser/filesystem-frontend-module.ts b/packages/filesystem/src/browser/filesystem-frontend-module.ts index 6b5695db471d5..18421a2bcc907 100644 --- a/packages/filesystem/src/browser/filesystem-frontend-module.ts +++ b/packages/filesystem/src/browser/filesystem-frontend-module.ts @@ -34,6 +34,7 @@ import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider'; import { RemoteFileServiceContribution } from './remote-file-service-contribution'; +import { FileSystemWatcherErrorHandler } from './filesystem-watcher-error-handler'; import { UTF8 } from '@theia/core/lib/common/encodings'; export default new ContainerModule(bind => { @@ -50,6 +51,8 @@ export default new ContainerModule(bind => { bind(FileServiceContribution).toService(RemoteFileServiceContribution); bind(FileSystemWatcher).toSelf().inSingletonScope(); + bind(FileSystemWatcherErrorHandler).toSelf().inSingletonScope(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any bind(FileSystem).toDynamicValue(({ container }) => { const fileService = container.get(FileService); diff --git a/packages/filesystem/src/browser/filesystem-watcher-error-handler.ts b/packages/filesystem/src/browser/filesystem-watcher-error-handler.ts new file mode 100644 index 0000000000000..87ce2f8048340 --- /dev/null +++ b/packages/filesystem/src/browser/filesystem-watcher-error-handler.ts @@ -0,0 +1,60 @@ +/******************************************************************************** + * Copyright (C) 2020 Arm and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { environment } from '@theia/application-package/lib/environment'; +import { MessageService } from '@theia/core'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; + +@injectable() +export class FileSystemWatcherErrorHandler { + + @inject(MessageService) protected readonly messageService: MessageService; + @inject(WindowService) protected readonly windowService: WindowService; + + protected watchHandlesExhausted: boolean = false; + + protected get instructionsLink(): string { + return 'https://code.visualstudio.com/docs/setup/linux#_visual-studio-code-is-unable-to-watch-for-file-changes-in-this-large-workspace-error-enospc'; + } + + public async handleError(): Promise { + if (!this.watchHandlesExhausted) { + this.watchHandlesExhausted = true; + if (this.isElectron()) { + const instructionsAction = 'Instructions'; + const action = await this.messageService.warn( + 'Unable to watch for file changes in this large workspace. Please follow the instructions link to resolve this issue.', + { timeout: 60000 }, + instructionsAction + ); + if (action === instructionsAction) { + this.windowService.openNewWindow(this.instructionsLink, { external: true }); + } + } else { + await this.messageService.warn( + 'Unable to watch for file changes in this large workspace. The information you see may not include recent file changes.', + { timeout: 60000 } + ); + } + } + } + + protected isElectron(): boolean { + return environment.electron.is(); + } + +} diff --git a/packages/filesystem/src/common/delegating-file-system-provider.ts b/packages/filesystem/src/common/delegating-file-system-provider.ts index 213d76f15fe55..bcd240d59cd4f 100644 --- a/packages/filesystem/src/common/delegating-file-system-provider.ts +++ b/packages/filesystem/src/common/delegating-file-system-provider.ts @@ -31,6 +31,9 @@ export class DelegatingFileSystemProvider implements Required(); readonly onDidChangeFile = this.onDidChangeFileEmitter.event; + private readonly onFileWatchErrorEmitter = new Emitter(); + readonly onFileWatchError = this.onFileWatchErrorEmitter.event; + constructor( protected readonly delegate: FileSystemProvider, protected readonly options: DelegatingFileSystemProvider.Options, @@ -38,6 +41,8 @@ export class DelegatingFileSystemProvider implements Required this.handleFileChanges(changes))); + this.toDispose.push(this.onFileWatchErrorEmitter); + this.toDispose.push(delegate.onFileWatchError(changes => this.onFileWatchErrorEmitter.fire())); } dispose(): void { diff --git a/packages/filesystem/src/common/files.ts b/packages/filesystem/src/common/files.ts index e96242479323d..4df3dccfc5413 100644 --- a/packages/filesystem/src/common/files.ts +++ b/packages/filesystem/src/common/files.ts @@ -564,6 +564,7 @@ export interface FileSystemProvider { readonly onDidChangeCapabilities: Event; readonly onDidChangeFile: Event; + readonly onFileWatchError: Event; watch(resource: URI, opts: WatchOptions): IDisposable; stat(resource: URI): Promise; diff --git a/packages/filesystem/src/common/filesystem-watcher-protocol.ts b/packages/filesystem/src/common/filesystem-watcher-protocol.ts index dc8845028a903..c2c3440afff55 100644 --- a/packages/filesystem/src/common/filesystem-watcher-protocol.ts +++ b/packages/filesystem/src/common/filesystem-watcher-protocol.ts @@ -40,6 +40,11 @@ export interface FileSystemWatcherClient { * Notify when files under watched uris are changed. */ onDidFilesChanged(event: DidFilesChangedParams): void; + + /** + * Notify when unable to watch files because of Linux handle limit. + */ + onError(): void; } export interface WatchOptions { diff --git a/packages/filesystem/src/common/remote-file-system-provider.ts b/packages/filesystem/src/common/remote-file-system-provider.ts index a9cf15e4b06ef..b823ec6b40e07 100644 --- a/packages/filesystem/src/common/remote-file-system-provider.ts +++ b/packages/filesystem/src/common/remote-file-system-provider.ts @@ -68,6 +68,7 @@ export interface RemoteFileStreamError extends Error { export interface RemoteFileSystemClient { notifyDidChangeFile(event: { changes: RemoteFileChange[] }): void; + notifyFileWatchError(): void; notifyDidChangeCapabilities(capabilities: FileSystemProviderCapabilities): void; onFileStreamData(handle: number, data: number[]): void; onFileStreamEnd(handle: number, error: RemoteFileStreamError | undefined): void; @@ -108,6 +109,9 @@ export class RemoteFileSystemProvider implements Required, D private readonly onDidChangeFileEmitter = new Emitter(); readonly onDidChangeFile = this.onDidChangeFileEmitter.event; + private readonly onFileWatchErrorEmitter = new Emitter(); + readonly onFileWatchError = this.onFileWatchErrorEmitter.event; + private readonly onDidChangeCapabilitiesEmitter = new Emitter(); readonly onDidChangeCapabilities = this.onDidChangeCapabilitiesEmitter.event; @@ -151,6 +155,9 @@ export class RemoteFileSystemProvider implements Required, D notifyDidChangeFile: ({ changes }) => { this.onDidChangeFileEmitter.fire(changes.map(event => ({ resource: new URI(event.resource), type: event.type }))); }, + notifyFileWatchError: () => { + this.onFileWatchErrorEmitter.fire(); + }, notifyDidChangeCapabilities: capabilities => this.setCapabilities(capabilities), onFileStreamData: (handle, data) => this.onFileStreamDataEmitter.fire([handle, Uint8Array.from(data)]), onFileStreamEnd: (handle, error) => this.onFileStreamEndEmitter.fire([handle, error]) @@ -338,6 +345,11 @@ export class FileSystemProviderServer implements RemoteFileSystemServer { }); } })); + this.toDispose.push(this.provider.onFileWatchError(() => { + if (this.client) { + this.client.notifyFileWatchError(); + } + })); } async getCapabilities(): Promise { diff --git a/packages/filesystem/src/node/disk-file-system-provider.ts b/packages/filesystem/src/node/disk-file-system-provider.ts index e9e4032f0636a..341c6c6c60636 100644 --- a/packages/filesystem/src/node/disk-file-system-provider.ts +++ b/packages/filesystem/src/node/disk-file-system-provider.ts @@ -95,6 +95,9 @@ export class DiskFileSystemProvider implements Disposable, private readonly onDidChangeFileEmitter = new Emitter(); readonly onDidChangeFile = this.onDidChangeFileEmitter.event; + private readonly onFileWatchErrorEmitter = new Emitter(); + readonly onFileWatchError = this.onFileWatchErrorEmitter.event; + protected readonly toDispose = new DisposableCollection( this.onDidChangeFileEmitter ); @@ -112,7 +115,8 @@ export class DiskFileSystemProvider implements Disposable, onDidFilesChanged: params => this.onDidChangeFileEmitter.fire(params.changes.map(({ uri, type }) => ({ resource: new URI(uri), type - }))) + }))), + onError: () => this.onFileWatchErrorEmitter.fire() }); } diff --git a/packages/filesystem/src/node/filesystem-watcher-client.ts b/packages/filesystem/src/node/filesystem-watcher-client.ts index 60495b65113ed..8e5fa512facdf 100644 --- a/packages/filesystem/src/node/filesystem-watcher-client.ts +++ b/packages/filesystem/src/node/filesystem-watcher-client.ts @@ -41,6 +41,11 @@ export class FileSystemWatcherServerClient implements FileSystemWatcherServer { if (this.client) { this.client.onDidFilesChanged(e); } + }, + onError: () => { + if (this.client) { + this.client.onError(); + } } }); this.toDispose.push(this.remote); diff --git a/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.spec.ts b/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.spec.ts index 3fed295aba0f8..22f7afad1856e 100644 --- a/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.spec.ts +++ b/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.spec.ts @@ -57,6 +57,8 @@ describe('nsfw-filesystem-watcher', function (): void { const watcherClient = { onDidFilesChanged(event: DidFilesChangedParams): void { event.changes.forEach(c => actualUris.add(c.uri.toString())); + }, + onError(): void { } }; watcherServer.setClient(watcherClient); @@ -92,6 +94,8 @@ describe('nsfw-filesystem-watcher', function (): void { const watcherClient = { onDidFilesChanged(event: DidFilesChangedParams): void { event.changes.forEach(c => actualUris.add(c.uri.toString())); + }, + onError(): void { } }; watcherServer.setClient(watcherClient); diff --git a/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.ts b/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.ts index 51a8f78d7462c..d35299aeded78 100644 --- a/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.ts +++ b/packages/filesystem/src/node/nsfw-watcher/nsfw-filesystem-watcher.ts @@ -131,6 +131,11 @@ export class NsfwFileSystemWatcherServer implements FileSystemWatcherServer { errorCallback: error => { // see https://github.com/atom/github/issues/342 console.warn(`Failed to watch "${basePath}":`, error); + if (error === 'Inotify limit reached') { + if (this.client) { + this.client.onError(); + } + } this.unwatchFileChanges(watcherId); }, ...this.options.nsfwOptions diff --git a/packages/plugin-ext/src/main/browser/file-system-main-impl.ts b/packages/plugin-ext/src/main/browser/file-system-main-impl.ts index 17e406e866852..416ff7f690490 100644 --- a/packages/plugin-ext/src/main/browser/file-system-main-impl.ts +++ b/packages/plugin-ext/src/main/browser/file-system-main-impl.ts @@ -160,6 +160,7 @@ class RemoteFileSystemProvider implements FileSystemProviderWithFileReadWriteCap private readonly _registration: IDisposable; readonly onDidChangeFile: Event = this._onDidChange.event; + readonly onFileWatchError: Event = new Emitter().event; // dummy, never fired readonly capabilities: FileSystemProviderCapabilities; readonly onDidChangeCapabilities: Event = Event.None;