Skip to content

Commit 6d1bdb1

Browse files
hanniavaleraHannia Valera
andcommitted
Presets: prevent re-entrant reapply loop triggered by symlinked preset files (#4670)
* attempt at fixing infinite preset loop, added new tests to validate * fixed tests for macos * changing approach to adding a startup grace period for filewatcher class --------- Co-authored-by: Hannia Valera <[email protected]>
1 parent 8d70158 commit 6d1bdb1

3 files changed

Lines changed: 432 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# What's New?
22

3+
## 1.23
4+
5+
Features:
6+
- triple: Add riscv32be riscv64be support. [#4648](https://github.com/microsoft/vscode-cmake-tools/pull/4648) [@lygstate](https://github.com/lygstate)
7+
8+
Bug Fixes:
9+
10+
- Fix infinite presets reloading loop when preset files are symlinks or include symlinked files. [#4668](https://github.com/microsoft/vscode-cmake-tools/issues/4668)
11+
- Fix user-level tasks defined in `~/.config/Code/User/tasks.json` causing infinite spinner. [#4659](https://github.com/microsoft/vscode-cmake-tools/pull/4659)
12+
- Fix "Copy Value" in CMake debugger copying variable name instead of value. [#4551](https://github.com/microsoft/vscode-cmake-tools/issues/4551)
13+
- cmakeDriver: Fixes getCompilerVersion by using compilerPath instead of compilerName. [#4647](https://github.com/microsoft/vscode-cmake-tools/pull/4647) [@lygstate](https://github.com/lygstate)
14+
315
## 1.22
416

517
Features:

src/presets/presetsController.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,29 +1632,53 @@ class FileWatcher implements vscode.Disposable {
16321632
private watchers: Map<string, chokidar.FSWatcher>;
16331633
// Debounce the change handler to avoid multiple changes being triggered by a single file change. Two change events are coming in rapid succession without this.
16341634
private canRunChangeHandler = true;
1635+
// Grace period flag to ignore events during watcher startup. When followSymlinks is false and
1636+
// watched files are symlinks, chokidar may emit spurious events during initialization that
1637+
// bypass ignoreInitial. This prevents infinite loops when reapplyPresets() recreates the watcher.
1638+
// See issue #4668.
1639+
private isInStartupGracePeriod = true;
16351640

16361641
public constructor(paths: string | string[], eventHandlers: Map<string, () => void>, options?: chokidar.WatchOptions) {
16371642
this.watchers = new Map<string, chokidar.FSWatcher>();
16381643

1639-
// We debounce the change event to avoid multiple changes being triggered by a single file change.
1640-
const onChange = eventHandlers.get('change');
1641-
if (onChange) {
1642-
const debouncedOnChange = () => {
1643-
if (this.canRunChangeHandler) {
1644-
onChange();
1645-
this.canRunChangeHandler = false;
1646-
setTimeout(() => (this.canRunChangeHandler = true), 500);
1647-
}
1648-
};
1649-
eventHandlers.set("change", debouncedOnChange);
1644+
// Allow a short grace period for the watcher to stabilize before processing events.
1645+
// This handles the case where symlinks cause spurious events during watcher setup.
1646+
// See issue #4668.
1647+
setTimeout(() => (this.isInStartupGracePeriod = false), 100);
1648+
1649+
// Wrap all event handlers to respect the startup grace period
1650+
const wrappedHandlers = new Map<string, () => void>();
1651+
for (const [event, handler] of eventHandlers) {
1652+
if (event === 'change') {
1653+
// Change events get additional debouncing to avoid multiple changes
1654+
// being triggered by a single file change
1655+
const debouncedOnChange = () => {
1656+
if (this.isInStartupGracePeriod) {
1657+
return; // Ignore events during startup grace period
1658+
}
1659+
if (this.canRunChangeHandler) {
1660+
handler();
1661+
this.canRunChangeHandler = false;
1662+
setTimeout(() => (this.canRunChangeHandler = true), 500);
1663+
}
1664+
};
1665+
wrappedHandlers.set(event, debouncedOnChange);
1666+
} else {
1667+
// Other events just respect the grace period
1668+
const wrappedHandler = () => {
1669+
if (this.isInStartupGracePeriod) {
1670+
return; // Ignore events during startup grace period
1671+
}
1672+
handler();
1673+
};
1674+
wrappedHandlers.set(event, wrappedHandler);
1675+
}
16501676
}
16511677

16521678
for (const path of Array.isArray(paths) ? paths : [paths]) {
16531679
try {
16541680
const watcher = chokidar.watch(path, { ...options });
1655-
const eventHandlerEntries = Array.from(eventHandlers);
1656-
for (let i = 0; i < eventHandlerEntries.length; i++) {
1657-
const [event, handler] = eventHandlerEntries[i];
1681+
for (const [event, handler] of wrappedHandlers) {
16581682
watcher.on(event, handler);
16591683
}
16601684
this.watchers.set(path, watcher);

0 commit comments

Comments
 (0)