非root (一般ユーザー)での systemd ユニットファイルの編集と登録、運用

systemd による  化は、非 root (一般ユーザー) でもできます。
改めて情報を整理しました。

## まず行う事

標準では対象ユーザーのログイン中のみプロセスが起動します。これをログインせずともプロセスが起動するようにします。

```shell
loginctl enable-linger $USER
```

## フォアグラウンドプロセスを対象にする場合のユニットファイル例

```ini:endless-loop.service.txt
[Unit]
Description=Endless loop (User)
After=default.target
Requires=default.target

[Service]
Type=simple
ExecStart=/bin/bash -c "while true ; do date ; sleep 3 ; done"
Restart=always

[Install]
WantedBy=default.target
```

インストール

```
cat endless-loop.service.txt | SYSTEMD_EDITOR=tee systemctl --user edit --full --force endless-loop.service
```

`edit` を使うと２つのメリットがあるのでオススメです。
1. ユニットファイルを環境に応じて自動作成・配置してくれる
    * どこに配置されたかは `systemctl cat` の出力に書いてあります
2. 編集終了後に `daemon-reload` を自動で行ってくれる

あとは OS 起動時に自動起動するようにします。

```shell:自動起動
systemctl --user enable endless-loop.service
```

他のスクリプトから起動する等、自動起動が不要であれば `disable` しておき、手動で `start` します。

```shell:手動で起動する
systemctl --user disable endless-loop.service
## 手動起動
systemctl --user start endless-loop.service
```

## After= や Requires= 等で参照できるtargetの一覧の取得方法

```shell
systemctl --user list-unit-files --type=target
```

```shell-session:Raspberry Pi OS (Desktop/Bookworm) での実行例
$ systemctl --user list-unit-files --type=target
UNIT FILE                    STATE  PRESET
basic.target                 static -
bluetooth.target             static -
default.target               static -
exit.target                  static -
graphical-session-pre.target static -
graphical-session.target     static -
paths.target                 static -
printer.target               static -
shutdown.target              static -
smartcard.target             static -
sockets.target               static -
sound.target                 static -
timers.target                static -
xdg-desktop-autostart.target static -
```

意味や効果は `man systemd.special` をご覧ください。
この中で使えそうなのはこのあたりでしょうか。

* 起動後: `default.target`
    * Web サーバー等の待ち受けに使える
* GUI ログイン後: `graphical-session.target`
    * キオスク端末化に使える

#### サンプル

※ ほぼ自分用ですが、お役に立てば。

FastAPI の立ち上げ (mise 利用)
```ini:fastapi-webapp.service
[Unit]
Description=FastAPI application with uvicorn

[Service]
Type=simple
WorkingDirectory=%h/fastapi_app1
ExecStart=%h/.local/bin/mise exec python -- uvicorn run:app --host 127.0.0.1 --port 8000
Restart=always
RestartSec=3
Environment=PYTHONUNBUFFERED=1
Environment=UVICORN_CMD_COLOR=false

[Install]
WantedBy=default.target
```

Chromium の KIOSK モード立ち上げ
```ini:chromium-kiosk.service
[Unit]
Description=Chromium Kiosk Mode on Wayland+Labwc (User)
After=graphical-session.target
Requires=graphical-session.target

[Service]
Type=simple
ExecStart=/usr/bin/chromium-browser --noerrdialogs --disable-infobars --no-first-run --enable-features=OverlayScrollbar --start-maximized --ozone-platform=wayland --disable-features=Translate --incognito --remote-debugging-port=9222 --kiosk "https://example.com"
Restart=always
Environment=XDG_RUNTIME_DIR=%t
Environment=WAYLAND_DISPLAY=wayland-0

[Install]
WantedBy=default.target
```

※ `After=` を `graphical-session.target` から `fastapi-webapp.service` に変えれば、FastAPI の起動後に chromium-browser が起動します

## ログ確認 (journalctl)

`journalctl` の `--user` や `-u` でログ表示が意図通りにできない場合は、他のオプションを使ってみてください。

```shell
journalctl _SYSTEMD_USER_UNIT=endless-loop.service
journalctl _UID=$(id -u)
```

以下のように補完が効く（ハズ）ので、忘れても安心かなと。

```shell-session
$ journalctl _<TAB>
_AUDIT_LOGINUID=              _GID=                         _SOURCE_MONOTONIC_TIMESTAMP=  _SYSTEMD_UNIT=
_AUDIT_SESSION=               _HOSTNAME=                    _SOURCE_REALTIME_TIMESTAMP=   _SYSTEMD_USER_SLICE=
_BOOT_ID=                     _KERNEL_DEVICE=               _STREAM_ID=                   _SYSTEMD_USER_UNIT=
_CAP_EFFECTIVE=               _KERNEL_SUBSYSTEM=            _SYSTEMD_CGROUP=              _TRANSPORT=
_CMDLINE=                     _MACHINE_ID=                  _SYSTEMD_INVOCATION_ID=       _UDEV_DEVNODE=
_COMM=                        _PID=                         _SYSTEMD_OWNER_UID=           _UDEV_SYSNAME=
_EXE=                         _RUNTIME_SCOPE=               _SYSTEMD_SLICE=               _UID=
$ journalctl _SYSTEMD_<TAB>
_SYSTEMD_CGROUP=         _SYSTEMD_OWNER_UID=      _SYSTEMD_UNIT=           _SYSTEMD_USER_UNIT=
_SYSTEMD_INVOCATION_ID=  _SYSTEMD_SLICE=          _SYSTEMD_USER_SLICE=
```

## 他サービスの起動まで待つ (PreExecStart= 利用)

他サービスの立ち上がりを待ちたい時に使えます。同じユーザー内であれば `After=` といった依存関係が使えますが、root 等の他ユーザーの参照はできませんし、外部サービスの場合は困ります。

`PreExecStart=` は 終了コードが 0 以外の時は繰り返し実行されるので、ヘルスチェックコマンドを入れられます。

`http://127.0.0.1:8000/api` の 200 番台返却をチェックする例です。
```ini
[Service]
ExecStartPre=/bin/bash -c 'for i in {1..30}; do curl --fail --max-time 3 http://127.0.0.1:8000/api && exit 0 || sleep 1; done; exit'
```

ping の例です。
```ini
[Service]
ExecStartPre=/bin/bash -c 'for i in {1..30}; do ping -c 1 HOST && exit 0 || sleep 1; done; exit 1'
```

内容が複雑になるならスクリプト化したり、 `Type=oneshot` のユニットを作成して、それを `After=` 等で参照するという手もあります。

## 参考資料

* [サーバー起動時に非rootユーザーでsystemdを使ってサービスを立ち上げる](https://qiita.com/k0kubun/items/3c94473506e0e370a227)
* [systemdのUnitファイルのパスを知らなくともUnitファイルを編集する方法](https://qiita.com/ma2shita/items/551b07c6f2bb6b8715a8)
* [OS起動時にsystemdで行われていること](https://qiita.com/ledmonster/items/5f2e1633d4124cb978fe)
* [systemd.unit — Unit configuration](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html)
* [systemdのunitファイル(service)でmiseを使う方法](https://qiita.com/ma2shita/items/b41d5645458ad4da5756)

EoT