Original link: BLOG

Code-server is an open source online VScode remote run service, this article from the source perspective to interpret how he moved to the browser to run vscode

Vscode version 1.39.2 Node version 10.16.0

Let’s warm up the vscode architecture

As a local Electron application, it adopts multi-process architecture;

IPC communication or RPC remote procedure call is carried out between the main process and many sub-processes. The plug-in system also acts as a process, and all plug-ins will run under this process, including language service protocol, etc.

The DEBUG protocol is different from other processes. Each time you run the DEBUG protocol, a new sub-process is started.

Common file reads and writes are completed in the main process.

Coder development process

In coder- Server README, there are development process steps, basically divided into

git clone https://github.com/microsoft/vscode
cd vscode
git checkout ${vscodeVersion} # See travis.yml for the version to use.
yarn
git clone https://github.com/cdr/code-server src/vs/server
cd src/vs/server
yarn
yarn patch:apply
yarn watch
# Wait for the initial compilation to complete (it will say "Finished compilation").
# Run the next command in another shell.
yarn start
# Visit http://localhost:8080
Copy the code
  1. First,cloneVscode project and switch to a fixed version
  2. hisclonesrc/vs/serverdirectory
  3. Put the magic parts through the patchpatchscoringVscode, perform watchvscodeThe TS code in the project is compiled into an executable JS file and output tooutdirectory
  4. To start, run node out of /vs/server/main.js

Therefore, we can think of it as the server of vscode, providing support for its web version, especially in the patch part

directory

├─ ci.bash // Build.ts // Build.ts // Build.ts task ├─ Ci.bash // Download download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download Download │ ├─ API. Ts // vscode API │ ├─ ├─ extHostNodeProxy. Ts │ ├─ extHostNodeProxy. Through the patch to play in the SRC/vs/workbench/services/extensions/worker/extHost services. Ts directory as a singleton service, │ ├─ ├─ MainThreadNode.ts // Upload.ts // upload.ts // upload.ts // upload.ts // upload.ts // upload.ts // upload.ts // upload.ts // │ ├─ ├─ └─ workshop.html // import HTML file │ ├─ ├─ workshop.ts // drag the file in the file tree to perform the service SRC/vs/workbench/API/worker/extHostExtensionService ts directory _loadCommonJSModule method, The key is to use the node-Browser and Requirefs wrapped by coder itself. │ ├─ nodeProxy. Ts │ ├─ Telemetry. Ts // Telemetry │ ├─ ├─ manifest.json ├─ └ // └.ts // Main ├─ cli.ts ├─ Connection.TS // Main FOR IPC Channel Link ├─ Insights Marketplace. Ts ├ ─ ─ NLS. Ts ├ ─ ─ protocol. The ts / / is mainly the websocket protocol ├ ─ ─ for server ts ├ ─ ─ the update. The ts ├ ─ ─ uriTransformer. Js └ ─ ─ ├─ download.txt └─ download.txt └─ download.txt └─ download.txtCopy the code

Startup sequence

To execute the start command in the server directory, run node out of /vs/server/main.js

The core of the main file is a single line

require(“.. /.. /bootstrap-amd”).load(“vs/server/src/node/cli”);

Load the CLI file from the AMD module, and the CLI file is located in the server/node/ CLI.

This file executes the main function

const main = async() :Promise<boolean | void | void[] > = > {const args = getArgs();
	if (process.env.LAUNCH_VSCODE) {
		await ipcMain.handshake();
		return startVscode(args);
	}
	return startCli(args) || new WrapperProcess(args).start();
};
Copy the code

The LAUNCH_VSCODE environment variable is null, and the startCli function is null. The WrapperProcess is the key.

When the WrapperProcess is instantiated, the IPC handshake is started for communication between processes, and the start function is executed after that

if (!this.started) {
    const child = this.spawn();
    this.started = ipcMain.handshake(child).then((a)= > {
        child.once("exit".(code) = >exit(code!) ); });this.process = child;
}
return this.started;
Copy the code

Create a child process with spawn, where spawn does the following

/ /...
const isBinary = (global as any).NBIN_LOADED;
return cp.spawn(process.argv[0], process.argv.slice(isBinary ? 2 : 1), { env: { ... process.env, LAUNCH_VSCODE:"true",
        NBIN_BYPASS: undefined,
        VSCODE_PARENT_PID: process.pid.toString(),
        NODE_OPTIONS: nodeOptions,
    },
    stdio: ["inherit"."inherit"."inherit"."ipc"]});Copy the code

The NBIN_LOADED environment is coder custom, nbin is mainly used to patch the Node fs module to achieve enhanced binary compilation process, which is also why the node version has strong constraints.

A child process is recreated with all the current parameters and returns, with stdio specifying the IPC channel and the LAUNCH_VSCODE environment variable set to true, at which point main is re-executed.

Go back to main and run startVscode after ipc shakes hands;

/ /...

const server = newMainServer({ ... options, port:typeofargs.port ! = ="undefined" ? parseInt(args.port, 10) : 8080,
    socket: args.socket,
}, args);

const [serverAddress, /* ignore */] = await Promise.all([
    server.listen(),
    unpackExecutables(),
]);

/ /...
Copy the code

In summary, cli is basically the process of setting up the IPC channel and preprocessing a bunch of parameters to start the server;

The Server side

The MainServer function comes to the server side, which inherits the abstract server class and starts the HTTP service in the constructor

public constructor(options: ServerOptions) {
    this.options = {
        host: options.auth === "password" && options.cert ? "0.0.0.0" : "localhost". options, basePath: options.basePath ? options.basePath.replace(/ / / + $/."") : "",
        password: options.password ? hash(options.password) : undefined};this.protocol = this.options.cert ? "https" : "http";
    if (this.protocol === "https") {
        const httpolyglot = localRequire<typeof import("httpolyglot") > ("httpolyglot/lib/index");
        this.server = httpolyglot.createServer({
            cert: this.options.cert && fs.readFileSync(this.options.cert),
            key: this.options.certKey && fs.readFileSync(this.options.certKey),
        }, this.onRequest);
    } else {
        this.server = http.createServer(this.onRequest); }}Copy the code

The cert parameter is a self-signed certificate, I guess for security reasons.

The onRequest method preprocesses the request header

/ /...
const payload = await this.preHandleRequest(request, parsedUrl);
/ /...
Copy the code

The preHandleRequest method intercepts the requested path, including a section

// Allow for a versioned static endpoint. This lets us cache every static
// resource underneath the path based on the version without any work and
// without adding query parameters which have their own issues.
// REVIEW: Discuss whether this is the best option; this is sort of a quick
// hack almost to get caching in the meantime but it does work pretty well.
if (/^\/static-/.test(base)) {
    base = "/static";
}
Copy the code

Reset the base path to /static by rematching all paths that start with static- for caching later

/ /...
case "/static":
    const response = await this.getResource(this.rootPath, requestPath);
    response.cache = true;
    return response;
/ /...
Copy the code

GetResource reads the contents of the file and returns it

protected asyncgetResource(... parts:string[]) :Promise<Response> {
    const filePath = this.ensureAuthorizedFilePath(... parts);return { content: await util.promisify(fs.readFile)(filePath), filePath };
}
Copy the code

All files except the static path and specific resource file paths go down to the handleRequest function;

This is the processing of various file resources, such as tar file processing, webview file processing and static resources processing, of course, there is a heartbeat;

For the root path/the getRoot function is returned

case "/": return this.getRoot(request, parsedUrl);
Copy the code

GetRoot function in the SRC/vs/server/SRC/browser/workbench. HTML has made some textual substitution treatment; 👇

private async getRoot(request: http.IncomingMessage, parsedUrl: url.UrlWithParsedQuery): Promise<Response> {
    const filePath = path.join(this.serverRoot, "browser/workbench.html");
    let [content, startPath] = await Promise.all([
        util.promisify(fs.readFile)(filePath, "utf8"),
        this.getFirstValidPath([
            { path: parsedUrl.query.workspace, workspace: true },
            { path: parsedUrl.query.folder, workspace: false},await this.readSettings()).lastVisited,
            { path: this.options.openUri }
        ]),
        this.servicesPromise,
    ]);

    if (startPath) {
        this.writeSettings({
            lastVisited: {
                path: startPath.uri.fsPath,
                workspace: startPath.workspace
            },
        });
    }

    const logger = this.services.get(ILogService) as ILogService;
    logger.info("request.url".`"${request.url}"`);

    const remoteAuthority = request.headers.host as string;
    const transformer = getUriTransformer(remoteAuthority);

    const environment = this.services.get(IEnvironmentService) as IEnvironmentService;
    const options: Options = {
        WORKBENCH_WEB_CONFIGURATION: {
            workspaceUri: startPath && startPath.workspace ? transformer.transformOutgoing(startPath.uri) : undefined, folderUri: startPath && ! startPath.workspace ? transformer.transformOutgoing(startPath.uri) :undefined,
            remoteAuthority,
            logLevel: getLogLevel(environment),
        },
        REMOTE_USER_DATA_URI: transformer.transformOutgoing(URI.file(environment.userDataPath)),
        PRODUCT_CONFIGURATION: {
            extensionsGallery: product.extensionsGallery,
        },
        NLS_CONFIGURATION: await getNlsConfiguration(environment.args.locale || await getLocaleFromConfig(environment.userDataPath), environment.userDataPath),
    };

    content = content.replace(/{{COMMIT}}/g, product.commit || "");
    for (const key in options) {
        content = content.replace({{`"${key}`}}".` '${JSON.stringify(options[key as keyof Options])}'`);
    }

    return { content, filePath };
}
Copy the code

First, it directly reads the content of Workbench. HTML, and then replaces the placeholders in the HTML file according to the options key, such as URI conversion, NLS multilingual configuration, Workbench configuration, etc., and then returns the file content and path.

<! -- Workbench Configuration -->
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}">

<! -- Workarounds/Hacks (remote user data uri) -->
<meta id="vscode-remote-user-data-uri" data-settings="{{REMOTE_USER_DATA_URI}}">
<! -- NOTE@coder: Added the commit for use in caching, the product for the extensions gallery URL, and nls for language support. -->
<meta id="vscode-remote-commit" data-settings="{{COMMIT}}">
<meta id="vscode-remote-product-configuration" data-settings="{{PRODUCT_CONFIGURATION}}">
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}">
Copy the code

The getFirstValidPath method is used to specify the workspace and file to open initially.

At this point, the main tasks of the Server abstract class are done, and the rest is done by MainServer, which executes the initializeServices method in its constructor.

Some IPC channels are registered, such as Logger log, plug-in DEBUG, Telemetry, nodeProxy node proxy, etc.

And register some dependency injection services, such as ILogService, IFileService file service and so on;

private async initializeServices(args: ParsedArgs): Promise<void> {
    const environmentService = new EnvironmentService(args, process.execPath);
    const logService = new SpdLogService(RemoteExtensionLogFileName, environmentService.logsPath, getLogLevel(environmentService));
    const fileService = new FileService(logService);
    fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(logService));

    this.allowedRequestPaths.push(
        path.join(environmentService.userDataPath, "clp"), // Language packs.environmentService.extensionsPath, environmentService.builtinExtensionsPath, ... environmentService.extraExtensionPaths, ... environmentService.extraBuiltinExtensionPaths, );this.ipc.registerChannel("logger".new LoggerChannel(logService));
    this.ipc.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel());

    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(IFileService, fileService);
    this.services.set(IProductService, { _serviceBrand: undefined. product });this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService));
    this.services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService));

    if(! environmentService.args["disable-telemetry"]) {
        this.services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [{
            appender: combinedAppender(
                new AppInsightsAppender("code-server".null.(a)= > new TelemetryClient(), logService),
                new LogAppender(logService),
            ),
            commonProperties: resolveCommonProperties(
                product.commit, product.codeServerVersion, await getMachineId(),
                [], environmentService.installSourcePath, "code-server",
            ),
            piiPaths: this.allowedRequestPaths,
        } as ITelemetryServiceConfig]));
    } else {
        this.services.set(ITelemetryService, NullTelemetryService);
    }

    await new Promise((resolve) = > {
        const instantiationService = new InstantiationService(this.services);
        this.services.set(ILocalizationsService, instantiationService.createInstance(LocalizationsService));
        this.services.set(INodeProxyService, instantiationService.createInstance(NodeProxyService));

        instantiationService.invokeFunction((a)= > {
            instantiationService.createInstance(LogsDataCleaner);
            const telemetryService = this.services.get(ITelemetryService) as ITelemetryService;
            this.ipc.registerChannel("extensions".new ExtensionManagementChannel(
                this.services.get(IExtensionManagementService) as IExtensionManagementService,
                (context) = > getUriTransformer(context.remoteAuthority),
            ));
            this.ipc.registerChannel("remoteextensionsenvironment".new ExtensionEnvironmentChannel(
                environmentService, logService, telemetryService, this.options.connectionToken || ""));this.ipc.registerChannel("request".new RequestChannel(this.services.get(IRequestService) as IRequestService));
            this.ipc.registerChannel("telemetry".new TelemetryChannel(telemetryService));
            this.ipc.registerChannel("nodeProxy".new NodeProxyChannel(this.services.get(INodeProxyService) as INodeProxyService));
            this.ipc.registerChannel("localizations", createChannelReceiver(this.services.get(ILocalizationsService) as ILocalizationsService));
            this.ipc.registerChannel("update".new UpdateChannel(instantiationService.createInstance(UpdateService)));
            this.ipc.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new FileProviderChannel(environmentService, logService));
            resolve(new ErrorTelemetry(telemetryService));
        });
    });
}
Copy the code

Once they’re all instantiated, the listen method is executed

public async listen(): Promise<string> {
    const environment = (this.services.get(IEnvironmentService) as EnvironmentService);
    const [address] = await Promise.all<string> ([super.listen(), ... [ environment.extensionsPath, ].map((p) = > mkdirp(p).then((a)= > p)),
    ]);
    return address;
}
Copy the code

Where the listen method in super.listen() is

public listen(): Promise<string> {
    if (!this.listenPromise) {
        this.listenPromise = new Promise((resolve, reject) = > {
            this.server.on("error", reject);
            this.server.on("upgrade".this.onUpgrade);
            const onListen = (a)= > resolve(this.address());
            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;
}
Copy the code

Originally websocket is set up in onUpgrade here, after the end of the start to listen to the port, there is only a section of websocket preprocessing

await this.preHandleWebSocket(request, socket);
Copy the code

It’s all base operations on WS, and returns handleWebSocket when it’s done; 👇

const protocol = new Protocol(await this.createProxy(socket), {
    reconnectionToken: <string>parsedUrl.query.reconnectionToken,
    reconnection: parsedUrl.query.reconnection === "true",
    skipWebSocketFrames: parsedUrl.query.skipWebSocketFrames === "true"});try {
    await this.connect(await protocol.handshake(), protocol);
} catch (error) {
    protocol.sendMessage({ type: "error", reason: error.message });
    protocol.dispose();
    protocol.getSocket().dispose();
}
Copy the code

If the socket is of TLSSocket type, it requires a certificate and a key. If the socket is of TLSSocket type, it returns a non-TLssocket.

Protocol inherited ipc.net (in src/vs/base/parts/ipc/common/ipc.net.ts directory) PersistentProtocol class, The agreement transfer message must be VSBuffer (in the SRC/vs/base/common/buffer. The ts directory)

readonly onControlMessage: Event<VSBuffer>
Copy the code

Then come to protocol and Handshake

/**
* 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); }}); }); }Copy the code

The handshake that establishes the protocol returns a message, which is then thrown into the Connect method along with the protocol itself.

Inside it is the processing of all the required connection types, which fall into three categories

export const enum ConnectionType {
	Management = 1,
	ExtensionHost = 2,
	Tunnel = 3,}Copy the code

Then the three connection types are processed respectively.

switch (message.desiredConnectionType) {
    case ConnectionType.ExtensionHost:
    case ConnectionType.Management:
        if (!this.connections.has(message.desiredConnectionType)) {
            this.connections.set(message.desiredConnectionType, new Map());
        }
        const connections = this.connections.get(message.desiredConnectionType)! ;const ok = async() = > {return message.desiredConnectionType === ConnectionType.ExtensionHost
                ? { debugPort: await this.getDebugPort() }
                : { type: "ok" };
        };

        const token = protocol.options.reconnectionToken;
        if (protocol.options.reconnection && connections.has(token)) {
            protocol.sendMessage(await ok());
            const buffer = protocol.readEntireBuffer();
            protocol.dispose();
            returnconnections.get(token)! .reconnect(protocol.getSocket(), buffer); }else if (protocol.options.reconnection || connections.has(token)) {
            throw new Error(protocol.options.reconnection
                ? "Unrecognized reconnection token"
                : "Duplicate reconnection token"
            );
        }

        protocol.sendMessage(await ok());

        let connection: Connection;
        if (message.desiredConnectionType === ConnectionType.Management) {
            connection = new ManagementConnection(protocol, token);
            this._onDidClientConnect.fire({
                protocol, onDidClientDisconnect: connection.onClose,
            });
            // TODO: Need a way to match clients with a connection. For now
            // dispose everything which only works because no extensions currently
            // utilize long-running proxies.
            (this.services.get(INodeProxyService) as NodeProxyService)._onUp.fire();
            connection.onClose((a)= > (this.services.get(INodeProxyService) as NodeProxyService)._onDown.fire());
        } else {
            const buffer = protocol.readEntireBuffer();
            connection = new ExtensionHostConnection(
                message.args ? message.args.language : "en",
                protocol, buffer, token,
                this.services.get(ILogService) as ILogService,
                this.services.get(IEnvironmentService) as IEnvironmentService,
            );
        }
        connections.set(token, connection);
        connection.onClose((a)= > connections.delete(token));
        this.disposeOldOfflineConnections(connections);
        break;
    case ConnectionType.Tunnel: return protocol.tunnel();
    default: throw new Error("Unrecognized connection type");
}
Copy the code

At this point, the main tasks of the server side are also completed, and the following things are generally done

  • Start thehttpservice
  • Process the resource request path
  • registeredipcChannels and dependency injection
  • To establishwebsocketcommunication

Of course, there are details like login and heartbeat;

Let’s go to port 8080, the default port it starts from

Workbench

You need to set the auth parameter to None at startup so that you don’t jump to the login page;

The root path loaded first returns the workbench.html file content mentioned above;

You can see that the placeholders have been replaced with configuration items and are in require.js AMD modular mode.

The baseUrl and Paths parameters are configured to specify the paths for each module. Since the staticBase path is applied to each module, the /static- will cache these files.

The next thing to do is load the main module loader.js, and the rest is vscode itself;

The main task of code-server is now complete