cron から起動されたジョブを、一時停止・再開する


我が家は伊豆の山奥なので光回線が来ておらず、遅い ADSL でやりくりをしています。一方、サーバは cron(8) で rsync(1) を起動してリモートからのファイルのミラーリングをしているのですが、こいつがネットワークの帯域を専有してしまうと他の作業ができなくなってしまいます。そこで、cron から実行されたコマンドを必要に応じて一時停止・再開できると良いなと思った次第です。

（この手のデーモン化の手段は、Linux なんかでは普通に提供されていたと思うんですが、Mac でも動かしたかったのと、pid ファイルやロックファイルを使いたくなかったので、以下のような書き方になっている次第です）

以下はサンプルです。`putdates0 exec` で、端末に延々と日付を表示し始めます。他の端末から `putdates0 stop` とすればプロセスは一時停止、`putdates0 cont` とすれば再開、`putdates0 snooze` とすればプロセスは一時停止した後 10 秒後に再開します。

~~~~bash:putdates0
#!/bin/bash
# -*- coding: utf-8 -*-

## プロセスの名前（システムでユニークに）
process_name="SimplePutDatesProcess"

## スヌーズさせた際の復帰までの秒数
snooze_interval="10"

## サブコマンド: exec | term | stop | cont | snooze | _doit
subcommand="$1"

pid=$(pgrep -f "^$process_name ")

## 子孫プロセスを一覧する
function get_children() {
	for pid in $(pgrep -P $1)
	do
		echo $pid
		get_children $pid
	done
}

## 子孫プロセスにシグナルを発行する
function kill_tree() {
	signal=$1
	pid=$2
	kill -s $signal $pid $(get_children $pid)
}

## サブコマンドの処理
case $subcommand in
	## プロセス名を変更するために、実際の仕事をするプロセスを exec する
	exec )
		if test -z "$pid"
		then
			exec -a "$process_name" bash "$0" _doit
		fi
		exit 0
		;;
	## プロセスの終了
	term )
		kill_tree TERM $pid
		;;
	## プロセスの一時停止
	stop )
		kill_tree STOP $pid
		;;
	## プロセスの再開
	cont )
		kill_tree CONT $pid
		;;
	## プロセスを一時停止するが、一定時間後に再開
	snooze )
		kill_tree STOP $pid
		(
			sleep $snooze_interval
			bash "$0" cont
		) &
		;;
	## 実際のジョブ
	_doit )
		while true
		do
			date
			sleep 1
		done
		;;
esac
~~~~

同種のコマンドを作成する際に上記コードをコピペしたくはありません。下記のように、実行コマンド（`putdates`）と実ジョブコマンド（`_putdates`）に実装を分けて、プロセス管理の機能は `smartprocess` の名前で外出ししました。`putdates stat` のように、プロセスの状態を確認するサブコマンドを追加しています。

~~~~bash:putdates
#!/bin/bash
# -*- coding: utf-8 -*-

exec smartprocess _putdates PutDatesProcess 3600 "$@"
~~~~

~~~~bash:_putdates
#!/bin/bash
# -*- coding: utf-8 -*-

while true
do
	date
	sleep 1
done
~~~~

~~~~bash:smartprocess
#!/bin/bash
# -*- coding: utf-8 -*-

job_command="$1"
process_name="$2"
snooze_interval="$3"
subcommand="$4"

pid=$(pgrep -f "^$process_name ")

## 子孫プロセス一覧
function get_children() {
	for pid in $(pgrep -P $1)
	do
		echo $pid
		get_children $pid
	done
}

## 子孫プロセスにシグナルを発行
function kill_tree() {
	signal=$1
	pid=$2
	kill -s $signal $pid $(get_children $pid)
}

## サブコマンドの処理
case $subcommand in
	## 実際の仕事をするプロセスを exec する
	exec )
		if test -z "$pid"
		then
			exec -a "$process_name" bash "$0" "${@:1:$#-1}" _doit
		fi
		exit 0
		;;
	## プロセスの終了
	term )
		kill_tree TERM $pid
		;;
	## プロセスの一時停止
	stop | sus | susp | suspend )
		kill_tree STOP $pid
		;;
	## プロセスの再開
	cont | res | resume )
		kill_tree CONT $pid
		;;
	## プロセスを一時停止するが、一定時間後に再開
	snooze )
		kill_tree STOP $pid
		(
			sleep $snooze_interval
			bash "$0" "${@:1:$#-1}" cont
		) &
		;;
	## 実際のジョブプロセスを、排他で起動
	_doit )
		$job_command
		;;
	## プロセスの状態を表示
	stat )
		if test -z "$pid"
		then
			echo Not Running
			exit 1
		fi
		statline=$(ps uww $pid | tail -n 1)
		stat=$(echo "$statline" | awk '{print $8}')
		echo $stat | grep -q -e T && state=Stopped || state=Running
		started=$(echo "$statline" | awk '{print $9}')
		time=$(echo "$statline" | awk '{print $10}')
		echo pid: $pid \| started: $started \| time: $time \| state: $state
		;;
esac
~~~~

このような感じで、実行コマンドと、時間のかかる実ジョブコマンドを用意すれば、いつでもジョブの一時停止と再開ができるようになります。

crontab(5) の設定的には、一定間隔で `exec` をしておき、`stop` 後の `cont` のし忘れに対処するために、一日に一度ほど `cont` するようにしておくと良いのではないでしょうか。

今のところ動作は良好なようで、Mac OS X、Linux 上で共に動作しています。何かお気づきの点がありましたらコメントをいただければと思います。

ではまた。