Notas preliminares

Notas preliminares

Eu tenho um programa dotnet em execução dentro do bash no tmux que ocasionalmente falha com um código de erro diferente de zero. Estou tentando usar um arquivo de serviço systemd para iniciar programaticamente meu programa dotnet dentro do tmux.

Aqui está o arquivo de serviço:

[Unit] 
Description=dotnet application

[Service] 
Type=forking 
ExecStart=/home/alpine_sour/scripts/rofdl
Restart=always 
User=root

[Install]
WantedBy=multi-user.target

Aqui está o 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"

Agora, quando inicio o serviço, o systemd escolhe o PID principal como o servidor tmux, o que presumo ser porque foi o primeiro comando executado. Portanto, quando meu programa na janela tmux sai com QUALQUER código de erro E não há mais janelas, o servidor tmux sai com um código de erro de sucesso, fazendo com que o systemd não reinicie. Mesmo se eu reiniciasse = sempre, o servidor tmux só reiniciaria se meu programa falhasse E não houvesse outras janelas.

  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

Então, estou me perguntando como faria com que o systemd rastreasse o nível mais baixo da bifurcação do processo, em vez do servidor tmux de nível mais alto. Preciso de uma maneira de dizer ao systemd para rastrear o processo filho do servidor tmux em vez do próprio servidor e reiniciar de acordo.

Responder1

Notas preliminares

  • Esta resposta é baseada em experimentos no Debian 9.
  • Presumo que seu serviço seja um serviço do sistema (em /etc/systemd/system).
  • O que você postou próximo ao final do corpo da pergunta parece umexcertode systemctl status …. Não diz nada sobre cgroups. Esta resposta pressupõeGrupos de controleestão envolvidos. Eu acho systemdque os exige, então eles devem ser.
  • O comando em si pode ser executado em loop, até ser bem-sucedido:

    cd /home/alpine_sour/rofdl && while ! dotnet run; do :; done
    

    mas entendo que você quer uma systemdsolução.


Problemas

Primeiro por favor leiacomo tmuxfunciona. Compreender qual processo é de quem é o filho será muito útil.

Quais processos pertencem ao serviço

No seu caso original, o serviço será considerado inativo (e pronto para reiniciar, se aplicável) após todos os processos da saída do cgroup.

Seu script tenta encerrar a tmuxsessão antiga, não o tmuxservidor antigo. Então tmux new(equivalente a tmux new-session) inicia um servidor ou usa o antigo.

  • Se usar o antigo, nem o servidor nem o seu comando ( dotnet …) serão descendentes do script. Estes processos não pertencerão ao cgroup associado ao serviço. Após a saída do script, systemdconsiderará o serviço inativo.

  • Se iniciar um novo tmuxservidor, o servidor e o comando serão atribuídos ao cgroup associado ao serviço. Então nosso comando pode terminar, mas se houver outras sessões/janelas (criadas posteriormente) dentro do servidor, o servidor poderá permanecer e systemdconsiderará o serviço ativo.

Se houver um processo principal, todo o cgroup será eliminado após a saída do processo principal. Com Type=simpleo processo principal é aquele especificado por ExecStart=. Com Type=forkingvocê precisa usar PIDFile=e passar um PID desta forma para especificar o processo principal. E quando você interrompe um serviço, systemdmata todos os processos que pertencem ao serviço. Portanto é importante incluir apenas processos específicos do serviço no cgroup. No seu caso, você pode querer excluir tmuxo servidor, mesmo que ele tenha sido iniciado dentro do serviço.

Existem ferramentas/maneiras de mover processos entre cgroups. Ou você pode executar um tmuxservidor separado específico para o serviço.

Como systemdsabe qual status de saída usar

Restart=on-failuredefine a dependência do status de saída do processo principal. Com Type=forkingele é aconselhável usar PIDFile=para systemdsaber qual status de saída usar.

systemdpode ou não ser capaz de recuperar o status de saída.

Quem recupera o status de saída

Depois que um filho sai, seu pai pode recuperar o status de saída (compareprocesso zumbi).

Independentemente de o tmuxservidor ser antigo ou novo, seu comando não será filho, systemda menos que fique órfão, o kernel define seu pai como PID 1 (ou algum outro) e o novo pai é o certo systemd.

O comando que você fornece tmux newfaz com que o tmuxservidor execute um shell, então o shell é executado dotnete espera que ele saia, ou execenquanto dotnetmantém o tmuxservidor como pai. Em qualquer caso, dotnettem um pai que não é systemd.

Você poderia ser órfão dotnetassim: nohup dotnet … &e deixar o referido shell sair. Você também precisaria armazenar o PID, usar PIDFile=no arquivo de configuração da unidade, para que o serviço saiba qual processo monitorar. Então pode funcionar.

Para ficar claro: em meus testes nohup sleep 300 &foi adotado com sucesso por systemdquem conseguiu então recuperar seu status de saída (depois que cuidei do cgroups).

Mas como você deseja usar tmuxem primeiro lugar, acho que seu comando interage com o terminal. Entãonohupnão é a ferramenta certa aqui. Deixar um processo órfão e mantê-lo conectado ao terminal pode ser complicado. Você deseja torná-lo órfão, mas não pode deixar o shell tmuxsimplesmente sair, porque isso matará seu painel (ou o deixará em um estado morto).

A nota Type=forkingdepende da adoção por systemd. O processo de serviço principal deve bifurcar e sair. Então systemdadota seu filho. Porém, esse daemon não deve interagir com nenhum terminal.

Outra abordagem é permitir que o shell dentro do tmuxservidor . Depois de sair, o servidor (como pai) conhece seu status de saída. Em algumas circunstâncias podemos consultar o servidor a partir de outro script e recuperar o status de saída.execdotnettmux

Ou o shell acionado por tmux newpode armazenar o status em um arquivo, para que possa ser recuperado por outro script.

Porque o que você usa ExecStart=é filho com systemdcerteza, este é o melhor candidato para "outro script". Ele deve esperar até poder recuperar o status de saída e, em seguida, usá-lo como seu próprio status de saída, para systemdobtê-lo. Observe que o serviço deve ser Type=simpleneste caso.

Alternativamente, você pode começar dotnet …fora de tmux, entãoreptyrde dentro do tmuxservidor. Desta forma dotnetpode ser uma criança systemddesde o início, podem surgir problemas quando você tenta roubar seu tty.


Soluções e exemplos

reptyrparatmux

Este exemplo executa o script em tty2. O script se prepara tmuxe execé para dotnet. Finalmente, um shell tmuxtenta roubar tty do que é now dotnet.

O arquivo de serviço:

[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:

  • Meus testes revelaram, htopem vez de, dotnet runuma condição de corrida ( htopaltera as configurações de seu terminal, reptyrpode interferir; portanto, sleep 5é uma solução alternativa ruim) e problemas com suporte ao mouse.
  • É possível remover o tmuxservidor do cgroup associado ao serviço. Você provavelmente quer fazer isso. Veja abaixo, onde está /sys/fs/cgroup/systemd/no código.

Sem tmux?

A solução acima é usada /dev/tty2de qualquer maneira. Se você precisar tmuxapenas fornecer um terminal de controle, considere cd /home/alpine_sour/rofdl && exec dotnet runsem reptyr, sem tmux. Mesmo sem o roteiro:

ExecStart=/bin/sh -c 'cd /home/alpine_sour/rofdl && exec dotnet run' rofdl

Este é o mais simples.

tmuxServidor separado

tmuxpermite que você execute mais de um servidor por usuário. Você precisa -Lou -S(veja man 1 tmux) especificar um soquete e segui-lo. Desta forma seu serviço pode rodar um tmuxservidor exclusivo. Vantagens:

  • O servidor e tudo o que você executa nele tmuxpertence ao cgroup do serviço por padrão.
  • O serviço pode destruir o tmuxservidor sem risco de que alguém (ou qualquer coisa) perca suas sessões. Ninguém mais deve usar este servidor, a menos que queira monitorar/interagir com o serviço. Se alguém usar para qualquer outra coisa, o problema é deles.

A capacidade de encerrar o tmuxservidor livremente permite que processos órfãos sejam executados em arquivos tmux. Considere o seguinte exemplo.

O arquivo de serviço:

[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

Explicação:

  1. O script principal mata o tmuxservidor exclusivo (se houver) e o inicia novamente. Depois que o servidor for iniciado, o script será encerrado. O serviço permanece porque resta pelo menos um processo no cgroup, o referido servidor.

  2. O servidor gera um shell para processar o script "interno". O script começa em 'after -de termina em 'before ||. Está tudo citado, mas as citações mudam de aspas simples para aspas duplas e voltam algumas vezes. É porque $tmuxe $serviceprecisam ser expandidas pelo shell que processa o script principal, outras variáveis ​​(por exemplo, $status) não devem ser expandidas até no shell "interno", dentro de tmux. O seguinte recurso pode ser útil:Expansão de parâmetros (expansão de variável) e cotações entre aspas.

  3. O shell interno tmuxse prepara para ignorar HUPo sinal.

  4. O shell registra seu PID no pidfile que o serviço espera.

  5. Em seguida, ele é executado dotnete armazena seu status de saída (estritamente, se cdfalhar, será o status de saída de cd).

  6. O shell mata o tmuxservidor. Poderíamos fazer isso kill "$PPID"também (vejaesse), mas se alguém tivesse encerrado o servidor e outro processo obtivesse seu PID, eliminaríamos um processo errado. O endereçamento tmuxé mais seguro. Por causa do trapshell sobrevive.

  7. Em seguida, o shell faz um loop até que seu PPID seja diferente do que era antes. Não podemos confiar na comparação $ppidporque $PPIDesta não é dinâmica; recuperamos o PPID atual de ps.

  8. Agora que o shell sabe que tem um novo pai, deveria ser systemd. Só agora systemdé possível recuperar o status de saída do shell. O shell sai com o status de saída exato recuperado dotnetanteriormente. Desta forma systemdobtém o status de saída apesar de dotnetnunca ter sido seu filho.

Recuperando o status de saída do tmuxservidor comum

Sua abordagem original usa um tmuxservidor comum (padrão), apenas manipula uma sessão chamada rof. Em geral podem existir ou surgir outras sessões, portanto o serviço nunca deve matar todo o servidor. Existem poucos aspectos. Deveríamos:

  • evitar systemdmatar o tmuxservidor, mesmo que o servidor tenha sido iniciado dentro do serviço;
  • fazer com que o processo systemdseja considerado dotnetparte do serviço, mesmo que tenha sido iniciado e tmuxnão iniciado dentro do serviço;
  • recuperar o status de saída de dotnetalguma forma.

O arquivo de serviço:

[Unit]
Description=dotnet application

[Service]
Type=simple
ExecStart=/home/alpine_sour/scripts/rofdl
Restart=on-failure
User=root

[Install]
WantedBy=multi-user.target

Observe que é Type=simpleagora, porque o script principal é o único filho garantido do qual podemos recuperar o status de saída. O script precisa descobrir o status de saída dotnet …e relatá-lo como se fosse seu.

/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

Explicação:

  1. Se tmux new-sessioncriar um servidor (porque não havia nenhum), nós o queremos em outro cgroup desde o início para evitar a condição de corrida quando outra coisa começar a usar o servidor e ainda não alteramos seu cgroup e systemddecidirmos encerrar o serviço por qualquer motivo . Tentei correr tmux new-sessione cgexecfalhei; portanto, outra abordagem: um subshell que altera seu próprio cgroup (gravando em /sys/fs/cgroup/systemd/system.slice/tasks) e depois execs em tmux new-session.

  2. O shell interno tmuxcomeça habilitando remain-on-exita opção para a sessão. Depois de sair, o painel permanece e outro processo (o script principal no nosso caso) pode recuperar seu status de saída do tmuxservidor.

  3. Enquanto isso, o script principal recupera o ID exclusivo do painel em que o outro shell é executado. Se alguém se conectar à sessão ou criar novos painéis e brincar com eles, o script principal ainda será capaz de encontrar o painel correto.

  4. O shell interno tmuxregistra seu PID no cgroup associado ao serviço, gravando-o em /sys/fs/cgroup/systemd/system.slice/rofdl.service/tasks.

  5. O shell interno tmuxé executado dotnet …. Depois de dotnetterminar, o shell sai. O status de saída recuperado dotneté relatado pelo shell ao tmuxservidor.

  6. Por causa disso remain-on-exit on, o painel permanece em um estado morto após a saída do shell "interno".

  7. Enquanto isso, o shell principal faz um loop até que o painel esteja morto. Em seguida, ele consulta o tmuxservidor quanto ao status de saída relevante e o reporta como seu. Dessa forma, systemdobtém o status de saída de dotnet.

Notas:

  • Novamente existemcitações dentro de citações.

  • Em vez dotnet rundisso, poderia ser exec dotnet run. A última forma é legal: dotnetsubstitui o shell interno, então há um processo em vez de dois. O problema é quando dotneté eliminado por um sinal que não consegue controlar. Acontece que #{pane_dead_status}reportará uma string vazia se o processo no painel for interrompido à força por um sinal. Manter um shell entre dotnete tmuxevita isso: o shell transforma informações (vejaessa questão) e retorna um número.

    Alguns shells (implementações?) Executam o último comando com implicit exec, algo que não queremos. Por isso usei exit "$?"depois dotnet ….

    Mas se o próprio shell for eliminado à força, o problema com o vazio #{pane_dead_status}reaparece. Como último recurso, status="${status:-255}"converte o status vazio para 255(embora não tenha certeza 255se é o melhor valor nesse caso).

  • Há uma condição de corrida: quando o script principal consulta tmux, #{pane_id}pode não ser o painel correto. Se alguém se conectar e jogar dentro da sessão antes tmux new-sessione depois tmux display-message, poderemos obter um painel errado. A janela de tempo é pequena, mas ainda não é tão elegante quanto eu queria.

    Se tmux new-sessionpudesse imprimir #{pane_id}no console como tmux display-message -ppode, não deverá haver problema. Com -PFele você pode mostrá-lo dentro da sessão. Não há suporte para -p.

  • Você pode querer alguma lógica caso o tmuxservidor seja morto.

Recuperando o status de saída via arquivo

O exemplo acima pode ser modificado, portanto remain-on-exit onnão é necessário, #{pane_id}não é necessário (condição de corrida evitada, pelo menos a descrita).

O arquivo de serviço do exemplo 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

O mecanismo é bastante simples: o shell principal remove o arquivo de status antigo (se houver), aciona tmuxe faz loop até que o arquivo reapareça. O shell "interno" grava o status de saída dotnetno arquivo, quando estiver pronto.

Notas:

  • E se a casca interna for morta? E se o arquivo não puder ser criado? É relativamente fácil chegar a uma situação em que o script principal não consegue sair do loop.
  • Escrever em um arquivo temporário e renomeá-lo é uma boa prática. Se o fizéssemos echo "$?" > "$statf", o arquivo seria criado vazio e depois gravado. Isto pode levar a uma situação em que o script principal lê uma string vazia como status. Em geral, o receptor pode obter dados incompletos: lendo até EOF enquanto o remetente está no meio da gravação e o arquivo ainda está prestes a crescer. A renomeação faz com que o arquivo certo com o conteúdo certo apareça instantaneamente.

Notas finais

  • Se você não puder ficar sem ele tmux, a solução com um tmuxservidor separado parece mais robusta.
  • Isto é o quedocumentaçãodiz sobre Restart=:

    Neste contexto, uma saída limpa significa um código de saída de , 0ou um dos sinais SIGHUP,, SIGINTou , e […]SIGTERMSIGPIPE

    Nota $?em um shell é apenas um número. De novo:esse link. Se suas dotnetsaídas devido a um sinal e a reinicialização dependerem de uma saída (não) limpa, as soluções das quais systemdrecupera o código de saída diretamente dotnetpodem se comportar de maneira diferente das soluções nas quais systemdrecupera o status de saída de um shell intermediário. Pesquise SuccessExitStatus=, pode ser útil.

Responder2

Talvez você possa usar RestartForceExitStatus=no arquivo de serviço

Obtém uma lista de definições de status de saída que, quando retornadas pelo processo de serviço principal, forçarão reinicializações automáticas do serviço, independentemente da configuração de reinicialização configurada com Restart=. O formato do argumento é semelhante a RestartPreventExitStatus=.

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

informação relacionada