予備的注釈

予備的注釈

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れて終了を待つか、サーバーを親として保持したまま にexecs します。いずれの場合も、の親は ではありません。dotnettmuxdotnetsystemd

次のように孤立させることができますdotnet: nohup dotnet … &、その後、該当のシェルを終了させます。また、ユニット構成ファイルで使用する PID を保存してPIDFile=、サービスがどのプロセスを監視するかを認識する必要があります。そうすれば、なんとか機能するかもしれません。

明確に言うと、私のテストでは、(cgroups を処理した後)終了ステータスを取得できる whonohup sleep 300 &によって正常に採用されました。systemd

tmuxしかし、そもそも使用したいのであれば、コマンドはターミナルとやり取りすると思います。nohupはここでは適切なツールではありません。プロセスをターミナルに接続したまま孤立させるのは難しいかもしれません。プロセスを孤立させたいのですが、その中のシェルをtmux単純に終了させることはできません。そうすると、そのパネルが強制終了される (またはデッド状態のままになる) からです。

注記はType=forkingによる採用に依存しますsystemd。メインのサービス プロセスはフォークして終了することになっています。その後、systemdその子を採用します。ただし、このようなデーモンはどの端末とも対話してはなりません。

tmux別のアプローチは、サーバー内のシェルexecにさせることですdotnet。終了すると、tmuxサーバー (親) は終了ステータスを認識します。状況によっては、別のスクリプトからサーバーを照会して終了ステータスを取得できます。

または、 によってトリガーされたシェルはtmux newステータスをファイルに保存し、別のスクリプトで取得できるようにすることもできます。

実行するのは確かにExecStart=の子であるため、これは「別のスクリプト」の最適な候補です。終了ステータスを取得できるまで待機し、それを独自の終了ステータスとして使用して取得する必要があります。この場合、サービスが である必要があることに注意してください。systemdsystemdType=simple

dotnet …あるいは、の外側から始めてtmuxreptyrサーバーの内部からtmux。この方法は最初から子dotnetプロセスである可能性があり、その tty を盗もうとすると問題が発生する可能性があります。systemd


解決策と例

reptyrtmux

この例では、 のスクリプトを実行しますtty2。スクリプトは を準備しtmuxexecに 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

説明:

  1. メイン スクリプトは、排他tmuxサーバー (存在する場合) を強制終了し、新たに起動します。サーバーが起動すると、スクリプトは終了します。cgroup 内に少なくとも 1 つのプロセス (該当するサーバー) が残っているため、サービスは残ります。

  2. サーバーは、"内部" スクリプトを処理するためのシェルを生成します。スクリプトは'afterで始まり-d'beforeで終わります||。すべて引用符で囲まれていますが、引用符は一重引用符から二重引用符に、またその逆に何度か変更されています。これは$tmux、と$serviceがメイン スクリプトを処理するシェルによって展開される必要があるためです。他の変数 (例$status) は、 内の "内部" シェルで展開されてはいけませんtmux。次のリソースが役立つかもしれません:パラメータ展開(変数展開)と引用符内の引用符

  3. 内部のシェルは信号tmuxを無視する準備をしますHUP

  4. シェルは、サービスが期待する pidfile にその PID を登録します。

  5. 次に、 が実行されdotnet、終了ステータスが保存されます (厳密には、cdが失敗した場合は の終了ステータスになりますcd)。

  6. シェルはtmuxサーバーを強制終了します。これは次のようにも実行できますkill "$PPID"これ) ですが、誰かがサーバーを強制終了し、別のプロセスがその PID を取得した場合、間違ったプロセスを強制終了することになります。 アドレス指定の方がtmux安全です。 のおかげで、trapシェルは存続します。

  7. 次に、シェルは PPID が以前のものと異なるまでループします。は動的ではないため、$ppidとの比較に頼ることはできません。現在の PPID を から取得します。$PPIDps

  8. これで、シェルは新しい親があることを認識し、 になるはずですsystemd。これで初めて、systemdはシェルから終了ステータスを取得できます。シェルは、dotnet以前に取得した正確な終了ステータスで終了します。この方法では、が子ではなかったというsystemd事実にもかかわらず、終了ステータスを取得します。dotnet

共通tmuxサーバーから終了ステータスを取得

元のアプローチでは、共通 (デフォルト)tmuxサーバーを使用し、 という名前のセッションのみを操作しますrof。一般に、他のセッションが存在したり発生したりする可能性があるため、サービスがサーバー全体を強制終了することはありません。いくつかの側面があります。次のことを実行する必要があります。

  • サーバーがサービス内から起動された場合でも、サーバーがsystemd強制終了されるのを防ぎます。tmux
  • プロセスがサービス内から開始されたのではなく、サービスから開始された場合でも、そのプロセスをサービスの一部と見なしsystemdます。dotnettmux
  • 何らかの方法で終了ステータスを取得します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

説明:

  1. がサーバーを作成する場合tmux new-session(サーバーが存在しなかったため)、他の何かがサーバーの使用を開始し、その cgroup をまだ変更していないため、systemd何らかの理由でサービスを強制終了することにした場合の競合状態を防ぐために、最初から別の cgroup にサーバーを配置する必要があります。tmux new-sessionで実行しようとしましたが失敗しました。そのため、別のアプローチとして、( に書き込むことによって)cgexec独自の cgroup を変更し、次にを に変更するサブシェルを使用します。/sys/fs/cgroup/systemd/system.slice/tasksexectmux new-session

  2. 内部のシェルは、セッションのオプションtmuxを有効にすることで起動しますremain-on-exit。終了後もペインは残り、別のプロセス (この場合はメイン スクリプト) がサーバーから終了ステータスを取得できますtmux

  3. その間、メイン スクリプトは、他のシェルが実行されているペインの一意の ID を取得します。誰かがセッションに接続したり、新しいペインを作成して操作したりする場合でも、メイン スクリプトは適切なペインを見つけることができます。

  4. 内部のシェルはtmux、 に書き込むことによって、サービスに関連付けられた cgroup に PID を登録します/sys/fs/cgroup/systemd/system.slice/rofdl.service/tasks

  5. 内部のシェルtmuxは を実行しますdotnet …。 がdotnet終了すると、シェルは終了します。 から取得された終了ステータスは、dotnetシェルによってサーバーに報告されますtmux

  6. のためremain-on-exit on、「内部」シェルが終了した後もペインはデッド状態のままになります。

  7. その間、メイン シェルはペインが消えるまでループします。次に、tmux関連する終了ステータスをサーバーに照会し、それを独自のものとして報告します。このようにして、systemdから終了ステータスを取得しますdotnet

ノート:

  • また、引用符内の引用符

  • の代わりにdotnet runを使用できますexec dotnet run。最後の形式は便利です。 はdotnet内部シェルを置き換え、プロセスが 2 つではなく 1 つになります。問題は、dotnetが処理できないシグナルによって強制終了された場合です。#{pane_dead_status}ペイン内のプロセスがシグナルによって強制終了された場合、 は空の文字列を報告します。 と の間にシェルを維持することでdotnettmuxこれを防ぐことができます。シェルは情報を変換します (を参照)。この質問) を実行し、数値を返します。

    一部のシェル (実装?) では、最後のコマンドが暗黙的に で実行されますがexec、これは望ましくありません。そのため、exit "$?"after を使用しましたdotnet …

    しかし、シェル自体が強制終了されると、空の問題が#{pane_dead_status}再び発生します。最後の手段として、status="${status:-255}"空の状態を255(ただし、このような場合に最適な値かどうかはわかりません255) に変換します。

  • 競合状態があります。メイン スクリプトが を照会するときtmux、それが正しいペインではない可能性があります。の前後#{pane_id}に誰かがセッションに接続してプレイした場合、間違ったペインが表示される可能性があります。時間枠は小さいですが、それでもこれは私が望んでいたほどエレガントではありません。tmux new-sessiontmux 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、またはシグナルSIGHUPSIGINT、 、 のいずれかを意味しますSIGTERMSIGPIPE[…]

    シェル内の注記は$?単なる数字です。繰り返しますが:このリンクdotnetシグナルによって が終了し、再起動が (非) クリーンな終了に依存する場合、systemdが から直接終了コードを取得するソリューションは、 が中間シェルから終了ステータスを取得するdotnetソリューションとは異なる動作をする可能性があります。 を調べると役立つ場合があります。systemdSuccessExitStatus=

答え2

RestartForceExitStatus=サービスファイルで使用できるかもしれません

メイン サービス プロセスによって返されたときに、Restart= で構成された再起動設定に関係なく、サービスの自動再起動を強制する終了ステータス定義のリストを取得します。引数の形式は、RestartPreventExitStatus= に似ています。

https://www.freedesktop.org/software/systemd/man/systemd.service.html

関連情報