Edited at

[Ubuntu 18.04標準機能] gnome-shellの拡張機能を自作してみる (その2)

More than 1 year has passed since last update.


拡張機能の作成


作りたいものと似た機能を持つ拡張機能を探す

いよいよコーディングに入っていくわけなのですが、ここで大きな壁が立ちはだかります。

gnome-shellではGjsと呼ばれるフレームワークに従って拡張機能を作成していくことになるのですが、このGjsはドキュメントがほとんど更新されておらず、また公式のWikiにある説明も非常に少ないため、ほとんど参考になりません。

そこで、まずは自分の作りたいものと似た機能をもつ拡張機能を探してきて、そのソースコードを参考にしましょう。インストールした拡張機能は$XDG_DATA_HOME/gnome-shell/extensionsに保存されるので、そこからソースコードを覗くことができます。

今回は、Hubic indicatorという拡張機能を参考にしました。


実装


モジュールのインポート

まずは、今回必要となるモジュールを読み込みます。

// 必要なモジュールのインポート

const St = imports.gi.St;
const Main = imports.ui.main;
const Lang = imports.lang;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const Mainloop = imports.mainloop;


パネルメニューを表すクラスの作成

gnome-shellの拡張機能はJavaScriptで記述しなければなりませんが、Langというモジュールを使うことで、JavaScriptでもオブジェクト指向に似たプログラミングを実現することができます。

例えばパネルメニューのオブジェクトを作成する際には、パネルメニューのベースとなるクラスを継承し、新たな派生クラスを割り当てることによって実現します。よって、Dropboxの同期状態を示すパネルメニューは、以下のように書けます。

const DropboxIndicator = new Lang.Class({

Name: 'DropboxIndicator',

// PanelMenu.Buttonクラスを継承 (必須)
Extends: PanelMenu.Button,

// コンストラクタ
_init: function() {
// 基底クラスのコンストラクタの呼び出し
this.parent(0.0, "Dropbox Indicator");

// 処理を記述
}
}

C++やJavaなどでGUIの作成経験がある方は、このような書き方に深い親近感を覚えるのではないでしょうか。

とはいえこれだけでは基底クラスと何も変わらないので、アイコンやボタンの設定を加えていきます。

  _init: function() {

// 基底クラスのコンストラクタの呼び出し
this.parent(0.0, "Dropbox Indicator");

// アイコンオブジェクトの定義
// icon_name: アイコンの種類を指定
// アイコン一覧は/usr/share/iconsなどを参照
// style_class: 適用するCSSスタイル
// stylesheet.cssなどで独自にスタイルを定義することが可能
let icon = new St.Icon({ icon_name: 'dropbox',
style_class: 'system-status-icon' });

// パネルメニューに登録
this.actor.add_actor(icon);

// レイアウトの定義
// ここではBoxLayoutを採用
this._box = new St.BoxLayout({ style_class: 'panel-status-button' });

// レイアウトを適用
this.actor.add_actor(this._box);

// 'Open Folder'ボタンの定義
let open_folder_btn = new PopupMenu.PopupMenuItem("Open folder");

// ボタンの登録
this.menu.addMenuItem(open_folder_btn);

// 'Open Folderボタンの定義'
let turn_off_btn = new PopupMenu.PopupMenuItem("Turn off");
this.menu.addMenuItem(turn_off_btn);

// イベントハンドラの登録
}

ここでやっていることは、


  1. アイコンオブジェクトの生成・登録

  2. レイアウトオブジェクトの生成・登録

  3. ボタンオブジェクトの生成・登録

です。icon_name属性に渡す値は、.desktopファイルの中のIconパラメータと同じで、/usr/share/iconsなどに入っている画像ファイルの拡張子を除いたものを指定します。まだ実験はしてないですがおそらく絶対パスでもいけるのではないかと思います。

style_classという属性では、適用するCSSのクラス名を指定します。

適用されるCSSファイルは、


  • /usr/share/gnome-shell/theme/gnome-shell/theme/gnome-shell.css (グローバル)

  • <拡張機能のディレクトリ>/stylesheet.css (拡張機能内のみ)

  • $HOME/.themes/???/gnome-shell/gnome-shell.css (user-theme拡張機能をインストールしている場合のみ、ユーザー個別)

の3種類ですが、基本的にいじるのは2番目のstylesheet.css(extension.jsと同じディレクトリ)だけで十分です。

今回はグローバルのCSSで定義されているクラスのみを使用しています。

さて、アイコンやボタンの定義が済んだら、イベントハンドラの設定を行います。今回は、ボタンのクリックイベント(activate)に関するハンドラの設定です。

    // イベントハンドラの登録

// Lang.bindで無名関数内のthisの値を指定できる。
open_folder_btn.connect('activate', Lang.bind(this, function(){
this.openFolder();
}));

turn_off_btn.connect('activate', Lang.bind(this, function(){
this.turnOff();
}));

ここでLang.bindという関数が登場していますが、これはfunction.bindメソッドと同じで、無名関数内のthisの値を指定します。これにより、DropboxIndicatorクラスのopenFolderメソッドやturnOffメソッドを呼び出します。

さて、登録が済んだら、これらのメソッドを定義しなければなりません。まずは'Turn off'というボタンをクリックするとdropbox stopというシェルコマンドを実行するようにします。

  turnOff: function() {

// `dropbox stop`というコマンドを非同期で実行
let [ok, pid] = GLib.spawn_async(null, ["dropbox", "stop"], null, GLib.SpawnFlags.SEARCH_PATH, null);
}

何やら不可解な関数が呼ばれていますね。やけに引数が多いような。

GLib.spawn_asyncという関数は、シェルコマンドを非同期で実行する関数です。引数の詳細については、GLibのドキュメントに詳しく書かれています。

ここでは、dropboxというコマンドを環境変数PATHの中から探索し、dropbox stopというコマンドを非同期で実行しています。

続いては'Open Folder'ボタンの設定ですが、こちらはやや複雑になります。

  openFolder: function() {

let ok, content, pid;

// パスの連結
// Pythonでいうos.path.join関数に相当
let dropbox_config_path = GLib.build_filenamev([GLib.get_home_dir(), ".dropbox", "info.json"]);

// Dropboxの設定ファイルの中身を取得
[ok, content] = GLib.file_get_contents(dropbox_config_path);

// 同期しているローカルフォルダのパスを取得
let sync_dir = JSON.parse(content)["personal"]["path"];

// ログに出力
log("sync_dir: " + sync_dir);

// `xdg-openコマンドでディレクトリを開く`
[ok, stdout, stderr, exit_status] = GLib.spawn_sync(null, ["xdg-open", sync_dir], null, GLib.SpawnFlags.SEARCH_PATH, null);
if (!ok) {
log("Failed to open directory");
}
}

まずはdropboxの同期先となるローカルフォルダを調べなければなりませんので、$HOME/.dropbox/info.jsonというファイルを読み込んでパスを取得しています。

その後は、おなじみのxdg-openコマンドでディレクトリを開いています。


デーモンプロセスの処理の実装

いよいよ処理の内容に移っていくわけなのですが、その前にパネルメニューのインスタンスをグローバル変数で保持しておくといろいろ楽になります。

let dbmenu = null;

Dropboxが同期開始した時にアイコンを表示するためには、何秒かおきにdropboxの同期状態を監視し続けなければなりません。

そこで登場するのが、Mainloop.timeout_add_secondsという関数です。これはJavaScriptのsetTimeout関数と同じように使うことができます。

これを使って、2秒おきに同期状態をチェックするようにしてみましょう。

set_timer() {

// タイマーオブジェクトの生成
// JavaScriptのsetTimeout関数に相当
// 2秒後に再度同期状態を取得する
Mainloop.timeout_add_seconds(2, function(){
update();
});
}

続いてはいよいよ、同期状態をチェックするupdate関数を実装していきます。

function update() {

set_timer();

// `dropbox running`コマンドの実行
let [ok, stdout, stderr, exit_status] = GLib.spawn_sync(null, ["dropbox", "running"], null, GLib.SpawnFlags.SEARCH_PATH, null);

if (dbmenu === null && exit_status > 0) {
// dropboxが稼働している状態
dbmenu = new DropboxIndicator;

// パネルメニューをトップバーに表示
Main.panel.addToStatusArea('dropbox-indicator', dbmenu, 0, "right");
} else if (dbmenu !== null && exit_status == 0) {
// dropboxが稼働していない状態

// パネルメニューの破棄
dbmenu.destroy();
dbmenu = null;
}
}

ここまで来ればほとんど説明することはないのですが一点だけ。

Main.panel.addToStatusAreaという関数で、第3引数(0)と第4引数("right")は何を表しているのでしょうか?

gnome-shellを使っている方はご存知かと思いますが、トップバーには右側、中央、左側の3つのスペースに分かれてウィジェットが配置されています。第4引数は、このうちどのスペースにウィジェットを配置するかを決めるものです。

そして第3引数は、そのスペースの中で、左から数えて何番目にウィジェットを置くかを指定します。

つまり、0なら一番左側、100なら一番右側に配置されることになります。

今回はrightの0番目なので、右側スペースの一番左端にウィジェットを配置するということになります。


init関数、enable関数、disable関数

最後に、init関数、enable関数、disable関数を実装して終了です。

function init() {}

function enable() {
update();
}

function disable() {
if (dbmenu !== null){
dbmenu.destroy();
}
}


実装完了

ここまでの完成版のソースコードはこちらから見ることができますので、記事を読んでもうまく実装できなかった方はこちらからコピペして使ってください。

続く -> その3


追記 (2018/5/29)

拡張機能を使っている時に気づいたことなのですが、2秒おきにCPU使用率が3-4%ほど上昇していて、もしやと思って調べてみました。

$ time dropbox running


real 0m0.080s
user 0m0.068s
sys 0m0.012s

あ、これは結構時間かかってますね。

C++使いの自分にとっては0.01秒でも長いのだから0.08秒は論外。

そこで、dropboxコマンドのソースコードを調べてみます。

するとどうやらdropboxコマンドはPythonで実装されている模様。どうりで遅いわけですね。

同期状態をチェックする部分は、/usr/bin/dropboxの161行目にあるis_dropbox_running()関数で実装されているので、C言語で同じ処理を実装してみます。


dropbox-running.c

#include <stdio.h>

#include <stdlib.h>
#include <string.h>
#include <ctype.h>

int get_pid(char* pid) {
const char* home_dir = getenv("HOME");
char filename[256];

snprintf(filename, sizeof(filename), "%s/.dropbox/dropbox.pid", home_dir);

FILE* fp = fopen(filename, "r");
if (fp == NULL) {
return 0;
}

int cnt = fread(pid, 1, sizeof(pid) - 1, fp);
fclose(fp);

pid[cnt] = '\0';

return 1;
}

int is_running(char* pid) {
char filename[24];
char buff[256];
snprintf(filename, sizeof(filename), "/proc/%s/cmdline", pid);

FILE* fp = fopen(filename, "r");
if (fp == NULL) {
return 0;
}

int cnt = fread(buff, 1, sizeof(buff) - 1, fp);
fclose(fp);

buff[cnt] = '\0';

for (int i = 0; i < cnt; i++) {
buff[i] = tolower(buff[i]);
}

if (strstr(buff, "dropbox")) {
return 1;
} else {
return 0;
}
}

int main() {
char pid[10];
int status = get_pid(pid);

if (status == 1) {
status = is_running(pid);
}

printf("%d", status);

return 0;
}


コンパイルオプションをいっぱいつけて高速化!

$ gcc -Wall -Wextra -o dropbox-running dropbox-running.c -O3 -DNDEBUG -march=native -mtune=native -mfpmath=both -static

$ mkdir -p ~/bin
$ mv ./dropbox-running ~/bin/
$ time dropbox-running
0
real 0m0.001s
user 0m0.001s
sys 0m0.000s

ふう、これでなんとか使い物になるな。

あとは拡張機能のupdate関数を少し書き換えて完成。

function update() {

set_timer();

// `dropbox-running`コマンドの実行
let [ok, stdout, stderr, exit_status] = GLib.spawn_sync(null, ["dropbox-running"], null, GLib.SpawnFlags.SEARCH_PATH, null);

if (dbmenu === null && stdout == "1") {
// dropboxが稼働している状態
dbmenu = new DropboxIndicator;

// パネルメニューをトップバーに表示
Main.panel.addToStatusArea('dropbox-indicator', dbmenu, 0, "right");
} else if (dbmenu !== null && stdout == "0") {
// dropboxが稼働していない状態

// パネルメニューの破棄
dbmenu.destroy();
dbmenu = null;
}
}