AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • 主页
  • 系统&网络
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • 主页
  • 系统&网络
    • 最新
    • 热门
    • 标签
  • Ubuntu
    • 最新
    • 热门
    • 标签
  • Unix
    • 最新
    • 标签
  • DBA
    • 最新
    • 标签
  • Computer
    • 最新
    • 标签
  • Coding
    • 最新
    • 标签
主页 / computer / 问题 / 1474408
Accepted
jay w
jay w
Asked: 2019-08-24 00:07:56 +0800 CST2019-08-24 00:07:56 +0800 CST 2019-08-24 00:07:56 +0800 CST

Tmux 窗口程序失败时 Systemd 服务重启

  • 772

我有一个在 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 服务器的子进程而不是服务器本身并相应地重新启动。

linux bash
  • 2 2 个回答
  • 1430 Views

2 个回答

  • Voted
  1. Best Answer
    Kamil Maciorowski
    2019-08-29T00:03:37+08:002019-08-29T00:03:37+08:00

    初步说明

    • 这个答案基于 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认为服务处于活动状态。

    如果只有一个主进程,则在主进程退出后整个 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服务器(作为父级)知道其退出状态。在某些情况下,我们可以从另一个脚本查询服务器并检索退出状态。

    或者由 触发的 shelltmux new可能会将状态存储在一个文件中,因此它可以被另一个脚本检索。

    因为你运行的ExecStart=是肯定的孩子systemd,所以这是“另一个脚本”的最佳候选者。它应该等到它可以检索退出状态,然后将其用作自己的退出状态,所以systemd得到它。注意服务应该Type=simple在这种情况下。

    或者,您可以从dotnet …外部开始tmux,然后reptyr从tmux服务器内部开始。这种方式dotnet可以systemd从一开始就是一个孩子,当你试图窃取它的 tty 时可能会出现问题。


    解决方案和示例

    reptyr至tmux

    此示例在tty2. 脚本准备stmux到. 最后,其中的一个 shell试图窃取现在的 tty 。execdotnettmuxdotnet

    服务文件:

    [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是一个糟糕的解决方法)和鼠标支持问题。
    • 可以从与服务关联的 cgroup 中删除tmux服务器。您可能想要这样做。请参阅下面/sys/fs/cgroup/systemd/的方式,代码中有。

    没有tmux?

    /dev/tty2无论如何使用上述解决方案。如果你tmux只需要提供一个控制终端,可以考虑cd /home/alpine_sour/rofdl && exec dotnet runwithout 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
    

    解释:

    1. 主脚本杀死独占tmux服务器(如果有的话)并重新启动它。服务器启动后,脚本退出。该服务仍然存在,因为 cgroup 中至少还剩下一个进程,即 said server。

    2. 服务器生成一个 shell 来处理“内部”脚本。脚本开始于'after-d并结束于'before ||。全部都被引用,但引用从单引号变为双引号并返回几次。这是因为$tmux和$service需要由处理主脚本的 shell 展开,其他变量(例如$status)必须在“内部”shell 中展开,在tmux. 以下资源可能会有所帮助:参数扩展(变量扩展)和引号内的引号。

    3. 里面的shelltmux准备忽略HUP信号。

    4. shell 在服务期望的 pid 文件中注册它的 PID。

    5. 然后它运行dotnet并存储它的退出状态(严格来说,如果cd失败那么它将是 的退出状态cd)。

    6. shell 杀死tmux服务器。我们也可以这样做kill "$PPID"(参见this),但是如果有人终止了服务器并且另一个进程获得了它的 PID,我们将终止一个错误的进程。寻址tmux更安全。因为trap壳幸存了下来。

    7. 然后 shell 循环直到它的 PPID 与之前不同。我们不能依赖于比较$ppid,$PPID因为后者不是动态的;我们从 检索当前的 PPID ps。

    8. 现在 shell 知道它有一个新的父级,它应该是systemd. 只有现在systemd才能从 shell 中检索退出状态。dotnetshell 以先前检索到的确切退出状态退出。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
    

    解释:

    1. 如果tmux new-session创建一个服务器(因为没有),我们希望它从一开始就在另一个 cgroup 中,以防止在其他东西开始使用服务器时出现竞争情况,而我们还没有更改它的 cgroup 并systemd决定出于任何原因终止该服务. 我试图运行但失败tmux new-session了cgexec;因此另一种方法:一个子shell改变它自己的cgroup(通过写入/sys/fs/cgroup/systemd/system.slice/tasks)然后execs到tmux new-session。

    2. 内部的 shelltmux通过为会话启用remain-on-exit选项开始。在它退出后,窗格仍然存在,另一个进程(在我们的例子中是主脚本)可以从tmux服务器检索它的退出状态。

    3. 与此同时,主脚本检索另一个 shell 运行所在窗格的唯一 ID。如果有人附加到会话或创建新窗格并使用它们,主脚本仍将能够找到正确的窗格。

    4. 内部的 shelltmux通过将其写入/sys/fs/cgroup/systemd/system.slice/rofdl.service/tasks.

    5. 里面的shelltmux运行dotnet …。dotnet终止后,shell 退出。从中检索到的退出状态dotnet由 shell 报告给tmux服务器。

    6. 由于remain-on-exit on,在“内部”外壳退出后,窗格仍处于死状态。

    7. 与此同时,主 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 like tmux 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:

    #!/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
    

    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 of dotnet to the file, when ready.

    Notes:

    • What if the inner shell is killed? What if the file cannot be created? It's relatively easy to get to a situation where the main script cannot exit the loop.
    • Writing to a temporary file and then renaming is a good practice. If we did 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

    • If you cannot go without tmux, the solution with a separate tmux server seems most robust.
    • This is what the documentation says about Restart=:

      In this context, a clean exit means an exit code of 0, or one of the signals SIGHUP, SIGINT, SIGTERM or SIGPIPE, and […]

      Note $? in a shell is just a number. Again: this link. If your dotnet exits because of a signal and restarting depends on (un-)clean exit, the solutions where systemd retrieves exit code directly from dotnet may behave differently than solutions where systemd retrieves exit status from an intermediary shell. Research SuccessExitStatus=, it may be useful.

    • 2
  2. Jvol Jvolizka
    2019-08-24T00:16:52+08:002019-08-24T00:16:52+08:00

    也许你可以RestartForceExitStatus=在服务文件中使用

    获取退出状态定义列表,当主服务进程返回时,将强制自动服务重新启动,而不管使用 Restart= 配置的重新启动设置如何。参数格式类似于 RestartPreventExitStatus=。

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

    • 0

相关问题

  • Notify-发送窗口下出现的通知

  • 以 root 身份运行 docker 容器

  • 如何在域和 Linux 活动目录中启用指纹传感器

  • 如何在CentOS 7 中将Ctrl+C 永久更改为Ctrl+K?

  • 如何从 WSL 打开 office 文件

Sidebar

Stats

  • 问题 205573
  • 回答 270741
  • 最佳答案 135370
  • 用户 68524
  • 热门
  • 回答
  • Marko Smith

    Windows 照片查看器因为内存不足而无法运行?

    • 5 个回答
  • Marko Smith

    支持结束后如何激活 WindowsXP?

    • 6 个回答
  • Marko Smith

    远程桌面间歇性冻结

    • 7 个回答
  • Marko Smith

    Windows 10 服务称为 AarSvc_70f961。它是什么,我该如何禁用它?

    • 2 个回答
  • Marko Smith

    子网掩码 /32 是什么意思?

    • 6 个回答
  • Marko Smith

    鼠标指针在 Windows 中按下的箭头键上移动?

    • 1 个回答
  • Marko Smith

    VirtualBox 无法以 VERR_NEM_VM_CREATE_FAILED 启动

    • 8 个回答
  • Marko Smith

    应用程序不会出现在 MacBook 的摄像头和麦克风隐私设置中

    • 5 个回答
  • Marko Smith

    ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] 证书验证失败:无法获取本地颁发者证书 (_ssl.c:1056)

    • 4 个回答
  • Marko Smith

    我如何知道 Windows 安装在哪个驱动器上?

    • 6 个回答
  • Martin Hope
    Albin 支持结束后如何激活 WindowsXP? 2019-11-18 03:50:17 +0800 CST
  • Martin Hope
    fixer1234 “HTTPS Everywhere”仍然相关吗? 2019-10-27 18:06:25 +0800 CST
  • Martin Hope
    Kagaratsch Windows 10 删除大量小文件的速度非常慢。有什么办法可以加快速度吗? 2019-09-23 06:05:43 +0800 CST
  • Martin Hope
    andre_ss6 远程桌面间歇性冻结 2019-09-11 12:56:40 +0800 CST
  • Martin Hope
    Riley Carney 为什么在 URL 后面加一个点会删除登录信息? 2019-08-06 10:59:24 +0800 CST
  • Martin Hope
    zdimension 鼠标指针在 Windows 中按下的箭头键上移动? 2019-08-04 06:39:57 +0800 CST
  • Martin Hope
    Inter Sys Ctrl+C 和 Ctrl+V 是如何工作的? 2019-05-15 02:51:21 +0800 CST
  • Martin Hope
    jonsca 我所有的 Firefox 附加组件突然被禁用了,我该如何重新启用它们? 2019-05-04 17:58:52 +0800 CST
  • Martin Hope
    MCK 是否可以使用文本创建二维码? 2019-04-02 06:32:14 +0800 CST
  • Martin Hope
    SoniEx2 更改 git init 默认分支名称 2019-04-01 06:16:56 +0800 CST

热门标签

windows-10 linux windows microsoft-excel networking ubuntu worksheet-function bash command-line hard-drive

Explore

  • 主页
  • 问题
    • 最新
    • 热门
  • 标签
  • 帮助

Footer

AskOverflow.Dev

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve