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 調用其他桌面軟體的服務。
請安裝 gjs 或 seed 其中一套,以便執行本文的範例。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 撰寫桌面應用軟體的目標,又更進一步了。
相關文章
- JavaScript 與 Desktop - WebKit
- JavaScript 與 Desktop - Desktop and WebKit
- GJS - D-Bbus 自動內觀(Introspect)與配置代理個體
樂多舊回應