Retour au journal de dev Point d’ingénierie

Trois façons de figer une app Mac — et comment Desk 0.9.0 les corrige toutes

SharkTTY Desk 0.9.0 est disponible dès aujourd'hui — Nouveautés donne le résumé côté utilisateur, et vous pouvez le télécharger ici. La fonctionnalité que nous mettons en avant là-bas, ce sont les liens de partage invité : inviter quelqu'un sur votre Mac avec un lien ou un QR code, sans compte nécessaire. Ce billet raconte une autre histoire — celle des coulisses, la traque de quelque chose de bien moins photogénique qu'une nouvelle fonctionnalité : l'app entière qui se fige complètement, roue arc-en-ciel tournant sans fin, pour des raisons qu'il a vraiment fallu du travail pour cerner. Trois causes racines indépendantes, trois correctifs séparés, tous dans cette build.

Cause une : un envoi bloquant sur le fil de l'interface

Quand vous consultez un autre Mac depuis Desk, la vidéo et les entrées transitent par un canal de données WebRTC, et le code qui envoyait sur ce canal s'exécutait de façon synchrone — sur le fil de l'interface. Dans des conditions normales, c'est invisible : l'envoi revient quasi instantanément. Mais le transport SCTP de WebRTC maintient un tampon d'envoi de taille fixe, 128 Kio, et si l'autre côté prend du retard et que ce tampon se remplit — de la contre-pression —, l'envoi peut se bloquer. Indéfiniment. Et quand ce blocage survient sur le fil de l'interface, ce n'est pas un à-coup dans un flux vidéo : c'est toute l'app. Les menus ne s'ouvrent plus, les fenêtres ne se redessinent plus, la roue tourne jusqu'à ce que quelque chose en amont cède. Les chemins de transport WebSocket et QUIC n'ont jamais eu ce problème — ils ne font pas transiter leurs envois par le même appel bloquant.

Le correctif : les envois sont désormais non bloquants. La leçon que nous en tirons est simple : « ça revient toujours assez vite » est une hypothèse, pas une garantie, et le fil de l'interface ne devrait jamais être celui qui la met à l'épreuve. Tout appel dont le pire cas est « se bloquer jusqu'à ce que le pair distant vide un tampon » n'a rien à faire à s'exécuter de façon synchrone dans la boucle de rendu, aussi rare que ce pire cas ait pu sembler en pratique.

Cause deux : un tube que personne ne vidait

La deuxième cause est un piège Unix ancien et bien connu, facile à réintroduire sans s'en rendre compte. Le tube de sortie d'un sous-processus a un petit tampon noyau — environ 64 Ko — et si personne ne le lit, le tampon se remplit, le processus enfant se bloque en essayant d'écrire davantage, et tout ce qui attend la fin de cet enfant se bloque aussi : waitUntilExit ne revient jamais, parce que le processus attendu est coincé à écrire dans un tube que personne n'écoute.

Un appel correspondant exactement à ce schéma tournait sur le fil principal toutes les 1,5 seconde dès que l'auxiliaire de capture d'écran verrouillé était activé — une vérification périodique qui, une fois sa cible saturant son tube, pouvait coincer l'app entière avec la régularité d'une horloge, et pas une seule fois mais de façon répétée. C'est ce qui rendait cette cause particulièrement perturbatrice : pas un cas limite rare, mais une minuterie récurrente s'écrasant directement sur un interblocage.

Le correctif comporte deux volets : la sortie que nous n'avons pas l'intention de lire part désormais vers le périphérique nul plutôt que vers un tube, et la sortie que nous consommons réellement est vidée avant d'attendre le processus. Leçon : le tube que vous créez est un contrat à le lire — si vous ne comptez pas le lire, n'en créez pas ; si vous le lisez, videz-le avant de vous bloquer en attendant celui qui y écrit.

Cause trois : un historique de plus en plus lent à l'usage

La troisième n'était pas tant un blocage qu'une hémorragie lente : les sessions de terminal mosh longues se synchronisaient bien au début, puis ralentissaient sensiblement à mesure qu'elles duraient. La cause était algorithmique : le coût de synchronisation de l'historique du terminal évoluait de façon quadratique avec la quantité d'historique, O(H²). Ni verrou ni fuite, juste une approche parfaite pour une session de cinq minutes et de plus en plus coûteuse pour une session de cinq heures — jusqu'à ressembler, vu de l'extérieur, au même genre de gel que causaient les deux autres.

Le correctif restructure l'historique pour utiliser un partage structurel, afin que les sessions longues restent rapides au lieu de ralentir avec l'âge. Leçon : le coût quadratique est invisible dans tout test rapide et toute démo, parce qu'il ne se manifeste qu'une fois qu'une session a tourné assez longtemps pour que H compte vraiment — ce qui veut dire que ce sont toujours vos vrais utilisateurs qui le découvrent, sur leurs sessions réellement les plus longues, à moins d'aller le chercher exprès.

Les liens de partage, désormais en https

La fonctionnalité mise en avant par 0.9.0, ce sont les liens de partage invité : depuis le panneau Partage sur votre Mac, vous pouvez inviter quelqu'un avec un lien ou un QR code, l'hôte contrôlant l'expiration, le nombre de personnes pouvant regarder en même temps, si c'est en lecture seule, et une liste blanche de comptes optionnelle — et chaque partage peut être révoqué à tout moment. Le lien lui-même ne transporte jamais la clé de connexion permanente de votre Mac : les invités échangent des tickets de courte durée émis par le cloud, et c'est l'agent sur votre Mac, pas le cloud, qui vérifie et applique l'expiration, les limites de concurrence, le mode lecture seule et la révocation. Il existe aussi une option précoce et gratuite pour proposer un partage sous forme de réservation exclusive à l'heure, où l'invité choisit un créneau horaire libre aligné sur l'UTC et obtient votre Mac en exclusivité pour cette fenêtre.

Ce que change 0.9.0, c'est la forme du lien lui-même : c'est désormais une URL https — https://safafish.com/s/#s=<code>&n=<name> — plutôt qu'une adresse ttyshare:// brute. Un lien https s'ouvre dans n'importe quel navigateur, sur n'importe quel appareil, vers une page de transfert qui propose « Ouvrir dans SharkTTY », pointe vers l'App Store si l'app n'est pas encore installée, et affiche un QR code à scanner depuis un iPhone ou un iPad. Coller le lien https dans l'app sur un appareil qui l'a déjà fonctionne aussi dans l'autre sens. Pour être précis sur ce que ce n'est pas encore : toucher le lien n'ouvre pas instantanément l'app iOS à lui seul — ce transfert dépend d'une build de l'app que nous n'avons pas encore publiée. Ce que vous obtenez aujourd'hui, c'est un lien qui s'ouvre dans n'importe quel navigateur vers une page qui le transmet à SharkTTY, ce qui est déjà bien plus accueillant à coller dans une discussion qu'un schéma d'URI que la moitié des appareils de vos invités ne reconnaîtront pas.

Aussi dans 0.9.0

Deux détails plus modestes qui méritent une mention. Dans les terminaux de Desk — SSH, shell local et mosh confondus — maintenir Option enfoncée et cliquer déplace désormais le curseur du shell à cet endroit, le même tour que jouent Terminal.app et iTerm2, via des touches fléchées synthétisées ; ça fonctionne aussi bien dans le tampon principal que dans les apps plein écran, sur l'écran alternatif. Et la loupe de précision avait un petit bogue : une fenêtre de limitation pouvait ignorer le dernier mouvement d'un glissement, si bien que la loupe se posait parfois un peu en retrait de l'endroit où le curseur s'était réellement arrêté. La position de repos reçoit désormais toujours sa mise à jour, si bien que le contenu agrandi se pose exactement sous le réticule.

Des questions ou des rapports de bogue sur l'un des points ci-dessus ? Le forum de retours est le moyen le plus rapide de nous joindre.