ninix-aya 4.3.9 & rss_reader 0.2

ninix-aya 4.3最後のリリースです.

今回はプラグインシステムのバグ修正と機能追加(standard version: 2.3 -> 2.4)です.

新しい機能はプラグインを作る(8)に書いたBasePlugin.open_dialog()です. これを使用したサンプルプラグインとしてrss_readerをリリースしました.(rss_readerを動作させるにはUniversal Feed Parser for Pythonが必要です.)

もしプラグインを作成する際に別の種類のダイアログが必要になったら, ここへのコメントかninix-aya開発プロジェクトの方へ書き込んで下さい. 出来るだけ対応します.

 

プラグインを作る(8)

プラグインがユーザーからの入力を受け取る方法を決めました.

  1. プラグインがBasePluginクラスのopen_dialogメソッドを呼ぶと, ninix-aya本体側でプラグイン用のダイアログを作成して開きます.(open_dialogは表示するメッセージを引数として取ります.)
  2. open_dialogの戻り値がユーザーが入力した値になります.(open_dialogは入力を待つ間ブロックします. ダイアログが出ている間もninix-aya本体は普通に動きます.)

具体的にはデータの保存/読み出しと同じmultiprocessing.JoinableQueueもしくはPipe()を使います.

このやり方が「プラグインを作る側」の労力が少なくなるので良いでしょう.(プラグインを作る人がGTK+の詳細について知らなくても出来ますし, ninix-aya本体側の実装を変更してもメソッドの使い方さえ変えなければプラグインは影響を受けないので.)

プラグイン用のダイアログをモーダルにするかどうかは要検討ですね. 他にもダイアログのデザインはどうするかとか細かい事は色々ありますが, とにかく作ってしまいましょう.

 

プラグインを作る(7)

現状のプラグイン作成の悩みは, 設定等のためにユーザーからの入力が欲しい場合にどうするかという事です.

ユーザーに選択肢を選んでもらえば良い場合はSSTPで出来るようになっているので問題が無いのですが, キーボードやDnDといった方法で任意の値の入力を受け取りたい場合には今の所方法がありません.

考えられる手段は2つあります.

  1. プラグイン独自にダイアログを出して入力を受け付ける.
  2. 本体のinputbox等の入力をプラグインが受け取る方法を作る.

1.の方法が良さそうですが, 現在のプラグインの実装では出来ません. その問題点とはmultiprocessingを使っている場合, プラグイン(子プロセス)側でGTK+を使うのはまずい(本体も含めて落ちる)という点にあります.(詳細を知りたい場合はここに良い解説があるので見て下さい.)

2.の方法の問題は本体側に追加する機能の仕様の決定から実装までが簡単ではないということです.

 

プラグインを作る(番外編1)

プラグインを作ったら次のステップとして, 通常のアプリケーションとしても配布する時が来るかもしれません. そこで今回はWindows用にhello_worldのインストーラパッケージを作成してみましょう.

なぜインストーラを作ることに拘るのか? 実は私はインストールではなくアンインストール機能の方を重要視しています.(LinuxのディストリビューションをSlackwareからDebianに変えた理由もそうでした.) アンインストール機能は環境をクリーンに維持し, アップデートしていく上で基本的な機能だと考えているからです.

さて, インストーラパッケージを作成してみましょう.

ネットを検索すると次の様な話が出て来ます. 「distutils2が出来ればもっと良くなる.」, 「それまでの間はdistributeを使うのが云々.」 未来は素晴しいと信じていますが, 今Pythonをインストールしただけで使えるのはdistutilsという意味ですね.(どうやらdistutils2はPython3.3から入るようです.)

Windows用のインストーラパッケージを作るにはmsiもしくはwininstのいずれかを選びます.(何度も言いますがeggは現在アンインストールの機能が無いので使いません.) 単純にファイルを目的の場所に置くだけならどちらでも良いのですが, ショートカットを作成するのであればwininstの方が簡単です. ショートカットを作るのに使える関数が準備されているからです. 今回はショートカットを作るのでwininstの方を使います.

用意するファイルはsetup.pyとhello_world_postinst.py(こちらのファイルの名前は何でも良いのですが, 他のプログラムのファイルと区別出来るようにしておくべきでしょう)の2つです. setup.pyから見ていきましょう.

from distutils.core import setup

setup(
    name = 'hello_world',
    version = '1.0',
    scripts = ['hello_world.py', 'hello_world_postinst.py'],
    options = {'bdist_wininst': {'install_script': 'hello_world_postinst.py'}}
)

scriptsには直接実行するスクリプトファイルを列記します. hello_world.pyだけでなくインストール時に実行するhello_world_postinst.pyもここに書いておく必要があります.

optionsはbdist_wininstでパッケージを作成する際のオプションを指定しています. コマンドラインで

python setup.py bdist_wininst --install-script=hello_world_postinst.py

の様に指定することも出来ますが, 毎回書くのは面倒なのでここに入れておきます.

hello_world_postinst.pyはここに載せると長くなるので省略します. ninix-ayaのninix_win32_postinst.pyから必要な部分をコピーして改変して下さい.(次のhello_worldのリリース時にはアーカイブに入れる予定です.)←単独ではninix.pluginが無いので動きませんね.:-(
ショートカットを作る上でのポイントは, create_shortcut()でショートカットを登録した後に, そのショートカットのパス名を引数にしてfile_created()を呼ぶことです.

それとは別にhello_world_postinst.pyについて1つ重要な注意点があります. インストーラから実行される部分についてはsys.exit()やraise SystemEixtを呼んではいけません. sys.exit()等でスクリプトを終了するとインストーラも終了してしまい, インストールの処理が中途半端な状態になってしまいます. 具体的にはパッケージをアンインストールしてもインストールしたファイルやショートカットが削除されずに残ってしまうといった現象が発生します.(私はここでしばらく悩みました. :-p)

ninix-aya 4.3.7 & otenkiyan 0.6

今回はクリスマスリリースにしました. 「お天気やん」も更新しています.

大きなバグが残っていなければ年内最後のリリースとなります.

時間の都合で栞互換モジュールの文字コード周りの修正を入れるのは見送りました.
PyGI GTK3への移行についてはCairo周りにまだ問題があるようなので(Using Cairo Regions in python with gi.repository), 出来るだけの準備をしておいてしばらく待つことになりそうです.

プラグインを作る(6)

プラグインのプロセスはいつ終了するのか? これは重要な点です.

プラグインが作成者の意図しない終わり方をすると問題を起こす場合があるからです. その最たるものがファイルへのデータ保存でしょう. ファイルへの書き込みが終わる前にプラグインのプロセスが終了して(させられて)しまうと, 出来上がるファイルの内容は意図していたのとは違ってしまいます.

ninix-ayaのプラグインはその処理を全て完了して終了する以外にエラーやninix-aya本体の終了によって終了します. エラーを起こして終了するのはプラグイン側の責任で処理すべきものなので, ここではninix-ayaが終了した時にどうなるかを説明します. 結論から言うとninix-aya終了の直前に有無を言わせずプラグインのプロセスは終了させられてしまいます. ninix-aya終了後にプロセスが残ってしまうのを嫌ってこの様な実装になっています. atexitを使って終了処理を登録してあっても実行されない点には特に注意して下さい.

しかし, これではファイルにデータを保存してもその処理が最後まで実行される保証がなく, データを失なうリスクがあります. 次の例はそういった状況をわざと起こさせるためのものです.

from ninix.plugin import BasePlugin

class Plugin(BasePlugin):
    def run(self):
        with open(os.path.join(self.directory, 'test.txt'), 'w') as f:
            for x in range(100):
                f.write('line{0}\n'.format(x))
                print 'count:', x
                time.sleep(1) # sec

このプラグインを実行してから100カウントが終わる前にninix-ayaを終了するとtest.txtの中身はどうなるでしょうか?

この問題を解決するためにBasePluginにはプラグイン終了後も保存されるデータを扱うための次の3つのメソッドが追加されました.(plugin standard 2.3以降で使用出来ます.)

self.set_variable(変数名, 変数値)
self.set_variables(変数の入った辞書)
self.get_variable(変数名)

変数は名前と値を持ち, どちらも文字列(str or unicode)(ninix-aya 4.3.7からUnicodeは使用出来なくなります. マルチバイト文字列はUTF-8でエンコードして下さい.)(str or Unicodeですが, BasePluginのメソッドに渡すマルチバイト文字にはUnicodeを使用して下さい. ファイルに保存する際にはUTF-8に変換されます.)です. 変数名には’:’を使用することは出来ません. これはデータを保存するSAVEDATAファイル(後述)で区切り文字として使用するためで, ‘:’が含まれているとエラーになります. 変数値をNoneにするとその変数は保存されなくなりSAVEDATAから消去されます. 存在しない変数をget_variableで取得しようとするとNoneが返されます. Noneと異なり空文字列は正しい値として扱われます.

set_variable(s)メソッド自体は必ず処理されることが保証されているわけではありません(これらのメソッドの処理が完了する前にプラグインが終了する可能性はあります)が, これらのメソッドから戻った時点で(ninix-ayaが異常終了しない限り)データが保存されることが保証されます.(具体的には変数の情報はキューを使ってninix-ayaに送られ, ninix-ayaがキューからデータを取り出して処理するまでset_variable(s)メソッドはブロックします.) 複数の変数を設定する場合にはset_variablesメソッドの方を使うことでまとめて処理することが出来ます. このメソッドが戻った時点で全ての変数が設定されたことが保証されます.(set_variableを複数回呼び出した場合には途中までしかデータが保存されない可能性があります. set_variablesの場合にはall or nothingです.) set_variablesの引数には

{変数名1: 変数値1, 変数名2: 変数値2, ……}

という形で変数を入れた辞書を渡します.

ninix-ayaに送られた変数の情報はninix-ayaの終了時にプラグインのディレクトリのSAVEDATAファイルに保存され, 次回のプラグインの起動時に読み込まれます.(データの保存のタイミングについては今後変更する可能性があります.)

これらのメソッドでは変数の値を全て文字列にして渡す必要があるため値の型の情報は保存することが出来ません. どうしても型情報を残す必要があれば変数名に入れる(例えば’test[int]’)などプラグイン作成者側での工夫が必要になります.(要望があればninix-aya側での対応も考えます.)

「お天気やん」はバージョン0.5から今回説明したメソッドを使って天気予報をする場所の情報を保存するようになっています. メソッドの使い方の参考にしてみて下さい.

最後にもう一点. ninix-aya 4.3.4以降は同じプラグインを複数起動することが出来ないようにしてあります. ですので, plugin standardを2.3以上に設定している限り, 同じプラグインが複数起動してデータの保存で競合するという心配はありません.

次回は常駐型プラグインを取り上げる予定です.

これでどうかな

プラグインでのデータの保存ですが, ninix-aya本体側で保存を行なうメソッドを作ることにしました.(ninix-ayaとプラグインの間でのデータのやりとりについては, multiprocessing.Queueを使うことを考えています.)

おそらくこれがninixで実装されなかった理由だと思うのですが, SetCookieの仕様ではクライアントの識別が不十分なので今後ninix-ayaでもSetCookie/GetCookieは実装しないことにしました.

次の課題

ninix-aya 4.3.3はこの土日にリリースする予定ですが, プラグインについて次に考えるべき課題が既に出てきています.

それはプラグインの中でのデータ保存です. 現在の実装ではプラグインの中でファイルへの書き込みを行なうのは, それが完了するという保証がないためリスクがあります. 何故かというとninix-aya本体が終了するとプラグインは強制終了させられるためです. この強制終了というのが結構荒っぽく, atexitで登録した処理も実行されません.(POSIX環境ではシグナルでプロセスを終了させています.)

もちろんプラグインがninix-ayaより先に終了していれば問題ありません. 短時間で終了するプラグインであれば問題は表面化しない可能性が高いのですが, 常駐型プラグインでデータを保存しようとすると確実に問題になります.

1つ目の解決方法はEXECUTE SSTP/1.1のSetCookie, GetCookieを使う事ですが, ninix-ayaにはまだ実装されていません. もし実装されれば, このコマンドを使ってninix-aya(親プロセス側)にデータの保存を任せる事で問題を回避出来ます.

2つ目の解決方法はプラグインをdaemon化しない事です. ninix-aya終了時にプラグインに終了するように伝達し, ninix-ayaはプラグインの終了を待つ事になります. この方法の問題点はプラグインが必ず終了するという保証が無いことです. joinにtimeoutを設定して一定時間だけ待つ様にする事も出来ますが, その一定時間が過ぎれば結局は強制終了させることになります.

今の所は上記2つの方法しか思い付いていませんが, 出来るだけ早くにこの問題を解決したいと考えています.

プラグインを作る(5)

今回はBasePluginのnotify_sstpメソッドとsend_sstpメソッドを使ってイベントやスクリプトをゴーストに送信してみましょう.

まずはhello_world.pyをsend_scriptではなくnotify_sstpsend_sstpを使って書き直してみます.(SEND SSTPのバージョンは送信内容によって自動的に決められます.)

script = r'hello world\e'
self.notify_sstp('hello_world', script_odict={'': script})
self.send_sstp('hello_world', script_odict={'': script})

これがIfGhostでゴーストを指定することなくスクリプトを送信する場合の形です. script_odictにはキーが空文字列のアイテムにスクリプトが入った辞書を指定します. スクリプトは1つしか無いので順序付き辞書を使う必要はありません.

では, 伺か – SSTPプロトコルに出ている次の例をnotify_sstpで送信してみましょう.(notify_sstpが使用するプロトコルは常にNOTIFY SSTP/1.1, 文字コードはUTF-8になります. BasePluginのメソッドに渡すマルチバイト文字列にはUnicodeを使用して下さい.)

NOTIFY SSTP/1.0
Sender: さくら
Event: OnMusicPlay
Reference0: 元祖高木ブー伝説
Reference1: 筋肉少女帯
Charset: Shift_JIS

self.notify_sstp(u'さくら', 'OnMusicPlay', ref=[u'元祖高木ブー伝説', u'筋肉少女帯'])

これがイベントを送信する基本的な形です. 次は保険反応付きの場合です.

NOTIFY SSTP/1.1
Sender: さくら
Event: OnMusicPlay
Reference0: 元祖高木ブー伝説
Reference1: 筋肉少女帯
IfGhost: なる,ゆうか
Script: \h\s0‥‥\w8\w8高木ブーだね。\u\s0‥‥\e
IfGhost: さくら,うにゅう
Script: \h\s0‥‥\w8\w8高木ブーだね。\u\s0‥‥\w8\w8むう。\e
Charset: Shift_JIS

まずプラグインの先頭にあるimport部分で順序付き辞書を使えるようにします.(Python2.6の場合には通常の辞書型を使用して下さい.)(ninix-aya 4.3.3からはPython 2.7が必須なので, ninix-ayaが動く環境であれば順序付き辞書は使えるはずです.)

from collections import OrderedDict

そして各スクリプトを順序付き辞書に順番通りに登録していきます.

odict = OrderedDict()
odict[u'なる,ゆうか'] = unicode(r'\h\s0‥‥\w8\w8高木ブーだね。\u\s0‥‥\e', 'utf-8')
odict[u'さくら,うにゅう'] = unicode(r'\h\s0‥‥\w8\w8高木ブーだね。\u\s0‥‥\w8\w8むう。\e', 'utf-8')

この辞書を使って保険スクリプトを送ります.

self.notify_sstp(u'さくら', 'OnMusicPlay', ref=[u'元祖高木ブー伝説', u'筋肉少女帯'], script_odict=odict)

最後に選択肢インターフェースを使ってみましょう. プラグインからユーザーへの問い合わせはプラグインで独自にウインドウを用意しても良いのですが, 簡単な内容であれば選択肢インターフェースが便利です. 次の例に相当する内容を送信してみましょう.

SEND SSTP/1.2
Sender: カードキャプター
Script: \h\s0どんな感じ?\n\n\q0[#temp0][まあまあ]\q1[#temp1][今ひとつ]\z
Entry: #temp0,\h\s0ふーん。\e
Entry: #temp1,\h\s0酒に逃げるなヨ!\e
Charset: Shift_JIS

引数entryにEntryの内容を列記したリストを渡します.

script = ur'\h\s0どんな感じ?\n\n\q0[#temp0][まあまあ]\q1[#temp1][今ひとつ]\z'
e_list = [ur'#temp0,\h\s0ふーん。\e', ur'#temp1,\h\s0酒に逃げるなヨ!\e']
response = self.notify_sstp('カードキャプター', entry=e_list, script_odict={'': script})
response = self.send_sstp(u'カードキャプター', entry=e_list, script_odict={'': script})

選択肢インターフェースを使ってユーザーに選択をしてもらう場合には2点程注意が必要です.
notify_sstpsend_sstpの戻り値(上の例ではresponseの値)には選択肢の数+1の場合(上の例では2+1=3通りの場合)があります. この+1は何らかの理由(通信が出来なかった場合やSSTP BREAK)で選択が行なわれなかった場合の空文字列です.(さらにnotify_sstpで選択肢インタフェースを使用した場合には, SHIORIがイベントに反応した場合にもスクリプトが実行されないために戻り値が空文字列になります.)
また, スクリプトにIfGhostを指定した場合にはninix-aya 4.3.2では選択の結果を戻り値として返すことが出来ません. ですので, 現状では選択肢インターフェースとIfGhostを組み合わせて使うのは避けなければなりません.(このバグは4.3.3で修正済みです.)

次回はプラグインを作る際に注意すべき点をいくつか取り上げつつ, もう少し本格的なコードを作成する予定です.

2011/12/19追記: 例として挙げたコードではマルチバイト文字を含むスクリプトをur”の形で指定してる場合と, r”で指定してunicode()で変換している場合があります. これは\uタグを含むスクリプトの場合にur”を使うとエラーになるためです.

プラグインを作る(4)

今回はninix-aya 4.3.2と4.3.3で追加された範囲も含めて, BasePluginの機能を見ていきます.

まず, plugin.txtのstandardを2.0から2.12.2に上げましょう. これで4.3.24.3.3で追加された機能も使用すると宣言したことになります.

プラグインの起動時にninix-ayaからBasePluginに渡される情報がいくつかあり, それはBasePluginを継承したPluginクラスの変数としてアクセス出来ます.

4.3.1で使える変数には以下のものがあります.

  • self.sstp_port: SSTP送信先のポート番号です.(デフォルトは9801です.)
  • self.directory: プラグインのインストール先ディレクトリ名です.
  • self.args:  plugin.txtのmenuitem(もしくはstartup)で指定した引数が入ったリストです.(前々回書き忘れていましたが引数は”,”で区切って複数指定出来るのでリストになっています.) 引数が指定されていなければリストは空です.

4.3.2で追加された変数は次の通りです.(4.3.3は変数の追加はありません.)

  • self.ninix_home: ninix-ayaのホームディレクトリです.
  • self.caller: プラグインを実行したゴーストの情報が入った辞書型変数です. self.caller[‘name’]に名前がself.caller[‘directory’]にディレクトリ名が入っています. startupから起動した場合には値は空文字列になっています. self.caller[‘name’]の値はdescript.txtのnameです.

4.3.3で追加された変数は次の1つです.

  • self.caller[‘ifghost’]: プラグインを実行したゴーストのIfGhostです. この値をIfGhostに指定してスクリプトを送ることで, プラグインを実行したゴーストに届きます.

BasePluginの持つメソッドとしては前回説明したsend_scriptの他に, 4.3.2からはNOTIFY SSTP/1.1リクエストを送るためのnotify_sstpが, 4.3.3からはSEND SSTPリクエストを送るためのsend_sstpが使用出来ます. 書式は次の通りです.

self.notify_sstp(sender, イベント, 参照情報, 一時エントリ, 保険スクリプト)
self.send_sstp(sender, 一時エントリ, スクリプト)

sender以外の引数(イベント以降の引数)は全て省略可能です.(全てを省略してしまうとリクエストの意味がありませんが, ともかくエラーにはなりません.)
2つのメソッドの違いはイベント指定(とそれに付随する参照情報)の有無です. notify_sstpではsenderとイベント, send_sstpではsenderとスクリプト最低1つが必須です. それ以外の引数は省略可能です.

イベントを送ってゴーストが持つ反応を引き出す(イベントへの反応が無い場合には保険スクリプトを実行させる)という使い方も出来ますし, イベントの指定を省略してスクリプトのみを送ることも出来ます. イベントを送ってゴーストが持つ反応を引き出す(イベントへの反応が無い場合には保険スクリプトを実行させる)場合にはnotify_sstpを使用し, イベントの指定をせずにスクリプトのみを送る場合にはsend_sstpを使用します. 詳細は仕様書(伺か – SSTPプロトコル, 非公式 SSTP/1.x プロトコル仕様書)を見て下さい. 選択肢インターフェース(SEND/1.2に相当)を使用した場合にはその戻り値(選ばれた選択肢)がこれらのメソッドの戻り値として返されます. それ以外の場合の戻り値は空文字列です.

プラグインの目的に合ったイベントがあるかどうかはこちらの仕様書(CROW・SSPリファレンス – Shiori)を確認して下さい. notify_sstpはOnFirstBoot等の特殊なイベントでも発生させることが出来てしまうので注意が必要です.(ゴーストによっては変数の初期化等の何度も実行することを想定していない処理を特定のイベントで実行している場合があります.) 今のところはninix-aya側でイベントを制限することはしていませんから, 全てのイベントを自由に実行することが出来ます.

参照情報と一時エントリは文字列の入ったリストとして渡すようにして下さい.

最後に保険スクリプト引数について少し説明します. ここに入る値は順序付き辞書collections.OrderedDictを想定していますが, 以降で述べる様な制限があるものの通常の辞書型でも動作します.(順序付き辞書を使用するにはPython 2.7もしくは3.1以降が必要になります.) 辞書のキーがIfGhostに対応しており, 辞書の値がSakuraスクリプトです. IfGhost無しのスクリプトを送る場合にはキーを”(空文字列)にして下さい. IfGhostに対応するゴーストがいなかった場合ですが, 順序付き辞書を使った場合は先頭のアイテムがデフォルトとして使用されます. 通常の辞書型を使った場合はどのアイテムがデフォルトになるかを指定する方法はありません.

今回は具体的なコードの実例が無かったので分かり難かったかもしれません. 次回は実際に動作するコードを例にnotify_sstpメソッドとsend_sstpメソッドを解説したいと思います.