O script morre quando o processo pai é finalizado

1

Eu tenho um serviço .NET Core rodando em um Debian 9, vamos chamá-lo de MyService. Em algum momento, este serviço está executando um script bash update.sh usando Process.Start() com ShellExecute=true .

Este script basicamente roda apt-get update; apt-get upgrade .

Durante a atualização do pacote, o processo do MyService é encerrado: o script de atualização também é encerrado e o apt-get upgrade também é eliminado, deixando pacotes inconsistentes que devem ser corrigidos manualmente.

O que eu quero é que update.sh NÃO seja encerrado quando o MyService for encerrado.

Eu tentei dividir update.sh em 2 partes, a primeira executando a segunda de maneiras diferentes; Eu tentei iniciar update2.sh com setsid e nohup , mas sempre recebo o mesmo resultado. Tentei executar o script update2.sh em um novo shell bash com /bin/bash /c "update2.sh" , mesmo resultado.

Como faço para executar um script iniciado a partir de um binário e separar completamente do processo binário, para que eu possa matar o binário enquanto o script continua rodando?

Aqui está o meu ambiente. O MyService é um binário que funciona como um serviço. update.sh é iniciado pelo MyService.

Código do .NET Core para iniciar o script de shell, dentro do binário do MyService:

var process = new Process();
process.EnableRaisingEvents = true; // to avoid [defunct] sh processes
process.StartInfo.FileName = "/opt/myservice/update.sh";
process.StartInfo.Arguments = "";
process.StartInfo.UseShellExecute = true;
process.StartInfo.CreateNoWindow = true;
process.Start();
process.WaitForExit(10000);
if (process.HasExited)
{
  Console.WriteLine("Exit code: " + process.ExitCode);
}
else
{
  Console.WriteLine("Child process still running after 10 seconds");
}

update.sh:

nohup /opt/myservice/update2.sh > /opt/myservice/update.log &
systemctl stop MyService

update2.sh:

apt-get update >> /opt/myservice/update.log
apt-get -y install --only-upgrade myservice-1.0 >> /opt/myservice/update.log

update2.sh nunca é executado porque é finalizado quando o MyService é finalizado por update.sh .

update.sh retorna o código 143, parece que foi morto.

2018-08-16 14:46:14.5215|Running update script: /opt/myservice/update.sh
2018-08-16 14:46:14.5883|Update script /opt/myservice/update.sh returned: 143

UPDATE

Eu tentei seguir as abordagens, obrigado por sugestões:

  • setsid
  • rejeitar
  • nohup
  • tela
  • tmux
  • anular

Toda abordagem tem o mesmo resultado, a terminação de todos os processos gerados. Eu suspeito que isso seja um "recurso" do .NET Core.

UPDATE 2

Descobri que systemctl stop MyService por padrão mata explicitamente todos os processos gerados por um serviço.

link

Se eu adicionar KillMode=process ao descritor de serviço, o script de atualização não será finalizado quando o serviço for finalizado.

Existe NO WAY para escapar do espaço PID de um serviço iniciado por systemctl . Todas as técnicas utilizadas, incluídas na resposta aceita, não geram um processo separado. Todo processo gerado é sempre eliminado por systemctl stop MyService , a menos que KillMode=process seja especificado.

Acabei criando um serviço separado MyServiceUpdater : esse serviço executa o script de atualização sem qualquer bifurcação. Como o espaço do PID é diferente, tudo funciona conforme o esperado. Essa foi uma longa jornada.

    
por Ghigo 16.08.2018 / 12:12

2 respostas

1

Em um sistema de teste do Centos 7 via

$ sudo rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm
$ sudo yum install dotnet-sdk-2.1

que resulta em dotnet-sdk-2.1-2.1.400-1.x86_64 sendo instalado, em seguida, com o código de teste

using System;
using System.Diagnostics;
using System.ComponentModel;
namespace myApp {
    class Program {
        static void Main(string[] args) {
            var process = new Process();
            process.EnableRaisingEvents = true; // to avoid [defunct] sh processes
            process.StartInfo.FileName = "/var/tmp/foo";
            process.StartInfo.Arguments = "";
            process.StartInfo.UseShellExecute = true;
            process.StartInfo.CreateNoWindow = true;
            process.Start();
            process.WaitForExit(10000);
            if (process.HasExited) {
                Console.WriteLine("Exit code: " + process.ExitCode);
            } else {
                Console.WriteLine("Child process still running after 10 seconds");
            }
        }
    }
}

e um script shell como /var/tmp/foo a strace pare e mostre que /var/tmp/foo é executado através de xdg-open , o que no meu sistema faz ... Não sei o que, parece uma complicação desnecessária.

$ strace -o foo -f dotnet run
Child process still running after 10 seconds
^C
$ grep /var/tmp/foo foo
25907 execve("/usr/bin/xdg-open", ["/usr/bin/xdg-open", "/var/tmp/foo"], [/* 37 vars */] <unfinished ...>
...

uma solução mais simples é simplesmente exec um programa que por sua vez pode ser um script de shell que faz o que você quer, o que para o .NET requer não usar o shell:

            process.StartInfo.UseShellExecute = false;

com esse conjunto, o strace mostra que /var/tmp/foo está sendo executado por meio de uma chamada (muito mais simples) execve(2) :

26268 stat("/var/tmp/foo", {st_mode=S_IFREG|0755, st_size=37, ...}) = 0
26268 access("/var/tmp/foo", X_OK)      = 0
26275 execve("/var/tmp/foo", ["/var/tmp/foo"], [/* 37 vars */] <unfinished ...>

e esse .NET se recusa a sair:

$ strace -o foo -f dotnet run
Child process still running after 10 seconds
^C^C^C^C^C^C^C^C

porque foo se substitui por algo que ignora a maioria dos sinais (notavelmente não USR2 , ou há sempre KILL (mas evite usar isso!)):

$ cat /var/tmp/foo
#!/bin/sh
exec /var/tmp/stayin-alive
$ cat /var/tmp/stayin-alive
#!/usr/bin/perl
use Sys::Syslog;
for my $s (qw(HUP INT QUIT PIPE ALRM TERM CHLD USR1)) {
   $SIG{$s} = \&shandle;
}
openlog( 'stayin-alive', 'ndelay,pid', LOG_USER );
while (1) {
    syslog LOG_NOTICE, "oh oh oh oh oh stayin alive";
    sleep 7;
}
sub shandle {
    syslog LOG_NOTICE, "nice try - @_";
}

daemonize

Com um processo que se desassocia do pai e um script de shell que executa alguns comandos (espero que equivalentes para o seu pretendido apt-get update; apt-get upgrade )

$ cat /var/tmp/a-few-things
#!/bin/sh
sleep 17 ; echo a >/var/tmp/output ; echo b >/var/tmp/output

podemos modificar o programa .NET para executar /var/tmp/solitary /var/tmp/a-few-things

            process.StartInfo.FileName = "/var/tmp/solitary";
            process.StartInfo.Arguments = "/var/tmp/a-few-things";
            process.StartInfo.UseShellExecute = false;

que, quando executado, faz com que o programa .NET saia rapidamente

$ dotnet run
Exit code: 0

e, eventualmente, o arquivo /var/tmp/output contém duas linhas escritas por um processo que não foi eliminado quando o programa .NET foi removido.

Você provavelmente deve salvar a saída dos comandos do APT em algum lugar, e também pode precisar de algo para que duas (ou mais!) atualizações não sejam necessárias tentando ser executado ao mesmo tempo, etc. Esta versão não pára para perguntas e ignora qualquer TERM sinais ( INT também pode precisar ser ignorado).

#!/bin/sh
trap '' TERM
set -e
apt-get --yes update
apt-get --yes upgrade
    
por 18.08.2018 / 00:46
2

Use crontab (ou at) (não mono / .net) para agendar tarefas.

Opções normais;

  • nohup my.sh &
  • tela -dm -S meu my.sh
  • tmux novo -d -s meu my.sh
  • service my start / systemctl inicia meu
  • Ctrl + Z, bg, rejeitar
por 16.08.2018 / 13:00