通過Webkit遠端除錯協議監聽網頁崩潰

BlackHole1發表於2019-04-22

背景介紹

因為正在開發一個專案,而這個專案使用到了puppeteer,其中有個功能是在puppeteer開啟的chrome裡開啟多個Tab,並進行管理。 雖然puppeteer可以開啟多個網站,但是並不利於管理,所有我使用的是外掛的方式,通過外掛來開啟多網站,並進行管理。

但是這裡有個需求是,當網站崩潰時,我要做出一些操作。但是目前網上沒有一個好的辦法去監聽當前網站是否崩潰。

可能有同學會說:puppeteer不是提供了一個page.on('error', fn)的方法,來進行監聽麼?

請注意上文中提到的,使用外掛開啟多個網站,puppeteer提供的方法只能對自己開啟的網站起作用,沒有使用puppeteer開啟的網站,page.on('error', fn)方法無能為力。

使用Service Workers

這個方法是由我同事Haitao提出來的思路。

在當前網站上執行一個Service Workers,因為在執行的時候Service Workers會再啟動一個單獨的程式,當前網站和Service Workers是兩個單獨的程式。也就是說當網站崩潰時,並不影響Service Workers程式。所以可通過心跳檢測來進行判斷網站是否崩潰。

網上也有阿里的同學寫的相關文章:如何監控網頁崩潰?

但是我並沒有使用這個方式,因為當Service Workers崩潰了,那就沒有任何辦法了,可能有同學會說:網站和Service Workers互相發心跳檢測。這可能是一種辦法,但是我不太喜歡這種方式。

使用Webkit的遠端除錯協議

介紹

在開始前,我們先去看下puppeteer的原始碼,為什麼puppeteer可以監聽到網頁的崩潰。

其程式碼在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 遠端除錯協議分析與實戰

作者資訊

相關文章