tmux の bash 内で dotnet プログラムを実行していますが、時々 0 以外のエラー コードで失敗します。systemd サービス ファイルを使用して、tmux 内で dotnet プログラムをプログラム的に起動しようとしています。
サービス ファイルは次のとおりです。
[Unit]
Description=dotnet application
[Service]
Type=forking
ExecStart=/home/alpine_sour/scripts/rofdl
Restart=always
User=root
[Install]
WantedBy=multi-user.target
rofdl シェル スクリプトは次のとおりです。
#!/bin/bash
/usr/bin/tmux kill-session -t "rof" 2> /dev/null || true
/usr/bin/tmux new -s "rof" -d "cd /home/alpine_sour/rofdl && dotnet run"
さて、サービスを開始すると、systemd はメイン PID を tmux サーバーとして選択します。これは、最初に実行されたコマンドだったためだと思います。したがって、tmux ウィンドウ内のプログラムが何らかのエラー コードで終了し、他にウィンドウがない場合、tmux サーバーは成功エラー コードで終了し、systemd は再起動しません。Restart=always に設定しても、tmux サーバーはプログラムが失敗し、他にウィンドウがない場合のみ再起動します。
Process: 24980 ExecStart=/home/alpine_sour/scripts/rofdl (code=exited, status=0/SUCCESS)
Main PID: 24984 (tmux: server)
├─24984 /usr/bin/tmux new -s rofdl -d cd /home/alpine_sour/rofdl && dotnet run -- start
├─24985 sh -c cd /home/alpine_sour/rofdl && dotnet run -- start
├─24987 dotnet run -- start
└─25026 dotnet exec /home/alpine_sour/rofdl/bin/Debug/netcoreapp2.1/rofdl.dll start
そこで、systemd に、上位レベルの tmux サーバーではなく、プロセス フォークの最下位レベルを追跡させるにはどうしたらよいかと考えています。systemd に、サーバー自体ではなく tmux サーバーの子プロセスを追跡させ、それに応じて再起動させる方法が必要です。
答え1
予備的注釈
- この回答は Debian 9 での実験に基づいています。
- あなたのサービスはシステム サービス (
/etc/systemd/system
) であると想定しています。 - 質問本文の最後のほうに投稿されたものは、抜粋から
systemctl status …
。cgroupsについては何も述べられていない。この回答はコントロールグループ関与しています。systemd
必要だと思いますので、関与しているに違いありません。 コマンド自体は、成功するまでループで実行される場合があります。
cd /home/alpine_sour/rofdl && while ! dotnet run; do :; done
しかし、解決策を望んでいるのは理解できます
systemd
。
問題点
まずお読みください仕組みtmux
どのプロセスが誰の子であるかを理解することは非常に役立ちます。
どのプロセスがサービスに属しているか
元のケースでは、cgroup のすべてのプロセスが終了した後、サービスは非アクティブ (該当する場合は再起動の準備ができている) と見なされます。
スクリプトは、tmux
古いtmux
サーバーではなく、古いセッションを終了しようとします。その後tmux new
( と同等tmux new-session
)、サーバーを起動するか、古いサーバーを使用します。
古いものを使用する場合、サーバーもコマンド (
dotnet …
) もスクリプトの子孫にはなりません。これらのプロセスは、サービスに関連付けられた cgroup に属しません。スクリプトが終了した後、systemd
サービスは非アクティブであるとみなされます。新しい
tmux
サーバーを起動すると、サーバーとコマンドはサービスに関連付けられた cgroup に割り当てられます。その後、コマンドは終了する可能性がありますが、サーバー内に他のセッション/ウィンドウ (後で作成) がある場合は、サーバーは残り、systemd
サービスをアクティブと見なします。
メイン プロセスが 1 つある場合、メイン プロセスが終了した後に cgroup 全体が強制終了されます。 では、Type=simple
メイン プロセスは で指定されたプロセスです。ExecStart=
では、メイン プロセスを指定するには、この方法で PID をType=forking
使用して渡す必要がありますPIDFile=
。また、サービスを停止すると、はサービスに属するすべてのプロセスを強制終了します。したがって、cgroup にはサービスに固有のプロセスのみを含めることが重要です。この場合、サーバーがサービス内から起動されている場合でも、サーバーsystemd
を除外する必要があります。tmux
cgroup 間でプロセスを移動するツールや方法があります。または、tmux
サービスに固有の別のサーバーを実行することもできます。
systemd
どの終了ステータスを使用するかを知る方法
Restart=on-failure
メイン プロセスの終了ステータスへの依存関係を設定します。どの終了ステータスを使用するかがわかるので、これType=forking
を使用することをお勧めします。PIDFile=
systemd
systemd
ただし、終了ステータスを取得できる場合とできない場合があります。
終了ステータスを取得する人
子プロセスが終了した後、親プロセスは終了ステータスを取得できます(ゾンビプロセス)。
サーバーが古いか新しいかに関係なく、コマンドは孤立しない限りtmux
子にはなりません。カーネルは親をPID 1に設定します(systemd
または他の) となり、新しい親が右になりますsystemd
。
に指定したコマンドにより、tmux new
サーバーtmux
はシェルを実行し、シェルは実行さdotnet
れて終了を待つか、サーバーを親として保持したまま にexec
s します。いずれの場合も、の親は ではありません。dotnet
tmux
dotnet
systemd
次のように孤立させることができますdotnet
: nohup dotnet … &
、その後、該当のシェルを終了させます。また、ユニット構成ファイルで使用する PID を保存してPIDFile=
、サービスがどのプロセスを監視するかを認識する必要があります。そうすれば、なんとか機能するかもしれません。
明確に言うと、私のテストでは、(cgroups を処理した後)終了ステータスを取得できる whonohup sleep 300 &
によって正常に採用されました。systemd
tmux
しかし、そもそも使用したいのであれば、コマンドはターミナルとやり取りすると思います。nohup
はここでは適切なツールではありません。プロセスをターミナルに接続したまま孤立させるのは難しいかもしれません。プロセスを孤立させたいのですが、その中のシェルをtmux
単純に終了させることはできません。そうすると、そのパネルが強制終了される (またはデッド状態のままになる) からです。
注記はType=forking
による採用に依存しますsystemd
。メインのサービス プロセスはフォークして終了することになっています。その後、systemd
その子を採用します。ただし、このようなデーモンはどの端末とも対話してはなりません。
tmux
別のアプローチは、サーバー内のシェルexec
にさせることですdotnet
。終了すると、tmux
サーバー (親) は終了ステータスを認識します。状況によっては、別のスクリプトからサーバーを照会して終了ステータスを取得できます。
または、 によってトリガーされたシェルはtmux new
ステータスをファイルに保存し、別のスクリプトで取得できるようにすることもできます。
実行するのは確かにExecStart=
の子であるため、これは「別のスクリプト」の最適な候補です。終了ステータスを取得できるまで待機し、それを独自の終了ステータスとして使用して取得する必要があります。この場合、サービスが である必要があることに注意してください。systemd
systemd
Type=simple
dotnet …
あるいは、の外側から始めてtmux
、reptyr
サーバーの内部からtmux
。この方法は最初から子dotnet
プロセスである可能性があり、その tty を盗もうとすると問題が発生する可能性があります。systemd
解決策と例
reptyr
にtmux
この例では、 のスクリプトを実行しますtty2
。スクリプトは を準備しtmux
、exec
に s しますdotnet
。最後に、 内のシェルが、tmux
現在 である の tty を盗もうとしますdotnet
。
サービス ファイル:
[Unit]
Description=dotnet application
[email protected]
[Service]
Type=simple
ExecStart=/home/alpine_sour/scripts/rofdl
Restart=on-failure
User=root
StandardInput=tty
TTYPath=/dev/tty2
TTYReset=yes
TTYVHangup=yes
[Install]
WantedBy=multi-user.target
/home/alpine_sour/scripts/rofdl
:
#!/bin/sh
tmux="/usr/bin/tmux"
"$tmux" kill-session -t "rof" 2> /dev/null
"$tmux" new-session -s "rof" -d "sleep 5; exec /usr/bin/reptyr $$" || exit 1
cd /home/alpine_sour/rofdl && exec dotnet run
ノート:
htop
の代わりにを使用したテストでは、dotnet run
競合状態 (htop
端末の設定が変更され、reptyr
干渉する可能性があるため、sleep 5
回避策としては不十分) とマウス サポートの問題が明らかになりました。tmux
サービスに関連付けられた cgroup からサーバーを削除することは可能です。おそらくこれを行う必要があるでしょう。/sys/fs/cgroup/systemd/
コード内の以下の部分を参照してください。
それなしtmux
?
上記のソリューションでは/dev/tty2
、とにかく を使用します。tmux
制御端末のみを提供する必要がある場合は、cd /home/alpine_sour/rofdl && exec dotnet run
なしreptyr
、 なしを検討してくださいtmux
。スクリプトがなくても、次のようになります。
ExecStart=/bin/sh -c 'cd /home/alpine_sour/rofdl && exec dotnet run' rofdl
これが一番簡単です。
別のtmux
サーバー
tmux
ユーザごとに複数のサーバを実行できます。-L
または-S
(を参照man 1 tmux
) を使用してソケットを指定し、それに従います。 この方法では、サービスは排他的なtmux
サーバを実行できます。 利点:
- サーバーとその中で実行されるすべてのものは、
tmux
デフォルトでサービスの cgroup に属します。 - このサービスは
tmux
、他の誰か (または何か) がセッションを失うリスクなしにサーバーを破壊できます。このサービスを監視または操作する場合を除き、他の誰もこのサーバーを使用しないでください。他の目的でこのサーバーを使用した場合、それはその人の問題です。
サーバーを自由に強制終了する機能により、tmux
で実行されているプロセスを孤立させることができますtmux
。次の例を検討してください。
サービス ファイル:
[Unit]
Description=dotnet application
[Service]
Type=forking
ExecStart=/home/alpine_sour/scripts/rofdl
Restart=on-failure
User=root
PIDFile=/var/run/rofdl.service.pid
[Install]
WantedBy=multi-user.target
/home/alpine_sour/scripts/rofdl
:
#!/bin/sh
tmux="/usr/bin/tmux"
service="rofdl.service"
"$tmux" -L "$service" kill-server 2> /dev/null
"$tmux" -L "$service" new-session -s "rof" -d '
trap "" HUP
ppid="$PPID"
echo "$$" > '" '/var/run/$service.pid' "'
cd /home/alpine_sour/rofdl && dotnet run
status="$?"
'" '$tmux' -L '$service' kill-server 2> /dev/null "'
while [ "$ppid" -eq "$(ps -o ppid= -p "$$")" ]; do sleep 2; done
exit "$status"
' || exit 1
説明:
メイン スクリプトは、排他
tmux
サーバー (存在する場合) を強制終了し、新たに起動します。サーバーが起動すると、スクリプトは終了します。cgroup 内に少なくとも 1 つのプロセス (該当するサーバー) が残っているため、サービスは残ります。サーバーは、"内部" スクリプトを処理するためのシェルを生成します。スクリプトは
'
afterで始まり-d
、'
beforeで終わります||
。すべて引用符で囲まれていますが、引用符は一重引用符から二重引用符に、またその逆に何度か変更されています。これは$tmux
、と$service
がメイン スクリプトを処理するシェルによって展開される必要があるためです。他の変数 (例$status
) は、 内の "内部" シェルで展開されてはいけませんtmux
。次のリソースが役立つかもしれません:パラメータ展開(変数展開)と引用符内の引用符。内部のシェルは信号
tmux
を無視する準備をしますHUP
。シェルは、サービスが期待する pidfile にその PID を登録します。
次に、 が実行され
dotnet
、終了ステータスが保存されます (厳密には、cd
が失敗した場合は の終了ステータスになりますcd
)。シェルは
tmux
サーバーを強制終了します。これは次のようにも実行できますkill "$PPID"
(これ) ですが、誰かがサーバーを強制終了し、別のプロセスがその PID を取得した場合、間違ったプロセスを強制終了することになります。 アドレス指定の方がtmux
安全です。 のおかげで、trap
シェルは存続します。次に、シェルは PPID が以前のものと異なるまでループします。は動的ではないため、
$ppid
との比較に頼ることはできません。現在の PPID を から取得します。$PPID
ps
これで、シェルは新しい親があることを認識し、 になるはずです
systemd
。これで初めて、systemd
はシェルから終了ステータスを取得できます。シェルは、dotnet
以前に取得した正確な終了ステータスで終了します。この方法では、が子ではなかったというsystemd
事実にもかかわらず、終了ステータスを取得します。dotnet
共通tmux
サーバーから終了ステータスを取得
元のアプローチでは、共通 (デフォルト)tmux
サーバーを使用し、 という名前のセッションのみを操作しますrof
。一般に、他のセッションが存在したり発生したりする可能性があるため、サービスがサーバー全体を強制終了することはありません。いくつかの側面があります。次のことを実行する必要があります。
- サーバーがサービス内から起動された場合でも、サーバーが
systemd
強制終了されるのを防ぎます。tmux
- プロセスがサービス内から開始されたのではなく、サービスから開始された場合でも、そのプロセスをサービスの一部と見なし
systemd
ます。dotnet
tmux
- 何らかの方法で終了ステータスを取得します
dotnet
。
サービス ファイル:
[Unit]
Description=dotnet application
[Service]
Type=simple
ExecStart=/home/alpine_sour/scripts/rofdl
Restart=on-failure
User=root
[Install]
WantedBy=multi-user.target
Type=simple
メイン スクリプトは終了ステータスを取得できる唯一の確実な子であるため、今はそうであることに注意してください。スクリプトは終了ステータスを見つけてdotnet …
、それを独自のものとして報告する必要があります。
/home/alpine_sour/scripts/rofdl
:
#!/bin/sh
tmux="/usr/bin/tmux"
service="rofdl.service"
slice="/sys/fs/cgroup/systemd/system.slice"
"$tmux" kill-session -t "rof" 2> /dev/null
( sh -c 'echo "$PPID"' > "$slice/tasks"
exec "$tmux" new-session -s "rof" -d "
'$tmux' set-option -t 'rof' remain-on-exit on "'
echo "$$" > '" '$slice/$service/tasks' "'
cd /home/alpine_sour/rofdl && dotnet run
exit "$?"
' || exit 1
)
pane="$("$tmux" display-message -p -t "rof" "#{pane_id}")"
while sleep 2; do
[ "$("$tmux" display-message -p -t "$pane" "#{pane_dead}")" -eq 0 ] || {
status="$("$tmux" display-message -p -t "$pane" "#{pane_dead_status}")"
status="${status:-255}"
exit "$status"
}
done
説明:
がサーバーを作成する場合
tmux new-session
(サーバーが存在しなかったため)、他の何かがサーバーの使用を開始し、その cgroup をまだ変更していないため、systemd
何らかの理由でサービスを強制終了することにした場合の競合状態を防ぐために、最初から別の cgroup にサーバーを配置する必要があります。tmux new-session
で実行しようとしましたが失敗しました。そのため、別のアプローチとして、( に書き込むことによって)cgexec
独自の cgroup を変更し、次にを に変更するサブシェルを使用します。/sys/fs/cgroup/systemd/system.slice/tasks
exec
tmux new-session
内部のシェルは、セッションのオプション
tmux
を有効にすることで起動しますremain-on-exit
。終了後もペインは残り、別のプロセス (この場合はメイン スクリプト) がサーバーから終了ステータスを取得できますtmux
。その間、メイン スクリプトは、他のシェルが実行されているペインの一意の ID を取得します。誰かがセッションに接続したり、新しいペインを作成して操作したりする場合でも、メイン スクリプトは適切なペインを見つけることができます。
内部のシェルは
tmux
、 に書き込むことによって、サービスに関連付けられた cgroup に PID を登録します/sys/fs/cgroup/systemd/system.slice/rofdl.service/tasks
。内部のシェル
tmux
は を実行しますdotnet …
。 がdotnet
終了すると、シェルは終了します。 から取得された終了ステータスは、dotnet
シェルによってサーバーに報告されますtmux
。のため
remain-on-exit on
、「内部」シェルが終了した後もペインはデッド状態のままになります。その間、メイン シェルはペインが消えるまでループします。次に、
tmux
関連する終了ステータスをサーバーに照会し、それを独自のものとして報告します。このようにして、systemd
から終了ステータスを取得しますdotnet
。
ノート:
また、引用符内の引用符。
の代わりに
dotnet run
を使用できますexec dotnet run
。最後の形式は便利です。 はdotnet
内部シェルを置き換え、プロセスが 2 つではなく 1 つになります。問題は、dotnet
が処理できないシグナルによって強制終了された場合です。#{pane_dead_status}
ペイン内のプロセスがシグナルによって強制終了された場合、 は空の文字列を報告します。 と の間にシェルを維持することでdotnet
、tmux
これを防ぐことができます。シェルは情報を変換します (を参照)。この質問) を実行し、数値を返します。一部のシェル (実装?) では、最後のコマンドが暗黙的に で実行されますが
exec
、これは望ましくありません。そのため、exit "$?"
after を使用しましたdotnet …
。しかし、シェル自体が強制終了されると、空の問題が
#{pane_dead_status}
再び発生します。最後の手段として、status="${status:-255}"
空の状態を255
(ただし、このような場合に最適な値かどうかはわかりません255
) に変換します。競合状態があります。メイン スクリプトが を照会するとき
tmux
、それが正しいペインではない可能性があります。の前後#{pane_id}
に誰かがセッションに接続してプレイした場合、間違ったペインが表示される可能性があります。時間枠は小さいですが、それでもこれは私が望んでいたほどエレガントではありません。tmux new-session
tmux display-message
のようにコンソールに
tmux new-session
出力できれば、問題はありません。を使用すると、セッション内で表示できます。 はサポートされていません。#{pane_id}
tmux display-message -p
-PF
-p
サーバーが強制終了された場合に備えて、何らかのロジックが必要になる場合があります
tmux
。
ファイル経由で終了ステータスを取得する
上記の例は変更可能なので、はremain-on-exit on
必要ありませ#{pane_id}
ん。(少なくとも説明した競合状態は回避されます)。
前の例のサービス ファイルは残ります。
/home/alpine_sour/scripts/rofdl
:
#!/bin/sh
tmux="/usr/bin/tmux"
service="rofdl.service"
slice="/sys/fs/cgroup/systemd/system.slice"
statf="/var/run/$service.status"
rm "$statf" 2>/dev/null
"$tmux" kill-session -t "rof" 2> /dev/null
( sh -c 'echo "$PPID"' > "$slice/tasks"
exec "$tmux" new-session -s "rof" -d '
echo "$$" > '" '$slice/$service/tasks' "'
cd /home/alpine_sour/rofdl && dotnet run
echo "$?" > '" '$statf.tmp'
mv '$statf.tmp' '$statf'
" || exit 1
)
while sleep 2; do
status="$(cat "$statf" 2>/dev/null)" && exit "$status"
done
メカニズムは非常に単純です。メイン シェルは古いステータス ファイル (存在する場合) を削除し、ファイルが再び現れるまでトリガーしてループします。準備ができたら、「内部」シェルは の終了ステータスをファイルにtmux
書き込みます。dotnet
ノート:
- 内部シェルが強制終了したらどうなるでしょうか? ファイルを作成できない場合はどうなるでしょうか? メイン スクリプトがループを終了できない状況に陥るのは比較的簡単です。
- 一時ファイルに書き込んでから名前を変更するのは良い方法です。 これを行うと
echo "$?" > "$statf"
、ファイルは空の状態で作成され、その後書き込まれます。これにより、メイン スクリプトがステータスとして空の文字列を読み取る状況が発生する可能性があります。一般的に、受信側は不完全なデータを取得する可能性があります。つまり、送信側が書き込み中であり、ファイルがまだ大きくなろうとしているときに EOF まで読み取ることになります。名前を変更すると、適切な内容の適切なファイルが即座に表示されます。
最後のメモ
- なしでは済まない場合は
tmux
、別のサーバーを使用したソリューションがtmux
最も堅牢なようです。 これがドキュメンテーションについて次のように述べています
Restart=
:この文脈では、クリーン終了とは、終了コード
0
、またはシグナルSIGHUP
、SIGINT
、 、 のいずれかを意味しますSIGTERM
。SIGPIPE
[…]シェル内の注記は
$?
単なる数字です。繰り返しますが:このリンクdotnet
シグナルによって が終了し、再起動が (非) クリーンな終了に依存する場合、systemd
が から直接終了コードを取得するソリューションは、 が中間シェルから終了ステータスを取得するdotnet
ソリューションとは異なる動作をする可能性があります。 を調べると役立つ場合があります。systemd
SuccessExitStatus=
答え2
RestartForceExitStatus=
サービスファイルで使用できるかもしれません
メイン サービス プロセスによって返されたときに、Restart= で構成された再起動設定に関係なく、サービスの自動再起動を強制する終了ステータス定義のリストを取得します。引数の形式は、RestartPreventExitStatus= に似ています。
https://www.freedesktop.org/software/systemd/man/systemd.service.html