예비 참고 사항

예비 참고 사항

0이 아닌 오류 코드로 인해 가끔 실패하는 tmux의 bash 내부에서 실행되는 dotnet 프로그램이 있습니다. tmux 내부에서 내 dotnet 프로그램을 프로그래밍 방식으로 시작하기 위해 systemd 서비스 파일을 사용하려고 합니다.

서비스 파일은 다음과 같습니다.

[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

그래서 더 높은 수준의 tmux 서버가 아닌 가장 낮은 수준의 프로세스 포크를 추적하도록 시스템화하는 방법이 궁금합니다. 서버 자체가 아닌 tmux 서버의 하위 프로세스를 추적하고 그에 따라 다시 시작하도록 systemd에 지시하는 방법이 필요합니다.

답변1

예비 참고 사항

  • 이 답변은 Debian 9의 실험을 기반으로 합니다.
  • 귀하의 서비스는 시스템 서비스(in /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=. 기본 프로세스를 지정하려면 이 방법으로 PID를 Type=forking사용하고 전달해야 합니다 . PIDFile=그리고 서비스를 중지하면 systemd해당 서비스에 속한 모든 프로세스가 종료됩니다. 따라서 cgroup의 서비스와 관련된 프로세스만 포함하는 것이 중요합니다. 귀하의 경우 tmux서비스 내에서 시작된 경우에도 서버를 제외할 수 있습니다 .

cgroup 간에 프로세스를 이동하는 도구/방법이 있습니다. 또는 tmux해당 서비스와 관련된 별도의 서버를 실행할 수도 있습니다 .

systemd어떤 종료 상태를 사용할지 어떻게 알 수 있나요?

Restart=on-failure기본 프로세스의 종료 상태에 대한 종속성을 설정합니다. 어떤 종료 상태를 사용할지 알고 사용 Type=forking하는 것이 PIDFile=좋습니다 .systemd

systemd하지만 종료 상태를 검색할 수도 있고 검색하지 못할 수도 있습니다.

종료 상태를 검색하는 사람

자식이 종료된 후 해당 부모는 종료 상태를 검색할 수 있습니다(비교좀비 프로세스).

서버가 오래되었거나 새로운 지 여부에 관계없이 tmux명령은 고아가 되지 않는 한 하위 명령이 아니며 systemd커널은 상위 서버를 PID 1(아니면 다른 것) 그리고 새로운 부모가 옳습니다 systemd.

tmux new서버가 쉘을 실행 하도록 하기 위해 제공하는 명령은 tmux쉘이 실행되어 dotnet종료될 때까지 기다리거나 서버를 상위 서버로 유지하면서 exec종료되도록 합니다 . 어쨌든 가 아닌 부모가 있습니다 .dotnettmuxdotnetsystemd

dotnet다음과 같이 고아가 될 수 있습니다 . nohup dotnet … &그런 다음 해당 쉘이 종료되도록 하십시오. 또한 PIDFile=서비스가 모니터링할 프로세스를 알 수 있도록 PID를 저장하고 장치 구성 파일에 사용해야 합니다 . 그러면 좀 효과가 있을 것 같아요.

명확하게 말하자면, 내 테스트에서는 종료 상태를 검색할 수 있는 사람 nohup sleep 300 &이 성공적으로 채택했습니다 systemd(cgroup을 처리한 후).

tmux하지만 처음부터 사용하고 싶기 때문에 명령이 터미널과 상호 작용하는 것 같습니다. 그래서nohup여기서는 올바른 도구가 아닙니다. 터미널에 연결된 상태를 유지하면서 프로세스를 분리하는 것은 까다로울 수 있습니다. 당신은 그것을 고아로 만들고 싶지만 내부의 쉘이 tmux단순히 종료되도록 할 수는 없습니다. 왜냐하면 이렇게 하면 해당 창을 죽이거나 죽은 상태로 남겨두기 때문입니다.

Note는 Type=forking의 채택에 의존합니다 systemd. 주요 서비스 프로세스는 분기되어 종료됩니다. 그런 다음 systemd아이를 입양합니다. 하지만 이러한 데몬은 어떤 터미널과도 상호 작용해서는 안 됩니다.

tmux또 다른 접근 방식은 서버 내의 쉘 execdotnet. 종료된 후 tmux서버(부모로서)는 종료 상태를 알게 됩니다. 어떤 상황에서는 다른 스크립트에서 서버를 쿼리하고 종료 상태를 검색할 수 있습니다.

또는 에 의해 트리거된 셸이 tmux new상태를 파일에 저장하여 다른 스크립트에서 검색할 수 있습니다.

당신이 실행하는 것은 확실히 ExecStart=의 자식이기 때문에 이것은 "다른 스크립트"에 대한 가장 좋은 후보입니다. systemd종료 상태를 검색할 수 있을 때까지 기다린 다음 이를 자체 종료 상태로 사용하여 systemd가져옵니다. Type=simple이 경우 서비스가 있어야 합니다 .

dotnet …또는 외부에서 시작할 수 있습니다 tmux.reptyr서버 내부에서 tmux. 이 방법은 처음부터 dotnet의 자식일 수 있으며 해당 tty를 훔치려고 할 때 문제가 나타날 수 있습니다.systemd


솔루션 및 예시

reptyr에게tmux

이 예에서는 에서 스크립트를 실행합니다 tty2. 스크립트가 준비 tmux되고 . 마지막으로 내부의 쉘이 현재의 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 runhtopreptyrsleep 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다른 사람(또는 모든 것)이 세션을 잃을 위험 없이 서버를 파괴할 수 있습니다. 서비스를 모니터링/상호작용하려는 경우를 제외하고는 누구도 이 서버를 사용해서는 안 됩니다. 누군가 그것을 다른 용도로 사용한다면 그것은 그 사람의 문제입니다.

서버를 자유롭게 종료할 수 있는 기능을 사용하면 .NET 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에 해당 서버라는 프로세스가 하나 이상 남아 있기 때문에 서비스는 그대로 유지됩니다.

  2. 서버는 "내부" 스크립트를 처리하기 위해 쉘을 생성합니다. 스크립트는 'after 에서 시작하고 before -d에서 끝납니다 . 모두 인용되었지만 인용은 작은 따옴표에서 큰 따옴표로 바뀌었다가 몇 번 되돌려졌습니다. 기본 스크립트를 처리하는 쉘에 의해 확장되어야 하기 때문에 다른 변수(예: ) 는 "내부" 쉘, 내부에 있을 때까지 확장되어서는 안 됩니다 . 다음 리소스가 도움이 될 수 있습니다.'||$tmux$service$statustmux매개변수 확장(변수 확장) 및 따옴표 안의 따옴표.

  3. 내부의 쉘은 신호를 tmux무시할 준비를 합니다 HUP.

  4. 쉘은 서비스가 예상하는 pidfile에 PID를 등록합니다.

  5. 그런 다음 실행되고 dotnet종료 상태를 저장합니다(엄격히 cd실패하면 종료 상태가 됩니다 cd).

  6. 쉘이 tmux서버를 죽입니다. 우리도 이것을 할 수 있습니다 kill "$PPID"(참조이것), 그러나 누군가가 서버를 종료하고 다른 프로세스가 해당 PID를 얻은 경우 잘못된 프로세스를 종료하게 됩니다. 주소 지정이 tmux더 안전합니다. 껍질이 살아남기 때문이다 trap.

  7. 그런 다음 PPID가 이전과 다를 때까지 쉘이 반복됩니다. 후자가 동적이지 않기 때문에 $ppid비교 에 의존할 수 없습니다 . $PPID에서 현재 PPID를 검색합니다 ps.

  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. 서버를 생성하는 경우 (아무것도 없었기 때문에) 다른 것이 서버를 사용하기 시작하고 아직 cgroup을 변경하지 않았으며 어떤 이유로든 서비스를 종료하기로 결정했을 tmux new-session때 경쟁 조건을 방지하기 위해 처음부터 다른 cgroup에 서버를 두기를 원합니다. systemd. 나는 tmux new-session함께 달리려고 노력했지만 cgexec실패했습니다. 따라서 또 다른 접근 방식: 자체 cgroup을 변경하고(에 기록하여 /sys/fs/cgroup/systemd/system.slice/tasks) execs를 로 변경하는 하위 쉘입니다 tmux new-session.

  2. 내부 셸은 세션에 대한 옵션을 tmux활성화하여 시작됩니다 . remain-on-exit종료된 후에도 창은 그대로 유지되며 다른 프로세스(이 경우 기본 스크립트)는 서버에서 종료 상태를 검색할 수 있습니다 tmux.

  3. 그 동안 기본 스크립트는 다른 셸이 실행되는 창의 고유 ID를 검색합니다. 누군가가 세션에 연결하거나 새 창을 만들고 이를 가지고 재생하는 경우 기본 스크립트는 여전히 올바른 창을 찾을 수 있습니다.

  4. 내부 쉘은 tmuxPID를 서비스와 연관된 cgroup에 기록하여 등록합니다 /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 runexec dotnet run. 마지막 형식은 훌륭합니다. dotnet내부 셸을 대체하므로 두 개가 아닌 하나의 프로세스가 있습니다. 문제는 dotnet처리할 수 없는 신호에 의해 종료될 때입니다. #{pane_dead_status}창의 프로세스가 신호에 의해 강제로 종료되면 빈 문자열이 보고됩니다 . 사이에 쉘을 유지하면 dotnet이를 tmux방지할 수 있습니다. 쉘은 정보를 변환합니다(참조이 질문) 숫자를 반환합니다.

    exec일부 쉘(구현?)은 우리가 원하지 않는 암시적 으로 마지막 명령을 실행합니다 . 그래서 exit "$?"이후에 사용했습니다 dotnet ….

    그러나 쉘 자체가 강제로 종료되면 빈 문제가 #{pane_dead_status}다시 나타납니다. 최후의 수단으로 status="${status:-255}"빈 상태를 다음으로 변환합니다 (비록 이 경우 최상의 값인지는 255확실하지 않지만 ).255

  • 경쟁 조건이 있습니다. 기본 스크립트가 을 쿼리 tmux할 때 #{pane_id}오른쪽 창이 아닐 수 있습니다. 누군가가 이후 tmux new-session및 이전 세션 내에서 연결하고 플레이한 경우 tmux display-message잘못된 창이 나타날 수 있습니다. 시간 창은 작지만 여전히 원하는 만큼 우아하지는 않습니다.

    can 처럼 콘솔에 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신호 중 하나를 의미합니다 .SIGHUPSIGINTSIGTERMSIGPIPE

    $?쉘의 메모는 단지 숫자일 뿐입니다. 다시:이 링크. dotnet신호로 인해 종료하고 다시 시작하는 것이 (비)깨끗한 종료에 따라 달라지는 경우 systemd종료 코드를 직접 검색하는 솔루션은 중간 셸에서 종료 상태를 검색하는 dotnet솔루션과 다르게 동작할 수 있습니다 . systemd연구 SuccessExitStatus=하면 유용할 수 있습니다.

답변2

RestartForceExitStatus=어쩌면 서비스 파일에서 사용할 수 있습니다

Restart=로 구성된 다시 시작 설정에 관계없이 기본 서비스 프로세스에서 반환될 때 자동 서비스 다시 시작을 강제하는 종료 상태 정의 목록을 가져옵니다. 인수 형식은 RestartPreventExitStatus=와 유사합니다.

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

관련 정보