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
- First,
clone
Vscode project and switch to a fixed version - his
clone
到src/vs/server
directory - Put the magic parts through the patch
patch
scoringVscode, perform
watch
将vscode
The TS code in the project is compiled into an executable JS file and output toout
directory - 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 the
http
service - Process the resource request path
- registered
ipc
Channels and dependency injection - To establish
websocket
communication
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