Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Extension/src/LanguageServer/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
170 changes: 113 additions & 57 deletions Extension/src/LanguageServer/configurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,10 @@ export class CppProperties {
private currentConfigurationIndex: PersistentFolderState<number> | 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<string, Date | undefined> = new Map<string, Date | undefined>();
private configurationProviderFailedToProvide: boolean = false;
private defaultCompilerPath: string | null = null;
private knownCompilers?: KnownCompiler[];
private defaultCStandard: string | null = null;
Expand Down Expand Up @@ -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<string> = 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<string> = new Set<string>();

// 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<string> = new Set<string>();
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;
}
}

Expand Down Expand Up @@ -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();
}
Expand Down