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

@@ -14,18 +14,6 @@ interface String { charAt: any; }
interface Array<T> { length: number; [n: number]: T; }`
};
export const safeList = {
path: "/safeList.json" as Path,
content: JSON.stringify({
commander: "commander",
express: "express",
jquery: "jquery",
lodash: "lodash",
moment: "moment",
chroma: "chroma-js"
})
};
function getExecutingFilePathFromLibFile(): string {
return combinePaths(getDirectoryPath(libFile.path), "tsc.js");
}
@@ -39,14 +27,15 @@ interface Array<T> { length: number; [n: number]: T; }`
environmentVariables?: ESMap<string, string>;
runWithoutRecursiveWatches?: boolean;
runWithFallbackPolling?: boolean;
inodeWatching?: boolean;
}
export function createWatchedSystem(fileOrFolderList: readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost {
return new TestServerHost(/*withSafelist*/ false, fileOrFolderList, params);
export function createWatchedSystem(fileOrFolderList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost {
return new TestServerHost(fileOrFolderList, params);
}
export function createServerHost(fileOrFolderList: readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost {
const host = new TestServerHost(/*withSafelist*/ true, fileOrFolderList, params);
export function createServerHost(fileOrFolderList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost {
const host = new TestServerHost(fileOrFolderList, params);
// Just like sys, patch the host to use writeFile
patchWriteFileEnsuringDirectory(host);
return host;
@@ -70,6 +59,9 @@ interface Array<T> { length: number; [n: number]: T; }`
}
export type FileOrFolderOrSymLink = File | Folder | SymLink;
export interface FileOrFolderOrSymLinkMap {
[path: string]: string | Omit<FileOrFolderOrSymLink, "path">;
}
export function isFile(fileOrFolderOrSymLink: FileOrFolderOrSymLink): fileOrFolderOrSymLink is File {
return isString((fileOrFolderOrSymLink as File).content);
}
@@ -195,70 +187,6 @@ interface Array<T> { length: number; [n: number]: T; }`
checkMap(caption, arrayToMap(actual, identity), expected, /*eachKeyCount*/ undefined);
}
export function checkWatchedFiles(host: TestServerHost, expectedFiles: readonly string[], additionalInfo?: string) {
checkMap(`watchedFiles:: ${additionalInfo || ""}::`, host.watchedFiles, expectedFiles, /*eachKeyCount*/ undefined);
}
export interface WatchFileDetails {
fileName: string;
pollingInterval: PollingInterval;
}
export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: ReadonlyESMap<string, number>, expectedDetails?: ESMap<string, WatchFileDetails[]>): void;
export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: readonly string[], eachFileWatchCount: number, expectedDetails?: ESMap<string, WatchFileDetails[]>): void;
export function checkWatchedFilesDetailed(host: TestServerHost, expectedFiles: ReadonlyESMap<string, number> | readonly string[], eachFileWatchCountOrExpectedDetails?: number | ESMap<string, WatchFileDetails[]>, expectedDetails?: ESMap<string, WatchFileDetails[]>) {
if (!isNumber(eachFileWatchCountOrExpectedDetails)) expectedDetails = eachFileWatchCountOrExpectedDetails;
if (isArray(expectedFiles)) {
checkMap(
"watchedFiles",
host.watchedFiles,
expectedFiles,
eachFileWatchCountOrExpectedDetails as number,
[expectedDetails, ({ fileName, pollingInterval }) => ({ fileName, pollingInterval })]
);
}
else {
checkMap(
"watchedFiles",
host.watchedFiles,
expectedFiles,
[expectedDetails, ({ fileName, pollingInterval }) => ({ fileName, pollingInterval })]
);
}
}
export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive: boolean) {
checkMap(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.fsWatchesRecursive : host.fsWatches, expectedDirectories, /*eachKeyCount*/ undefined);
}
export interface WatchDirectoryDetails {
directoryName: string;
fallbackPollingInterval: PollingInterval;
fallbackOptions: WatchOptions | undefined;
}
export function checkWatchedDirectoriesDetailed(host: TestServerHost, expectedDirectories: ReadonlyESMap<string, number>, recursive: boolean, expectedDetails?: ESMap<string, WatchDirectoryDetails[]>): void;
export function checkWatchedDirectoriesDetailed(host: TestServerHost, expectedDirectories: readonly string[], eachDirectoryWatchCount: number, recursive: boolean, expectedDetails?: ESMap<string, WatchDirectoryDetails[]>): void;
export function checkWatchedDirectoriesDetailed(host: TestServerHost, expectedDirectories: ReadonlyESMap<string, number> | readonly string[], recursiveOrEachDirectoryWatchCount: boolean | number, recursiveOrExpectedDetails?: boolean | ESMap<string, WatchDirectoryDetails[]>, expectedDetails?: ESMap<string, WatchDirectoryDetails[]>) {
if (typeof recursiveOrExpectedDetails !== "boolean") expectedDetails = recursiveOrExpectedDetails;
if (isArray(expectedDirectories)) {
checkMap(
`fsWatches${recursiveOrExpectedDetails ? " recursive" : ""}`,
recursiveOrExpectedDetails as boolean ? host.fsWatchesRecursive : host.fsWatches,
expectedDirectories,
recursiveOrEachDirectoryWatchCount as number,
[expectedDetails, ({ directoryName, fallbackPollingInterval, fallbackOptions }) => ({ directoryName, fallbackPollingInterval, fallbackOptions })]
);
}
else {
recursiveOrExpectedDetails = recursiveOrEachDirectoryWatchCount as boolean;
checkMap(
`fsWatches${recursiveOrExpectedDetails ? " recursive" : ""}`,
recursiveOrExpectedDetails ? host.fsWatchesRecursive : host.fsWatches,
expectedDirectories,
[expectedDetails, ({ directoryName, fallbackPollingInterval, fallbackOptions }) => ({ directoryName, fallbackPollingInterval, fallbackOptions })]
);
}
}
export function checkOutputContains(host: TestServerHost, expected: readonly string[]) {
const mapExpected = new Set(expected);
const mapSeen = new Set<string>();
@@ -355,17 +283,22 @@ interface Array<T> { length: number; [n: number]: T; }`
export interface TestFsWatcher {
cb: FsWatchCallback;
directoryName: string;
fallbackPollingInterval: PollingInterval;
fallbackOptions: WatchOptions | undefined;
inode: number | undefined;
}
export interface ReloadWatchInvokeOptions {
export interface WatchInvokeOptions {
/** Invokes the directory watcher for the parent instead of the file changed */
invokeDirectoryWatcherInsteadOfFileChanged: boolean;
/** When new file is created, do not invoke watches for it */
ignoreWatchInvokedWithTriggerAsFileCreate: boolean;
/** Invoke the file delete, followed by create instead of file changed */
invokeFileDeleteCreateAsPartInsteadOfChange: boolean;
/** Dont invoke delete watches */
ignoreDelete: boolean;
/** Skip inode check on file or folder create*/
skipInodeCheckOnCreate: boolean;
/** When invoking rename event on fs watch, send event with file name suffixed with tilde */
useTildeAsSuffixInRenameEventFileName: boolean;
}
export enum Tsc_WatchFile {
@@ -384,7 +317,6 @@ interface Array<T> { length: number; [n: number]: T; }`
useCaseSensitiveFileNames: boolean;
executingFilePath: string;
currentDirectory: string;
fileOrFolderorSymLinkList: readonly FileOrFolderOrSymLink[];
newLine?: string;
useWindowsStylePaths?: boolean;
environmentVariables?: ESMap<string, string>;
@@ -417,14 +349,16 @@ interface Array<T> { length: number; [n: number]: T; }`
public defaultWatchFileKind?: () => WatchFileKind | undefined;
public storeFilesChangingSignatureDuringEmit = true;
watchFile: HostWatchFile;
private inodeWatching: boolean | undefined;
private readonly inodes?: ESMap<Path, number>;
watchDirectory: HostWatchDirectory;
constructor(
public withSafeList: boolean,
fileOrFolderorSymLinkList: readonly FileOrFolderOrSymLink[],
fileOrFolderorSymLinkList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[],
{
useCaseSensitiveFileNames, executingFilePath, currentDirectory,
newLine, windowsStyleRoot, environmentVariables,
runWithoutRecursiveWatches, runWithFallbackPolling
runWithoutRecursiveWatches, runWithFallbackPolling,
inodeWatching,
}: TestServerHostCreationParameters = {}) {
this.useCaseSensitiveFileNames = !!useCaseSensitiveFileNames;
this.newLine = newLine || "\n";
@@ -438,6 +372,11 @@ interface Array<T> { length: number; [n: number]: T; }`
this.runWithFallbackPolling = !!runWithFallbackPolling;
const tscWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE");
const tscWatchDirectory = this.environmentVariables && this.environmentVariables.get("TSC_WATCHDIRECTORY");
if (inodeWatching) {
this.inodeWatching = true;
this.inodes = new Map();
}
const { watchFile, watchDirectory } = createSystemWatchFunctions({
// We dont have polling watch file
// it is essentially fsWatch but lets get that separate from fsWatch and
@@ -451,22 +390,29 @@ interface Array<T> { length: number; [n: number]: T; }`
getModifiedTime: this.getModifiedTime.bind(this),
setTimeout: this.setTimeout.bind(this),
clearTimeout: this.clearTimeout.bind(this),
fsWatch: this.fsWatch.bind(this),
fsWatchWorker: this.fsWatchWorker.bind(this),
fileSystemEntryExists: this.fileSystemEntryExists.bind(this),
useCaseSensitiveFileNames: this.useCaseSensitiveFileNames,
getCurrentDirectory: this.getCurrentDirectory.bind(this),
fsSupportsRecursiveFsWatch: tscWatchDirectory ? false : !runWithoutRecursiveWatches,
directoryExists: this.directoryExists.bind(this),
getAccessibleSortedChildDirectories: path => this.getDirectories(path),
realpath: this.realpath.bind(this),
tscWatchFile,
tscWatchDirectory,
defaultWatchFileKind: () => this.defaultWatchFileKind?.(),
inodeWatching: !!this.inodeWatching,
sysLog: s => this.write(s + this.newLine),
});
this.watchFile = watchFile;
this.watchDirectory = watchDirectory;
this.reloadFS(fileOrFolderorSymLinkList);
}
private nextInode = 0;
private setInode(path: Path) {
if (this.inodes) this.inodes.set(path, this.nextInode++);
}
// Output is pretty
writeOutputIsTTY() {
return true;
@@ -504,53 +450,31 @@ interface Array<T> { length: number; [n: number]: T; }`
this.time = time;
}
private reloadFS(fileOrFolderOrSymLinkList: readonly FileOrFolderOrSymLink[], options?: Partial<ReloadWatchInvokeOptions>) {
private reloadFS(fileOrFolderOrSymLinkList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[]) {
Debug.assert(this.fs.size === 0);
fileOrFolderOrSymLinkList = fileOrFolderOrSymLinkList.concat(this.withSafeList ? safeList : []);
const filesOrFoldersToLoad: readonly FileOrFolderOrSymLink[] = !this.windowsStyleRoot ? fileOrFolderOrSymLinkList :
fileOrFolderOrSymLinkList.map<FileOrFolderOrSymLink>(f => {
const result = clone(f);
result.path = this.getHostSpecificPath(f.path);
return result;
});
for (const fileOrDirectory of filesOrFoldersToLoad) {
const path = this.toFullPath(fileOrDirectory.path);
// If its a change
const currentEntry = this.fs.get(path);
if (currentEntry) {
if (isFsFile(currentEntry)) {
if (isFile(fileOrDirectory)) {
// Update file
if (currentEntry.content !== fileOrDirectory.content) {
this.modifyFile(fileOrDirectory.path, fileOrDirectory.content, options);
}
if (isArray(fileOrFolderOrSymLinkList)) {
fileOrFolderOrSymLinkList.forEach(f => this.ensureFileOrFolder(!this.windowsStyleRoot ?
f :
{ ...f, path: this.getHostSpecificPath(f.path) }
));
}
else {
for (const key in fileOrFolderOrSymLinkList) {
if (hasProperty(fileOrFolderOrSymLinkList, key)) {
const path = this.getHostSpecificPath(key);
const value = fileOrFolderOrSymLinkList[key];
if (isString(value)) {
this.ensureFileOrFolder({ path, content: value });
}
else {
// TODO: Changing from file => folder/Symlink
this.ensureFileOrFolder({ path, ...value });
}
}
else if (isFsSymLink(currentEntry)) {
// TODO: update symlinks
}
else {
// Folder
if (isFile(fileOrDirectory)) {
// TODO: Changing from folder => file
}
else {
// Folder update: Nothing to do.
currentEntry.modifiedTime = this.now();
this.invokeFsWatches(currentEntry.fullPath, "change", currentEntry.modifiedTime);
}
}
}
else {
this.ensureFileOrFolder(fileOrDirectory, options && options.ignoreWatchInvokedWithTriggerAsFileCreate);
}
}
}
modifyFile(filePath: string, content: string, options?: Partial<ReloadWatchInvokeOptions>) {
modifyFile(filePath: string, content: string, options?: Partial<WatchInvokeOptions>) {
const path = this.toFullPath(filePath);
const currentEntry = this.fs.get(path);
if (!currentEntry || !isFsFile(currentEntry)) {
@@ -558,8 +482,8 @@ interface Array<T> { length: number; [n: number]: T; }`
}
if (options && options.invokeFileDeleteCreateAsPartInsteadOfChange) {
this.removeFileOrFolder(currentEntry, returnFalse);
this.ensureFileOrFolder({ path: filePath, content });
this.removeFileOrFolder(currentEntry, /*isRenaming*/ false, options);
this.ensureFileOrFolder({ path: filePath, content }, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ undefined, /*ignoreParentWatch*/ undefined, options);
}
else {
currentEntry.content = content;
@@ -568,11 +492,11 @@ interface Array<T> { length: number; [n: number]: T; }`
if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) {
const directoryFullPath = getDirectoryPath(currentEntry.fullPath);
this.invokeFileWatcher(directoryFullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime, /*useFileNameInCallback*/ true);
this.invokeFsWatchesCallbacks(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath);
this.invokeRecursiveFsWatches(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath);
this.invokeFsWatchesCallbacks(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
this.invokeRecursiveFsWatches(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
}
else {
this.invokeFileAndFsWatches(currentEntry.fullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime);
this.invokeFileAndFsWatches(currentEntry.fullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
}
}
}
@@ -584,7 +508,7 @@ interface Array<T> { length: number; [n: number]: T; }`
Debug.assert(!!file);
// Only remove the file
this.removeFileOrFolder(file, returnFalse, /*isRenaming*/ true);
this.removeFileOrFolder(file, /*isRenaming*/ true);
// Add updated folder with new folder name
const newFullPath = getNormalizedAbsolutePath(newFileName, this.currentDirectory);
@@ -604,7 +528,7 @@ interface Array<T> { length: number; [n: number]: T; }`
Debug.assert(!!folder);
// Only remove the folder
this.removeFileOrFolder(folder, returnFalse, /*isRenaming*/ true);
this.removeFileOrFolder(folder, /*isRenaming*/ true);
// Add updated folder with new folder name
const newFullPath = getNormalizedAbsolutePath(newFolderName, this.currentDirectory);
@@ -631,6 +555,7 @@ interface Array<T> { length: number; [n: number]: T; }`
newFolder.entries.push(entry);
}
this.fs.set(entry.path, entry);
this.setInode(entry.path);
this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Created);
if (isFsFolder(entry)) {
this.renameFolderEntries(entry, entry);
@@ -638,29 +563,29 @@ interface Array<T> { length: number; [n: number]: T; }`
}
}
ensureFileOrFolder(fileOrDirectoryOrSymLink: FileOrFolderOrSymLink, ignoreWatchInvokedWithTriggerAsFileCreate?: boolean, ignoreParentWatch?: boolean) {
ensureFileOrFolder(fileOrDirectoryOrSymLink: FileOrFolderOrSymLink, ignoreWatchInvokedWithTriggerAsFileCreate?: boolean, ignoreParentWatch?: boolean, options?: Partial<WatchInvokeOptions>) {
if (isFile(fileOrDirectoryOrSymLink)) {
const file = this.toFsFile(fileOrDirectoryOrSymLink);
// file may already exist when updating existing type declaration file
if (!this.fs.get(file.path)) {
const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath), ignoreParentWatch);
this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate);
const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath), ignoreParentWatch, options);
this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate, options);
}
}
else if (isSymLink(fileOrDirectoryOrSymLink)) {
const symLink = this.toFsSymLink(fileOrDirectoryOrSymLink);
Debug.assert(!this.fs.get(symLink.path));
const baseFolder = this.ensureFolder(getDirectoryPath(symLink.fullPath), ignoreParentWatch);
this.addFileOrFolderInFolder(baseFolder, symLink, ignoreWatchInvokedWithTriggerAsFileCreate);
const baseFolder = this.ensureFolder(getDirectoryPath(symLink.fullPath), ignoreParentWatch, options);
this.addFileOrFolderInFolder(baseFolder, symLink, ignoreWatchInvokedWithTriggerAsFileCreate, options);
}
else {
const fullPath = getNormalizedAbsolutePath(fileOrDirectoryOrSymLink.path, this.currentDirectory);
this.ensureFolder(getDirectoryPath(fullPath), ignoreParentWatch);
this.ensureFolder(fullPath, ignoreWatchInvokedWithTriggerAsFileCreate);
this.ensureFolder(getDirectoryPath(fullPath), ignoreParentWatch, options);
this.ensureFolder(fullPath, ignoreWatchInvokedWithTriggerAsFileCreate, options);
}
}
private ensureFolder(fullPath: string, ignoreWatch: boolean | undefined): FsFolder {
private ensureFolder(fullPath: string, ignoreWatch: boolean | undefined, options: Partial<WatchInvokeOptions> | undefined): FsFolder {
const path = this.toPath(fullPath);
let folder = this.fs.get(path) as FsFolder;
if (!folder) {
@@ -668,34 +593,39 @@ interface Array<T> { length: number; [n: number]: T; }`
const baseFullPath = getDirectoryPath(fullPath);
if (fullPath !== baseFullPath) {
// Add folder in the base folder
const baseFolder = this.ensureFolder(baseFullPath, ignoreWatch);
this.addFileOrFolderInFolder(baseFolder, folder, ignoreWatch);
const baseFolder = this.ensureFolder(baseFullPath, ignoreWatch, options);
this.addFileOrFolderInFolder(baseFolder, folder, ignoreWatch, options);
}
else {
// root folder
Debug.assert(this.fs.size === 0 || !!this.windowsStyleRoot);
this.fs.set(path, folder);
this.setInode(path);
}
}
Debug.assert(isFsFolder(folder));
return folder;
}
private addFileOrFolderInFolder(folder: FsFolder, fileOrDirectory: FsFile | FsFolder | FsSymLink, ignoreWatch?: boolean) {
private addFileOrFolderInFolder(folder: FsFolder, fileOrDirectory: FsFile | FsFolder | FsSymLink, ignoreWatch?: boolean, options?: Partial<WatchInvokeOptions>) {
if (!this.fs.has(fileOrDirectory.path)) {
insertSorted(folder.entries, fileOrDirectory, (a, b) => compareStringsCaseSensitive(getBaseFileName(a.path), getBaseFileName(b.path)));
}
folder.modifiedTime = this.now();
this.fs.set(fileOrDirectory.path, fileOrDirectory);
this.setInode(fileOrDirectory.path);
if (ignoreWatch) {
return;
}
this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Created, fileOrDirectory.modifiedTime);
this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed, folder.modifiedTime);
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.inodeWatching = inodeWatching;
}
private removeFileOrFolder(fileOrDirectory: FsFile | FsFolder | FsSymLink, isRemovableLeafFolder: (folder: FsFolder) => boolean, isRenaming = false) {
private removeFileOrFolder(fileOrDirectory: FsFile | FsFolder | FsSymLink, isRenaming?: boolean, options?: Partial<WatchInvokeOptions>) {
const basePath = getDirectoryPath(fileOrDirectory.path);
const baseFolder = this.fs.get(basePath) as FsFolder;
if (basePath !== fileOrDirectory.path) {
@@ -708,20 +638,16 @@ interface Array<T> { length: number; [n: number]: T; }`
if (isFsFolder(fileOrDirectory)) {
Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming);
}
this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted);
this.invokeFileAndFsWatches(baseFolder.fullPath, FileWatcherEventKind.Changed, baseFolder.modifiedTime);
if (basePath !== fileOrDirectory.path &&
baseFolder.entries.length === 0 &&
isRemovableLeafFolder(baseFolder)) {
this.removeFileOrFolder(baseFolder, isRemovableLeafFolder);
}
if (!options?.ignoreDelete) this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted, /*modifiedTime*/ undefined, options?.useTildeAsSuffixInRenameEventFileName);
this.inodes?.delete(fileOrDirectory.path);
if (!options?.ignoreDelete) this.invokeFileAndFsWatches(baseFolder.fullPath, FileWatcherEventKind.Changed, baseFolder.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
}
deleteFile(filePath: string) {
const path = this.toFullPath(filePath);
const currentEntry = this.fs.get(path) as FsFile;
Debug.assert(isFsFile(currentEntry));
this.removeFileOrFolder(currentEntry, returnFalse);
this.removeFileOrFolder(currentEntry);
}
deleteFolder(folderPath: string, recursive?: boolean) {
@@ -735,11 +661,11 @@ interface Array<T> { length: number; [n: number]: T; }`
this.deleteFolder(fsEntry.fullPath, recursive);
}
else {
this.removeFileOrFolder(fsEntry, returnFalse);
this.removeFileOrFolder(fsEntry);
}
});
}
this.removeFileOrFolder(currentEntry, returnFalse);
this.removeFileOrFolder(currentEntry);
}
private watchFileWorker(fileName: string, cb: FileWatcherCallback, pollingInterval: PollingInterval) {
@@ -750,69 +676,73 @@ interface Array<T> { length: number; [n: number]: T; }`
);
}
private fsWatch(
private fsWatchWorker(
fileOrDirectory: string,
_entryKind: FileSystemEntryKind,
cb: FsWatchCallback,
recursive: boolean,
fallbackPollingInterval: PollingInterval,
fallbackOptions: WatchOptions | undefined): FileWatcher {
return this.runWithFallbackPolling ?
this.watchFile(
fileOrDirectory,
createFileWatcherCallback(cb),
fallbackPollingInterval,
fallbackOptions
) :
createWatcher(
recursive ? this.fsWatchesRecursive : this.fsWatches,
this.toFullPath(fileOrDirectory),
{
directoryName: fileOrDirectory,
cb,
fallbackPollingInterval,
fallbackOptions
}
);
cb: FsWatchCallback,
) {
if (this.runWithFallbackPolling) throw new Error("Need to use fallback polling instead of file system native watching");
const path = this.toFullPath(fileOrDirectory);
// Error if the path does not exist
if (this.inodeWatching && !this.inodes?.has(path)) throw new Error();
const result = createWatcher(
recursive ? this.fsWatchesRecursive : this.fsWatches,
path,
{
directoryName: fileOrDirectory,
cb,
inode: this.inodes?.get(path)
}
) as FsWatchWorkerWatcher;
result.on = noop;
return result;
}
invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind, modifiedTime?: Date, useFileNameInCallback?: boolean) {
invokeWatcherCallbacks(this.watchedFiles.get(this.toPath(fileFullPath)), ({ cb, fileName }) => cb(useFileNameInCallback ? fileName : fileFullPath, eventKind, modifiedTime));
}
private fsWatchCallback(map: MultiMap<Path, TestFsWatcher>, fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string) {
invokeWatcherCallbacks(map.get(this.toPath(fullPath)), ({ cb }) => cb(eventName, entryFullPath ? this.getRelativePathToDirectory(fullPath, entryFullPath) : "", modifiedTime));
private fsWatchCallback(map: MultiMap<Path, TestFsWatcher>, fullPath: string, eventName: "rename" | "change", modifiedTime: Date | undefined, entryFullPath: string | undefined, useTildeSuffix: boolean | undefined) {
const path = this.toPath(fullPath);
const currentInode = this.inodes?.get(path);
invokeWatcherCallbacks(map.get(path), ({ cb, inode }) => {
// TODO::
if (this.inodeWatching && inode !== undefined && inode !== currentInode) return;
let relativeFileName = (entryFullPath ? this.getRelativePathToDirectory(fullPath, entryFullPath) : "");
if (useTildeSuffix) relativeFileName = (relativeFileName ? relativeFileName : getBaseFileName(fullPath)) + "~";
cb(eventName, relativeFileName, modifiedTime);
});
}
invokeFsWatchesCallbacks(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string) {
this.fsWatchCallback(this.fsWatches, fullPath, eventName, modifiedTime, entryFullPath);
invokeFsWatchesCallbacks(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string, useTildeSuffix?: boolean) {
this.fsWatchCallback(this.fsWatches, fullPath, eventName, modifiedTime, entryFullPath, useTildeSuffix);
}
invokeFsWatchesRecursiveCallbacks(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string) {
this.fsWatchCallback(this.fsWatchesRecursive, fullPath, eventName, modifiedTime, entryFullPath);
invokeFsWatchesRecursiveCallbacks(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string, useTildeSuffix?: boolean) {
this.fsWatchCallback(this.fsWatchesRecursive, fullPath, eventName, modifiedTime, entryFullPath, useTildeSuffix);
}
private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) {
return getRelativePathToDirectoryOrUrl(directoryFullPath, fileFullPath, this.currentDirectory, this.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
}
private invokeRecursiveFsWatches(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string) {
this.invokeFsWatchesRecursiveCallbacks(fullPath, eventName, modifiedTime, entryFullPath);
private invokeRecursiveFsWatches(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string, useTildeSuffix?: boolean) {
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);
this.invokeRecursiveFsWatches(basePath, eventName, modifiedTime, entryFullPath || fullPath, useTildeSuffix);
}
}
private invokeFsWatches(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date) {
this.invokeFsWatchesCallbacks(fullPath, eventName, modifiedTime);
this.invokeFsWatchesCallbacks(getDirectoryPath(fullPath), eventName, modifiedTime, fullPath);
this.invokeRecursiveFsWatches(fullPath, eventName, modifiedTime);
private 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.invokeRecursiveFsWatches(fullPath, eventName, modifiedTime, /*entryFullPath*/ undefined, useTildeSuffix);
}
private invokeFileAndFsWatches(fileOrFolderFullPath: string, eventKind: FileWatcherEventKind, modifiedTime?: Date) {
private invokeFileAndFsWatches(fileOrFolderFullPath: string, eventKind: FileWatcherEventKind, modifiedTime?: Date, useTildeSuffix?: boolean) {
this.invokeFileWatcher(fileOrFolderFullPath, eventKind, modifiedTime);
this.invokeFsWatches(fileOrFolderFullPath, eventKind === FileWatcherEventKind.Changed ? "change" : "rename", modifiedTime);
this.invokeFsWatches(fileOrFolderFullPath, eventKind === FileWatcherEventKind.Changed ? "change" : "rename", modifiedTime, useTildeSuffix);
}
private toFsEntry(path: string): FSEntryBase {
@@ -881,6 +811,10 @@ interface Array<T> { length: number; [n: number]: T; }`
return this.getRealFsEntry(isFsFolder, path, fsEntry);
}
fileSystemEntryExists(s: string, entryKind: FileSystemEntryKind) {
return entryKind === FileSystemEntryKind.File ? this.fileExists(s) : this.directoryExists(s);
}
fileExists(s: string) {
const path = this.toFullPath(s);
return !!this.getRealFile(path);
@@ -1046,11 +980,11 @@ interface Array<T> { length: number; [n: number]: T; }`
}
}
prependFile(path: string, content: string, options?: Partial<ReloadWatchInvokeOptions>): void {
prependFile(path: string, content: string, options?: Partial<WatchInvokeOptions>): void {
this.modifyFile(path, content + this.readFile(path), options);
}
appendFile(path: string, content: string, options?: Partial<ReloadWatchInvokeOptions>): void {
appendFile(path: string, content: string, options?: Partial<WatchInvokeOptions>): void {
this.modifyFile(path, this.readFile(path) + content, options);
}
@@ -1095,14 +1029,14 @@ interface Array<T> { length: number; [n: number]: T; }`
}
writtenFiles?: ESMap<Path, number>;
diff(baseline: string[], base: ESMap<string, FSEntry> = new Map()) {
this.fs.forEach(newFsEntry => {
diffFsEntry(baseline, base.get(newFsEntry.path), newFsEntry, this.writtenFiles);
diff(baseline: string[], base: ESMap<Path, FSEntry> = new Map()) {
this.fs.forEach((newFsEntry, path) => {
diffFsEntry(baseline, base.get(path), newFsEntry, this.inodes?.get(path), this.writtenFiles);
});
base.forEach(oldFsEntry => {
const newFsEntry = this.fs.get(oldFsEntry.path);
base.forEach((oldFsEntry, path) => {
const newFsEntry = this.fs.get(path);
if (!newFsEntry) {
diffFsEntry(baseline, oldFsEntry, newFsEntry, this.writtenFiles);
diffFsEntry(baseline, oldFsEntry, newFsEntry, this.inodes?.get(path), this.writtenFiles);
}
});
baseline.push("");
@@ -1150,86 +1084,77 @@ interface Array<T> { length: number; [n: number]: T; }`
}
}
function diffFsFile(baseline: string[], fsEntry: FsFile) {
baseline.push(`//// [${fsEntry.fullPath}]\r\n${fsEntry.content}`, "");
function diffFsFile(baseline: string[], fsEntry: FsFile, newInode: number | undefined) {
baseline.push(`//// [${fsEntry.fullPath}]${inodeString(newInode)}\r\n${fsEntry.content}`, "");
}
function diffFsSymLink(baseline: string[], fsEntry: FsSymLink) {
baseline.push(`//// [${fsEntry.fullPath}] symlink(${fsEntry.symLink})`);
function diffFsSymLink(baseline: string[], fsEntry: FsSymLink, newInode: number | undefined) {
baseline.push(`//// [${fsEntry.fullPath}] symlink(${fsEntry.symLink})${inodeString(newInode)}`);
}
function diffFsEntry(baseline: string[], oldFsEntry: FSEntry | undefined, newFsEntry: FSEntry | undefined, writtenFiles: ESMap<string, any> | undefined): void {
function inodeString(inode: number | undefined) {
return inode !== undefined ? ` Inode:: ${inode}` : "";
}
function diffFsEntry(baseline: string[], oldFsEntry: FSEntry | undefined, newFsEntry: FSEntry | undefined, newInode: number | undefined, writtenFiles: ESMap<string, any> | undefined): void {
const file = newFsEntry && newFsEntry.fullPath;
if (isFsFile(oldFsEntry)) {
if (isFsFile(newFsEntry)) {
if (oldFsEntry.content !== newFsEntry.content) {
diffFsFile(baseline, newFsEntry);
diffFsFile(baseline, newFsEntry, newInode);
}
else if (oldFsEntry.modifiedTime !== newFsEntry.modifiedTime) {
if (oldFsEntry.fullPath !== newFsEntry.fullPath) {
baseline.push(`//// [${file}] file was renamed from file ${oldFsEntry.fullPath}`);
baseline.push(`//// [${file}] file was renamed from file ${oldFsEntry.fullPath}${inodeString(newInode)}`);
}
else if (writtenFiles && !writtenFiles.has(newFsEntry.path)) {
baseline.push(`//// [${file}] file changed its modified time`);
baseline.push(`//// [${file}] file changed its modified time${inodeString(newInode)}`);
}
else {
baseline.push(`//// [${file}] file written with same contents`);
baseline.push(`//// [${file}] file written with same contents${inodeString(newInode)}`);
}
}
}
else {
baseline.push(`//// [${oldFsEntry.fullPath}] deleted`);
if (isFsSymLink(newFsEntry)) {
diffFsSymLink(baseline, newFsEntry);
diffFsSymLink(baseline, newFsEntry, newInode);
}
}
}
else if (isFsSymLink(oldFsEntry)) {
if (isFsSymLink(newFsEntry)) {
if (oldFsEntry.symLink !== newFsEntry.symLink) {
diffFsSymLink(baseline, newFsEntry);
diffFsSymLink(baseline, newFsEntry, newInode);
}
else if (oldFsEntry.modifiedTime !== newFsEntry.modifiedTime) {
if (oldFsEntry.fullPath !== newFsEntry.fullPath) {
baseline.push(`//// [${file}] symlink was renamed from symlink ${oldFsEntry.fullPath}`);
baseline.push(`//// [${file}] symlink was renamed from symlink ${oldFsEntry.fullPath}${inodeString(newInode)}`);
}
else if (writtenFiles && !writtenFiles.has(newFsEntry.path)) {
baseline.push(`//// [${file}] symlink changed its modified time`);
baseline.push(`//// [${file}] symlink changed its modified time${inodeString(newInode)}`);
}
else {
baseline.push(`//// [${file}] symlink written with same link`);
baseline.push(`//// [${file}] symlink written with same link${inodeString(newInode)}`);
}
}
}
else {
baseline.push(`//// [${oldFsEntry.fullPath}] deleted symlink`);
if (isFsFile(newFsEntry)) {
diffFsFile(baseline, newFsEntry);
diffFsFile(baseline, newFsEntry, newInode);
}
}
}
else if (isFsFile(newFsEntry)) {
diffFsFile(baseline, newFsEntry);
diffFsFile(baseline, newFsEntry, newInode);
}
else if (isFsSymLink(newFsEntry)) {
diffFsSymLink(baseline, newFsEntry);
diffFsSymLink(baseline, newFsEntry, newInode);
}
}
function serializeTestFsWatcher({ directoryName, fallbackPollingInterval, fallbackOptions }: TestFsWatcher) {
function serializeTestFsWatcher({ directoryName, inode }: TestFsWatcher) {
return {
directoryName,
fallbackPollingInterval,
fallbackOptions: serializeWatchOptions(fallbackOptions)
};
}
function serializeWatchOptions(fallbackOptions: WatchOptions | undefined) {
if (!fallbackOptions) return undefined;
const { watchFile, watchDirectory, fallbackPolling, ...rest } = fallbackOptions;
return {
watchFile: watchFile !== undefined ? WatchFileKind[watchFile] : undefined,
watchDirectory: watchDirectory !== undefined ? WatchDirectoryKind[watchDirectory] : undefined,
fallbackPolling: fallbackPolling !== undefined ? PollingWatchKind[fallbackPolling] : undefined,
...rest
inode,
};
}