У меня есть программа 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
Объяснение:
Основной скрипт убивает эксклюзивный
tmux
сервер (если таковой имеется) и запускает его заново. После запуска сервера скрипт завершает работу. Служба остается, поскольку в cgroup остался хотя бы один процесс, указанный сервер.Сервер порождает оболочку для обработки "внутреннего" скрипта. Скрипт начинается с
'
after-d
и заканчивается'
before||
. Он весь заключен в кавычки, но кавычки меняются с одинарных на двойные и обратно несколько раз. Это потому, что$tmux
и$service
должны быть расширены оболочкой, обрабатывающей основной скрипт, другие переменные (например,$status
) не должны быть расширены до "внутренней" оболочки, внутриtmux
. Следующий ресурс может оказаться полезным:Расширение параметров (расширение переменных) и кавычки внутри кавычек.Внутренняя оболочка
tmux
готовится игнорироватьHUP
сигнал.Оболочка регистрирует свой PID в pidfile, который ожидает служба.
Затем он запускается
dotnet
и сохраняет свой статус завершения (строго говоря, вcd
случае сбоя это будет статус завершенияcd
).Оболочка убивает
tmux
сервер. Мы могли бы сделать этоkill "$PPID"
также (см.этот), но если кто-то убьет сервер и другой процесс получит его PID, мы убьём не тот процесс. Адресацияtmux
безопаснее. Из-заtrap
оболочки выживает.Затем оболочка зацикливается до тех пор, пока ее PPID не станет отличаться от того, что было раньше. Мы не можем полагаться на сравнение
$ppid
с,$PPID
поскольку последнее не является динамическим; мы извлекаем текущий PPID изps
.Теперь оболочка знает, что у нее есть новый родитель, это должен быть
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
Объяснение:
Если
tmux new-session
создает сервер (потому что его не было), мы хотим, чтобы он был в другой cgroup с самого начала, чтобы предотвратить состояние гонки, когда что-то еще начинает использовать сервер, а мы еще не изменили его cgroup, иsystemd
решает убить службу по какой-либо причине. Я пытался запуститьtmux new-session
сcgexec
и потерпел неудачу; поэтому другой подход: подоболочка, которая изменяет свою собственную cgroup (записывая в/sys/fs/cgroup/systemd/system.slice/tasks
), а затемexec
s вtmux new-session
.Оболочка внутри
tmux
запускается путем включенияremain-on-exit
опции для сеанса. После выхода панель остается, и другой процесс (в нашем случае основной скрипт) может получить статус выхода с сервераtmux
.В это время основной скрипт извлекает уникальный идентификатор панели, на которой работает другая оболочка. Если кто-то подключается к сеансу или создает новые панели и работает с ними, основной скрипт все равно сможет найти нужную панель.
Внутренняя оболочка
tmux
регистрирует свой PID в cgroup, связанной со службой, записывая его в/sys/fs/cgroup/systemd/system.slice/rofdl.service/tasks
.Оболочка внутри
tmux
запускаетсяdotnet …
. Послеdotnet
завершения оболочка завершает работу. Статус выхода, полученный из,dotnet
сообщается оболочкой серверуtmux
.Из-за этого
remain-on-exit on
панель остается в мертвом состоянии после выхода «внутренней» оболочки.В это время основная оболочка зацикливается, пока панель не умрет. Затем она запрашивает у
tmux
сервера соответствующий статус выхода и сообщает его как свой собственный. Таким образомsystemd
получает статус выхода изdotnet
.
Примечания:
Опять же естьцитаты в кавычках.
Вместо
dotnet run
этого может бытьexec dotnet run
. Последняя форма хороша:dotnet
заменяет внутреннюю оболочку, так что есть один процесс вместо двух. Проблема в том, что когдаdotnet
он завершается сигналом, который он не может обработать. Оказывается,#{pane_dead_status}
он сообщит о пустой строке, если процесс в панели принудительно завершается сигналом. Поддержание оболочки междуdotnet
иtmux
предотвращает это: оболочка преобразует информацию (см.этот вопрос) и возвращает число.Некоторые оболочки (реализации?) запускают самую последнюю команду с неявным
exec
, чего мы не хотим. Вот почему я использовалexit "$?"
afterdotnet …
.Но если саму оболочку принудительно убить, проблема с пустым
#{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 -p
can, то проблем быть не должно. С-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