Add https server
This commit is contained in:
266
src/channel.ts
Normal file
266
src/channel.ts
Normal file
@ -0,0 +1,266 @@
|
||||
import * as path from "path";
|
||||
|
||||
import { getPathFromAmdModule } from "vs/base/common/amd";
|
||||
import { VSBuffer } from "vs/base/common/buffer";
|
||||
import { Emitter, Event } from "vs/base/common/event";
|
||||
import { IDisposable } from "vs/base/common/lifecycle";
|
||||
import { OS } from "vs/base/common/platform";
|
||||
import { URI, UriComponents } from "vs/base/common/uri";
|
||||
import { transformOutgoingURIs } from "vs/base/common/uriIpc";
|
||||
import { IServerChannel } from "vs/base/parts/ipc/common/ipc";
|
||||
import { IDiagnosticInfo } from "vs/platform/diagnostics/common/diagnosticsService";
|
||||
import { IEnvironmentService } from "vs/platform/environment/common/environment";
|
||||
import { IExtensionDescription, ExtensionIdentifier } from "vs/platform/extensions/common/extensions";
|
||||
import { FileDeleteOptions, FileOverwriteOptions, FileType, IStat, IWatchOptions, FileOpenOptions } from "vs/platform/files/common/files";
|
||||
import { ILogService } from "vs/platform/log/common/log";
|
||||
import pkg from "vs/platform/product/node/package";
|
||||
import product from "vs/platform/product/node/product";
|
||||
import { IRemoteAgentEnvironment } from "vs/platform/remote/common/remoteAgentEnvironment";
|
||||
import { ExtensionScanner, ExtensionScannerInput } from "vs/workbench/services/extensions/node/extensionPoints";
|
||||
import { DiskFileSystemProvider } from "vs/workbench/services/files/node/diskFileSystemProvider";
|
||||
|
||||
import { getUriTransformer } from "vs/server/src/util";
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See: src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts.
|
||||
*/
|
||||
export class FileProviderChannel implements IServerChannel, IDisposable {
|
||||
private readonly provider: DiskFileSystemProvider;
|
||||
private readonly watchers = new Map<string, Watcher>();
|
||||
|
||||
public constructor(private readonly logService: ILogService) {
|
||||
this.provider = new DiskFileSystemProvider(this.logService);
|
||||
}
|
||||
|
||||
public listen(context: any, event: string, args?: any): Event<any> {
|
||||
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.
|
||||
case "filechange":
|
||||
const session = args[0];
|
||||
const emitter = new Emitter({
|
||||
onFirstListenerAdd: () => {
|
||||
const provider = new Watcher(this.logService);
|
||||
this.watchers.set(session, provider);
|
||||
const transformer = getUriTransformer(context.remoteAuthority);
|
||||
provider.onDidChangeFile((events) => {
|
||||
emitter.fire(events.map((event) => ({
|
||||
...event,
|
||||
resource: transformer.transformOutgoing(event.resource),
|
||||
})));
|
||||
});
|
||||
provider.onDidErrorOccur((event) => emitter.fire(event));
|
||||
},
|
||||
onLastListenerRemove: () => {
|
||||
this.watchers.get(session)!.dispose();
|
||||
this.watchers.delete(session);
|
||||
},
|
||||
});
|
||||
|
||||
return emitter.event;
|
||||
}
|
||||
|
||||
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]);
|
||||
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]);
|
||||
}
|
||||
|
||||
throw new Error(`Invalid call "${command}"`);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.watchers.forEach((w) => w.dispose());
|
||||
this.watchers.clear();
|
||||
}
|
||||
|
||||
private async stat(resource: UriComponents): Promise<IStat> {
|
||||
return this.provider.stat(URI.from(resource));
|
||||
}
|
||||
|
||||
private async open(resource: UriComponents, opts: FileOpenOptions): Promise<number> {
|
||||
return this.provider.open(URI.from(resource), opts);
|
||||
}
|
||||
|
||||
private async close(fd: number): Promise<void> {
|
||||
return this.provider.close(fd);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
private write(fd: number, pos: number, buffer: VSBuffer, offset: number, length: number): Promise<number> {
|
||||
return this.provider.write(fd, pos, buffer.buffer, offset, length);
|
||||
}
|
||||
|
||||
private async delete(resource: UriComponents, opts: FileDeleteOptions): Promise<void> {
|
||||
return this.provider.delete(URI.from(resource), opts);
|
||||
}
|
||||
|
||||
private async mkdir(resource: UriComponents): Promise<void> {
|
||||
return this.provider.mkdir(URI.from(resource));
|
||||
}
|
||||
|
||||
private async readdir(resource: UriComponents): Promise<[string, FileType][]> {
|
||||
return this.provider.readdir(URI.from(resource));
|
||||
}
|
||||
|
||||
private async rename(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this.provider.rename(URI.from(resource), URI.from(target), opts);
|
||||
}
|
||||
|
||||
private copy(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this.provider.copy(URI.from(resource), URI.from(target), opts);
|
||||
}
|
||||
|
||||
private async watch(session: string, req: number, resource: UriComponents, opts: IWatchOptions): Promise<void> {
|
||||
this.watchers.get(session)!._watch(req, URI.from(resource), opts);
|
||||
}
|
||||
|
||||
private async unwatch(session: string, req: number): Promise<void> {
|
||||
this.watchers.get(session)!.unwatch(req);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See: src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts.
|
||||
*/
|
||||
export class ExtensionEnvironmentChannel implements IServerChannel {
|
||||
public constructor(
|
||||
private readonly environment: IEnvironmentService,
|
||||
private readonly log: ILogService,
|
||||
) {}
|
||||
|
||||
public listen(_: unknown, event: string): Event<any> {
|
||||
throw new Error(`Invalid listen "${event}"`);
|
||||
}
|
||||
|
||||
public async call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case "getEnvironmentData":
|
||||
return transformOutgoingURIs(
|
||||
await this.getEnvironmentData(args.language),
|
||||
getUriTransformer(context.remoteAuthority),
|
||||
);
|
||||
case "getDiagnosticInfo": return this.getDiagnosticInfo();
|
||||
case "disableTelemetry": return this.disableTelemetry();
|
||||
}
|
||||
throw new Error(`Invalid call "${command}"`);
|
||||
}
|
||||
|
||||
private async getEnvironmentData(locale: string): Promise<IRemoteAgentEnvironment> {
|
||||
return {
|
||||
pid: process.pid,
|
||||
appRoot: URI.file(this.environment.appRoot),
|
||||
appSettingsHome: this.environment.appSettingsHome,
|
||||
settingsPath: this.environment.machineSettingsHome,
|
||||
logsPath: URI.file(this.environment.logsPath),
|
||||
extensionsPath: URI.file(this.environment.extensionsPath),
|
||||
extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, "extension-host")),
|
||||
globalStorageHome: URI.file(this.environment.globalStorageHome),
|
||||
userHome: URI.file(this.environment.userHome),
|
||||
extensions: await this.scanExtensions(locale),
|
||||
os: OS,
|
||||
};
|
||||
}
|
||||
|
||||
private async scanExtensions(locale: string): Promise<IExtensionDescription[]> {
|
||||
const root = getPathFromAmdModule(require, "");
|
||||
|
||||
const translations = {}; // TODO: translations
|
||||
|
||||
// TODO: there is also this.environment.extensionDevelopmentLocationURI to look into.
|
||||
const scanBuiltin = async (): Promise<IExtensionDescription[]> => {
|
||||
const input = new ExtensionScannerInput(
|
||||
pkg.version, product.commit, locale, !!process.env.VSCODE_DEV,
|
||||
path.resolve(root, "../extensions"),
|
||||
true,
|
||||
false,
|
||||
translations,
|
||||
);
|
||||
const extensions = await ExtensionScanner.scanExtensions(input, this.log);
|
||||
// TODO: there is more to do if process.env.VSCODE_DEV is true.
|
||||
return extensions;
|
||||
};
|
||||
|
||||
const scanInstalled = async (): Promise<IExtensionDescription[]> => {
|
||||
const input = new ExtensionScannerInput(
|
||||
pkg.version, product.commit, locale, !!process.env.VSCODE_DEV,
|
||||
this.environment.extensionsPath, false, true, translations,
|
||||
);
|
||||
return ExtensionScanner.scanExtensions(input, this.log);
|
||||
};
|
||||
|
||||
return Promise.all([scanBuiltin(), scanInstalled()]).then((allExtensions) => {
|
||||
// It's possible to get duplicates.
|
||||
const uniqueExtensions = new Map<string, IExtensionDescription>();
|
||||
allExtensions.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;
|
||||
this.log.warn(
|
||||
`Extension ${id} in ${oldPath} has been overridden ${newPath}`,
|
||||
);
|
||||
}
|
||||
uniqueExtensions.set(id, extension);
|
||||
});
|
||||
});
|
||||
|
||||
const finalExtensions = <IExtensionDescription[]>[];
|
||||
uniqueExtensions.forEach((e) => finalExtensions.push(e));
|
||||
return finalExtensions;
|
||||
});
|
||||
}
|
||||
|
||||
private getDiagnosticInfo(): Promise<IDiagnosticInfo> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
private disableTelemetry(): Promise<void> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
}
|
157
src/cli.ts
Normal file
157
src/cli.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import * as os from "os";
|
||||
|
||||
import { validatePaths } from "vs/code/node/paths";
|
||||
import { parseMainProcessArgv } from "vs/platform/environment/node/argvHelper";
|
||||
import { ParsedArgs } from "vs/platform/environment/common/environment";
|
||||
import { buildHelpMessage, buildVersionMessage, options } from "vs/platform/environment/node/argv";
|
||||
import product from "vs/platform/product/node/product";
|
||||
import pkg from "vs/platform/product/node/package";
|
||||
|
||||
import { MainServer, WebviewServer } from "vs/server/src/server";
|
||||
import "vs/server/src/tar";
|
||||
import { generateCertificate } from "vs/server/src/util";
|
||||
|
||||
interface Args extends ParsedArgs {
|
||||
"allow-http"?: boolean;
|
||||
cert?: string;
|
||||
"cert-key"?: string;
|
||||
"extra-builtin-extensions-dir"?: string;
|
||||
"extra-extensions-dir"?: string;
|
||||
host?: string;
|
||||
"no-auth"?: boolean;
|
||||
open?: string;
|
||||
port?: string;
|
||||
socket?: string;
|
||||
"webview-port"?: string;
|
||||
"webview-socket"?: string;
|
||||
}
|
||||
|
||||
// The last item is _ which is like -- so our options need to come before it.
|
||||
const last = options.pop()!;
|
||||
|
||||
// Remove options that won't work or don't make sense.
|
||||
let i = options.length;
|
||||
while (i--) {
|
||||
switch (options[i].id) {
|
||||
case "add":
|
||||
case "diff":
|
||||
case "file-uri":
|
||||
case "folder-uri":
|
||||
case "goto":
|
||||
case "new-window":
|
||||
case "reuse-window":
|
||||
case "wait":
|
||||
case "disable-gpu":
|
||||
// TODO: pretty sure these don't work but not 100%.
|
||||
case "max-memory":
|
||||
case "prof-startup":
|
||||
case "inspect-extensions":
|
||||
case "inspect-brk-extensions":
|
||||
options.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
options.push({ id: "allow-http", type: "boolean", cat: "o", description: "Allow http connections." });
|
||||
options.push({ id: "cert", type: "string", cat: "o", description: "Path to certificate." });
|
||||
options.push({ id: "cert-key", type: "string", cat: "o", description: "Path to certificate key." });
|
||||
options.push({ id: "extra-builtin-extensions-dir", type: "string", cat: "o", description: "Path to extra builtin extension directory." });
|
||||
options.push({ id: "extra-extensions-dir", type: "string", cat: "o", description: "Path to extra user extension directory." });
|
||||
options.push({ id: "host", type: "string", cat: "o", description: "Host for the main and webview servers." });
|
||||
options.push({ id: "no-auth", type: "string", cat: "o", description: "Disable password authentication." });
|
||||
options.push({ id: "open", type: "boolean", cat: "o", description: "Open in the browser on startup." });
|
||||
options.push({ id: "port", type: "string", cat: "o", description: "Port for the main server." });
|
||||
options.push({ id: "socket", type: "string", cat: "o", description: "Listen on a socket instead of host:port." });
|
||||
options.push({ id: "webview-port", type: "string", cat: "o", description: "Port for the webview server." });
|
||||
options.push({ id: "webview-socket", type: "string", cat: "o", description: "Listen on a socket instead of host:port." });
|
||||
|
||||
options.push(last);
|
||||
|
||||
interface IMainCli {
|
||||
main: (argv: ParsedArgs) => Promise<void>;
|
||||
}
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
const args = validatePaths(parseMainProcessArgv(process.argv)) as Args;
|
||||
|
||||
if (!product.extensionsGallery) {
|
||||
product.extensionsGallery = {
|
||||
serviceUrl: process.env.SERVICE_URL || "https://v1.extapi.coder.com",
|
||||
itemUrl: process.env.ITEM_URL || "",
|
||||
controlUrl: "",
|
||||
recommendationsUrl: "",
|
||||
};
|
||||
}
|
||||
|
||||
const version = `${(pkg as any).codeServerVersion || "development"}-vsc${pkg.version}`;
|
||||
if (args.help) {
|
||||
const executable = `${product.applicationName}${os.platform() === "win32" ? ".exe" : ""}`;
|
||||
return console.log(buildHelpMessage(
|
||||
product.nameLong, executable,
|
||||
version,
|
||||
undefined,
|
||||
false,
|
||||
));
|
||||
}
|
||||
|
||||
if (args.version) {
|
||||
return console.log(buildVersionMessage(version, product.commit));
|
||||
}
|
||||
|
||||
const shouldSpawnCliProcess = (): boolean => {
|
||||
return !!args["install-source"]
|
||||
|| !!args["list-extensions"]
|
||||
|| !!args["install-extension"]
|
||||
|| !!args["uninstall-extension"]
|
||||
|| !!args["locate-extension"]
|
||||
|| !!args["telemetry"];
|
||||
};
|
||||
|
||||
if (shouldSpawnCliProcess()) {
|
||||
const cli = await new Promise<IMainCli>((c, e) => require(["vs/code/node/cliProcessMain"], c, e));
|
||||
await cli.main(args);
|
||||
// There is some WriteStream instance keeping it open so force an exit.
|
||||
return process.exit(0);
|
||||
}
|
||||
|
||||
const options = {
|
||||
host: args["host"]
|
||||
|| (args["no-auth"] || args["allow-http"] ? "localhost" : "0.0.0.0"),
|
||||
allowHttp: args["allow-http"],
|
||||
cert: args["cert"],
|
||||
certKey: args["cert"],
|
||||
};
|
||||
|
||||
if (!options.allowHttp && (!options.cert || !options.certKey)) {
|
||||
const { cert, certKey } = await generateCertificate();
|
||||
options.cert = cert;
|
||||
options.certKey = certKey;
|
||||
}
|
||||
|
||||
const webviewPort = typeof args["webview-port"] !== "undefined"
|
||||
&& parseInt(args["webview-port"], 10) || 8444;
|
||||
const webviewServer = new WebviewServer({
|
||||
...options,
|
||||
port: webviewPort,
|
||||
socket: args["webview-socket"],
|
||||
});
|
||||
|
||||
const port = typeof args.port !== "undefined" && parseInt(args.port, 10) || 8443;
|
||||
const server = new MainServer({
|
||||
...options,
|
||||
port,
|
||||
socket: args.socket,
|
||||
}, webviewServer, args);
|
||||
|
||||
const [webviewAddress, serverAddress] = await Promise.all([
|
||||
webviewServer.listen(),
|
||||
server.listen()
|
||||
]);
|
||||
console.log(`Main server listening on ${serverAddress}`);
|
||||
console.log(`Webview server listening on ${webviewAddress}`);
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
177
src/connection.ts
Normal file
177
src/connection.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import * as cp from "child_process";
|
||||
|
||||
import { getPathFromAmdModule } from "vs/base/common/amd";
|
||||
import { VSBuffer } from "vs/base/common/buffer";
|
||||
import { Emitter } from "vs/base/common/event";
|
||||
import { ISocket } from "vs/base/parts/ipc/common/ipc.net";
|
||||
import { NodeSocket, WebSocketNodeSocket } from "vs/base/parts/ipc/node/ipc.net";
|
||||
import { ILogService } from "vs/platform/log/common/log";
|
||||
import { IExtHostReadyMessage, IExtHostSocketMessage } from "vs/workbench/services/extensions/common/extensionHostProtocol";
|
||||
|
||||
import { Protocol } from "vs/server/src/protocol";
|
||||
import { uriTransformerPath } from "vs/server/src/util";
|
||||
|
||||
export abstract class Connection {
|
||||
private readonly _onClose = new Emitter<void>();
|
||||
public readonly onClose = this._onClose.event;
|
||||
|
||||
private timeout: NodeJS.Timeout | undefined;
|
||||
private readonly wait = 1000 * 60;
|
||||
|
||||
private closed: boolean = false;
|
||||
|
||||
public constructor(protected protocol: Protocol) {
|
||||
// onClose seems to mean we want to disconnect, so close immediately.
|
||||
protocol.onClose(() => this.close());
|
||||
|
||||
// If the socket closes, we want to wait before closing so we can
|
||||
// reconnect in the meantime.
|
||||
protocol.onSocketClose(() => {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.close();
|
||||
}, this.wait);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the connection on a new socket.
|
||||
*/
|
||||
public reconnect(protocol: Protocol, buffer: VSBuffer): void {
|
||||
if (this.closed) {
|
||||
throw new Error("Cannot reconnect to closed connection");
|
||||
}
|
||||
clearTimeout(this.timeout as any); // Not sure why the type doesn't work.
|
||||
this.protocol = protocol;
|
||||
this.connect(protocol.getSocket(), buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close and clean up connection. This will also kill the socket the
|
||||
* connection is on. Probably not safe to reconnect once this has happened.
|
||||
*/
|
||||
protected close(): void {
|
||||
if (!this.closed) {
|
||||
this.closed = true;
|
||||
this.protocol.sendDisconnect();
|
||||
this.dispose();
|
||||
this.protocol.dispose();
|
||||
this._onClose.fire();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the connection.
|
||||
*/
|
||||
protected abstract dispose(): void;
|
||||
|
||||
/**
|
||||
* Connect to a new socket.
|
||||
*/
|
||||
protected abstract connect(socket: ISocket, buffer: VSBuffer): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for all the IPC channels.
|
||||
*/
|
||||
export class ManagementConnection extends Connection {
|
||||
protected dispose(): void {
|
||||
// Nothing extra to do here.
|
||||
}
|
||||
|
||||
protected connect(socket: ISocket, buffer: VSBuffer): void {
|
||||
this.protocol.beginAcceptReconnection(socket, buffer);
|
||||
this.protocol.endAcceptReconnection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage the extension host process.
|
||||
*/
|
||||
export class ExtensionHostConnection extends Connection {
|
||||
private process: cp.ChildProcess;
|
||||
|
||||
public constructor(protocol: Protocol, private readonly log: ILogService) {
|
||||
super(protocol);
|
||||
const socket = this.protocol.getSocket();
|
||||
const buffer = this.protocol.readEntireBuffer();
|
||||
this.process = this.spawn(socket, buffer);
|
||||
}
|
||||
|
||||
protected dispose(): void {
|
||||
this.process.kill();
|
||||
}
|
||||
|
||||
protected connect(socket: ISocket, buffer: VSBuffer): void {
|
||||
this.sendInitMessage(socket, buffer);
|
||||
}
|
||||
|
||||
private sendInitMessage(nodeSocket: ISocket, buffer: VSBuffer): void {
|
||||
const socket = nodeSocket instanceof NodeSocket
|
||||
? nodeSocket.socket
|
||||
: (nodeSocket as WebSocketNodeSocket).socket.socket;
|
||||
|
||||
socket.pause();
|
||||
|
||||
const initMessage: IExtHostSocketMessage = {
|
||||
type: "VSCODE_EXTHOST_IPC_SOCKET",
|
||||
initialDataChunk: (buffer.buffer as Buffer).toString("base64"),
|
||||
skipWebSocketFrames: nodeSocket instanceof NodeSocket,
|
||||
};
|
||||
|
||||
this.process.send(initMessage, socket);
|
||||
}
|
||||
|
||||
private spawn(socket: ISocket, buffer: VSBuffer): cp.ChildProcess {
|
||||
const proc = cp.fork(
|
||||
getPathFromAmdModule(require, "bootstrap-fork"),
|
||||
[
|
||||
"--type=extensionHost",
|
||||
`--uriTransformerPath=${uriTransformerPath()}`
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
AMD_ENTRYPOINT: "vs/workbench/services/extensions/node/extensionHostProcess",
|
||||
PIPE_LOGGING: "true",
|
||||
VERBOSE_LOGGING: "true",
|
||||
VSCODE_EXTHOST_WILL_SEND_SOCKET: "true",
|
||||
VSCODE_HANDLES_UNCAUGHT_ERRORS: "true",
|
||||
VSCODE_LOG_STACK: "false",
|
||||
},
|
||||
silent: true,
|
||||
},
|
||||
);
|
||||
|
||||
proc.on("error", (error) => {
|
||||
console.error(error);
|
||||
this.close();
|
||||
});
|
||||
|
||||
proc.on("exit", (code, signal) => {
|
||||
console.error("Extension host exited", { code, signal });
|
||||
this.close();
|
||||
});
|
||||
|
||||
proc.stdout.setEncoding("utf8");
|
||||
proc.stderr.setEncoding("utf8");
|
||||
proc.stdout.on("data", (data) => this.log.info("Extension host stdout", data));
|
||||
proc.stderr.on("data", (data) => this.log.error("Extension host stderr", data));
|
||||
proc.on("message", (event) => {
|
||||
if (event && event.type === "__$console") {
|
||||
const severity = this.log[event.severity] ? event.severity : "info";
|
||||
this.log[severity]("Extension host", event.arguments);
|
||||
}
|
||||
});
|
||||
|
||||
const listen = (message: IExtHostReadyMessage) => {
|
||||
if (message.type === "VSCODE_EXTHOST_IPC_READY") {
|
||||
proc.removeListener("message", listen);
|
||||
this.sendInitMessage(socket, buffer);
|
||||
}
|
||||
};
|
||||
|
||||
proc.on("message", listen);
|
||||
|
||||
return proc;
|
||||
}
|
||||
}
|
170
src/insights.ts
Normal file
170
src/insights.ts
Normal file
@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Used by node
|
||||
*/
|
||||
import * as https from "https";
|
||||
import * as os from "os";
|
||||
|
||||
export const defaultClient = "filler";
|
||||
|
||||
export class TelemetryClient {
|
||||
public channel = {
|
||||
setUseDiskRetryCaching: (): void => undefined,
|
||||
};
|
||||
|
||||
public constructor() {
|
||||
//
|
||||
}
|
||||
|
||||
public trackEvent(options: {
|
||||
name: string;
|
||||
properties: object;
|
||||
measurements: object;
|
||||
}): void {
|
||||
if (!options.properties) {
|
||||
options.properties = {};
|
||||
}
|
||||
if (!options.measurements) {
|
||||
options.measurements = {};
|
||||
}
|
||||
|
||||
try {
|
||||
const cpus = os.cpus();
|
||||
// tslint:disable-next-line:no-any
|
||||
(options.measurements as any).cpu = {
|
||||
model: cpus[0].model,
|
||||
cores: cpus.length,
|
||||
};
|
||||
} catch (ex) {
|
||||
// Nothin
|
||||
}
|
||||
|
||||
try {
|
||||
// tslint:disable-next-line:no-any
|
||||
(options.measurements as any).memory = {
|
||||
virtual_free: os.freemem(),
|
||||
virtual_used: os.totalmem(),
|
||||
};
|
||||
} catch (ex) {
|
||||
//
|
||||
}
|
||||
|
||||
try {
|
||||
// tslint:disable:no-any
|
||||
(options.properties as any)["common.shell"] = os.userInfo().shell;
|
||||
(options.properties as any)["common.release"] = os.release();
|
||||
(options.properties as any)["common.arch"] = os.arch();
|
||||
// tslint:enable:no-any
|
||||
} catch (ex) {
|
||||
//
|
||||
}
|
||||
|
||||
try {
|
||||
// tslint:disable-next-line:no-any
|
||||
(options.properties as any)["common.machineId"] = machineIdSync();
|
||||
} catch (ex) {
|
||||
//
|
||||
}
|
||||
|
||||
try {
|
||||
const request = https.request({
|
||||
host: "v1.telemetry.coder.com",
|
||||
port: 443,
|
||||
path: "/track",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
request.on("error", () => {
|
||||
// Do nothing, we don"t really care
|
||||
});
|
||||
request.write(JSON.stringify(options));
|
||||
request.end();
|
||||
} catch (ex) {
|
||||
// Suppress all errs
|
||||
}
|
||||
}
|
||||
|
||||
public flush(options: {
|
||||
readonly callback: () => void;
|
||||
}): void {
|
||||
options.callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Taken from https://github.com/automation-stack/node-machine-id
|
||||
import { exec, execSync } from "child_process";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
const isWindowsProcessMixedOrNativeArchitecture = (): "" | "mixed" | "native" => {
|
||||
// detect if the node binary is the same arch as the Windows OS.
|
||||
// or if this is 32 bit node on 64 bit windows.
|
||||
if (process.platform !== "win32") {
|
||||
return "";
|
||||
}
|
||||
if (process.arch === "ia32" && process.env.hasOwnProperty("PROCESSOR_ARCHITEW6432")) {
|
||||
return "mixed";
|
||||
}
|
||||
|
||||
return "native";
|
||||
};
|
||||
|
||||
let { platform } = process,
|
||||
win32RegBinPath = {
|
||||
native: "%windir%\\System32",
|
||||
mixed: "%windir%\\sysnative\\cmd.exe /c %windir%\\System32",
|
||||
"": "",
|
||||
},
|
||||
guid = {
|
||||
darwin: "ioreg -rd1 -c IOPlatformExpertDevice",
|
||||
win32: `${win32RegBinPath[isWindowsProcessMixedOrNativeArchitecture()]}\\REG ` +
|
||||
"QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography " +
|
||||
"/v MachineGuid",
|
||||
linux: "( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname ) | head -n 1 || :",
|
||||
freebsd: "kenv -q smbios.system.uuid || sysctl -n kern.hostuuid",
|
||||
// tslint:disable-next-line:no-any
|
||||
} as any;
|
||||
|
||||
const hash = (guid: string): string => {
|
||||
return createHash("sha256").update(guid).digest("hex");
|
||||
};
|
||||
|
||||
const expose = (result: string): string => {
|
||||
switch (platform) {
|
||||
case "darwin":
|
||||
return result
|
||||
.split("IOPlatformUUID")[1]
|
||||
.split("\n")[0].replace(/\=|\s+|\"/ig, "")
|
||||
.toLowerCase();
|
||||
case "win32":
|
||||
return result
|
||||
.toString()
|
||||
.split("REG_SZ")[1]
|
||||
.replace(/\r+|\n+|\s+/ig, "")
|
||||
.toLowerCase();
|
||||
case "linux":
|
||||
return result
|
||||
.toString()
|
||||
.replace(/\r+|\n+|\s+/ig, "")
|
||||
.toLowerCase();
|
||||
case "freebsd":
|
||||
return result
|
||||
.toString()
|
||||
.replace(/\r+|\n+|\s+/ig, "")
|
||||
.toLowerCase();
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${process.platform}`);
|
||||
}
|
||||
};
|
||||
|
||||
let cachedMachineId: string | undefined;
|
||||
|
||||
const machineIdSync = (): string => {
|
||||
if (cachedMachineId) {
|
||||
return cachedMachineId;
|
||||
}
|
||||
let id: string = expose(execSync(guid[platform]).toString());
|
||||
cachedMachineId = hash(id);
|
||||
|
||||
return cachedMachineId;
|
||||
};
|
109
src/protocol.ts
Normal file
109
src/protocol.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import * as crypto from "crypto";
|
||||
import * as net from "net";
|
||||
|
||||
import { VSBuffer } from "vs/base/common/buffer";
|
||||
import { NodeSocket, WebSocketNodeSocket } from "vs/base/parts/ipc/node/ipc.net";
|
||||
import { PersistentProtocol } from "vs/base/parts/ipc/common/ipc.net";
|
||||
import { AuthRequest, ConnectionTypeRequest, HandshakeMessage } from "vs/platform/remote/common/remoteAgentConnection";
|
||||
|
||||
export interface SocketOptions {
|
||||
readonly reconnectionToken: string;
|
||||
readonly reconnection: boolean;
|
||||
readonly skipWebSocketFrames: boolean;
|
||||
}
|
||||
|
||||
export class Protocol extends PersistentProtocol {
|
||||
private disposed: boolean = false;
|
||||
|
||||
public constructor(
|
||||
secWebsocketKey: string,
|
||||
socket: net.Socket,
|
||||
public readonly options: SocketOptions,
|
||||
) {
|
||||
super(
|
||||
options.skipWebSocketFrames
|
||||
? new NodeSocket(socket)
|
||||
: new WebSocketNodeSocket(new NodeSocket(socket)),
|
||||
);
|
||||
socket.on("error", () => this.dispose());
|
||||
socket.on("end", () => this.dispose());
|
||||
|
||||
// This magic value is specified by the websocket spec.
|
||||
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
const reply = crypto.createHash("sha1")
|
||||
.update(secWebsocketKey + magic)
|
||||
.digest("base64");
|
||||
|
||||
socket.write([
|
||||
"HTTP/1.1 101 Switching Protocols",
|
||||
"Upgrade: websocket",
|
||||
"Connection: Upgrade",
|
||||
`Sec-WebSocket-Accept: ${reply}`,
|
||||
].join("\r\n") + "\r\n\r\n");
|
||||
}
|
||||
|
||||
public sendDisconnect(): void {
|
||||
if (!this.disposed) {
|
||||
super.sendDisconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(error?: Error): void {
|
||||
if (!this.disposed) {
|
||||
this.disposed = true;
|
||||
if (error) {
|
||||
this.sendMessage({ type: "error", reason: error.message });
|
||||
}
|
||||
super.dispose();
|
||||
this.getSocket().dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a handshake to get a connection request.
|
||||
*/
|
||||
public handshake(): Promise<ConnectionTypeRequest> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const handler = this.onControlMessage((rawMessage) => {
|
||||
try {
|
||||
const message = JSON.parse(rawMessage.toString());
|
||||
switch (message.type) {
|
||||
case "auth": return this.authenticate(message);
|
||||
case "connectionType":
|
||||
handler.dispose();
|
||||
return resolve(message);
|
||||
default: throw new Error("Unrecognized message type");
|
||||
}
|
||||
} catch (error) {
|
||||
handler.dispose();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: This ignores the authentication process entirely for now.
|
||||
*/
|
||||
private authenticate(_message: AuthRequest): void {
|
||||
this.sendMessage({
|
||||
type: "sign",
|
||||
data: "",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: implement.
|
||||
*/
|
||||
public tunnel(): void {
|
||||
throw new Error("Tunnel is not implemented yet");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a handshake message. In the case of the extension host, it just sends
|
||||
* back a debug port.
|
||||
*/
|
||||
public sendMessage(message: HandshakeMessage | { debugPort?: number } ): void {
|
||||
this.sendControl(VSBuffer.fromString(JSON.stringify(message)));
|
||||
}
|
||||
}
|
458
src/server.ts
Normal file
458
src/server.ts
Normal file
@ -0,0 +1,458 @@
|
||||
import * as fs from "fs";
|
||||
import * as http from "http";
|
||||
import * as https from "https";
|
||||
import * as net from "net";
|
||||
import * as path from "path";
|
||||
import * as tls from "tls";
|
||||
import * as util from "util";
|
||||
import * as url from "url";
|
||||
|
||||
import { Emitter } from "vs/base/common/event";
|
||||
import { sanitizeFilePath } from "vs/base/common/extpath";
|
||||
import { getMediaMime } from "vs/base/common/mime";
|
||||
import { extname } from "vs/base/common/path";
|
||||
import { UriComponents, URI } from "vs/base/common/uri";
|
||||
import { IPCServer, ClientConnectionEvent, StaticRouter } from "vs/base/parts/ipc/common/ipc";
|
||||
import { mkdirp } from "vs/base/node/pfs";
|
||||
import { LogsDataCleaner } from "vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner";
|
||||
import { IConfigurationService } from "vs/platform/configuration/common/configuration";
|
||||
import { ConfigurationService } from "vs/platform/configuration/node/configurationService";
|
||||
import { IDialogService } from "vs/platform/dialogs/common/dialogs";
|
||||
import { DialogChannelClient } from "vs/platform/dialogs/node/dialogIpc";
|
||||
import { IEnvironmentService, ParsedArgs } from "vs/platform/environment/common/environment";
|
||||
import { EnvironmentService } from "vs/platform/environment/node/environmentService";
|
||||
import { IExtensionManagementService, IExtensionGalleryService } from "vs/platform/extensionManagement/common/extensionManagement";
|
||||
import { ExtensionGalleryChannel } from "vs/platform/extensionManagement/node/extensionGalleryIpc";
|
||||
import { ExtensionGalleryService } from "vs/platform/extensionManagement/node/extensionGalleryService";
|
||||
import { ExtensionManagementChannel } from "vs/platform/extensionManagement/node/extensionManagementIpc";
|
||||
import { ExtensionManagementService } from "vs/platform/extensionManagement/node/extensionManagementService";
|
||||
import { SyncDescriptor } from "vs/platform/instantiation/common/descriptors";
|
||||
import { InstantiationService } from "vs/platform/instantiation/common/instantiationService";
|
||||
import { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection";
|
||||
import { ILocalizationsService } from "vs/platform/localizations/common/localizations";
|
||||
import { LocalizationsService } from "vs/platform/localizations/node/localizations";
|
||||
import { getLogLevel, ILogService } from "vs/platform/log/common/log";
|
||||
import { LogLevelSetterChannel } from "vs/platform/log/common/logIpc";
|
||||
import { SpdLogService } from "vs/platform/log/node/spdlogService";
|
||||
import { IProductConfiguration } from "vs/platform/product/common/product";
|
||||
import product from "vs/platform/product/node/product";
|
||||
import { ConnectionType, ConnectionTypeRequest } from "vs/platform/remote/common/remoteAgentConnection";
|
||||
import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from "vs/platform/remote/common/remoteAgentFileSystemChannel";
|
||||
import { IRequestService } from "vs/platform/request/node/request";
|
||||
import { RequestService } from "vs/platform/request/node/requestService";
|
||||
import { ITelemetryService } from "vs/platform/telemetry/common/telemetry";
|
||||
import { NullTelemetryService } from "vs/platform/telemetry/common/telemetryUtils";
|
||||
import { RemoteExtensionLogFileName } from "vs/workbench/services/remote/common/remoteAgentService";
|
||||
// import { TelemetryService } from "vs/workbench/services/telemetry/electron-browser/telemetryService";
|
||||
import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api";
|
||||
|
||||
import { Connection, ManagementConnection, ExtensionHostConnection } from "vs/server/src/connection";
|
||||
import { ExtensionEnvironmentChannel, FileProviderChannel , } from "vs/server/src/channel";
|
||||
import { Protocol } from "vs/server/src/protocol";
|
||||
import { getUriTransformer, useHttpsTransformer } from "vs/server/src/util";
|
||||
|
||||
export enum HttpCode {
|
||||
Ok = 200,
|
||||
NotFound = 404,
|
||||
BadRequest = 400,
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
WORKBENCH_WEB_CONGIGURATION: IWorkbenchConstructionOptions;
|
||||
REMOTE_USER_DATA_URI: UriComponents | URI;
|
||||
PRODUCT_CONFIGURATION: IProductConfiguration | null;
|
||||
CONNECTION_AUTH_TOKEN: string;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
content?: string | Buffer;
|
||||
code?: number;
|
||||
headers: http.OutgoingHttpHeaders;
|
||||
}
|
||||
|
||||
export class HttpError extends Error {
|
||||
public constructor(message: string, public readonly code: number) {
|
||||
super(message);
|
||||
// @ts-ignore
|
||||
this.name = this.constructor.name;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServerOptions {
|
||||
readonly port: number;
|
||||
readonly host: string;
|
||||
readonly socket?: string;
|
||||
readonly allowHttp?: boolean;
|
||||
readonly cert?: string;
|
||||
readonly certKey?: string;
|
||||
}
|
||||
|
||||
export abstract class Server {
|
||||
// The underlying web server.
|
||||
protected readonly server: http.Server | https.Server;
|
||||
|
||||
protected rootPath = path.resolve(__dirname, "../../..");
|
||||
|
||||
private listenPromise: Promise<string> | undefined;
|
||||
|
||||
public constructor(private readonly options: ServerOptions) {
|
||||
if (this.options.cert && this.options.certKey) {
|
||||
useHttpsTransformer();
|
||||
const httpolyglot = require.__$__nodeRequire(path.resolve(__dirname, "../node_modules/httpolyglot/lib/index")) as typeof import("httpolyglot");
|
||||
this.server = httpolyglot.createServer({
|
||||
cert: fs.readFileSync(this.options.cert),
|
||||
key: fs.readFileSync(this.options.certKey),
|
||||
}, this.onRequest);
|
||||
} else {
|
||||
this.server = http.createServer(this.onRequest);
|
||||
}
|
||||
}
|
||||
|
||||
public listen(): Promise<string> {
|
||||
if (!this.listenPromise) {
|
||||
this.listenPromise = new Promise((resolve, reject) => {
|
||||
this.server.on("error", reject);
|
||||
const onListen = () => resolve(this.address(this.server, this.options.allowHttp));
|
||||
if (this.options.socket) {
|
||||
this.server.listen(this.options.socket, onListen);
|
||||
} else {
|
||||
this.server.listen(this.options.port, this.options.host, onListen);
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.listenPromise;
|
||||
}
|
||||
|
||||
protected abstract handleRequest(
|
||||
base: string,
|
||||
requestPath: string,
|
||||
parsedUrl: url.UrlWithParsedQuery,
|
||||
request: http.IncomingMessage,
|
||||
): Promise<Response>;
|
||||
|
||||
protected async getResource(filePath: string): Promise<Response> {
|
||||
const content = await util.promisify(fs.readFile)(filePath);
|
||||
return {
|
||||
content,
|
||||
headers: {
|
||||
"Content-Type": getMediaMime(filePath) || {
|
||||
".css": "text/css",
|
||||
".html": "text/html",
|
||||
".js": "text/javascript",
|
||||
".json": "application/json",
|
||||
}[extname(filePath)] || "text/plain",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
|
||||
const secure = (request.connection as tls.TLSSocket).encrypted;
|
||||
if (!this.options.allowHttp && !secure) {
|
||||
response.writeHead(302, {
|
||||
Location: "https://" + request.headers.host + request.url,
|
||||
});
|
||||
return response.end();
|
||||
}
|
||||
|
||||
try {
|
||||
if (request.method !== "GET") {
|
||||
throw new HttpError(
|
||||
`Unsupported method ${request.method}`,
|
||||
HttpCode.BadRequest,
|
||||
);
|
||||
}
|
||||
|
||||
const parsedUrl = url.parse(request.url || "", true);
|
||||
|
||||
const fullPath = decodeURIComponent(parsedUrl.pathname || "/");
|
||||
const match = fullPath.match(/^(\/?[^/]*)(.*)$/);
|
||||
const [, base, requestPath] = match
|
||||
? match.map((p) => p !== "/" ? p.replace(/\/$/, "") : p)
|
||||
: ["", "", ""];
|
||||
|
||||
const { content, headers, code } = await this.handleRequest(
|
||||
base, requestPath, parsedUrl, request,
|
||||
);
|
||||
response.writeHead(code || HttpCode.Ok, {
|
||||
"Cache-Control": "max-age=86400",
|
||||
// TODO: ETag?
|
||||
...headers,
|
||||
});
|
||||
response.end(content);
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
||||
error = new HttpError("Not found", HttpCode.NotFound);
|
||||
}
|
||||
response.writeHead(typeof error.code === "number" ? error.code : 500);
|
||||
response.end(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
private address(server: net.Server, http?: boolean): string {
|
||||
const address = server.address();
|
||||
const endpoint = typeof address !== "string"
|
||||
? ((address.address === "::" ? "localhost" : address.address) + ":" + address.port)
|
||||
: address;
|
||||
return `${http ? "http" : "https"}://${endpoint}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class MainServer extends Server {
|
||||
// Used to notify the IPC server that there is a new client.
|
||||
public readonly _onDidClientConnect = new Emitter<ClientConnectionEvent>();
|
||||
public readonly onDidClientConnect = this._onDidClientConnect.event;
|
||||
|
||||
// This is separate instead of just extending this class since we can't
|
||||
// use properties in the super call. This manages channels.
|
||||
private readonly ipc = new IPCServer(this.onDidClientConnect);
|
||||
|
||||
// Persistent connections. These can reconnect within a timeout.
|
||||
private readonly connections = new Map<ConnectionType, Map<string, Connection>>();
|
||||
|
||||
private readonly services = new ServiceCollection();
|
||||
|
||||
public constructor(
|
||||
options: ServerOptions,
|
||||
private readonly webviewServer: WebviewServer,
|
||||
args: ParsedArgs,
|
||||
) {
|
||||
super(options);
|
||||
|
||||
this.server.on("upgrade", async (request, socket) => {
|
||||
const protocol = this.createProtocol(request, socket);
|
||||
try {
|
||||
await this.connect(await protocol.handshake(), protocol);
|
||||
} catch (error) {
|
||||
protocol.dispose(error);
|
||||
}
|
||||
});
|
||||
|
||||
const environmentService = new EnvironmentService(args, process.execPath);
|
||||
const logService = new SpdLogService(RemoteExtensionLogFileName, environmentService.logsPath, getLogLevel(environmentService));
|
||||
this.ipc.registerChannel("loglevel", new LogLevelSetterChannel(logService));
|
||||
|
||||
const router = new StaticRouter((context: any) => {
|
||||
return context.clientId === "renderer";
|
||||
});
|
||||
|
||||
this.services.set(ILogService, logService);
|
||||
this.services.set(IEnvironmentService, environmentService);
|
||||
this.services.set(IConfigurationService, new SyncDescriptor(ConfigurationService, [environmentService.machineSettingsResource]));
|
||||
this.services.set(IRequestService, new SyncDescriptor(RequestService));
|
||||
this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService));
|
||||
this.services.set(ITelemetryService, NullTelemetryService); // TODO: telemetry
|
||||
this.services.set(IDialogService, new DialogChannelClient(this.ipc.getChannel("dialog", router)));
|
||||
this.services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService));
|
||||
|
||||
const instantiationService = new InstantiationService(this.services);
|
||||
|
||||
this.services.set(ILocalizationsService, instantiationService.createInstance(LocalizationsService));
|
||||
|
||||
instantiationService.invokeFunction(() => {
|
||||
instantiationService.createInstance(LogsDataCleaner);
|
||||
this.ipc.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new FileProviderChannel(logService));
|
||||
this.ipc.registerChannel("remoteextensionsenvironment", new ExtensionEnvironmentChannel(environmentService, logService));
|
||||
const extensionsService = this.services.get(IExtensionManagementService) as IExtensionManagementService;
|
||||
const extensionsChannel = new ExtensionManagementChannel(extensionsService, (context) => getUriTransformer(context.remoteAuthority));
|
||||
this.ipc.registerChannel("extensions", extensionsChannel);
|
||||
const galleryService = this.services.get(IExtensionGalleryService) as IExtensionGalleryService;
|
||||
const galleryChannel = new ExtensionGalleryChannel(galleryService);
|
||||
this.ipc.registerChannel("gallery", galleryChannel);
|
||||
});
|
||||
}
|
||||
|
||||
public async listen(): Promise<string> {
|
||||
const environment = (this.services.get(IEnvironmentService) as EnvironmentService);
|
||||
const mkdirs = Promise.all([
|
||||
environment.extensionsPath,
|
||||
].map((p) => mkdirp(p)));
|
||||
const [address] = await Promise.all([
|
||||
super.listen(),
|
||||
mkdirs,
|
||||
]);
|
||||
return address;
|
||||
}
|
||||
|
||||
protected async handleRequest(
|
||||
base: string,
|
||||
requestPath: string,
|
||||
parsedUrl: url.UrlWithParsedQuery,
|
||||
request: http.IncomingMessage,
|
||||
): Promise<Response> {
|
||||
switch (base) {
|
||||
case "/":
|
||||
return this.getRoot(request, parsedUrl);
|
||||
case "/node_modules":
|
||||
case "/out":
|
||||
return this.getResource(path.join(this.rootPath, base, requestPath));
|
||||
// TODO: this setup means you can't request anything from the root if it
|
||||
// starts with /node_modules or /out, although that's probably low risk.
|
||||
// There doesn't seem to be a really good way to solve this since some
|
||||
// resources are requested by the browser (like the extension icon) and
|
||||
// some by the file provider (like the extension README). Maybe add a
|
||||
// /resource prefix and a file provider that strips that prefix?
|
||||
default:
|
||||
return this.getResource(path.join(base, requestPath));
|
||||
}
|
||||
}
|
||||
|
||||
private async getRoot(request: http.IncomingMessage, parsedUrl: url.UrlWithParsedQuery): Promise<Response> {
|
||||
const htmlPath = path.join(
|
||||
this.rootPath,
|
||||
'out/vs/code/browser/workbench/workbench.html',
|
||||
);
|
||||
|
||||
let content = await util.promisify(fs.readFile)(htmlPath, "utf8");
|
||||
|
||||
const remoteAuthority = request.headers.host as string;
|
||||
const transformer = getUriTransformer(remoteAuthority);
|
||||
|
||||
const webviewEndpoint = await this.webviewServer.listen();
|
||||
|
||||
const cwd = process.env.VSCODE_CWD || process.cwd();
|
||||
const workspacePath = parsedUrl.query.workspace as string | undefined;
|
||||
const folderPath = !workspacePath ? parsedUrl.query.folder as string | undefined || cwd: undefined;
|
||||
|
||||
const options: Options = {
|
||||
WORKBENCH_WEB_CONGIGURATION: {
|
||||
workspaceUri: workspacePath
|
||||
? transformer.transformOutgoing(URI.file(sanitizeFilePath(workspacePath, cwd)))
|
||||
: undefined,
|
||||
folderUri: folderPath
|
||||
? transformer.transformOutgoing(URI.file(sanitizeFilePath(folderPath, cwd)))
|
||||
: undefined,
|
||||
remoteAuthority,
|
||||
webviewEndpoint,
|
||||
},
|
||||
REMOTE_USER_DATA_URI: transformer.transformOutgoing(
|
||||
(this.services.get(IEnvironmentService) as EnvironmentService).webUserDataHome,
|
||||
),
|
||||
PRODUCT_CONFIGURATION: product,
|
||||
CONNECTION_AUTH_TOKEN: "",
|
||||
};
|
||||
|
||||
Object.keys(options).forEach((key) => {
|
||||
content = content.replace(`"{{${key}}}"`, `'${JSON.stringify(options[key])}'`);
|
||||
});
|
||||
|
||||
content = content.replace('{{WEBVIEW_ENDPOINT}}', webviewEndpoint);
|
||||
|
||||
return {
|
||||
content,
|
||||
headers: {
|
||||
"Content-Type": "text/html",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private createProtocol(request: http.IncomingMessage, socket: net.Socket): Protocol {
|
||||
if (request.headers.upgrade !== "websocket") {
|
||||
throw new Error("HTTP/1.1 400 Bad Request");
|
||||
}
|
||||
|
||||
const options = {
|
||||
reconnectionToken: "",
|
||||
reconnection: false,
|
||||
skipWebSocketFrames: false,
|
||||
};
|
||||
|
||||
if (request.url) {
|
||||
const query = url.parse(request.url, true).query;
|
||||
if (query.reconnectionToken) {
|
||||
options.reconnectionToken = query.reconnectionToken as string;
|
||||
}
|
||||
if (query.reconnection === "true") {
|
||||
options.reconnection = true;
|
||||
}
|
||||
if (query.skipWebSocketFrames === "true") {
|
||||
options.skipWebSocketFrames = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new Protocol(
|
||||
request.headers["sec-websocket-key"] as string,
|
||||
socket,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
private async connect(message: ConnectionTypeRequest, protocol: Protocol): Promise<void> {
|
||||
switch (message.desiredConnectionType) {
|
||||
case ConnectionType.ExtensionHost:
|
||||
case ConnectionType.Management:
|
||||
const debugPort = await this.getDebugPort();
|
||||
const ok = message.desiredConnectionType === ConnectionType.ExtensionHost
|
||||
? (debugPort ? { debugPort } : {})
|
||||
: { type: "ok" };
|
||||
|
||||
if (!this.connections.has(message.desiredConnectionType)) {
|
||||
this.connections.set(message.desiredConnectionType, new Map());
|
||||
}
|
||||
|
||||
const connections = this.connections.get(message.desiredConnectionType)!;
|
||||
const token = protocol.options.reconnectionToken;
|
||||
|
||||
if (protocol.options.reconnection && connections.has(token)) {
|
||||
protocol.sendMessage(ok);
|
||||
const buffer = protocol.readEntireBuffer();
|
||||
protocol.dispose();
|
||||
return connections.get(token)!.reconnect(protocol, buffer);
|
||||
}
|
||||
|
||||
if (protocol.options.reconnection || connections.has(token)) {
|
||||
throw new Error(protocol.options.reconnection
|
||||
? "Unrecognized reconnection token"
|
||||
: "Duplicate reconnection token"
|
||||
);
|
||||
}
|
||||
|
||||
protocol.sendMessage(ok);
|
||||
|
||||
let connection: Connection;
|
||||
if (message.desiredConnectionType === ConnectionType.Management) {
|
||||
connection = new ManagementConnection(protocol);
|
||||
this._onDidClientConnect.fire({
|
||||
protocol,
|
||||
onDidClientDisconnect: connection.onClose,
|
||||
});
|
||||
} else {
|
||||
connection = new ExtensionHostConnection(
|
||||
protocol, this.services.get(ILogService) as ILogService,
|
||||
);
|
||||
}
|
||||
connections.set(protocol.options.reconnectionToken, connection);
|
||||
connection.onClose(() => {
|
||||
connections.delete(protocol.options.reconnectionToken);
|
||||
});
|
||||
break;
|
||||
case ConnectionType.Tunnel: return protocol.tunnel();
|
||||
default: throw new Error("Unrecognized connection type");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: implement.
|
||||
*/
|
||||
private async getDebugPort(): Promise<number | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class WebviewServer extends Server {
|
||||
protected async handleRequest(
|
||||
base: string,
|
||||
requestPath: string,
|
||||
): Promise<Response> {
|
||||
const webviewPath = path.join(
|
||||
this.rootPath,
|
||||
"out/vs/workbench/contrib/webview/browser/pre",
|
||||
);
|
||||
|
||||
if (base === "/") {
|
||||
base = "/index.html";
|
||||
}
|
||||
|
||||
return this.getResource(path.join(webviewPath, base, requestPath));
|
||||
}
|
||||
}
|
224
src/tar.ts
Normal file
224
src/tar.ts
Normal file
@ -0,0 +1,224 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as tarStream from "tar-stream";
|
||||
import { promisify } from "util";
|
||||
|
||||
import * as nls from "vs/nls";
|
||||
import * as vszip from "vs/base/node/zip";
|
||||
import { CancellationToken } from "vs/base/common/cancellation";
|
||||
import { mkdirp } from "vs/base/node/pfs";
|
||||
|
||||
// We will be overriding these, so keep a reference to the original.
|
||||
const vszipExtract = vszip.extract;
|
||||
const vszipBuffer = vszip.buffer;
|
||||
|
||||
export interface IExtractOptions {
|
||||
overwrite?: boolean;
|
||||
|
||||
/**
|
||||
* Source path within the TAR/ZIP archive. Only the files
|
||||
* contained in this path will be extracted.
|
||||
*/
|
||||
sourcePath?: string;
|
||||
}
|
||||
|
||||
export interface IFile {
|
||||
path: string;
|
||||
contents?: Buffer | string;
|
||||
localPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the standard VS Code behavior for zipping extensions to use the TAR
|
||||
* format instead of ZIP.
|
||||
*/
|
||||
export const zip = (tarPath: string, files: IFile[]): Promise<string> => {
|
||||
return new Promise<string>((c, e): void => {
|
||||
const pack = tarStream.pack();
|
||||
const chunks: Buffer[] = [];
|
||||
const ended = new Promise<Buffer>((res): void => {
|
||||
pack.on("end", () => {
|
||||
res(Buffer.concat(chunks));
|
||||
});
|
||||
});
|
||||
pack.on("data", (chunk) => {
|
||||
chunks.push(chunk as Buffer);
|
||||
});
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
pack.entry({
|
||||
name: file.path,
|
||||
}, file.contents);
|
||||
}
|
||||
pack.finalize();
|
||||
|
||||
ended.then((buffer) => {
|
||||
return promisify(fs.writeFile)(tarPath, buffer);
|
||||
}).then(() => {
|
||||
c(tarPath);
|
||||
}).catch((ex) => {
|
||||
e(ex);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Override the standard VS Code behavior for extracting archives to first
|
||||
* attempt to process the archive as a TAR and then fall back to the original
|
||||
* implementation for processing ZIPs.
|
||||
*/
|
||||
export const extract = (archivePath: string, extractPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> => {
|
||||
return new Promise<void>((c, e): void => {
|
||||
extractTar(archivePath, extractPath, options, token).then(c).catch((ex) => {
|
||||
if (!ex.toString().includes("Invalid tar header")) {
|
||||
e(ex);
|
||||
|
||||
return;
|
||||
}
|
||||
vszipExtract(archivePath, extractPath, options, token).then(c).catch(e);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Override the standard VS Code behavior for buffering archives to first
|
||||
* process the Buffer as a TAR and then fall back to the original
|
||||
* implementation for processing ZIPs.
|
||||
*/
|
||||
export const buffer = (targetPath: string, filePath: string): Promise<Buffer> => {
|
||||
return new Promise<Buffer>((c, e): void => {
|
||||
let done: boolean = false;
|
||||
extractAssets(targetPath, new RegExp(filePath), (assetPath: string, data: Buffer) => {
|
||||
if (path.normalize(assetPath) === path.normalize(filePath)) {
|
||||
done = true;
|
||||
c(data);
|
||||
}
|
||||
}).then(() => {
|
||||
if (!done) {
|
||||
e("couldn't find asset " + filePath);
|
||||
}
|
||||
}).catch((ex) => {
|
||||
if (!ex.toString().includes("Invalid tar header")) {
|
||||
e(ex);
|
||||
|
||||
return;
|
||||
}
|
||||
vszipBuffer(targetPath, filePath).then(c).catch(e);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Override the standard VS Code behavior for extracting assets from archive
|
||||
* Buffers to use the TAR format instead of ZIP.
|
||||
*/
|
||||
const extractAssets = (tarPath: string, match: RegExp, callback: (path: string, data: Buffer) => void): Promise<void> => {
|
||||
return new Promise<void>(async (c, e): Promise<void> => {
|
||||
try {
|
||||
const buffer = await promisify(fs.readFile)(tarPath);
|
||||
const extractor = tarStream.extract();
|
||||
extractor.once("error", e);
|
||||
extractor.on("entry", (header, stream, next) => {
|
||||
const name = header.name;
|
||||
if (match.test(name)) {
|
||||
extractData(stream).then((data) => {
|
||||
callback(name, data);
|
||||
next();
|
||||
}).catch(e);
|
||||
stream.resume();
|
||||
} else {
|
||||
stream.on("end", () => {
|
||||
next();
|
||||
});
|
||||
stream.resume();
|
||||
}
|
||||
});
|
||||
extractor.on("finish", () => {
|
||||
c();
|
||||
});
|
||||
extractor.write(buffer);
|
||||
extractor.end();
|
||||
} catch (ex) {
|
||||
e(ex);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const extractData = (stream: NodeJS.ReadableStream): Promise<Buffer> => {
|
||||
return new Promise<Buffer>((c, e): void => {
|
||||
const fileData: Buffer[] = [];
|
||||
stream.on("data", (data) => fileData.push(data));
|
||||
stream.on("end", () => {
|
||||
const fd = Buffer.concat(fileData);
|
||||
c(fd);
|
||||
});
|
||||
stream.on("error", e);
|
||||
});
|
||||
};
|
||||
|
||||
const extractTar = (tarPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> => {
|
||||
return new Promise<void>(async (c, e): Promise<void> => {
|
||||
try {
|
||||
const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : "");
|
||||
const buffer = await promisify(fs.readFile)(tarPath);
|
||||
const extractor = tarStream.extract();
|
||||
extractor.once("error", e);
|
||||
extractor.on("entry", (header, stream, next) => {
|
||||
const rawName = path.normalize(header.name);
|
||||
|
||||
const nextEntry = (): void => {
|
||||
stream.resume();
|
||||
next();
|
||||
};
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
return nextEntry();
|
||||
}
|
||||
|
||||
if (!sourcePathRegex.test(rawName)) {
|
||||
return nextEntry();
|
||||
}
|
||||
|
||||
const fileName = rawName.replace(sourcePathRegex, "");
|
||||
const targetFileName = path.join(targetPath, fileName);
|
||||
if (/\/$/.test(fileName)) {
|
||||
stream.resume();
|
||||
mkdirp(targetFileName).then(() => {
|
||||
next();
|
||||
}, e);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const dirName = path.dirname(fileName);
|
||||
const targetDirName = path.join(targetPath, dirName);
|
||||
if (targetDirName.indexOf(targetPath) !== 0) {
|
||||
e(nls.localize("invalid file", "Error extracting {0}. Invalid file.", fileName));
|
||||
|
||||
return nextEntry();
|
||||
}
|
||||
|
||||
return mkdirp(targetDirName, undefined, token).then(() => {
|
||||
const fstream = fs.createWriteStream(targetFileName, { mode: header.mode });
|
||||
fstream.once("close", () => {
|
||||
next();
|
||||
});
|
||||
fstream.once("error", e);
|
||||
stream.pipe(fstream);
|
||||
stream.resume();
|
||||
});
|
||||
});
|
||||
extractor.once("finish", c);
|
||||
extractor.write(buffer);
|
||||
extractor.end();
|
||||
} catch (ex) {
|
||||
e(ex);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Override original functionality so we can use tar instead of zip.
|
||||
const target = vszip as typeof vszip;
|
||||
target.zip = zip;
|
||||
target.extract = extract;
|
||||
target.buffer = buffer;
|
358
src/upload.ts
Normal file
358
src/upload.ts
Normal file
@ -0,0 +1,358 @@
|
||||
import { exec } from "child_process";
|
||||
import { appendFile } from "fs";
|
||||
import { promisify } from "util";
|
||||
import { logger } from "@coder/logger";
|
||||
import { escapePath } from "@coder/protocol";
|
||||
import { NotificationService, INotificationService, ProgressService, IProgressService, IProgress, Severity } from "./fill/notification";
|
||||
|
||||
export interface IURI {
|
||||
readonly path: string;
|
||||
readonly fsPath: string;
|
||||
readonly scheme: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an uploadable directory, so we can query for existing files once.
|
||||
*/
|
||||
interface IUploadableDirectory {
|
||||
existingFiles: string[];
|
||||
filesToUpload: Map<string, File>;
|
||||
preparePromise?: Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* There doesn't seem to be a provided type for entries, so here is an
|
||||
* incomplete version.
|
||||
*/
|
||||
interface IEntry {
|
||||
name: string;
|
||||
isFile: boolean;
|
||||
file: (cb: (file: File) => void) => void;
|
||||
createReader: () => ({
|
||||
readEntries: (cb: (entries: Array<IEntry>) => void) => void;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles file uploads.
|
||||
*/
|
||||
export class Upload {
|
||||
private readonly maxParallelUploads = 100;
|
||||
private readonly readSize = 32000; // ~32kb max while reading in the file.
|
||||
private readonly packetSize = 32000; // ~32kb max when writing.
|
||||
private readonly logger = logger.named("Upload");
|
||||
private readonly currentlyUploadingFiles = new Map<string, File>();
|
||||
private readonly queueByDirectory = new Map<string, IUploadableDirectory>();
|
||||
private progress: IProgress | undefined;
|
||||
private uploadPromise: Promise<string[]> | undefined;
|
||||
private resolveUploadPromise: (() => void) | undefined;
|
||||
private finished = 0;
|
||||
private uploadedFilePaths = <string[]>[];
|
||||
private total = 0;
|
||||
|
||||
public constructor(
|
||||
private _notificationService: INotificationService,
|
||||
private _progressService: IProgressService,
|
||||
) {}
|
||||
|
||||
public set notificationService(service: INotificationService) {
|
||||
this._notificationService = service;
|
||||
}
|
||||
|
||||
public get notificationService(): INotificationService {
|
||||
return this._notificationService;
|
||||
}
|
||||
|
||||
public set progressService(service: IProgressService) {
|
||||
this._progressService = service;
|
||||
}
|
||||
|
||||
public get progressService(): IProgressService {
|
||||
return this._progressService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload dropped files. This will try to upload everything it can. Errors
|
||||
* will show via notifications. If an upload operation is ongoing, the files
|
||||
* will be added to that operation.
|
||||
*/
|
||||
public async uploadDropped(event: DragEvent, uploadDir: IURI): Promise<string[]> {
|
||||
this.addDirectory(uploadDir.path);
|
||||
await this.queueFiles(event, uploadDir);
|
||||
this.logger.debug( // -1 so we don't include the uploadDir itself.
|
||||
`Uploading ${this.queueByDirectory.size - 1} directories and ${this.total} files`,
|
||||
);
|
||||
await this.prepareDirectories();
|
||||
if (!this.uploadPromise) {
|
||||
this.uploadPromise = this.progressService.start("Uploading files...", (progress) => {
|
||||
return new Promise((resolve): void => {
|
||||
this.progress = progress;
|
||||
this.resolveUploadPromise = (): void => {
|
||||
const uploaded = this.uploadedFilePaths;
|
||||
this.uploadPromise = undefined;
|
||||
this.resolveUploadPromise = undefined;
|
||||
this.uploadedFilePaths = [];
|
||||
this.finished = 0;
|
||||
this.total = 0;
|
||||
resolve(uploaded);
|
||||
};
|
||||
});
|
||||
}, () => {
|
||||
this.cancel();
|
||||
});
|
||||
}
|
||||
this.uploadFiles();
|
||||
|
||||
return this.uploadPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all file uploads.
|
||||
*/
|
||||
public async cancel(): Promise<void> {
|
||||
this.currentlyUploadingFiles.clear();
|
||||
this.queueByDirectory.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create directories and get existing files.
|
||||
* On failure, show the error and remove the failed directory from the queue.
|
||||
*/
|
||||
private async prepareDirectories(): Promise<void> {
|
||||
await Promise.all(Array.from(this.queueByDirectory).map(([path, dir]) => {
|
||||
if (!dir.preparePromise) {
|
||||
dir.preparePromise = this.prepareDirectory(path, dir);
|
||||
}
|
||||
|
||||
return dir.preparePromise;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory and get existing files.
|
||||
* On failure, show the error and remove the directory from the queue.
|
||||
*/
|
||||
private async prepareDirectory(path: string, dir: IUploadableDirectory): Promise<void> {
|
||||
await Promise.all([
|
||||
promisify(exec)(`mkdir -p ${escapePath(path)}`).catch((error) => {
|
||||
const message = error.message.toLowerCase();
|
||||
if (message.includes("file exists")) {
|
||||
throw new Error(`Unable to create directory at ${path} because a file exists there`);
|
||||
}
|
||||
throw new Error(error.message || `Unable to upload ${path}`);
|
||||
}),
|
||||
// Only get files, so we don't show an override option that will just
|
||||
// fail anyway.
|
||||
promisify(exec)(`find ${escapePath(path)} -maxdepth 1 -not -type d`).then((stdio) => {
|
||||
dir.existingFiles = stdio.stdout.split("\n");
|
||||
}),
|
||||
]).catch((error) => {
|
||||
this.queueByDirectory.delete(path);
|
||||
this.notificationService.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload as many files as possible. When finished, resolve the upload promise.
|
||||
*/
|
||||
private uploadFiles(): void {
|
||||
const finishFileUpload = (path: string): void => {
|
||||
++this.finished;
|
||||
this.currentlyUploadingFiles.delete(path);
|
||||
this.progress!.report(Math.floor((this.finished / this.total) * 100));
|
||||
this.uploadFiles();
|
||||
};
|
||||
while (this.queueByDirectory.size > 0 && this.currentlyUploadingFiles.size < this.maxParallelUploads) {
|
||||
const [dirPath, dir] = this.queueByDirectory.entries().next().value;
|
||||
if (dir.filesToUpload.size === 0) {
|
||||
this.queueByDirectory.delete(dirPath);
|
||||
continue;
|
||||
}
|
||||
const [filePath, item] = dir.filesToUpload.entries().next().value;
|
||||
this.currentlyUploadingFiles.set(filePath, item);
|
||||
dir.filesToUpload.delete(filePath);
|
||||
this.uploadFile(filePath, item, dir.existingFiles).then(() => {
|
||||
finishFileUpload(filePath);
|
||||
}).catch((error) => {
|
||||
this.notificationService.error(error);
|
||||
finishFileUpload(filePath);
|
||||
});
|
||||
}
|
||||
if (this.queueByDirectory.size === 0 && this.currentlyUploadingFiles.size === 0) {
|
||||
this.resolveUploadPromise!();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file.
|
||||
*/
|
||||
private async uploadFile(path: string, file: File, existingFiles: string[]): Promise<void> {
|
||||
if (existingFiles.includes(path)) {
|
||||
const shouldOverwrite = await new Promise((resolve): void => {
|
||||
this.notificationService.prompt(
|
||||
Severity.Error,
|
||||
`${path} already exists. Overwrite?`,
|
||||
[{
|
||||
label: "Yes",
|
||||
run: (): void => resolve(true),
|
||||
}, {
|
||||
label: "No",
|
||||
run: (): void => resolve(false),
|
||||
}],
|
||||
() => resolve(false),
|
||||
);
|
||||
});
|
||||
if (!shouldOverwrite) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await new Promise(async (resolve, reject): Promise<void> => {
|
||||
let readOffset = 0;
|
||||
const reader = new FileReader();
|
||||
const seek = (): void => {
|
||||
const slice = file.slice(readOffset, readOffset + this.readSize);
|
||||
readOffset += this.readSize;
|
||||
reader.readAsArrayBuffer(slice);
|
||||
};
|
||||
|
||||
const rm = async (): Promise<void> => {
|
||||
await promisify(exec)(`rm -f ${escapePath(path)}`);
|
||||
};
|
||||
|
||||
await rm();
|
||||
|
||||
const load = async (): Promise<void> => {
|
||||
const buffer = new Uint8Array(reader.result as ArrayBuffer);
|
||||
let bufferOffset = 0;
|
||||
|
||||
while (bufferOffset <= buffer.length) {
|
||||
// Got canceled while sending data.
|
||||
if (!this.currentlyUploadingFiles.has(path)) {
|
||||
await rm();
|
||||
|
||||
return resolve();
|
||||
}
|
||||
const data = buffer.slice(bufferOffset, bufferOffset + this.packetSize);
|
||||
|
||||
try {
|
||||
await promisify(appendFile)(path, data);
|
||||
} catch (error) {
|
||||
await rm();
|
||||
|
||||
const message = error.message.toLowerCase();
|
||||
if (message.includes("no space")) {
|
||||
return reject(new Error("You are out of disk space"));
|
||||
} else if (message.includes("is a directory")) {
|
||||
return reject(new Error(`Unable to upload ${path} because there is a directory there`));
|
||||
}
|
||||
|
||||
return reject(new Error(error.message || `Unable to upload ${path}`));
|
||||
}
|
||||
|
||||
bufferOffset += this.packetSize;
|
||||
}
|
||||
|
||||
if (readOffset >= file.size) {
|
||||
this.uploadedFilePaths.push(path);
|
||||
|
||||
return resolve();
|
||||
}
|
||||
|
||||
seek();
|
||||
};
|
||||
|
||||
reader.addEventListener("load", load);
|
||||
|
||||
seek();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue files from a drop event. We have to get the files first; we can't do
|
||||
* it in tandem with uploading or the entries will disappear.
|
||||
*/
|
||||
private async queueFiles(event: DragEvent, uploadDir: IURI): Promise<void> {
|
||||
if (!event.dataTransfer || !event.dataTransfer.items) {
|
||||
return;
|
||||
}
|
||||
const promises: Array<Promise<void>> = [];
|
||||
for (let i = 0; i < event.dataTransfer.items.length; i++) {
|
||||
const item = event.dataTransfer.items[i];
|
||||
if (typeof item.webkitGetAsEntry === "function") {
|
||||
promises.push(this.traverseItem(item.webkitGetAsEntry(), uploadDir.fsPath).catch(this.notificationService.error));
|
||||
} else {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
this.addFile(uploadDir.fsPath, uploadDir.fsPath + "/" + file.name, file);
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses an entry and add files to the queue.
|
||||
*/
|
||||
private async traverseItem(entry: IEntry, parentPath: string): Promise<void> {
|
||||
if (entry.isFile) {
|
||||
return new Promise<void>((resolve): void => {
|
||||
entry.file((file) => {
|
||||
this.addFile(
|
||||
parentPath,
|
||||
parentPath + "/" + file.name,
|
||||
file,
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
parentPath += "/" + entry.name;
|
||||
this.addDirectory(parentPath);
|
||||
|
||||
await new Promise((resolve): void => {
|
||||
const promises: Array<Promise<void>> = [];
|
||||
const dirReader = entry.createReader();
|
||||
// According to the spec, readEntries() must be called until it calls
|
||||
// the callback with an empty array.
|
||||
const readEntries = (): void => {
|
||||
dirReader.readEntries((entries) => {
|
||||
if (entries.length === 0) {
|
||||
Promise.all(promises).then(resolve).catch((error) => {
|
||||
this.notificationService.error(error);
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
promises.push(...entries.map((child) => this.traverseItem(child, parentPath)));
|
||||
readEntries();
|
||||
}
|
||||
});
|
||||
};
|
||||
readEntries();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a file to the queue.
|
||||
*/
|
||||
private addFile(parentPath: string, path: string, file: File): void {
|
||||
++this.total;
|
||||
this.addDirectory(parentPath);
|
||||
this.queueByDirectory.get(parentPath)!.filesToUpload.set(path, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a directory to the queue.
|
||||
*/
|
||||
private addDirectory(path: string): void {
|
||||
if (!this.queueByDirectory.has(path)) {
|
||||
this.queueByDirectory.set(path, {
|
||||
existingFiles: [],
|
||||
filesToUpload: new Map(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance.
|
||||
export const upload = new Upload(new NotificationService(), new ProgressService());
|
28
src/uriTransformerHttp.js
Normal file
28
src/uriTransformerHttp.js
Normal file
@ -0,0 +1,28 @@
|
||||
// This file is included via a regular Node require. I'm not sure how (or if)
|
||||
// we can write this in Typescript and have it compile to non-AMD syntax.
|
||||
module.exports = (remoteAuthority, https) => {
|
||||
return {
|
||||
transformIncoming: (uri) => {
|
||||
switch (uri.scheme) {
|
||||
case "https": return { scheme: "file", path: uri.path };
|
||||
case "http": return { scheme: "file", path: uri.path };
|
||||
case "file": return { scheme: "vscode-local", path: uri.path };
|
||||
default: return uri;
|
||||
}
|
||||
},
|
||||
transformOutgoing: (uri) => {
|
||||
switch (uri.scheme) {
|
||||
case "vscode-local": return { scheme: "file", path: uri.path };
|
||||
case "file": return { scheme: https ? "https" : "http", authority: remoteAuthority, path: uri.path };
|
||||
default: return uri;
|
||||
}
|
||||
},
|
||||
transformOutgoingScheme: (scheme) => {
|
||||
switch (scheme) {
|
||||
case "vscode-local": return "file";
|
||||
case "file": return https ? "https" : "http";
|
||||
default: return scheme;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
3
src/uriTransformerHttps.js
Normal file
3
src/uriTransformerHttps.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = (remoteAuthority) => {
|
||||
return require("./uriTransformerHttp")(remoteAuthority, true);
|
||||
};
|
60
src/util.ts
Normal file
60
src/util.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as util from "util";
|
||||
|
||||
import { getPathFromAmdModule } from "vs/base/common/amd";
|
||||
import { URITransformer, IRawURITransformer } from "vs/base/common/uriIpc";
|
||||
import { mkdirp } from "vs/base/node/pfs";
|
||||
|
||||
export const tmpdir = path.join(os.tmpdir(), "code-server");
|
||||
|
||||
export const generateCertificate = async (): Promise<{ cert: string, certKey: string }> => {
|
||||
const paths = {
|
||||
cert: path.join(tmpdir, "self-signed.cert"),
|
||||
certKey: path.join(tmpdir, "self-signed.key"),
|
||||
};
|
||||
|
||||
const exists = await Promise.all([
|
||||
util.promisify(fs.exists)(paths.cert),
|
||||
util.promisify(fs.exists)(paths.certKey),
|
||||
]);
|
||||
|
||||
await mkdirp(tmpdir);
|
||||
|
||||
if (!exists[0] || !exists[1]) {
|
||||
const pem = require.__$__nodeRequire(path.resolve(__dirname, "../node_modules/pem/lib/pem")) as typeof import("pem");
|
||||
const certs = await new Promise<import("pem").CertificateCreationResult>((resolve, reject): void => {
|
||||
pem.createCertificate({ selfSigned: true }, (error, result) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
await Promise.all([
|
||||
util.promisify(fs.writeFile)(paths.cert, certs.certificate),
|
||||
util.promisify(fs.writeFile)(paths.certKey, certs.serviceKey),
|
||||
]);
|
||||
}
|
||||
|
||||
return paths;
|
||||
};
|
||||
|
||||
let secure: boolean;
|
||||
export const useHttpsTransformer = (): void => {
|
||||
secure = true;
|
||||
};
|
||||
|
||||
export const uriTransformerPath = (): string => {
|
||||
return getPathFromAmdModule(
|
||||
require,
|
||||
"vs/server/src/uriTransformerHttp" + (secure ? "s": ""),
|
||||
);
|
||||
};
|
||||
|
||||
export const getUriTransformer = (remoteAuthority: string): URITransformer => {
|
||||
const rawURITransformerFactory = <any>require.__$__nodeRequire(uriTransformerPath());
|
||||
const rawURITransformer = <IRawURITransformer>rawURITransformerFactory(remoteAuthority);
|
||||
return new URITransformer(rawURITransformer);
|
||||
};
|
Reference in New Issue
Block a user