最近更新: 2010-10-19

JavaScript 與 Desktop - DBus

我先前在 ICOS 2010 記事 提及目前有多項軟體專案,正試圖將 Web 軟體開發經驗延伸到 Linux 桌面軟體開發領域。 在那之中,以 gjs 和 seed 這兩項專案的成果最接近實用階段。這兩套都是基於 C 與 GNOME Library 的 JavaScript 解譯器實作品。透過 GNOME Library 的 GObject introspection framework ,它們可以呼叫系統中所安裝的其他函數庫。故而它們可以用於開發一般的 Linux 桌面軟體。

我這兩天試用 Ubuntu 10.10 與 gnome-shell 時,同時嘗試著用 gjs 和 seed 撰寫一些小程式。首先嘗試的項目是透過 D-Bus 調用其他桌面軟體的服務。

請安裝 gjsseed 其中一套,以便執行本文的範例。Ubuntu 的使用者,在 Ubuntu 10.04/10.10 中,皆有提供 gjs 與 seed 的套件;Ubuntu 10.04 的 libgjs0 有版本衝突的bug,請勾選非正式版本套件庫(lucid-proposed repository)安裝非正式版本。其他散佈版本的使用者,可能要自行編譯。

gjs/seed 調用 D-Bus 之基本步驟

如果你曾經在其他程式語言中,使用過 D-Bus 功能,那麼你看到 JavaScript 目前提供的 D-Bus 調用能力時,應該會覺得它相當低階,仍有許多細節要由程序員自己處理。這使得 gjs/seed 目前在使用 D-Bus 時,顯得不是那麼簡便。

Message taking loop

在 gjs/seed 中調用 D-Bus 服務時,你要自己建立一個提供 D-Bus 使用的訊息處理迴圈。

seed 提供的範例中,進入訊息處理迴圈的程式碼如下:

var GLib = imports.gi.GLib;
var mainloop = GLib.main_loop_new();
GLib.main_loop_run(mainloop);

至於 gjs ,則本身提供了一個 mainloop 模組可用。如下列:

var Mainloop = imports.mainloop;
Mainloop.run('');

本文範例在 Ubuntu 10.04 與 10.10 上,分別以 gjs 和 seed 測試過。在撰寫過程中,我發現 Ubuntu 10.10 的 seed 相依的環境有問題。seed 的範例程式碼最後皆使用 GLib 的 main_loop_new() 建立訊息處理迴圈。但是它所使用的 glib binding ,並沒有定義 main_loop_new()main_loop_run() 函數,所以在調用遠端服務的 D-Bus 方法後,無法取得回傳訊息。我目前還無法確定這個問題是否僅存在於 Ubuntu 10.10 提供的 seed 套件身上。Ubuntu 10.04 倒是很正常。

Proxy object

在 gjs/seed 中,首先你必須為你嘗試調用的遠端個體,建立一個本地端的代理(proxy object)。所以我們要先定義一個代理個體的類別。至於 JavaScript 定義類別的方式,請自行學習。例如本部落格的其他相關文章,此處不複述。

下列為 gjs/seed 使用的 D-Bus 代理個體類別的固定範本:

function ProxyObjectClassName() {
    this._init();
};
ProxyObjectClassName.prototype = {
    _init: function() {
    // session bus or system bus

	DBus.session.proxifyObject (this,
				   'dbus service name',
				   'dbus service object path');
    }
};
Interface spec

定義了代理個體類別後,你還要描述你的調用對象的介面規格(在 Python 和 Ruby 中,可以內觀得之(introspect),不必程序員描述)。

gjs/seed 使用下列的表格形式描述介面規格:

{
    name: 'interface name',
    methods: [
        { name: 'Method name',
            inSignature: 'the type signature of input parameters',
            outSignature: 'the type signature of input values'
        }
        ,
        ...
    ],
    signals: [
        { name: 'Signal name',
            inSignature: '',
            outSignature: 'the type signature of input values'
        }
        ,
        ...
    ],
    properties: [
        { name: 'Property name',
            signature: 'the type signature of property',
            access: 'read or readwrite'
        }
        ,
        ...
    ]
}

當你填完上面那張表後,請將它交給 gjs/seed 實作的 DBus.proxifyPrototype() 方法。DBus.proxifyPrototype() 會把那張表描述的介面內容,注入我們指定的代理類別。

至於如何得知你的服務對象的 D-Bus 規格呢?途徑有二。一、查看它的文件有沒有描述。二、透過 DBus Introspectable 介面的 Introspect 方法查詢。我個人偏好第二種方式,因為它的查詢結果會很明確地告訴我型態簽名(D-Bus 規格的型態簽名之意義,請自行查詢),而不需我自己翻譯。下列示範如何利用 dbus-send 工具內觀 Notifications 的介面規格。

$ dbus-send --session --print-reply --dest=org.freedesktop.Notifications \
    /org/freedesktop/Notifications \
    org.freedesktop.DBus.Introspectable.Introspect
使用 callback 模式接受回傳值

不論你調用遠端個體的方法,或傾聽它的訊號,在 gjs/seed 中,都是使用 callback 模式接受回傳值。

調用遠端方法時,將你的 callback 函數放在最後一個參數;若你不需要回傳值,就不必提供 callback 函數。callback 函數將接收一個參數。如果遠端方法的回傳結果只有一個,那麼 callback 收到的參數就是一個變數;若有多個回傳結果,則你會得到一張回傳值表。請參考 dbus-notify.js

傾聽遠端訊號時,你的 callback 函數會接收至少一個參數。第一個參數是發訊個體,其他的參數值依序排列。請參考 dbus-notify.js

Notifications Notify

第一個撰寫的程式,將透過桌面管理程式提供的 Notifications 介面(See also: Desktop Notifications Specification ),在桌面上彈出提示訊息。這是衡量你的程式是否與桌面環境親密結合的一項功能指標。

下列為 dbus-notify.js 的程式碼。

const DBus = imports.dbus;
// 'const' 是 ECMAScript v5 的保留用字,目前尚未定義其用途。

// 在 gjs 和 seed 中,這個關鍵字的用途是定義一個常數。


function Notifications() { // 定義代理個體的類別

    this._init();
};
Notifications.prototype = {
    _init: function() {
	DBus.session.proxifyObject (this,
				   'org.freedesktop.Notifications',
				   '/org/freedesktop/Notifications');
    }
};
DBus.proxifyPrototype(Notifications.prototype, // 將介面內容注入代理類別

{   // 描述 org.freedesktop.Notifications 的介面內容

    name: 'org.freedesktop.Notifications',
    methods: [
        { name: 'GetServerInformation', inSignature: '', outSignature: 'ssss'},
        { name: 'Notify', inSignature: 'susssasa{sv}i', outSignature: 'u' }
    ],
    signals: [
        { name: 'NotificationClosed', inSignature: '', outSignature: 'uu' }
    ]
});

var notifier = new Notifications(); // 建立遠端個體在本地端的代理者


notifier.GetServerInformationRemote(
    function(result) {
        print(typeof result);
        for (var p in result)
            print(p + ': ' + result[p]);
    }
);

notifier.NotifyRemote(
    "appname", 0, "message-im", "Test", "body", [], {}, -1,
    function(result) {
        print("result: " + result);
    }
);

notifier.connect('NotificationClosed',
    function(emitter, id, reason) {
        print("Closed. ID: " + id + ", reason: " + reason);
        main_quit();
    }
);

function main_quit() {
    if (typeof Seed != 'undefined')
        Seed.quit();
    else
        Mainloop.quit('');
}

if (typeof Seed != 'undefined') {
    var GLib = imports.gi.GLib;
    var mainloop = GLib.main_loop_new();
    GLib.main_loop_run(mainloop);
}
else {
    var Mainloop = imports.mainloop;
    Mainloop.run('');
}

執行結果如下列所示,同時你會看到桌面上彈出一個訊息視窗:

$ dbus-send --session --print-reply --dest=org.freedesktop.Notifications \
    /org/freedesktop/Notifications \
    org.freedesktop.DBus.Introspectable.Introspect

$ gjs dbus-notify.js
object
0: notify-osd
1: Canonical Ltd
2: 1.0
3: 1.1
result: 17
Closed. ID: 17, reason: 1

NetworkManager and Introspectable

第二個範例將調用 NetworkManager 的 org.freedesktop.NetworkManager.GetDevices 方法以及 org.freedesktop.DBus.Introspectable.Introspect 。

GetDevices 方法可得知目前有幾個網路設備可用。

大部份的 D-Bus 服務都會實作 org.freedesktop.DBus.Introspectable 介面,提供 Introspect 方法。讓其他人可以藉由這個方法查看介面規格。我們查詢的結果會是一份 XML 文件,若我們進一步分析該文件,就可以直接將分析結果交給 DBus.proxifyPrototype() 注入指定的代理類別,而不必自己描述。在此範例中,只是展示我們可以在執行時內觀介面規格,並未進一步實現分析與注入動作。

const DBus = imports.dbus;

function NetworkManager() {
    this._init();
}
NetworkManager.prototype = {
    _init: function() {
	    DBus.system.proxifyObject (this,
	       'org.freedesktop.NetworkManager',
	       '/org/freedesktop/NetworkManager');
    }
}
DBus.proxifyPrototype (NetworkManager.prototype,
{
    name: 'org.freedesktop.NetworkManager',
    methods: [
        { name: 'GetDevices', inSignature: '', outSignature: 'ao' }
    ]
});

function Introspectable() {
    this._init();
}
Introspectable.prototype = {
    _init: function() {
        DBus.system.proxifyObject (this,
		   'org.freedesktop.NetworkManager',
		   '/org/freedesktop/NetworkManager');
    }
}
DBus.proxifyPrototype (Introspectable.prototype,
{
    name: 'org.freedesktop.DBus.Introspectable',
    methods: [
        { name: 'Introspect', inSignature: '', outSignature: 's' },
    ]
});

var nm = new NetworkManager();
var introspectable = new Introspectable();

introspectable.IntrospectRemote(function(result) {
    print(result);
});

nm.GetDevicesRemote(function(result) {
    print("Devices:");
	for (var i = 0; i < result.length; i++) {
	    print(result[i]);
	}
	main_quit();
});

function main_quit() {
    if (typeof Seed != 'undefined')
        Seed.quit();
    else
        Mainloop.quit('');
}

if (typeof Seed != 'undefined') {
    var GLib = imports.gi.GLib;
    var mainloop = GLib.main_loop_new();
    GLib.main_loop_run(mainloop);
}
else {
    var Mainloop = imports.mainloop; //Seed doesn't define this.

    Mainloop.run('');
}

執行結果如下列所示:

$ seed nm-get-devices.js

    .
    .
    .

Devices:
/org/freedesktop/NetworkManager/Devices/0

結語

儘管 gjs/seed 目前在使用 D-Bus 時,仍然不太方便。但整體功能上倒沒有發生什麼錯誤。我們距離用 JavaScript 撰寫桌面應用軟體的目標,又更進一步了。

相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/14229429.html

樂多舊回應
lp81sam@hotmail.com(lp81sam) (#comment-21384631)
Tue, 09 Nov 2010 07:26:13 +0800
Hello,很喜歡看你的Blog
介紹你一個專案
Joose
Joose is a meta object system for JavaScript
http://code.google.com/p/joose-js/

之前我在Wiki看Metaobject時
http://en.wikipedia.org/wiki/Metaobject

看到Joose這個特別的Javascript專案
我也沒特別深究,只是剛好看到這篇文章,又再度想起來
想說可以介紹給你,也許你會對他有興趣。
未留名 (#comment-21400633)
Mon, 15 Nov 2010 23:29:15 +0800
Joose 的實作目的,和我的目的不相同。

謝謝你提供的資訊,不過我目前用不著。