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

967 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 :
```c
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 :
```text
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 :
```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 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 :
```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.
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 :
```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 lexé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
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
```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()
```
Lannulation 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 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
```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
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 :
```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 dexé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 jusquau 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 lerreur 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 lexé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 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 :
```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 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é :
```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 :
- lappel du worker ;
- lappel du callback utilisateur ;
- une fonction de destruction fournie par lappelant ;
- 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 quune 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 dannulation
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 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 :
```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 ;
- 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 :
```bash
rg -n \
'#include <gtk|sqlite3_|Database|Investigation' \
include/core/background_task.h \
src/core/background_task.c
```
Résultat attendu :
```text
aucune sortie
```
Vérifier les primitives asynchrones :
```bash
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é :
```bash
rg -n \
'pthread_|pthread.h' \
include/core/background_task.h \
src/core/background_task.c
```
Résultat attendu :
```text
aucune sortie
```
Vérifier labsence de sortie forcée :
```bash
rg -n \
'\bexit\s*\(' \
src/core/background_task.c \
tests/test_background_task.c
```
Résultat attendu :
```text
aucune sortie
```
---
# 18. Fichiers concernés
```text
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
```bash
make clean
make
make test
git diff --check
git status --short
```
```bash
git add \
include/core/background_task.h \
src/core/background_task.c \
tests/test_background_task.c \
Makefile
```
```bash
git diff --cached --stat
git diff --cached
```
```bash
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 :
```text
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.