Terminer proprement un programme console

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.