Dropboxもどきを作る(1)において、複数のマシンのデータを自動で同期するアプリケーションSYNCBOXを作成した。SYNCBOXはサーバを経由して複数のクライアントPCのファイルを同期するアプリケーション。それぞれのクライアントの~/SYNCBOX/ディレクトリ以下をrsync(1)で同期する。多数台の接続を想定し、同時に2台以上のクライアントとサーバが同期処理しないように、排他制御の仕組みを入れてある。
排他制御は、クライアント側からサーバ側にディレクトリを作りにいき、その成否を利用して行っている。mkdir(1)を排他区間の作成に使う手法だ※1。
クライアントからサーバへの同期はクライアントの~/SYNCBOX/ディレクトリ以下が変更されたときだけでよいので、対象が変更されたタイミングでサーバへ同期しにいくようにしてみよう。inotifywait(1)コマンドを使うと、こうしたファイルの変更などを検出できる。
たとえば~/SYNCBOX/以下のディレクトリを監視は次のように実施できる。
$ inotifywait -mr ~/SYNCBOX/ Setting up watches. Beware: since -r was given, this may take a while! Watches established.
inotifywaitプログラムが動作し続ける。ほかの端末から~/SYNCBOX/以下を操作すると、次のように操作が報告される。
$ cd ~/SYNCBOX/ $ touch try $ rm try $ cat ~/TESTDATA | head -n 1000 > try $
/home/ueda/SYNCBOX/ OPEN try /home/ueda/SYNCBOX/ ATTRIB try /home/ueda/SYNCBOX/ CLOSE_WRITE,CLOSE try /home/ueda/SYNCBOX/ DELETE try /home/ueda/SYNCBOX/ CLOSE_WRITE,CLOSE try /home/ueda/SYNCBOX/ MODIFY try /home/ueda/SYNCBOX/ OPEN try /home/ueda/SYNCBOX/ MODIFY try ... /home/ueda/SYNCBOX/ MODIFY try /home/ueda/SYNCBOX/ MODIFY try /home/ueda/SYNCBOX/ CLOSE_WRITE,CLOSE try
inotifywait(1)は上記のようにファイルに関するさまざまなイベントに反応するが、-eオプションで特定のイベントだけ引っ掛けることもできる。このコマンドを使って同期対象のディレクトリをモニタリングし、変更を感知したら特定のファイルを作成するスクリプトSYNCBOX.WATCHを次のように作成する。
$ cd ~/.syncbox/ $ cat SYNCBOX.WATCH #!/bin/sh dir=/home/ueda/SYNCBOX sys=/home/ueda/.syncbox touch $sys/PUSH.REQUEST inotifywait -e moved_to -e close_write -mr $dir | while read str ; do [ -e $sys/PUSH.REQUEST ] && touch $sys/PUSH.WAIT touch $sys/PUSH.REQUEST done $
SYNCBOX.WATCHは次の2つのイベントをモニタリングする。
ファイルが~/SYNCBOX/に移動してきた
~/SYNCBOX/内でなにかファイルの書き込みが終わってファイルが閉じられた
イベントが発生すると~/.syncbox/以下にPUSH.REQUESTとPUSH.WAITというファイルを作成する。PUSH.WAITはPUSH.REQUESTがすでに存在する場合に限って作成する。
SYNCBOX.WATCHの動きに対応するようにSYNCBOX.SYNCを書き換える。
$ cat SYNCBOX.SYNC #!/bin/sh -xv # SYNCBOX.SYNC # written by R. Ueda (ユニバーサル・シェル・プログラミング研究所) Jul. 21, 2012 exec 2> /tmp/$(basename $0) server=sync.example.jp sys=/home/ueda/.syncbox s="$server:/home/ueda/SYNCBOX/" c="/home/ueda/SYNCBOX/" MESSAGE () { DISPLAY=:0 notify-send "SYNCBOX: $1" } ERROR_CHECK(){ [ "$?" = "0" ] && return DISPLAY=:0 notify-send "SYNCBOX: $1" exit 1 } #ロックがとれなかったらすぐ終了 ssh -o ConnectTimeout=5 $server "mkdir $sys/LOCK" || exit 0 #同期の必要がなければすぐ終了 NUM=$(rsync -auzin --timeout=30 $s $c | wc -c) #通信に失敗した、あるいは同期済みなら終了 if [ "$NUM" = "" -o "$NUM" -eq 0 ] ; then ssh -o ConnectTimeout=5 $server "rmdir $sys/LOCK" exit 0 fi #pull############################ MESSAGE "受信開始" rsync -auz --timeout=30 $s $c ERROR_CHECK "受信中断" MESSAGE "受信完了" #push############################ while [ -e "$sys/PUSH.REQUEST" ] ; do MESSAGE "送信開始" rsync -auz --timeout=30 $c $s ERROR_CHECK "送信中断" rm $sys/PUSH.REQUEST [ -e $sys/PUSH.WAIT ] && mv $sys/PUSH.WAIT $sys/PUSH.REQUEST MESSAGE "送信完了" done ssh -o ConnectTimeout=5 $server "rmdir $sys/LOCK" exit 0 $
上記では同期の必要がなければ同期処理を実行しないというチェックも追加してある。一旦rsync(1)を空実行して同期を実施する必要があるかどうかをチェックさせている。
実際にサービスとして動作させるには、あといくつかスクリプトを用意する必要がある。ここではSYNCBOX.LOOPとSYNCBOX.SUSSTOPというスクリプトを用意した。SYNCBOX.LOOPは定期的にSYNCBOX.SYNCを実行するプログラム、SYNCBOX.SUSSTOPはノートPCを使っている場合のサスペンド対策だと思ってもらえればよい。サスペンドすると不必要にSYNCBOX.SYNCが動作するケースがあるため、これを検出して対処している。
$ cd ~/.syncbox/ $ cat SYNCBOX.LOOP #!/bin/sh -xv # SYNCBOX.LOOP : 1分ごとにSYNCBOX.SYNCを実行する while : ; do /home/ueda/.syncbox/SYNCBOX.SYNC sleep 60 done $
$ cat SYNCBOX.SUSSTOP #!/bin/sh -xv # SYNCBOX.SUSSTOP : 不必要に動作しているSYNCBOX.SYNCを終了させる FROM=$(date +%s) while sleep 1 ; do TO=$(date +%s) DIFF=$(( TO - FROM )) if [ "$DIFF" -gt 2 ] ; then kill $(ps auxww | grep SYNCBOX.SYNC | grep -v grep | awk '{print $2}') FROM=$(date +%s) fi FROM=$TO done $
Unix系のOSではservice(8)でサービスの制御を行うことが多い。Apache HTTPd Serverの起動や停止を次のように作業したことがあるだろう。
$ service apache start $ service apache stop
SYNCBOXもほかのサービスと同じようにservice(8)で制御できるようにしたい。ただし、このあたりのスクリプトはディストリビューションごとに大きく異なるので、利用しているOSに合わせて作成する必要がある。代表的なプラットフォームとしてここではFreeBSDとUbuntuでの方法を紹介する。
FreeBSDの場合、rc(8) (開発コード名: rcNG)の書式に則ってrc.d形式のスクリプトを記述して/usr/local/etc/rc.d/に配置すればよい。たとえば次のようにrc.dスクリプトを作成する。
$ cat /usr/local/etc/rc.d/syncbox #!/bin/sh # PROVIDE: syncbox # REQUIRE: LOGIN # KEYWORD: shutdown # Add the following lines to /etc/rc.conf to enable `syncbox': # # syncbox_enable="YES" . /etc/rc.subr name="syncbox" rcvar=syncbox_enable start_cmd=syncbox_start stop_cmd=syncbox_stop homedir=/home/ueda command=$homedir/SYNCBOX.LOOP syncbox_already_running() { echo "syncbox already running?" exit 1 } syncbox_not_running() { echo "syncbox is not running." exit 1 } syncbox_start() { ps auxww | grep -v grep | grep -q SYNCBOX.SUSSTOP && syncbox_already_running ps auxww | grep -v grep | grep -q SYNCBOX.LOOP && syncbox_already_running ps auxww | grep -v grep | grep -q SYNCBOX.WATCH && syncbox_already_running $homedir/SYNCBOX.SUSSTOP & $homedir/SYNCBOX.LOOP & $homedir/SYNCBOX.WATCH & } syncbox_stop() { ps auxww | grep -v grep | grep -q SYNCBOX.SUSSTOP || syncbox_not_running ps auxww | grep -v grep | grep -q SYNCBOX.LOOP || syncbox_not_running ps auxww | grep -v grep | grep -q SYNCBOX.WATCH || syncbox_not_running kill $(ps auxww | grep SYNCBOX.SUSSTOP | grep -v grep | awk '{print $2}') kill $(ps auxww | grep SYNCBOX.LOOP | grep -v grep | awk '{print $2}') kill $(ps auxww | grep SYNCBOX.WATCH | grep -v grep | awk '{print $2}') } load_rc_config "$name" run_rc_command "$1" $
サービスとして制御できるように/etc/rc.confにsyncbox_enable="YES"の設定を追加する。
$ grep syncbox /etc/rc.conf syncbox_enable="YES" $
次のようにservice(8)経由でSYNCBOXを制御できるようになる。
$ service syncbox help /usr/local/etc/rc.d/syncbox: unknown directive 'help'. Usage: /usr/local/etc/rc.d/syncbox [fast|force|one|quiet](start|stop|restart|rcvar|status|poll) $ service syncbox onestart $ service syncbox onestart syncbox already running? % service syncbox onestop $ service syncbox onestop syncbox is not running. $
/etc/rc.confにsyncbox_enable="YES"の設定が追加してあれば、SYNCBOXはシステム起動時に自動的に起動するようになる。
Ubuntuの場合、まず次のような制御スクリプトを作成する。
$ cat SYNCBOX.INIT #!/bin/sh # # SYNCBOX.INIT SYNCBOXの起動・終了 # # written by R. Ueda (r-ueda@usp-lab.com) exec 2> /dev/null sys=/home/ueda/.syncbox case "$1" in start) ps cax | grep -q SYNCBOX.SUSSTOP && exit 1 ps cax | grep -q SYNCBOX.LOOP && exit 1 ps cax | grep -q SYNCBOX.WATCH && exit 1 $sys/SYNCBOX.SUSSTOP & $sys/SYNCBOX.LOOP & $sys/SYNCBOX.WATCH & ;; stop) killall SYNCBOX.SUSSTOP killall SYNCBOX.LOOP killall SYNCBOX.WATCH ;; *) echo "Usage: SYNCBOX {start|stop}" >&2 exit 1 ;; esac exit 0 $
SYNCBOX.INITの動作確認をすると次のようになる。
$ cd ~/.syncbox/ $ ./SYNCBOX.INIT start $ ps cax | grep SYNCBOX 26072 pts/5 S 0:00 SYNCBOX.SUSSTOP 26073 pts/5 S 0:00 SYNCBOX.LOOP 26075 pts/5 S 0:00 SYNCBOX.SYNC $ ./SYNCBOX.INIT start $ echo $? 1 ←すでに動作しているので2度目は起動しない $ ./SYNCBOX.INIT stop $ ps cax | grep SYNC $
service(8)から制御できるように、/etc/init.d/以下に次のようにリンクを作成する。
$ cd /etc/init.d/ $ ln -s /home/ueda/.syncbox/SYNCBOX.INIT syncbox $ ls -l syncbox lrwxrwxrwx 1 root root 32 8月 17 10:08 syncbox -> /home/ueda/.syncbox/SYNCBOX.INIT $
次のようにservice(8)経由で制御できる。
$ service syncbox start $ ps cax | grep SYNCBOX 26433 pts/3 S 0:00 SYNCBOX.SUSSTOP 26434 pts/3 S 0:00 SYNCBOX.LOOP 26435 pts/3 S 0:00 SYNCBOX.SYNC $ service syncbox start $ echo $? 1 $ service syncbox stop $ ps cax | grep SYNCBOX $
システム起動時にSYNCBOXが起動するように/etc/rc.localファイルにSYNCBOX.INITを仕掛ける。
$ cat /etc/rc.local #!/bin/sh -e # # rc.local # (略) su - ueda -c '/home/ueda/.syncbox/SYNCBOX.INIT start' exit 0 $
システムを再起動して次のようにサービスが起動していれば設定完了だ。
# reboot ...再起動... $ ps caxu | grep SYNC ueda 1364 0.0 0.0 17472 1460 ? S 10:46 0:00 SYNCBOX.SUSSTOP ueda 1366 0.0 0.0 4392 608 ? S 10:46 0:00 SYNCBOX.LOOP $
コマンドとシェルスクリプトでそれなりに実用的に使用できる同期サービスが実装できたことになる。
オンラインストレージもどきSYNCBOXを作った。テクニックをまとめると次のとおり。
sshとrsyncのタイムアウト
rsyncの使い方
notify-sendinotify (inotifywait)
mkdirを使った排他制御
serviceの使い方
sshを使ったリモートからのコマンド実行
ユーザが使えるOSの機能はほとんどコマンドで用意されている。そのため、シェルスクリプトを書けるとOSの提供する機能をフル活用することができる。これはシェルスクリプトでアプリケーションを書く大きな利点だ。
※1 使用するファイルシステムやカーネル内部での実装にも依存するのだが、より厳密に処理する必要がある場合には、シンボリックリンクの作成を使用してアトミックな処理とした方がよい。シンボリックリンクの作成はアトミックに処理される。
Software Design 2012年11月号 上田隆一著、「テキストデータならお手のもの 開眼シェルスクリプト 【11】Dropboxもどきを作る(2) ― 起動の簡略化/同期タイミングの改善」より加筆修正後転載