Skip to content

Commit d4b8ff6

Browse files
authored
Directories dont check modified time when sending "change" event (#57938)
1 parent a1c4732 commit d4b8ff6

File tree

5 files changed

+377
-17
lines changed

5 files changed

+377
-17
lines changed

src/compiler/sys.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -378,16 +378,24 @@ function createDynamicPriorityPollingWatchFile(host: {
378378
}
379379
}
380380

381-
function createUseFsEventsOnParentDirectoryWatchFile(fsWatch: FsWatch, useCaseSensitiveFileNames: boolean): HostWatchFile {
381+
function createUseFsEventsOnParentDirectoryWatchFile(
382+
fsWatch: FsWatch,
383+
useCaseSensitiveFileNames: boolean,
384+
getModifiedTime: NonNullable<System["getModifiedTime"]>,
385+
fsWatchWithTimestamp: boolean | undefined,
386+
): HostWatchFile {
382387
// One file can have multiple watchers
383388
const fileWatcherCallbacks = createMultiMap<string, FileWatcherCallback>();
389+
const fileTimestamps = fsWatchWithTimestamp ? new Map<string, Date>() : undefined;
384390
const dirWatchers = new Map<string, DirectoryWatcher>();
385391
const toCanonicalName = createGetCanonicalFileName(useCaseSensitiveFileNames);
386392
return nonPollingWatchFile;
387393

388394
function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback, _pollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined): FileWatcher {
389395
const filePath = toCanonicalName(fileName);
390-
fileWatcherCallbacks.add(filePath, callback);
396+
if (fileWatcherCallbacks.add(filePath, callback).length === 1 && fileTimestamps) {
397+
fileTimestamps.set(filePath, getModifiedTime(fileName) || missingFileModifiedTime);
398+
}
391399
const dirPath = getDirectoryPath(filePath) || ".";
392400
const watcher = dirWatchers.get(dirPath) ||
393401
createDirectoryWatcher(getDirectoryPath(fileName) || ".", dirPath, fallbackOptions);
@@ -410,15 +418,29 @@ function createUseFsEventsOnParentDirectoryWatchFile(fsWatch: FsWatch, useCaseSe
410418
const watcher = fsWatch(
411419
dirName,
412420
FileSystemEntryKind.Directory,
413-
(_eventName: string, relativeFileName, modifiedTime) => {
421+
(eventName: string, relativeFileName) => {
414422
// When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined"
415423
if (!isString(relativeFileName)) return;
416424
const fileName = getNormalizedAbsolutePath(relativeFileName, dirName);
425+
const filePath = toCanonicalName(fileName);
417426
// Some applications save a working file via rename operations
418-
const callbacks = fileName && fileWatcherCallbacks.get(toCanonicalName(fileName));
427+
const callbacks = fileName && fileWatcherCallbacks.get(filePath);
419428
if (callbacks) {
429+
let currentModifiedTime;
430+
let eventKind = FileWatcherEventKind.Changed;
431+
if (fileTimestamps) {
432+
const existingTime = fileTimestamps.get(filePath)!;
433+
if (eventName === "change") {
434+
currentModifiedTime = getModifiedTime(fileName) || missingFileModifiedTime;
435+
if (currentModifiedTime.getTime() === existingTime.getTime()) return;
436+
}
437+
currentModifiedTime ||= getModifiedTime(fileName) || missingFileModifiedTime;
438+
fileTimestamps.set(filePath, currentModifiedTime);
439+
if (existingTime === missingFileModifiedTime) eventKind = FileWatcherEventKind.Created;
440+
else if (currentModifiedTime === missingFileModifiedTime) eventKind = FileWatcherEventKind.Deleted;
441+
}
420442
for (const fileCallback of callbacks) {
421-
fileCallback(fileName, FileWatcherEventKind.Changed, modifiedTime);
443+
fileCallback(fileName, eventKind, currentModifiedTime);
422444
}
423445
}
424446
},
@@ -974,7 +996,7 @@ export function createSystemWatchFunctions({
974996
);
975997
case WatchFileKind.UseFsEventsOnParentDirectory:
976998
if (!nonPollingWatchFile) {
977-
nonPollingWatchFile = createUseFsEventsOnParentDirectoryWatchFile(fsWatch, useCaseSensitiveFileNames);
999+
nonPollingWatchFile = createUseFsEventsOnParentDirectoryWatchFile(fsWatch, useCaseSensitiveFileNames, getModifiedTime, fsWatchWithTimestamp);
9781000
}
9791001
return nonPollingWatchFile(fileName, callback, pollingInterval, getFallbackOptions(options));
9801002
default:
@@ -1191,7 +1213,7 @@ export function createSystemWatchFunctions({
11911213
return watchPresentFileSystemEntryWithFsWatchFile();
11921214
}
11931215
try {
1194-
const presentWatcher = (!fsWatchWithTimestamp ? fsWatchWorker : fsWatchWorkerHandlingTimestamp)(
1216+
const presentWatcher = (entryKind === FileSystemEntryKind.Directory || !fsWatchWithTimestamp ? fsWatchWorker : fsWatchWorkerHandlingTimestamp)(
11951217
fileOrDirectory,
11961218
recursive,
11971219
inodeWatching ?

src/testRunner/unittests/helpers/virtualFileSystemWithWatch.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -501,12 +501,12 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
501501
else {
502502
currentEntry.content = content;
503503
currentEntry.modifiedTime = this.now();
504-
this.fs.get(getDirectoryPath(currentEntry.path))!.modifiedTime = this.now();
505504
if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) {
506505
const directoryFullPath = getDirectoryPath(currentEntry.fullPath);
507-
this.invokeFileWatcher(directoryFullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime);
508-
this.invokeFsWatchesCallbacks(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
509-
this.invokeRecursiveFsWatches(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
506+
this.fs.get(getDirectoryPath(currentEntry.path))!.modifiedTime = this.now();
507+
this.invokeFileWatcher(directoryFullPath, FileWatcherEventKind.Changed, /*modifiedTime*/ undefined);
508+
this.invokeFsWatchesCallbacks(directoryFullPath, "rename", /*modifiedTime*/ undefined, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
509+
this.invokeRecursiveFsWatches(directoryFullPath, "rename", /*modifiedTime*/ undefined, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
510510
}
511511
else {
512512
this.invokeFileAndFsWatches(currentEntry.fullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
@@ -634,7 +634,7 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
634634
const inodeWatching = this.inodeWatching;
635635
if (options?.skipInodeCheckOnCreate) this.inodeWatching = false;
636636
this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Created, fileOrDirectory.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
637-
this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed, fileOrDirectory.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
637+
this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed, folder.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
638638
this.inodeWatching = inodeWatching;
639639
}
640640

@@ -741,13 +741,13 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
741741
this.invokeFsWatchesRecursiveCallbacks(fullPath, eventName, modifiedTime, entryFullPath, useTildeSuffix);
742742
const basePath = getDirectoryPath(fullPath);
743743
if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) {
744-
this.invokeRecursiveFsWatches(basePath, eventName, modifiedTime, entryFullPath || fullPath, useTildeSuffix);
744+
this.invokeRecursiveFsWatches(basePath, eventName, /*modifiedTime*/ undefined, entryFullPath || fullPath, useTildeSuffix);
745745
}
746746
}
747747

748748
invokeFsWatches(fullPath: string, eventName: "rename" | "change", modifiedTime: Date | undefined, useTildeSuffix: boolean | undefined) {
749749
this.invokeFsWatchesCallbacks(fullPath, eventName, modifiedTime, fullPath, useTildeSuffix);
750-
this.invokeFsWatchesCallbacks(getDirectoryPath(fullPath), eventName, modifiedTime, fullPath, useTildeSuffix);
750+
this.invokeFsWatchesCallbacks(getDirectoryPath(fullPath), eventName, /*modifiedTime*/ undefined, fullPath, useTildeSuffix);
751751
this.invokeRecursiveFsWatches(fullPath, eventName, modifiedTime, /*entryFullPath*/ undefined, useTildeSuffix);
752752
}
753753

src/testRunner/unittests/tscWatch/watchEnvironment.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -690,11 +690,11 @@ describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different po
690690
});
691691

692692
describe("with fsWatch with fsWatchWithTimestamp", () => {
693-
function verify(fsWatchWithTimestamp: boolean) {
693+
function verify(fsWatchWithTimestamp: boolean, watchFile?: "useFsEventsOnParentDirectory") {
694694
verifyTscWatch({
695695
scenario,
696-
subScenario: `fsWatch/fsWatchWithTimestamp ${fsWatchWithTimestamp}`,
697-
commandLineArgs: ["-w", "--extendedDiagnostics"],
696+
subScenario: `fsWatch/fsWatchWithTimestamp ${fsWatchWithTimestamp}${watchFile ? ` ${watchFile}` : ""}`,
697+
commandLineArgs: ["-w", "--extendedDiagnostics", ...(watchFile ? ["--watchFile", watchFile] : [])],
698698
sys: () =>
699699
createWatchedSystem(
700700
{
@@ -723,6 +723,8 @@ describe("unittests:: tsc-watch:: watchEnvironment:: tsc-watch with different po
723723
}
724724
verify(/*fsWatchWithTimestamp*/ true);
725725
verify(/*fsWatchWithTimestamp*/ false);
726+
verify(/*fsWatchWithTimestamp*/ true, "useFsEventsOnParentDirectory");
727+
verify(/*fsWatchWithTimestamp*/ false, "useFsEventsOnParentDirectory");
726728
});
727729

728730
verifyTscWatch({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
currentDirectory:: /user/username/projects/myproject useCaseSensitiveFileNames: false
2+
Input::
3+
//// [/a/lib/lib.d.ts]
4+
/// <reference no-default-lib="true"/>
5+
interface Boolean {}
6+
interface Function {}
7+
interface CallableFunction {}
8+
interface NewableFunction {}
9+
interface IArguments {}
10+
interface Number { toExponential: any; }
11+
interface Object {}
12+
interface RegExp {}
13+
interface String { charAt: any; }
14+
interface Array<T> { length: number; [n: number]: T; }
15+
16+
//// [/user/username/projects/myproject/main.ts]
17+
export const x = 10;
18+
19+
//// [/user/username/projects/myproject/tsconfig.json]
20+
{
21+
"files": [
22+
"main.ts"
23+
]
24+
}
25+
26+
27+
/a/lib/tsc.js -w --extendedDiagnostics --watchFile useFsEventsOnParentDirectory
28+
Output::
29+
[HH:MM:SS AM] Starting compilation in watch mode...
30+
31+
Current directory: /user/username/projects/myproject CaseSensitiveFileNames: false
32+
FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/tsconfig.json 2000 {"watchFile":5} Config file
33+
Synchronizing program
34+
CreatingProgramWith::
35+
roots: ["/user/username/projects/myproject/main.ts"]
36+
options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"}
37+
FileWatcher:: Added:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":5} Source file
38+
FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 250 {"watchFile":5} Source file
39+
DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 {"watchFile":5} Type roots
40+
Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/myproject/node_modules/@types 1 {"watchFile":5} Type roots
41+
DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 {"watchFile":5} Type roots
42+
Elapsed:: *ms DirectoryWatcher:: Added:: WatchInfo: /user/username/projects/node_modules/@types 1 {"watchFile":5} Type roots
43+
[HH:MM:SS AM] Found 0 errors. Watching for file changes.
44+
45+
46+
47+
//// [/user/username/projects/myproject/main.js]
48+
"use strict";
49+
Object.defineProperty(exports, "__esModule", { value: true });
50+
exports.x = void 0;
51+
exports.x = 10;
52+
53+
54+
55+
PolledWatches::
56+
/user/username/projects/myproject/node_modules/@types: *new*
57+
{"pollingInterval":500}
58+
/user/username/projects/node_modules/@types: *new*
59+
{"pollingInterval":500}
60+
61+
FsWatches::
62+
/a/lib: *new*
63+
{}
64+
/user/username/projects/myproject: *new*
65+
{}
66+
67+
Program root files: [
68+
"/user/username/projects/myproject/main.ts"
69+
]
70+
Program options: {
71+
"watch": true,
72+
"extendedDiagnostics": true,
73+
"configFilePath": "/user/username/projects/myproject/tsconfig.json"
74+
}
75+
Program structureReused: Not
76+
Program files::
77+
/a/lib/lib.d.ts
78+
/user/username/projects/myproject/main.ts
79+
80+
Semantic diagnostics in builder refreshed for::
81+
/a/lib/lib.d.ts
82+
/user/username/projects/myproject/main.ts
83+
84+
Shape signatures in builder refreshed for::
85+
/a/lib/lib.d.ts (used version)
86+
/user/username/projects/myproject/main.ts (used version)
87+
88+
exitCode:: ExitStatus.undefined
89+
90+
Change:: emulate access
91+
92+
Input::
93+
94+
Output::
95+
FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":5} Source file
96+
Scheduling update
97+
Elapsed:: *ms FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":5} Source file
98+
99+
100+
Timeout callback:: count: 1
101+
1: timerToUpdateProgram *new*
102+
103+
Before running Timeout callback:: count: 1
104+
1: timerToUpdateProgram
105+
106+
After running Timeout callback:: count: 0
107+
Output::
108+
Synchronizing program
109+
110+
111+
112+
113+
exitCode:: ExitStatus.undefined
114+
115+
Change:: modify file contents
116+
117+
Input::
118+
//// [/user/username/projects/myproject/main.ts]
119+
export const x = 10;export const y = 10;
120+
121+
122+
Output::
123+
FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":5} Source file
124+
Scheduling update
125+
Elapsed:: *ms FileWatcher:: Triggered with /user/username/projects/myproject/main.ts 1:: WatchInfo: /user/username/projects/myproject/main.ts 250 {"watchFile":5} Source file
126+
127+
128+
Timeout callback:: count: 1
129+
2: timerToUpdateProgram *new*
130+
131+
Before running Timeout callback:: count: 1
132+
2: timerToUpdateProgram
133+
134+
After running Timeout callback:: count: 0
135+
Output::
136+
Synchronizing program
137+
[HH:MM:SS AM] File change detected. Starting incremental compilation...
138+
139+
CreatingProgramWith::
140+
roots: ["/user/username/projects/myproject/main.ts"]
141+
options: {"watch":true,"extendedDiagnostics":true,"configFilePath":"/user/username/projects/myproject/tsconfig.json"}
142+
[HH:MM:SS AM] Found 0 errors. Watching for file changes.
143+
144+
145+
146+
//// [/user/username/projects/myproject/main.js]
147+
"use strict";
148+
Object.defineProperty(exports, "__esModule", { value: true });
149+
exports.y = exports.x = void 0;
150+
exports.x = 10;
151+
exports.y = 10;
152+
153+
154+
155+
156+
Program root files: [
157+
"/user/username/projects/myproject/main.ts"
158+
]
159+
Program options: {
160+
"watch": true,
161+
"extendedDiagnostics": true,
162+
"configFilePath": "/user/username/projects/myproject/tsconfig.json"
163+
}
164+
Program structureReused: Completely
165+
Program files::
166+
/a/lib/lib.d.ts
167+
/user/username/projects/myproject/main.ts
168+
169+
Semantic diagnostics in builder refreshed for::
170+
/user/username/projects/myproject/main.ts
171+
172+
Shape signatures in builder refreshed for::
173+
/user/username/projects/myproject/main.ts (computed .d.ts)
174+
175+
exitCode:: ExitStatus.undefined

0 commit comments

Comments
 (0)