Tengo un programa dotnet ejecutándose dentro de bash en tmux que ocasionalmente falla con un código de error distinto de cero. Estoy intentando utilizar un archivo de servicio systemd para iniciar mediante programación mi programa dotnet dentro de tmux.
Aquí está el archivo de servicio:
[Unit]
Description=dotnet application
[Service]
Type=forking
ExecStart=/home/alpine_sour/scripts/rofdl
Restart=always
User=root
[Install]
WantedBy=multi-user.target
Aquí está el script de shell 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"
Ahora, cuando inicio el servicio, systemd elige el PID principal como servidor tmux, lo que supongo se debe a que fue el primer comando ejecutado. Por lo tanto, cuando mi programa en la ventana tmux sale con CUALQUIER código de error Y no hay más ventanas, el servidor tmux sale con un código de error exitoso, lo que hace que systemd no se reinicie. Incluso si tuviera que reiniciar = siempre, el servidor tmux solo se reiniciaría si mi programa falla Y no hay otras ventanas.
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
Entonces me pregunto cómo haría para que systemd rastree el nivel más bajo de la bifurcación del proceso en lugar del servidor tmux de nivel superior. Necesito una manera de decirle a systemd que rastree el proceso secundario del servidor tmux en lugar del servidor mismo y reinicie en consecuencia.
Respuesta1
Notas preliminares
- Esta respuesta se basa en experimentos en Debian 9.
- Supongo que su servicio es un servicio del sistema (en
/etc/systemd/system
). - Lo que publicaste cerca del final del cuerpo de la pregunta parece unextractode
systemctl status …
. No dice nada sobre cgroups. Esta respuesta suponeGrupos de controlestan involucrados. Creo quesystemd
los requiere, así deben ser. El comando en sí puede ejecutarse en un bucle hasta que tenga éxito:
cd /home/alpine_sour/rofdl && while ! dotnet run; do :; done
pero entiendo que quieres una
systemd
solución.
Problemas
primero por favor leacomo tmux
funciona. Comprender qué proceso es de quién es el hijo será de gran ayuda.
¿Qué procesos pertenecen al servicio?
En su caso original, el servicio se considerará inactivo (y listo para reiniciarse, si corresponde) después de que todos los procesos de su cgroup salgan.
Su secuencia de comandos intenta finalizar la tmux
sesión anterior, no el tmux
servidor anterior. Luego tmux new
(equivalente a tmux new-session
) inicia un servidor o usa el anterior.
Si usa el anterior, ni el servidor ni su comando (
dotnet …
) serán descendientes del script. Estos procesos no pertenecerán al cgroup asociado al servicio. Una vez que salga el script,systemd
el servicio se considerará inactivo.Si inicia un nuevo
tmux
servidor, entonces el servidor y el comando se asignarán al cgroup asociado con el servicio. Entonces nuestro comando puede finalizar, pero si hay otras sesiones/ventanas (creadas más tarde) dentro del servidor, el servidor puede permanecer ysystemd
considerará el servicio activo.
Si hay un proceso principal, todo el cgroup se elimina después de que sale el proceso principal. Con Type=simple
el proceso principal es el especificado por ExecStart=
. Debe Type=forking
usar PIDFile=
y pasar un PID de esta manera para especificar el proceso principal. Y cuando detienes un servicio, systemd
elimina todos los procesos que pertenecen al servicio. Por lo tanto, es importante incluir sólo procesos específicos del servicio en el cgroup. En su caso, es posible que desee excluir tmux
el servidor, incluso si se inició desde el servicio.
Hay herramientas/formas de mover procesos entre cgroups. O puede ejecutar un tmux
servidor independiente específico para el servicio.
¿Cómo systemd
sabe qué estado de salida usar?
Restart=on-failure
establece la dependencia del estado de salida del proceso principal. Se Type=forking
recomienda su uso PIDFile=
para systemd
saber qué estado de salida usar.
systemd
Sin embargo, es posible que pueda o no recuperar el estado de salida.
¿Quién recupera el estado de salida?
Después de que un niño sale, su padre puede recuperar el estado de salida (compáreseproceso zombie).
Independientemente de si el tmux
servidor es antiguo o nuevo, su comando no será hijo de systemd
a menos que quede huérfano, el kernel establece su padre en PID 1 (o algún otro) y el nuevo padre tiene la razón systemd
.
El comando que proporciona tmux new
hace que el tmux
servidor ejecute un shell, luego el shell se ejecuta dotnet
y espera a que salga, o exec
mientras dotnet
mantiene el tmux
servidor como padre. En cualquier caso dotnet
tiene un padre que no lo es systemd
.
Podría quedar huérfano dotnet
de esta manera: nohup dotnet … &
y luego dejar que dicho shell salga. También necesitará almacenar el PID y usarlo PIDFile=
en el archivo de configuración de la unidad, para que el servicio sepa qué proceso monitorear. Entonces podría funcionar.
Para ser claros: en mis pruebas nohup sleep 300 &
fue adoptado con éxito por systemd
quién pudo luego recuperar su estado de salida (después de que me ocupé de cgroups).
Pero como quieres usarlo tmux
en primer lugar, supongo que tu comando interactúa con la terminal. Entoncesnohup
No es la herramienta adecuada aquí. Dejar huérfano un proceso mientras se mantiene conectado al terminal puede ser complicado. Quiere dejarlo huérfano, pero no puede dejar que el shell tmux
simplemente salga, porque esto matará su panel (o lo dejará en un estado inactivo).
La nota Type=forking
se basa en la adopción por parte de systemd
. Se supone que el proceso de servicio principal se bifurca y sale. Luego systemd
adopta a su hijo. Sin embargo, dicho demonio no debería interactuar con ningún terminal.
Otro enfoque es dejar que el shell dentro del tmux
servidor . Después de salir, el servidor (como padre) conoce su estado de salida. En algunas circunstancias podemos consultar el servidor desde otro script y recuperar el estado de salida.exec
dotnet
tmux
O el shell activado por tmux new
puede almacenar el estado en un archivo, para que otro script pueda recuperarlo.
Debido a que lo que ejecuta ExecStart=
es un hijo de systemd
seguro, este es el mejor candidato para "otro script". Debería esperar hasta que pueda recuperar el estado de salida y luego usarlo como su propio estado de salida, para así systemd
obtenerlo. Tenga en cuenta que el servicio debería ser Type=simple
en este caso.
Alternativamente, puedes comenzar dotnet …
fuera de tmux
y luegoreptyr
desde el interior del tmux
servidor. De esta forma dotnet
puede ser hijo systemd
desde el principio, pueden aparecer problemas al intentar robar su tty.
Soluciones y ejemplos
reptyr
atmux
Este ejemplo ejecuta el script en formato tty2
. El guión se prepara tmux
y exec
s a dotnet
. Finalmente, un caparazón dentro tmux
intenta robarle a tty lo que es ahora dotnet
.
El archivo de servicio:
[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
Notas:
- Mis pruebas con
htop
en lugar dedotnet run
revelaron una condición de carrera (htop
cambia la configuración de su terminal,reptyr
puede interferir; por lo tanto,sleep 5
es una mala solución) y problemas con la compatibilidad con el mouse. - Es posible eliminar el
tmux
servidor del cgroup asociado con el servicio. Probablemente quieras hacer esto. Vea a continuación, dónde está/sys/fs/cgroup/systemd/
el código.
Sin tmux
?
La solución anterior se utiliza /dev/tty2
de todos modos. Si solo necesita tmux
proporcionar una terminal de control, considere cd /home/alpine_sour/rofdl && exec dotnet run
sin reptyr
, sin tmux
. Incluso sin el guión:
ExecStart=/bin/sh -c 'cd /home/alpine_sour/rofdl && exec dotnet run' rofdl
Este es el más simple.
tmux
Servidor separado
tmux
le permite ejecutar más de un servidor por usuario. Necesita -L
o -S
(ver man 1 tmux
) especificar un socket y luego atenerse a él. De esta manera su servicio puede ejecutar un tmux
servidor exclusivo. Ventajas:
- El servidor y todo lo que ejecuta dentro de él
tmux
pertenece al grupo c del servicio de forma predeterminada. - El servicio puede destruir el
tmux
servidor sin riesgo de que alguien (o cualquier cosa) pierda sus sesiones. Nadie más debería usar este servidor, a menos que quiera monitorear/interactuar con el servicio. Si alguien lo usa para otra cosa es su problema.
La capacidad de cerrar el tmux
servidor libremente le permite dejar huérfanos los procesos que se ejecutan en tmux
. Considere el siguiente ejemplo.
El archivo de servicio:
[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
Explicación:
El script principal cierra el
tmux
servidor exclusivo (si lo hay) y lo inicia de nuevo. Una vez iniciado el servidor, el script se cierra. El servicio permanece porque queda al menos un proceso en el cgroup, dicho servidor.El servidor genera un shell para procesar el script "interno". El guión comienza en
'
después-d
y termina en'
antes||
. Está todo entre comillas, pero las comillas cambian de comillas simples a dobles y viceversa varias veces. Es porque$tmux
y$service
necesita ser expandido por el shell que procesa el script principal, otras variables (por ejemplo$status
) no deben expandirse hasta que estén en el shell "interno", dentro detmux
. El siguiente recurso puede resultar útil:Expansión de parámetros (expansión de variables) y comillas dentro de comillas.El caparazón interior
tmux
se prepara para ignorarHUP
la señal.El shell registra su PID en el archivo pid que espera el servicio.
Luego se ejecuta
dotnet
y almacena su estado de salida (estrictamente, sicd
falla, será el estado de salida decd
).El shell mata al
tmux
servidor. Podríamos hacer estokill "$PPID"
también (vereste), pero si alguien hubiera eliminado el servidor y otro proceso obtuviera su PID, eliminaríamos un proceso incorrecto. Dirigirsetmux
es más seguro. Gracias a ello,trap
el caparazón sobrevive.Luego, el shell realiza un bucle hasta que su PPID es diferente al que era antes. No podemos confiar en comparar
$ppid
con$PPID
porque este último no es dinámico; recuperamos el PPID actual deps
.Ahora el shell sabe que tiene un nuevo padre, debería ser
systemd
. Sólo ahorasystemd
es posible recuperar el estado de salida del shell. El shell sale con el estado de salida exacto recuperadodotnet
anteriormente. De esta manerasystemd
obtiene el estado de salida a pesar de quedotnet
nunca fue su hijo.
Recuperar el estado de salida del tmux
servidor común
Su enfoque original utiliza un tmux
servidor común (predeterminado), solo manipula una sesión denominada rof
. En general, pueden existir o surgir otras sesiones, por lo que el servicio nunca debe cerrar todo el servidor. Hay pocos aspectos. Deberíamos:
- evitar
systemd
que se cierre eltmux
servidor, incluso si el servidor se inició desde dentro del servicio; - hacer que el proceso
systemd
se consideredotnet
parte del servicio, incluso si se iniciótmux
desde dentro del servicio; - recuperar el estado de salida de
dotnet
alguna manera.
El archivo de servicio:
[Unit]
Description=dotnet application
[Service]
Type=simple
ExecStart=/home/alpine_sour/scripts/rofdl
Restart=on-failure
User=root
[Install]
WantedBy=multi-user.target
Tenga en cuenta que es Type=simple
ahora, porque el script principal es el único hijo asegurado del que podemos recuperar el estado de salida. El script necesita averiguar el estado de salida dotnet …
e informarlo como propio.
/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
Explicación:
Si
tmux new-session
crea un servidor (porque no había ninguno), lo queremos en otro cgroup desde el principio para evitar la condición de carrera cuando algo más comienza a usar el servidor y aún no hemos cambiado su cgroup ysystemd
decidimos cerrar el servicio por cualquier motivo. . Intenté seguir adelantetmux new-session
ycgexec
fallé; por lo tanto, otro enfoque: una subcapa que cambia su propio cgroup (escribiendo en/sys/fs/cgroup/systemd/system.slice/tasks
) y luegoexec
s entmux new-session
.El shell interior
tmux
comienza habilitandoremain-on-exit
la opción para la sesión. Después de salir, el panel permanece y otro proceso (el script principal en nuestro caso) puede recuperar su estado de salida deltmux
servidor.Mientras tanto, el script principal recupera la identificación única del panel en el que se ejecuta el otro shell. Si alguien se conecta a la sesión o crea nuevos paneles y juega con ellos, el script principal aún podrá encontrar el panel correcto.
El shell interno
tmux
registra su PID en el cgroup asociado con el servicio escribiéndolo en/sys/fs/cgroup/systemd/system.slice/rofdl.service/tasks
.El caparazón interior
tmux
corredotnet …
. Después dedotnet
terminar, el shell sale.dotnet
El shell informa al servidor del estado de salida recuperadotmux
.Debido a
remain-on-exit on
, el panel permanece en un estado inactivo después de que sale el shell "interno".Mientras tanto, el shell principal realiza un bucle hasta que el panel esté muerto. Luego consulta al
tmux
servidor sobre el estado de salida relevante y lo informa como propio. De esta manerasystemd
se obtiene el estado de salida dedotnet
.
Notas:
De nuevo haycitas dentro de comillas.
En lugar de
dotnet run
eso podría serexec dotnet run
. La última forma es agradable:dotnet
reemplaza la capa interna, por lo que hay un proceso en lugar de dos. El problema es cuandodotnet
muere debido a una señal que no puede manejar. Resulta que#{pane_dead_status}
informará una cadena vacía si una señal cierra a la fuerza el proceso en el panel. Mantener un shell entredotnet
ytmux
previene esto: el shell transforma la información (veresta pregunta) y devuelve un número.Algunos shells (¿implementaciones?) ejecutan el último comando con implícito
exec
, algo que no queremos. Por eso uséexit "$?"
afterdotnet …
.Pero si se mata a la fuerza el propio caparazón,
#{pane_dead_status}
reaparece el problema del vacío. Como último recurso,status="${status:-255}"
convierte el estado vacío en255
(aunque no estoy seguro de255
que sea el mejor valor en tal caso).Hay una condición de carrera: cuando el script principal solicita
tmux
,#{pane_id}
es posible que no sea el panel correcto. Si alguien se conectó y jugó dentro de la sesión anteriortmux new-session
y posteriortmux display-message
, es posible que obtengamos un panel incorrecto. La ventana de tiempo es pequeña, pero aún así no es tan elegante como quería.Si
tmux new-session
pudiera imprimir#{pane_id}
en la consola comotmux display-message -p
puede, no debería haber ningún problema. Con-PF
él puedes mostrarlo dentro de la sesión. No hay soporte para-p
.Es posible que desee algo de lógica en caso de que el
tmux
servidor muera.
Recuperar el estado de salida mediante un archivo
El ejemplo anterior se puede modificar, por lo que no es remain-on-exit on
necesario #{pane_id}
(se evita la condición de carrera, al menos la descrita).
El archivo de servicio del ejemplo anterior permanece.
/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
El mecanismo es bastante sencillo: el shell principal elimina el archivo de estado antiguo (si lo hay), lo activa tmux
y realiza un bucle hasta que el archivo reaparece. El shell "interno" escribe el estado de salida dotnet
en el archivo, cuando está listo.
Notas:
- ¿Qué pasa si se mata la capa interior? ¿Qué pasa si no se puede crear el archivo? Es relativamente fácil llegar a una situación en la que el script principal no puede salir del bucle.
- Escribir en un archivo temporal y luego cambiarle el nombre es una buena práctica. Si lo hiciéramos
echo "$?" > "$statf"
, el archivo se crearía vacío y luego se escribiría en él. Esto podría llevar a una situación en la que el script principal lea una cadena vacía como estado. En general, el receptor puede recibir datos incompletos: leyendo hasta el EOF mientras el remitente está en mitad de la escritura y el archivo aún está a punto de crecer. Cambiar el nombre hace que el archivo correcto con el contenido correcto aparezca instantáneamente.
Notas finales
- Si no puede prescindir de
tmux
, la solución con untmux
servidor independiente parece más sólida. Esto es lo que eldocumentacióndice sobre
Restart=
:En este contexto, una salida limpia significa un código de salida de
0
, o una de las señalesSIGHUP
,SIGINT
,SIGTERM
oSIGPIPE
, y […]La nota
$?
en un caparazón es solo un número. De nuevo:este enlace. Si susdotnet
salidas debido a una señal y el reinicio dependen de una salida (no) limpia, las soluciones dondesystemd
se recupera el código de salida directamentedotnet
pueden comportarse de manera diferente a las soluciones dondesystemd
se recupera el estado de salida desde un shell intermediario. InvestigaSuccessExitStatus=
, puede ser útil.
Respuesta2
Tal vez puedas usarlo RestartForceExitStatus=
en el archivo de servicio.
Toma una lista de definiciones de estado de salida que, cuando las devuelve el proceso de servicio principal, forzarán reinicios automáticos del servicio, independientemente de la configuración de reinicio configurada con Restart=. El formato del argumento es similar a RestartPreventExitStatus=.
https://www.freedesktop.org/software/systemd/man/systemd.service.html