💤

sleepwatcherを用いてmacOSのスリープ/復帰時に任意のスクリプトを実行する

2024/02/17に公開

Sleepwatcherとは

Macのスリープや復帰、無操作状態などを監視できるCLIツール(デーモン)。

https://formulae.brew.sh/formula/sleepwatcher

開発者の個人サイトに記載の説明によると、一部オプションはApple Siliconでは動作しないとのこと。

https://www.bernhard-baehr.de/

実際に本記事執筆時点(2024年02月)のMacBookPro(M1, 2021)において、 スリープと復帰の監視はできるが、無操作状態の監視ができない(※) ことまで確認済み。
※無操作状態のオプションは利用可能だが、後述の通り正常な挙動はしない。

さらに、開発者から詳細なDocsは提供されてない模様。
ただし、Homebrew経由でインストールすれば、 brew info で基本的な使い方を確認できるので助かる。

なお、公式Docsではないが、下記個人ブログに情報が集まっているので、こちらも参考になるかもしれない。
(2014年に投稿されたものにも関わらず、2022年時点でも著者からコメント欄で返信が来ているのすごい)

https://www.kodiakskorner.com/log/258

用語の定義

スリープ

スリープしたタイミングを指す。
Macの画面が暗くなったタイミング、がわかりやすい言い方かも。
(より詳細な定義は各自調べていただく形で……)

復帰

スリープが解除されたタイミングを指す。
例えば、Macの画面がオンになった時点でスリープが解除されたとみなされるため、ログインパスワード入力画面でwakeupスクリプトは走る。
(こちらも、より詳細な定義は各自調べていただく形で……)

無操作

キーボード、マウスを操作しない状態を指す。
(こちらも、より詳細な定義は(以下略))

基本的な使い方

まず、sleepwatcherをインストールする。
MacなのでHomebrew経由がわかりやすいかと。
(後述の通り、sleepwatcherの起動も手軽にできるのでHomebrewがオススメ)

# インストールする
$ brew install sleepwatcher

試してみる。

スリープ時にスクリプトを実行する

# 実行するスクリプトを記載する `~/sample.sh` ファイルを作成する
$ touch ~/sample.sh

# 今回は適当に "hello world at <実行日時>" と出力するスクリプトを用意
$ echo 'echo hello world at `date +%Y%m%dT%H:%M:%S`' >> ~/sample.sh

# 実行権限を付与する
$ chmod +x ~/sample.sh

# sleepwatcherを実行し、スリープ時にスクリプトが実行されるようにする
# `-s` がスリープ時に動作させるためのオプション
$ /opt/homebrew/opt/sleepwatcher/sbin/sleepwatcher -V -s ~/sample.sh

# Macをスリープさせると、下記の通り実行結果が出力される
# (もちろん、結果を確認するためにはスリープ解除する必要がある)
# 日時がスリープ時点のものになっていることから、スリープ時にスクリプトが実行されたことがわかる
hello world at 20240214T23:48:33
sleepwatcher: sleep: /Users/hogehoge/sample.sh: 0

# 再度スリープすると再度実行される
hello world at 20240214T23:49:07
sleepwatcher: sleep: /Users/hogehoge/sample.sh: 0

# sleepwatcherを停止する際は `ctrl + c` などで適宜対応

復帰時にスクリプトを実行する

基本的にスリープ時と同様。
差分はsleepwatcher起動時のオプションが、復帰時の場合は -w を用いる点。

# 復帰時にスクリプトが実行される設定でsleepwatcherを起動する
$ /opt/homebrew/opt/sleepwatcher/sbin/sleepwatcher -V -w ~/sample.sh

# Macをスリープさせ、復帰させると下記の通り実行結果が出力される
# 日時が復帰時点のものになっていることから、復帰時にスクリプトが実行されたことがわかる
hello world at 20240215T00:03:05
sleepwatcher: wakeup: /Users/hogehoge/sample.sh: 0

# 再度スリープ→復帰の流れを辿ると、再実行される
hello world at 20240215T00:03:40
sleepwatcher: wakeup: /Users/hogehoge/sample.sh: 0

無操作時にスクリプトを実行する

基本的にスリープ時や復帰時と同様。
差分はsleepwatcher起動時のオプションが、無操作時の場合は -i を用いる点と、待機時間(どれくらいの時間無操作状態が実行したらスクリプトを実行するか)を -t で設定する点。

# 30s間無操作状態が続いた場合にスクリプトが実行される設定でsleepwatcherを起動する
$ /opt/homebrew/opt/sleepwatcher/sbin/sleepwatcher -V -i ~/sample.sh -t 300

# 30s後に下記の通り実行結果が出力される
hello world at 20240215T00:12:02
sleepwatcher: idle: /Users/minonono/sample.sh: 0

無操作時の動作検証もヨシ👉
……と述べたいところだが、実は無操作時の監視挙動にはいくつか問題があるので実用性は薄いかもしれない。

  • 問題その1: 操作をしていても(キーボード入力やマウス入力をしていても)、sleepwatcherの起動から指定時間経過後にスクリプトが実行されてしまう。
    • 無操作状態の監視とは……?
  • 問題その2: スクリプトが一度しか実行されない。再実行するためにはsleepwatcherを再起動する必要がある。
    • -R オプションの追加で繰り返し実行ができる情報も見かけたが、結局実現方法わからず仕舞い……
    • -t 300 -R 3 などを試しても、sleepwatcherの起動から30秒後に一度スクリプト実行されるだけ……

実用的な使い方

sleepwatcherではスリープ/復帰の監視までは実現可能なことが把握できた。
(無操作状態の監視は実用性ないので諦め)

とはいえ、都度 /opt/homebrew/opt/sleepwatcher/sbin/sleepwatcher -V -s sample1.sh -w sample2.sh といった形で起動するのは辛いので、常時起動させる方法を記す。

Homebrewでインストールしたので、 brew services でsleepwatcherを管理できる。

# sleepwatcherを常時起動しておく
$ brew services start sleepwatcher

### 以下は補足 ###

# 起動状態の確認
$ brew services list
Name         Status  User     File
sleepwatcher started hogehoge ~/Library/LaunchAgents/homebrew.mxcl.sleepwatcher.plist

# 停止方法
$ brew services stop sleepwatcher

# sleepwatcherの使い方
$ brew info sleepwatcher

デフォルト状態では、各スクリプトの記載先は下記ファイルとなる。

  • ~/.sleep : スリープ時に実行するスクリプト
  • ~/.wakeup : 復帰時に実行するスクリプト
  • ~/.idle : 無操作時に実行するスクリプト)

そのため、それぞれのファイルを作成し、実行したいスクリプトを記載し、実行権限も忘れずに付与しておきたい。

応用編

とはいえ、デフォルトファイル以外を利用したい場面や、そもそもHomebrewでsleepwatcherの管理をしたくない場面もあると思うので、その対策も記す。

brew services list で表示された ~/Library/LaunchAgents/homebrew.mxcl.sleepwatcher.plist が大いに参考になる。
このファイルはHomebrew経由でsleepwatcherを起動した際に自動生成されるファイル。
(デーモンの起動条件や定義などを管理するためのProperty List File)

コメントを勝手に追記したが、中身はこちら。(おそらく環境差分はそこまでないはず……!)
~/Library/LaunchAgents/homebrew.mxcl.sleepwatcher.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>KeepAlive</key> <!-- プロセスが落ちた際に自動で再起動する -->
	<true/>
	<key>Label</key>
	<string>homebrew.mxcl.sleepwatcher</string>
	<key>LimitLoadToSessionType</key>
	<array>
		<string>Aqua</string>
		<string>Background</string>
		<string>LoginWindow</string>
		<string>StandardIO</string>
		<string>System</string>
	</array>
	<key>ProgramArguments</key>
	<array>
		<!-- 以下でsleepwatcherを起動している -->
		<string>/opt/homebrew/opt/sleepwatcher/sbin/sleepwatcher</string>
		<string>-V</string>
		<string>-s</string>
		<string>/Users/hogehoge/.sleep</string> <!-- ここでスリープ時に実行するファイルを指定している, ユーザ名は適宜読み替える -->
		<string>-w</string>
		<string>/Users/hogehoge/.wakeup</string> <!-- ここで復帰時に実行するファイルを指定している, ユーザ名は適宜読み替える -->
	</array>
	<key>RunAtLoad</key> <!-- システム起動時にデーモンを起動する -->
	<true/>
</dict>
</plist>

上記ファイル内にて指定されているスクリプトファイルのパスを変更すれば良さそうなことがわかった。
しかし、Homebrewによって自動作成されたplistを直接編集すると、 brew services restartstop した場合に編集内容が元通りになってしまう。
そのため、任意のファイルを指定する場合は自前のplistを作成して自前のデーモンを用意するのが良さそう。

ということで、下記のように自前のplistを作成する。
(ユーザレベルでの実行を想定しているので ~/Library/LaunchAgents/ ディレクトリに設置するが、例のごとく詳細は各自調べていただく形で……)

~/Library/LaunchAgents/local.sample.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>KeepAlive</key>
    <true/>
    <key>Label</key>
    <string>local.sample</string> <!-- 任意の名前, ファイル名と同じにする慣習がある模様? -->
    <key>LimitLoadToSessionType</key>
    <array>
        <string>Aqua</string>
        <string>Background</string>
        <string>LoginWindow</string>
        <string>StandardIO</string>
        <string>System</string>
    </array>
    <key>ProgramArguments</key>
    <array>
        <string>/opt/homebrew/opt/sleepwatcher/sbin/sleepwatcher</string> <!-- sleepwatcherのパスを適切なものに変更 -->
        <string>-V</string>
        <string>-s</string>
        <string>/Users/hogehoge/Documents/sample_s.sh</string> <!-- ユーザ名とスクリプトパスは任意 -->
        <string>-w</string>
        <string>/Users/hogehoge/Documents/sample_w.sh</string> <!-- ユーザ名とスクリプトパスは任意 -->
    </array>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

今回、スクリプトファイルの中身はわかりやすく下記のようにしておく。
(plistファイル内で指定した ~/Documents ディレクトリ配下に作成する)

~/Documents/sample_s.sh
# スリープ時に `hello sleeping world <スリープ日時>` をtxtファイルに書き込む処理
echo hello sleeping world at `date +%Y%m%dT%H:%M:%S` >> ~/Documents/sample.txt
~/Documents/sample_w.sh
# 復帰時に `hello wakeup world <スリープ日時>` をtxtファイルに書き込む処理
echo hello wakeup world at `date +%Y%m%dT%H:%M:%S` >> ~/Documents/sample.txt

実行権限を付与するのもお忘れなく。

$ chmod +x ~/Documents/sample_s.sh
$ chmod +x ~/Documents/sample_w.sh

最後に、作成したplistファイルをlaunchdに読み込ませて起動させれば準備完了。

$ launchctl load ~/Library/LaunchAgents/local.sample.plist

# デーモンを停止させたい場合
$ launchctl unload ~/Library/LaunchAgents/local.sample.plist

これにて、
Macをスリープさせると hello sleeping world at 20240217T15:01:35 が、
スリープから復帰すると hello wakeup world at 20240217T15:02:52 が、
~/Documents/sample.txt ファイルに追記される。

めでたい。

余談

本記事を書くに至った背景、つまりsleepwatcherの使い方を調べた経緯は、以下の記事内のことを実現したかったため。

https://qiita.com/c6tower/items/4a20020eaf1eccf3ffbb

夢が広がる。

Discussion