先週アメリカ合衆国のトランプ大統領が来日しましたね。そこで今回はトランプ大統領のツイートを「biff」で通知してみましょう。
そもそも「biff」とは
「biff」とはBSD由来のメール通知システムです。通知を受け取るかどうかを設定するbiffコマンドと実際に通知を発行するcomsatデーモンが協調して、以下のような流れでユーザーにメールが届いたことを通知します。
- comsatデーモンは定期的に
/var/run/utmp
からログインユーザーのリストを取得しておく
- MTAがメールを受け取る
- MTAがcomsatデーモンに「受信者」「メールボックスファイル名」「受信したメールのファイル上のオフセット」をUDPで通知する
- comsatデーモンは「受信者」がログインしているか確認する
- comsatデーモンは受信者が使用している端末ファイル(
/dev/ttyX
や/dev/pts/X
)のbiffビットが立っているか確認する
- biffビットが立っている端末ファイルに対して、メールの「From:」と「Subject:」、さらに本文の一部を書き込む
- 先頭7行もしくは560文字の小さいサイズに到達した時点で書き込みを終了する
要するに「エディターを開いていようが何していようが、問答無用で(仮想)端末デバイス上にメール着信通知を行う」システムです。つまり古いUnix環境であれば便利だったかもしれないツールではあるものの、現在の環境ではほぼ役に立たないというかむしろ邪魔になるツールです。そのため現在ではまず使われていませんし、512番ポートが開いていることも稀でしょう[1]。
しかしながら「メールを受信したらユーザーに通知する」というアイデアそのものは今でも有用です。xbiffやgnubiffといった「ユーザーの邪魔をしないような」単体の通知システムが実装されましたし、今ではメールクライアントそのものにも通知システムが搭載されていることが一般的です。
ところでこのオリジナルのbiffコマンドとcomsatデーモンそのものはパッケージリポジトリに残っています。そこで今回はあえてこのbiffコマンドを使うことで、昔の環境ではどのように「You've Got Mail」していたのか体験してみることにしましょう。
正直に言うと「トランプ大統領とbiff」という話をやりたかっただけです[2]。
biffのインストール
biffとcomsatデーモンはともにbiffパッケージに含まれています。よってインストールは簡単です。
最後にエラーが出ていますが、comsatはinetd経由で起動するサービスであるにも関わらず最近のシステムだとinetdがインストールされていないことが原因です。/etc/inetd.conf
の中身を確認した上でupdate-inetd
コマンドを実行しようとするものの、/etc/inetd.conf
が存在しないために上記のようなgrepコマンドのエラーが出ています。本来はbiffパッケージがxinetdなどに依存しておくべきですが、今回はinetdを使わないためそのままにしておきます。
inetdとsystemd
そう、inetdです。comstatは/etc/inetd.confに以下の項目を記述することでサービスを有効化します。
/etc/services
ファイルを確認するとbiffサービスについて記載されていることがわかります。
inetd経由で起動するサービスの特徴のひとつが、「起動したサービスの標準入出力がクライアント側のソケットの入出力に繋がっている」ことです。つまりサービスプロセスが標準入力から(0番のファイルディスクリプターから)読み込むと、それはクライアントから送られてきたデータを読み込むことになりますし、標準出力に書き出したデータはネットワーク経由でクライアントに送られることになります。実際にcomsatのソースコードには、以下のようなコードがあります。
このためinetd用のサービスをそのまま実行してもうまく動きません。comsatデーモンを動かすためにはinetdサービスが必要になります。ここで素直にxinetdをインストールしてもいいのですが、実はsystemdはinetdのようなsocketドリブンのサービスを作ることも可能なのです。今回はみんな大好きsystemdを使って実現してみましょう。
まずsocket unitを作成します。「/etc/systemd/system/biff.sock
」を次の内容で作成します。
内容はごくごくシンプルで、「localhostの512ポートで待ち受けるsocket unit」です。ListenDatagramでデータグラムのsocketにしています。ストリームにしたい場合は、ListenStreamにしてください。またその場合は、Acceptも設定したほうがいいでしょう。
ListenDatagramのアドレス部分には「[::1]:512
」を指定しています。ここは次のようなルールになっています[3]。
- 「/」で始まる場合はUnixドメインソケットのパスとして使われます
- 「@」で始まる場合はLinuxの抽象ソケットアドレスとして扱います
- 単一の数字の場合はポート番号として扱い、システム上のすべてのインターフェースをlistenします
- 「A.B.C.D:PORT」の場合はIPv4アドレスとポート番号として指定したアドレスでlistenします
- IPv6表記の場合はIPv6でlistenしますが、設定によってはIPv4アドレスも同時にlistenします
上記の例ではIPv6表記のループバックアドレスを指定しているため、ローカルからIPv6と設定次第ではIPv4でもアクセスできることになります。
次にこのsocket unitにパケットが届いたときにアクティベートする本体のサービスファイルを「/etc/systemd/system/biff.service
」として作成します。
見た目はごくごく普通のサービスファイルですね。comsatデーモン自身はforkしないのでサービスタイプは「Type=simple
」です。またRequires
には先程作成したbiff.socket
を指定しています。
inetdライクなサービスにするために「StandardInput=socket
」を設定しています。これによりinetdと同じく標準入力がsocketにつながることになります。ちなみにcomsatデーモンは何もデータを返しません(標準出力に対してsend()
やwrite()
を呼びません)。よってStandardOutput
は設定していません。一般的なinetdサービスであれば、StandardOutput
も設定する必要があるでしょう。
biff.serviceはbiff.socketによってアクティベートされるので、biff.socketのみ起動しておきましょう。
これで512番ポートにパケットが届くとcomsatが起動するようになりました。
biffのテスト
実際にbiffパケットをcomsatデーモンに送って試してみましょう。biffのパケットは次のようなフォーマットになっています[4]。
「ユーザー名」は送信対象のユーザー名です。comsatはこのユーザー名が使っている端末ファイルのうち、biffビットがセットされているファイルに対してメッセージを送ります。
「ファイルオフセット」と「ファイル名」が実際のメッセージの位置です。ファイル名は/usr/include/paths.h
の「_PATH_MAILDIR
」以下のファイルパスになります。Ubuntuの場合は「/var/mail
」が設定されていますので「/var/mail/ファイル名
」と解釈されます。
「/var/mail/ファイル名
」はいわゆるmbox形式のメールアーカイブファイルであり、1つのファイルに延々とメールデータを記録していくフォーマットです。Ubuntuだとたとえばmailutilsパッケージのmail
コマンドで表示できます。一般的には「From 送信元 日時」の行から始まり、何行かのヘッダーフィールド、空行、本文、空行までで1メッセージとなります。
とりあえず適当なメールボックスとメッセージを作成しておきましょう。
端末ファイルの「biffビット」は、とどのつまりファイルオーナーの実行可能ビットです。biff
コマンドでオン・オフできますが、chmod
で代用してもかまいません。とりあえず手元の端末でオンにしてみましょう。デスクトップ環境の端末エミュレーター上のような仮想端末でも動作します。
これでcomsatデーモンがこの端末上にメッセージを表示できるようになりました。では、デーモンにパケットを送ってみましょう。bashであれば「/dev/udp/アドレス/ポート
」でUDP通信を行えます。
無事にオフセット0、つまりファイルの先頭にあるメッセージを表示できることがわかりましたね。このメッセージは「biff y
」を実行した端末すべてに表示されます。表示を無効化する場合は「biff n
」です。
特定のユーザーのツイートを取得するスクリプト
仕組みさえわかってしまえば、どう活用するかはあなたのアイデア次第です。メールのように外部からpushされる通知のうち、作業を中断してまで受け取りたい通知と言えば、そうTwitterです[5]。
Twitterの情報を取得する方法はいろいろありますが、今回はPythonのTwitterライブラリーであるTwythonを使うことにしましょう。なお今回紹介するPythonスクリプトは3.6が前提になっています。これはmbox用のメッセージを構築するために、3.6で追加されたemail.messageを使用しているためです。よってUbuntu
17.04であればpython3.6パッケージ、Ubuntu 16.04以前であれば別途Python 3.6をインストールする必要があることに注意してください。
ただしemail.messageでないといけないというわけではなく、メッセージ中身はただのプレーンテキストなので、手で構築するように改修すればPython 3.6より前のバージョンでも動きます。
あらかじめapps.twitter.comでConsumer Key/Consumer Secret/Access Token/Access Token Secretの4つのキーを生成しておいてください。今回は特にTwitterに書き込むことはしませんので、パーミッションはread-onlyで十分です。Stream APIを使うためUser authentication(OAuth 1.0a)を使う必要があります。そこであらかじめAccess
Tokenも取得しているわけです。
今回使うPythonスクリプトは次のとおりです。
USERS
はツイートを取得したいユーザーのscreen_name
のリストです。SEND_TO
が送信対象のユーザー名になります。HOST
がcomsatデーモンのアドレス(今回はIPv6のループバックアドレス)、PORT
がそのポート番号です。
Twython
とTwythonStreamer
についてはTwythonのドキュメントを見てもらうほうがはやいでしょう。ほぼチュートリアルどおりの構成です。引数にfollow
を追加しているのは、Public StreamをユーザーIDでフィルタリングした場合、mentionにそのユーザーIDが含まれているツイートも引っかかってしまうからです。そこでon_success()
で送信者がfollow
にリストアップしたユーザーのときのみcomsatに通知するようにしています。
save_mbox()
とsend_biff()
がbiff固有の処理です。save_mbox()
ではemail.message.EmailMessage
で電子メールのメッセージを作成しています。最初の行に「From Twitter 日付」形式のメッセージを記録し、ツイートの中身をBodyとしています。「From:
」ヘッダーにはツイートした人のscreen_name
を残しています。
mbox形式のファイルはmailboxライブラリーのmailbox.mbox
で操作できます。add()
メソッドでメッセージを簡単に追加できるのですが、追加したメッセージのファイル先頭からのオフセット位置を簡単に知るすべはありません。mailboxにはは指定したメッセージ部分のみをファイルオブジェクトとして取得できるget_file()
メソッドが存在しますので、このオブジェクトの_start
プロパティで代用しています。アンドキュメンテッドなプロパティを使いたくない場合は、from_line
で検索して算出してもいいかもしれません。
保存先(SEND_TO
)は相対パスになっています。このまま実行するとカレントディレクトリに保存されることになるので注意してください。カレントディレクトリの位置はのちほどsystemd側で調整する予定です。ただしこのスクリプトはbiffでしか使わないことを考えると、/var/mail
までスクリプトの中で含めてしまうという方法もあるでしょう。
send_biff()
の方はごく普通のネットワークプログラミングです。以下のフォーマットのメッセージを送っています。
サービスファイルの作成
先ほど作成したスクリプトもsystemdから起動するように「/etc/systemd/system/trump.service
」を作成しておきましょう。
network-online.target
を使うことで、ネットワーク接続が有効化されたときにサービスが起動することになります。これはNetworkMangerでもsystemd-networkdでも有効です。サービスを有効化すると、ツイートの取得を開始します。
あとはbiff y
した端末の前で靴紐がほどけているか確認しながら待っていると、トランプ大統領のツイートが流れてくるはずです。
ちなみにcomsatは渡された文字列に対してtoascii()
で最上位ビットを落とそうとします。つまり日本語や絵文字はまったく表示できません。ただemail.message.EmailMessage
の時点でBase64エンコードするため、表示された文字列をbase64
コマンドでデコードすれば日本語や絵文字に戻すことは可能です。