シェルでコマンドを実行中は Mac をスリープさせないようにする方法
:warning: この記事は macOS 専用です。
:information_source: Linux でも、macOS の caffeinate
コマンドと同等のコマンドがあれば使えるかもしれません。もしご存じの方がいたらコメントで教えていただけるとありがたいです。
はじめに
シェルを操作しているときに、たまに時間がかかるコマンドを実行することがあると思います。
その際にコマンド終了まで放置していると Mac がスリープしてしまうことがあります。
もしかしたらシステムはスリープしていなくて、ディスプレイだけがスリープしているのかもしれませんが、ディスプレイがスリープしていると、コマンドが中断されてしまっているのではないかと不安になりますし、ときおりコマンドの進捗状況を確認したくてもスリープしていてわからないことがあります。
そこで、コマンド実行中は Mac がスリープしない (ディスプレイのスリープも含む) ようにする方法を紹介します。
環境
- macOS Big Sur 11.1
- Zsh 5.8
設定方法
以下を ~/.zshrc
に貼り付けます。
:warning: ~/.zshrc
の該当部分だけを抜き出して貼り付けているので、下記の ~/.zshrc
単体での動作は確認していません。もし正常に動作していなければコメントで教えてください。
_tn_cmd=''
need_caffeine=true
_tn_caffeinate_pid=''
preexec() {
_tn_cmd=$1
need_caffeine=true
while read line
do
if [[ $_tn_cmd =~ ^([[:blank:]]+.*)*$line([[:blank:]]+.*)*$ ]]; then
need_caffeine=false
fi
done < ~/.decaffeinated_command_list
if type caffeinate 1>/dev/null 2>/dev/null && "${need_caffeine}"; then
(caffeinate -d & echo $!) | read _tn_caffeinate_pid
fi
}
precmd() {
if "${need_caffeine}" && [[ "$_tn_caffeinate_pid" =~ ^[0-9]+$ ]]; then
kill "$_tn_caffeinate_pid"
fi
_tn_caffeinate_pid=''
}
次に、~/.decaffeinated_command_list
というファイルを作ります。
$ touch ~/.decaffeinated_command_list
そして、~/.decaffeinated_command_list
に以下を書き込みます。
exit
exec
source
script
とりあえず設定方法だけ知りたい方はこれで終わりです。
解説
ここから先は具体的な解説をします。
caffeinate
コマンド
Mac には caffeinate
というコマンドが用意されています。このコマンドを実行すると Mac をスリープさせないようにすることができます。
caffeinate
コマンドのオプション
オプション | 効果 | 備考 |
---|---|---|
-d |
ディスプレイをスリープさせないようにします | |
-i |
システムをアイドル状態にさせないようにします | |
-m |
ディスクをスリープさせないようにします | |
-s |
システムをスリープさせないようにします | MacBook の場合、電源に接続されているときのみ有効 |
今回はディスプレイをスリープさせたくないので、-d
を指定します。
preexec()
と precmd()
~.zshrc
に preexec()
という関数を用意すると、何かしらのコマンドの実行前に preexec()
の中身が実行されます。
また、precmd()
という関数を用意すると、コマンドの終了後 (厳密には新しいプロンプトが表示される直前) に precmd()
の中身が実行されます。
これを利用して、インタラクティブシェルでコマンド実行前に caffeinate
コマンドをバックグラウンドで実行しディスプレイのスリープを抑制し、コマンド終了後に caffeinate
コマンドのプロセスを kill します。
~/.decaffeinated_command_list
について
~/.decaffeinated_command_list
というファイルを作りましたが、これは caffeinate
コマンドを実行しないコマンドのリストです。
~/.decaffeinated_command_list
を再掲します。
exit
exec
source
script
たとえば、exit
コマンドがこのリストの中に入っているので、インタラクティブシェルで exit
コマンドを実行したときは caffeinate
コマンドは実行されないというわけです。
こうしておかないと caffeinate
コマンドのプロセスが残り続ける (ずっとスリープしなくなる) ことになります。
exit
コマンドでシェルを終了したり、exec
コマンドや source
コマンドでシェルや ~/.zshrc
を再読み込みしたりした場合、コマンド実行前に preexec()
の中身は実行されますが、precmd()
の中身は実行されません。
ということは、preexec()
が実行されて caffeinate
コマンドが実行されるのは良いものの、precmd()
が実行されないので caffeinate
コマンドのプロセスが残り続けてしまいます。こうすると不眠不休の Mac になってしまいます。
それを避けるために、preexec()
は実行されるが precmd()
が実行されないようなコマンドは ~/.decaffeinated_command_list
に入れておくことをおすすめします。
なお、上記の理屈により、ssh
で別のマシンにログイン中も Mac はスリープしません。SSH 先では caffeinate
コマンドが使えないので、SSH 接続中はずっとスリープしないようにしても良いと個人的には思っていますが、これが気に入らない場合は ssh
も ~/.decaffeinated_command_list
に追加してください。
他にも、man
や less
や vim
を開きっぱなしにして放置している間にスリープしてほしい場合は、それらを ~/.decaffeinated_command_list
に追加してください。
$!
について
(caffeinate -d & echo $!) | read _tn_caffeinate_pid
$!
にはバックグラウンドプロセスのうち、直前に実行されたプロセスのプロセス番号が入っています。
上記のスクリプトでは、caffeinate -d &
で caffeinate
コマンドがバックグラウンドで起動したので、$!
には caffeinate
コマンドのバックグラウンドプロセスのプロセス番号が入っています。
そのプロセス番号を _tn_caffeinate_pid
に代入しています。コマンド終了時に precmd()
でこのプロセス番号を指定して kill しています。
pkill caffeinate
ではダメな理由
プロセス番号を保持しなくても pkill caffeinate
を使えば caffeinate
コマンドのプロセスを kill することはできます。
しかしそれだと、別のシェルセッションで実行中の caffeinate
コマンドのプロセスも一緒に kill されてしまいます。
たとえばシェルセッションを 2 つ起動していて、片方で時間がかかるコマンドを実行させていて、もう片方で別の作業をしていたとします。
もし pkill caffeinate
を使っていた場合、作業している側のシェルで caffeinate
コマンドのプロセスをすべて kill してしまっているので、もう片方の、時間がかかるコマンドがまだ終了していなくてもスリープしてしまいます。
作業の合間に休憩しようとしてスリープしてしまうことになります。
それを防ぐために、ちゃんとプロセス番号を保持して、そのコマンドを実行したときの caffeinate
コマンドのプロセスだけを kill するようにします。
caffeinate
コマンドの起動と終了のチェック
おまけ: コマンド実行前に、本当に caffeinate
コマンドが実行されるのかどうか、そして終了後にちゃんとプロセスが kill されているのかが気になるかもしれません。
その場合は以下を ~/.zshrc
に追記します。
+ export PROMPT_STATE=""
+
+ caffeine_count() {
+ echo -e "\033[38;05;172m$(ps aux | grep caffeinate | grep -cv grep)\033[00m"
+ }
+
+ get_prompt_state() {
+ PROMPT_STATE="☕️ $(caffeine_count)"
+ }
+ get_prompt_state
_tn_cmd=''
need_caffeine=true
_tn_caffeinate_pid=''
preexec() {
_tn_cmd=$1
need_caffeine=true
while read line
do
if [[ $_tn_cmd =~ ^([[:blank:]]+.*)*$line([[:blank:]]+.*)*$ ]]; then
need_caffeine=false
fi
done < ~/.decaffeinated_command_list
if type caffeinate 1>/dev/null 2>/dev/null && "${need_caffeine}"; then
(caffeinate -d & echo $!) | read _tn_caffeinate_pid
fi
+
+ if "${need_caffeine}"; then
+ if ! { [ "$(ps aux | grep caffeinate | grep "$_tn_caffeinate_pid" | grep -cv grep)" -eq 1 ] && [[ "$_tn_caffeinate_pid" =~ ^[0-9]+$ ]]; } then
+ echo -e "\033[1;93mWARNING:\033[00m Running out of caffeine! Computer may sleep while executing \`\033[1m$_tn_cmd\033[00m' if it takes long time\033[00m"
+ echo
+ fi
+ fi
}
precmd() {
if "${need_caffeine}" && [[ "$_tn_caffeinate_pid" =~ ^[0-9]+$ ]]; then
kill "$_tn_caffeinate_pid"
fi
_tn_caffeinate_pid=''
+
+ get_prompt_state
}
preexec()
では、以下のいずれかの条件がそろっているときに、コマンド実行前に caffeinate
コマンドが実行されていないという警告を出します。
- 文字列
"caffeinate"
と、直前のバックグラウンドプロセスの番号で grep したときにcaffeinate
コマンドのプロセスが 1 件ではない -
caffeinate
コマンドのバックグラウンドプロセス番号を保持する$_tn_caffeinate_pid
の中身が数値ではない (空文字列などになっている)
また、precmd()
では get_prompt_state()
を呼び出していて、この関数で環境変数 PROMPT_STATE
をセットしています。
PROMPT_STATE
には caffeine_count()
の実行結果が入っていて、caffeine_count()
は caffeinate
コマンドのプロセスの数を返します。
あとはこれを環境変数 PROMPT
の中に入れておけば、常にプロンプトに caffeinate
コマンドのプロセスの数が表示されます。
ぼくは Starship を使っているので、環境変数 PROMPT
に入れる代わりに Starship の env_var
モジュールにセットしています。
[env_var]
variable = "PROMPT_STATE"
default = "???"
style = "bold yellow"
これで以下のように表示されます。
"☕️ 0" と表示されていますね。caffeinate
コマンドのプロセスがないことがわかります。ここの数字が 1 以上だった場合は、別のシェルセッションで何かしらのコマンドが実行されているので今はスリープしないということを表しています。
Bash や Linux では使えないのが弱点
Bash だと Bash-Preexec を使えば Zsh の preexec()
や precmd()
相当のことができるのですが、試してみたところ、残念ながら正しく機能しませんでした。
理由はわかりませんが、Bash-Preexec でバックグラウンドプロセスに回すような処理を書くと、インタラクティブシェルでパイプを使ったときにコマンドがストップしてしまいます。
[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh
preexec() {
# バックグラウンドで実行する処理を書く
caffeinate -d &
}
$ ps aux | grep caffeinate
# 処理がストップしてしまう
もしかしたらこれはバグかもしれないので、今後修正されるかもしれません。
Fish に関しては試していないのでわかりませんが、--on-event fish_preexec
と --on-event fish_postexec
が、それぞれ Zsh の preexec()
と precmd()
に似ているので代用できるかもしれません。
また、この caffeinate
コマンドは macOS (Darwin) 専用なので Linux では使えません。Linux にも caffeinate
コマンド相当のコマンドがあれば使えると思います。
Discussion