Always recreate the file watcher when rename event occurs (#48997)

* Convert some of the watchEnvironment tests to baselines for updating later

* Add tests for inode watching by making fsWatch part of system function that tests presence before creating fs watch

* Refactor for simpler tests

* Accept map of file content or file or symlink or folder

* Add test when rename event occurs when file has already reappeared

* On rename event for the file, replace file watcher irrespective of file presence

* Fix regex

* Ensure that when doing inode watching watchers is replaces only on disappearance or appearance

* Some logging for debugging further

* Revert "Some logging for debugging further"

This reverts commit dd2164ac83.

* Add test when rename event occurs on mac with ~ appended to file name

* If the relativeFileName ends with tilde, remove it from the event

* Some logging for debugging further

* Revert "Some logging for debugging further"

This reverts commit e1ba8a8d5f.

* Add documentation and fail safe the event firing
This commit is contained in:
Sheetal Nandi
2022-06-08 17:54:57 -07:00
committed by GitHub
parent ce639352bb
commit f5ad78720f
541 changed files with 43227 additions and 7785 deletions

View File

@@ -518,7 +518,7 @@ namespace ts {
useCaseSensitiveFileNames: boolean;
getCurrentDirectory: System["getCurrentDirectory"];
getAccessibleSortedChildDirectories(path: string): readonly string[];
directoryExists(dir: string): boolean;
fileSystemEntryExists: FileSystemEntryExists;
realpath(s: string): string;
setTimeout: NonNullable<System["setTimeout"]>;
clearTimeout: NonNullable<System["clearTimeout"]>;
@@ -535,7 +535,7 @@ namespace ts {
useCaseSensitiveFileNames,
getCurrentDirectory,
getAccessibleSortedChildDirectories,
directoryExists,
fileSystemEntryExists,
realpath,
setTimeout,
clearTimeout
@@ -655,7 +655,7 @@ namespace ts {
function nonSyncUpdateChildWatches(dirName: string, dirPath: Path, fileName: string, options: WatchOptions | undefined) {
// Iterate through existing children and update the watches if needed
const parentWatcher = cache.get(dirPath);
if (parentWatcher && directoryExists(dirName)) {
if (parentWatcher && fileSystemEntryExists(dirName, FileSystemEntryKind.Directory)) {
// Schedule the update and postpone invoke for callbacks
scheduleUpdateChildWatches(dirName, dirPath, fileName, options);
return;
@@ -733,7 +733,7 @@ namespace ts {
if (!parentWatcher) return false;
let newChildWatches: ChildDirectoryWatcher[] | undefined;
const hasChanges = enumerateInsertsAndDeletes<string, ChildDirectoryWatcher>(
directoryExists(parentDir) ? mapDefined(getAccessibleSortedChildDirectories(parentDir), child => {
fileSystemEntryExists(parentDir, FileSystemEntryKind.Directory) ? mapDefined(getAccessibleSortedChildDirectories(parentDir), child => {
const childFullName = getNormalizedAbsolutePath(child, parentDir);
// Filter our the symbolic link directories since those arent included in recursive watch
// which is same behaviour when recursive: true is passed to fs.watch
@@ -780,7 +780,12 @@ namespace ts {
export type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string | undefined, modifiedTime?: Date) => void;
/*@internal*/
export type FsWatch = (fileOrDirectory: string, entryKind: FileSystemEntryKind, callback: FsWatchCallback, recursive: boolean, fallbackPollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined) => FileWatcher;
/*@internal*/
export interface FsWatchWorkerWatcher extends FileWatcher {
on(eventName: string, listener: () => void): void;
}
/*@internal*/
export type FsWatchWorker = (fileOrDirectory: string, recursive: boolean, callback: FsWatchCallback) => FsWatchWorkerWatcher;
/*@internal*/
export const enum FileSystemEntryKind {
File,
@@ -843,6 +848,9 @@ namespace ts {
};
}
/*@internal*/
export type FileSystemEntryExists = (fileorDirectrory: string, entryKind: FileSystemEntryKind) => boolean;
/*@internal*/
export interface CreateSystemWatchFunctions {
// Polling watch file
@@ -852,11 +860,11 @@ namespace ts {
setTimeout: NonNullable<System["setTimeout"]>;
clearTimeout: NonNullable<System["clearTimeout"]>;
// For fs events :
fsWatch: FsWatch;
fsWatchWorker: FsWatchWorker;
fileSystemEntryExists: FileSystemEntryExists;
useCaseSensitiveFileNames: boolean;
getCurrentDirectory: System["getCurrentDirectory"];
fsSupportsRecursiveFsWatch: boolean;
directoryExists: System["directoryExists"];
getAccessibleSortedChildDirectories(path: string): readonly string[];
realpath(s: string): string;
// For backward compatibility environment variables
@@ -864,6 +872,8 @@ namespace ts {
useNonPollingWatchers?: boolean;
tscWatchDirectory: string | undefined;
defaultWatchFileKind: System["defaultWatchFileKind"];
inodeWatching: boolean;
sysLog: (s: string) => void;
}
/*@internal*/
@@ -872,22 +882,25 @@ namespace ts {
getModifiedTime,
setTimeout,
clearTimeout,
fsWatch,
fsWatchWorker,
fileSystemEntryExists,
useCaseSensitiveFileNames,
getCurrentDirectory,
fsSupportsRecursiveFsWatch,
directoryExists,
getAccessibleSortedChildDirectories,
realpath,
tscWatchFile,
useNonPollingWatchers,
tscWatchDirectory,
defaultWatchFileKind,
inodeWatching,
sysLog,
}: CreateSystemWatchFunctions): { watchFile: HostWatchFile; watchDirectory: HostWatchDirectory; } {
let dynamicPollingWatchFile: HostWatchFile | undefined;
let fixedChunkSizePollingWatchFile: HostWatchFile | undefined;
let nonPollingWatchFile: HostWatchFile | undefined;
let hostRecursiveDirectoryWatcher: HostWatchDirectory | undefined;
let hitSystemWatcherLimit = false;
return {
watchFile,
watchDirectory
@@ -989,7 +1002,7 @@ namespace ts {
hostRecursiveDirectoryWatcher = createDirectoryWatcherSupportingRecursive({
useCaseSensitiveFileNames,
getCurrentDirectory,
directoryExists,
fileSystemEntryExists,
getAccessibleSortedChildDirectories,
watchDirectory: nonRecursiveWatchDirectory,
realpath,
@@ -1059,6 +1072,147 @@ namespace ts {
};
}
}
function fsWatch(
fileOrDirectory: string,
entryKind: FileSystemEntryKind,
callback: FsWatchCallback,
recursive: boolean,
fallbackPollingInterval: PollingInterval,
fallbackOptions: WatchOptions | undefined
): FileWatcher {
let lastDirectoryPartWithDirectorySeparator: string | undefined;
let lastDirectoryPart: string | undefined;
if (inodeWatching) {
lastDirectoryPartWithDirectorySeparator = fileOrDirectory.substring(fileOrDirectory.lastIndexOf(directorySeparator));
lastDirectoryPart = lastDirectoryPartWithDirectorySeparator.slice(directorySeparator.length);
}
/** Watcher for the file system entry depending on whether it is missing or present */
let watcher = !fileSystemEntryExists(fileOrDirectory, entryKind) ?
watchMissingFileSystemEntry() :
watchPresentFileSystemEntry();
return {
close: () => {
// Close the watcher (either existing file system entry watcher or missing file system entry watcher)
watcher.close();
watcher = undefined!;
}
};
function updateWatcher(createWatcher: () => FileWatcher) {
// If watcher is not closed, update it
if (watcher) {
sysLog(`sysLog:: ${fileOrDirectory}:: Changing watcher to ${createWatcher === watchPresentFileSystemEntry ? "Present" : "Missing"}FileSystemEntryWatcher`);
watcher.close();
watcher = createWatcher();
}
}
/**
* Watch the file or directory that is currently present
* and when the watched file or directory is deleted, switch to missing file system entry watcher
*/
function watchPresentFileSystemEntry(): FileWatcher {
if (hitSystemWatcherLimit) {
sysLog(`sysLog:: ${fileOrDirectory}:: Defaulting to watchFile`);
return watchPresentFileSystemEntryWithFsWatchFile();
}
try {
const presentWatcher = fsWatchWorker(
fileOrDirectory,
recursive,
inodeWatching ?
callbackChangingToMissingFileSystemEntry :
callback
);
// Watch the missing file or directory or error
presentWatcher.on("error", () => {
callback("rename", "");
updateWatcher(watchMissingFileSystemEntry);
});
return presentWatcher;
}
catch (e) {
// Catch the exception and use polling instead
// Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point
// so instead of throwing error, use fs.watchFile
hitSystemWatcherLimit ||= e.code === "ENOSPC";
sysLog(`sysLog:: ${fileOrDirectory}:: Changing to watchFile`);
return watchPresentFileSystemEntryWithFsWatchFile();
}
}
function callbackChangingToMissingFileSystemEntry(event: "rename" | "change", relativeName: string | undefined) {
// In some scenarios, file save operation fires event with fileName.ext~ instead of fileName.ext
// To ensure we see the file going missing and coming back up (file delete and then recreated)
// and watches being updated correctly we are calling back with fileName.ext as well as fileName.ext~
// The worst is we have fired event that was not needed but we wont miss any changes
// especially in cases where file goes missing and watches wrong inode
let originalRelativeName: string | undefined;
if (relativeName && endsWith(relativeName, "~")) {
originalRelativeName = relativeName;
relativeName = relativeName.slice(0, relativeName.length - 1);
}
// because relativeName is not guaranteed to be correct we need to check on each rename with few combinations
// Eg on ubuntu while watching app/node_modules the relativeName is "node_modules" which is neither relative nor full path
if (event === "rename" &&
(!relativeName ||
relativeName === lastDirectoryPart ||
endsWith(relativeName, lastDirectoryPartWithDirectorySeparator!))) {
const modifiedTime = getModifiedTime(fileOrDirectory) || missingFileModifiedTime;
if (originalRelativeName) callback(event, originalRelativeName, modifiedTime);
callback(event, relativeName, modifiedTime);
if (inodeWatching) {
// If this was rename event, inode has changed means we need to update watcher
updateWatcher(modifiedTime === missingFileModifiedTime ? watchMissingFileSystemEntry : watchPresentFileSystemEntry);
}
else if (modifiedTime === missingFileModifiedTime) {
updateWatcher(watchMissingFileSystemEntry);
}
}
else {
if (originalRelativeName) callback(event, originalRelativeName);
callback(event, relativeName);
}
}
/**
* Watch the file or directory using fs.watchFile since fs.watch threw exception
* Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point
*/
function watchPresentFileSystemEntryWithFsWatchFile(): FileWatcher {
return watchFile(
fileOrDirectory,
createFileWatcherCallback(callback),
fallbackPollingInterval,
fallbackOptions
);
}
/**
* Watch the file or directory that is missing
* and switch to existing file or directory when the missing filesystem entry is created
*/
function watchMissingFileSystemEntry(): FileWatcher {
return watchFile(
fileOrDirectory,
(_fileName, eventKind, modifiedTime) => {
if (eventKind === FileWatcherEventKind.Created) {
modifiedTime ||= getModifiedTime(fileOrDirectory) || missingFileModifiedTime;
if (modifiedTime !== missingFileModifiedTime) {
callback("rename", "", modifiedTime);
// Call the callback for current file or directory
// For now it could be callback for the inner directory creation,
// but just return current directory, better than current no-op
updateWatcher(watchPresentFileSystemEntry);
}
}
},
fallbackPollingInterval,
fallbackOptions
);
}
}
}
/**
@@ -1273,8 +1427,6 @@ namespace ts {
let activeSession: import("inspector").Session | "stopping" | undefined;
let profilePath = "./profile.cpuprofile";
let hitSystemWatcherLimit = false;
const Buffer: {
new (input: string, encoding?: string): any;
from?(input: string, encoding?: string): any;
@@ -1295,19 +1447,21 @@ namespace ts {
getModifiedTime,
setTimeout,
clearTimeout,
fsWatch,
fsWatchWorker,
useCaseSensitiveFileNames,
getCurrentDirectory,
fileSystemEntryExists,
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
fsSupportsRecursiveFsWatch,
directoryExists,
getAccessibleSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories,
realpath,
tscWatchFile: process.env.TSC_WATCHFILE,
useNonPollingWatchers: process.env.TSC_NONPOLLING_WATCHER,
tscWatchDirectory: process.env.TSC_WATCHDIRECTORY,
defaultWatchFileKind: () => sys!.defaultWatchFileKind?.(),
inodeWatching: isLinuxOrMacOs,
sysLog,
});
const nodeSystem: System = {
args: process.argv.slice(2),
@@ -1568,139 +1722,19 @@ namespace ts {
}
}
function fsWatch(
function fsWatchWorker(
fileOrDirectory: string,
entryKind: FileSystemEntryKind,
callback: FsWatchCallback,
recursive: boolean,
fallbackPollingInterval: PollingInterval,
fallbackOptions: WatchOptions | undefined
): FileWatcher {
let options: any;
let lastDirectoryPartWithDirectorySeparator: string | undefined;
let lastDirectoryPart: string | undefined;
if (isLinuxOrMacOs) {
lastDirectoryPartWithDirectorySeparator = fileOrDirectory.substr(fileOrDirectory.lastIndexOf(directorySeparator));
lastDirectoryPart = lastDirectoryPartWithDirectorySeparator.slice(directorySeparator.length);
}
/** Watcher for the file system entry depending on whether it is missing or present */
let watcher = !fileSystemEntryExists(fileOrDirectory, entryKind) ?
watchMissingFileSystemEntry() :
watchPresentFileSystemEntry();
return {
close: () => {
// Close the watcher (either existing file system entry watcher or missing file system entry watcher)
watcher.close();
watcher = undefined!;
}
};
/**
* Invoke the callback with rename and update the watcher if not closed
* @param createWatcher
*/
function invokeCallbackAndUpdateWatcher(createWatcher: () => FileWatcher, modifiedTime?: Date) {
sysLog(`sysLog:: ${fileOrDirectory}:: Changing watcher to ${createWatcher === watchPresentFileSystemEntry ? "Present" : "Missing"}FileSystemEntryWatcher`);
// Call the callback for current directory
callback("rename", "", modifiedTime);
// If watcher is not closed, update it
if (watcher) {
watcher.close();
watcher = createWatcher();
}
}
/**
* Watch the file or directory that is currently present
* and when the watched file or directory is deleted, switch to missing file system entry watcher
*/
function watchPresentFileSystemEntry(): FileWatcher {
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
if (options === undefined) {
if (fsSupportsRecursiveFsWatch) {
options = { persistent: true, recursive: !!recursive };
}
else {
options = { persistent: true };
}
}
if (hitSystemWatcherLimit) {
sysLog(`sysLog:: ${fileOrDirectory}:: Defaulting to fsWatchFile`);
return watchPresentFileSystemEntryWithFsWatchFile();
}
try {
const presentWatcher = _fs.watch(
fileOrDirectory,
options,
isLinuxOrMacOs ?
callbackChangingToMissingFileSystemEntry :
callback
);
// Watch the missing file or directory or error
presentWatcher.on("error", () => invokeCallbackAndUpdateWatcher(watchMissingFileSystemEntry));
return presentWatcher;
}
catch (e) {
// Catch the exception and use polling instead
// Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point
// so instead of throwing error, use fs.watchFile
hitSystemWatcherLimit ||= e.code === "ENOSPC";
sysLog(`sysLog:: ${fileOrDirectory}:: Changing to fsWatchFile`);
return watchPresentFileSystemEntryWithFsWatchFile();
}
}
function callbackChangingToMissingFileSystemEntry(event: "rename" | "change", relativeName: string | undefined) {
// because relativeName is not guaranteed to be correct we need to check on each rename with few combinations
// Eg on ubuntu while watching app/node_modules the relativeName is "node_modules" which is neither relative nor full path
const modifiedTime = getModifiedTime(fileOrDirectory) || missingFileModifiedTime;
return event === "rename" &&
(!relativeName ||
relativeName === lastDirectoryPart ||
(relativeName.lastIndexOf(lastDirectoryPartWithDirectorySeparator!) !== -1 && relativeName.lastIndexOf(lastDirectoryPartWithDirectorySeparator!) === relativeName.length - lastDirectoryPartWithDirectorySeparator!.length)) &&
modifiedTime === missingFileModifiedTime ?
invokeCallbackAndUpdateWatcher(watchMissingFileSystemEntry, modifiedTime) :
callback(event, relativeName, modifiedTime);
}
/**
* Watch the file or directory using fs.watchFile since fs.watch threw exception
* Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point
*/
function watchPresentFileSystemEntryWithFsWatchFile(): FileWatcher {
return watchFile(
fileOrDirectory,
createFileWatcherCallback(callback),
fallbackPollingInterval,
fallbackOptions
);
}
/**
* Watch the file or directory that is missing
* and switch to existing file or directory when the missing filesystem entry is created
*/
function watchMissingFileSystemEntry(): FileWatcher {
return watchFile(
fileOrDirectory,
(_fileName, eventKind, modifiedTime) => {
if (eventKind === FileWatcherEventKind.Created) {
modifiedTime ||= getModifiedTime(fileOrDirectory) || missingFileModifiedTime;
if (modifiedTime !== missingFileModifiedTime) {
// Call the callback for current file or directory
// For now it could be callback for the inner directory creation,
// but just return current directory, better than current no-op
invokeCallbackAndUpdateWatcher(watchPresentFileSystemEntry, modifiedTime);
}
}
},
fallbackPollingInterval,
fallbackOptions
);
}
callback: FsWatchCallback,
) {
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
return _fs.watch(
fileOrDirectory,
fsSupportsRecursiveFsWatch ?
{ persistent: true, recursive: !!recursive } : { persistent: true },
callback
);
}
function readFileWorker(fileName: string, _encoding?: string): string | undefined {