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 :

  1. L’émulateur de terminal crée un pseudo-terminal (PTY).
  2. Un shell (zsh, bash…) est lancé à l’intérieur de ce PTY.
  3. Quand vous tapez npm run dev, le shell crée un processus enfant qui exécute Node.js.
  4. 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 setsid ou un double fork. Une fois détaché, le processus ne reçoit plus le SIGHUP de 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, zsh ou bash peut ne pas envoyer SIGHUP à ses enfants. La valeur de l’option huponexit (bash) ou HUP (zsh) détermine ce comportement. Par défaut, bash ne l’envoie pas.

  • nohup implicite : certains gestionnaires de processus ou scripts wrapper lancent les commandes avec nohup sans 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 : rwhois est 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 :

SignalNuméroComportement par défautUsage
SIGHUP1Terminer le processusEnvoyé quand le terminal se ferme
SIGINT2Terminer le processusEnvoyé par Ctrl+C
SIGTERM15Terminer le processusEnvoyé par kill (par défaut)
SIGKILL9Terminer immédiatementEnvoyé 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+C avant de fermer le terminal.
  • lsof -i :<port> pour diagnostiquer.
  • kill <PID> pour nettoyer.
  • Configurer huponexit/HUP dans 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.

← Retour aux articles