日の出・日の入りで発動するsystemd.target

# 目的
https://qiita.com/Yasuwo/items/7cb025fb5e0e2ead96e7
 
この記事で日の出・日の入りまで待って指定のコマンドを実行するスクリプトを作成したが、
他にも色々実行したいとなったときに、複数このスクリプトを走らせるのは無駄だし、
実行を一元管理するにも systemd を活用できないか？と思ったので取り組んだ。

# さっそく成果物
```~/.config/systemd/user/runat@.service
[Unit]
Description=wait until %I

[Service]
Type=oneshot
ExecStart=python .local/bin/runat.py --verbose %I
```

```~/.config/systemd/user/runat@.target
[Unit]
Description=run at %I
Requires=%N.service
After=%N.service
StopWhenUnneeded=yes
```

```~/.config/systemd/user/runat@.timer
[Unit]
Description=timer for %N.target

[Timer]
OnCalendar=00:00:00
Unit=%N.target

[Install]
WantedBy=timers.target
```

```~/.config/systemd/user/runat@sunset.timer.d/noon.conf
[Timer]
OnCalendar=
OnCalendar=12:00:00
```

日の入りで夕焼け小焼けを再生するサービスファイル例は下記の通り。

```~/.config/systemd/user/yuyakekoyake.service
[Unit]
Description=song yuyake-koyake
Requires=pipewire.service pipewire-pulse.service
After=runat@sunset.target

[Service]
Type=oneshot
ExecStart=play .local/share/sounds/yuyakekoyake.mp3

[Install]
WantedBy=runat@sunset.target
```

上記が準備できたら、下記コマンドで有効化する。

```shell
loginctl enable-linger
systemctl --user enable yuyakekoyake.service
systemctl --user enable runat@sunset.timer
systemctl --user start runat@sunset.timer
```

# ポイント

## ユニットの依存関係と順序関係

ユニットファイルに書いた内容を箇条書きにするとこんなかんじ。
- runat@.timer は timers.target に弱く必要とされている(WantedBy=)
- runat@.timer はタイマー条件を満たすと runat@.target を起動する(Unit=)
- runat@.target は runat@.service を必要としている(Requires=)
- runat@.target は runat@.service の後に起動する(After=)
- yuyakekoyake.service は runat@sunset.target に弱く必要とされている(WantedBy=)
- yuyakekoyake.service は runat@sunset.target の後に起動する(After=)

以上により下記のような流れになる。

1. runat@sunset.timer がタイマーとして作動する
1. runat@sunset.timer タイマーの条件が満たされると runat@sunset.target を起動しようとする
1. runat@sunset.target を起動するには先に runat@sunset.service を起動する必要があるので runat@sunset.service を実行する
1. Pythonスクリプトが実行され、日の入りまで待ち、runat@sunset.service の起動が完了する
1. runat@sunset.service の起動完了を受けて runat@sunset.target の起動が完了する
1. runat@sunset.target の起動完了を受けて yuyakekoyake.service が起動する

他の解説サイトでもたくさん言及されているが systemd において依存関係(Want/WantedBy/Requires/RequiredBy)と順序関係(Before/After)は完全に別管理なので、定義を忘れないように。。。


## StopWhenUnneeded
runat@.target に `StopWhenUnneeded=yes` という設定をしているが、
この設定がないと、一度 runat@.timer により active になった runat@.target は、そのままずっと active のままになってしまい、 runat@.timer から起動できなくなってしまう。
そこでこの設定により、日の出・日の入りの処理が終わり次第、 runat@.target を停止する。

cf. man 5 systemd.unit
> StopWhenUnneeded=
> Takes a boolean argument. If true, this unit will be stopped when it is no longer used. Note that, in order to minimize the work to be executed, systemd will not stop units by default unless they are conflicting with other units, or the user explicitly requested their shut down. If this option is set, a unit will be automatically cleaned up if no other active unit requires it. Defaults to false.
> (真偽値を取る。真ならばこのユニットはもう使われなくなったときに停止する。なお、実行負荷を最小化するためにsystemdの既定では他のユニットとコンフリクトするか、ユーザから明示的にシャットダウンが要求されない限りユニットを停止することはない。このオプションがセットされていると、他のアクティブなユニットから必要とされなくなったユニットは自動的にクリーンアップされる。デフォルトは偽である。)


## ターゲットユニットによるグルーピング
今回、yuyakekoyake.service は runat@.service ではなく runat@.target に依存関係を持たせた。
サービスユニットでも問題なかった気がするが、勉強のため。。

ちなみにターゲットユニットとは `man 5 systemd.target` によると
>DESCRIPTION
>
>A unit configuration file whose name ends in ".target" encodes information about a target unit of systemd, which is used for grouping units and as well-known synchronization points during start-up.
>(ユニットファイルのうちファイル名が ".target" で終わるものはsystemdのターゲットユニットに関する情報を格納し、複数ユニットのグルーピングや、起動時におけるよく知られた同期点を提供する。)
>This unit type has no specific options. See systemd.unit(5) for the common options of all unit configuration files. The common configuration items are configured in the generic [Unit] and [Install] sections. A separate [Target] section does not exist, since no target-specific options may be configured.
>(このユニットタイプは特別なオプションを持たない。すべてのユニット設定ファイルの共通オプションはsystemd.unit(5)を参照すること。共通の設定項目は一般の[Unit]および[Install]セクションで設定される。ターゲット固有のオプションは存在しないため、個別の[Target]セクションは存在しない。)
>Target units do not offer any additional functionality on top of the generic functionality provided by units. They exist merely to group units via dependencies (useful as boot targets), and to establish standardized names for synchronization points used in dependencies between units. Among other things, target units are a more flexible replacement for SysV runlevels in the classic SysV init system. (And for compatibility reasons special target units such as runlevel3.target exist which are used by the SysV runlevel compatibility code in systemd. See systemd.special(7) for details). 
>(ターゲットユニットは、「ユニット」として以外に何も追加の機能を提供しない。単に依存関係を通じたユニットのグルーピング(ブートターゲットとして使いやすい)や、ユニット間の依存関係における同期点の標準的な名前を提供するために存在する。とりわけ、ターゲットユニットは昔のSysV initシステムにおけるランレベルの置き換えとしてより使いやすい。(そして互換性のため systemdには runlevel3.target のようなSysVランレベル互換コードとして使われる特別なターゲットユニットが存在する。詳細はsystemd.special(7)を参照すること。)

とのことなので、まぁ分かりやすくするため、という目的で使ってよい感じがする。


## systemdのテンプレート機能
ユニットファイルに使われている @ は、systemd のテンプレート機能のもの。
たとえば yuyakekoyake.service では依存関係として runat@sunset.target を指定しているが、こういう書き方ができるのもテンプレート機能のおかげである。

テンプレート機能の動作は次の通り。

1. いったん runat@sunset.target が存在しないか確認する。もし存在すれば、そのユニットが直接使われる。ただし通常 @ はテンプレート機能のために使われるためまず存在しないだろう
1. @以降から拡張子までを取り除いたテンプレートユニットファイルが存在しないか確認する。runat@sunset.target の場合は runat@.target ファイルだ
1. テンプレートユニットが見つかったら、それがそのまま使われ、テンプレートユニットは runat@sunset.target として「インスタンス化」される。ただし、テンプレートユニット内で%iや%I指定子でインスタンス名を参照することができる

こうすることで、今回は runat@ ユニットしか定義していないにもかかわらず runat@sunset.target 以外にも runat@sunrise.target なども使用することができる。
ただし、その場合は個別に `systemctl --user enable runat@sunrise.timer` 等実行する必要があるので注意すること。


## ユニットファイルの指定子(specifier)機能
前節でも出てきたが、ユニットファイル内では様々な指定子(specifier)が利用できる。
指定子一覧は `man 5 systemd.unit` の `SPECIFIERS` セクションを参照のこと。


## loginctl enable-linger
これめっちゃ重要。
通常、ユーザ空間 systemd はユーザがログアウトしてしばらくすると(30秒くらい？)で終了してしまう。
つまり、ユーザタイマーも動作しない。(ついでに pipwire とかも)
そのため `loginctl enable-linger` でユーザログアウト後も(詳しく言うと、ブート直後からも)ユーザ空間のsystemdを実行させるようにしておく。


## ユニットファイルのオーバーライドによる起動時刻の変更
上記まででは出てきてないが、たとえば日の入りカウントダウンタイマーである runat@sunset.timer は深夜0時ではなく正午くらいに動き始めるのがちょうどよさそう。
ということで、ユニットファイルの一部だけを書き換えることがオーバーライド機能により可能だ。

オーバーライドはユニットファイル置き場に `<ユニット名>.d` というディレクトリを置き、その中に `.conf` 拡張子を持つファイルを配置し、そのファイルに上書きする内容を記述することで実施する。
しかも、実際にこの例の通り、テンプレートから生成されたインスタンスユニットに対してもオーバーライドできるのが良いところ。

なお、通常はオーバーライドファイルを直接作成するのではなく
`systemctl --user edit runat@sunset.timer` コマンドでエディタが開き、オーバーライドファイルを簡単に作成できる。
この場合、オーバーライドファイルは override.conf という名前になるようだ。