code-server/src/node/channel.ts

314 lines
12 KiB
TypeScript
Raw Normal View History

2019-06-29 05:37:23 +07:00
import * as path from "path";
import { VSBuffer } from "vs/base/common/buffer";
2019-06-29 05:37:23 +07:00
import { Emitter, Event } from "vs/base/common/event";
import { IDisposable } from "vs/base/common/lifecycle";
2019-06-29 05:37:23 +07:00
import { OS } from "vs/base/common/platform";
import { URI, UriComponents } from "vs/base/common/uri";
2019-07-12 05:12:52 +07:00
import { transformOutgoingURIs } from "vs/base/common/uriIpc";
2019-06-29 05:37:23 +07:00
import { IServerChannel } from "vs/base/parts/ipc/common/ipc";
import { IDiagnosticInfo } from "vs/platform/diagnostics/common/diagnostics";
2019-06-29 05:37:23 +07:00
import { IEnvironmentService } from "vs/platform/environment/common/environment";
2019-08-10 06:50:05 +07:00
import { ExtensionIdentifier, IExtensionDescription } from "vs/platform/extensions/common/extensions";
import { FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileType, IStat, IWatchOptions } from "vs/platform/files/common/files";
import { DiskFileSystemProvider } from "vs/platform/files/node/diskFileSystemProvider";
import { ILogService } from "vs/platform/log/common/log";
import product from "vs/platform/product/common/product";
2019-06-29 05:37:23 +07:00
import { IRemoteAgentEnvironment } from "vs/platform/remote/common/remoteAgentEnvironment";
2019-07-17 02:57:02 +07:00
import { ITelemetryService } from "vs/platform/telemetry/common/telemetry";
import { INodeProxyService } from "vs/server/src/common/nodeProxy";
import { getTranslations } from "vs/server/src/node/nls";
import { getUriTransformer, localRequire } from "vs/server/src/node/util";
2019-08-10 06:50:05 +07:00
import { ExtensionScanner, ExtensionScannerInput } from "vs/workbench/services/extensions/node/extensionPoints";
2019-07-12 05:12:52 +07:00
/**
* Extend the file provider to allow unwatching.
*/
class Watcher extends DiskFileSystemProvider {
public readonly watches = new Map<number, IDisposable>();
public dispose(): void {
this.watches.forEach((w) => w.dispose());
this.watches.clear();
super.dispose();
}
public _watch(req: number, resource: URI, opts: IWatchOptions): void {
this.watches.set(req, this.watch(resource, opts));
}
public unwatch(req: number): void {
this.watches.get(req)!.dispose();
this.watches.delete(req);
}
}
2019-06-29 05:37:23 +07:00
2019-07-08 22:33:58 +07:00
export class FileProviderChannel implements IServerChannel, IDisposable {
private readonly provider: DiskFileSystemProvider;
private readonly watchers = new Map<string, Watcher>();
2019-07-13 05:13:49 +07:00
public constructor(
private readonly environmentService: IEnvironmentService,
private readonly logService: ILogService,
) {
this.provider = new DiskFileSystemProvider(this.logService);
}
2019-07-02 22:34:03 +07:00
public listen(context: any, event: string, args?: any): Event<any> {
2019-06-29 05:37:23 +07:00
switch (event) {
// This is where the actual file changes are sent. The watch method just
// adds things that will fire here. That means we have to split up
// watchers based on the session otherwise sessions would get events for
// other sessions. There is also no point in having the watcher unless
// something is listening. I'm not sure there is a different way to
// dispose, anyway.
2019-06-29 05:37:23 +07:00
case "filechange":
const session = args[0];
const emitter = new Emitter({
onFirstListenerAdd: () => {
const provider = new Watcher(this.logService);
this.watchers.set(session, provider);
2019-07-02 22:34:03 +07:00
const transformer = getUriTransformer(context.remoteAuthority);
provider.onDidChangeFile((events) => {
emitter.fire(events.map((event) => ({
...event,
2019-07-02 22:34:03 +07:00
resource: transformer.transformOutgoing(event.resource),
})));
});
provider.onDidErrorOccur((event) => emitter.fire(event));
},
onLastListenerRemove: () => {
this.watchers.get(session)!.dispose();
this.watchers.delete(session);
},
});
return emitter.event;
2019-06-29 05:37:23 +07:00
}
throw new Error(`Invalid listen "${event}"`);
}
public call(_: unknown, command: string, args?: any): Promise<any> {
switch (command) {
case "stat": return this.stat(args[0]);
case "open": return this.open(args[0], args[1]);
case "close": return this.close(args[0]);
case "read": return this.read(args[0], args[1], args[2]);
2019-06-29 05:37:23 +07:00
case "write": return this.write(args[0], args[1], args[2], args[3], args[4]);
case "delete": return this.delete(args[0], args[1]);
case "mkdir": return this.mkdir(args[0]);
case "readdir": return this.readdir(args[0]);
case "rename": return this.rename(args[0], args[1], args[2]);
case "copy": return this.copy(args[0], args[1], args[2]);
case "watch": return this.watch(args[0], args[1], args[2], args[3]);
case "unwatch": return this.unwatch(args[0], args[1]);
2019-06-29 05:37:23 +07:00
}
throw new Error(`Invalid call "${command}"`);
}
2019-07-08 22:33:58 +07:00
public dispose(): void {
this.watchers.forEach((w) => w.dispose());
this.watchers.clear();
}
private async stat(resource: UriComponents): Promise<IStat> {
2019-07-13 05:13:49 +07:00
return this.provider.stat(this.transform(resource));
2019-06-29 05:37:23 +07:00
}
private async open(resource: UriComponents, opts: FileOpenOptions): Promise<number> {
2019-07-13 05:13:49 +07:00
return this.provider.open(this.transform(resource), opts);
2019-06-29 05:37:23 +07:00
}
private async close(fd: number): Promise<void> {
return this.provider.close(fd);
2019-06-29 05:37:23 +07:00
}
private async read(fd: number, pos: number, length: number): Promise<[VSBuffer, number]> {
const buffer = VSBuffer.alloc(length);
const bytesRead = await this.provider.read(fd, pos, buffer.buffer, 0, length);
return [buffer, bytesRead];
2019-06-29 05:37:23 +07:00
}
private write(fd: number, pos: number, buffer: VSBuffer, offset: number, length: number): Promise<number> {
return this.provider.write(fd, pos, buffer.buffer, offset, length);
2019-06-29 05:37:23 +07:00
}
private async delete(resource: UriComponents, opts: FileDeleteOptions): Promise<void> {
2019-07-13 05:13:49 +07:00
return this.provider.delete(this.transform(resource), opts);
2019-06-29 05:37:23 +07:00
}
private async mkdir(resource: UriComponents): Promise<void> {
2019-07-13 05:13:49 +07:00
return this.provider.mkdir(this.transform(resource));
2019-06-29 05:37:23 +07:00
}
private async readdir(resource: UriComponents): Promise<[string, FileType][]> {
2019-07-13 05:13:49 +07:00
return this.provider.readdir(this.transform(resource));
2019-06-29 05:37:23 +07:00
}
private async rename(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
2019-07-13 05:13:49 +07:00
return this.provider.rename(this.transform(resource), URI.from(target), opts);
2019-06-29 05:37:23 +07:00
}
private copy(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
2019-07-13 05:13:49 +07:00
return this.provider.copy(this.transform(resource), URI.from(target), opts);
2019-06-29 05:37:23 +07:00
}
private async watch(session: string, req: number, resource: UriComponents, opts: IWatchOptions): Promise<void> {
2019-07-13 05:13:49 +07:00
this.watchers.get(session)!._watch(req, this.transform(resource), opts);
2019-06-29 05:37:23 +07:00
}
private async unwatch(session: string, req: number): Promise<void> {
this.watchers.get(session)!.unwatch(req);
2019-06-29 05:37:23 +07:00
}
2019-07-13 05:13:49 +07:00
private transform(resource: UriComponents): URI {
// Used for walkthrough content.
if (/^\/static[^/]*\//.test(resource.path)) {
return URI.file(this.environmentService.appRoot + resource.path.replace(/^\/static[^/]*\//, "/"));
// Used by the webview service worker to load resources.
} else if (resource.path === "/vscode-resource" && resource.query) {
try {
const query = JSON.parse(resource.query);
if (query.requestResourcePath) {
return URI.file(query.requestResourcePath);
}
} catch (error) { /* Carry on. */ }
2019-07-13 05:13:49 +07:00
}
return URI.from(resource);
}
2019-06-29 05:37:23 +07:00
}
export class ExtensionEnvironmentChannel implements IServerChannel {
2019-07-03 00:36:41 +07:00
public constructor(
private readonly environment: IEnvironmentService,
private readonly log: ILogService,
2019-07-17 02:57:02 +07:00
private readonly telemetry: ITelemetryService,
private readonly connectionToken: string,
2019-07-03 00:36:41 +07:00
) {}
2019-06-29 05:37:23 +07:00
2019-07-02 22:34:03 +07:00
public listen(_: unknown, event: string): Event<any> {
2019-06-29 05:37:23 +07:00
throw new Error(`Invalid listen "${event}"`);
}
2019-07-03 00:36:41 +07:00
public async call(context: any, command: string, args?: any): Promise<any> {
2019-06-29 05:37:23 +07:00
switch (command) {
2019-07-02 22:34:03 +07:00
case "getEnvironmentData":
return transformOutgoingURIs(
2019-07-03 00:36:41 +07:00
await this.getEnvironmentData(args.language),
2019-07-02 22:34:03 +07:00
getUriTransformer(context.remoteAuthority),
);
2019-06-29 05:37:23 +07:00
case "getDiagnosticInfo": return this.getDiagnosticInfo();
case "disableTelemetry": return this.disableTelemetry();
}
throw new Error(`Invalid call "${command}"`);
}
2019-07-03 00:36:41 +07:00
private async getEnvironmentData(locale: string): Promise<IRemoteAgentEnvironment> {
2019-06-29 05:37:23 +07:00
return {
pid: process.pid,
connectionToken: this.connectionToken,
2019-07-02 22:34:03 +07:00
appRoot: URI.file(this.environment.appRoot),
appSettingsHome: this.environment.appSettingsHome,
settingsPath: this.environment.machineSettingsHome,
logsPath: URI.file(this.environment.logsPath),
2019-08-10 06:50:05 +07:00
extensionsPath: URI.file(this.environment.extensionsPath!),
2019-07-03 00:36:41 +07:00
extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, "extension-host")),
2019-07-02 22:34:03 +07:00
globalStorageHome: URI.file(this.environment.globalStorageHome),
userHome: URI.file(this.environment.userHome),
2019-07-03 00:36:41 +07:00
extensions: await this.scanExtensions(locale),
2019-06-29 05:37:23 +07:00
os: OS,
};
}
2019-07-03 00:36:41 +07:00
private async scanExtensions(locale: string): Promise<IExtensionDescription[]> {
const translations = await getTranslations(locale, this.environment.userDataPath);
2019-07-03 00:36:41 +07:00
const scanMultiple = (isBuiltin: boolean, isUnderDevelopment: boolean, paths: string[]): Promise<IExtensionDescription[][]> => {
return Promise.all(paths.map((path) => {
return ExtensionScanner.scanExtensions(new ExtensionScannerInput(
product.version,
product.commit,
locale,
!!process.env.VSCODE_DEV,
path,
isBuiltin,
isUnderDevelopment,
translations,
), this.log);
}));
};
const scanBuiltin = async (): Promise<IExtensionDescription[][]> => {
return scanMultiple(true, false, [this.environment.builtinExtensionsPath, ...this.environment.extraBuiltinExtensionPaths]);
2019-07-03 00:36:41 +07:00
};
const scanInstalled = async (): Promise<IExtensionDescription[][]> => {
2019-08-10 06:50:05 +07:00
return scanMultiple(false, true, [this.environment.extensionsPath!, ...this.environment.extraExtensionPaths]);
2019-07-03 00:36:41 +07:00
};
return Promise.all([scanBuiltin(), scanInstalled()]).then((allExtensions) => {
const uniqueExtensions = new Map<string, IExtensionDescription>();
allExtensions.forEach((multipleExtensions) => {
multipleExtensions.forEach((extensions) => {
extensions.forEach((extension) => {
const id = ExtensionIdentifier.toKey(extension.identifier);
if (uniqueExtensions.has(id)) {
const oldPath = uniqueExtensions.get(id)!.extensionLocation.fsPath;
const newPath = extension.extensionLocation.fsPath;
2019-07-20 05:43:54 +07:00
this.log.warn(`${oldPath} has been overridden ${newPath}`);
}
uniqueExtensions.set(id, extension);
});
2019-07-03 00:36:41 +07:00
});
});
2019-07-20 05:43:54 +07:00
return Array.from(uniqueExtensions.values());
2019-07-03 00:36:41 +07:00
});
}
2019-06-29 05:37:23 +07:00
private getDiagnosticInfo(): Promise<IDiagnosticInfo> {
throw new Error("not implemented");
}
2019-07-17 02:57:02 +07:00
private async disableTelemetry(): Promise<void> {
this.telemetry.setEnabled(false);
2019-06-29 05:37:23 +07:00
}
}
export class NodeProxyService implements INodeProxyService {
public _serviceBrand = undefined;
public readonly server: import("@coder/node-browser/out/server/server").Server;
private readonly _onMessage = new Emitter<string>();
public readonly onMessage = this._onMessage.event;
private readonly _$onMessage = new Emitter<string>();
public readonly $onMessage = this._$onMessage.event;
2019-10-11 05:05:24 +07:00
public readonly _onDown = new Emitter<void>();
public readonly onDown = this._onDown.event;
2019-10-11 05:05:24 +07:00
public readonly _onUp = new Emitter<void>();
public readonly onUp = this._onUp.event;
2019-10-11 05:05:24 +07:00
// Unused because the server connection will never permanently close.
private readonly _onClose = new Emitter<void>();
public readonly onClose = this._onClose.event;
public constructor() {
// TODO: down/up
const { Server } = localRequire<typeof import("@coder/node-browser/out/server/server")>("@coder/node-browser/out/server/server");
this.server = new Server({
onMessage: this.$onMessage,
onClose: this.onClose,
onDown: this.onDown,
onUp: this.onUp,
send: (message: string): void => {
this._onMessage.fire(message);
}
});
}
public send(message: string): void {
this._$onMessage.fire(message);
}
}