Processus orphelins sur Mac malgré la fermeture du terminal
Vous venez de fermer toutes vos fenêtres de terminal, et pourtant http://localhost:4321 affiche toujours votre site. Fantôme ? Bug ? Non : un processus orphelin. Voici ce qui se passe sous le capot, et comment reprendre le contrôle.
Le scénario
Je développe un site avec Astro (mais c’est valable aussi pour Vite, Next.js, Hugo, peu importe). Je lance npm run dev, le serveur démarre sur le port 4321. En fin de session, je ferme la fenêtre du terminal je passe à autre chose.
Le lendemain, par curiosité ou par habitude, j’ouvre http://localhost:4321 dans mon navigateur. Et là, surprise : le site est toujours accessible.
Tous les terminaux sont fermés. Aucune fenêtre n’est ouverte. Alors, qui fait tourner le serveur ?
Ce qui se passe réellement
Pour comprendre, il faut revenir à un concept fondamental d’Unix : la relation entre un terminal, un shell et un processus.
La hiérarchie des processus
Quand vous ouvrez un terminal sur macOS (Terminal.app, iTerm2, Wezterm …), voici ce qui se passe en coulisse :
- L’émulateur de terminal crée un pseudo-terminal (PTY).
- Un shell (zsh, bash…) est lancé à l’intérieur de ce PTY.
- Quand vous tapez
npm run dev, le shell crée un processus enfant qui exécute Node.js. - Node.js peut lui-même lancer des sous-processus (workers, watchers de fichiers, etc.).
On obtient une arborescence :
Terminal.app
└── zsh (PID 1234)
└── node (PID 5678) ← votre serveur de dev
└── esbuild (PID 5679)
Pourquoi le processus survit
Quand vous fermez la fenêtre du terminal, l’émulateur envoie un signal SIGHUP (hangup) au shell. Le shell, en mourant, est censé propager ce signal à ses processus enfants. Mais dans la pratique, plusieurs mécanismes peuvent empêcher cette propagation :
-
Le processus s’est détaché : certains outils de build se « daemonisent » ou se détachent de leur processus parent via
setsidou un doublefork. Une fois détaché, le processus ne reçoit plus leSIGHUPde son parent. -
Le signal est intercepté : un processus peut installer un handler qui ignore
SIGHUP(signal(SIGHUP, SIG_IGN)). Node.js et certains frameworks font exactement cela pour éviter de mourir sur une déconnexion accidentelle. -
Le shell ne propage pas : selon sa configuration,
zshoubashpeut ne pas envoyerSIGHUPà ses enfants. La valeur de l’optionhuponexit(bash) ouHUP(zsh) détermine ce comportement. Par défaut,bashne l’envoie pas. -
nohupimplicite : certains gestionnaires de processus ou scripts wrapper lancent les commandes avecnohupsans que vous le sachiez.
Le résultat : votre processus Node.js devient un processus orphelin. Il est adopté par le processus launchd (PID 1, l’équivalent de init sur Linux), et il continue tranquillement de tourner, d’écouter sur son port, et de servir vos pages.
Diagnostiquer le problème
Identifier le processus
La commande lsof (list open files) permet de voir qui écoute sur un port :
lsof -i :4321
Résultat :
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 27084 aureliendincuff 35u IPv6 0xf4d2... 0t0 TCP localhost:rwhois (LISTEN)
node 27084 aureliendincuff 42u IPv6 0xaab7... 0t0 TCP localhost:rwhois->localhost:57225 (ESTABLISHED)
On apprend ici que :
- Le processus est
node(colonne COMMAND). - Son PID est
27084. - Il écoute (
LISTEN) sur le port en IPv6. - Il a même une connexion active (
ESTABLISHED) — probablement votre navigateur qui a gardé une connexion ouverte ou un hot-reload WebSocket.
Note :
rwhoisest le nom de service associé au port 4321 dans/etc/services. C’est juste un alias, rien à voir avec le protocole rwhois lui-même.
Voir l’arbre de parenté
Pour confirmer que le processus est orphelin :
ps -o ppid= -p 27084
Si le PPID (parent process ID) est 1, le processus a bien été adopté par launchd : il est orphelin.
Vérifier ce qu’il exécute
ps -p 27084 -o command=
Cela affiche la commande complète, par exemple :
/usr/local/bin/node ./node_modules/.bin/astro dev
Vous avez maintenant la certitude : c’est bien votre serveur de développement Astro.
Résoudre le problème
Solution immédiate : tuer le processus
# Arrêt propre (SIGTERM)
kill 27084
# Si le processus résiste (SIGKILL — dernier recours)
kill -9 27084
Préférez toujours kill (SIGTERM) en premier : il laisse au processus le temps de fermer proprement ses connexions et de libérer ses ressources. kill -9 est plus brutal — le processus est détruit immédiatement.
Solution en une ligne
Si vous voulez un raccourci pour tuer tout ce qui écoute sur un port donné :
lsof -ti :4321 | xargs kill
L’option -t de lsof affiche uniquement les PID, ce qui permet de les passer directement à kill.
Prévenir le problème
1. Toujours utiliser Ctrl+C
La bonne pratique, c’est de ne jamais fermer la fenêtre du terminal pour arrêter un serveur. Utilisez Ctrl+C dans le terminal : cela envoie un SIGINT directement au processus en avant-plan, qui s’arrête proprement dans la grande majorité des cas.
2. Configurer le shell pour propager SIGHUP
Pour bash, ajoutez dans votre ~/.bashrc :
shopt -s huponexit
Pour zsh, ajoutez dans votre ~/.zshrc :
setopt HUP
Cela force le shell à envoyer SIGHUP à tous ses enfants quand il se ferme.
3. Configurer votre émulateur de terminal
Dans les préférences de votre terminal, cherchez le comportement à la fermeture. Par exemple, dans iTerm2 :
- Preferences → Profiles → Session → Closing : vous pouvez configurer l’envoi de signaux spécifiques aux processus en cours.
Aller plus loin : comprendre les signaux Unix
Voici les signaux clés à connaître dans ce contexte :
| Signal | Numéro | Comportement par défaut | Usage |
|---|---|---|---|
SIGHUP | 1 | Terminer le processus | Envoyé quand le terminal se ferme |
SIGINT | 2 | Terminer le processus | Envoyé par Ctrl+C |
SIGTERM | 15 | Terminer le processus | Envoyé par kill (par défaut) |
SIGKILL | 9 | Terminer immédiatement | Envoyé par kill -9, ne peut pas être intercepté |
La différence fondamentale : SIGTERM et SIGINT peuvent être interceptés par le processus pour faire un nettoyage avant de mourir (fermer les fichiers, les connexions réseau, etc.). SIGKILL ne peut pas être intercepté — c’est le bouton d’arrêt d’urgence.
En résumé
Fermer la fenêtre d’un terminal ne garantit pas l’arrêt des processus qu’il exécutait. C’est un comportement normal d’Unix, pas un bug. Le processus devient orphelin, continue de tourner, et le port reste occupé.
Les réflexes à adopter :
- Toujours arrêter un serveur avec
Ctrl+Cavant de fermer le terminal. lsof -i :<port>pour diagnostiquer.kill <PID>pour nettoyer.- Configurer
huponexit/HUPdans votre shell comme filet de sécurité.
Plus qu’un bug ou un “problème”, je trouvais ce cas d’usage intéressant et didactique de par les diverses notions techniques qu’il a permis d’aborder.