labfy-investigation/docs/tickets/closed/TICKET-034.md
2026-07-17 10:55:40 +02:00

18 KiB
Raw Blame History

Ticket #034 — Modèle et exécuteur de tâches asynchrones

Contexte

Labfy Investigation va bientôt exécuter des traitements potentiellement longs :

  • calcul dempreintes ;
  • copie de fichiers ;
  • extraction de métadonnées ;
  • lancement doutils 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 dactivité. 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 :

BackgroundTask

capable de :

  • exécuter une fonction de travail dans un thread GLib ;
  • conserver son état ;
  • suivre sa progression ;
  • accepter une demande dannulation ;
  • 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 sappuyer sur :

GTask
GCancellable
GMutex
gatomicrefcount

Il ne doit dépendre ni de GTK, ni de SQLite, ni dune enquête particulière.


Architecture attendue

Créer :

include/core/background_task.h
src/core/background_task.c
tests/test_background_task.c

Le flux général doit être :

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 :

include/core/background_task.h

définir :

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 :

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

typedef struct BackgroundTask BackgroundTask;

La structure interne ne doit jamais apparaître dans le header.


3. Définir le worker

Ajouter :

typedef gboolean (*BackgroundTaskWorker)(
    BackgroundTask *task,
    GCancellable *cancellable,
    gpointer worker_data,
    gpointer *result,
    GError **error
);

Contrat :

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 sexé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 :

background_task_report_progress()

depuis son thread.


4. Définir le callback final

Ajouter :

typedef void (*BackgroundTaskCompletionCallback)(
    BackgroundTask *task,
    gpointer user_data
);

Le callback doit être appelé après la mise à jour de létat final.

Lorsquune 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 derreur

Ajouter :

typedef enum
{
    BACKGROUND_TASK_ERROR_INVALID_ARGUMENT,
    BACKGROUND_TASK_ERROR_ALREADY_STARTED,
    BACKGROUND_TASK_ERROR_WORKER_PROTOCOL
} BackgroundTaskError;

Puis :

#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

BackgroundTask *background_task_new(
    const char *title
);

BackgroundTask *background_task_ref(
    BackgroundTask *task
);

void background_task_unref(
    BackgroundTask *task
);

background_task_new() doit refuser :

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 lexécution même si son propriétaire visuel disparaît.


Démarrage

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 :

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 :

lappelant conserve worker_data
lappelant 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 nest 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 jusquau callback final.

Annulation

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 :

g_cancellable_set_error_if_cancelled()

ou :

g_cancellable_is_cancelled()

Lannulation ne doit jamais tuer brutalement un thread.


Progression

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 nest 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 dactivité.


Accesseurs

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

get_title() :
    pointeur emprunté
    valide pendant la durée de vie de task
    titre immuable

dup_status_message() :
    nouvelle chaîne
    lappelant doit appeler g_free()

dup_error() :
    nouvelle copie
    lappelant doit appeler g_error_free()

get_result() :
    pointeur emprunté
    ne doit jamais être libéré par lappelant

get_result() ne doit être considéré comme valide quaprès létat :

BACKGROUND_TASK_STATE_COMPLETED

7. Structure interne recommandée

Dans :

src/core/background_task.c

la structure peut contenir :

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 dexécution

Créer une structure privée, par exemple :

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 jusquau callback final.


9. Fonction exécutée dans le thread

Créer un trampoline privé compatible avec :

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 lerreur avec g_task_return_error() ;
  6. créer une erreur BACKGROUND_TASK_ERROR_WORKER_PROTOCOL si le worker viole son contrat.

Cas à traiter :

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 lexécution échoue, il doit être détruit avec result_destroy.


10. Callback interne de fin

Créer un callback privé compatible avec :

GAsyncReadyCallback

Il doit :

  1. appeler g_task_propagate_pointer() ;
  2. déterminer le nouvel état ;
  3. conserver le résultat ou lerreur ;
  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 :

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 quaucune référence interne dexé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.

Lappel suivant doit être accepté :

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 :

  • lappel du worker ;
  • lappel du callback utilisateur ;
  • une fonction de destruction fournie par lappelant ;
  • un appel potentiellement bloquant.

13. Tests unitaires

Créer :

tests/test_background_task.c

Les tests doivent utiliser un GMainLoop pour attendre le callback final.

Test de construction

Vérifier :

background_task_new(NULL) == NULL
background_task_new("") == NULL

Vérifier quune tâche valide commence avec :

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 :

é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 :

FALSE

avec une erreur :

G_IO_ERROR_FAILED

Vérifier :

état FAILED
résultat NULL
erreur conservée
message conservé

Test de protocole invalide

Créer un worker qui retourne :

FALSE

sans renseigner GError.

Vérifier :

état FAILED
domaine BACKGROUND_TASK_ERROR
code BACKGROUND_TASK_ERROR_WORKER_PROTOCOL

Test dannulation

Créer un worker qui travaille par petites étapes et vérifie régulièrement le GCancellable.

Programmer :

background_task_cancel()

depuis le contexte principal avec g_timeout_add().

Vérifier :

é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 :

background_task_start()

Vérifier :

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 nest 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 lappelant.

Vérifier que :

  • la tâche reste vivante jusquau callback ;
  • aucun accès mémoire invalide na lieu ;
  • la destruction finale intervient après la fin de lexécution.

14. Makefile

Le code de production est déjà découvert automatiquement si le Makefile utilise :

SRC := $(shell find src -name "*.c")

Ajouter toutefois une cible de test dédiée :

TEST_BACKGROUND_TASK := tests/test_background_task

La cible doit compiler au minimum :

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 :

test
clean

Sortie attendue :

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 ;
  • dexécution de commande externe ;
  • dadaptateur 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 dacceptation

  • 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 quune fois.
  • Le worker sexécute dans un thread secondaire.
  • Le callback final revient sur le contexte principal.
  • La progression est comprise entre 0.0 et 1.0.
  • Lannulation est coopérative.
  • Le résultat est conservé jusquà la destruction.
  • Lerreur 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 nest 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 labsence de GTK et SQLite :

rg -n \
    '#include <gtk|sqlite3_|Database|Investigation' \
    include/core/background_task.h \
    src/core/background_task.c

Résultat attendu :

aucune sortie

Vérifier les primitives asynchrones :

rg -n \
    'GTask|GCancellable|GMutex|g_atomic_ref_count' \
    include/core/background_task.h \
    src/core/background_task.c

Vérifier quaucun thread POSIX brut nest ajouté :

rg -n \
    'pthread_|pthread.h' \
    include/core/background_task.h \
    src/core/background_task.c

Résultat attendu :

aucune sortie

Vérifier labsence de sortie forcée :

rg -n \
    '\bexit\s*\(' \
    src/core/background_task.c \
    tests/test_background_task.c

Résultat attendu :

aucune sortie

18. Fichiers concernés

include/core/background_task.h
src/core/background_task.c
tests/test_background_task.c
Makefile

Aucune modification de Application ou de GTK nest attendue.


19. Commit attendu

make clean
make
make test
git diff --check
git status --short
git add \
    include/core/background_task.h \
    src/core/background_task.c \
    tests/test_background_task.c \
    Makefile
git diff --cached --stat
git diff --cached
git commit -m "feat(core): add asynchronous background task"
git push

Résultat attendu

Après ce ticket, Labfy disposera dune primitive générique pour exécuter proprement les futurs traitements longs :

SHA-256
copie de preuve
ExifTool
dig
RDAP
TLS
HTTP
recherche Web
génération de rapport

Le ticket #035 pourra ensuite construire une file de tâches et un panneau dactivité au-dessus de cette abstraction.