¿Es posible capturar una señal Ctrl + C y ejecutar una función de limpieza, de manera "diferida"?

207

Quiero capturar la señal Ctrl+C( SIGINT) enviada desde la consola e imprimir algunos totales de ejecución parcial.

¿Es esto posible en Golang?

Nota: Cuando publiqué la pregunta por primera vez, estaba confundido acerca de Ctrl+Cser en SIGTERMlugar de SIGINT.

Sebastián Grignoli
fuente

Respuestas:

261

Puede usar el paquete os / signal para manejar las señales entrantes. Ctrl+ Ces SIGINT , por lo que puede usar esto para atrapar 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
    }
}()

La manera en que usted hace que su programa finalice e imprima información depende totalmente de usted.

Lily Ballard
fuente
¡Gracias! Entonces ... ^ C no es SIGTERM, entonces? ACTUALIZACIÓN: Lo sentimos, el enlace que proporcionó es lo suficientemente detallado.
Sebastián Grignoli
19
En lugar de for sig := range g {, también puede usar <-sigchancomo en esta respuesta anterior: stackoverflow.com/questions/8403862/…
Denys Séguret
3
@dystroy: Claro, si realmente vas a terminar el programa en respuesta a la primera señal. Al usar el bucle, puede capturar todas las señales si decide no terminar el programa.
Lily Ballard
79
Nota: en realidad debe compilar el programa para que esto funcione. Si ejecuta el programa a través go runde una consola y envía un SIGTERM a través de ^ C, la señal se escribe en el canal y el programa responde, pero parece caer inesperadamente del bucle. ¡Esto se debe a que SIGRERM go runtambién funciona! (¡Esto me ha causado una confusión sustancial!)
William Pursell
55
Tenga en cuenta que para que la goroutine obtenga tiempo de procesador para manejar la señal, la goroutine principal debe llamar a una operación de bloqueo o llamar runtime.Goscheden un lugar apropiado (en el bucle principal de su programa, si tiene una)
misterbee
107

Esto 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
    }
}

fuente
1
Para otros lectores: mire la respuesta de @adamonduty para obtener una explicación de por qué quiere atrapar a OS.Interrupt y syscall.
Chris
1
¿Por qué estás usando un canal sin bloqueo? ¿Es esto necesario?
Awn
44
@Barry, ¿por qué el tamaño del búfer es 2 en lugar de 1?
pdeva
2
Aquí hay un extracto de la documentación . "La señal del paquete no bloqueará el envío a c: la persona que llama debe asegurarse de que c tenga suficiente espacio de búfer para mantenerse al día con la velocidad de señal esperada. Para un canal utilizado para la notificación de un solo valor de señal, un búfer de tamaño 1 es suficiente".
bmdelacruz
25

Para agregar un poco a las otras respuestas, si realmente desea atrapar SIGTERM (la señal predeterminada enviada por el comando kill), puede usar syscall.SIGTERMen lugar de os.Interrupt. Tenga en cuenta que la interfaz syscall es específica del sistema y podría no funcionar en todas partes (por ejemplo, en Windows). Pero funciona muy bien para atrapar ambos:

c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
....
adamlamar
fuente
77
La signal.Notifyfunción permite especificar varias señales a la vez. Por lo tanto, puede simplificar su código a signal.Notify(c, os.Interrupt, syscall.SIGTERM).
jochen
Creo que lo descubrí después de publicar. ¡Fijo!
adamlamar
2
¿Qué pasa con os.kill?
Awn
2
@Eclipse ¡Gran pregunta! os.Kill corresponde a syscall.Kill, que es una señal que puede enviarse pero no capturarse. Es equivalente al comando kill -9 <pid>. Si desea atrapar kill <pid>y apagar con gracia, debe usar syscall.SIGTERM.
adamlamar
@adamlamar Ahh, eso tiene sentido. ¡Gracias!
Awn
18

Había (al momento de publicar) uno o dos pequeños errores tipográficos en la respuesta aceptada arriba, así que aquí está la versión limpia. En este ejemplo, detengo el generador de perfiles de CPU cuando recibo 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
fuente
2
Tenga en cuenta que para que la goroutine obtenga tiempo de procesador para manejar la señal, la goroutine principal debe llamar a una operación de bloqueo o llamar runtime.Goscheden un lugar apropiado (en el bucle principal de su programa, si tiene una)
misterbee
8

Todo lo anterior parece funcionar cuando se empalma, pero la página de señales de gobyexample tiene un ejemplo realmente limpio y completo de captura de señal. Vale la pena agregar a esta lista.

willscripted
fuente
0

La muerte es una biblioteca simple que utiliza canales y un grupo de espera para esperar las señales de apagado. Una vez que se haya recibido la señal, llamará a un método de cierre en todas sus estructuras que desea limpiar.

Ben Aldrich
fuente
3
¿Tantas líneas de código y una dependencia de biblioteca externa para hacer lo que se puede hacer en cuatro líneas de código? (según la respuesta aceptada)
Jacob
le permite hacer la limpieza de todos ellos en paralelo y cerrar estructuras automáticamente si tienen la interfaz de cierre estándar.
Ben Aldrich
0

Puede tener una rutina diferente que detecte las señales syscall.SIGINT y syscall.SIGTERM y las transmita a un canal utilizando la señal . Puede enviar un gancho a esa rutina utilizando un canal y guardarlo en un segmento de función. Cuando se detecta la señal de apagado en el canal, puede ejecutar esas funciones en el segmento. Esto se puede usar para limpiar los recursos, esperar a que finalicen las rutinas de ejecución, conservar datos o imprimir totales de ejecución parciales.

Escribí una utilidad pequeña y simple para agregar y ejecutar ganchos en el apagado. Espero que pueda ser de ayuda.

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

Puede hacer esto de manera 'diferida'.

ejemplo para apagar un servidor con gracia:

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
fuente
0

Esta es otra versión que funciona en caso de que tenga que limpiar algunas tareas. El código dejará el proceso de limpieza en su 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
}

en caso de que necesite limpiar la función principal, también necesita capturar la señal en el hilo principal usando go func ().

Hlex
fuente
-2

Solo para el registro si alguien necesita una forma de manejar las señales en Windows. Tenía un requisito para manejar desde el programa A llamando al programa B a través de os / exec pero el programa B nunca pudo terminar con gracia porque enviaba señales a través de ex. cmd.Process.Signal (syscall.SIGTERM) u otras señales no son compatibles con Windows. La forma en que manejé es creando un archivo temporal como una señal ex. .signal.term a través del programa A y el programa B deben verificar si ese archivo existe en la base de intervalos, si existe el archivo, saldrá del programa y se encargará de una limpieza si es necesario, estoy seguro de que hay otras formas, pero esto hizo el trabajo.

user212806
fuente