我有一個在 tmux 中的 bash 內部運行的 dotnet 程序,它偶爾會失敗並出現非零錯誤代碼。我正在嘗試使用 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 shell 腳本:
#!/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 …
。它沒有提及 cgroup。這個答案假設對照組都參與其中。我認為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
認為服務處於活動狀態。
如果只有一個主進程,則主進程退出後整個 cgroup 都會被殺死。主Type=simple
進程是由 指定的進程ExecStart=
。您Type=forking
需要使用PIDFile=
並透過這種方式傳遞 PID 來指定主進程。當您停止服務時,systemd
會殺死屬於該服務的所有進程。因此,僅在 cgroup 中包含特定於該服務的進程非常重要。在您的情況下,您可能想要排除tmux
伺服器,即使它是從服務內部啟動的。
有一些工具/方法可以在 cgroup 之間移動進程。或者您可以運行tmux
特定於該服務的單獨伺服器。
如何systemd
知道要使用哪種退出狀態
Restart=on-failure
設定對主進程退出狀態的依賴。建議Type=forking
使用它,PIDFile=
以便systemd
知道要使用什麼退出狀態。
systemd
但可能無法檢索退出狀態。
誰檢索退出狀態
子進程退出後,其父進程可以檢索退出狀態(比較殭屍行程)。
無論tmux
伺服器是舊的還是新的,您的命令都不會成為子命令,systemd
除非它成為孤兒,核心將其父級設定為 PID 1 (或其他一些)而新手父母是正確的systemd
。
您提供的命令tmux new
使tmux
伺服器運行 shell,然後 shell 要么運行dotnet
並等待其退出,要么exec
在將伺服器dotnet
保持tmux
為父級的情況下退出。無論如何,dotnet
都有一個不是的父母systemd
。
你可以dotnet
這樣孤立:nohup dotnet … &
,然後讓所謂的 shell 退出。您還需要儲存 PID,PIDFile=
在單元設定檔中使用,以便服務知道要監視哪個進程。那麼它可能會起作用。
需要明確的是:在我的測試中,nohup sleep 300 &
被成功採用,systemd
然後可以檢索其退出狀態(在我處理 cgroups 之後)。
但既然你想tmux
先使用,我猜你的指令會與終端機互動。所以nohup
在這裡不是正確的工具。在保持進程與終端連接的同時孤立進程可能會很棘手。您想要孤立它,但不能讓 shelltmux
簡單地退出,因為這會殺死它的窗格(或使其處於死亡狀態)。
注意Type=forking
依賴採用systemd
。主服務進程應該分叉並退出。然後systemd
收養牠的孩子。但此類守護程序不應與任何終端互動。
tmux
另一種方法是讓伺服器內的 shellexec
進行dotnet
.退出後,tmux
伺服器(作為父級)知道其退出狀態。在某些情況下,我們可以從另一個腳本查詢伺服器並檢索退出狀態。
或者由 觸發的 shelltmux new
可以將狀態儲存在檔案中,以便可以由另一個腳本擷取。
因為您運行的內容肯定ExecStart=
是 的子級,所以這是「另一個腳本」的最佳候選者。systemd
它應該等到它可以檢索退出狀態,然後將其用作自己的退出狀態,以便systemd
獲取它。請注意,服務應該是Type=simple
在這種情況下。
或者,您可以從dotnet …
之外開始tmux
,然後reptyr
從伺服器內部tmux
。這種方式dotnet
可以從一開始就是一個孩子systemd
,當你試圖竊取它的tty時可能會出現問題。
解決方案和範例
reptyr
到tmux
此範例運行tty2
.腳本準備好tmux
並exec
發送至dotnet
。最後,其中的一個 shelltmux
試圖竊取 tty 的 now 內容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
without reptyr
, without 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 中至少還剩下一個進程,即上述伺服器。伺服器產生一個 shell 來處理「內部」腳本。腳本從
'
after開始,在before-d
結束。全部都被引用了,但是引用從單引號變為雙引號,然後又返回了幾次。這是因為並且需要透過處理主腳本的 shell 進行擴展,其他變數(例如)必須在「內部」shell 內部進行擴展。以下資源可能會有所幫助:'
||
$tmux
$service
$status
tmux
參數擴展(變數擴展)和引號內的引號。內部的外殼
tmux
準備忽略HUP
訊號。shell 在服務期望的 pidfile 中註冊其 PID。
然後它運行
dotnet
並儲存其退出狀態(嚴格來說,如果cd
失敗那麼它將是 的退出狀態cd
)。shell 殺死
tmux
伺服器。我們kill "$PPID"
也可以這樣做(參見這),但是如果有人殺死了伺服器並且另一個進程獲得了它的 PID,我們就會殺死一個錯誤的進程。尋址tmux
更安全。因為trap
外殼得以倖存。然後 shell 會循環直到它的 PPID 與之前不同。我們不能依賴比較
$ppid
,$PPID
因為後者不是動態的;我們從 中檢索目前的 PPIDps
。現在 shell 知道它有一個新的父級,它應該是
systemd
.現在才systemd
能夠從 shell 檢索退出狀態。 shell 以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 中,以防止當其他東西開始使用該伺服器並且我們尚未更改其 cgroup 並systemd
決定出於任何原因終止該服務時出現競爭情況。我嘗試跑步tmux new-session
,cgexec
但失敗了;因此,另一種方法是:一個子 shell 更改自己的 cgroup(透過寫入/sys/fs/cgroup/systemd/system.slice/tasks
),然後exec
s 到tmux new-session
。內部的 shell
tmux
透過啟用remain-on-exit
會話選項來啟動。退出後,該窗格仍然存在,另一個進程(在我們的例子中是主腳本)可以從伺服器檢索其退出狀態tmux
。同時,主腳本會擷取另一個 shell 執行所在窗格的唯一 ID。
內部的 shell
tmux
透過將其 PID 寫入與服務關聯的 cgroup 中來註冊它的 PID/sys/fs/cgroup/systemd/system.slice/rofdl.service/tasks
。裡面的外殼在
tmux
運作dotnet …
。終止後dotnet
,shell 退出。檢索到的退出狀態dotnet
由 shell 回報給tmux
伺服器。由於
remain-on-exit on
,在「內」外殼退出後,窗格仍處於死亡狀態。同時,主 shell 會循環,直到窗格停止運作。然後它向
tmux
伺服器查詢相關的退出狀態並將其報告為自己的退出狀態。這種方式systemd
可以從 取得退出狀態dotnet
。
筆記:
再次有引號內的引號。
取而代之的
dotnet run
是exec dotnet run
。最後一種形式很好:dotnet
替換了內殼,因此只有一個進程而不是兩個。問題是當dotnet
被它無法處理的信號殺死時。事實證明#{pane_dead_status}
,如果窗格中的進程被訊號強行終止,則會報告空字串。在dotnet
和之間維護一個 shelltmux
可以防止這種情況:shell 轉換資訊(參見這個問題)並傳回一個數字。一些 shell(實作?)使用隱式運行最後一個命令
exec
,這是我們不想要的。這就是我使用exit "$?"
after 的原因dotnet …
。但如果強行殺掉shell本身,又會出現空的問題
#{pane_dead_status}
。作為最後的手段status="${status:-255}"
將空狀態轉換為255
(儘管我不確定255
在這種情況下是最佳值)。有一個競爭條件:當主腳本查詢 時
tmux
,#{pane_id}
它可能不是右窗格。如果有人在 之後tmux new-session
和 之前在會話中附加並進行遊戲tmux display-message
,我們可能會得到錯誤的窗格。時間窗口很小,但這仍然沒有我想要的那麼優雅。如果
tmux new-session
能像can那樣印#{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
這個機制非常簡單:主 shell 刪除舊的狀態檔案(如果有),觸發tmux
並循環,直到檔案重新出現。準備好後,「內部」shell 將退出狀態寫入dotnet
檔案。
筆記:
- 如果內殼被殺死怎麼辦?如果無法建立檔案怎麼辦?相對容易出現主腳本無法退出循環的情況。
- 寫入臨時檔案然後重命名是一個很好的做法。如果我們這樣做
echo "$?" > "$statf"
,該文件將被創建為空,然後寫入。這可能會導致主腳本讀取空字串作為狀態的情況。一般來說,接收方可能會得到不完整的資料:讀取直到 EOF,而發送方正在寫入中間並且檔案即將增長。重新命名可以立即顯示具有正確內容的正確檔案。
最後的筆記
答案2
RestartForceExitStatus=
也許你可以在服務文件中使用
取得退出狀態定義列表,當主服務程序傳回該列表時,將強制自動服務重新啟動,無論使用 Restart= 配置的重新啟動設定為何。參數格式類似 RestartPreventExitStatus=。
https://www.freedesktop.org/software/systemd/man/systemd.service.html