AUDIT-2026-09-24.md
Rama main · réplica de lectura del repositorio original.
# Audit du 24 septembre 2026 — bugs, améliorations, optimisations
État : sources basées sur `d2282f8` (0.13.3). Lecture complète des modules cœur
(`serve.py`, `runtime.py`, `blink_engine.py`, `blink_registre.py`,
`blink_models.py`, `blink_auth.py`, `blink_ts_demux.py`, `blink_webrtc.py`,
`blink_cli.py`, `maj.py`, `watch.py`, `tray.py`, parties critiques de
`merge_daily.py`) et contrôle ciblé de `serve_app.js` (échappement HTML :
`h()` systématique et CSP à nonce, rien à signaler). Les audits précédents
(`AUDIT-2026-08-13.md`, `AUDIT-2026-09-07.md`) ont été pris comme acquis :
rien de ce qui y est déjà corrigé n'est répété ici.
Le TLS du relais Live View (`BlinkLiveStream.auth`) n'a pas été touché
(voir `AGENTS.md`).
## Corrigé dans cette branche
Chaque correction a son test de non-régression, vérifié en échec sur le
code d'origine puis en succès après correction.
### 1. Course sur `merge_daily.save_json` — requêtes en échec au chargement de la page
`save_json` écrivait toujours dans `path.with_suffix(".tmp")`, un nom partagé
par tous les écrivains. `serve.py` tourne sur un `ThreadingHTTPServer` et
`load()` (`serve_app.js`) demande `/api/clips` et `/api/videos` en parallèle.
Dès qu'il existe au moins un enregistrement du direct et une vidéo
assemblée, les deux routes réécrivent `assembled_durations.json` : le second
`replace` ne trouve plus le temporaire consommé par le premier.
Reproduction : 2 threads × 300 écritures → **197 `FileNotFoundError`**.
Côté page, la requête touchée échoue sans réponse (connexion coupée).
Sous Windows, un temporaire unique ne suffit pas : `os.replace` échoue en
`PermissionError` (« Access is denied ») quand un autre écrivain ou un
lecteur tient la cible ouverte au même instant (162 échecs sur 600 en CI
Windows sur la première version de cette correction).
Correction : temporaire unique par appel via `runtime._ecrire_texte_atomique`,
un verrou par fichier pour les threads d'un même processus, et quelques
nouvelles tentatives rapprochées sur `PermissionError` (lecteurs d'un autre
thread ou processus, antivirus). Test : `test_merge_daily_save_json_concurrence.py`,
dont un cas qui simule la sémantique Windows de `os.replace` sur tous les
systèmes.
### 2. Onboarding : port, fuseau et hôte de confiance des réglages ignorés
`blink_cli.accueillir()` lançait `serve` avec le seul supplément tapé à la
main et sondait `_port_demande(supplement) or 8765`. Avec un port
personnalisé dans les réglages (souvent choisi précisément parce que 8765
est pris), une session expirée rendait la reconnexion impossible : le
serveur temporaire tentait 8765, et le navigateur s'ouvrait sur ce port.
Même problème pour `--trusted-host` (page de connexion en 403 via tunnel).
Correction : même bloc fixe que la composition complète
(`runtime.standard()[1:LONGUEUR_BLOC_SERVE]`), le supplément gardant la
priorité. Tests : `test_blink_cli_supervision.py` (deux cas ajoutés).
### 3. Suppression manuelle : faux « supprimé de la source » après renumérotation USB
`_marquer_sources_supprimees` marquait `source_deleted` toute entrée USB du
module dont le numéro (tiré du nom de fichier) manquait au manifeste. Or le
code affirme lui-même, à trois endroits, que le Sync Module renumérote ses
clips lors d'une réindexation — et la suppression, elle, s'en protège en
rapprochant par caméra et instant. Après une réindexation, tous les clips
encore présents mais renumérotés étaient marqués, ce qui **masquait
définitivement leur case « Supprimer »** dans la galerie.
Correction : n'est marquée absente qu'une entrée dont ni le numéro, ni le
couple (caméra, instant ±2 s) ne figurent au manifeste — hors clips que ce
lot vient de supprimer. Règle strictement plus prudente ; un faux négatif
reste bénin (la demande suivante répond `deja_absent` et marque alors).
Test : `test_serve_selection_blink.py`.
### 4. Hôte de confiance homonyme d'un verbe, ou commençant par « - »
`runtime.standard()` écrit toujours `--trusted-host <valeur>`, mais cette
option manquait à `_OPTIONS_VALEUR_UNIQUE` : une valeur `watch`, `list`,
`open`… ouvrait un nouveau groupe dans `decouper_verbes()`, et `serve`
refusait de démarrer faute de valeur. De même, le validateur acceptait
`-x`, qu'argparse lit comme une option. Corrigé des deux côtés.
Tests : `test_blink2video_audit.py`, `test_serve_reglages_validation.py`.
### 5. `do_POST` : corps JSON non-objet, octets non UTF-8, `Content-Length` négatif
Un corps `[]`, `42` ou `null` levait `AttributeError` sur `payload.get` ;
des octets non UTF-8 levaient `UnicodeDecodeError` (non intercepté) ; un
`Content-Length: -1` faisait `rfile.read(-1)`, qui attend la fermeture d'une
connexion HTTP/1.1 persistante et bloquait le thread. Tous répondent
désormais 400. Seuls des clients déjà munis du jeton y avaient accès :
robustesse plus que sécurité. Test : `test_serve_post_routage.py`.
### 6. Optimisation : `known_identities()` relisait tout le registre à chaque requête média
Chaque `/thumb/clip/…` et chaque requête `Range` du lecteur vidéo passaient
par un parse JSON complet du registre : **~27 ms par requête pour 10 000
clips** (5,4 Mo) sur une machine rapide, bien davantage sous Windows 7 —
plusieurs secondes de CPU pour une page de vignettes. Mémorisé désormais
tant que le fichier ne change pas (clé : chemin, `mtime_ns`, taille, inode ;
le registre est toujours remplacé atomiquement, donc l'inode change à chaque
écriture). Appels suivants : ~5 µs. Test : `test_serve_known_identities_cache.py`.
## Non corrigé — à arbitrer
### Bugs probables
- **Identité de processus sous Linux sensible aux sauts d'horloge**
(`runtime.identite_processus`, `ps -o lstart=`). `lstart` est recalculé à
chaque appel depuis `btime` (`/proc/stat`), que le noyau décale quand
l'horloge murale est réglée d'un coup (NTP en mode « step », VM après
reprise, réglage manuel). Un décalage d'une seconde ou plus change
l'identité de *tous* les processus vivants : `verrou()` purge alors le
verrou d'un propriétaire bien vivant, `lire_instances()` efface des fiches
actives et `stop` refuse de tuer (« PID réattribué »). Linux et Docker
seulement (Windows et macOS mémorisent une heure de création fixe). Non
reproduit : impossible de décaler l'horloge dans l'environnement d'audit.
Piste : champ 22 de `/proc/<pid>/stat` (ticks depuis le démarrage,
insensible aux réglages d'horloge, et sans sous-processus), en acceptant
l'ancien format le temps d'une migration.
- **Agrégats hebdomadaires/mensuels volumineux** (`merge_daily.concat_copy`).
La copie de flux `-c copy +faststart` est coupée après 300 s fixes
(`SILENCE_MAX`). Un mois chargé (≈ 10 Go pour une caméra active) demande
une copie puis une réécriture complète pour `faststart` : sur disque
dur lent ou USB 2.0, 300 s ne suffisent pas. L'échec déclenche alors le
repli `run_ffmpeg_batch`, c'est-à-dire un **réencodage complet du mois**,
recommencé chaque jour où l'empreinte change. Ordre de grandeur estimé,
non mesuré sur matériel réel. Pistes : `-progress pipe:1` et chien de
garde au silence comme `run_ffmpeg_batch`, ou délai proportionnel à la
taille des parties. Hebdo et mensuel sont désactivés par défaut, ce qui
limite l'exposition.
- **Verrous définitivement bloqués après un plantage mal placé**
(`runtime.verrou`). Un processus tué entre `O_CREAT|O_EXCL` et l'écriture
laisse un verrou vide, lu « illisible » à jamais ; tué dans la section de
purge, il laisse un `.purge` qui interdit toute purge future. Fenêtres de
quelques microsecondes, mais conséquence permanente (intervention
manuelle). Piste : écrire PID et identité dans `.purge`, traiter un verrou
vide plus vieux que quelques secondes comme périmé.
- **Superposition du registre** (`blink_registre._ecrire_registre`). Chaque
sauvegarde réécrit la copie complète de l'appelant ; seule l'exclusion a
une fusion dédiée. Un champ posé par un autre processus entre la lecture
et l'écriture (`source_deleted` par la suppression manuelle pendant un
passage de téléchargement) est donc perdu. Impact faible : la case
« Supprimer » réapparaît, et une nouvelle demande répond `deja_absent`.
### Robustesse et maintenance
- **Journaux sans rotation** : `direct.log`, `serve_erreurs.log`, `watch.log`,
`merge.log`, `maj.log` croissent indéfiniment. `serve_erreurs.log` reçoit
un bloc par requête refusée en 403 : avec `BLINK_BIND`, un balayage du
réseau suffit à le faire grossir sans limite.
- **Direct MSE** : `-loglevel info` y est marqué « temporaire ». À ce niveau,
ffmpeg termine ses lignes de progression par `\r`, que `readline()` ne
découpe pas : vérifié, 7 mises à jour en 4 s arrivent en une seule ligne de
719 octets, soit environ 60 Ko d'une traite, à la fin de chaque direct de
5 min. Ajouter `-nostats` ou revenir à `error`.
- **`BlinkSession.call`** : tant que `connect_saved()` échoue, chaque appel
crée une `ClientSession` jamais fermée, soit une fuite de connecteurs.
- **`LoginFlow`** : les boucles asyncio sont arrêtées, jamais fermées.
- **`runtime.marquer()`** : écriture non atomique de `.blink_passages.json` et
lecture-modification-écriture sans verrou entre download, watch et merge.
- **Caches sans purge** : `_VERROUS_MINIATURE`, `_DURATIONS`, et les JSON
`excluded_durations`/`assembled_durations` ne retirent jamais une entrée
dont le fichier a disparu.
- **WebRTC** : le type de charge utile dynamique `110 + 2n` dépasse 127 au
9ᵉ profil H.264 distinct vu par le processus. Le PTS 33 bits n'est pas
traité au rebouclage : environ 0,3 % des sessions de 5 min perdent ensuite
leur cadencement.
- **`/api/choisir-dossier`** crée une fenêtre Tk depuis un thread HTTP. Sous
macOS, Tk exige le thread principal, avec un risque de plantage du
processus en mode sources. Le bundle n'est pas concerné (tkinter exclu).
- **Traduction** : plusieurs messages du serveur restent en français seul
(`_slot_occupe_message`, erreurs de réglages, du direct et de connexion),
alors que l'interface est bilingue.
- **Mise à jour** : l'empreinte SHA-256 vient du même canal (GitHub) que
l'archive, si bien qu'un compte GitHub compromis suffit à publier un
binaire accepté. Piste : signature détachée (minisign, Sigstore).
### Optimisations
- `runtime.traduire()` → `lire_langue()` → `app_dir()` : deux lectures de
fichier et un `resolve()` par message affiché, y compris dans les boucles
par clip.
- `processus_vivant()` et `identite_processus()` lancent `ps` à chaque appel
sous Linux et macOS : un par verrou pris, et un toutes les 3 s via
`/api/travail` pendant chaque calcul. `/proc` suffirait sous Linux.
- `/api/passages` relit tout le registre toutes les 60 s pour n'en compter que
les entrées : même cache que `known_identities()` possible.
- `collect()` : jusqu'à trois `stat` par clip de la fenêtre, et `valid_mp4`
sur chaque enregistrement du direct à chaque `/api/clips`.
## Vérifications
- Suite unitaire complète (`python -B -m unittest discover`, CPython 3.11,
Linux) : **914 tests réussis**, 9 ignorés (896 avant l'audit, 18 ajoutés).
- `python tests.py` (chaîne vidéo et interface, ffmpeg 6.1 de la
distribution) : tout est vert.
- `python docs.py --check` et `python -m compileall -q .` : OK.
- Non exécuté ici : CPython 3.8 (build Windows 7), Windows et macOS.
- Aucun réveil de caméra, aucune suppression distante, aucune session Blink :
tout est simulé ou local.