最近更新: 2011-12-27

GJS - D-Bbus 自動內觀(Introspect)與配置代理個體

我之前在「JavaScript 與 Desktop - DBus」說明 gjs 如何調用 D-Bus 服務時,提到 gjs 提供的 D-Bus 實作內容相當低階,以致於我們必須要自己定義我們想要調用的 D-Bus 代理個體的介面原型。在該文中我也提到,大部份的 D-Bus 服務都會實作 org.freedesktop.DBus.Introspectable 介面,提供 Introspect 方法(D-Bus 的 introspect 在 Java/C# 中採用的說法就是 reflect),讓其他人可以藉由這個方法查看介面規格。我們查詢的結果會是一份 XML 文件,若我們進一步分析該文件,就可以直接將分析結果交給 DBus.proxifyPrototype() 注入指定的代理類別。

本文就是想要利用 gjs 實作的 E4X 能力,去解析遠端個體傳回的內觀資訊(introspection),由程式自行產生 gjs dbus 模組所需的介面原型敘述,再注入成為新的類別。免除由程序員自己手寫介面原型敘述的不便。

程式概觀

我的實作程式主要有兩個工作函數。第一個工作函數 IntrospectPrototype() 負責取得指定對象的介面原型表。第二個工作函數 ProxyObjectFactory() 則產生 D-Bus 遠端個體的代理者。

IntrospectPrototype

IntrospectPrototype() 負責調用指定對象的 DBus Introspect() 方法取回其內觀資訊(XML文件),再利用 gjs 的 E4X 能力解析成為 gjs DBus.proxifyPrototype 預期的介面原型表,將此表回傳。

DBus.proxifyPrototype 要求的介面原型資訊表之格式如下:

{
  介面名稱:
  {
    name: "介面名稱",
    methods: [
      { name: "方法名稱", inSignature: "型態簽名", outSignature: "型態簽名"},
      ...(可以有多個方法)
    ],
    signals: [
      { name: "訊號名稱", inSignature: "型態簽名", outSignature: "型態簽名"},
      ...(可以有多個訊號)
    ]
},
...(可以有多個介面)
}

在以網頁瀏覽器為載具的 JavaScript 環境中,我們習慣利用 DOM API 處理 XML 文件內容。但在以 gjs 為載具的環境中,並沒有提供 DOM API ,故我們不能用 DOM API 處理 XML 文件。所幸 gjs 支援 E4X (ECMAScript for XML) 規格,所以我們可以利用 gjs 本身的語言能力處理 XML 文件。這也使本文的實作工作簡單許多。

關於 E4X 的使用,可以參考「ECMAScript E4X」。另一個常常與 gjs 相提的 Seed,並不支援 E4X,所以 Seed 不能使用本文實作的程式。

ProxyObjectFactory

ProxyObjectFactory() 是 D-Bus 遠端個體代理者工廠。它會產生一個新類別(匿名的類別),注入 IntrospectPrototype() 所取回的介面原型資訊,產生我們所需要的 D-Bus 服務的代理個體。

ProxyObjectFactory() 要 4 個必要參數項目,分別指出你要連接的 DBus 類型、服務名稱、遠端個體路徑以及要存取的介面名稱。此外還要一個額外的參數項目,依程式流程,給予一個回呼函數或是一個介面原型表。

關於在 JavaScript 中產生匿名類別的技巧,請參考「JavaScript - 產生類別的類別」。

具內觀能力的 dbus.js

我直接繼承 gjs 原本的 dbus 模組,但為了與原本的有所區別,我將我寫的 dbus.js 放在名為 rock 的名稱空間中。這牽涉到 gjs 使用名稱空間的技巧,在後面的使用範例中會略加說明。

/*************************************************************
rock.dbus
Copyright 2011 rock <shirock.tw@gmail.com>
License: GNU LGPL (http://www.gnu.org/licenses/lgpl.html)
*************************************************************/
const _Lang = imports.lang;
_Lang.copyProperties(imports.dbus, this);

/**
分析輸出端點的 XML 資料,轉成下列的表格形式:
  {
    name: 端點名稱,
    inSignature: 輸入參數的 DBus 型態簽名,
    outSignature: 輸出參數的 DBus 型態簽名
  };
@param export_node : DBus 輸出端點的 XML 資料。
 它可以是 DBus Method,也可以是 DBus Signal。
@return false | 參數內容表.
 */
function _parseDIXArgs(export_node) {
    var po = { name: false, inSignature: '', outSignature: '' };
    po.name = export_node.@name.toString();
    if (po.name == null || po.name.length == 0)
        return false;

    var n = 0, i = 0;
    /*
    n = export_node.arg.length();
    print("\t\tNumber of Arg: " + n);
    var arg_node;
    for (var i = 0; i < n; ++i) {
        arg_node = export_node.arg[i];
        // <arg name="interface" direction="in" type="s"/>
        print("\t\tArg name: "  + arg_node.@name +
              "; dir: "         + arg_node.@direction +
              "; type: "        + arg_node.@type)
    }
    */

    var ds = {inSignature: 'in', outSignature: 'out'};
    var d, args, sig;
    for (d in ds) {
        try {
            args = export_node.arg.(@direction == ds[d]);
        }
        catch(e) {
            if (e.name == "ReferenceError")
                args = export_node.arg;
            else
                throw e;
        }
        n = args.length();
        if (n > 0) {
            sig = []
            for (i = 0; i < n; ++i) {
                sig.push(args[i].@type);
            }
            po[d] = sig.join('');
        }
    }
    return po;
}

/**
分析 DBus Introspect XML 內容。
分析輸出端點的 XML 資料,轉成 gjs DBus.proxifyPrototype 要求的表格形式。
@param introspection Introspect 回傳的 XML 原始文件。
@return DBus介面原型表
 */
function _parseDBusIntrospectXML(introspection) {
    var x = new XML(introspection.replace(/<!DOCTYPE [^>]+>/,'')); // gjs supports E4X.

    // The DOCTYPE description of DBus XML look like illegal for E4X,

    // so I need to strip it.


    var export_types = {method: 'methods', signal: 'signals'};
    var extype, extypeval;
    var number_export = 0, export_node;
    var iface_table = {};
    var iface_node, iface_desc, result;

    for(var i = 0; i < x.interface.length(); ++i) {
        iface_node = x.interface[i];
        iface_desc = {
            name: iface_node.@name.toString(),
            methods: [],
            signals: []
        };

        for (extype in export_types) {
            extypeval = export_types[extype];
            number_export = iface_node[extype].length();
            if (number_export > 0) {
                iface_desc[extypeval] = [];
                for(var j = 0; j < number_export; ++j) {
                    export_node = iface_node[extype][j];
                    result = _parseDIXArgs(export_node);
                    if (result)
                        iface_desc[extypeval].push(result);
                }
            }
        } //foreach export_types

        iface_table[iface_desc.name] = iface_desc;
    } // foreach interface

    return iface_table;
}

/**
內觀 DBus 服務的原型資訊。
@return prototypes 內觀所得的原型資訊表(prototypes)。
 如果指定的 DBus 服務沒有實作 Introspectable 介面,則 prototypes 為 false。
 */
function IntrospectPrototype(bus, service_name, object_path, callback) {
    var introspectableProxy = new IntrospectableProxy(bus,
            service_name, object_path);

    var prototypes = false;
    var mainloop = imports.mainloop;

    introspectableProxy.IntrospectRemote(function(result){
        if (result != null && result.length > 0)
            prototypes = _parseDBusIntrospectXML(result);
        if (callback == undefined)
            mainloop.quit("");
        else
            callback(prototypes);
    },
    CALL_FLAG_START);

    if (callback == undefined)
        mainloop.run("");
    return prototypes;
}

/**
DBus 遠端個體代理者工廠.
args:
 required:
    bus
    service_name
    object_path
    interface_name

 option:
    interface_prototype
    callback
 */
function ProxyObjectFactory(args)
{
    var poc = false;    // proxy object class

    var po = false;     // proxy object

    var proto = false;  // prototype


    function _construct_proxy_object(proto) {
        poc = new Function("this._init();");
        poc.prototype = {
            _init: function() {
	            args.bus.proxifyObject(this, args.service_name, args.object_path);
            }
        };
        // 將介面原型注入代理類別

        proxifyPrototype(poc.prototype, proto);
        po = new poc();
        poc = false; // release

    }

    if (args.interface_prototype) {
        proto = args.interface_prototype;
    }
    else if (args.callback) {
        IntrospectPrototype(args.bus, args.service_name, args.object_path,
            function(protos){
                if (protos[args.interface_name] == null)
                    return false;
                proto = protos[args.interface_name];
                _construct_proxy_object(proto);
                return args.callback(po);
            });
        return true;
    }
    else {
        var protos = IntrospectPrototype(args.bus, args.service_name, args.object_path);
        if (protos[args.interface_name] == null)
            return false;
        proto = protos[args.interface_name];
        protos = false; //release

    }
    _construct_proxy_object(proto);

    if (args.callback)
        return args.callback(po);
    return po;
}

ProxyObjectFactory() 的參數項目 interface_prototypecallback 的差別在於使用時機,你必須根據你調用 ProxyObjectFactory() 的時機,指定正確的項目。這個「時機」,指的是你的程序狀態是否進入「事件迴圈」狀態。當你的程序進入「事件迴圈」狀態後,你必須指定 callback 。

之所以會有這種時機區別,原因在於 D-Bus 是一種非同步訊息機制,也可以說是一種事件機制。它的事件由 GNOME Library 的事件迴圈(event loop)輪詢觸發。GNOME Library 事件迴圈可以是 mainloop 也可以是 Gtk.main 。在本文中預設是用 mainloop 作為事件迴圈。當你的程序調用 mainloop.run() 之後,你的程序狀態就進入一個無限迴圈中,不停地輪詢是否有新的事件發生。當事件發生時,就調用已登記的回呼函數(callback)處理。

D-Bus 會把它的事件註冊在 GNOME Library 事件迴圈中,讓事件迴圈分派工作。所以在 gjs 中,所有的 D-Bus 方法,都應透過回呼函數處理結果。請參考「JavaScript 與 Desktop - DBus」的寫法,調用 D-Bus 方法的結果,並不是儲放在函數回傳值,而是由 callback 處理。

但本文實作時,卻遇到了一個矛盾。我希望程序在進入事件迴圈之前,就先透過 D-Bus 的 Introspect 方法取得 D-Bus 服務的內觀資訊。但要取得 D-Bus 服務的內觀資訊,就必須要進入事件迴圈之後,才可以觸發 D-Bus Introspect 方法的事件。簡單地說,我想要在事件之前得到訊息,但這訊息要在事件之後才能產生。本文實作時,就是遇到了這麼一個矛盾。

我解決這個矛盾的方法很簡單,我在 IntrospectPrototype() 設計了一個參數 callback。如果沒有指定這個參數,那就認為程序尚未進入事件迴圈。然後我就直接調用 mainloop.run() 等待 IntrospectRemote() 回傳的內觀資訊。當取回內觀資訊後,就馬上用 mainloop.quit() 脫離事件迴圈,讓程序繼續進行 IntrospectPrototype() 下一步的回傳動作。我在 IntrospectPrototype() 內設計了一個小迴圈等待我想要的事件發生,之後才回傳。這讓我的 IntrospectPrototype() 表現得像是一個同步函數。

但是在一個程序中(更精確地說是一個線程中),只能有一個事件迴圈。如果你的程序狀態已經進入了事件迴圈,那麼調用 mainloop.run() 的動作將會造成程式錯誤。所以我的 IntrospectPrototype()ProxyObjectFactory() 就依是否進入事件迴圈為區別,劃分了要給 callback 參數的時機與不給 callback 參數的時機。進入事件迴圈前,不必給 callback 參數;進入事件迴圈後,則必須給 calback 參數。

使用範例

//const DBus = imports.dbus;

const DBus = imports.rock.dbus;
const Mainloop = imports.mainloop;

// new way via rock.dbus.

// Case 1: 在程式狀態進入 mainloop 之前可用此形式。

let notifier = DBus.ProxyObjectFactory({
    bus: DBus.session,
    service_name:   'org.freedesktop.Notifications',
    object_path:    '/org/freedesktop/Notifications',
    interface_name: 'org.freedesktop.Notifications'});

const NetworkManagerServiceName = 'org.freedesktop.NetworkManager';

let network_manager = DBus.ProxyObjectFactory({
    bus:        DBus.system,
    service_name:       NetworkManagerServiceName,
    object_path:        '/org/freedesktop/NetworkManager',
    interface_name:     'org.freedesktop.NetworkManager'});

network_manager.GetDevicesRemote(function(paths){
    // 此間內容已處於 mainloop 狀態,必須使用下列兩種形式調用 DBus.ProxyObjectFactory.

    for (let i in paths) {
        print("Path: " + paths[i]);

        // Case 2: 若不知介面原型,則必須給予回呼函數.

        // async (callback) mode:

        DBus.ProxyObjectFactory({
            bus:    DBus.system,
            service_name:   NetworkManagerServiceName,
            object_path:    paths[i],
            interface_name: 'org.freedesktop.DBus.Properties' ,
            callback: function(network_device){
                network_device.GetAllRemote("org.freedesktop.NetworkManager.Device",
                function(result){
                    print("-- begin callback output (should display after sync output) --");
                    for (var p in result)
                        print("\t" + p + ": " + result[p]);
                    print("-- end callback output --");
                });
            }
        });

        // Case 3: 你給予介面原型(interface_prototype), 則流程可循序進行.

        // sync mode:

        let network_device = DBus.ProxyObjectFactory({
            bus:    DBus.system,
            service_name:   NetworkManagerServiceName,
            object_path:    paths[i],
            interface_name: 'org.freedesktop.DBus.Properties',
            interface_prototype: DBus.Properties});

        network_device.GetAllRemote("org.freedesktop.NetworkManager.Device", function(result){
            print("-- begin sync output --");
            for (var p in result)
                print("\t" + p + ": " + result[p]);
            print("-- end sync output --");
        });
   }
});

notifier.NotifyRemote(
    "appname", 0, "message-im", "Test", "end after 2 seconds", [], {}, -1,
    function(result) {
        print("result: " + result);
    },
    DBus.CALL_FLAG_START
);

var source_id = Mainloop.timeout_add_seconds(2, function(){
    print("quit");
    Mainloop.quit('');
    return false;
});

Mainloop.run('');

與「JavaScript 與 Desktop - DBus」的內容相比,最大的差異就在於此處的 demo.js 已經不再需要程序員定義 D-Bus 服務的原型資訊與類別了。這一切都由 rock.dbus 處理。demo.js 展示了三種不同的使用方式,其中有 callback 參數與沒有 callback 參數的方式,要根據前一節所說的使用時機決定。

下圖是 demo.js 執行於 Linux Mint 12 桌面的結果。

demo.js執行結果

順便說明 gjs 的名稱空間與模組管理方式。demo.js 第一行註解的是 gjs 內建的 dbus 模組,第二行使用了本文實作的 dbus 模組。那麼 gjs 是如何找到我自定的 dbus 模組呢? 首先, gjs 會先按 include path 找尋模組;其次, gjs 會將模組的名稱空間對應到目錄結構,根據名稱空間往 include path 底下的子目錄找尋相同主檔名的 js 文件。

gjs 預設的 include path 為 /usr/share/gjs-1.0。第一行註解的 imports.dbus 將指示 gjs 載入 /usr/share/gjs-1.0/dbus.js。第二行的 imports.rock.dbus 則將指示 gjs 載入 /usr/share/gjs-1.0/rock/dbus.js。如果你想要指定其他的路徑作為 gjs 的 include path ,執行 gjs 時請加上 -I--include-path=DIR 參數。

本文實作的 dbus.js,我在 Ubuntu 10.04 (gjs 0.5), Ubuntu 11.04 (gjs 0.7), Linux Mint 12 (gjs 1.3) 三種不同的 gjs 版本環境上都使用過。

樂多舊網址: http://blog.roodo.com/rocksaying/archives/18612549.html