Est-il possible de capturer un signal Ctrl + C et d'exécuter une fonction de nettoyage, de manière «différée»?

207

Je veux capturer le signal Ctrl+C( SIGINT) envoyé depuis la console et imprimer des totaux partiels.

Est-ce possible à Golang?

Note: Quand je suis posté la question que je confus au sujet d' Ctrl+Cêtre au SIGTERMlieu de SIGINT.

Sebastián Grignoli
la source

Réponses:

261

Vous pouvez utiliser le package os / signal pour gérer les signaux entrants. Ctrl+ Cest SIGINT , vous pouvez donc l'utiliser pour intercepter 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 manière dont vous faites interrompre votre programme et imprimer des informations vous appartient entièrement.

Lily Ballard
la source
Merci! Alors ... ^ C ce n'est pas SIGTERM, alors? MISE À JOUR: Désolé, le lien que vous avez fourni est suffisamment détaillé!
Sebastián Grignoli
19
Au lieu de for sig := range g {, vous pouvez également utiliser <-sigchancomme dans cette réponse précédente: stackoverflow.com/questions/8403862/…
Denys Séguret
3
@dystroy: Bien sûr, si vous allez réellement terminer le programme en réponse au premier signal. En utilisant la boucle, vous pouvez capter tous les signaux si vous décidez de ne pas terminer le programme.
Lily Ballard
79
Remarque: vous devez réellement créer le programme pour que cela fonctionne. Si vous exécutez le programme via go rundans une console et envoyez un SIGTERM via ^ C, le signal est écrit dans le canal et le programme répond, mais semble sortir de la boucle de manière inattendue. C'est parce que le SIGRERM va go runaussi! (Cela m'a causé beaucoup de confusion!)
William Pursell
5
Notez que pour que le goroutine ait le temps du processeur pour gérer le signal, le goroutine principal doit appeler une opération de blocage ou appeler runtime.Goschedà un endroit approprié (dans la boucle principale de votre programme, s'il en a un)
misterbee
107

Cela marche:

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

la source
1
Pour les autres lecteurs: regardez la réponse de @adamonduty pour une explication de la raison pour laquelle vous voulez attraper os.Interrupt et syscall.SIGTERM Cela aurait été bien d'inclure son explication dans cette réponse, surtout depuis qu'il a posté des mois avant vous.
Chris
1
Pourquoi utilisez-vous un canal non bloquant? Est-ce nécessaire?
Awn
4
@Barry pourquoi la taille du tampon est-elle de 2 au lieu de 1?
pdeva
2
Voici un extrait de la documentation . "Le signal du package ne bloquera pas l'envoi à c: l'appelant doit s'assurer que c dispose d'un espace tampon suffisant pour suivre le débit de signal attendu. Pour un canal utilisé pour la notification d'une seule valeur de signal, un tampon de taille 1 est suffisant."
bmdelacruz
25

Pour ajouter un peu aux autres réponses, si vous voulez réellement attraper SIGTERM (le signal par défaut envoyé par la commande kill), vous pouvez utiliser syscall.SIGTERMà la place de os.Interrupt. Attention, l'interface syscall est spécifique au système et peut ne pas fonctionner partout (par exemple sur Windows). Mais cela fonctionne bien pour attraper les deux:

c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
....
adamlamar
la source
7
La signal.Notifyfonction permet de spécifier plusieurs signaux à la fois. Ainsi, vous pouvez simplifier votre code en signal.Notify(c, os.Interrupt, syscall.SIGTERM).
jochen
Je pense que je l'ai découvert après avoir posté. Fixé!
adamlamar
2
Et os.Kill?
Awn
2
@Eclipse Grande question! os.Kill correspond à syscall.Kill, qui est un signal qui peut être envoyé mais pas capté. Son équivalent à la commande kill -9 <pid>. Si vous voulez intercepter kill <pid>et arrêter gracieusement, vous devez utiliser syscall.SIGTERM.
adamlamar
@adamlamar Ahh, c'est logique. Merci!
Awn
18

Il y avait (au moment de la publication) une ou deux petites fautes de frappe dans la réponse acceptée ci-dessus, alors voici la version nettoyée. Dans cet exemple, j'arrête le profileur de processeur lors de la réception de 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
la source
2
Notez que pour que le goroutine ait le temps du processeur pour gérer le signal, le goroutine principal doit appeler une opération de blocage ou appeler runtime.Goschedà un endroit approprié (dans la boucle principale de votre programme, s'il en a un)
misterbee
8

Tous les éléments ci-dessus semblent fonctionner lorsqu'ils sont épissés, mais la page des signaux de gobyexample propose un exemple vraiment propre et complet de capture de signal. Vaut la peine d'être ajouté à cette liste.

willscripted
la source
0

Death est une bibliothèque simple qui utilise des canaux et un groupe d'attente pour attendre les signaux d'arrêt. Une fois le signal reçu, il appellera une méthode close sur toutes vos structures que vous souhaitez nettoyer.

Ben Aldrich
la source
3
Tant de lignes de code et une dépendance de bibliothèque externe pour faire ce qui peut être fait en quatre lignes de code? (selon la réponse acceptée)
Jacob
il vous permet de nettoyer tous en parallèle et de fermer automatiquement les structures si elles ont l'interface de fermeture standard.
Ben Aldrich
0

Vous pouvez avoir une goroutine différente qui détecte les signaux syscall.SIGINT et syscall.SIGTERM et les relaie à un canal à l'aide de signal.Notify . Vous pouvez envoyer un crochet à ce goroutine en utilisant un canal et l'enregistrer dans une tranche de fonction. Lorsque le signal d'arrêt est détecté sur le canal, vous pouvez exécuter ces fonctions dans la tranche. Cela peut être utilisé pour nettoyer les ressources, attendre la fin des goroutines en cours d'exécution, conserver les données ou imprimer les totaux des exécutions partielles.

J'ai écrit un petit utilitaire simple pour ajouter et exécuter des hooks à l'arrêt. J'espère que cela peut être utile.

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

Vous pouvez le faire de manière «différée».

exemple pour arrêter un serveur normalement:

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
la source
0

Il s'agit d'une autre version qui fonctionne au cas où vous auriez des tâches à nettoyer. Le code laissera le processus de nettoyage dans leur méthode.

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
}

si vous avez besoin de nettoyer la fonction principale, vous devez également capturer le signal dans le thread principal en utilisant go func ().

Hlex
la source
-2

Juste pour mémoire si quelqu'un a besoin d'un moyen de gérer les signaux sous Windows. J'avais besoin de gérer depuis prog A en appelant prog B via os / exec mais prog B n'a jamais pu se terminer gracieusement car l'envoi de signaux via ex. cmd.Process.Signal (syscall.SIGTERM) ou d'autres signaux ne sont pas pris en charge sous Windows. La façon dont j'ai géré est de créer un fichier temporaire comme un signal ex. .signal.term via prog A et prog B doit vérifier si ce fichier existe sur la base de l'intervalle, s'il existe, il quittera le programme et effectuera un nettoyage si nécessaire, je suis sûr qu'il existe d'autres moyens mais cela a fait l'affaire.

user212806
la source