PT3サーバ自動起動・シャットダウン用スクリプト
BIOSの設定
RTCで起動できる様に設定する
Asrock H77M-ITXの場合は
Advanced Screen -> ACPI Configuration -> RTC Alarm Power On
自動復帰の確認
以下のコマンドを実行し,3分後に自動的に起動されることを確認する
$ echo `date +%s -d +3min` | sudo tee /sys/class/rtc/rtc0/wakealarm $ sudo shutdown -h now
上記のコマンドで成功しない場合はrtcwake
を使用すると成功するかもしれません
スクリプト作成
とりあえず,スクリプト全体は以下の通り.
#!/bin/python # -*- coding: utf-8 -*- import subprocess import datetime import time import logging from logging.handlers import RotatingFileHandler SUDO_PASS="実行ユーザのパスワード\n" log_handler = RotatingFileHandler( filename="/var/www/html/epgrec/video/pt3_savepower.log", mode="a", maxBytes=1000000, backupCount=3, encoding="utf-8", delay=False ) # log_handler.setLevel(logging.INFO) log_handler.setFormatter( logging.Formatter("%(asctime)s - %(levelname)-8s - %(message)s") ) logger = logging.getLogger() logger.setLevel(logging.INFO) logger.addHandler(log_handler) def is_terminal_login(): stdout, stderr = subprocess.Popen( ["/usr/bin/who"], stdout=subprocess.PIPE, stderr=subprocess.PIPE ).communicate() result = len([line for line in stdout.split("\n") if not line == ""]) logger.info("[terminal]: %d" % result) return False if result is 0 else True def is_recording_at(): stdout, stderr = subprocess.Popen( ["sudo", "-S", "/bin/atq"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE ).communicate(SUDO_PASS) result = len([line for line in stdout.split("\n") if "=" in line]) logger.info("[rec_at ]: %d" % result) logger.debug(stdout) return False if result is 0 else True def is_recording_ps(): stdout, stderr = subprocess.Popen( ["/bin/ps", "-el"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE ).communicate() ps_recpt1 = [line for line in stdout.split("\n") if "recpt1" in line] result = len(ps_recpt1) logger.info("[rec_ps ]: %d" % result) logger.debug(stdout) return False if result is 0 else True def is_port_established(): stdout, stderr = subprocess.Popen( ["sudo", "-S", "/sbin/lsof", "-i", ":ssh,http,https"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE ).communicate(SUDO_PASS) port_est = [line for line in stdout.split("\n") if "ESTABLISHED" in line] result = len(port_est) logger.info("[port ]: %d" % result) logger.debug(stdout) return False if result is 0 else True def convert_timestr(atq_line): result = atq_line.replace("Jan", "01") result = result.replace("Feb", "02") result = result.replace("Mar", "03") result = result.replace("Apr", "04") result = result.replace("May", "05") result = result.replace("Jun", "06") result = result.replace("Jul", "07") result = result.replace("Aug", "08") result = result.replace("Sep", "09") result = result.replace("Oct", "10") result = result.replace("Nov", "11") return result.replace("Dec", "12") def get_at_schedules(): stdout, stderr = subprocess.Popen( ["sudo", "-S", "/bin/atq"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE ).communicate(SUDO_PASS) schedule = [] for line in stdout.split("\n"): if line is "": continue tmp = line.split() time_str = convert_timestr(" ".join([tmp[5], tmp[2], tmp[3], tmp[4]])) time_str = datetime.datetime.strptime(time_str, "%Y %m %d %H:%M:%S") unixtime = int(time.mktime(time_str.timetuple())) schedule.append(unixtime) return schedule def get_closest_schedule(): return sorted(get_at_schedules())[0] def is_scheduled(unixtime): return any(s == unixtime for s in get_at_schedules()) def set_getepg_schedule(unixtime): if is_scheduled(unixtime): logger.info("getepg is already scheduled") else: getepg_at = datetime.datetime.fromtimestamp(unixtime).strftime("%y%m%d%H%M") stdout, stderr = subprocess.Popen( ["/bin/at", "-f", "/var/www/html/epgrec/video/at_getepg.sh", "-t", getepg_at], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE ).communicate() logger.info("[getepg ]: %s" % datetime.datetime.fromtimestamp(unixtime)) def set_rtcwake(unixtime): logger.info("[wakeup ]: %s" % datetime.datetime.fromtimestamp(unixtime)) stdout, stderr = subprocess.Popen( ["sudo", "-S", "/usr/sbin/rtcwake", "-m", "off", "-t", str(unixtime)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE ).communicate(SUDO_PASS) logger.debug(stdout) def main(): logger.info("=============== START LOGGING ===============") if any([ is_terminal_login(), is_recording_at(), is_recording_ps(), is_port_established() ]): logger.info("someone using") else: now = int(time.time()) closest_waketime = now + 1800 closest_schedule = get_closest_schedule() wake_to_record = closest_schedule - 300 wake_to_getepg = closest_schedule - 1800 getepg_time = wake_to_getepg + 300 logger.info("NOW : %s" % datetime.datetime.fromtimestamp(now)) logger.info("WEPG : %s" % datetime.datetime.fromtimestamp(wake_to_getepg)) logger.info("GEPG : %s" % datetime.datetime.fromtimestamp(getepg_time)) logger.info("WREC : %s" % datetime.datetime.fromtimestamp(wake_to_record)) logger.info("CLOSE: %s" % datetime.datetime.fromtimestamp(closest_schedule)) if wake_to_getepg > closest_waketime: set_getepg_schedule(getepg_time) set_rtcwake(wake_to_getepg) elif wake_to_record > closest_waketime: set_rtcwake(wake_to_record) else: logger.info("close to wakeup time") if __name__ == '__main__': main()
グローバルスコープ
SUDO_PASS
スクリプト実行ユーザーのパスワードを入れてsudo
を実行できるようにする。
"パスワード\n"
のように末尾に\n
が入っているが,これがパスワード入力後のエンターキーの代わりになる。log_handler
に循環ログを定義する
main()
is_terminal_login()
ターミナルにログインしているユーザがいるかis_recording_at()
録画中かをat
コマンドで確認is_recording_ps()
録画中かをps
コマンドで確認is_port_established()
ssh,http,httpsのポートが使用中(接続中)であるかを確認
で,サーバの使用状況を確認し,シャットダウンして問題がなければ,
now = int(time.time())
現在時刻closest_waketime = now + 1800
次回起動の最短時刻(シャットダウンした場合に現在時刻から1800秒以内に起動する必要がある場合はシャットダウンしないようにするための値)closest_schedule = get_closest_schedule()
一番近い録画開始時刻wake_to_record = closest_schedule - 300
録画のために起動する時間(起動から録画開始まで,300秒の余裕をもたせる)wake_to_getepg = closest_schedule - 1800
時間に余裕がある場合は録画開始の1800秒前に起動しEPGの更新を行うgetepg_time = wake_to_getepg + 300
EPGの更新を行う場合にEPGの更新を開始する時刻(起動からEPG更新開始まで,300秒の余裕をもたせる)
上で設定した時刻を使用し
- 時間に余裕がある(
wake_to_getepg > closest_waketime
)場合
at
にEPG取得のスケジュールを登録した後,rtcwake
でシステムを停止する - EPG更新の余裕がない(
wake_to_record > closest_waketime
)場合は
rtcwake
でシステムを停止するだけにする - それ以外の場合 現在時刻から30分以内に録画開始する必要があるのでPCへの負荷を考慮し,シャットダウンせずに待機する
is_terminal_login()
who
を使用し,ログイン中のユーザを取得する
以下のコマンドを実行し,行数を数えることでログイン中のユーザがいるかを判断する
$ who ユーザー名 pts/0 2016-01-23 18:42 (192.168.19.5) ユーザー名 pts/1 2016-01-23 19:09 (192.168.19.5)
who
の実行結果stdout
に対し,以下の処理をすることで行数を数える。
len([line for line in stdout.split("\n") if not line == ""])
who
の実行結果を\n
で分割すると
[ "ユーザ1...", "ユーザ2...", "" ]
のように最後の改行コード後にも文字列があるように分割されるので
if not line == ""
を入れている
is_recording_at()
atq
を使用し,実行中のatジョブを取得する
$ sudo atq 524 Fri Jan 29 00:29:00 2016 a ユーザー名 455 Sat Jan 23 02:30:00 2016 = ユーザー名 465 Sun Jan 24 03:45:00 2016 a ユーザー名 466 Sun Jan 24 12:15:00 2016 a ユーザー名 467 Sun Jan 24 09:35:00 2016 a ユーザー名
sudo atq
の実行結果stdout
に対し,以下の処理で=
を含む行数を数える。
len([line for line in stdout.split("\n") if "=" in line])
is_recording_ps()
ps
を使用し,recpt1
を使用しているプロセスがあるかを確認する
$ ps -el F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD ... 0 S 1000 2936 2935 0 82 2 - 68925 hrtime ? 00:00:00 recorder.php 0 S 1000 2940 2936 0 82 2 - 2902 wait ? 00:00:00 do-record.sh 0 S 1000 2941 2940 4 82 2 - 59385 pt3_dm ? 00:02:01 recpt1 0 S 0 2942 1 0 80 0 - 61852 poll_s ? 00:00:00 pcscd ...
ps -el
の実行結果stdout
に対し,以下の処理でrecpt1
を含む行数を数える。
len([line for line in stdout.split("\n") if "recpt1" in line])
is_port_established()
lsof
を使用し,ssh,http,https
が使用されているかを確認する
$ sudo lsof -i :ssh,http,https COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME sshd 1081 root 3u IPv4 17772 0t0 TCP *:ssh (LISTEN) sshd 1081 root 4u IPv6 17783 0t0 TCP *:ssh (LISTEN) nginx 1127 root 6u IPv4 18486 0t0 TCP *:http (LISTEN) nginx 1128 nginx 3u IPv4 27768 0t0 TCP PT3:http->192.168.19.5:34158 (ESTABLISHED) nginx 1128 nginx 6u IPv4 18486 0t0 TCP *:http (LISTEN) sshd 3032 root 3u IPv4 25384 0t0 TCP PT3:ssh->192.168.19.5:60890 (ESTABLISHED) sshd 3034 pt3 3u IPv4 25384 0t0 TCP PT3:ssh->192.168.19.5:60890 (ESTABLISHED) sshd 3043 root 3u IPv4 25436 0t0 TCP PT3:ssh->192.168.19.5:60902 (ESTABLISHED) sshd 3045 pt3 3u IPv4 25436 0t0 TCP PT3:ssh->192.168.19.5:60902 (ESTABLISHED)
sudo lsof -i :ssh,http,https
の実行結果stdout
に対し,以下の処理でESTABLISHED
を含む行数を数える。
len([line for line in stdout.split("\n") if "ESTABLISHED" in line])
get_at_schedule()
atq
でスケジュールされているスケジュールの各時刻をunixtimeに変換したリストを返す
atq
からは以下のフォーマットで出力されるので
467 Sun Jan 24 09:35:00 2016 a ユーザー名
以下のようにフォーマットを変更し
convert_timestr()
関数で月コード(Jan)を数値(01)に変換する
2016 01 24 09:35:00
この文字列を以下でunixtimeに変換する
time_str = datetime.datetime.strptime(time_str, "%Y %m %d %H:%M:%S") unixtime = int(time.mktime(time_str.timetuple()))
get_closest_schedule()
get_at_schedule()
から得られたunixtimeのリストをソートし,最小値(最も近いスケジュールのunixtime)を取得する
set_getepg_schedule()
引数のunixtimeの時刻にat
にEPG更新をスケジュールする
- サーバが自動起動される前に手動でサーバを起動した場合には同じ時刻に複数のEPG更新がスケジュールする可能性があるので
is_scheduled()
で引数のunixtimeの時刻にat
にスケジュールがないかを確認する at
は直接php(getepg.php
)を実行できないため,以下のシェルスクリプトを経由してスケジュールを登録する
#!/bin/bash /var/www/html/epgrec/getepg.php
set_rtcwake()
rtcwake
を使用し,自動起動時刻を設定する
また,rtcwake
を実行すると自動的にシャットダウンされる
スクリプトのcron実行
sudo
を含むスクリプトをcronで実行すると
sudo: sorry, you must have a tty to run sudo
と言われ実行できないのでttyなしでsudo
できるようにする
今回はPT3専用のサーバなので簡単に以下のように56行目のDefaults requiretty
をコメントアウトした
$ sudo visudo # Disable "ssh hostname sudo <cmd>", because it will show the password in clear. # You have to run "ssh -t hostname sudo <cmd>". # #Defaults requiretty
以上で実行できるはずなので10分ごとにスクリプトを実行するようにする
$ crontab -e */10 * * * * python /path/to/script.py