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 achosystemd
que 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
systemd
solução.
Problemas
Primeiro por favor leiacomo tmux
funciona. 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 tmux
sessão antiga, não o tmux
servidor 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,systemd
considerará o serviço inativo.Se iniciar um novo
tmux
servidor, 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 esystemd
considerará o serviço ativo.
Se houver um processo principal, todo o cgroup será eliminado após a saída do processo principal. Com Type=simple
o processo principal é aquele especificado por ExecStart=
. Com Type=forking
você precisa usar PIDFile=
e passar um PID desta forma para especificar o processo principal. E quando você interrompe um serviço, systemd
mata 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 tmux
o servidor, mesmo que ele tenha sido iniciado dentro do serviço.
Existem ferramentas/maneiras de mover processos entre cgroups. Ou você pode executar um tmux
servidor separado específico para o serviço.
Como systemd
sabe qual status de saída usar
Restart=on-failure
define a dependência do status de saída do processo principal. Com Type=forking
ele é aconselhável usar PIDFile=
para systemd
saber qual status de saída usar.
systemd
pode 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 tmux
servidor ser antigo ou novo, seu comando não será filho, systemd
a 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 new
faz com que o tmux
servidor execute um shell, então o shell é executado dotnet
e espera que ele saia, ou exec
enquanto dotnet
mantém o tmux
servidor como pai. Em qualquer caso, dotnet
tem um pai que não é systemd
.
Você poderia ser órfão dotnet
assim: 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 systemd
quem conseguiu então recuperar seu status de saída (depois que cuidei do cgroups).
Mas como você deseja usar tmux
em primeiro lugar, acho que seu comando interage com o terminal. Entãonohup
nã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 tmux
simplesmente sair, porque isso matará seu painel (ou o deixará em um estado morto).
A nota Type=forking
depende da adoção por systemd
. O processo de serviço principal deve bifurcar e sair. Então systemd
adota seu filho. Porém, esse daemon não deve interagir com nenhum terminal.
Outra abordagem é permitir que o shell dentro do tmux
servidor . 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.exec
dotnet
tmux
Ou o shell acionado por tmux new
pode armazenar o status em um arquivo, para que possa ser recuperado por outro script.
Porque o que você usa ExecStart=
é filho com systemd
certeza, 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 systemd
obtê-lo. Observe que o serviço deve ser Type=simple
neste caso.
Alternativamente, você pode começar dotnet …
fora de tmux
, entãoreptyr
de dentro do tmux
servidor. Desta forma dotnet
pode ser uma criança systemd
desde o início, podem surgir problemas quando você tenta roubar seu tty.
Soluções e exemplos
reptyr
paratmux
Este exemplo executa o script em tty2
. O script se prepara tmux
e exec
é para dotnet
. Finalmente, um shell tmux
tenta 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,
htop
em vez de,dotnet run
uma condição de corrida (htop
altera as configurações de seu terminal,reptyr
pode interferir; portanto,sleep 5
é uma solução alternativa ruim) e problemas com suporte ao mouse. - É possível remover o
tmux
servidor 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/tty2
de qualquer maneira. Se você precisar tmux
apenas fornecer um terminal de controle, considere cd /home/alpine_sour/rofdl && exec dotnet run
sem reptyr
, sem tmux
. Mesmo sem o roteiro:
ExecStart=/bin/sh -c 'cd /home/alpine_sour/rofdl && exec dotnet run' rofdl
Este é o mais simples.
tmux
Servidor separado
tmux
permite que você execute mais de um servidor por usuário. Você precisa -L
ou -S
(veja man 1 tmux
) especificar um soquete e segui-lo. Desta forma seu serviço pode rodar um tmux
servidor exclusivo. Vantagens:
- O servidor e tudo o que você executa nele
tmux
pertence ao cgroup do serviço por padrão. - O serviço pode destruir o
tmux
servidor 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 tmux
servidor 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:
O script principal mata o
tmux
servidor 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.O servidor gera um shell para processar o script "interno". O script começa em
'
after-d
e termina em'
before||
. Está tudo citado, mas as citações mudam de aspas simples para aspas duplas e voltam algumas vezes. É porque$tmux
e$service
precisam 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 detmux
. O seguinte recurso pode ser útil:Expansão de parâmetros (expansão de variável) e cotações entre aspas.O shell interno
tmux
se prepara para ignorarHUP
o sinal.O shell registra seu PID no pidfile que o serviço espera.
Em seguida, ele é executado
dotnet
e armazena seu status de saída (estritamente, secd
falhar, será o status de saída decd
).O shell mata o
tmux
servidor. Poderíamos fazer issokill "$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çamentotmux
é mais seguro. Por causa dotrap
shell sobrevive.Em seguida, o shell faz um loop até que seu PPID seja diferente do que era antes. Não podemos confiar na comparação
$ppid
porque$PPID
esta não é dinâmica; recuperamos o PPID atual deps
.Agora que o shell sabe que tem um novo pai, deveria ser
systemd
. Só agorasystemd
é possível recuperar o status de saída do shell. O shell sai com o status de saída exato recuperadodotnet
anteriormente. Desta formasystemd
obtém o status de saída apesar dedotnet
nunca ter sido seu filho.
Recuperando o status de saída do tmux
servidor comum
Sua abordagem original usa um tmux
servidor 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
systemd
matar otmux
servidor, mesmo que o servidor tenha sido iniciado dentro do serviço; - fazer com que o processo
systemd
seja consideradodotnet
parte do serviço, mesmo que tenha sido iniciado etmux
não iniciado dentro do serviço; - recuperar o status de saída de
dotnet
alguma 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=simple
agora, 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:
Se
tmux new-session
criar 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 esystemd
decidirmos encerrar o serviço por qualquer motivo . Tentei corrertmux new-session
ecgexec
falhei; portanto, outra abordagem: um subshell que altera seu próprio cgroup (gravando em/sys/fs/cgroup/systemd/system.slice/tasks
) e depoisexec
s emtmux new-session
.O shell interno
tmux
começa habilitandoremain-on-exit
a 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 dotmux
servidor.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.
O shell interno
tmux
registra seu PID no cgroup associado ao serviço, gravando-o em/sys/fs/cgroup/systemd/system.slice/rofdl.service/tasks
.O shell interno
tmux
é executadodotnet …
. Depois dedotnet
terminar, o shell sai. O status de saída recuperadodotnet
é relatado pelo shell aotmux
servidor.Por causa disso
remain-on-exit on
, o painel permanece em um estado morto após a saída do shell "interno".Enquanto isso, o shell principal faz um loop até que o painel esteja morto. Em seguida, ele consulta o
tmux
servidor quanto ao status de saída relevante e o reporta como seu. Dessa forma,systemd
obtém o status de saída dedotnet
.
Notas:
Novamente existemcitações dentro de citações.
Em vez
dotnet run
disso, poderia serexec dotnet run
. A última forma é legal:dotnet
substitui o shell interno, então há um processo em vez de dois. O problema é quandodotnet
é 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 entredotnet
etmux
evita 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 useiexit "$?"
depoisdotnet …
.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 para255
(embora não tenha certeza255
se é 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 antestmux new-session
e depoistmux 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-session
pudesse imprimir#{pane_id}
no console comotmux display-message -p
pode, não deverá haver problema. Com-PF
ele você pode mostrá-lo dentro da sessão. Não há suporte para-p
.Você pode querer alguma lógica caso o
tmux
servidor seja morto.
Recuperando o status de saída via arquivo
O exemplo acima pode ser modificado, portanto remain-on-exit on
nã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 tmux
e faz loop até que o arquivo reapareça. O shell "interno" grava o status de saída dotnet
no 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 umtmux
servidor separado parece mais robusta. Isto é o quedocumentaçãodiz sobre
Restart=
:Neste contexto, uma saída limpa significa um código de saída de ,
0
ou um dos sinaisSIGHUP
,,SIGINT
ou , e […]SIGTERM
SIGPIPE
Nota
$?
em um shell é apenas um número. De novo:esse link. Se suasdotnet
saídas devido a um sinal e a reinicialização dependerem de uma saída (não) limpa, as soluções das quaissystemd
recupera o código de saída diretamentedotnet
podem se comportar de maneira diferente das soluções nas quaissystemd
recupera o status de saída de um shell intermediário. PesquiseSuccessExitStatus=
, 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