我有一个在 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 无法重新启动。即使我要重新启动=总是,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 服务器的子进程而不是服务器本身并相应地重新启动。
初步说明
/etc/systemd/system
)。systemctl status …
。它没有提到 cgroups。此答案假定涉及控制组。我认为systemd
需要它们,所以它们必须是。命令本身可能会循环运行,直到成功:
但我知道你想要一个
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=
在单元配置文件中使用,以便服务知道要监视哪个进程。那么它可能有点工作。需要明确的是:在我的测试中,谁可以检索它的退出状态(在我处理了 cgroups 之后)
nohup sleep 300 &
成功地采用了它。systemd
但是既然您想首先使用
tmux
,我想您的命令会与终端交互。所以nohup
这里不是正确的工具。在保持进程连接到终端的同时孤立进程可能很棘手。你想孤立它,但你不能让其中的 shelltmux
简单地退出,因为这会杀死它的窗格(或使其处于死状态)。注意
Type=forking
依赖于systemd
. 主要服务进程应该分叉并退出。然后systemd
收养它的孩子。这样的守护进程不应该与任何终端交互。另一种方法是让
tmux
服务器内部的 shellexec
到dotnet
. 退出后,tmux
服务器(作为父级)知道其退出状态。在某些情况下,我们可以从另一个脚本查询服务器并检索退出状态。或者由 触发的 shell
tmux new
可能会将状态存储在一个文件中,因此它可以被另一个脚本检索。因为你运行的
ExecStart=
是肯定的孩子systemd
,所以这是“另一个脚本”的最佳候选者。它应该等到它可以检索退出状态,然后将其用作自己的退出状态,所以systemd
得到它。注意服务应该Type=simple
在这种情况下。或者,您可以从
dotnet …
外部开始tmux
,然后reptyr
从tmux
服务器内部开始。这种方式dotnet
可以systemd
从一开始就是一个孩子,当你试图窃取它的 tty 时可能会出现问题。解决方案和示例
reptyr
至tmux
此示例在
tty2
. 脚本准备stmux
到. 最后,其中的一个 shell试图窃取现在的 tty 。exec
dotnet
tmux
dotnet
服务文件:
/home/alpine_sour/scripts/rofdl
:笔记:
htop
而不是的测试dotnet run
揭示了竞争条件(htop
更改其终端的设置,reptyr
可能会干扰;因此sleep 5
是一个糟糕的解决方法)和鼠标支持问题。tmux
服务器。您可能想要这样做。请参阅下面/sys/fs/cgroup/systemd/
的方式,代码中有。没有
tmux
?/dev/tty2
无论如何使用上述解决方案。如果你tmux
只需要提供一个控制终端,可以考虑cd /home/alpine_sour/rofdl && exec dotnet run
withoutreptyr
,withouttmux
。即使没有脚本:这是最简单的。
独立
tmux
服务器tmux
允许您为每个用户运行多个服务器。您需要-L
或-S
(参见 参考资料man 1 tmux
)指定一个套接字,然后坚持使用它。这样你的服务就可以运行一个独占tmux
服务器。优点:tmux
属于该服务的 cgroup。tmux
服务器,而不会有任何人(或任何东西)失去会话的风险。没有其他人应该使用此服务器,除非他们想要监视/与服务交互。如果有人将它用于其他用途,那是他们的问题。自由终止服务器的能力
tmux
允许您孤立在tmux
. 考虑以下示例。服务文件:
/home/alpine_sour/scripts/rofdl
:解释:
主脚本杀死独占
tmux
服务器(如果有的话)并重新启动它。服务器启动后,脚本退出。该服务仍然存在,因为 cgroup 中至少还剩下一个进程,即 said server。服务器生成一个 shell 来处理“内部”脚本。脚本开始于
'
after-d
并结束于'
before||
。全部都被引用,但引用从单引号变为双引号并返回几次。这是因为$tmux
和$service
需要由处理主脚本的 shell 展开,其他变量(例如$status
)必须在“内部”shell 中展开,在tmux
. 以下资源可能会有所帮助:参数扩展(变量扩展)和引号内的引号。里面的shell
tmux
准备忽略HUP
信号。shell 在服务期望的 pid 文件中注册它的 PID。
然后它运行
dotnet
并存储它的退出状态(严格来说,如果cd
失败那么它将是 的退出状态cd
)。shell 杀死
tmux
服务器。我们也可以这样做kill "$PPID"
(参见this),但是如果有人终止了服务器并且另一个进程获得了它的 PID,我们将终止一个错误的进程。寻址tmux
更安全。因为trap
壳幸存了下来。然后 shell 循环直到它的 PPID 与之前不同。我们不能依赖于比较
$ppid
,$PPID
因为后者不是动态的;我们从 检索当前的 PPIDps
。现在 shell 知道它有一个新的父级,它应该是
systemd
. 只有现在systemd
才能从 shell 中检索退出状态。dotnet
shell 以先前检索到的确切退出状态退出。systemd
尽管事实dotnet
从来都不是它的孩子,但这种方式获得了退出状态。tmux
从公共服务器检索退出状态您原来的方法使用一个公共(默认)
tmux
服务器,它只操作一个名为rof
. 一般来说,其他会话可能存在或出现,因此该服务永远不应终止整个服务器。有几个方面。我们应该:systemd
杀死tmux
服务器,即使服务器是从服务中启动的;systemd
考虑dotnet
进程成为服务的一部分,即使它tmux
不是从服务内部启动的;dotnet
从某种程度上检索退出状态。服务文件:
请注意
Type=simple
现在,因为主脚本是我们可以从中检索退出状态的唯一有保证的子脚本。该脚本需要找出退出状态dotnet …
并将其报告为自己的。/home/alpine_sour/scripts/rofdl
:解释:
如果
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
通过将其写入/sys/fs/cgroup/systemd/system.slice/rofdl.service/tasks
.里面的shell
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(实现?)使用 implicit 运行最后一个命令
exec
,这是我们不想要的。这就是我使用exit "$?"
after的原因dotnet …
。但是如果shell本身被强行kill掉,空的问题又会
#{pane_dead_status}
出现。作为最后的手段status="${status:-255}"
,将空状态转换为255
(尽管我不确定255
在这种情况下是否是最佳值)。存在竞争条件:当主脚本查询
tmux
时#{pane_id}
,它可能不是正确的窗格。tmux new-session
如果有人在 之后和之前附加并在会话中播放tmux display-message
,我们可能会得到一个错误的窗格。时间窗口很小,这仍然没有我想要的那么优雅。If
tmux new-session
could print#{pane_id}
to the console liketmux display-message -p
can, there should be no problem. With-PF
it can show it within the session. There is no support for-p
.You may want some logic in case the
tmux
server gets killed.Retrieving exit status via file
The above example can be modified, so
remain-on-exit on
is not needed,#{pane_id}
is not needed (race condition avoided, at least the described one).The service file from the previous example remains.
/home/alpine_sour/scripts/rofdl
:The mechanism is pretty straightforward: the main shell removes the old status file (if any), triggers
tmux
and loops until the file reappears. The "inner" shell writes the exit status ofdotnet
to the file, when ready.Notes:
echo "$?" > "$statf"
, the file would be created empty, then written to. This might lead to a situation when the main script reads an empty string as status. In general the receiver might get incomplete data: reading until EOF while the sender is mid-write and the file is yet about to grow. Renaming makes the right file with the right content appear instantly.Final notes
tmux
, the solution with a separatetmux
server seems most robust.This is what the documentation says about
Restart=
:Note
$?
in a shell is just a number. Again: this link. If yourdotnet
exits because of a signal and restarting depends on (un-)clean exit, the solutions wheresystemd
retrieves exit code directly fromdotnet
may behave differently than solutions wheresystemd
retrieves exit status from an intermediary shell. ResearchSuccessExitStatus=
, it may be useful.也许你可以
RestartForceExitStatus=
在服务文件中使用https://www.freedesktop.org/software/systemd/man/systemd.service.html