大学の講義でICT実践なるものがあり、スマートスピーカーを作ったのでそのプロセスを記録しておこうと思います。
systemdでの自動実行やOpenJTalkのインストールなどを行ったため、linux周りについての知識も身についたと思います。
本記事ではpythonコードの解説はほぼ行わず、
環境構築やライブラリのインストール、systemdの設定などをメインに扱っています。
pythonのコードについてはgithubのコードを見ればおおよそわかります。
GPIO_○○
の記述を全削除すれば、その処理過程や内容はかなりわかりやすくなるはずです。
主な目次
何をつくるの?
Geminiとfaster-whisperを使って、無課金のスマートスピーカーを作ります。
もともとChatGPTと通信したかったのですが、ChatGPT apiのクレジット購入ができませんでした。
そのためGeminiの無料枠を利用します。
ちなみに、ChatGPT apiの無料クレジット付与については2025年1月17日現在、すでに終了しています。
ChatGPT apiの使用を想定していたため、入力内容をDeepLで英訳してからAPIに送信したりする機能もあります。
(元々ChatGPTと通信する場合にトークン数をなるべく節約したかった)
そのため、ChatGPTと通信する処理については中途半端に終わっています。
(ちゃんと通信が出来ていることは確認したが、応答の読み上げやGPTに渡す会話履歴の機能などについては動作未確認)
スマートスピーカーとの会話はスプレッドシートに記録され、設定したメールアドレスに送信されます。
全体の処理内容はこんな感じ。
今回作成したプログラムはすべてgithubに公開されています。
GPTに聞いたらライセンスを書いておけと言われたのでひとまずMITライセンスにしておきます。
これをダウンロードして、お手元のRaspberryPi 5 で実行できるようにします。
参考元
使用機器
Raspberry Pi 5 8GBモデル
USBスピーカー: サンワサプライ MM-SPU218K
USBマイク: MillSO
実行環境
Raspberry Pi OS 64bit
python 3.9.21
pyenvを利用して、仮想環境上ですべてのプログラムを実行しています。
1.環境構築
pyenvのインストール、パス設定
brewの方が良いと聞きますが、自分の場合curlで行いました。
GPTにお願いして出てきたコマンドをそのまま実行してます。
curl https://pyenv.run | bash
ホームディレクトリ直下にある.bashrc
ファイルに以下の内容を末尾に追加し、pyenvのパスを通します。
# 他のいろんな記述...
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
.bashrc
の変更内容を反映
source ~/.bashrc
これでpyenvコマンドが使えるようになります。
以下、pythonのインストールです。
pyenv install --list
で利用可能なpythonのバージョンを確認できます。
今回はpython 3.9.21をインストールしていきます。
pyenv install 3.9.21
インストールが終わったら、今回使用する仮想環境を構築していきます。
RaspGPT
を仮想環境の名前とします。
pyenv virtualenv 3.9.21 RaspGPT
これで仮想環境を構築することができます。
次は仮想環境を有効にします。
pyenv activate RaspGPT
これで仮想環境に入れます。
2.必要なライブラリのインストール
sudo apt install 編
忙しい方向けに。まとめた記述はこれ
sudo apt install portaudio19-dev
sudo apt-get install open-jtalk
sudo apt-get install open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001
pyaudioのインストールに失敗しないようにインストール
sudo apt install portaudio19-dev
OpenJTalkのインストール
sudo apt-get install open-jtalk
sudo apt-get install open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001
英語ならこれもある。(今回は使わなかった。)
sudo apt install espeak-ng espeak-ng-data
espeak-ng "Hello, this is a test of the eSpeak-ng engine."
特にportaudio19-dev
は、古い方の名前(たしかportaudio-dev
だったか……?)がよく出てくるので、注意です。
他にも色々インストールはしましたが、そのへんはエラーが出たら適宜インストールをお願いします。
ChatGPTに投げたり、ググれば出てくるはずです。
OpenJTalkの準備
OpenJTalkとは、
先程のコマンドを実行すれば、
/usr/share/hts-voice/nitech-jp-atr503-m001/
にnitech_jp_atr503_m001.htsvoice
というファイルがダウンロードされます。
これが音声モデルです。
また、
/var/lib/mecab/dic/open-jtalk/
にnaist-jdic
という辞書がダウンロードされます。
デフォルト音声モデルでも良いのですが、せっかくなので他のモデルもダウンロードします。
調べたところ初音ミクモデルなんかも有志の方が作られているとか。
ですが、今回はいろんなところで挙げられている「MEI」というhtsファイルをダウンロードします。
ここからダウンロードできるので、ダウンロードしたら解凍して、
MMDAgent_Example-1.8/Voice/mei
にある.htsファイルを、
/usr/share/hts-voice/
以下に移動させます。
githubに上げたコードはデフォルトでMEIのボイスを設定してあるので注意してください。
pip install 編
これをreqirements.txtに保存します。
ctranslate2
faster-whisper
openai
gpiozero
requests
pyaudio
pyttsx3
google-generativeai
次のコマンドを実行します。
pip install -r reqirements.txt
これで必要なものはまとめてインストールできます。
覚えておくと便利です。
ディレクトリはこんな配置になっています。
3.Main.py実行のために必要な設定
ソースコードはgithubに上げているので、そこからダウンロードしてください。
以降はホームディレクトリ直下にクローンしたものと仮定して、すなわち
~/RaspGPT
の形でダウンロードされているという前提で説明を進めます
GPIOと回路周り
こんな感じの回路を想定しています。
faster-whisper-smallのダウンロード
cd ~/RaspGPT/modules
git clone https://huggingface.co/Systran/faster-whisper-small
git LFSの設定は別にいらないです。
ファイルサイズの関係上、「model.bin」をダウンロードできなかったりするので、
から手動でダウンロードし、modules/faster-whisper-small
のmodel.binに上書きします。
mediumモデルやlargeモデルも同じやりかたでダウンロードできます。
Main.pyにapiキーを設定。
自分で取得をお願いします。
Main.pyを開くとmain関数内冒頭のほうにAPIキーを格納するための変数があるのでそこに記述をお願いします。
GAS_URL
は後ほど。
main():
# ....
API_KEY_DEEPL = "hogehoge_DEEPL"
API_KEY_GPT = None
API_KEY_GEMINI = "hogehoge_GEMINI"
GAS_URL = "hogehoge_GAS_URL"
# ....
USBマイクの設定
modules/Record.py
では、録音を行う際にUSBデバイスのインデックスが必要。
そのために使用するマイクのデバイス名を調べます。
マイクを抜き差ししながら
lsusb
とするとマイクのデバイス名が大体わかります。自分の場合は
Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 003 Device 002: ID 1a81:1004 Holtek Semiconductor, Inc. Wireless Dongle 2.4 GHZ HT82D40REW
Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 002: ID 1b3f:5580 Generalplus Technology Inc. SF-558
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
こんな感じで表示されて、抜き差しの結果
Bus 001 Device 002: ID 1b3f:5580 Generalplus Technology Inc. SF-558
SF-558というのがマイクの名前だとわかりましたので、Main.py
のINPUT_DEVICE_INDEX
を
def main():
# ...
INPUT_DEVICE_INDEX = 'SF-558'
# ...
と書き換えます。
ハードウェア名がかぶってたりすると不具合を起こす可能性があるので注意をお願いします。
不具合が起きた場合pyaudio側で認識されているデバイス名をフルで入力すれば確実なので、
python modules/Record.py
としてデバイス名の一覧を取得し、コピペすればうまく行くはずです。(多分エラーが出ますが)
Google SpreadSheetの設定
Google SpreadSheetにアクセス。適当な名前のスプレッドシートを作り、
1行目、A列から順に Input | TranslatedInput | Response | DateTime
と記述します。
記述したら、 「拡張機能」>「AppsScript」
として、
以下のGASを記述します。
メールアドレスについては自分で記述をお願いします。
function doPost(e) {
//data = {"Input":"test", "TranslatedInput":"test", "Response":"test"}
const timestamp = new Date();
const mailAddress = "<任意のメールアドレス>";
const mailSubject = `RaspGPT@${timestamp}`;
try {
// 受信データをパース
Logger.log(e)
const data = JSON.parse(e.postData.contents);
// スプレッドシートを取得
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");
Logger.log(sheet)
// スプレッドシートのカラム名を取得(1行目がカラム名と仮定)
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
Logger.log(headers)
// データのキーを確認して不一致がある場合はエラーを返す
const dataKeys = Object.keys(data);
const invalidKeys = dataKeys.filter(key => !headers.includes(key));
if (invalidKeys.length > 0) {
throw new Error(`Invalid keys detected: ${invalidKeys.join(", ")}. Allowed keys are: ${headers.join(", ")}.`);
}
// データを追加する行を作成
const row = headers.map(header => (header in data ? data[header] : "")); // マッチするデータがあれば値を、それ以外は空白に
row[row.length-1] = timestamp; // 最後の列にタイムスタンプを追加
// 新しい行をスプレッドシートに追記
sheet.appendRow(row);
// JSONの内容を改行区切りで文字列に直す
const mailMassage = row.join("\n\n");
// メールを送信
eMailSend(mailAddress, mailSubject, mailMassage);
// 成功レスポンスを返す
return ContentService.createTextOutput(
JSON.stringify({ status: "success", message: "Data appended successfully." })
).setMimeType(ContentService.MimeType.JSON);
} catch (error) {
// エラーレスポンスを返す
Logger.log(error)
return ContentService.createTextOutput(
JSON.stringify({ status: "error", message: error.message+" __mail send error__ " })
).setMimeType(ContentService.MimeType.JSON);
}
}
function eMailSend(mailAddress, mailSubject, mailMassage) {
try {
// メール送信
GmailApp.sendEmail(mailAddress, mailSubject, mailMassage);
} catch (error) {
Logger.log(error)
// エラー時のレスポンスを返す
return ContentService.createTextOutput(
JSON.stringify({ status: "error", message: error.message })
).setMimeType(ContentService.MimeType.JSON);
}
}
記述したら、ウェブアプリケーションとしてデプロイします。
アクセスできるユーザーは「全員」 とします。
デプロイしたら、GASを実行するためのURLが表示されるので、それをコピー。
Main.pyの GAS_URL に貼り付けます。
これで、SpreadSheet.pyから先程記述したGASを実行し、スプレッドシートへの記録とメール送信を行うことができます。
一応、ここまでの設定を行えば
cd ~/RaspGPT
python Main.py
とするだけで実行はできるはずです。
4.systemdを使った自動実行とその他の実装
ここからはsystemdを使って機能を追加していきます。
RaspberryPi5では、/etc/rc.localを使った自動起動ができないと思われます。
また、今回は
- Main.pyがUSB接続デバイスを取り扱う
- pyenvの仮想環境からプログラムを自動で実行する
という2点があるため、一部bashファイルの記述やserviceファイルの記述内容にも注意が必要です。
これから、以下の機能の実装手順を解説していきます。
- プログラム(Main.py)の自動実行
- 特定のSSIDに対するwifi自動接続
- シャットダウンボタンの実装
共通手順
以下のコードを実行して、start_raspgpt.sh
, wifi_reconnect.sh
, button_shutdown.sh
すべてに実行権限を付与します。
これがないとsystemdからバッシュファイルを実行できません。
chmod +x *.sh
あとで使うので、自分のユーザーidを確認してください。
id
一番最初にでてくるuidという番号を使います。
もし登録しているユーザが複数なら、
logname
で自分のユーザー名を確認して、どれがidの出力結果から自分のidを特定してください。
以下、それぞれの機能について説明します。
1. プログラム(Main.py)の自動実行
start_raspgpt.shの編集
まず、start_raspgpt.shを開いて、***で囲ってある部分を編集します。
#!/bin/bash
# pyenv の環境変数とパスを設定
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
# pyenv を初期化
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
# スピーカーの音量調整
amixer sset Master 80%
# 指定の仮想環境を有効化
pyenv shell RaspGPT
# スクリプト実行ディレクトリに移動
cd /home/***自分のユーザー名***/RaspGPT
# Python スクリプトを実行
python Main.py
raspgpt.serviceの編集
_service
ディレクトリ以下にservice
ファイルが格納されているので、これを編集します。
cd ~/RaspGPT/_service
sudo nano raspgpt.service
この記述内容のうち、***で囲ってある箇所を編集してください。
[Unit]
Description=Raspgpt Auto Start Service
After=network.target
[Service]
Type=simple
User=***自分のユーザー名***
WorkingDirectory=/home/***自分のユーザー名***/RaspGPT
ExecStart=/home/***自分のユーザー名***/RaspGPT/start_raspgpt.sh
Environment="XDG_RUNTIME_DIR"=/run/user/***自分のユーザーid***
Restart=always
RestartSec=7
[Install]
WantedBy=multi-user.target
記述したら、Ctrl + S
で保存し、Ctrl + X
でエディタを閉じて、
sudo cp raspgpt.service /etc/systemd/system/raspgpt.service
sudo systemctl daemon-reload
sudo systemctl enable raspgpt.service
sudo systemctl start raspgpt.service
としてやれば、systemdがラズパイ起動時に毎回
/home/***自分のユーザー名***/RaspGPT/start_raspgpt.sh
を実行し、その結果Main.pyが実行されるようになります。
もしこれでMain.pyがうまく動かない場合は、
sudo journalctl -u raspgpt.service
としてやればサービスのログを確認できるのでここから原因を探りましょう。
2. と 3. のおおまかな流れはstart_raspgpt.service
と一緒なので、同じ箇所は説明を省きます。
2. 特定のSSIDに対するwifi自動接続
wifiが切れたりしても勝手に再接続を試みるバッシュファイル。
時々このバッシュファイルが動いていてもGUI側に接続するか否かのウィンドウが表示されることがある。
なんとなく、wifiのSSIDを変えると次回起動時に聞いてくる印象です。
実際に使う場合はそこに気をつければ大丈夫。
wifi_reconnect.shの編集
SSIDとネットワークインターフェースを記述
#!/bin/bash
# 再接続を試みるSSIDのリスト(必要に応じて追加・変更)
TARGET_SSIDS=("***SSIDを設定***")
# チェックするネットワークインターフェース名(例: wlan0)
WIFI_INTERFACE="***ネットワークインターフェースを設定***"
# 接続確認と再接続試行のループ
while true; do
# 現在の接続状態をチェック
if nmcli -t -f DEVICE,STATE device status | grep "^${WIFI_INTERFACE}:connected" > /dev/null; then
echo "Wi‑Fiは接続済みです。"
break
else
echo "Wi‑Fi接続がありません。再接続を試みます..."
# 各SSIDに対して再接続を試みる
for ssid in "${TARGET_SSIDS[@]}"; do
echo "SSID: $ssid への接続を試行中..."
nmcli device wifi connect "$ssid" ifname "$WIFI_INTERFACE"
# 接続に成功したらループを抜ける
if nmcli -t -f DEVICE,STATE device status | grep "^${WIFI_INTERFACE}:connected" > /dev/null; then
echo "$ssid に接続しました。"
break
fi
done
fi
# 一定間隔をおいて再チェック(例: 60秒)
sleep 3
done
wifi_reconnect.serviceの編集
cd ~/RaspGPT/_service
sudo nano wifi_reconnect.service
ユーザー名を記述
[Unit]
Description=Wi-Fi Reconnect Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart= /home/***ユーザー名***/RaspGPT/wifi_reconnect.sh
Restart=on-failure
User=***ユーザー名***
WorkingDirectory=/home/***ユーザー名***/RaspGPT
[Install]
WantedBy=multi-user.target
以下のコマンドを実行
sudo cp wifi_reconnect.service /etc/systemd/system/wifi_reconnect.service
sudo systemctl daemon-reload
sudo systemctl enable wifi_reconnect.service
sudo systemctl start wifi_reconnect.service
NetworkManagerのディスパッチャディレクトリを編集
ネットワークの状態が変化した時に自動で実行される。
以下のコマンドを実行。
sudo nano /etc/NetworkManager/dispatcher.d/99-restart-wifi-reconnect
次の内容を記述
#!/bin/bash
INTERFACE="$1"
STATUS="$2"
# 特定のWi-Fiインターフェースと切断イベントを検知
if [ "$INTERFACE" = "wlan0" ] && [ "$STATUS" = "down" ]; then
systemctl restart wifi_reconnect.service
fi
3. シャットダウンボタンの実装
RaspberryPi5にデフォルトで付いている電源ボタンを長押しした場合、正常終了しているのか怪しかったためシャットダウンボタンも一応実装。
ボタンが押されるとコマンド
sudo shutdown -h now
が実行される。
3番ピンと6番ピンをつないでシャットダウンボタンを作る記事もあったが、
RaspberryPi5でこの機能は排除されたと思われる。
button_shutdown.shの編集
ユーザー名を記述
#!/bin/bash
# pyenv の環境変数とパスを設定
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
# pyenv を初期化
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
# 指定の仮想環境を有効化
pyenv shell RaspGPT
# スクリプト実行ディレクトリに移動
cd /home/***自分のユーザー名***/RaspGPT
# Python スクリプトを実行
python button_shutdown.py
button_shutdown.serviceの編集
cd ~/RaspGPT/_service
sudo nano button_shutdown.service
ユーザー名を記述
[Unit]
Description=Button Triggered Shutdown
After=multi-user.target
[Service]
ExecStart=/home/***自分のユーザー名***/RaspGPT/button_shutdown.sh
Environment="XDG_RUNTIME_DIR"=/run/user/***ユーザーid***
Restart=always
RestartSec=7
User=***自分のユーザー名***
[Install]
WantedBy=multi-user.target
以下のコマンドを実行
sudo cp button_shutdown.service /etc/systemd/system/button_shutdown.service
sudo systemctl daemon-reload
sudo systemctl enable button_shutdown.service
sudo systemctl start button_shutdown.service
これで完成(のはず)
以上の手順を踏めば、RaspberryPi起動時にMain.pyが実行され、そのほか諸々の機能も正常に実行されるはずです。
今回目指したこと
今回作るスマートスピーカーは、このような特徴があります。
- ローカルで文字起こし
- DeepLと通信して翻訳
- Geminiと通信(何気にあんまり見かけない)
- 送られてきた内容を逐次読み上げる(レスポンスの高速化)
- スプレッドシートに内容を記録、メール送信
- GPIOピンからのボタン入力で処理を行うため、モニター不要
- シャットダウンボタンを実装することで安全に終了可能
- ラズパイ起動時に自動でプログラムが起動(プログラムが終了してもすぐに再度実行される)
- 指定したSSIDと接続できない場合、再接続を試みる
さっくり言うと
実用的なスマートスピーカーを目指して作りました。
かなり多機能になっているかと思われます。
尚の事、ChatGPT APIを使えなかったことが悔やまれます。
使い方メモ
自分でダウンロードしたhtsを使いたい
自分で追加したいモデルがあれば適宜.hts
ファイルをダウンロードして、
/usr/share/hts-voice/
に追加して、modules
ディレクトリ内のVoReading.py
のvoice_paths
を編集して、
# Set default paths
voice_paths = {
"DEFAULT": "/usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice",
"MEI_ANG": "/usr/share/hts-voice/mei_angry.htsvoice",
"MEI_BAS": "/usr/share/hts-voice/mei_angry.htsvoice",
"MEI_HAP": "/usr/share/hts-voice/mei_angry.htsvoice",
"MEI_NOR": "/usr/share/hts-voice/mei_angry.htsvoice",
"MEI_SAD": "/usr/share/hts-voice/mei_angry.htsvoice",
"新しく追加する音声の名前": "usr/share/hts-voice/追加したhtsのファイル名"
}
Main.py
の
#=============================
# OpenJTalkの設定
#=============================
# 話すスピード
SPEED = 1.3
# 声の種類
VOICE_TYPE = "新しく追加する音声の名前"
# 辞書のパス
DIC_PATH = None
VOICE_TYPE
を変更してください。これでモデルの変更が可能です。
おまけ
RaspberryPi 5で直接文字起こししたときの速度
faster-whisper
INT8量子化モデルを使用しています。
smallモデルではbatch_size=3
, beam_size=1
mediumモデルではbatch_size=1
, beam_size=1
デフォルトで設定しています。
使用するモデルについては、
WHISPER_MODEL
を変更することで変えられます。
使ってみた感じ
faster-whisper-small
普通にスマートスピーカーとして使うならこちらが推奨。
短い音声(4〜8秒)ならほぼ同じ秒数で処理可能。(9秒前後のことが多い)
長い音声(48秒とか)ならそれより短い時間(28秒前後)で処理可能。
beam_size
が大きいと逆に精度が落ちることもあり、なんともいえない。
デフォルトでは 1 に設定して、応答速度を上げている。
batch_size
は何度か試した結果、3がベストだった。
ただし精度に不安あり。
そのため、Main.pyではinitial_prompt
を設定することで文字起こしの文脈が通りやすくなるよう調整している。
また、
- Geminiの最新の応答内容を文ごとに分割し、ユーザが設定した数だけランダムにサンプリングして文章を記録
- ユーザが最初に設定した
WHISPER_MODEL_CONTEXT_DEFAULT
に先程サンプリングした文章を加えて、それをinitial_prompt
に渡す
という処理を行っている。
これにより、会話の文脈にあった文字起こしをしやすくなるよう調整している。
が、それでもおかしな出力が返ってくることがある。
faster-whisper-medium
精度はsmallモデルよりも明らかに高い。
安定度が違う。
smallとmediumの間に超えられない壁があるんじゃないかと思うくらい差が歴然。
しかし、文字起こしにかかる時間が長い。
短い音声(4〜8秒)でも20秒以上かかることがザラ。
長い音声(48秒)なら70秒程度。
batch_size
や beam_size
も調整したが無理。
応答が遅くてもいいやって人はこっちを使うのがいいかも。
結論
- faster-whisper-smallがスマートスピーカーとして向いている
- faster-whisper-mediumは応答が遅くても良い、正確性を重視したい場合に向いている
modules以下のpythonファイルからいじくれるので、興味がある方は試してみてください。
systemdのENVIRONMENT="XDG_RUNTIME_DIR"="/run/user/***自分のユーザーid***"
とは何?
systemdは、システム環境でserviceファイル内で指定した内容を実行します。
この際ユーザーは指定されておらず、USBデバイスへのアクセスなどができません。
そのため、
Environment="XDG_RUNTIME_DIR"=/run/user/***自分のユーザーid***
としてXDG_RUNTIME_DIR
に/run/user/***自分のユーザーid***
を指定してやることでserviceファイルの内容をどのユーザーとして実行するのかを指定しています。
エラーの原因がわからず調べていたところ、他の方がこの記述でうまくいったと報告していたので私も試してみたところうまくいきました。
よくわからなかったのでXDG_RUNTIME_DIRについてドキュメントを調べたところ、こんな内容が書かれていました。
以下ChatGPTの翻訳
pam_systemd
のDescription部分の翻訳
pam_systemd
は、ユーザーのセッションをsystemd
のログインマネージャであるsystemd-logind.service(8)
に登録し、それによってsystemd
のコントロールグループ階層に統合します。このモジュールは、ユーザーの JSON ユーザーレコード(定義されている場合)に基づき、セッションに対してさまざまなリソース管理やランタイムパラメータを適用します。
ログイン時の動作
pam_systemd
はsystemd-logind.service
と連携して以下を実行します:
- ユーザーのランタイムディレクトリの作成:
/run/user/$UID
がまだ存在しない場合、新しいtmpfs
ファイルシステムとしてディレクトリを作成またはマウントし、クォータを適用します。その所有権はログインしたユーザーに変更されます。
- 環境変数
$XDG_SESSION_ID
の初期化:
- 監査機能が利用可能で、
pam_loginuid.so
がこのモジュールの前に実行されている場合、監査セッションID(/proc/self/sessionid
)から初期化されます。そうでない場合、独立したセッションカウンタが使用されます。
- 新しいシステムスコープユニットの作成:
- セッション用に新しい
systemd
スコープユニットが作成されます。- これがユーザーの最初の同時セッションである場合、自動的にユーザー専用のスライスユニットが
user.slice
配下に作成され、その中にスコープが配置されます。- また、
systemd
のユーザーマネージャインスタンスを実行するuser@.service
のインスタンスが開始されます。
- ユーザー環境変数の設定:
- ユーザーの JSON レコードに基づき、
$TZ
(タイムゾーン)、$LANG
(ロケール)の環境変数を設定します。- また、ユーザーレコードで明示的に設定された環境変数をインポートし、
umask
、nice
レベル、リソース制限を初期化します。
ログアウト時の動作
- プロセスの終了:
logind.conf(5)
の設定でKillUserProcesses=
が有効になっている場合、セッション内のすべてのプロセスが終了します。- ユーザーの最後の同時セッションが終了した場合、
systemd
のユーザーインスタンスとユーザーのスライスユニットも終了します。
- ランタイムディレクトリの削除:
- ユーザーの最後の同時セッションが終了すると、
/run/user/$UID
とその内容も削除されます。
例外
- システムが
systemd
を初期化システムとして起動していない場合、このモジュールは何もせず、すぐにPAM_SUCCESS
を返します。
この仕組みによって、ユーザーごとのセッション管理、プロセスのクリーンアップ、リソースの分離が適切に行われ、システムの安定性とセキュリティが向上します。
ドキュメントのEnvironment $XDG_RUNTIME_DIR
について
ユーザープライベートで書き込み可能なディレクトリへのパスであり、マシン上でユーザーがログインしている間に関連付けられるものです。このディレクトリは、ユーザーが初めてログインしたときに自動的に作成され、ユーザーが最後にログアウトしたときに削除されます。
ユーザーが同時に2回ログインした場合、両方のセッションで同じ
$XDG_RUNTIME_DIR
とその内容が共有されます。一方、ユーザーが1回ログインし、その後ログアウトし、再びログインした場合、このディレクトリの内容はその間に消失します。ただし、アプリケーションはこの動作に依存せず、古いファイル(スタイルファイル)を扱えるようにする必要があります。このディレクトリにセッションごとのプライベートデータを保存するには、ファイル名に
$XDG_SESSION_ID
の値を含めるべきです。このディレクトリは、AF_UNIXソケット、FIFO、PIDファイルなど、ランタイムファイルシステムオブジェクトに使用されるべきです。また、このディレクトリはローカルであることが保証され、オペレーティングシステムが提供する可能な限りのファイルシステム機能セットを提供します。詳細については、XDG Base Directory Specification を参照してください。なお、現在のユーザーがセッションの元のユーザーでない場合、
$XDG_RUNTIME_DIR
は設定されません。
わからなかったのでさらに調べる
このサイトの $XDG_RUNTIME_DIR
についての記述をChatGPTに翻訳させる。
$XDG_RUNTIME_DIR
関連の記述について要約
$XDG_RUNTIME_DIR
は、ユーザー固有の一時的なランタイムファイルやその他のファイルオブジェクト(例: ソケット、名前付きパイプなど)を保存するためのベースディレクトリを定義する環境変数です。主なポイント:
所有権とアクセス権: このディレクトリはユーザー自身が所有し、ユーザーのみが読み書きアクセスできる必要があります。Unix のアクセスモードは
0700
に設定されるべきです。ライフサイクル: ディレクトリの寿命はユーザーのログインセッションに関連付けられています。ユーザーが最初にログインしたときに作成され、完全にログアウトしたときに削除される必要があります。
一貫性: ユーザーが複数回ログインした場合、同じディレクトリが指し示され、最初のログインから最後のログアウトまで存在し続ける必要があります。
持続性: このディレクトリ内のファイルは、システムの再起動や完全なログアウト/ログインサイクルを超えて持続しない必要があります。
ローカルファイルシステム: ディレクトリはローカルファイルシステム上にあり、他のシステムと共有されない必要があります。また、オペレーティングシステムの標準に従った完全な機能を備えている必要があります。
これらの要件により、
$XDG_RUNTIME_DIR
はユーザーのランタイムデータの安全で一時的な保存場所として機能します。
「/run/user/$UID
がまだ存在しない場合、新しい tmpfs
ファイルシステムとしてディレクトリを作成またはマウントし、クォータを適用します。その所有権はログインしたユーザーに変更されます。」
「なお、現在のユーザーがセッションの元のユーザーでない場合、$XDG_RUNTIME_DIR
は設定されません。」
こうした記述から、systemd側でログインユーザーを設定するためにXDG_RUNTIME_DIR
を参照していると解釈しています。
ちなみに、systemdのserviceファイル内では
Environment=TEST_VALUE="hogehoge"
と記述することで変数を代入できたりするとか。
詳しく知りたい方はこちらを参考にどうぞ
systemdのパラメータ(INSTALL, UNITなど)についてはこちらが参考になる