Skip to content

Commit

Permalink
[release-2.2] use separate process to probe if drive is safe to watch (
Browse files Browse the repository at this point in the history
…#14098) (#14124)

* use separate process to probe if drive is safe to watch (#14098)

use dedicated process to determine if it is safe to watch folders

* added release-2.2
  • Loading branch information
vladima committed Feb 16, 2017
1 parent 96b52c8 commit 08fe20e
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 15 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -17,6 +17,7 @@ branches:
only:
- master
- release-2.1
- release-2.2

install:
- npm uninstall typescript
Expand Down
9 changes: 7 additions & 2 deletions Jakefile.js
Expand Up @@ -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/";
Expand Down Expand Up @@ -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"));

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
});
Expand Down
32 changes: 20 additions & 12 deletions src/compiler/sys.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down
80 changes: 79 additions & 1 deletion src/server/server.ts
Expand Up @@ -12,6 +12,7 @@ namespace ts.server {

const childProcess: {
fork(modulePath: string, args: string[], options?: { execArgv: string[], env?: MapLike<string> }): NodeChildProcess;
execFileSync(file: string, args: string[], options: { stdio: "ignore", env: MapLike<string> }): string | Buffer;
} = require("child_process");

const os: {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 = <ServerHost>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<boolean>();
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"));
Expand Down
10 changes: 10 additions & 0 deletions src/server/watchGuard/tsconfig.json
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig-base",
"compilerOptions": {
"removeComments": true,
"outFile": "../../../built/local/watchGuard.js"
},
"files": [
"watchGuard.ts"
]
}
19 changes: 19 additions & 0 deletions src/server/watchGuard/watchGuard.ts
@@ -0,0 +1,19 @@
/// <reference types="node" />

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);

0 comments on commit 08fe20e

Please sign in to comment.