mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-06-16 07:13:43 -05:00
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 commitdd2164ac83. * 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 commite1ba8a8d5f. * Add documentation and fail safe the event firing
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user