動態修改 NodeJS 程式中的變數值

OneAPM官方技術部落格發表於2015-07-01

如果一個 NodeJS 程式正在執行,有辦法修改程式中的變數值麼?答案是:通過 V8 的 Debugger 介面可以!本文將詳細介紹實現步驟。

啟動一個 HTTP Server

用簡單的 Hello World 做例子吧,不過略作修改。在 global 下放一個變數 message, 然後列印出來:

// message content will be modified !
global.message = "hello world!";

var server = require('http').createServer(function (req, res) {
  res.end(global.message);
}).listen(8001);

console.log('pid = %d', process.pid);

用命令啟動 Server,此時,通過用瀏覽器訪問 http://localhost:8001 可以看到網頁內容是 hello world!。 接下來我們將嘗試在不改變程式碼,不重啟程式的情況下把 message 換成 "hello bugs!"

使 Server 程式進入 Debug 模式

V8 引擎在實現的時候留了 Debugger 介面。 通過命令 node --debug-brk=5858 [filename] 可以啟動一個指令碼,並且立即進入 Debug 模式。

那麼如果是已經執行著的 NodeJS 程式,可以進入 Debug 模式嗎?也是可以的,在作業系統下給 NodeJS 的程式發一個 SIGUSR1 訊號,可以讓程式進入 Debug 模式。 進入 Debug 模式的程式會在本地啟動一個 TCP Server 並且預設監聽5858 埠。

此時在另一個命令列視窗執行命令 node debug localhost:5858 就可以連線到 Debugger 除錯埠, 並且可以使用很多常用的 Debug 命令,比如 c繼續執行,s 步入, o步出等。

Debugger 協議

使用node debug hostname:port 命令連線到程式進行 Debug 的方式比較簡單,但是要完成一些高階的功能就會處處受限,因為它只封裝了 Debugger 協議中 command 的一部分。 下面介紹一下這個簡單的協議。

Client 和 Server 的通訊是通過 TCP 進行的。 DebugClient 第一次連線到 DebugServer 的時候會拿到一些 Header,比如 Node 版本, V8 版本等。後面緊跟著一個空的訊息1

訊息1

Type: connect\r\n
V8-Version: 3.28.71.19\r\n
Protocol-Version: 1\r\n
Embedding-Host: node v0.12.4\r\n
Content-Length: 0\r\n
\r\n

訊息實體由 Header 和 Body 組成,訊息1的 Body 為空,所以 Header 中對應的 Content-Length 為 0。而在下面這個例子裡,Body 為一個單行的 JSON 字串,這是由協議所規定的。

訊息2

Content-Length: 46\r\n
\r\n
{"command":"version","type":"request","seq":1}

訊息2的型別( type )是request,代表這是 Client 發給 Server 的命令,其他的可能值是 responseevent分別代表 Server 對 Client 的相應,和 Server 端發生的事件。

訊息3

Content-Length: 137\r\n
\r\n
{"seq":1,"request_seq":1,"type":"response","command":"version","success":true,"body":     {"V8Version":"3.28.71.19"},"refs":[],"running":true}

訊息2是 Client 傳送給 Server的,訊息3是 Server 對 Client 的相應,那麼如何判斷訊息3是不是訊息2的結果呢?可以看到訊息2中的 seq 值是1,而 訊息3中的request_seq值是1。 Debugger 協議正是通過這兩個值把非同步返回的結果和請求一一對應起來的。

Debugger 協議就是這麼的簡單。

例項化一個 Debugger Client

瞭解了 Debugger 協議後,相信好奇心強的程式設計師已經躍躍欲試自己實現一個了。本著不重複發明輪子的原則開始在網上找實現,找了好久找到這個庫 pDebug, 可惜這個庫已經好久不更新了。後來通過閱讀 node-inspector 的原始碼才發現,其實 NodeJS 自帶了一個 Debugger 模組, 相關程式碼在 _debugger 模組裡(原始碼),由於模組名是以 _ 開頭的,所以網上找不到它的 API,好在程式碼註釋寫的非常詳細,很快就能上手。

我們需要的正是這個模組下的 Client, 而 Client 其實是繼承於 Socket 的.

var Client = require('_debugger').Client;
var client = new Client();

client.connect(5858);
client.on('ready', function () {
    // 連線成功
});

通過 Debugger 介面執行命令

接下來我們來看看如何修改這個 global 的變數,程式碼如下

function modifyTheMessage(newMessage) {
    var msg = {
        'command': 'evaluate',
        'arguments': {
            'expression': 'global.message="' + newMessage + '"',
            'global': true
        }
    };
    client.req(msg, function (err, body, res) {
        console.log('modified to %s', newMessage);
    });
}

client.req方法封裝了 type=request 訊息型別 和seq 自增的邏輯,因此在構造 msg JSON物件的時候不需要指明這兩個屬性。 我們要修改 message 其實就是在 JavaScript 呼叫的頂層執行 global.message=newMessage

總結

此時,再訪問http://localhost:8001 可以看到網頁上顯示的內容已經由 'hello world!' 變成了 'hello bugs!',是不是很神奇。

這種方式也帶來了很多可能性:

  • 動態修改配置 線上的伺服器不用重啟就可以應用新的配置

  • 模組注入 通過其他任意語言編寫的應用程式為已經執行的 NodeJS 程式注入新的模組

  • 效能監控 可以剝離使用者線上程式碼對第三方效能監控模組的直接依賴

  • 錯誤監控 發生異常時,通過 Debugger 可以抓到發生錯誤的函式和行號,並且抓取各個呼叫棧中的每一個變數,即使是在閉包裡

  • Chrome 除錯 由於 Chrome 也是基於 V8 的,上述方法也可以用於 Chrome 相關的功能整合

關於

  1. 本文相關的原始碼在: https://github.com/wyvernnot/interference_demo;

  2. 如果你也對 Debugger 協議感興趣,可以安裝 oneapm-debugger 這個工具,它可以幫助你檢視 Debug 過程中所有實際傳送的資料。 oneapm-debugger


本文系OneAPM工程師編譯整理。OneAPM是中國基礎軟體領域的新興領軍企業,能幫助企業使用者和開發者輕鬆實現:緩慢的程式程式碼和SQL語句的實時抓取。想閱讀更多技術文章,請訪問OneAPM官方技術部落格

相關文章