Mac の launchd / launchctl から display notification で通知を飛ばす

## 背景
Macの標準機能の時報は時間を言ってくれる機能しかないが、音を消している時でも気づけるようにバナー通知を飛ばしたい。
ただそれだけなのにドはまりしたので困っている人のためになればと思い書いた。

## 概要
Mac は cron も使えるけど launchd を推奨。

launchd に登録するには plist を作成して launchctl コマンドを叩く。

通知を飛ばすには AppleScript で display notification を実行する。

AppleScript を動かすには osascript コマンドを使う。

AppleScript から通知を飛ばすには スクリプトエディタ.app で一度 display notification を実行して、環境設定の通知で通知をオンにしなければならない。

launchd から osascript コマンドを実行するなら plist の ProgramArguments で osascript を直接書くか、別途作ったシェルスクリプトを書いて実行権限を付与しておけばいい。

シェルスクリプトを用意して `/bin/bash` の引数でシェルスクリプトを指定する場合、環境設定のプライバシーとセキュリティで `/bin/bash` にディスクアクセスの権限を付与する必要がある（対象のディレクトリでもいいし、フルディスクアクセスでもいいが対象のディレクトリは画面から指定する方法が見当たらない）。

※なお、`brew install terminal-notifier`でもOK。これなら権限とか気にせずサクッと動いた。今回はroot権限もらえてない会社パソコンでの作業なので osascript に固執した。

シェルスクリプトに実行権限を与えて直接起動（plist の ProgramArguments で `/path/to/hoge.sh` を指定）する方法ならディスクアクセスの権限は不要だが、下記手順では bash に引数でスクリプト名を渡す方法で書いておく。

## 手順
### シェルスクリプト作成
```:show.sh
#!/bin/bash
hour=`expr "$(date +%H)"`
time=$(date +%H:%M)

if [[ $((hour)) -gt 7 && $((hour)) -lt 22 ]] ; then

/usr/bin/osascript -e 'display notification "" with title  "'$time'" '

fi
```
実行権限付与（やらなくてもいいが）
```:sh
chmod u+x show.sh
```
この時点では動かしても通知は出ない。

### スクリプトエディタ.app の通知設定
スクリプトエディタ.appを開いてdisplay notification を実行する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12406/8bc0ab90-9a6a-44b7-9f3e-779597e9de7e.png)

▶を押して実行すると、環境設定の通知の画面に「スクリプトエディタ」が出てくるので通知を ON にする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12406/f9f87a61-f0d4-4bf1-867f-a18ec4cce442.png)

これでターミナルなどで
```
osascript -e 'display notification "Hello!"'
```
などとやると通知が出るようになる。

### plist作成

```: org.USER_NAME.hourlyAlerm.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>org.USER_NAME.hourlyAlerm</string>

    <!-- 実行するコマンドと引数 -->
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/Users/USER_NAME/Documents/scripts/hourlyAlerm/show.sh</string>
    </array>

    <key>StartCalendarInterval</key>
    <dict>
        <key>Minute</key>
        <integer>0</integer>
    </dict>

    <!-- 標準出力・標準エラーの出力先 -->
    <key>StandardOutPath</key>
    <string>/Users/USER_NAME/Library/Logs/org.USER_NAME.hourlyAlerm.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/USER_NAME/Library/Logs/org.USER_NAME.hourlyAlerm.err.log</string>
</dict>
</plist>
```
USER_NAME や hourlyAlerm はただのJOB名やPATHなのでお好きなものに書き換えて。

PATH は通っていない可能性大なので `launchctl setenv PATH 〜〜` で PATH を設定するかすべてフルパスで書く。

bash からシェルスクリプトを読み込めるようにするため、環境設定のプライバシーとセキュリティでフルディスクアクセスか対象のディレクトリへのアクセス権を与える

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12406/99e12966-acfc-4446-8cc7-890b60e32fef.png)


↓ Shift + Command + . で /bin などの隠しファイルも表示できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12406/1525a34b-7af5-4750-b464-e04508a2b557.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12406/a5147db9-5c63-4549-8380-7de60104baf8.png)

これで plist に 
```
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/path/to/hoge.sh</string>
</array>
```
ってしたときに bash が hoge.sh を読み込んで実行できるようになる。

なお、logの出力先を `/Users/USER_NAME/Documents/` などの下にすると`launchctl list` で見たときの Status が 78 になって動かない。bash にディスクフルアクセス与えてもダメだった。
フルディスクアクセスを付与する際に、自分で作った hoge.sh は選ぶことが出来なかった。

### launchctl で登録して動作確認
いまどきは load/unload ではなく bootstrap/bootout らしい。
```:sh
launchctl bootstrap gui/$(id -u) org.USER_NAME.hourlyAlerm.plist
```
ここで環境設定の「ログイン項目と機能拡張」に bash が登録されたと通知が出る。

これで対象の時間になったら通知が出るはず。

なんかおかしかったら `launchctl list` で出る Status が 0 以外の数字になっている。でもその数字が何を意味するのかググってもたどり着けなかった。

↓構文チェックは
```:sh
plutil -lint org.USER_NAME.hourlyAlerm.plist
```
↓これで launchd のログが見られるらしいが今回は役に立たなかった
```:sh
log stream --predicate 'subsystem == "com.apple.launchd"' --info
tail -f /var/log/system.log
launchctl print gui/$(id -u)/org.USER_NAME.hourlyAlerm
```
↓plist に書けば Debug 情報が出せるらしいが今回は役に立たなかった
```:plist
    <key>Debug</key>
    <true/>
```
plistを修正して反映させたいときは、一度 bootoutで登録解除してから再登録する。
```:sh
#↓まず登録解除して、
launchctl bootout gui/$(id -u) org.USER_NAME.hourlyAlerm.plist
#エディタで修正して、
vi org.USER_NAME.hourlyAlerm.plist
#↓再登録
launchctl bootstrap gui/$(id -u) org.USER_NAME.hourlyAlerm.plist
```
※bootout する前に plist 消しちゃうった場合
```:sh
launchctl remove gui/$(id -u)/org.USER_NAME.hourlyAlerm
#もしくは
launchctl bootout gui/$(id -u)/org.USER_NAME.hourlyAlerm
```

自分が使うときだけ動けばいいので ~/Library/LaunchAgents/ の中にいれる
```:sh
mkdir ~/Library/LaunchAgents/
cp org.USER_NAME.hourlyAlerm.plist ~/Library/LaunchAgents/
```
`launchctl bootstrap` するだけでは再起動後に登録解除されてしまっている。ここに入れておけば再起動後も読み込まれる。

用途によって保存場所が異なるので参考までに↓
| 保存場所 | 用途 |
|:-:|:-:|
|~/Library/LaunchAgents|ユーザー権限で動くユーザごとのエージェント|
|/Library/LaunchAgents|管理者権限で動くユーザごとのエージェント|
|/Library/LaunchDaemons|管理者権限で動くシステム全体的なデーモン|
| /System/Library/LaunchAgents | OSの一部として動くユーザごとのエージェント|
|  /System/Library/LaunchDaemons |  OSの一部として動くシステム全体的なデーモン |


ユーザー権限でない場合は `launchctl bootstrap` するときに sudo が必要で、オプションも `gui` ではなく `system` になる。
```:sh
sudo launchctl bootstrap system org.USER_NAME.hourlyAlerm.plist
```

あと、さっきまで動いていたのに突然動かなくなって困り果てていたら、22時過ぎて集中モードのおやすみモードに入って通知がオフになってただけだったという罠もあった。


:::note info
参考：
Apple公式資料　[Macの「ターミナル」でのlaunchdを使ったスクリプトの管理](https://support.apple.com/ja-jp/guide/terminal/apdc6c1077b-5d5d-4d35-9c19-60f2397b2369/mac)
これ以上の詳細は man page にしかないっぽい。man page にもStatus codeなどは記載されていない。
:::

おしまい。