Aller au contenu principal

Protection Anti-Bootloop, Rollback automatique et Acquittement

Un risque critique de tout système de mise à jour à chaud est le déploiement d'un patch contenant une anomalie qui provoque un crash dès l'initialisation de l'application (bootloop). TkushPatch met en œuvre une double barrière de protection :

  1. Au niveau du moteur natif Rust (updater/library/src/updater.rs) : détection automatique d'échec de démarrage, révocation locale immédiate du patch défectueux et envoi d'un événement télémétrique PatchInstallFailure.
  2. Au niveau du SDK Flutter tk_ota_push (OtaRollbackTracker dans lib/src/security/ota_security_shield.dart) : détection de la régression du numéro de patch (current_patch < last_executed_patch) et déclenchement du hook onRollbackDetected pour purger les caches applicatifs locaux.

1. Mécanisme natif Anti-Bootloop (Moteur Rust)​

Fonctionnement du bouclier anti-crash (Rollback Shield)
  1. Démarrage sous surveillance : Au lancement de l'application, un compteur de tentatives est incrémenté.
  2. Confirmation de stabilité : Dès que l'interface s'affiche avec succès, le patch actif est marqué comme stable.
  3. Repli automatique si échec : Si l'application plante avant confirmation, le moteur mobile désactive le patch et redémarre instantanément sur la release de base saine.

Détection de crash au démarrage (handle_prior_boot_failure_if_necessary)​

Lorsqu'un patch est chargé au démarrage, le moteur Rust enregistre son numéro dans state.json comme patch en cours d'amorçage (currently_booting_patch) avec l'horodatage boot_started_at.

Si l'application crashe avant que le moteur Flutter n'ait confirmé le démarrage complet, l'ouverture suivante de l'application exécute handle_prior_boot_failure_if_necessary() (updater/library/src/updater.rs) :

  1. Le moteur constate que currently_booting_patch est toujours renseigné.
  2. Il appelle state.record_boot_failure_for_patch(patch.number) : le patch est marqué définitivement comme défectueux (PatchKnownBad) sur cet appareil afin de ne plus jamais tenter de le charger (UpdateStatus::UpdateIsBadPatch).
  3. Il prépare un message de diagnostic précis : crash_recovery: patch <N> failed to boot (detected_at=<ts>,boot_started_at=<ts>,file_ok=<bool>,file_size=<bytes>)
  4. Il met en file d'attente un événement EventType::PatchInstallFailure (__patch_install_failure__) qui est transmis au serveur sur POST /api/v1/patches/events.

Rollback piloté par le serveur (rolled_back_patch_numbers)​

En plus de la détection locale de crash, le serveur peut ordonner la révocation de patchs via le tableau rolled_back_patch_numbers dans la réponse de POST /api/v1/patches/check. Dès réception de cette liste, la fonction roll_back_patches_if_needed(rolled_back_patches) du moteur Rust désinstalle localement les patchs révoqués.

2. Résilience côté Dart et nettoyage du cache (OtaRollbackTracker)​

Lorsqu'une application subit un rollback du Patch #2 vers le Patch #1 (ou vers le binaire Store #0), le Patch #2 a pu écrire des données dans SharedPreferences, Hive ou SQLite avec un nouveau schéma que le code du Patch #1 ne comprend pas.

Le SDK tk_ota_push résout ce problème grâce à OtaRollbackTracker :

final ota = TkOtaPush.init(
appReleaseVersion: '1.0.0+3',
// Optionnel : utiliser FileOtaPatchStateStore pour persister last_executed_patch sur disque
rollbackTracker: OtaRollbackTracker(
store: FileOtaPatchStateStore(File('/chemin/sandbox/ota_patch_state.json')),
),
onRollbackDetected: (OtaRollbackEvent event) async {
debugPrint(
'Rollback détecté sur la release ${event.releaseVersion} '
'à ${event.detectedAt.toIso8601String()} : '
'Patch #${event.previousPatchNumber} -> Patch #${event.restoredPatchNumber}',
);
// Purger ici les caches locaux potentiellement incompatibles avant runApp()
},
);

Fonctionnement de verifyStartupAndHandleRollback()​

  1. À chaque appel de syncWithRemoteConfig (Étape 0a) ou d'un appel explicite à verifyStartupAndHandleRollback(), le SDK lit le patch actif (currentPatchNumber() ?? 0).
  2. OtaRollbackTracker.evaluateAndRecord compare ce numéro avec last_executed_patch enregistré pour cette releaseVersion dans OtaPatchStateStore (InMemoryOtaPatchStateStore par défaut ou FileOtaPatchStateStore sur fichier JSON).
  3. Il met à jour last_executed_patch avec la valeur courante.
  4. Si effectiveCurrent < previous, il retourne un objet OtaRollbackEvent contenant :
    • releaseVersion (String)
    • previousPatchNumber (int)
    • restoredPatchNumber (int, 0 si retour à la Release Store de base)
    • detectedAt (DateTime en UTC)
  5. Le callback onRollbackDetected est immédiatement attendu (await) avant la suite de l'initialisation.

3. Télémétrie envoyée au serveur (POST /api/v1/patches/events)​

Le moteur Rust envoie les événements suivants (définis dans updater/library/src/events.rs) au contrôleur ShorebirdUpdaterController.reportPatchEvent :

Enum Rust (EventType)Identifiant JSON (type)Déclencheur
PatchDownload__patch_download__Le téléchargement binaire du patch est terminé.
PatchInstallSuccess__patch_install__Le patch a démarré avec succès au lancement suivant.
PatchInstallFailure__patch_install_failure__Le patch a échoué au démarrage (crash_recovery) ou lors de l'installation.
PatchUpdateFailure__patch_update_failure__Échec lors de la vérification ou de la préparation de la mise à jour.

Chaque événement PatchEvent contient également une classification normalisée failure_category (FailureCategory dans updater.rs) :

  • "network" (réessayable)
  • "server" (réessayable)
  • "out_of_space" (non réessayable)
  • "permission_denied" (non réessayable)
  • "filesystem" (non réessayable)
  • "corrupt_patch" (non réessayable)
  • "invalid_signature" (non réessayable)
  • "unknown" (réessayable par prudence)

4. Acquittement d'alerte Rollback dans la Console Support (/support)​

Lorsqu'un appareil remonte un événement __patch_install_failure__, son statut passe en alerte Rollback Actif dans l'écran Device Fleet & Support (/support) de la Console Web.

  • Un opérateur ou ingénieur support peut inspecter l'appareil (via son client_id ou son code court #6959347F) et cliquer sur Acquitter l'alerte Rollback.
  • Le serveur enregistre l'horodatage rollback_acknowledged_at sur l'enregistrement de l'appareil sans jamais supprimer ni modifier les événements du journal d'audit télémétrique.
  • L'indicateur de santé de l'appareil repasse à l'état normal tant qu'aucun nouvel échec postérieur (latestFailureAt > rollbackAcknowledgedAt) ne survient.