mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-12-11 17:41:26 -06:00
use separate process to probe if drive is safe to watch (#14098)
use dedicated process to determine if it is safe to watch folders
This commit is contained in:
parent
c90040effb
commit
8c54bbaa04
@ -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);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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: {
|
||||
@ -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 = <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"));
|
||||
|
||||
10
src/server/watchGuard/tsconfig.json
Normal file
10
src/server/watchGuard/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig-base",
|
||||
"compilerOptions": {
|
||||
"removeComments": true,
|
||||
"outFile": "../../../built/local/watchGuard.js"
|
||||
},
|
||||
"files": [
|
||||
"watchGuard.ts"
|
||||
]
|
||||
}
|
||||
19
src/server/watchGuard/watchGuard.ts
Normal file
19
src/server/watchGuard/watchGuard.ts
Normal file
@ -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);
|
||||
Loading…
x
Reference in New Issue
Block a user