Notas preliminares

Notas preliminares

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 que systemdlos 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 systemdsolución.


Problemas

primero por favor leacomo tmuxfunciona. 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 tmuxsesión anterior, no el tmuxservidor 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, systemdel servicio se considerará inactivo.

  • Si inicia un nuevo tmuxservidor, 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 y systemdconsiderará el servicio activo.

Si hay un proceso principal, todo el cgroup se elimina después de que sale el proceso principal. Con Type=simpleel proceso principal es el especificado por ExecStart=. Debe Type=forkingusar PIDFile=y pasar un PID de esta manera para especificar el proceso principal. Y cuando detienes un servicio, systemdelimina 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 tmuxel servidor, incluso si se inició desde el servicio.

Hay herramientas/formas de mover procesos entre cgroups. O puede ejecutar un tmuxservidor independiente específico para el servicio.

¿Cómo systemdsabe qué estado de salida usar?

Restart=on-failureestablece la dependencia del estado de salida del proceso principal. Se Type=forkingrecomienda su uso PIDFile=para systemdsaber qué estado de salida usar.

systemdSin 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 tmuxservidor es antiguo o nuevo, su comando no será hijo de systemda 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 newhace que el tmuxservidor ejecute un shell, luego el shell se ejecuta dotnety espera a que salga, o execmientras dotnetmantiene el tmuxservidor como padre. En cualquier caso dotnettiene un padre que no lo es systemd.

Podría quedar huérfano dotnetde 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 systemdquién pudo luego recuperar su estado de salida (después de que me ocupé de cgroups).

Pero como quieres usarlo tmuxen primer lugar, supongo que tu comando interactúa con la terminal. EntoncesnohupNo 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 tmuxsimplemente salga, porque esto matará su panel (o lo dejará en un estado inactivo).

La nota Type=forkingse basa en la adopción por parte de systemd. Se supone que el proceso de servicio principal se bifurca y sale. Luego systemdadopta a su hijo. Sin embargo, dicho demonio no debería interactuar con ningún terminal.

Otro enfoque es dejar que el shell dentro del tmuxservidor . 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.execdotnettmux

O el shell activado por tmux newpuede almacenar el estado en un archivo, para que otro script pueda recuperarlo.

Debido a que lo que ejecuta ExecStart=es un hijo de systemdseguro, 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í systemdobtenerlo. Tenga en cuenta que el servicio debería ser Type=simpleen este caso.

Alternativamente, puedes comenzar dotnet …fuera de tmuxy luegoreptyrdesde el interior del tmuxservidor. De esta forma dotnetpuede ser hijo systemddesde el principio, pueden aparecer problemas al intentar robar su tty.


Soluciones y ejemplos

reptyratmux

Este ejemplo ejecuta el script en formato tty2. El guión se prepara tmuxy execs a dotnet. Finalmente, un caparazón dentro tmuxintenta 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 htopen lugar de dotnet runrevelaron una condición de carrera ( htopcambia la configuración de su terminal, reptyrpuede interferir; por lo tanto, sleep 5es una mala solución) y problemas con la compatibilidad con el mouse.
  • Es posible eliminar el tmuxservidor 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/tty2de todos modos. Si solo necesita tmuxproporcionar una terminal de control, considere cd /home/alpine_sour/rofdl && exec dotnet runsin 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.

tmuxServidor separado

tmuxle permite ejecutar más de un servidor por usuario. Necesita -Lo -S(ver man 1 tmux) especificar un socket y luego atenerse a él. De esta manera su servicio puede ejecutar un tmuxservidor exclusivo. Ventajas:

  • El servidor y todo lo que ejecuta dentro de él tmuxpertenece al grupo c del servicio de forma predeterminada.
  • El servicio puede destruir el tmuxservidor 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 tmuxservidor 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:

  1. El script principal cierra el tmuxservidor 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.

  2. El servidor genera un shell para procesar el script "interno". El guión comienza en 'después -dy termina en 'antes ||. Está todo entre comillas, pero las comillas cambian de comillas simples a dobles y viceversa varias veces. Es porque $tmuxy $servicenecesita 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 de tmux. El siguiente recurso puede resultar útil:Expansión de parámetros (expansión de variables) y comillas dentro de comillas.

  3. El caparazón interior tmuxse prepara para ignorar HUPla señal.

  4. El shell registra su PID en el archivo pid que espera el servicio.

  5. Luego se ejecuta dotnety almacena su estado de salida (estrictamente, si cdfalla, será el estado de salida de cd).

  6. El shell mata al tmuxservidor. Podríamos hacer esto kill "$PPID"también (vereste), pero si alguien hubiera eliminado el servidor y otro proceso obtuviera su PID, eliminaríamos un proceso incorrecto. Dirigirse tmuxes más seguro. Gracias a ello, trapel caparazón sobrevive.

  7. Luego, el shell realiza un bucle hasta que su PPID es diferente al que era antes. No podemos confiar en comparar $ppidcon $PPIDporque este último no es dinámico; recuperamos el PPID actual de ps.

  8. Ahora el shell sabe que tiene un nuevo padre, debería ser systemd. Sólo ahora systemdes posible recuperar el estado de salida del shell. El shell sale con el estado de salida exacto recuperado dotnetanteriormente. De esta manera systemdobtiene el estado de salida a pesar de que dotnetnunca fue su hijo.

Recuperar el estado de salida del tmuxservidor común

Su enfoque original utiliza un tmuxservidor 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 systemdque se cierre el tmuxservidor, incluso si el servidor se inició desde dentro del servicio;
  • hacer que el proceso systemdse considere dotnetparte del servicio, incluso si se inició tmuxdesde dentro del servicio;
  • recuperar el estado de salida de dotnetalguna 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=simpleahora, 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:

  1. Si tmux new-sessioncrea 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 y systemddecidimos cerrar el servicio por cualquier motivo. . Intenté seguir adelante tmux new-sessiony cgexecfallé; por lo tanto, otro enfoque: una subcapa que cambia su propio cgroup (escribiendo en /sys/fs/cgroup/systemd/system.slice/tasks) y luego execs en tmux new-session.

  2. El shell interior tmuxcomienza habilitando remain-on-exitla 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 del tmuxservidor.

  3. 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.

  4. El shell interno tmuxregistra su PID en el cgroup asociado con el servicio escribiéndolo en /sys/fs/cgroup/systemd/system.slice/rofdl.service/tasks.

  5. El caparazón interior tmuxcorre dotnet …. Después de dotnetterminar, el shell sale. dotnetEl shell informa al servidor del estado de salida recuperado tmux.

  6. Debido a remain-on-exit on, el panel permanece en un estado inactivo después de que sale el shell "interno".

  7. Mientras tanto, el shell principal realiza un bucle hasta que el panel esté muerto. Luego consulta al tmuxservidor sobre el estado de salida relevante y lo informa como propio. De esta manera systemdse obtiene el estado de salida de dotnet.

Notas:

  • De nuevo haycitas dentro de comillas.

  • En lugar de dotnet runeso podría ser exec dotnet run. La última forma es agradable: dotnetreemplaza la capa interna, por lo que hay un proceso en lugar de dos. El problema es cuando dotnetmuere 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 entre dotnety tmuxpreviene 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 "$?"after dotnet ….

    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 en 255(aunque no estoy seguro de 255que 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 anterior tmux new-sessiony posterior tmux 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-sessionpudiera imprimir #{pane_id}en la consola como tmux display-message -ppuede, 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 tmuxservidor muera.

Recuperar el estado de salida mediante un archivo

El ejemplo anterior se puede modificar, por lo que no es remain-on-exit onnecesario #{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 tmuxy realiza un bucle hasta que el archivo reaparece. El shell "interno" escribe el estado de salida dotneten 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 un tmuxservidor 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ñales SIGHUP, SIGINT, SIGTERMo SIGPIPE, y […]

    La nota $?en un caparazón es solo un número. De nuevo:este enlace. Si sus dotnetsalidas debido a una señal y el reinicio dependen de una salida (no) limpia, las soluciones donde systemdse recupera el código de salida directamente dotnetpueden comportarse de manera diferente a las soluciones donde systemdse recupera el estado de salida desde un shell intermediario. Investiga SuccessExitStatus=, 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

información relacionada