Steven
Steven7 min de lecture

La vérification de sécurité qui a rendu notre app impossible à quitter

La mise à jour automatique a été la fonctionnalité la plus difficile que nous ayons jamais livrée — six versions en quatre jours pour l'empêcher de rendre l'app inutilisable. Le pire bug, c'est nous qui l'avons introduit en voulant être prudents : une vérification « de sécurité » de 500 millisecondes qui transformait une mise à jour ratée en un processus qu'on ne pouvait littéralement pas quitter.

Ingénierie
Electron
Fiabilité
Versions de GeekBye
La vérification de sécurité qui a rendu notre app impossible à quitter

Chaque développeur d''app de bureau sous-estime la mise à jour automatique exactement une fois. Ça ressemble à un problème résolu — une bibliothèque télécharge une nouvelle version et redémarre ton app. Puis tu la livres, et tu apprends que « redémarre ton app » est l''une des choses les plus dangereuses qu''on puisse demander à un programme, parce que ça arrive au moment précis où ton app est en train de se démonter elle-même et où elle a la plus faible marge d''erreur.

La mise à jour automatique de GeekBye a demandé six versions en quatre jours — de la v1.5.14 à la v1.5.19 — pour se stabiliser. Voici l''histoire du pire bug de ce tronçon, que nous avons provoqué nous-mêmes en voulant être prudents.

Six versions, et celle qui comptait

L''arc a commencé de façon banale. La v1.5.14 a corrigé un bug de niveau coquille embarrassant : le flux de mise à jour pointait vers un nom de dépôt GitHub qui n''existait pas, si bien que le programme de mise à jour interrogeait un 404. La v1.5.15 a ajouté un bouton manuel « Check for Updates » et un vrai message d''erreur. Puis les bugs de quitAndInstall ont commencé, et les versions se sont enchaînées vite — parce que quand ton mécanisme de mise à jour est cassé, tu ne peux pas livrer son correctif à travers le mécanisme de mise à jour. Chaque itération est un pari de réinstallation manuelle.

Celle qui compte, c''est la v1.5.18. Tout son contenu tenait dans un unique commit dont le titre me fait encore grimacer : restaurer le comportement original de quitAndInstall pour éviter une app impossible à tuer.

Comment « être prudent » a rendu l''app inutilisable

Voici le décor. Quand une mise à jour est téléchargée, le quitAndInstall d''Electron est censé fermer l''app et basculer sur la nouvelle version. Dans une version antérieure, quelqu''un — raisonnablement — s''est inquiété du fait que quitter inconditionnellement était risqué. Et si l''installation échouait ? Ne serait-il pas plus sûr de ne quitter que si tout paraissait sain ?

Alors le code s''est doté d''une garde qui semblait sensée :

autoUpdater.quitAndInstall(false, true)
setTimeout(() => {
  if (this.updateDownloaded)
    app.quit() // only quit if the update is still "good"
  else console.log('Error detected — keeping app open')
}, 500)

La logique : lance l''installation, attends une demi-seconde, et ne force le app.quit() final que si le drapeau updateDownloaded est toujours vrai — sinon garde l''app ouverte pour que l''utilisateur ne se retrouve pas coincé.

Le piège est à une ligne de là, dans le gestionnaire d''erreur. Ce gestionnaire posait this.updateDownloaded = false. Alors imagine une installation ratée : l''événement error se déclenche et efface le drapeau. Mais quitAndInstall avait déjà entamé le démontage — il avait fermé les fenêtres et retiré les écouteurs de fermeture de l''app. Puis le minuteur de 500ms se réveille, vérifie le drapeau désormais faux, décide « erreur détectée, garder l''app ouverte » et saute app.quit().

Tu as maintenant, sous macOS en particulier, le pire état possible. macOS ne quitte pas une app juste parce que sa dernière fenêtre s''est fermée — c''est le comportement window-all-closed sur lequel s''appuie toute app Mac. Le processus est donc toujours vivant, mais il n''a pas de fenêtre, pas de chemin par la barre de menu, et ses écouteurs de fermeture ont été arrachés. Il n''y a rien à cliquer. Cmd-Q n''a personne à qui parler. La seule issue est Force Quit depuis Activity Monitor. La vérification « de sécurité » avait transformé une mise à jour ratée — une gêne récupérable — en un zombie que tu ne pouvais pas tuer.

Le correctif : le démontage doit être inconditionnel

La correction de la v1.5.18 est presque agressivement ennuyeuse, et c''est tout l''intérêt. Elle supprime l''astuce :

  1. Retirer les écouteurs window-all-closed et before-quit qui pouvaient interférer.
  2. Détruire chaque fenêtre — window.destroy(), pas window.close(). La fermeture peut être bloquée par un gestionnaire ; la destruction non. Quand tu t''engages à t''éteindre, tu ne le demandes pas poliment.
  3. Appeler quitAndInstall.
  4. Appeler app.quit() inconditionnellement.

Pas de drapeau, pas de minuteur, pas de « garde-la ouverte au cas où ». Parce que la vérité sur un chemin d''extinction, c''est qu''une extinction à moitié faite est pire que l''un ou l''autre résultat. Quitter complètement, c''est bien. Rester complètement ouvert, c''est bien. Le seul état que tu ne dois jamais atteindre, c''est démonté mais toujours en cours d''exécution — et c''est exactement l''état dans lequel une fermeture conditionnelle peut te laisser coincé.

Deux autres leçons apprises la même semaine

Le bug de l''app impossible à tuer est le titre, mais la course aux six versions a durci deux autres habitudes qui valent la peine d''être volées.

Filtre ta télémétrie de plantage par signatures exactes, pas par mots-clés larges. En pleine course, nous avons découvert que notre système de remontée d''erreurs était configuré pour écarter tout ce qui contenait des mots comme permission, token ou microphone — une tentative de couper le bruit qui avalait discrètement de vrais plantages qui mentionnaient par hasard ces mots. Nous avons arraché les filtres génériques et les avons remplacés par des chaînes de refus exactes (le message précis que macOS émet quand une permission est refusée) et des codes réseau transitoires spécifiques comme ERR_NETWORK_CHANGED. La réduction du bruit et le masquage des bugs, c''est le même bouton tourné dans des sens opposés ; si tu filtres au feeling, tu filtreras justement ce que tu avais besoin de voir.

Chaque chemin automatique a besoin d''une issue de secours manuelle. La mise à jour automatique est par nature au mieux — les réseaux flanchent, les installations échouent. Alors chaque mode de défaillance a reçu un recours humain : le bouton manuel « Check for Updates », des relances à recul exponentiel, un minuteur de revérification et — réservé spécifiquement au cas où la tentative manuelle d''un utilisateur échouait — un message en langage clair « supprime l''app et réinstalle-la depuis le site web ». Le chemin automatique est un confort ; le chemin manuel est la garantie.

À retenir

  1. Une garde autour d''une action irréversible est plus dangereuse que l''action. La fermeture conditionnelle a tenté d''empêcher une mauvaise mise à jour de fermer l''app, et a créé à la place un état pire que quitter ou ne pas quitter. Les chemins d''extinction et d''installation devraient être inconditionnels et idempotents — jamais conditionnés à un drapeau mutable qu''un autre gestionnaire peut retourner sous tes pieds.
  2. Sous macOS, « sans fenêtres » n''est pas « sans app ». Toute logique de démontage doit tenir compte de la plateforme où un processus sans fenêtre continue de tourner. Teste le chemin de l''échec, sur le vrai OS, pas seulement le chemin heureux.
  3. La fonctionnalité que tu livres via le système de mise à jour ne peut pas être testée via le système de mise à jour. Cette asymétrie explique pourquoi la mise à jour automatique mérite un code paranoïaque, inconditionnel et vérifié à la main en profondeur. Tu n''as le droit de la corriger par la voie facile qu''après qu''elle fonctionne déjà.

C''est le tout premier chapitre du travail de fiabilité qui est devenu à terme GeekBye v2. Pour voir où ce chemin a mené, lis ce que coûte vraiment une version 2 (v2.0.0) et tout l''arc dans l''anatomie d''une livraison de logiciel à la perfection.