0. 回顾

上一篇,我们介绍了进程间通信,主进程通过 child_process 启动了 tsserver
主进程通过 child.stdin.write 写内容,向子进程发消息。

  1. child.stdin.write(`${JSON.stringify(openFile)}\n`);

子进程处理完业务逻辑之后,向自己进程的 stdout 发消息,
回调到主进程的 child.stdout.on data 事件监听函数中。

  1. child.stdout.on('data', data => {
  2. console.log(data.toString());
  3. });

下文我们来跟踪一下这些消息的处理过程,看看有哪些值得注意的地方。

1. 启动调试

以上我们启动了两个 VSCode 实例,分别称为 client 端与 server 端,准备调试 tsserver。
我们看到 server 端的 .vscode/launch.json 是与项目无关的,因此,可以放到 TypeScript 源码仓库的调试配置中。

淡如止水 TypeScript (九):通信过程 - 图1

然后按以下步骤启动调试。
(1)client 端,按 F5 启动调试
淡如止水 TypeScript (九):通信过程 - 图2
client 端执行完 spawn 后,tsserver 就启动了。

(2)server 端,按 F5 attach 到已经启动的 tsserver
淡如止水 TypeScript (九):通信过程 - 图3
我们看到两个 VSCode 实例,都停在了断点处。

(3)server 端 lib/tsserver 的调试,仍然会遇到无法进入 .ts 的问题
与第二篇一样,我们需要在 require 的时候,点击 Step Into,进入 src/compiler/core.ts#L1 中。
淡如止水 TypeScript (九):通信过程 - 图4

淡如止水 TypeScript (九):通信过程 - 图5

2. tsserver 启动事件

tsserver 启动后,在没有收到任何消息时,会先向主进程发送一条消息,
为了理解这条消息的发送逻辑是怎样的,我们需要将 client 端 child.stdin.write 的内容先注释掉。

client 端 index.js 的文件内容修改如下,

  1. const path = require('path');
  2. const { spawn } = require('child_process');
  3. const root = '/Users/.../TypeScript'; // <- 这是 TypeScript 源码仓库的根目录
  4. const child = spawn('node', [
  5. '--inspect-brk=9002',
  6. path.join(root, 'bin/tsserver'),
  7. ]);
  8. child.stdout.on('data', data => {
  9. console.log(data.toString());
  10. });
  11. child.on('close', code => {
  12. console.log(code);
  13. });
  14. // 注释掉了 child.stdin.write

然后我们启动 client 端,再 attach server 端。
通过 “灵犀一指”,我们断定 tsserver 会执行到这里。
淡如止水 TypeScript (九):通信过程 - 图6

发生在 attach 函数中,src/tsserver/server.ts#L325

  1. class ... implements ITypingsInstaller {
  2. ...
  3. attach(projectService: ProjectService) {
  4. ...
  5. this.installer = childProcess.fork(combinePaths(__dirname, "typingsInstaller.js"), args, { execArgv });
  6. this.installer.on("message", m => this.handleMessage(m));
  7. this.event({ pid: this.installer.pid }, "typingsInstallerPid");
  8. ...
  9. }
  10. ...
  11. }

我们来分析调用栈,
bin/tsserver#L2,加载 ../built/local/tsserver.js 文件,

  1. ...
  2. require('../built/local/tsserver.js');

VSCode 根据 source map 反查到 src/tsserver/server.ts 文件。

src/tsserver/server.ts#L976,加载过程中会执行,new IOSession

  1. namespace ts.server {
  2. ...
  3. const ioSession = new IOSession();
  4. ...
  5. }

new IOSession 会调用父类 Session 的构造函数,src/tsserver/server.ts#L506

  1. class IOSession extends Session {
  2. ...
  3. constructor() {
  4. ...
  5. super({
  6. ...
  7. });
  8. ...
  9. }
  10. ...
  11. }
  1. export class Session implements EventSender {
  2. ...
  3. constructor(opts: SessionOptions) {
  4. ...
  5. this.projectService = new ProjectService(settings);
  6. ...
  7. }
  8. ...
  9. }

Session 构造函数中,会调用 new ProjectServicesrc/server/editorServices.ts#L430

  1. export class ProjectService {
  2. ...
  3. constructor(opts: ProjectServiceOptions) {
  4. ...
  5. this.typingsInstaller.attach(this);
  6. ...
  7. }
  8. ...
  9. }

接着调用了 this.typingsInstaller.attachsrc/tsserver/server.ts#L282

  1. class NodeTypingsInstaller implements ITypingsInstaller {
  2. ...
  3. attach(projectService: ProjectService) {
  4. ...
  5. this.installer = childProcess.fork(combinePaths(__dirname, "typingsInstaller.js"), args, { execArgv });
  6. this.installer.on("message", m => this.handleMessage(m));
  7. this.event({ pid: this.installer.pid }, "typingsInstallerPid");
  8. ...
  9. }
  10. ...
  11. }

attach 函数中,又启动了一个子进程,为全局 TypeScript 缓存安装类型依赖。
淡如止水 TypeScript (九):通信过程 - 图7

我们看到新启动的进程 --inspect-brk=9003,相当于这样调用,

  1. $ node --inspect-brk=9003 /Users/.../TypeScript/built/local/typingsInstaller.js \
  2. --globalTypingsCacheLocation /Users/.../Library/Caches/typescript/3.7 \
  3. --typesMapLocation /Users/.../TypeScript/built/local/typesMap.json \

built/local/typingsInstaller.js 会在 /Users/.../Library/Caches/typescript/3.7 这个位置安装依赖。
这里的逻辑暂时先不用在意。

安装完依赖之后,attach 函数调用了 this.event,向 stdout 发消息。
由于主进程中监控了 tsserver 子进程的 stdout 事件。
所以,启动 tsserver 之后,主进程会先收到一条消息。

淡如止水 TypeScript (九):通信过程 - 图8

消息内容如下,

  1. Content-Length: 76
  2. {"seq":0,"type":"event","event":"typingsInstallerPid","body":{"pid":19087}}

3. 与 tsserver 的交互

3.1 command: open

了解了 tsserver 的启动事件之后,client 端就不会被莫名其妙的一条消息搞糊涂了。
现在我们取消 client 端 index.jschild.stdin.write 相关的注释,与上一篇内容一致。

  1. const path = require('path');
  2. const { spawn } = require('child_process');
  3. const root = '/Users/.../TypeScript'; // <- 这是 TypeScript 源码仓库的根目录
  4. const child = spawn('node', [
  5. '--inspect-brk=9002',
  6. path.join(root, 'bin/tsserver'),
  7. ]);
  8. child.stdout.on('data', data => {
  9. console.log(data.toString());
  10. });
  11. child.on('close', code => {
  12. console.log(code);
  13. });
  14. const filePath = path.join(root, 'debug/index.ts');
  15. const openFile = {
  16. seq: 0,
  17. type: 'request',
  18. command: 'open',
  19. arguments: {
  20. file: filePath,
  21. }
  22. };
  23. const getQuickInfo = {
  24. seq: 1,
  25. type: 'request',
  26. command: 'quickinfo',
  27. arguments: {
  28. file: filePath,
  29. line: 1,
  30. offset: 7
  31. }
  32. };
  33. child.stdin.write(`${JSON.stringify(openFile)}\n`);
  34. child.stdin.write(`${JSON.stringify(getQuickInfo)}\n`);

重新启动 client 端,然后 attach server 端。
server 端启动事件执行完之后,我们继续运行 client 端到 child.stdin.write 位置。
淡如止水 TypeScript (九):通信过程 - 图9
注意,child.stdin.write 尾部 \n 换行符。

然后我们去 server 端 src/tsserver/server.ts#L576 打个断点,

  1. class IOSession extends Session {
  2. ...
  3. listen() {
  4. rl.on("line", (input: string) => {
  5. const message = input.trim();
  6. this.onMessage(message);
  7. });
  8. ...
  9. }
  10. }

client 端继续执行,就会发现 server 端跑到了断点中,
淡如止水 TypeScript (九):通信过程 - 图10

我们看到 message 的值正是 child.stdin.write 发送过来的。
接着 server 端会处理这个消息。

不幸的是,这段消息是一个 open command,

  1. {
  2. seq: 0,
  3. type: 'request',
  4. command: 'open', // open 类型的 command
  5. arguments: {
  6. file: filePath,
  7. }
  8. }

tsserver 对于 open command 并不会向主进程返回消息。
所以,主进程并不会收到任何消息。
我们在 server 端按 F5 让它跑完。

3.2 command: quickinfo

上文我们了解到 child.stdin.write 发送的一条 open command 并没有返回任何消息给主进程,
我们让 server 端代码继续执行了。

现在回到 client 端,继续执行下一条 child.stdin.write
淡如止水 TypeScript (九):通信过程 - 图11

server 端立即收到了新消息,进入断点中,
淡如止水 TypeScript (九):通信过程 - 图12

message 内容正好是 child.stdin.write 写入的内容。

  1. {
  2. seq: 1,
  3. type: 'request',
  4. command: 'quickinfo', // quickinfo 类型的 command
  5. arguments: {
  6. file: filePath,
  7. line: 1,
  8. offset: 7
  9. }
  10. }

这是一条 quickinfo command 执行完毕之后,tsserver 是会向主进程返回消息的。
我们来看会返回什么,于是 server 端按 F5 执行完。

client 端的断点会跑到 child.stdout.on data 事件中,并且会连续进入两次,
第一次,会打印 tsserver 启动事件发回的消息,
第二次,并不是打印 open command 的消息(因为它不返回消息),而是打印了 quickinfo command 返回的消息。
淡如止水 TypeScript (九):通信过程 - 图13

  1. Content-Length: 76
  2. {"seq":0,"type":"event","event":"typingsInstallerPid","body":{"pid":19563}}
  3. Content-Length: 245
  4. {"seq":0,"type":"response","command":"quickinfo","request_seq":1,"success":true,"body":{"kind":"const","kindModifiers":"","start":{"line":1,"offset":7},"end":{"line":1,"offset":8},"displayString":"const i: number","documentation":"","tags":[]}}

最后,我们来看看 quickinfo command 返回了什么内容,
关键内容是 displayString 的内容,

  1. const i: number

这正是我们在 debug/index.ts 文件中,鼠标悬停到 i 标识符上展示的内容,

  1. const i: number = 1;

淡如止水 TypeScript (九):通信过程 - 图14

而我们传入的 quickinfo command 参数中,

  1. {
  2. seq: 1,
  3. type: 'request',
  4. command: 'quickinfo',
  5. arguments: {
  6. file: filePath,
  7. line: 1, // 第 1 行
  8. offset: 7 // 第 7 个字符,刚好是 i
  9. }
  10. }

1 行(line: 1),第 7 个字符(offset: 7),刚好是 i


总结

本文跟踪了 tsserver 启动事件,以及 openquickinfo 两个 command 的消息交互过程。
tsserver 端的业务逻辑,我们没有详细追究。

但是留意到,tsserver 启动时,不接受任何消息,也会主动向主进程发送一条 typingsInstallerPid 消息,

  1. Content-Length: 76
  2. {"seq":0,"type":"event","event":"typingsInstallerPid","body":{"pid":19087}}

其次,open command 并不会返回任何消息,
最后,quickinfo command 会返回 debug/index.ts 中变量 i 鼠标悬停上去展示的内容,
详见 displayString 的值。

  1. Content-Length: 245
  2. {"seq":0,"type":"response","command":"quickinfo","request_seq":1,"success":true,"body":{"kind":"const","kindModifiers":"","start":{"line":1,"offset":7},"end":{"line":1,"offset":8},"displayString":"const i: number","documentation":"","tags":[]}}

参考

TypeScript v3.7.3