UECジャーナル

開眼☆シェルスクリプト【11】Dropboxもどきを作る(2) ― 起動の簡略化/同期タイミングの改善

はじめに

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/以下を操作すると、次のように操作が報告される。

~/SYNCBOX/以下を操作する

$ cd ~/SYNCBOX/
$ touch try
$ rm try
$ cat ~/TESTDATA | head -n 1000 > try
$ 

inotifywait(1)の出力

/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
$ 

service(8)でサービスを制御する

Unix系のOSではservice(8)でサービスの制御を行うことが多い。Apache HTTPd Serverの起動や停止を次のように作業したことがあるだろう。

$ service apache start
$ service apache stop

SYNCBOXもほかのサービスと同じようにservice(8)で制御できるようにしたい。ただし、このあたりのスクリプトはディストリビューションごとに大きく異なるので、利用しているOSに合わせて作成する必要がある。代表的なプラットフォームとしてここではFreeBSDとUbuntuでの方法を紹介する。

service(8)向け制御スクリプト FreeBSD

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はシステム起動時に自動的に起動するようになる。

service(8)向け制御スクリプト Ubuntu

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) ― 起動の簡略化/同期タイミングの改善」より加筆修正後転載

Last modified: 2014-01-13 00:00:00