確実にタイマー予約するまでの格闘
前回からの続き。rec_radiko.shをタイマー予約して起動したい。そうすれば、指定したラジオ番組を時間に左右されずに視聴することができる。今時の小学1年生でさえ、当然のように好きなTV番組を予約録画して楽しんでいる。以前購入したStaitionTVだって、録画予約機能が実現されていた。タイマー予約機能は必須である。
しかし、そのタイマー予約機能を市販のソフトウェアの機能を借りずに自ら実現しようとすると、意外と大変である。当初は、そんなのlaunchdの設定をしておけば簡単にできるだろう、とたかをくくっていたが、実は奥が深かった...。以下は確実にタイマー予約するまでの試行錯誤の軌跡である。
launchdで試す
- まずはOSXでお決まりのlaunchdで試してみた。
- OSXのすべてのプロセスは、launchdが管理している。
- launchdは、何を、いつ起動するかという情報を設定しておけば、そのタイミングでプロセスを起動していくれる。
- cronさえも、launchdが管理する1プロセスでしかない。
- 例えば、「毎週日曜日の17:00にrec_radiko.shを起動する」には、以下のように設定するのだ。
- launchdの設定はplist形式のxmlファイルなのだが、人には難解である。
- だから、いつも人に優しいLingonを使ってお手軽に設定している。
- 上記設定を~/Library/LaunchAgents/com.bebekoubou.rec_radikoru_日_1700_60_FMJ.plistとして保存した。
- これで、「毎週日曜日の17:00にrec_radiko.shが起動する」はず。
...なのだが、実はうまく動かなかった。
launchdでの苦悩
動かない原因はいろいろあった。
設定を反映させる
- まず、launchd.plistを新規設定したら、再起動あるいはロードをしなければならない。
- 既存のlaunchd.plistを修正した場合は、再起動あるいは一旦アンロードしてから再びロードする必要がある。
- 再起動は面倒なので、アンロード・ロードしてみた。
$ launchctl unload ~/Library/LaunchAgents/com.bebekoubou.rec_radikoru_日_1700_60_FMJ.plist $ launchctl load ~/Library/LaunchAgents/com.bebekoubou.rec_radikoru_日_1700_60_FMJ.plist
- ちゃんと設定が反映されると、以下のように確認できるはずである。
$ launchctl list|grep 'com.bebekoubou.rec_radiko' - 0 com.bebekoubou.rec_radiko_日_17:00_60_J-WAVE
- ちなみに、launchctl listで表示されるのはファイル名ではなく、その内容のLabel項目である。
日本語ファイル名はNG
- さらに、launchd.plistのファイル名に日本語が混じっていると、設定がうまく反映されなかった。
- 理由は分からないが、素直に日本語ファイル名はやめて、「日」の部分をAppleScriptの数値形式「1」に変更した。
- ちなみに、launchd.plistの内容のLabel項目については、そのまま日本語の日曜日でOK。
$ launchctl unload ~/Library/LaunchAgents/com.bebekoubou.rec_radikoru_1_1700_60_FMJ.plist $ launchctl load ~/Library/LaunchAgents/com.bebekoubou.rec_radikoru_1_1700_60_FMJ.plist
- これでlaunchdの設定は問題なく設定されたはず。
スリープを解除する
- ところが、まだ日曜日の17:00にrec_radiko.shは起動してくれない。
- それはMacBookがスリープしてしまうから。
- launchdと言えども、スリープ中の起動はしてくれないのだ。
- システム環境設定 >> 省エネルギーでスリープしない設定にしてしまえば簡単なのだが、それはちょっと乱暴な気がする。
- ここは慎重に、システム環境設定 >> 省エネルギー >> スケジュール...から「起動またはスリープ解除」の設定をしてみた。
- これで「毎週日曜日の17:00にスリープが解除される」ようになった。
ディープスリープに対応する
- ところが、まだ日曜日の17:00にrec_radiko.shは起動してくれない...ことがある。
- よくよく観察してみると、それはMacBookがディープスリープしている場合があるから。
- ディープスリープしていると、OS環境が復帰するまでに30秒前後かかることがある。
- その間にlaunchd.plistに設定した時間が経過してしまい、rec_radiko.shが起動しないようなのだ。
- それを回避するために、launchd.plistで指定した時間の1分前にスリープ解除の時間を指定してみる。
- それからコンピュータがスリープするまでの時間の設定を3分以上にしておいた。
- 1分ではrec_radiko.shが起動する前に、再びスリープしてしまう心配があるので。
これで安定して「毎週日曜日の17:00にrec_radiko.shが起動する」ようになった!
スリープさせない努力
- ところが、録音は開始されるようになったものの、1時間ものの番組が5分くらいしか録音されていない...。
- なるほど、コンピュータのスリープの設定が3分とか5分になっているから、録音中に再びスリープに陥っているのだった。
- ならばコンピュータのスリープの設定を1時間に設定してしまえば済むことなのだが、やはりちょっと乱暴な方法。
- それに、じゃあ4時間ものの番組を録音したい時はどうする?という問題にもなってくる。
- 調べてみると、録音を開始した時にpmsetでアイドルスリープを回避するのが良さそうだと思った。
$ pmset noidle
Preventing idle sleep (^C to exit)...
- さらに調べを進めると、OSX 10.8にはcaffeinateという素晴らしすぎるコマンドが追加されていた!
$ caffeinate /Users/zari/Library/Scripts/rec_radiko.sh -t 3600 FMJ
複数の予約に対応する
- これで「毎週日曜日の17:00から1時間の録音」については、どうにかできるようになった!
- でも、じゃあ月曜日の6:00からも録音したいとか、複数のタイマー予約をしようとすると、とたんに事が難しくなる...。
- launchdの設定は複数設定できるのに、システム環境設定 >> 省エネルギー >> スケジュールでは、たった1つのスケジュールしか組めないのだ。
- 1つのスケジュールで複数の予約に対応するためには、定期的あるいは録音が完了するたびに、次の直近の予約を確認してスリープ解除のスケジュールを再設定するしかなさそう。
- しかも、rec_radiko.sh用にスケジュールを常に利用してしまうので、それ以外の目的でスケジュールを設定する自由も失われてしまう...。
- そんな事を悩みながら調べていると、pmsetコマンドには2系統のスケジュール設定があることに気付いた!
- pmset repeatは、いつものシステム環境設定 >> 省エネルギー >> スケジュール...の設定をするコマンド。(繰り返しのスケジュール用)
$ sudo pmset repeat wake U 16:59:00
- 一方、pmset scheduleを設定すると、上記の繰り返しスケジュールとは別に単発のスケジュールを複数設定できるのだ!(単発のスケジュール用)
$ sudo pmset schedule wake "01/27/13 16:59:00" $ sudo pmset schedule wake "01/28/13 5:59:00"
- pmset scheduleは単発のスケジュールなので、"MM/dd/yy HH:mm:ss"の形式("月/日/西暦2桁 時/分/秒")で日時指定する。
- すべてのスケジュールは、pmset -g sched コマンドで確認できる。
$ pmset -g sched
Repeating power events:
wake at 4:59PM Sunday
Scheduled power events:
[0] wake at 01/27/13 16:59:30
[1] wake at 01/28/13 05:59:00
素晴らしい!pmset scheduleでスリープ解除の設定をすることにした。
曜日を日付に変換する
- pmset scheduleは、単発のスケジュール用である。
- そのため曜日指定ではなく、日時指定で設定を行う。
- だから、毎週日曜日の17:00からというlaunchdの情報を見て、それを日時に変換する必要がある。
- 次の日曜日が何月何日なのか?果たして求められるだろうか。やってみた。
$ date -v+sun -v16H -v59M -v0S "+%m/%d/%y %H:%M:%S"
01/27/13 16:59:00
- できた!と思ったのだが、上記コマンドでは"01/27/13の日曜の18:00"に実行しても"01/27/13 16:59:00"を返してしまう。(すでに16:59を過ぎているのに...。)
- 指定時間を経過している場合は、さらに1週間後の日付にする必要があるのだ。
- そこで、以下のような現在時刻まで考えた次の曜日を求める関数を作ってみた。
date_from_w_hhmm() { weekday=(sun mon tue wed thu fri sat sun) hh=`date -j -f "%H%M" $2 +%H` mm=`date -j -f "%H%M" $2 +%M` current_date=`date +%s` preset_date=`date -v+${weekday[$1]} -v${hh}H -v${mm}M -v0S +%s` n=`expr $current_date / $preset_date` date -v+${weekday[$1]} -v+${n}w -v${hh}H -v${mm}M -v0S -v-30S "+%m/%d/%y %H:%M:%S" }
- date_from_w_hhmm 曜日(0-6) 時間(hhmm) の形式で利用する。
- ディープスリープ解除に必要な時間を考えて、指定した時刻の30秒前を求めるようにしてみた。
$ date_from_w_hhmm 0 1700
01/27/13 16:59:30
これで、曜日と時刻からpmset scheduleに必要な日付情報を簡単に求められるようになった!
連続する曜日に対応する
- 番組を録音する場合、平日(月曜から金曜)の6時から録音したい、なんて欲求はたぶん出てくる。
- そんな場合、現状ではlaunchd.plistを5ファイル作るしかない...。
- 一方、cronを利用すると、そんな予約設定も素晴らしく簡潔に書ける。
0 6 * * 1-5 /Users/zari/Library/Scripts/rec_radiko.sh -t 3600 FMJ
- 0日 1月 2火 3水 4木 5金 6土 7日に対応する。
- 1-5=月曜から金曜
- 6-7=土曜から日曜
- 1,3,5=月曜, 水曜, 金曜
- cronの設定は簡潔であり、その意味を知っている人にとっては必要最小限の手間であらゆる設定ができるところが素晴らしい。
- いっそのこと、launchdをやめてcronで予約設定したくなる。
- 一方のlaunchdでは、月曜から金曜を設定する時は、個別に5ファイルに分けて設定するしかないのだろうか?
- それではあまりにも不便すぎる...。そう思って調べてみると、1つのlaunchd.plistにまとめて設定する方法があった!
- なるほど!StartCalendarIntervalの値にはarrary(配列)を指定することもできて、
- dictを配列で束ねて設定しておけば、1つのplistで複数の曜日の予約もできるのだ!
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>com.bebekoubou.rec_radikoru_月-金_6:00_60_J-WAVE</string> <key>ProgramArguments</key> <array> <string>/usr/bin/caffeinate</string> <string>/Users/bebe/Library/Scripts/radikoru/rec_radikoru.app/Contents/Resources/Scripts/rec_radiko4.sh</string> <string>-t</string> <string>3600</string> <string>FMJ</string> </array> <key>StartCalendarInterval</key> <array> <dict> <key>Hour</key> <integer>6</integer> <key>Minute</key> <integer>0</integer> <key>Weekday</key> <integer>1</integer> </dict> <dict> <key>Hour</key> <integer>6</integer> <key>Minute</key> <integer>0</integer> <key>Weekday</key> <integer>2</integer> </dict> <dict> <key>Hour</key> <integer>6</integer> <key>Minute</key> <integer>0</integer> <key>Weekday</key> <integer>3</integer> </dict> <dict> <key>Hour</key> <integer>6</integer> <key>Minute</key> <integer>0</integer> <key>Weekday</key> <integer>4</integer> </dict> <dict> <key>Hour</key> <integer>6</integer> <key>Minute</key> <integer>0</integer> <key>Weekday</key> <integer>5</integer> </dict> </array> </dict> </plist>
- ちなみに、曜日を複数指定しない場合のStartCalendarIntervalは、以下のようになっていた。
...中略... <key>StartCalendarInterval</key> <dict> <key>Hour</key> <integer>17</integer> <key>Minute</key> <integer>0</integer> <key>Weekday</key> <integer>0</integer> </dict> ...中略...
- 曜日の複数指定はLingonの簡易設定では無理なので、自分でxmlを編集する必要がある。
以上の知識を総動員すると、手間はかかるがlaunchdとpmsetによって、自在にタイマー予約が可能になるのである。
- あとは、いかにこの手間のかかる設定を、可能な限り簡潔に設定するスクリプトに仕上げるか、
- そこがコーディングする上で難しい所であり、と同時に創意工夫によって楽しめる所でもある。