经过Webkit远程调试协议监听网页崩溃

背景介绍

由于正在开发一个项目,而这个项目使用到了puppeteer,其中有个功能是在puppeteer打开的chrome里打开多个Tab,并进行管理。 虽然puppeteer能够打开多个网站,可是并不利于管理,全部我使用的是插件的方式,经过插件来打开多网站,并进行管理。html

可是这里有个需求是,当网站崩溃时,我要作出一些操做。可是目前网上没有一个好的办法去监听当前网站是否崩溃。node

可能有同窗会说:puppeteer不是提供了一个page.on('error', fn)的方法,来进行监听么?git

请注意上文中提到的,使用插件打开多个网站,puppeteer提供的方法只能对本身打开的网站起做用,没有使用puppeteer打开的网站,page.on('error', fn)方法无能为力。github

使用Service Workers

这个方法是由我同事Haitao提出来的思路。web

在当前网站上运行一个Service Workers,由于在运行的时候Service Workers会再启动一个单独的进程,当前网站和Service Workers是两个单独的进程。也就是说当网站崩溃时,并不影响Service Workers进程。因此可经过心跳检测来进行判断网站是否崩溃。chrome

网上也有阿里的同窗写的相关文章:如何监控网页崩溃?json

可是我并无使用这个方式,由于当Service Workers崩溃了,那就没有任何办法了,可能有同窗会说:网站和Service Workers互相发心跳检测。这多是一种办法,可是我不太喜欢这种方式。websocket

使用Webkit的远程调试协议

介绍

在开始前,咱们先去看下puppeteer的源码,为何puppeteer能够监听到网页的崩溃。socket

其代码在lib/Page.js文件里。函数

首先能够看到Page是一个Class,其继承了EventEmitterEventEmitterpage提供了on方法,也就是咱们以前看到的:page.on('error', fn)

从这里就可知,在Page Class里,有地方调用了this.emit('error')来触发error event。搜了一下,发现其代码在_onTargetCrashed方法里。如:

Imgur

触发crash的方法,咱们找到了。那这个_onTargetCrashed又是在哪触发的呢?

Imgur

可见,是一个叫client的方法监听到了Inspector.targetCrashed事件,而这个事件触发了_onTargetCrashed函数,clinet方法就再也不跟了,由于跳地方较多,只须要知道,最终client是一个websocket的产物。而websocket建立的代码在lib/Launcher.js里。代码位置

Imgur

注意这两行:

const transport = new PipeTransport((chromeProcess.stdio[3]), (chromeProcess.stdio[4]));

connection = new Connection('', transport, slowMo);
复制代码

chromeProcessnodejs中的spawn产物,代码为:

代码位置

const chromeProcess = childProcess.spawn(
  chromeExecutable,
  chromeArguments,
  {
    detached: process.platform !== 'win32',
    env,
    stdio
  }
);
复制代码

其中chromeArgumentschrome启动的参数列表,此列表是有一个--remote-debugging-的:

代码位置

if (!chromeArguments.some(argument => argument.startsWith('--remote-debugging-')))
  chromeArguments.push(pipe ? '--remote-debugging-pipe' : '--remote-debugging-port=0');
复制代码

如今就明朗多了,Inspector.targetCrashed这个事件,是由Webkit远程调试协议也就是remote debugging protocol提供的。

其定义在webkit的Inspector.json里: Source/WebCore/inspector/Inspector.json#L39-L42

关于这个event的commit url为:github.com/WebKit/webk…

编写解决方案代码

如今咱们知道了,只要能监听到Inspector.targetCrashed事件,就能够知道网站是否关闭了。咱们先在puppeteer的启动参数里,增长一行启动参数:

puppeteer.launch({
  '--remote-debugging-port=9222',
  // other args
});
复制代码

puppeteer启动时,会监听本地的9222端口,其中路径/json为当前的详情。如:

Imgur

其格式为:

[
  {
    "description": "",
    "devtoolsFrontendUrl": "/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/A1CB5A9CC25A7EE8A99C6A4A1876E4D3",
    "faviconUrl": "https://s.ytimg.com/yts/img/favicon_32-vflOogEID.png",
    "id": "A1CB5A9CC25A7EE8A99C6A4A1876E4D3",
    "title": "張三李四 Chang and Lee 【等無此人 Waiting】 - YouTube",
    "type": "page",
    "url": "https://www.youtube.com/watch?v=lAcUGvpRkig&list=PL3p0C_7POnMHG-b0dzkeTVdNuM6yRE5iQ&index=10&t=0s",
    "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/A1CB5A9CC25A7EE8A99C6A4A1876E4D3"
  },
  // other
{
复制代码

其中的type为当前进程的详情:

  • page: 网页
  • iframe: 网页嵌套的iframe
  • background_page: 插件页面
  • service_worker: Service Workers

这个type的做用在于,你只想监听某一类型的崩溃。

还有一个更主要的字段:webSocketDebuggerUrl。咱们将使用这个字段的值,来进行获取消息。有一个简单的demo:

const http =  require('http');
const WebSocket = require('ws');

http.get('http://127.0.0.1:9222/json', res => {
  res.addListener('data', data => {
    const result = JSON.parse(data.toString());
    result.forEach(info => {
      const client = new WebSocket(info.webSocketDebuggerUrl);
      client.on('message', data => {
        if (data.indexOf('"method":"Inspector.targetCrashed"') !== -1) {
          console.error('crash!');
        }
      });
    });
  })
})
复制代码

先看懂这段代码,后面的代码才好理解,由于代码过于简单,这里就再也不介绍了。

这段代码有个问题是,插件打开网站时,会存在必定的延迟,可能会致使某些网站没有被监听到,并且当这段代码运行后,插件再打开网站时,也不会监听到。针对这个问题,优化了下代码:

const http =  require('http');
const WebSocket = require('ws');

module.exports = () => {
  const wsList = {};
  let crashStaus = false;

  const getWsList = () => {
    return new Promise((resolve) => {
      http.get('http://127.0.0.1:9222/json', res => {
        res.addListener('data', data => {
          try {
            const result = JSON.parse(data.toString());
            const tempWsList = {};

            result.forEach(info => {
              if (typeof wsList[info.id] === 'undefined') {
                tempWsList[info.id] = info.webSocketDebuggerUrl;
                wsList[info.id] = info.webSocketDebuggerUrl;
              }
            });

            if (Object.keys(tempWsList).length !== 0) {
              resolve(tempWsList);
            }
          } catch (e) {
            console.error(e);
          }
        });
      });
    });
  };

  setInterval(() => {
    getWsList().then(list => {
      Object.values(list).forEach(wsUrl => {
        const client = new WebSocket(wsUrl);
        client.on('message', data => {
          if (data.indexOf('"method":"Inspector.targetCrashed"') !== -1) {
            if (!crashStaus) {
              crashStaus = true;
              console.log('crash!!!');
            }
          }
        });
      })
    });
  }, 1000);
};
复制代码

其中须要说明一下这段代码:

if (!crashStaus) {
  crashStaus = true;
  console.log('crash!!!');
}
复制代码

由于个人需求是,任何一个进程crash了,就关闭整个服务,从新运行。因此若是有多个进程同时crash了。个人代码只走一次,不想让他走屡次。这个是针对我这里的需求,各位同窗能够根据本身的需求更改代码。

参考

Webkit 远程调试协议初探

Chrome 远程调试协议分析与实战

做者信息

其余

我司(爱乐奇)招人,感兴趣的小伙伴能够来投简历呀。

弹性工做制、每日水果、同事都特别nice、96五、团建、五险一金...

地点上海浦软大厦

相关文章
相关标签/搜索