From 787c995985b31c995e4249cc0790db592665f649 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 16 Jan 2018 16:35:34 -0800 Subject: [PATCH] Allow recursive directory watching on non supported file system --- src/compiler/core.ts | 38 +++++ src/compiler/sys.ts | 161 +++++++++++++++++++--- src/compiler/utilities.ts | 1 - src/compiler/watchUtilities.ts | 4 - src/harness/unittests/tscWatchMode.ts | 2 +- src/harness/virtualFileSystemWithWatch.ts | 2 +- src/server/project.ts | 5 +- src/server/utilities.ts | 30 ---- 8 files changed, 183 insertions(+), 60 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 75564779280..2df3b46fdb2 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -20,6 +20,12 @@ namespace ts { /* @internal */ namespace ts { + export const emptyArray: never[] = [] as never[]; + + export function closeFileWatcher(watcher: FileWatcher) { + watcher.close(); + } + /** Create a MapLike with good performance. */ function createDictionaryObject(): MapLike { const map = Object.create(/*prototype*/ null); // tslint:disable-line:no-null-keyword @@ -3270,4 +3276,36 @@ namespace ts { export function singleElementArray(t: T | undefined): T[] | undefined { return t === undefined ? undefined : [t]; } + + export function enumerateInsertsAndDeletes(newItems: ReadonlyArray, oldItems: ReadonlyArray, comparer: (a: T, b: U) => Comparison, inserted: (newItem: T) => void, deleted: (oldItem: U) => void, unchanged?: (oldItem: U, newItem: T) => void) { + unchanged = unchanged || noop; + let newIndex = 0; + let oldIndex = 0; + const newLen = newItems.length; + const oldLen = oldItems.length; + while (newIndex < newLen && oldIndex < oldLen) { + const newItem = newItems[newIndex]; + const oldItem = oldItems[oldIndex]; + const compareResult = comparer(newItem, oldItem); + if (compareResult === Comparison.LessThan) { + inserted(newItem); + newIndex++; + } + else if (compareResult === Comparison.GreaterThan) { + deleted(oldItem); + oldIndex++; + } + else { + unchanged(oldItem, newItem); + newIndex++; + oldIndex++; + } + } + while (newIndex < newLen) { + inserted(newItems[newIndex++]); + } + while (oldIndex < oldLen) { + deleted(oldItems[oldIndex++]); + } + } } diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 640a9f86259..c7f378e7478 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -57,6 +57,8 @@ namespace ts { /* @internal */ export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval) => FileWatcher; + /* @internal */ + export type HostWatchDirectory = (fileName: string, callback: DirectoryWatcherCallback, recursive?: boolean) => FileWatcher; /* @internal */ export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time @@ -286,6 +288,93 @@ namespace ts { return false; } + /*@internal*/ + export interface RecursiveDirectoryWatcherHost { + watchDirectory: HostWatchDirectory; + getAccessileSortedChildDirectories(path: string): ReadonlyArray; + filePathComparer: Comparer; + } + + /** + * Watch the directory recursively using host provided method to watch child directories + * that means if this is recursive watcher, watch the children directories as well + * (eg on OS that dont support recursive watch using fs.watch use fs.watchFile) + */ + /*@internal*/ + export function createRecursiveDirectoryWatcher(host: RecursiveDirectoryWatcherHost): (directoryName: string, callback: DirectoryWatcherCallback) => FileWatcher { + type ChildWatches = ReadonlyArray; + interface DirectoryWatcher extends FileWatcher { + childWatches: ChildWatches; + dirName: string; + } + + return createDirectoryWatcher; + + /** + * Create the directory watcher for the dirPath. + */ + function createDirectoryWatcher(dirName: string, callback: DirectoryWatcherCallback): DirectoryWatcher { + const watcher = host.watchDirectory(dirName, fileName => { + // Call the actual callback + callback(fileName); + + // Iterate through existing children and update the watches if needed + updateChildWatches(result, callback); + }); + + let result: DirectoryWatcher = { + close: () => { + watcher.close(); + result.childWatches.forEach(closeFileWatcher); + result = undefined; + }, + dirName, + childWatches: emptyArray + }; + updateChildWatches(result, callback); + return result; + } + + function updateChildWatches(watcher: DirectoryWatcher, callback: DirectoryWatcherCallback) { + // Iterate through existing children and update the watches if needed + if (watcher) { + watcher.childWatches = watchChildDirectories(watcher.dirName, watcher.childWatches, callback); + } + } + + /** + * Watch the directories in the parentDir + */ + function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, callback: DirectoryWatcherCallback): ChildWatches { + let newChildWatches: DirectoryWatcher[] | undefined; + enumerateInsertsAndDeletes( + host.getAccessileSortedChildDirectories(parentDir), + existingChildWatches, + (child, childWatcher) => host.filePathComparer(getNormalizedAbsolutePath(child, parentDir), childWatcher.dirName), + createAndAddChildDirectoryWatcher, + closeFileWatcher, + addChildDirectoryWatcher + ); + + return newChildWatches || emptyArray; + + /** + * Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list + */ + function createAndAddChildDirectoryWatcher(childName: string) { + const result = createDirectoryWatcher(getNormalizedAbsolutePath(childName, parentDir), callback); + addChildDirectoryWatcher(result); + } + + /** + * Add child directory watcher to the new ChildDirectoryWatcher list + */ + function addChildDirectoryWatcher(childWatcher: DirectoryWatcher) { + (newChildWatches || (newChildWatches = [])).push(childWatcher); + } + } + } + /** * Partial interface of the System thats needed to support the caching of directory structure */ @@ -402,7 +491,8 @@ namespace ts { } const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER; - const tscWatchOption = process.env.TSC_WATCHOPTION; + const tscWatchFile = process.env.TSC_WATCHFILE; + const tscWatchDirectory = process.env.TSC_WATCHDIRECTORY; const nodeSystem: System = { args: process.argv.slice(2), @@ -483,19 +573,7 @@ namespace ts { } }; nodeSystem.watchFile = getWatchFile(); - nodeSystem.watchDirectory = (directoryName, callback, recursive) => { - // 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 fsWatchDirectory(directoryName, (eventName, relativeFileName) => { - // In watchDirectory we only care about adding and removing files (when event name is - // "rename"); changes made within files are handled by corresponding fileWatchers (when - // event name is "change") - if (eventName === "rename") { - // When deleting a file, the passed baseFileName is null - callback(!relativeFileName ? relativeFileName : normalizePath(combinePaths(directoryName, relativeFileName))); - } - }, recursive); - }; + nodeSystem.watchDirectory = getWatchDirectory(); return nodeSystem; function isFileSystemCaseSensitive(): boolean { @@ -516,7 +594,7 @@ namespace ts { } function getWatchFile(): HostWatchFile { - switch (tscWatchOption) { + switch (tscWatchFile) { case "PriorityPollingInterval": // Use polling interval based on priority when create watch using host.watchFile return fsWatchFile; @@ -536,6 +614,29 @@ namespace ts { (fileName, callback) => fsWatchFile(fileName, callback); } + function getWatchDirectory(): HostWatchDirectory { + // 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) + const fsSupportsRecursive = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin"); + if (fsSupportsRecursive) { + return watchDirectoryUsingFsWatch; + } + + const watchDirectory = tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile" ? watchDirectoryUsingFsWatchFile : watchDirectoryUsingFsWatch; + const watchDirectoryRecursively = createRecursiveDirectoryWatcher({ + filePathComparer: useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, + getAccessileSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories, + watchDirectory + }); + + return (directoryName, callback, recursive) => { + if (recursive) { + return watchDirectoryRecursively(directoryName, callback); + } + watchDirectory(directoryName, callback); + }; + } + function createNonPollingWatchFile() { // One file can have multiple watchers const fileWatcherCallbacks = createMultiMap(); @@ -616,11 +717,11 @@ namespace ts { type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string) => void; - function createFsWatchFileCallback(callback: FsWatchCallback): FileWatcherCallback { + function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback { return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", ""); } - function createFsWatchCallback(fileName: string, callback: FileWatcherCallback): FsWatchCallback { + function createFsWatchCallbackForFileWatcherCallback(fileName: string, callback: FileWatcherCallback): FsWatchCallback { return eventName => { if (eventName === "rename") { callback(fileName, fileExists(fileName) ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted); @@ -632,6 +733,18 @@ namespace ts { }; } + function createFsWatchCallbackForDirectoryWatcherCallback(directoryName: string, callback: DirectoryWatcherCallback): FsWatchCallback { + return (eventName, relativeFileName) => { + // In watchDirectory we only care about adding and removing files (when event name is + // "rename"); changes made within files are handled by corresponding fileWatchers (when + // event name is "change") + if (eventName === "rename") { + // When deleting a file, the passed baseFileName is null + callback(!relativeFileName ? directoryName : normalizePath(combinePaths(directoryName, relativeFileName))); + } + }; + } + function fsWatch(fileOrDirectory: string, entryKind: FileSystemEntryKind.File | FileSystemEntryKind.Directory, callback: FsWatchCallback, recursive: boolean, fallbackPollingWatchFile: HostWatchFile, pollingInterval?: number): FileWatcher { let options: any; /** Watcher for the file system entry depending on whether it is missing or present */ @@ -700,7 +813,7 @@ namespace ts { * 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 fallbackPollingWatchFile(fileOrDirectory, createFsWatchFileCallback(callback), pollingInterval); + return fallbackPollingWatchFile(fileOrDirectory, createFileWatcherCallback(callback), pollingInterval); } /** @@ -720,18 +833,26 @@ namespace ts { } function watchFileUsingFsWatch(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) { - return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallback(fileName, callback), /*recursive*/ false, fsWatchFile, pollingInterval); + return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, fsWatchFile, pollingInterval); } function watchFileUsingDynamicWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) { const watchFile = createDynamicPriorityPollingWatchFile(nodeSystem); - return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval); + return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval); } function fsWatchDirectory(directoryName: string, callback: FsWatchCallback, recursive?: boolean): FileWatcher { return fsWatch(directoryName, FileSystemEntryKind.Directory, callback, !!recursive, fsWatchFile); } + function watchDirectoryUsingFsWatch(directoryName: string, callback: DirectoryWatcherCallback, recursive?: boolean) { + return fsWatchDirectory(directoryName, createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback), recursive); + } + + function watchDirectoryUsingFsWatchFile(directoryName: string, callback: DirectoryWatcherCallback) { + return fsWatchFile(directoryName, () => callback(directoryName), PollingInterval.Medium); + } + function readFile(fileName: string, _encoding?: string): string | undefined { if (!fileExists(fileName)) { return undefined; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index ffcdf42c67e..75083e159c5 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2,7 +2,6 @@ /* @internal */ namespace ts { - export const emptyArray: never[] = [] as never[]; export const resolvingEmptyArray: never[] = [] as never[]; export const emptyMap: ReadonlyMap = createMap(); export const emptyUnderscoreEscapedMap: ReadonlyUnderscoreEscapedMap = emptyMap as ReadonlyUnderscoreEscapedMap; diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 75298cc87c0..56dd68bb25e 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -181,10 +181,6 @@ namespace ts { return `WatchInfo: ${file} ${flags} ${getDetailWatchInfo ? getDetailWatchInfo(detailInfo1, detailInfo2) : ""}`; } - export function closeFileWatcher(watcher: FileWatcher) { - watcher.close(); - } - export function closeFileWatcherOf(objWithWatcher: T) { objWithWatcher.watcher.close(); } diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index ffdf02c4eb1..81f4d2ed19e 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -2123,7 +2123,7 @@ declare module "fs" { }; const files = [file1, libFile]; const environmentVariables = createMap(); - environmentVariables.set("TSC_WATCHOPTION", "DynamicPriorityPolling"); + environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling"); const host = createWatchedSystem(files, { environmentVariables }); const watch = createWatchModeWithoutConfigFile([file1.path], host); diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 7f732a9c672..6c9855fde0e 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -270,7 +270,7 @@ interface Array {}` this.executingFilePath = this.getHostSpecificPath(executingFilePath); this.currentDirectory = this.getHostSpecificPath(currentDirectory); this.reloadFS(fileOrFolderList); - this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHOPTION") === "DynamicPriorityPolling" ? + this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE") === "DynamicPriorityPolling" ? createDynamicPriorityPollingWatchFile(this) : undefined; } diff --git a/src/server/project.ts b/src/server/project.ts index f1c4dd6af85..c7bff91ff7c 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -891,7 +891,7 @@ namespace ts.server { const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; this.externalFiles = this.getExternalFiles(); - enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles, + enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles, compareStringsCaseSensitive, // Ensure a ScriptInfo is created for new external files. This is performed indirectly // by the LSHost for files in the program when the program is retrieved above but // the program doesn't contain external files so this must be done explicitly. @@ -899,8 +899,7 @@ namespace ts.server { const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.currentDirectory, this.directoryStructureHost); scriptInfo.attachToProject(this); }, - removed => this.detachScriptInfoFromProject(removed), - compareStringsCaseSensitive + removed => this.detachScriptInfoFromProject(removed) ); const elapsed = timestamp() - start; this.writeLog(`Finishing updateGraphWorker: Project: ${this.getProjectName()} structureChanged: ${hasChanges} Elapsed: ${elapsed}ms`); diff --git a/src/server/utilities.ts b/src/server/utilities.ts index c44419f8cf3..262489dad5b 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -276,36 +276,6 @@ namespace ts.server { return index === 0 || value !== array[index - 1]; } - export function enumerateInsertsAndDeletes(newItems: SortedReadonlyArray, oldItems: SortedReadonlyArray, inserted: (newItem: T) => void, deleted: (oldItem: T) => void, comparer: Comparer) { - let newIndex = 0; - let oldIndex = 0; - const newLen = newItems.length; - const oldLen = oldItems.length; - while (newIndex < newLen && oldIndex < oldLen) { - const newItem = newItems[newIndex]; - const oldItem = oldItems[oldIndex]; - const compareResult = comparer(newItem, oldItem); - if (compareResult === Comparison.LessThan) { - inserted(newItem); - newIndex++; - } - else if (compareResult === Comparison.GreaterThan) { - deleted(oldItem); - oldIndex++; - } - else { - newIndex++; - oldIndex++; - } - } - while (newIndex < newLen) { - inserted(newItems[newIndex++]); - } - while (oldIndex < oldLen) { - deleted(oldItems[oldIndex++]); - } - } - /* @internal */ export function indent(str: string): string { return "\n " + str;