18 KiB
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 :
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 :
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 :
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 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 :
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.
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 :
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
FALSEsans fournir deGError; - worker retournant
TRUEtout 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 l’exé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 :
l’appelant conserve worker_data
l’appelant conserve completion_data
Contraintes
La fonction doit :
- vérifier
task; - vérifier
worker; - vérifier la convention
GError; - refuser une tâche qui n’est plus
PENDING; - créer un
GCancellable; - passer l’état à
RUNNING; - enregistrer la date de début ;
- lancer le worker avec
g_task_run_in_thread(); - conserver une référence interne jusqu’au 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()
L’annulation 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.0et1.0; - ignorée si la tâche n’est pas
RUNNING; status_messageest copié ;status_message == NULLest accepté.
Le ticket #035 pourra lire régulièrement ces valeurs pour mettre à jour le panneau d’activité.
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
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 :
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 d’exé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 jusqu’au callback final.
9. Fonction exécutée dans le thread
Créer un trampoline privé compatible avec :
GTaskThreadFunc
Il doit :
- récupérer le contexte ;
- appeler le worker ;
- vérifier le contrat de retour ;
- retourner le résultat avec
g_task_return_pointer(); - retourner l’erreur avec
g_task_return_error(); - créer une erreur
BACKGROUND_TASK_ERROR_WORKER_PROTOCOLsi 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 l’exé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 :
- appeler
g_task_propagate_pointer(); - déterminer le nouvel état ;
- conserver le résultat ou l’erreur ;
- enregistrer la date de fin ;
- forcer la progression à
1.0en cas de succès ; - appeler le callback utilisateur ;
- détruire les données du callback utilisateur ;
- 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 :
- vérifier qu’aucune référence interne d’exécution ne subsiste ;
- détruire
resultavecresult_destroy; - libérer
error; - libérer les chaînes ;
- libérer
GCancellable; - libérer les éventuelles données de callback restantes ;
- nettoyer
GMutex; - libérer la structure.
L’appel 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 :
- 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 :
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 qu’une 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 :
- signale plusieurs progressions ;
- renvoie une chaîne allouée ;
- 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 d’annulation
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_destroyest appelé exactement une fois ;completion_data_destroyest 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 :
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 ;
- 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
BackgroundTaskest 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.0et1.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.
makeréussit.make testréussit.git diff --checkne retourne aucune erreur.
17. Audit attendu
Vérifier l’absence 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 qu’aucun thread POSIX brut n’est ajouté :
rg -n \
'pthread_|pthread.h' \
include/core/background_task.h \
src/core/background_task.c
Résultat attendu :
aucune sortie
Vérifier l’absence 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 n’est 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 d’une 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 d’activité au-dessus de cette abstraction.