É possível capturar um sinal Ctrl + C e executar uma função de limpeza, de forma "adiada"?

207

Quero capturar o sinal Ctrl+C( SIGINT) enviado do console e imprimir alguns totais parciais de execução.

Isso é possível em Golang?

Nota: Quando publiquei a pergunta pela primeira vez, fiquei confuso sobre Ctrl+Cestar em SIGTERMvez de SIGINT.

Sebastián Grignoli
fonte

Respostas:

260

Você pode usar o pacote os / signal para lidar com os sinais recebidos. Ctrl+ Cé SIGINT , então você pode usá-lo para interceptar os.Interrupt.

c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func(){
    for sig := range c {
        // sig is a ^C, handle it
    }
}()

A maneira como você faz com que o seu programa encerre e imprima as informações é inteiramente sua.

Lily Ballard
fonte
Obrigado! Então ... ^ C não é SIGTERM, então? ATUALIZAÇÃO: Desculpe, o link que você forneceu é detalhado o suficiente!
Sebastián Grignoli
19
Em vez de for sig := range g {, você também pode usar <-sigchancomo nesta resposta anterior: stackoverflow.com/questions/8403862/…
Denys Séguret 30/12/12
3
@dystroy: Claro, se você realmente vai terminar o programa em resposta ao primeiro sinal. Ao usar o loop, você pode capturar todos os sinais se decidir não finalizar o programa.
Lily Ballard
79
Nota: você deve realmente criar o programa para que isso funcione. Se você executar o programa go runem um console e enviar um SIGTERM via ^ C, o sinal será gravado no canal e o programa responderá, mas parecerá sair inesperadamente do loop. Isso ocorre porque o SIGRERM também vai go run! (Isso tem me causa confusão substancial!)
William Pursell
5
Note-se que para que o goroutine para obter tempo de processador para lidar com o sinal, o goroutine principal deve chamar uma operação de bloqueio ou de chamadas runtime.Goschedem local apropriado (em loop principal do programa, se ele tiver um)
misterbee
107

Isso funciona:

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time" // or "runtime"
)

func cleanup() {
    fmt.Println("cleanup")
}

func main() {
    c := make(chan os.Signal)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    go func() {
        <-c
        cleanup()
        os.Exit(1)
    }()

    for {
        fmt.Println("sleeping...")
        time.Sleep(10 * time.Second) // or runtime.Gosched() or similar per @misterbee
    }
}

fonte
1
Para outros leitores: Veja a resposta de @adamonduty para obter uma explicação sobre o motivo pelo qual você deseja capturar os.Interrupt e syscall.SIGTERM Seria bom incluir a explicação dele nesta resposta, especialmente desde que ele postou meses antes de você.
Chris
1
Por que você está usando um canal sem bloqueio? Isso é necessário?
Awn
4
@ Barry, por que o tamanho do buffer 2 em vez de 1?
Pdeva
2
Aqui está um trecho da documentação . "O sinal do pacote não bloqueia o envio para c: o chamador deve garantir que c tenha espaço de buffer suficiente para acompanhar a taxa de sinal esperada. Para um canal usado para notificação de apenas um valor de sinal, um buffer de tamanho 1 é suficiente."
21418 bmdelacruz
25

Para adicionar um pouco às outras respostas, se você realmente deseja capturar o SIGTERM (o sinal padrão enviado pelo comando kill), pode usar syscall.SIGTERMno lugar do os.Interrupt. Cuidado que a interface syscall é específica do sistema e pode não funcionar em qualquer lugar (por exemplo, no Windows). Mas funciona bem para capturar os dois:

c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
....
adamlamar
fonte
7
A signal.Notifyfunção permite especificar vários sinais ao mesmo tempo. Assim, você pode simplificar seu código para signal.Notify(c, os.Interrupt, syscall.SIGTERM).
jochen
Acho que descobri isso depois de postar. Fixo!
Adamlamar
2
E o os.Kill?
Awn
2
@Eclipse Ótima pergunta! os.Kill corresponde a syscall.Kill, que é um sinal que pode ser enviado, mas não capturado. É equivalente ao comando kill -9 <pid>. Se você deseja capturar kill <pid>e desligar normalmente, você deve usar syscall.SIGTERM.
Adamlamar 07/04
@adamlamar Ahh, isso faz sentido. Obrigado!
Awn
18

Havia (no momento da publicação) um ou dois pequenos erros de digitação na resposta aceita acima, então aqui está a versão limpa. Neste exemplo, paro o criador de perfil da CPU ao receber Ctrl+ C.

// capture ctrl+c and stop CPU profiler                            
c := make(chan os.Signal, 1)                                       
signal.Notify(c, os.Interrupt)                                     
go func() {                                                        
  for sig := range c {                                             
    log.Printf("captured %v, stopping profiler and exiting..", sig)
    pprof.StopCPUProfile()                                         
    os.Exit(1)                                                     
  }                                                                
}()    
gravitron
fonte
2
Note-se que para que o goroutine para obter tempo de processador para lidar com o sinal, o goroutine principal deve chamar uma operação de bloqueio ou de chamadas runtime.Goschedem local apropriado (em loop principal do programa, se ele tiver um)
misterbee
0

A morte é uma biblioteca simples que usa canais e um grupo de espera para aguardar sinais de desligamento. Depois que o sinal for recebido, ele chamará um método close em todas as suas estruturas que você deseja limpar.

Ben Aldrich
fonte
3
Tantas linhas de código e uma biblioteca externa dependem para fazer o que pode ser feito em quatro linhas de código? (conforme resposta aceita)
Jacob
Ele permite que você faça a limpeza de todos eles em paralelo e feche automaticamente as estruturas, se elas tiverem a interface de fechamento padrão.
Ben Aldrich
0

Você pode ter uma goroutine diferente que detecte os sinais syscall.SIGINT e syscall.SIGTERM e retransmiti-los para um canal usando signal.Notify . Você pode enviar um gancho para essa goroutine usando um canal e salvá-lo em uma fatia de função. Quando o sinal de desligamento é detectado no canal, você pode executar essas funções na fatia. Isso pode ser usado para limpar os recursos, aguardar a conclusão de goroutines em execução, persistir dados ou imprimir totais parciais de execução.

Eu escrevi um utilitário pequeno e simples para adicionar e executar ganchos no desligamento. Espero que possa ser de ajuda.

https://github.com/ankit-arora/go-utils/blob/master/go-shutdown-hook/shutdown-hook.go

Você pode fazer isso de uma maneira "adiada".

exemplo para desligar um servidor normalmente:

srv := &http.Server{}

go_shutdown_hook.ADD(func() {
    log.Println("shutting down server")
    srv.Shutdown(nil)
    log.Println("shutting down server-done")
})

l, err := net.Listen("tcp", ":3090")

log.Println(srv.Serve(l))

go_shutdown_hook.Wait()
Ankit Arora
fonte
0

Esta é outra versão que funciona caso você tenha algumas tarefas para limpar. O código deixará o processo de limpeza em seu método.

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"

)



func main() {

    _,done1:=doSomething1()
    _,done2:=doSomething2()

    //do main thread


    println("wait for finish")
    <-done1
    <-done2
    fmt.Print("clean up done, can exit safely")

}

func doSomething1() (error, chan bool) {
    //do something
    done:=make(chan bool)
    c := make(chan os.Signal, 2)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    go func() {
        <-c
        //cleanup of something1
        done<-true
    }()
    return nil,done
}


func doSomething2() (error, chan bool) {
    //do something
    done:=make(chan bool)
    c := make(chan os.Signal, 2)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    go func() {
        <-c
        //cleanup of something2
        done<-true
    }()
    return nil,done
}

caso você precise limpar a função principal, você precisa capturar o sinal na thread principal usando go func () também.

Hlex
fonte
-2

Só para constar se alguém precisa de uma maneira de lidar com sinais no Windows. Eu tinha um requisito para lidar com prog A chamando prog B através de os / exec, mas prog B nunca foi capaz de terminar normalmente porque enviando sinais através de ex. O cmd.Process.Signal (syscall.SIGTERM) ou outros sinais não são suportados no Windows. A maneira como lidei com isso é criando um arquivo temporário como um sinal ex. .signal.term através de prog A e prog B precisa verificar se esse arquivo existe na base do intervalo; se existir, ele sairá do programa e executará uma limpeza, se necessário, tenho certeza de que existem outras maneiras, mas isso foi útil.

user212806
fonte