mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
Fix web TS Server trying to read files outside of project root (#173591)
There's a bug on the TS Server side that causes it to traverse out of the project root when checking for directories like `node_modules`. On web this ends up being super slow because it goes to the network This fix blocks those reads
This commit is contained in:
@@ -14,6 +14,9 @@ import { URI } from 'vscode-uri';
|
||||
const watchFiles: Map<string, { path: string; callback: ts.FileWatcherCallback; pollingInterval?: number; options?: ts.WatchOptions }> = new Map();
|
||||
const watchDirectories: Map<string, { path: string; callback: ts.DirectoryWatcherCallback; recursive?: boolean; options?: ts.WatchOptions }> = new Map();
|
||||
let session: WorkerSession | undefined;
|
||||
|
||||
const projectRootPaths = new Map</* original path*/ string, /* parsed URI */ URI>();
|
||||
|
||||
// END GLOBALS
|
||||
// BEGIN misc internals
|
||||
const indent: (str: string) => string = (ts as any).server.indent;
|
||||
@@ -188,7 +191,7 @@ function createServerHost(extensionUri: URI, logger: ts.server.Logger, apiClient
|
||||
const contents = fs.readFile(toResource(path)).slice();
|
||||
return textDecoder.decode(contents);
|
||||
} catch (error) {
|
||||
logNormal('Error fs.readFile', { path, error });
|
||||
logNormal('Error fs.readFile', { path, error: error + '' });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
@@ -202,7 +205,7 @@ function createServerHost(extensionUri: URI, logger: ts.server.Logger, apiClient
|
||||
try {
|
||||
return fs.stat(toResource(path)).size;
|
||||
} catch (error) {
|
||||
logNormal('Error fs.getFileSize', { path, error });
|
||||
logNormal('Error fs.getFileSize', { path, error: error + '' });
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
@@ -220,7 +223,7 @@ function createServerHost(extensionUri: URI, logger: ts.server.Logger, apiClient
|
||||
try {
|
||||
fs.writeFile(toResource(path), textEncoder.encode(data));
|
||||
} catch (error) {
|
||||
logNormal('Error fs.writeFile', { path, error });
|
||||
logNormal('Error fs.writeFile', { path, error: error + '' });
|
||||
}
|
||||
},
|
||||
resolvePath(path: string): string {
|
||||
@@ -244,7 +247,7 @@ function createServerHost(extensionUri: URI, logger: ts.server.Logger, apiClient
|
||||
try {
|
||||
return fs.stat(toResource(path)).type === FileType.File;
|
||||
} catch (error) {
|
||||
logNormal('Error fs.fileExists', { path, error });
|
||||
logNormal('Error fs.fileExists', { path, error: error + '' });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -258,7 +261,7 @@ function createServerHost(extensionUri: URI, logger: ts.server.Logger, apiClient
|
||||
try {
|
||||
return fs.stat(toResource(path)).type === FileType.Directory;
|
||||
} catch (error) {
|
||||
logNormal('Error fs.directoryExists', { path, error });
|
||||
logNormal('Error fs.directoryExists', { path, error: error + '' });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@@ -272,7 +275,7 @@ function createServerHost(extensionUri: URI, logger: ts.server.Logger, apiClient
|
||||
try {
|
||||
fs.createDirectory(toResource(path));
|
||||
} catch (error) {
|
||||
logNormal('Error fs.createDirectory', { path, error });
|
||||
logNormal('Error fs.createDirectory', { path, error: error + '' });
|
||||
}
|
||||
},
|
||||
getExecutingFilePath(): string {
|
||||
@@ -297,7 +300,7 @@ function createServerHost(extensionUri: URI, logger: ts.server.Logger, apiClient
|
||||
try {
|
||||
return new Date(fs.stat(toResource(path)).mtime);
|
||||
} catch (error) {
|
||||
logNormal('Error fs.getModifiedTime', { path, error });
|
||||
logNormal('Error fs.getModifiedTime', { path, error: error + '' });
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
@@ -311,7 +314,7 @@ function createServerHost(extensionUri: URI, logger: ts.server.Logger, apiClient
|
||||
try {
|
||||
fs.delete(toResource(path));
|
||||
} catch (error) {
|
||||
logNormal('Error fs.deleteFile', { path, error });
|
||||
logNormal('Error fs.deleteFile', { path, error: error + '' });
|
||||
}
|
||||
},
|
||||
createHash: generateDjb2Hash,
|
||||
@@ -386,7 +389,7 @@ function createServerHost(extensionUri: URI, logger: ts.server.Logger, apiClient
|
||||
/**
|
||||
* Copied from toResource in typescriptServiceClient.ts
|
||||
*/
|
||||
function toResource(filepath: string) {
|
||||
function toResource(filepath: string): URI {
|
||||
if (filepath.startsWith('/lib.') && filepath.endsWith('.d.ts')) {
|
||||
return URI.from({
|
||||
scheme: extensionUri.scheme,
|
||||
@@ -394,14 +397,48 @@ function createServerHost(extensionUri: URI, logger: ts.server.Logger, apiClient
|
||||
path: extensionUri.path + '/dist/browser/typescript/' + filepath.slice(1)
|
||||
});
|
||||
}
|
||||
const parts = filepath.match(/^\/([^\/]+)\/([^\/]*)(?:\/(.+))?$/);
|
||||
if (!parts) {
|
||||
throw new Error('complex regex failed to match ' + filepath);
|
||||
|
||||
const uri = filePathToResourceUri(filepath);
|
||||
if (!uri) {
|
||||
throw new Error(`Could not parse path ${filepath}`);
|
||||
}
|
||||
return URI.parse(parts[1] + '://' + (parts[2] === 'ts-nul-authority' ? '' : parts[2]) + (parts[3] ? '/' + parts[3] : ''));
|
||||
|
||||
// Check if TS is trying to read a file outside of the project root.
|
||||
// We allow reading files on unknown scheme as these may be loose files opened by the user.
|
||||
// However we block reading files on schemes that are on a known file system with an unknown root
|
||||
let allowRead: 'implicit' | 'block' | 'allow' = 'implicit';
|
||||
for (const projectRoot of projectRootPaths.values()) {
|
||||
if (uri.scheme === projectRoot.scheme) {
|
||||
if (uri.toString().startsWith(projectRoot.toString())) {
|
||||
allowRead = 'allow';
|
||||
break;
|
||||
}
|
||||
|
||||
// Tentatively block the read but a future loop may allow it
|
||||
allowRead = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
if (allowRead === 'block') {
|
||||
throw new Error(`Could not read file outside of project root ${filepath}`);
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
|
||||
function filePathToResourceUri(filepath: string): URI | undefined {
|
||||
const parts = filepath.match(/^\/([^\/]+)\/([^\/]*)(?:\/(.+))?$/);
|
||||
if (!parts) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const scheme = parts[1];
|
||||
const authority = parts[2] === 'ts-nul-authority' ? '' : parts[2];
|
||||
const path = parts[3];
|
||||
return URI.from({ scheme, authority, path: (path ? '/' + path : path) });
|
||||
}
|
||||
|
||||
class WasmCancellationToken implements ts.server.ServerCancellationToken {
|
||||
shouldCancel: (() => boolean) | undefined;
|
||||
currentRequestId: number | undefined = undefined;
|
||||
@@ -435,12 +472,14 @@ interface StartSessionOptions {
|
||||
}
|
||||
|
||||
class WorkerSession extends ts.server.Session<{}> {
|
||||
wasmCancellationToken: WasmCancellationToken;
|
||||
listener: (message: any) => void;
|
||||
|
||||
readonly wasmCancellationToken: WasmCancellationToken;
|
||||
readonly listener: (message: any) => void;
|
||||
|
||||
constructor(
|
||||
host: ts.server.ServerHost,
|
||||
options: StartSessionOptions,
|
||||
public port: MessagePort,
|
||||
public readonly port: MessagePort,
|
||||
logger: ts.server.Logger,
|
||||
hrtime: ts.server.SessionOptions['hrtime']
|
||||
) {
|
||||
@@ -450,12 +489,13 @@ class WorkerSession extends ts.server.Session<{}> {
|
||||
cancellationToken,
|
||||
...options,
|
||||
typingsInstaller: ts.server.nullTypingsInstaller, // TODO: Someday!
|
||||
byteLength: () => { throw new Error('Not implemented'); }, // Formats the message text in send of Session which is overriden in this class so not needed
|
||||
byteLength: () => { throw new Error('Not implemented'); }, // Formats the message text in send of Session which is overridden in this class so not needed
|
||||
hrtime,
|
||||
logger,
|
||||
canUseEvents: true,
|
||||
});
|
||||
this.wasmCancellationToken = cancellationToken;
|
||||
|
||||
this.listener = (message: any) => {
|
||||
// TEMP fix since Cancellation.retrieveCheck is not correct
|
||||
function retrieveCheck2(data: any) {
|
||||
@@ -472,9 +512,27 @@ class WorkerSession extends ts.server.Session<{}> {
|
||||
if (shouldCancel) {
|
||||
this.wasmCancellationToken.shouldCancel = shouldCancel;
|
||||
}
|
||||
|
||||
try {
|
||||
if (message.data.command === 'updateOpen') {
|
||||
const args = message.data.arguments as ts.server.protocol.UpdateOpenRequestArgs;
|
||||
for (const open of args.openFiles ?? []) {
|
||||
if (open.projectRootPath) {
|
||||
const uri = filePathToResourceUri(open.projectRootPath);
|
||||
if (uri) {
|
||||
projectRootPaths.set(open.projectRootPath, uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Noop
|
||||
}
|
||||
|
||||
this.onMessage(message.data);
|
||||
};
|
||||
}
|
||||
|
||||
public override send(msg: ts.server.protocol.Message) {
|
||||
if (msg.type === 'event' && !this.canUseEvents) {
|
||||
if (this.logger.hasLevel(ts.server.LogLevel.verbose)) {
|
||||
@@ -487,18 +545,22 @@ class WorkerSession extends ts.server.Session<{}> {
|
||||
}
|
||||
this.port.postMessage(msg);
|
||||
}
|
||||
|
||||
protected override parseMessage(message: {}): ts.server.protocol.Request {
|
||||
return message as ts.server.protocol.Request;
|
||||
}
|
||||
|
||||
protected override toStringMessage(message: {}) {
|
||||
return JSON.stringify(message, undefined, 2);
|
||||
}
|
||||
|
||||
override exit() {
|
||||
this.logger.info('Exiting...');
|
||||
this.port.removeEventListener('message', this.listener);
|
||||
this.projectService.closeLog();
|
||||
close();
|
||||
}
|
||||
|
||||
listen() {
|
||||
this.logger.info(`webServer.ts: tsserver starting to listen for messages on 'message'...`);
|
||||
this.port.onmessage = this.listener;
|
||||
|
||||
Reference in New Issue
Block a user