美文网首页Node.js
[Node] 淡如止水 TypeScript (九):通信过程

[Node] 淡如止水 TypeScript (九):通信过程

作者: 何幻 | 来源:发表于2020-01-02 19:33 被阅读0次

    0. 回顾

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

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

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

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

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

    1. 启动调试

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

    然后按以下步骤启动调试。

    (1)client 端,按 F5 启动调试


    client 端执行完 spawn 后,tsserver 就启动了。

    (2)server 端,按 F5 attach 到已经启动的 tsserver


    我们看到两个 VSCode 实例,都停在了断点处。

    (3)server 端 lib/tsserver 的调试,仍然会遇到无法进入 .ts 的问题
    与第二篇一样,我们需要在 require 的时候,点击 Step Into,进入 src/compiler/core.ts#L1 中。


    2. tsserver 启动事件

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

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

    const path = require('path');
    const { spawn } = require('child_process');
    
    const root = '/Users/.../TypeScript';  // <- 这是 TypeScript 源码仓库的根目录
    
    const child = spawn('node', [
      '--inspect-brk=9002',
      path.join(root, 'bin/tsserver'),
    ]);
    
    child.stdout.on('data', data => {
      console.log(data.toString());
    });
    
    child.on('close', code => {
      console.log(code);
    });
    
    // 注释掉了 child.stdin.write
    

    然后我们启动 client 端,再 attach server 端。
    通过 “灵犀一指”,我们断定 tsserver 会执行到这里。

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

    class ... implements ITypingsInstaller {
      ...
    
      attach(projectService: ProjectService) {
        ...
        this.installer = childProcess.fork(combinePaths(__dirname, "typingsInstaller.js"), args, { execArgv });
        this.installer.on("message", m => this.handleMessage(m));
    
        this.event({ pid: this.installer.pid }, "typingsInstallerPid");
        ...
      }
    
      ...
    }
    

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

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

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

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

    namespace ts.server {
      ...
      const ioSession = new IOSession();
      ...
    }
    

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

    class IOSession extends Session {
      ...
      constructor() {
        ...
        super({
          ...
        });
    
        ...
      }
      ...
    }
    
    export class Session implements EventSender {
      ...
    
      constructor(opts: SessionOptions) {
        ...
        this.projectService = new ProjectService(settings);
        ...
      }
    
      ...
    }
    

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

    export class ProjectService {
      ...
    
      constructor(opts: ProjectServiceOptions) {
        ...
        this.typingsInstaller.attach(this);
        ...
      }
    
      ...
    }
    

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

    class NodeTypingsInstaller implements ITypingsInstaller {
      ...
    
      attach(projectService: ProjectService) {
        ...
        this.installer = childProcess.fork(combinePaths(__dirname, "typingsInstaller.js"), args, { execArgv });
        this.installer.on("message", m => this.handleMessage(m));
    
        this.event({ pid: this.installer.pid }, "typingsInstallerPid");
        ...
      }
    
      ...
    }
    

    attach 函数中,又启动了一个子进程,为全局 TypeScript 缓存安装类型依赖。

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

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

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

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

    消息内容如下,

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

    3. 与 tsserver 的交互

    3.1 command: open

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

    const path = require('path');
    const { spawn } = require('child_process');
    
    const root = '/Users/.../TypeScript';  // <- 这是 TypeScript 源码仓库的根目录
    
    const child = spawn('node', [
      '--inspect-brk=9002',
      path.join(root, 'bin/tsserver'),
    ]);
    
    child.stdout.on('data', data => {
      console.log(data.toString());
    });
    
    child.on('close', code => {
      console.log(code);
    });
    
    const filePath = path.join(root, 'debug/index.ts');
    
    const openFile = {
      seq: 0,
      type: 'request',
      command: 'open',
      arguments: {
        file: filePath,
      }
    };
    const getQuickInfo = {
      seq: 1,
      type: 'request',
      command: 'quickinfo',
      arguments: {
        file: filePath,
        line: 1,
        offset: 7
      }
    };
    
    child.stdin.write(`${JSON.stringify(openFile)}\n`);
    child.stdin.write(`${JSON.stringify(getQuickInfo)}\n`);
    

    重新启动 client 端,然后 attach server 端。
    server 端启动事件执行完之后,我们继续运行 client 端到 child.stdin.write 位置。

    注意,child.stdin.write 尾部 \n 换行符。

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

    class IOSession extends Session {
      ...
    
      listen() {
        rl.on("line", (input: string) => {
          const message = input.trim();
          this.onMessage(message);
        });
        
        ...
      }
    }
    

    client 端继续执行,就会发现 server 端跑到了断点中,


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

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

    {
      seq: 0,
      type: 'request',
      command: 'open',  // open 类型的 command
      arguments: {
        file: filePath,
      }
    }
    

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

    3.2 command: quickinfo

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

    现在回到 client 端,继续执行下一条 child.stdin.write

    server 端立即收到了新消息,进入断点中,


    message 内容正好是 child.stdin.write 写入的内容。
    {
      seq: 1,
      type: 'request',
      command: 'quickinfo',  // quickinfo 类型的 command
      arguments: {
        file: filePath,
        line: 1,
        offset: 7
      }
    }
    

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

    client 端的断点会跑到 child.stdout.on data 事件中,并且会连续进入两次,
    第一次,会打印 tsserver 启动事件发回的消息,
    第二次,并不是打印 open command 的消息(因为它不返回消息),而是打印了 quickinfo command 返回的消息。

    Content-Length: 76
    
    {"seq":0,"type":"event","event":"typingsInstallerPid","body":{"pid":19563}}
    
    Content-Length: 245
    
    {"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 的内容,

    const i: number
    

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

    const i: number = 1;
    

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

    {
      seq: 1,
      type: 'request',
      command: 'quickinfo',
      arguments: {
        file: filePath,
        line: 1,   // 第 1 行
        offset: 7  // 第 7 个字符,刚好是 i
      }
    }
    

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


    总结

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

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

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

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

    Content-Length: 245
    
    {"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

    相关文章

      网友评论

        本文标题:[Node] 淡如止水 TypeScript (九):通信过程

        本文链接:https://www.haomeiwen.com/subject/yynooctx.html