經過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五、團建、五險一金...

地點上海浦軟大廈

相關文章
相關標籤/搜索