From d4b8ff691855cb15880489dcc6232dfc66fc4f69 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 25 Mar 2024 16:19:30 -0700 Subject: [PATCH] Directories dont check modified time when sending "change" event (#57938) --- src/compiler/sys.ts | 36 +++- .../helpers/virtualFileSystemWithWatch.ts | 14 +- .../unittests/tscWatch/watchEnvironment.ts | 8 +- ...tamp-false-useFsEventsOnParentDirectory.js | 175 ++++++++++++++++++ ...stamp-true-useFsEventsOnParentDirectory.js | 161 ++++++++++++++++ 5 files changed, 377 insertions(+), 17 deletions(-) create mode 100644 tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-false-useFsEventsOnParentDirectory.js create mode 100644 tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-true-useFsEventsOnParentDirectory.js diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index f45efab6b66..1b404c8bf8c 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -378,16 +378,24 @@ function createDynamicPriorityPollingWatchFile(host: { } } -function createUseFsEventsOnParentDirectoryWatchFile(fsWatch: FsWatch, useCaseSensitiveFileNames: boolean): HostWatchFile { +function createUseFsEventsOnParentDirectoryWatchFile( + fsWatch: FsWatch, + useCaseSensitiveFileNames: boolean, + getModifiedTime: NonNullable, + fsWatchWithTimestamp: boolean | undefined, +): HostWatchFile { // One file can have multiple watchers const fileWatcherCallbacks = createMultiMap(); + const fileTimestamps = fsWatchWithTimestamp ? new Map() : undefined; const dirWatchers = new Map(); const toCanonicalName = createGetCanonicalFileName(useCaseSensitiveFileNames); return nonPollingWatchFile; function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback, _pollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined): FileWatcher { const filePath = toCanonicalName(fileName); - fileWatcherCallbacks.add(filePath, callback); + if (fileWatcherCallbacks.add(filePath, callback).length === 1 && fileTimestamps) { + fileTimestamps.set(filePath, getModifiedTime(fileName) || missingFileModifiedTime); + } const dirPath = getDirectoryPath(filePath) || "."; const watcher = dirWatchers.get(dirPath) || createDirectoryWatcher(getDirectoryPath(fileName) || ".", dirPath, fallbackOptions); @@ -410,15 +418,29 @@ function createUseFsEventsOnParentDirectoryWatchFile(fsWatch: FsWatch, useCaseSe const watcher = fsWatch( dirName, FileSystemEntryKind.Directory, - (_eventName: string, relativeFileName, modifiedTime) => { + (eventName: string, relativeFileName) => { // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" if (!isString(relativeFileName)) return; const fileName = getNormalizedAbsolutePath(relativeFileName, dirName); + const filePath = toCanonicalName(fileName); // Some applications save a working file via rename operations - const callbacks = fileName && fileWatcherCallbacks.get(toCanonicalName(fileName)); + const callbacks = fileName && fileWatcherCallbacks.get(filePath); if (callbacks) { + let currentModifiedTime; + let eventKind = FileWatcherEventKind.Changed; + if (fileTimestamps) { + const existingTime = fileTimestamps.get(filePath)!; + if (eventName === "change") { + currentModifiedTime = getModifiedTime(fileName) || missingFileModifiedTime; + if (currentModifiedTime.getTime() === existingTime.getTime()) return; + } + currentModifiedTime ||= getModifiedTime(fileName) || missingFileModifiedTime; + fileTimestamps.set(filePath, currentModifiedTime); + if (existingTime === missingFileModifiedTime) eventKind = FileWatcherEventKind.Created; + else if (currentModifiedTime === missingFileModifiedTime) eventKind = FileWatcherEventKind.Deleted; + } for (const fileCallback of callbacks) { - fileCallback(fileName, FileWatcherEventKind.Changed, modifiedTime); + fileCallback(fileName, eventKind, currentModifiedTime); } } }, @@ -974,7 +996,7 @@ export function createSystemWatchFunctions({ ); case WatchFileKind.UseFsEventsOnParentDirectory: if (!nonPollingWatchFile) { - nonPollingWatchFile = createUseFsEventsOnParentDirectoryWatchFile(fsWatch, useCaseSensitiveFileNames); + nonPollingWatchFile = createUseFsEventsOnParentDirectoryWatchFile(fsWatch, useCaseSensitiveFileNames, getModifiedTime, fsWatchWithTimestamp); } return nonPollingWatchFile(fileName, callback, pollingInterval, getFallbackOptions(options)); default: @@ -1191,7 +1213,7 @@ export function createSystemWatchFunctions({ return watchPresentFileSystemEntryWithFsWatchFile(); } try { - const presentWatcher = (!fsWatchWithTimestamp ? fsWatchWorker : fsWatchWorkerHandlingTimestamp)( + const presentWatcher = (entryKind === FileSystemEntryKind.Directory || !fsWatchWithTimestamp ? fsWatchWorker : fsWatchWorkerHandlingTimestamp)( fileOrDirectory, recursive, inodeWatching ? diff --git a/src/testRunner/unittests/helpers/virtualFileSystemWithWatch.ts b/src/testRunner/unittests/helpers/virtualFileSystemWithWatch.ts index 042f59ca847..7e24e71306d 100644 --- a/src/testRunner/unittests/helpers/virtualFileSystemWithWatch.ts +++ b/src/testRunner/unittests/helpers/virtualFileSystemWithWatch.ts @@ -501,12 +501,12 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost, else { currentEntry.content = content; currentEntry.modifiedTime = this.now(); - this.fs.get(getDirectoryPath(currentEntry.path))!.modifiedTime = this.now(); if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) { const directoryFullPath = getDirectoryPath(currentEntry.fullPath); - this.invokeFileWatcher(directoryFullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime); - this.invokeFsWatchesCallbacks(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName); - this.invokeRecursiveFsWatches(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName); + this.fs.get(getDirectoryPath(currentEntry.path))!.modifiedTime = this.now(); + this.invokeFileWatcher(directoryFullPath, FileWatcherEventKind.Changed, /*modifiedTime*/ undefined); + this.invokeFsWatchesCallbacks(directoryFullPath, "rename", /*modifiedTime*/ undefined, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName); + this.invokeRecursiveFsWatches(directoryFullPath, "rename", /*modifiedTime*/ undefined, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName); } else { this.invokeFileAndFsWatches(currentEntry.fullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName); @@ -634,7 +634,7 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost, const inodeWatching = this.inodeWatching; if (options?.skipInodeCheckOnCreate) this.inodeWatching = false; this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Created, fileOrDirectory.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName); - this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed, fileOrDirectory.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName); + this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed, folder.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName); this.inodeWatching = inodeWatching; } @@ -741,13 +741,13 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost, this.invokeFsWatchesRecursiveCallbacks(fullPath, eventName, modifiedTime, entryFullPath, useTildeSuffix); const basePath = getDirectoryPath(fullPath); if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) { - this.invokeRecursiveFsWatches(basePath, eventName, modifiedTime, entryFullPath || fullPath, useTildeSuffix); + this.invokeRecursiveFsWatches(basePath, eventName, /*modifiedTime*/ undefined, entryFullPath || fullPath, useTildeSuffix); } } invokeFsWatches(fullPath: string, eventName: "rename" | "change", modifiedTime: Date | undefined, useTildeSuffix: boolean | undefined) { this.invokeFsWatchesCallbacks(fullPath, eventName, modifiedTime, fullPath, useTildeSuffix); - this.invokeFsWatchesCallbacks(getDirectoryPath(fullPath), eventName, modifiedTime, fullPath, useTildeSuffix); + this.invokeFsWatchesCallbacks(getDirectoryPath(fullPath), eventName, /*modifiedTime*/ undefined, fullPath, useTildeSuffix); this.invokeRecursiveFsWatches(fullPath, eventName, modifiedTime, /*entryFullPath*/ undefined, useTildeSuffix); } diff --git a/src/testRunner/unittests/tscWatch/watchEnvironment.ts b/src/testRunner/unittests/tscWatch/watchEnvironment.ts index c8278c047b0..e936d62ee46 100644 --- a/src/testRunner/unittests/tscWatch/watchEnvironment.ts +++ b/src/testRunner/unittests/tscWatch/watchEnvironment.ts @@ -690,11 +690,11 @@ describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different po }); describe("with fsWatch with fsWatchWithTimestamp", () => { - function verify(fsWatchWithTimestamp: boolean) { + function verify(fsWatchWithTimestamp: boolean, watchFile?: "useFsEventsOnParentDirectory") { verifyTscWatch({ scenario, - subScenario: `fsWatch/fsWatchWithTimestamp ${fsWatchWithTimestamp}`, - commandLineArgs: ["-w", "--extendedDiagnostics"], + subScenario: `fsWatch/fsWatchWithTimestamp ${fsWatchWithTimestamp}${watchFile ? ` ${watchFile}` : ""}`, + commandLineArgs: ["-w", "--extendedDiagnostics", ...(watchFile ? ["--watchFile", watchFile] : [])], sys: () => createWatchedSystem( { @@ -723,6 +723,8 @@ describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different po } verify(/*fsWatchWithTimestamp*/ true); verify(/*fsWatchWithTimestamp*/ false); + verify(/*fsWatchWithTimestamp*/ true, "useFsEventsOnParentDirectory"); + verify(/*fsWatchWithTimestamp*/ false, "useFsEventsOnParentDirectory"); }); verifyTscWatch({ diff --git a/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-false-useFsEventsOnParentDirectory.js b/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-false-useFsEventsOnParentDirectory.js new file mode 100644 index 00000000000..7266fe1c8b6 --- /dev/null +++ b/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-false-useFsEventsOnParentDirectory.js @@ -0,0 +1,175 @@ +currentDirectory:: /user/username/projects/myproject useCaseSensitiveFileNames: false +Input:: +//// [/a/lib/lib.d.ts] +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } + +//// [/user/username/projects/myproject/main.ts] +export const x = 10; + +//// [/user/username/projects/myproject/tsconfig.json] +{ + "files": [ + "main.ts" + ] +} + + +/a/lib/tsc.js -w --extendedDiagnostics --watchFile useFsEventsOnParentDirectory +Output:: +[HH:MM:SS AM] Starting compilation in watch mode... + +Current directory: /user/username/projects/myproject CaseSensitiveFileNames: false +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/tsconfig.json 2000 {"watchFile":5} Config file +Synchronizing program +CreatingProgramWith:: + roots: ["/user/username/projects/myproject/main.ts"] + options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":5} Source file +FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 250 {"watchFile":5} Source file +DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 {"watchFile":5} Type roots +Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 {"watchFile":5} Type roots +DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 {"watchFile":5} Type roots +Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 {"watchFile":5} Type roots +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + + + +//// [/user/username/projects/myproject/main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.x = void 0; +exports.x = 10; + + + +PolledWatches:: +/user/username/projects/myproject/node_modules/@types: *new* + {"pollingInterval":500} +/user/username/projects/node_modules/@types: *new* + {"pollingInterval":500} + +FsWatches:: +/a/lib: *new* + {} +/user/username/projects/myproject: *new* + {} + +Program root files: [ + "/user/username/projects/myproject/main.ts" +] +Program options: { + "watch": true, + "extendedDiagnostics": true, + "configFilePath": "/user/username/projects/myproject/tsconfig.json" +} +Program structureReused: Not +Program files:: +/a/lib/lib.d.ts +/user/username/projects/myproject/main.ts + +Semantic diagnostics in builder refreshed for:: +/a/lib/lib.d.ts +/user/username/projects/myproject/main.ts + +Shape signatures in builder refreshed for:: +/a/lib/lib.d.ts (used version) +/user/username/projects/myproject/main.ts (used version) + +exitCode:: ExitStatus.undefined + +Change:: emulate access + +Input:: + +Output:: +FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":5} Source file +Scheduling update +Elapsed:: *ms FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":5} Source file + + +Timeout callback:: count: 1 +1: timerToUpdateProgram *new* + +Before running Timeout callback:: count: 1 +1: timerToUpdateProgram + +After running Timeout callback:: count: 0 +Output:: +Synchronizing program + + + + +exitCode:: ExitStatus.undefined + +Change:: modify file contents + +Input:: +//// [/user/username/projects/myproject/main.ts] +export const x = 10;export const y = 10; + + +Output:: +FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":5} Source file +Scheduling update +Elapsed:: *ms FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":5} Source file + + +Timeout callback:: count: 1 +2: timerToUpdateProgram *new* + +Before running Timeout callback:: count: 1 +2: timerToUpdateProgram + +After running Timeout callback:: count: 0 +Output:: +Synchronizing program +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +CreatingProgramWith:: + roots: ["/user/username/projects/myproject/main.ts"] + options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + + + +//// [/user/username/projects/myproject/main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.y = exports.x = void 0; +exports.x = 10; +exports.y = 10; + + + + +Program root files: [ + "/user/username/projects/myproject/main.ts" +] +Program options: { + "watch": true, + "extendedDiagnostics": true, + "configFilePath": "/user/username/projects/myproject/tsconfig.json" +} +Program structureReused: Completely +Program files:: +/a/lib/lib.d.ts +/user/username/projects/myproject/main.ts + +Semantic diagnostics in builder refreshed for:: +/user/username/projects/myproject/main.ts + +Shape signatures in builder refreshed for:: +/user/username/projects/myproject/main.ts (computed .d.ts) + +exitCode:: ExitStatus.undefined diff --git a/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-true-useFsEventsOnParentDirectory.js b/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-true-useFsEventsOnParentDirectory.js new file mode 100644 index 00000000000..9f8f174641f --- /dev/null +++ b/tests/baselines/reference/tscWatch/watchEnvironment/fsWatch/fsWatchWithTimestamp-true-useFsEventsOnParentDirectory.js @@ -0,0 +1,161 @@ +currentDirectory:: /user/username/projects/myproject useCaseSensitiveFileNames: false +Input:: +//// [/a/lib/lib.d.ts] +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } + +//// [/user/username/projects/myproject/main.ts] +export const x = 10; + +//// [/user/username/projects/myproject/tsconfig.json] +{ + "files": [ + "main.ts" + ] +} + + +/a/lib/tsc.js -w --extendedDiagnostics --watchFile useFsEventsOnParentDirectory +Output:: +[HH:MM:SS AM] Starting compilation in watch mode... + +Current directory: /user/username/projects/myproject CaseSensitiveFileNames: false +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/tsconfig.json 2000 {"watchFile":5} Config file +Synchronizing program +CreatingProgramWith:: + roots: ["/user/username/projects/myproject/main.ts"] + options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":5} Source file +FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 250 {"watchFile":5} Source file +DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 {"watchFile":5} Type roots +Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 {"watchFile":5} Type roots +DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 {"watchFile":5} Type roots +Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 {"watchFile":5} Type roots +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + + + +//// [/user/username/projects/myproject/main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.x = void 0; +exports.x = 10; + + + +PolledWatches:: +/user/username/projects/myproject/node_modules/@types: *new* + {"pollingInterval":500} +/user/username/projects/node_modules/@types: *new* + {"pollingInterval":500} + +FsWatches:: +/a/lib: *new* + {} +/user/username/projects/myproject: *new* + {} + +Program root files: [ + "/user/username/projects/myproject/main.ts" +] +Program options: { + "watch": true, + "extendedDiagnostics": true, + "configFilePath": "/user/username/projects/myproject/tsconfig.json" +} +Program structureReused: Not +Program files:: +/a/lib/lib.d.ts +/user/username/projects/myproject/main.ts + +Semantic diagnostics in builder refreshed for:: +/a/lib/lib.d.ts +/user/username/projects/myproject/main.ts + +Shape signatures in builder refreshed for:: +/a/lib/lib.d.ts (used version) +/user/username/projects/myproject/main.ts (used version) + +exitCode:: ExitStatus.undefined + +Change:: emulate access + +Input:: + +Before running Timeout callback:: count: 0 + +After running Timeout callback:: count: 0 + + +exitCode:: ExitStatus.undefined + +Change:: modify file contents + +Input:: +//// [/user/username/projects/myproject/main.ts] +export const x = 10;export const y = 10; + + +Output:: +FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":5} Source file +Scheduling update +Elapsed:: *ms FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":5} Source file + + +Timeout callback:: count: 1 +1: timerToUpdateProgram *new* + +Before running Timeout callback:: count: 1 +1: timerToUpdateProgram + +After running Timeout callback:: count: 0 +Output:: +Synchronizing program +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +CreatingProgramWith:: + roots: ["/user/username/projects/myproject/main.ts"] + options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"} +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + + + +//// [/user/username/projects/myproject/main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.y = exports.x = void 0; +exports.x = 10; +exports.y = 10; + + + + +Program root files: [ + "/user/username/projects/myproject/main.ts" +] +Program options: { + "watch": true, + "extendedDiagnostics": true, + "configFilePath": "/user/username/projects/myproject/tsconfig.json" +} +Program structureReused: Completely +Program files:: +/a/lib/lib.d.ts +/user/username/projects/myproject/main.ts + +Semantic diagnostics in builder refreshed for:: +/user/username/projects/myproject/main.ts + +Shape signatures in builder refreshed for:: +/user/username/projects/myproject/main.ts (computed .d.ts) + +exitCode:: ExitStatus.undefined