Les programmes console ne sont pas morts: il est courant de permettre à un service Windows d’être lancé en mode console, et ASP.NET Core est initialement prévu pour être exécuté en mode console (en self host).
Dans ce contexte, se pose rapidement le problème de terminer proprement le
contexte d’exécution (libérer les ressources IDisposable, éventuellement
persister un état, annuler proprement les tâches en cours, etc.).
Jusqu’à présent, j’invitais l’utilisateur à appuyer sur la touche Echap et je capturais cet événement, sans monopoliser l’entrée console, grâce à des fonctions P/Invoke. Je ne décrirai pas cette méthode ici car, premièrement, elle s’est avérée ne pas être fiable à l’usage et, deuxièmement, elle ne sert à rien si l’utilisateur ferme simplement la fenêtre par sa petite croix rouge. On aura tous constaté, avec émerveillement ou frustration, que le fait que la croix soit rouge n’inquiète pas le moins du monde les utilisateurs du programme quant à la fin « propre » dudit programme; même en faisant preuve de pédagogie auprès de ces utilisateurs. Je parle ici d’utilisateurs du monde de l’IT (développeurs inclus).

Solution avec la fonction native SetConsoleCtrlHandler (Kernel32)
J’ai pris connaissance d’une nouvelle solution apparemment plus fiable, dans une
certaine mesure: capturer la fermeture de la fenêtre ou la séquence clavier
CTRL+C. Cela se fait toujours via P/Invoke, avec la fonction native
SetConsoleCtrlHandler.
Exemple
Un programe console basique est sur un Gist ici.
Le principe est simple: on inscrit un callback via la fonction
SetConsoleCtrlHandler
qui permet de capturer les signaux CTRL_CLOSE_EVENT (fermeture de la fenêtre
de la console) et CTRL_C_EVENT (CTRL+C). Les autres signaux sont moins
pertinents dans un contexte de service Windows exécuté en mode console.
Le signal est transmis par le système à notre processus via un nouveau thread créé pour l’occasion à l’intérieur de celui-ci. Cela est à prendre en compte si le callback manipule un état partagé avec un autre thread (ce qui est hautement probable).
Le callback invoqué peut évaluer le signal reçu et éventuellement provoquer
l’arrêt du programme (ou pas). Si une action est entreprise, il doit retourner
la valeur true, sinon false.
Si la valeur trueest retournée, le système ne propagera pas le signal aux
autres callbacks éventuellement inscrits sauf pour le signal
CTRL_CLOSE_EVENT qui invoquera toujours le callback par défaut du système qui
consiste à arrêter le processus. Ce dernier signal laisse juste un peu de temps
pour agir (5 secondes sur Windows 7 d’après ce que j’ai observé mais cela peut
varier d’un système à un autre). Par conséquent, si l’arrêt propre dépasse ce
délai, le programme sera tué. Il ne semble pas y avoir de
moyen efficace d’empêcher cela
et c’est une bonne chose. Et effectivement le délai pourra être court dans
certains cas mais on peut considérer qu’un programme qui a besoin de plus de
temps pour s’arrêter devrait être géré différemment, en contexte de service par
exemple. On pourra s’amuser à lire à ce sujet
cet article
pointé par le précédent lien.
Le signal CTRL_C_EVENT est plus sympathique pour nous puisqu’il n’y a pas de
timeout pour le callback: en effet cette séquence,
dans sa nature,
doit permettre d’interrompre la tâche en cours. Par défaut il s’agit du
processus entier (ce que fait le callback par défaut si on retourne false dans
notre callback), mais il pourrait s’agir d’une tâche dans le processus, sans
arrêter entièrement celui-ci.
Cet exemple a été testé et fonctionne sur Windows 7 et Windows 10.