Предварительные заметки

Предварительные заметки

У меня есть программа dotnet, запущенная внутри bash в tmux, которая иногда дает сбой с ненулевым кодом ошибки. Я пытаюсь использовать файл службы systemd для программного запуска моей программы dotnet внутри tmux.

Вот файл сервиса:

[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будет считать сервис активным.

Если есть один основной процесс, вся cgroup будет завершена после завершения основного процесса. С Type=simpleосновным процессом является тот, который указан с помощью ExecStart=. С Type=forkingвам нужно использовать PIDFile=и передать PID таким образом, чтобы указать основной процесс. И когда вы останавливаете службу, systemdзавершает все процессы, которые принадлежат службе. Поэтому важно включать только процессы, специфичные для службы, в cgroup. В вашем случае вы можете захотеть исключить tmuxсервер, даже если он запущен из службы.

Существуют инструменты/способы перемещения процессов между cgroups. Или вы можете запустить отдельный tmuxсервер, специфичный для сервиса.

Как systemdузнать, какой статус выхода использовать

Restart=on-failureустанавливает зависимость от статуса выхода основного процесса. С Type=forkingего рекомендуется использовать, PIDFile=чтобы systemdзнать, какой статус выхода использовать.

systemdОднако может и не получиться восстановить статус выхода.

Кто получает статус выхода

После выхода дочернего элемента его родительский элемент может получить статус выхода (сравнитезомби-процесс).

Независимо от того, является ли tmuxсервер старым или новым, ваша команда не будет дочерней, systemdесли только она не станет осиротевшей, ядро ​​установит ее родительскую команду на PID 1 (или какой-то другой) и новый родитель — правый systemd.

Команда, которую вы предоставляете, tmux newзаставляет tmuxсервер запустить оболочку, затем оболочка либо запускается dotnetи ждет ее выхода, либо execпереходит в dotnet, сохраняя tmuxсервер в качестве родителя. В любом случае dotnetимеет родителя, который не является systemd.

Вы можете сделать сиротой dotnet, как это: nohup dotnet … &, затем позволить указанной оболочке выйти. Вам также нужно будет сохранить PID, использовать PIDFile=в файле конфигурации юнита, чтобы служба знала, какой процесс отслеживать. Тогда это может как-то сработать.

Для ясности: в моих тестах nohup sleep 300 &был успешно принят, systemdкоторый затем смог получить свой статус выхода (после того, как я позаботился о cgroups).

Но поскольку вы хотите использовать tmuxв первую очередь, я полагаю, что ваша команда взаимодействует с терминалом. Так чтоnohupне является правильным инструментом здесь. Осиротить процесс, сохраняя его подключенным к терминалу, может быть сложно. Вы хотите осиротить его, но вы не можете позволить оболочке внутри tmuxпросто выйти, потому что это убьет его панель (или оставит ее в мертвом состоянии).

Примечание Type=forkingполагается на принятие systemd. Основной процесс службы должен разветвляться и завершаться. Затем systemdпринимает своего потомка. Такой демон не должен взаимодействовать ни с каким терминалом.

Другой подход — позволить оболочке внутри tmuxсервера exec. dotnetПосле выхода tmuxсервер (как родитель) знает его статус выхода. В некоторых обстоятельствах мы можем запросить сервер из другого скрипта и получить статус выхода.

Или оболочка, запущенная с помощью, tmux newможет сохранить статус в файле, чтобы его можно было извлечь с помощью другого скрипта.

Поскольку то, что вы запускаете, наверняка ExecStart=является потомком systemd, это лучший кандидат на роль "другого скрипта". Он должен подождать, пока не сможет получить статус выхода, а затем использовать его как свой собственный статус выхода, поэтому получает его. Обратите внимание, что в этом случае systemdдолжна быть служба .Type=simple

В качестве альтернативы вы можете начать dotnet …за пределами tmux, тогдаreptyrизнутри сервера tmux. Этот способ dotnetможет быть потомком systemdс самого начала, проблемы могут возникнуть при попытке украсть его tty.


Решения и примеры

reptyrкtmux

Этот пример запускает скрипт в tty2. Скрипт подготавливается tmuxи execпереходит в 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по умолчанию принадлежит контрольной группе сервиса.
  • Служба может уничтожить 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 остался хотя бы один процесс, указанный сервер.

  2. Сервер порождает оболочку для обработки "внутреннего" скрипта. Скрипт начинается с 'after -dи заканчивается 'before ||. Он весь заключен в кавычки, но кавычки меняются с одинарных на двойные и обратно несколько раз. Это потому, что $tmuxи $serviceдолжны быть расширены оболочкой, обрабатывающей основной скрипт, другие переменные (например, $status) не должны быть расширены до "внутренней" оболочки, внутри tmux. Следующий ресурс может оказаться полезным:Расширение параметров (расширение переменных) и кавычки внутри кавычек.

  3. Внутренняя оболочка tmuxготовится игнорировать HUPсигнал.

  4. Оболочка регистрирует свой PID в pidfile, который ожидает служба.

  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считался 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и потерпел неудачу; поэтому другой подход: подоболочка, которая изменяет свою собственную cgroup (записывая в /sys/fs/cgroup/systemd/system.slice/tasks), а затем execs в tmux new-session.

  2. Оболочка внутри tmuxзапускается путем включения remain-on-exitопции для сеанса. После выхода панель остается, и другой процесс (в нашем случае основной скрипт) может получить статус выхода с сервера tmux.

  3. В это время основной скрипт извлекает уникальный идентификатор панели, на которой работает другая оболочка. Если кто-то подключается к сеансу или создает новые панели и работает с ними, основной скрипт все равно сможет найти нужную панель.

  4. Внутренняя оболочка tmuxрегистрирует свой PID в 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 runэтого может быть exec dotnet run. Последняя форма хороша: dotnetзаменяет внутреннюю оболочку, так что есть один процесс вместо двух. Проблема в том, что когда dotnetон завершается сигналом, который он не может обработать. Оказывается, #{pane_dead_status}он сообщит о пустой строке, если процесс в панели принудительно завершается сигналом. Поддержание оболочки между dotnetи tmuxпредотвращает это: оболочка преобразует информацию (см.этот вопрос) и возвращает число.

    Некоторые оболочки (реализации?) запускают самую последнюю команду с неявным exec, чего мы не хотим. Вот почему я использовал exit "$?"after dotnet ….

    Но если саму оболочку принудительно убить, проблема с пустым #{pane_dead_status}появляется снова. В крайнем случае status="${status:-255}"преобразует пустой статус в 255(хотя я не уверен, 255что это лучшее значение в таком случае).

  • Есть состояние гонки: когда основной скрипт запрашивает tmux, #{pane_id}это может быть не та панель. Если кто-то подключился и играл внутри сеанса после tmux new-sessionи до tmux display-message, мы можем получить неправильную панель. Временное окно мало, но все равно это не так элегантно, как я хотел.

    Если бы tmux new-sessionможно было печатать #{pane_id}на консоль, как tmux display-message -pcan, то проблем быть не должно. С -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, или один из сигналов SIGHUP, SIGINT, SIGTERMили SIGPIPE, и […]

    Примечание $?в оболочке — это просто число. Еще раз:эта ссылка. Если ваши dotnetвыходы из-за сигнала и перезапуск зависит от (не)чистого выхода, решения, где systemdизвлекается код выхода напрямую, dotnetмогут вести себя иначе, чем решения, где systemdизвлекается статус выхода из промежуточной оболочки. Исследуйте SuccessExitStatus=, это может быть полезно.

решение2

Может быть, вы можете использовать RestartForceExitStatus=файл в сервисе

Принимает список определений статуса выхода, которые при возврате основным процессом службы приведут к автоматическому перезапуску службы, независимо от настройки перезапуска, настроенной с помощью Restart=. Формат аргумента аналогичен RestartPreventExitStatus=.

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

Связанный контент