# Ticket #034 — Modèle et exécuteur de tâches asynchrones ## Contexte Labfy Investigation va bientôt exécuter des traitements potentiellement longs : - calcul d’empreintes ; - copie de fichiers ; - extraction de métadonnées ; - lancement d’outils externes ; - recherches DNS et réseau ; - analyse de résultats ; - génération de rapports. Ces opérations ne doivent jamais bloquer la boucle principale GTK. Le ticket #035 ajoutera une file de tâches et un panneau d’activité. Avant cela, il faut créer une abstraction asynchrone fiable, indépendante de GTK et réutilisable par tous les futurs modules. ## Objectif Créer un type opaque : ```c BackgroundTask ``` capable de : - exécuter une fonction de travail dans un thread GLib ; - conserver son état ; - suivre sa progression ; - accepter une demande d’annulation ; - conserver un résultat ; - conserver une erreur ; - enregistrer ses dates de début et de fin ; - appeler un callback de fin sur le contexte principal ; - garantir une gestion correcte de sa durée de vie. Le module doit s’appuyer sur : ```text GTask GCancellable GMutex gatomicrefcount ``` Il ne doit dépendre ni de GTK, ni de SQLite, ni d’une enquête particulière. --- # Architecture attendue Créer : ```text include/core/background_task.h src/core/background_task.c tests/test_background_task.c ``` Le flux général doit être : ```text background_task_new() ↓ background_task_start() ↓ GTask exécute le worker dans un thread ↓ le worker signale sa progression ↓ succès / erreur / annulation ↓ callback de fin sur le contexte principal ↓ background_task_unref() ``` --- # 1. Définir les états publics Dans : ```text include/core/background_task.h ``` définir : ```c typedef enum { BACKGROUND_TASK_STATE_PENDING, BACKGROUND_TASK_STATE_RUNNING, BACKGROUND_TASK_STATE_COMPLETED, BACKGROUND_TASK_STATE_FAILED, BACKGROUND_TASK_STATE_CANCELLED } BackgroundTaskState; ``` Transitions autorisées : ```text PENDING → RUNNING RUNNING → COMPLETED RUNNING → FAILED RUNNING → CANCELLED ``` Une tâche terminée ne peut jamais être redémarrée. --- # 2. Définir le type opaque ```c typedef struct BackgroundTask BackgroundTask; ``` La structure interne ne doit jamais apparaître dans le header. --- # 3. Définir le worker Ajouter : ```c typedef gboolean (*BackgroundTaskWorker)( BackgroundTask *task, GCancellable *cancellable, gpointer worker_data, gpointer *result, GError **error ); ``` Contrat : ```text succès : retourne TRUE error reste NULL result peut être NULL ou contenir un résultat échec : retourne FALSE error doit normalement être renseignée result doit rester NULL annulation : retourne FALSE error appartient au domaine G_IO_ERROR code G_IO_ERROR_CANCELLED ``` Le worker s’exécute dans un thread secondaire. Il ne doit jamais : - manipuler directement GTK ; - accéder à un widget ; - modifier une structure non protégée ; - appeler le callback final lui-même. Le worker peut appeler : ```c background_task_report_progress() ``` depuis son thread. --- # 4. Définir le callback final Ajouter : ```c typedef void (*BackgroundTaskCompletionCallback)( BackgroundTask *task, gpointer user_data ); ``` Le callback doit être appelé après la mise à jour de l’état final. Lorsqu’une tâche est lancée depuis le thread principal GTK, le callback doit revenir sur ce contexte principal grâce au comportement de `GTask`. --- # 5. Définir le domaine d’erreur Ajouter : ```c typedef enum { BACKGROUND_TASK_ERROR_INVALID_ARGUMENT, BACKGROUND_TASK_ERROR_ALREADY_STARTED, BACKGROUND_TASK_ERROR_WORKER_PROTOCOL } BackgroundTaskError; ``` Puis : ```c #define BACKGROUND_TASK_ERROR \ background_task_error_quark() GQuark background_task_error_quark(void); ``` Utilisation : - argument invalide ; - seconde tentative de démarrage ; - worker retournant `FALSE` sans fournir de `GError` ; - worker retournant `TRUE` tout en fournissant une erreur. --- # 6. API publique attendue ## Construction et références ```c BackgroundTask *background_task_new( const char *title ); BackgroundTask *background_task_ref( BackgroundTask *task ); void background_task_unref( BackgroundTask *task ); ``` `background_task_new()` doit refuser : ```text title == NULL title vide ``` La tâche utilise un comptage de références atomique. Ne pas exposer une fonction `background_task_free()`. Cette décision est importante : la tâche doit pouvoir rester vivante pendant l’exécution même si son propriétaire visuel disparaît. --- ## Démarrage ```c gboolean background_task_start( BackgroundTask *task, BackgroundTaskWorker worker, gpointer worker_data, GDestroyNotify worker_data_destroy, GDestroyNotify result_destroy, BackgroundTaskCompletionCallback completion_callback, gpointer completion_data, GDestroyNotify completion_data_destroy, GError **error ); ``` ### Propriété des paramètres Si le démarrage réussit : ```text BackgroundTask prend en charge worker_data BackgroundTask prend en charge completion_data BackgroundTask prend en charge le futur result ``` Les fonctions de destruction correspondantes seront appelées au moment approprié. Si le démarrage échoue : ```text l’appelant conserve worker_data l’appelant conserve completion_data ``` ### Contraintes La fonction doit : 1. vérifier `task` ; 2. vérifier `worker` ; 3. vérifier la convention `GError` ; 4. refuser une tâche qui n’est plus `PENDING` ; 5. créer un `GCancellable` ; 6. passer l’état à `RUNNING` ; 7. enregistrer la date de début ; 8. lancer le worker avec `g_task_run_in_thread()` ; 9. conserver une référence interne jusqu’au callback final. --- ## Annulation ```c void background_task_cancel( BackgroundTask *task ); gboolean background_task_is_cancelled( const BackgroundTask *task ); ``` `background_task_cancel()` exprime une demande. Le worker reste responsable de vérifier régulièrement : ```c g_cancellable_set_error_if_cancelled() ``` ou : ```c g_cancellable_is_cancelled() ``` L’annulation ne doit jamais tuer brutalement un thread. --- ## Progression ```c void background_task_report_progress( BackgroundTask *task, double progress, const char *status_message ); ``` Règles : - utilisable depuis le worker ; - protégée par `GMutex` ; - valeur limitée entre `0.0` et `1.0` ; - ignorée si la tâche n’est pas `RUNNING` ; - `status_message` est copié ; - `status_message == NULL` est accepté. Le ticket #035 pourra lire régulièrement ces valeurs pour mettre à jour le panneau d’activité. --- ## Accesseurs ```c const char *background_task_get_title( const BackgroundTask *task ); BackgroundTaskState background_task_get_state( const BackgroundTask *task ); double background_task_get_progress( const BackgroundTask *task ); char *background_task_dup_status_message( const BackgroundTask *task ); gint64 background_task_get_started_at_us( const BackgroundTask *task ); gint64 background_task_get_finished_at_us( const BackgroundTask *task ); GError *background_task_dup_error( const BackgroundTask *task ); gpointer background_task_get_result( const BackgroundTask *task ); ``` ### Propriété des valeurs ```text get_title() : pointeur emprunté valide pendant la durée de vie de task titre immuable dup_status_message() : nouvelle chaîne l’appelant doit appeler g_free() dup_error() : nouvelle copie l’appelant doit appeler g_error_free() get_result() : pointeur emprunté ne doit jamais être libéré par l’appelant ``` `get_result()` ne doit être considéré comme valide qu’après l’état : ```text BACKGROUND_TASK_STATE_COMPLETED ``` --- # 7. Structure interne recommandée Dans : ```text src/core/background_task.c ``` la structure peut contenir : ```c struct BackgroundTask { gatomicrefcount reference_count; GMutex mutex; char *title; char *status_message; BackgroundTaskState state; double progress; gint64 started_at_us; gint64 finished_at_us; GCancellable *cancellable; gpointer result; GDestroyNotify result_destroy; GError *error; BackgroundTaskCompletionCallback completion_callback; gpointer completion_data; GDestroyNotify completion_data_destroy; }; ``` Les champs mutables doivent être protégés par `mutex`. Le titre est immuable après construction. --- # 8. Contexte interne d’exécution Créer une structure privée, par exemple : ```c typedef struct { BackgroundTask *task; BackgroundTaskWorker worker; gpointer worker_data; GDestroyNotify worker_data_destroy; GDestroyNotify result_destroy; } BackgroundTaskRunContext; ``` Le `worker_data` doit être détruit lorsque le contexte `GTask` est libéré. Le pointeur `task` peut être non propriétaire si une référence interne distincte garantit sa durée de vie jusqu’au callback final. --- # 9. Fonction exécutée dans le thread Créer un trampoline privé compatible avec : ```c GTaskThreadFunc ``` Il doit : 1. récupérer le contexte ; 2. appeler le worker ; 3. vérifier le contrat de retour ; 4. retourner le résultat avec `g_task_return_pointer()` ; 5. retourner l’erreur avec `g_task_return_error()` ; 6. créer une erreur `BACKGROUND_TASK_ERROR_WORKER_PROTOCOL` si le worker viole son contrat. Cas à traiter : ```text FALSE + error valide : échec normal FALSE + error NULL : erreur de protocole TRUE + error non NULL : erreur de protocole TRUE + result quelconque : succès ``` Si un résultat a été produit alors que l’exécution échoue, il doit être détruit avec `result_destroy`. --- # 10. Callback interne de fin Créer un callback privé compatible avec : ```c GAsyncReadyCallback ``` Il doit : 1. appeler `g_task_propagate_pointer()` ; 2. déterminer le nouvel état ; 3. conserver le résultat ou l’erreur ; 4. enregistrer la date de fin ; 5. forcer la progression à `1.0` en cas de succès ; 6. appeler le callback utilisateur ; 7. détruire les données du callback utilisateur ; 8. libérer la référence interne de la tâche. Détermination de l’état : ```text aucune erreur : COMPLETED G_IO_ERROR_CANCELLED : CANCELLED autre erreur : FAILED ``` Le callback utilisateur doit observer un objet déjà entièrement finalisé. --- # 11. Destruction de la tâche Quand la dernière référence est libérée : 1. vérifier qu’aucune référence interne d’exécution ne subsiste ; 2. détruire `result` avec `result_destroy` ; 3. libérer `error` ; 4. libérer les chaînes ; 5. libérer `GCancellable` ; 6. libérer les éventuelles données de callback restantes ; 7. nettoyer `GMutex` ; 8. libérer la structure. L’appel suivant doit être accepté : ```c background_task_unref(NULL); ``` --- # 12. Sécurité des threads Les opérations suivantes doivent utiliser `GMutex` : - lecture et écriture de l’état ; - progression ; - message de progression ; - dates ; - résultat ; - erreur ; - accès au cancellable si nécessaire. Ne jamais conserver le mutex verrouillé pendant : - l’appel du worker ; - l’appel du callback utilisateur ; - une fonction de destruction fournie par l’appelant ; - un appel potentiellement bloquant. --- # 13. Tests unitaires Créer : ```text tests/test_background_task.c ``` Les tests doivent utiliser un `GMainLoop` pour attendre le callback final. ## Test de construction Vérifier : ```c background_task_new(NULL) == NULL background_task_new("") == NULL ``` Vérifier qu’une tâche valide commence avec : ```text PENDING progression 0.0 date de début 0 date de fin 0 aucune erreur aucun résultat ``` ## Test de succès Créer un worker qui : 1. signale plusieurs progressions ; 2. renvoie une chaîne allouée ; 3. retourne `TRUE`. Vérifier dans le callback : ```text état COMPLETED progression 1.0 résultat correct erreur NULL date de début > 0 date de fin >= date de début callback appelé une seule fois ``` Vérifier que `result_destroy` est appelé lors du dernier `unref`. ## Test d’échec Créer un worker qui retourne : ```c FALSE ``` avec une erreur : ```text G_IO_ERROR_FAILED ``` Vérifier : ```text état FAILED résultat NULL erreur conservée message conservé ``` ## Test de protocole invalide Créer un worker qui retourne : ```c FALSE ``` sans renseigner `GError`. Vérifier : ```text état FAILED domaine BACKGROUND_TASK_ERROR code BACKGROUND_TASK_ERROR_WORKER_PROTOCOL ``` ## Test d’annulation Créer un worker qui travaille par petites étapes et vérifie régulièrement le `GCancellable`. Programmer : ```c background_task_cancel() ``` depuis le contexte principal avec `g_timeout_add()`. Vérifier : ```text état CANCELLED erreur G_IO_ERROR_CANCELLED callback final appelé aucun crash ``` ## Test de double démarrage Démarrer une tâche puis rappeler immédiatement : ```c background_task_start() ``` Vérifier : ```text FALSE BACKGROUND_TASK_ERROR_ALREADY_STARTED ``` La première exécution doit continuer normalement. ## Test des données utilisateur Vérifier que : - `worker_data_destroy` est appelé exactement une fois ; - `completion_data_destroy` est appelé exactement une fois ; - aucune donnée n’est détruite lorsque `background_task_start()` échoue avant transfert de propriété. ## Test du comptage de références Démarrer une tâche puis libérer immédiatement la référence de l’appelant. Vérifier que : - la tâche reste vivante jusqu’au callback ; - aucun accès mémoire invalide n’a lieu ; - la destruction finale intervient après la fin de l’exécution. --- # 14. Makefile Le code de production est déjà découvert automatiquement si le Makefile utilise : ```make SRC := $(shell find src -name "*.c") ``` Ajouter toutefois une cible de test dédiée : ```make TEST_BACKGROUND_TASK := tests/test_background_task ``` La cible doit compiler au minimum : ```text tests/test_background_task.c src/core/background_task.c ``` Lier avec les paquets GLib/GIO déjà utilisés par le projet. Ajouter le binaire aux cibles : ```text test clean ``` Sortie attendue : ```text BackgroundTask : tous les tests sont valides. ``` --- # 15. Hors périmètre Ce ticket ne doit pas encore ajouter : - de file de tâches ; - de limite de concurrence ; - de panneau GTK ; - de persistance SQLite ; - de tâche associée à une enquête ; - d’exécution de commande externe ; - d’adaptateur ExifTool ; - de recherche DNS ; - de système de notifications ; - de reprise après redémarrage ; - de priorité entre tâches. Ces fonctions viendront dans les tickets suivants. --- # 16. Critères d’acceptation - [ ] `BackgroundTask` est opaque. - [ ] Le module ne dépend pas de GTK. - [ ] Le module ne dépend pas de SQLite. - [ ] Le module utilise `GTask`. - [ ] Le module utilise `GCancellable`. - [ ] Le module utilise un comptage de références. - [ ] Le module protège son état avec `GMutex`. - [ ] Une tâche ne peut être démarrée qu’une fois. - [ ] Le worker s’exécute dans un thread secondaire. - [ ] Le callback final revient sur le contexte principal. - [ ] La progression est comprise entre `0.0` et `1.0`. - [ ] L’annulation est coopérative. - [ ] Le résultat est conservé jusqu’à la destruction. - [ ] L’erreur est conservée jusqu’à la destruction. - [ ] Les dates de début et de fin sont enregistrées. - [ ] Les données utilisateur sont détruites exactement une fois. - [ ] La tâche reste vivante pendant son exécution. - [ ] Aucun callback utilisateur n’est appelé sous mutex. - [ ] Tous les tests unitaires passent. - [ ] Les anciens tests restent valides. - [ ] `make` réussit. - [ ] `make test` réussit. - [ ] `git diff --check` ne retourne aucune erreur. --- # 17. Audit attendu Vérifier l’absence de GTK et SQLite : ```bash rg -n \ '#include