diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index e46743815..8300a6b2d 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -2022,9 +2022,13 @@ export class DefaultClient implements Client { } const provider: CustomConfigurationProvider1 | undefined = getCustomConfigProviders().get(providerId); if (!provider || !provider.isReady) { + this.configuration.configurationProviderFailed(); return; } const resultCode = await this.provideCustomConfigurationAsync(docUri, provider); + if (resultCode !== "success") { + this.configuration.configurationProviderFailed(); + } telemetry.logLanguageServerEvent('provideCustomConfiguration', { providerId, resultCode }); } finally { onFinished(); diff --git a/Extension/src/LanguageServer/configurations.ts b/Extension/src/LanguageServer/configurations.ts index 3339514cc..1b70ed78a 100644 --- a/Extension/src/LanguageServer/configurations.ts +++ b/Extension/src/LanguageServer/configurations.ts @@ -136,9 +136,10 @@ export class CppProperties { private currentConfigurationIndex: PersistentFolderState | undefined; private configFileWatcher: vscode.FileSystemWatcher | null = null; private configFileWatcherFallbackTime: Date = new Date(); // Used when file watching fails. - private compileCommandsFile: vscode.Uri | undefined | null = undefined; - private compileCommandsFileWatchers: fs.FSWatcher[] = []; - private compileCommandsFileWatcherFallbackTime: Date = new Date(); // Used when file watching fails. + private compileCommandsFile: string | undefined = undefined; + private compileCommandsFileWatcher: fs.FSWatcher | undefined = undefined; + private compileCommandsLastCheckedTime: Map = new Map(); + private configurationProviderFailedToProvide: boolean = false; private defaultCompilerPath: string | null = null; private knownCompilers?: KnownCompiler[]; private defaultCStandard: string | null = null; @@ -1104,54 +1105,78 @@ export class CppProperties { } } - this.updateCompileCommandsFileWatchers(); + this.configurationProviderFailedToProvide = false; + this.clearStaleCompileCommandsPaths(); + this.updateCompileCommandsFileWatcher(); if (!this.configurationIncomplete) { this.onConfigurationsChanged(); } } + private clearStaleCompileCommandsPaths(): void { + const paths: Set = new Set(); + this.configurationJson?.configurations.forEach((config: Configuration) => { + const path = this.resolvePath(config.compileCommands); + if (path.length > 0) { + paths.add(path); + } + }); + + for (const path of this.compileCommandsLastCheckedTime.keys()) { + if (!paths.has(path)) { + this.compileCommandsLastCheckedTime.delete(path); + } + } + } + private compileCommandsFileWatcherTimer?: NodeJS.Timeout; - private compileCommandsFileWatcherFiles: Set = new Set(); - // Dispose existing and loop through cpp and populate with each file (exists or not) as you go. - // paths are expected to have variables resolved already - public updateCompileCommandsFileWatchers(): void { - if (this.configurationJson) { - this.compileCommandsFileWatchers.forEach((watcher: fs.FSWatcher) => watcher.close()); - this.compileCommandsFileWatchers = []; // reset it - const filePaths: Set = new Set(); - this.configurationJson.configurations.forEach(c => { - if (c.compileCommands) { - const fileSystemCompileCommandsPath: string = this.resolvePath(c.compileCommands); - if (fs.existsSync(fileSystemCompileCommandsPath)) { - filePaths.add(fileSystemCompileCommandsPath); + public updateCompileCommandsFileWatcher(): void { + this.compileCommandsFileWatcher?.close(); + this.compileCommandsFileWatcher = undefined; + + // If the configuration provider is set, rely on it until it fails to do so. + if (this.CurrentConfiguration?.configurationProvider && !this.configurationProviderFailedToProvide) { + return; + } + + const path: string = this.resolvePath(this.CurrentConfiguration?.compileCommands); + try { + // Before starting the file watcher, we check if the file has changed since last time we checked it. + // This is used to detect if the file has changed while we used a different configuration. + const stats = fs.statSync(path); + const lastChecked: Date | undefined = this.compileCommandsLastCheckedTime.get(path); + if (lastChecked === undefined || stats.mtime > lastChecked) { + this.onCompileCommandsChanged(path); + } + this.compileCommandsLastCheckedTime.set(path, new Date()); + + this.compileCommandsFileWatcher = fs.watch(path, (eventType: fs.WatchEventType, _: string | null) => { + // Wait 1 second after a change to allow time for the write to finish. + clearInterval(this.compileCommandsFileWatcherTimer); + this.compileCommandsFileWatcherTimer = setTimeout(() => { + this.onCompileCommandsChanged(path); + clearInterval(this.compileCommandsFileWatcherTimer); + this.compileCommandsFileWatcherTimer = undefined; + this.compileCommandsLastCheckedTime.set(path, new Date()); + + // If the file was deleted/renamed, + // Linux based systems lose track of the file. (inode deleted) + // We need to close the watcher and wait until file is created again. + if (eventType === "rename") { + this.compileCommandsFileWatcher?.close(); + this.compileCommandsFileWatcher = undefined; + this.compileCommandsFile = undefined; } - } + }, 1000); }); - try { - filePaths.forEach((path: string) => { - this.compileCommandsFileWatchers.push(fs.watch(path, () => { - // Wait 1 second after a change to allow time for the write to finish. - if (this.compileCommandsFileWatcherTimer) { - clearInterval(this.compileCommandsFileWatcherTimer); - } - this.compileCommandsFileWatcherFiles.add(path); - this.compileCommandsFileWatcherTimer = setTimeout(() => { - this.compileCommandsFileWatcherFiles.forEach((path: string) => { - this.onCompileCommandsChanged(path); - }); - if (this.compileCommandsFileWatcherTimer) { - clearInterval(this.compileCommandsFileWatcherTimer); - } - this.compileCommandsFileWatcherFiles.clear(); - this.compileCommandsFileWatcherTimer = undefined; - }, 1000); - })); - }); - } catch (e) { - // The file watcher limit is hit. - // TODO: Check if the compile commands file has a higher timestamp during the interval timer. - } + } + catch (e: any) { + // Either file not created or too many active watchers. + // Rely on polling until the file is created. + // Then, file watching will be attempted again. + this.compileCommandsFileWatcher?.close(); + this.compileCommandsFileWatcher = undefined; } } @@ -2300,34 +2325,65 @@ export class CppProperties { }); } + /** + * Configuration provider failed to provide for some file. + * This is used to determine if we should start watching for changes in the `compileCommands` file. + * + * NOTE: This is only used when `configurationProvider` is set. + */ + public configurationProviderFailed(): void { + this.configurationProviderFailedToProvide = true; + } + + /** + * Manually check for changes in the compileCommands file. + * + * NOTE: The check is skipped on any of the following terms: + * - There is an active `compile_commands.json` file watcher. + * - The `configurationProvider` property is set and `configurationProviderFailed()` was not called. + * - The `compileCommands` property is not set. + */ public checkCompileCommands(): void { - // Check for changes in case of file watcher failure. + if (this.compileCommandsFileWatcher !== undefined) { + return; + } + if (this.CurrentConfiguration?.configurationProvider && !this.configurationProviderFailedToProvide) { + return; + } const compileCommands: string | undefined = this.CurrentConfiguration?.compileCommands; - if (!compileCommands) { + if (compileCommands === undefined) { return; } + const compileCommandsFile: string | undefined = this.resolvePath(compileCommands); - fs.stat(compileCommandsFile, (err, stats) => { - if (err) { - if (err.code === "ENOENT" && this.compileCommandsFile) { - this.compileCommandsFileWatchers = []; // reset file watchers - this.onCompileCommandsChanged(compileCommandsFile); - this.compileCommandsFile = null; // File deleted - } - } else if (stats.mtime > this.compileCommandsFileWatcherFallbackTime) { - this.compileCommandsFileWatcherFallbackTime = new Date(); + try { + const stats = fs.statSync(compileCommandsFile); + const lastChecked: Date | undefined = this.compileCommandsLastCheckedTime.get(compileCommandsFile); + if (this.compileCommandsFile === undefined || lastChecked === undefined || stats.mtime > lastChecked) { + this.compileCommandsLastCheckedTime.set(compileCommandsFile, new Date()); this.onCompileCommandsChanged(compileCommandsFile); - this.compileCommandsFile = vscode.Uri.file(compileCommandsFile); // File created. + this.compileCommandsFile = compileCommandsFile; // File created/modified. } - }); + } + catch (err: any) { + if (err.code === "ENOENT" && this.compileCommandsFile) { + this.onCompileCommandsChanged(compileCommandsFile); + this.compileCommandsFile = undefined; // File deleted. + } + } + + const providerInsufficient: boolean = !this.CurrentConfiguration?.configurationProvider || this.configurationProviderFailedToProvide; + if (this.compileCommandsFile !== undefined && providerInsufficient) { + this.updateCompileCommandsFileWatcher(); + } } dispose(): void { this.disposables.forEach((d) => d.dispose()); this.disposables = []; - this.compileCommandsFileWatchers.forEach((watcher: fs.FSWatcher) => watcher.close()); - this.compileCommandsFileWatchers = []; // reset it + this.compileCommandsFileWatcher?.close(); + this.compileCommandsFileWatcher = undefined; this.diagnosticCollection.dispose(); }