diff --git a/.travis.yml b/.travis.yml index 08bd7817c79f8..5d6663ad6d6e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ branches: only: - master - release-2.1 + - release-2.2 install: - npm uninstall typescript diff --git a/Jakefile.js b/Jakefile.js index 1aba38fddb695..4ef576f6675e6 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -15,6 +15,7 @@ var servicesDirectory = "src/services/"; var serverDirectory = "src/server/"; var typingsInstallerDirectory = "src/server/typingsInstaller"; var cancellationTokenDirectory = "src/server/cancellationToken"; +var watchGuardDirectory = "src/server/watchGuard"; var harnessDirectory = "src/harness/"; var libraryDirectory = "src/lib/"; var scriptsDirectory = "scripts/"; @@ -80,6 +81,7 @@ var compilerSources = filesFromConfig("./src/compiler/tsconfig.json"); var servicesSources = filesFromConfig("./src/services/tsconfig.json"); var cancellationTokenSources = filesFromConfig(path.join(serverDirectory, "cancellationToken/tsconfig.json")); var typingsInstallerSources = filesFromConfig(path.join(serverDirectory, "typingsInstaller/tsconfig.json")); +var watchGuardSources = filesFromConfig(path.join(serverDirectory, "watchGuard/tsconfig.json")); var serverSources = filesFromConfig(path.join(serverDirectory, "tsconfig.json")) var languageServiceLibrarySources = filesFromConfig(path.join(serverDirectory, "tsconfig.library.json")); @@ -570,8 +572,11 @@ compileFile(cancellationTokenFile, cancellationTokenSources, [builtLocalDirector var typingsInstallerFile = path.join(builtLocalDirectory, "typingsInstaller.js"); compileFile(typingsInstallerFile, typingsInstallerSources, [builtLocalDirectory].concat(typingsInstallerSources), /*prefixes*/ [copyright], /*useBuiltCompiler*/ true, { outDir: builtLocalDirectory, noOutFile: false }); +var watchGuardFile = path.join(builtLocalDirectory, "watchGuard.js"); +compileFile(watchGuardFile, watchGuardSources, [builtLocalDirectory].concat(watchGuardSources), /*prefixes*/ [copyright], /*useBuiltCompiler*/ true, { outDir: builtLocalDirectory, noOutFile: false }); + var serverFile = path.join(builtLocalDirectory, "tsserver.js"); -compileFile(serverFile, serverSources, [builtLocalDirectory, copyright, cancellationTokenFile, typingsInstallerFile].concat(serverSources), /*prefixes*/ [copyright], /*useBuiltCompiler*/ true, { types: ["node"], preserveConstEnums: true }); +compileFile(serverFile, serverSources, [builtLocalDirectory, copyright, cancellationTokenFile, typingsInstallerFile, watchGuardFile].concat(serverSources), /*prefixes*/ [copyright], /*useBuiltCompiler*/ true, { types: ["node"], preserveConstEnums: true }); var tsserverLibraryFile = path.join(builtLocalDirectory, "tsserverlibrary.js"); var tsserverLibraryDefinitionFile = path.join(builtLocalDirectory, "tsserverlibrary.d.ts"); compileFile( @@ -665,7 +670,7 @@ task("generate-spec", [specMd]); // Makes a new LKG. This target does not build anything, but errors if not all the outputs are present in the built/local directory desc("Makes a new LKG out of the built js files"); task("LKG", ["clean", "release", "local"].concat(libraryTargets), function () { - var expectedFiles = [tscFile, servicesFile, serverFile, nodePackageFile, nodeDefinitionsFile, standaloneDefinitionsFile, tsserverLibraryFile, tsserverLibraryDefinitionFile, cancellationTokenFile, typingsInstallerFile, buildProtocolDts].concat(libraryTargets); + var expectedFiles = [tscFile, servicesFile, serverFile, nodePackageFile, nodeDefinitionsFile, standaloneDefinitionsFile, tsserverLibraryFile, tsserverLibraryDefinitionFile, cancellationTokenFile, typingsInstallerFile, buildProtocolDts, watchGuardFile].concat(libraryTargets); var missingFiles = expectedFiles.filter(function (f) { return !fs.existsSync(f); }); diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index c90a3484a8a4c..cf50ab6482b11 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -59,6 +59,21 @@ namespace ts { declare var global: any; declare var __filename: string; + export function getNodeMajorVersion() { + if (typeof process === "undefined") { + return undefined; + } + const version: string = process.version; + if (!version) { + return undefined; + } + const dot = version.indexOf("."); + if (dot === -1) { + return undefined; + } + return parseInt(version.substring(1, dot)); + } + declare class Enumerator { public atEnd(): boolean; public moveNext(): boolean; @@ -315,9 +330,8 @@ namespace ts { } const watchedFileSet = createWatchedFileSet(); - function isNode4OrLater(): boolean { - return parseInt(process.version.charAt(1)) >= 4; - } + const nodeVersion = getNodeMajorVersion(); + const isNode4OrLater = nodeVersion >= 4; function isFileSystemCaseSensitive(): boolean { // win32\win64 are case insensitive platforms @@ -485,14 +499,12 @@ namespace ts { // 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) let options: any; - if (!directoryExists(directoryName) || (isUNCPath(directoryName) && process.platform === "win32")) { - // do nothing if either - // - target folder does not exist - // - this is UNC path on Windows (https://github.com/Microsoft/TypeScript/issues/13874) + if (!directoryExists(directoryName)) { + // do nothing if target folder does not exist return noOpFileWatcher; } - if (isNode4OrLater() && (process.platform === "win32" || process.platform === "darwin")) { + if (isNode4OrLater && (process.platform === "win32" || process.platform === "darwin")) { options = { persistent: true, recursive: !!recursive }; } else { @@ -512,10 +524,6 @@ namespace ts { }; } ); - - function isUNCPath(s: string): boolean { - return s.length > 2 && s.charCodeAt(0) === CharacterCodes.slash && s.charCodeAt(1) === CharacterCodes.slash; - } }, resolvePath: function(path: string): string { return _path.resolve(path); diff --git a/src/server/server.ts b/src/server/server.ts index d828a717a4150..2a5990f425268 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -12,6 +12,7 @@ namespace ts.server { const childProcess: { fork(modulePath: string, args: string[], options?: { execArgv: string[], env?: MapLike }): NodeChildProcess; + execFileSync(file: string, args: string[], options: { stdio: "ignore", env: MapLike }): string | Buffer; } = require("child_process"); const os: { @@ -59,7 +60,7 @@ namespace ts.server { interface NodeChildProcess { send(message: any, sendHandle?: any): void; - on(message: "message", f: (m: any) => void): void; + on(message: "message" | "exit", f: (m: any) => void): void; kill(): void; pid: number; } @@ -576,7 +577,84 @@ namespace ts.server { } } + function extractWatchDirectoryCacheKey(path: string, currentDriveKey: string) { + path = normalizeSlashes(path); + if (isUNCPath(path)) { + // UNC path: extract server name + // //server/location + // ^ <- from 0 to this position + const firstSlash = path.indexOf(directorySeparator, 2); + return firstSlash !== -1 ? path.substring(0, firstSlash).toLowerCase() : path; + } + const rootLength = getRootLength(path); + if (rootLength === 0) { + // relative path - assume file is on the current drive + return currentDriveKey; + } + if (path.charCodeAt(1) === CharacterCodes.colon && path.charCodeAt(2) === CharacterCodes.slash) { + // rooted path that starts with c:/... - extract drive letter + return path.charAt(0).toLowerCase(); + } + if (path.charCodeAt(0) === CharacterCodes.slash && path.charCodeAt(1) !== CharacterCodes.slash) { + // rooted path that starts with slash - /somename - use key for current drive + return currentDriveKey; + } + // do not cache any other cases + return undefined; + } + + function isUNCPath(s: string): boolean { + return s.length > 2 && s.charCodeAt(0) === CharacterCodes.slash && s.charCodeAt(1) === CharacterCodes.slash; + } + const sys = ts.sys; + // use watchGuard process on Windows when node version is 4 or later + const useWatchGuard = process.platform === "win32" && getNodeMajorVersion() >= 4; + if (useWatchGuard) { + const currentDrive = extractWatchDirectoryCacheKey(sys.resolvePath(sys.getCurrentDirectory()), /*currentDriveKey*/ undefined); + const statusCache = createMap(); + const originalWatchDirectory = sys.watchDirectory; + sys.watchDirectory = function (path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher { + const cacheKey = extractWatchDirectoryCacheKey(path, currentDrive); + let status = cacheKey && statusCache.get(cacheKey); + if (status === undefined) { + if (logger.hasLevel(LogLevel.verbose)) { + logger.info(`${cacheKey} for path ${path} not found in cache...`); + } + try { + const args = [combinePaths(__dirname, "watchGuard.js"), path]; + if (logger.hasLevel(LogLevel.verbose)) { + logger.info(`Starting ${process.execPath} with args ${JSON.stringify(args)}`); + } + childProcess.execFileSync(process.execPath, args, { stdio: "ignore", env: { "ELECTRON_RUN_AS_NODE": "1" } }); + status = true; + if (logger.hasLevel(LogLevel.verbose)) { + logger.info(`WatchGuard for path ${path} returned: OK`); + } + } + catch (e) { + status = false; + if (logger.hasLevel(LogLevel.verbose)) { + logger.info(`WatchGuard for path ${path} returned: ${e.message}`); + } + } + if (cacheKey) { + statusCache.set(cacheKey, status); + } + } + else if (logger.hasLevel(LogLevel.verbose)) { + logger.info(`watchDirectory for ${path} uses cached drive information.`); + } + if (status) { + // this drive is safe to use - call real 'watchDirectory' + return originalWatchDirectory.call(sys, path, callback, recursive); + } + else { + // this drive is unsafe - return no-op watcher + return { close() { } }; + } + } + } // Override sys.write because fs.writeSync is not reliable on Node 4 sys.write = (s: string) => writeMessage(new Buffer(s, "utf8")); diff --git a/src/server/watchGuard/tsconfig.json b/src/server/watchGuard/tsconfig.json new file mode 100644 index 0000000000000..ef9b0ab0603c1 --- /dev/null +++ b/src/server/watchGuard/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig-base", + "compilerOptions": { + "removeComments": true, + "outFile": "../../../built/local/watchGuard.js" + }, + "files": [ + "watchGuard.ts" + ] +} \ No newline at end of file diff --git a/src/server/watchGuard/watchGuard.ts b/src/server/watchGuard/watchGuard.ts new file mode 100644 index 0000000000000..7a0307a912059 --- /dev/null +++ b/src/server/watchGuard/watchGuard.ts @@ -0,0 +1,19 @@ +/// + +if (process.argv.length < 3) { + process.exit(1); +} +const directoryName = process.argv[2]; +const fs: { watch(directoryName: string, options: any, callback: () => {}): any } = require("fs"); +// main reason why we need separate process to check if it is safe to watch some path +// is to guard against crashes that cannot be intercepted with protected blocks and +// code in tsserver already can handle normal cases, like non-existing folders. +// This means that here we treat any result (success or exception) from fs.watch as success since it does not tear down the process. +// The only case that should be considered as failure - when watchGuard process crashes. +try { + const watcher = fs.watch(directoryName, { recursive: true }, () => ({})) + watcher.close(); +} +catch (_e) { +} +process.exit(0); \ No newline at end of file