feat(core): add asynchronous background task
This commit is contained in:
parent
f7b521cda6
commit
2a8ed10034
7 changed files with 3808 additions and 2 deletions
13
Makefile
13
Makefile
|
|
@ -40,6 +40,7 @@ TEST_ERROR = tests/test_error
|
|||
TEST_INVESTIGATION_RECORD = tests/test_investigation_record
|
||||
TEST_INVESTIGATION_DAO := tests/test_investigation_dao
|
||||
TEST_INVESTIGATION_SESSION := tests/test_investigation_session
|
||||
TEST_BACKGROUND_TASK := tests/test_background_task
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
|
|
@ -137,6 +138,11 @@ $(TEST_INVESTIGATION_SESSION): \
|
|||
src/database/error.c
|
||||
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
|
||||
|
||||
$(TEST_BACKGROUND_TASK): \
|
||||
tests/test_background_task.c \
|
||||
src/core/background_task.c
|
||||
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
|
||||
|
||||
test: \
|
||||
$(TEST_NODE) \
|
||||
$(TEST_TREE_MODEL) \
|
||||
|
|
@ -148,7 +154,8 @@ test: \
|
|||
$(TEST_ERROR) \
|
||||
$(TEST_INVESTIGATION_RECORD) \
|
||||
$(TEST_INVESTIGATION_DAO) \
|
||||
$(TEST_INVESTIGATION_SESSION)
|
||||
$(TEST_INVESTIGATION_SESSION) \
|
||||
$(TEST_BACKGROUND_TASK)
|
||||
@echo "Exécution des tests..."
|
||||
@./$(TEST_NODE)
|
||||
@./$(TEST_TREE_MODEL)
|
||||
|
|
@ -161,6 +168,7 @@ test: \
|
|||
@$(TEST_INVESTIGATION_RECORD)
|
||||
@$(TEST_INVESTIGATION_DAO)
|
||||
@$(TEST_INVESTIGATION_SESSION)
|
||||
@$(TEST_BACKGROUND_TASK)
|
||||
@echo "Tous les tests sont valides."
|
||||
|
||||
%.o: %.c
|
||||
|
|
@ -181,6 +189,7 @@ clean:
|
|||
$(TEST_ERROR) \
|
||||
$(TEST_INVESTIGATION_RECORD) \
|
||||
$(TEST_INVESTIGATION_DAO) \
|
||||
$(TEST_INVESTIGATION_SESSION)
|
||||
$(TEST_INVESTIGATION_SESSION) \
|
||||
$(TEST_BACKGROUND_TASK)
|
||||
|
||||
.PHONY: clean run test
|
||||
|
|
|
|||
967
docs/tickets/closed/TICKET-034.md
Normal file
967
docs/tickets/closed/TICKET-034.md
Normal file
|
|
@ -0,0 +1,967 @@
|
|||
# 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 <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 qu’aucun thread POSIX brut n’est 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 l’absence 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 n’est 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 d’une 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 d’activité au-dessus de cette abstraction.
|
||||
289
include/core/background_task.h
Normal file
289
include/core/background_task.h
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
/******************************************************************************
|
||||
* @file background_task.h
|
||||
* @brief Tâche générique exécutée en arrière-plan.
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef LABFY_INVESTIGATION_BACKGROUND_TASK_H
|
||||
#define LABFY_INVESTIGATION_BACKGROUND_TASK_H
|
||||
|
||||
#include <gio/gio.h>
|
||||
|
||||
/**
|
||||
* @brief Représentation opaque d'une tâche asynchrone.
|
||||
*/
|
||||
typedef struct BackgroundTask BackgroundTask;
|
||||
|
||||
/**
|
||||
* @brief États possibles d'une tâche.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
BACKGROUND_TASK_STATE_PENDING,
|
||||
BACKGROUND_TASK_STATE_RUNNING,
|
||||
BACKGROUND_TASK_STATE_COMPLETED,
|
||||
BACKGROUND_TASK_STATE_FAILED,
|
||||
BACKGROUND_TASK_STATE_CANCELLED
|
||||
} BackgroundTaskState;
|
||||
|
||||
/**
|
||||
* @brief Codes d'erreur propres au module BackgroundTask.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
BACKGROUND_TASK_ERROR_INVALID_ARGUMENT,
|
||||
BACKGROUND_TASK_ERROR_ALREADY_STARTED,
|
||||
BACKGROUND_TASK_ERROR_WORKER_PROTOCOL
|
||||
} BackgroundTaskError;
|
||||
|
||||
/**
|
||||
* @brief Domaine d'erreur du module BackgroundTask.
|
||||
*/
|
||||
#define BACKGROUND_TASK_ERROR \
|
||||
background_task_error_quark()
|
||||
|
||||
/**
|
||||
* @brief Fonction exécutée dans un thread secondaire.
|
||||
*
|
||||
* En cas de succès, la fonction retourne TRUE et peut placer un résultat
|
||||
* dans result.
|
||||
*
|
||||
* En cas d'échec, elle retourne FALSE et renseigne normalement error.
|
||||
*
|
||||
* @param task Tâche en cours d'exécution.
|
||||
* @param cancellable Objet d'annulation associé à la tâche.
|
||||
* @param worker_data Données privées du worker.
|
||||
* @param result Emplacement recevant le résultat alloué.
|
||||
* @param error Emplacement recevant une erreur.
|
||||
*
|
||||
* @return TRUE en cas de succès, sinon FALSE.
|
||||
*/
|
||||
typedef gboolean (*BackgroundTaskWorker)(
|
||||
BackgroundTask *task,
|
||||
GCancellable *cancellable,
|
||||
gpointer worker_data,
|
||||
gpointer *result,
|
||||
GError **error
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Callback appelé lorsque la tâche est terminée.
|
||||
*
|
||||
* Ce callback est appelé après la mise à jour de l'état final.
|
||||
*
|
||||
* @param task Tâche terminée.
|
||||
* @param user_data Données privées du callback.
|
||||
*/
|
||||
typedef void (*BackgroundTaskCompletionCallback)(
|
||||
BackgroundTask *task,
|
||||
gpointer user_data
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne le domaine d'erreur du module.
|
||||
*
|
||||
* @return Quark GLib du domaine d'erreur.
|
||||
*/
|
||||
GQuark background_task_error_quark(void);
|
||||
|
||||
/**
|
||||
* @brief Crée une nouvelle tâche en attente.
|
||||
*
|
||||
* @param title Titre non vide de la tâche.
|
||||
*
|
||||
* @return Nouvelle tâche, ou NULL si le titre est invalide.
|
||||
*/
|
||||
BackgroundTask *background_task_new(
|
||||
const char *title
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Ajoute une référence à une tâche.
|
||||
*
|
||||
* @param task Tâche concernée.
|
||||
*
|
||||
* @return La tâche fournie, ou NULL.
|
||||
*/
|
||||
BackgroundTask *background_task_ref(
|
||||
BackgroundTask *task
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Libère une référence à une tâche.
|
||||
*
|
||||
* La fonction accepte task == NULL.
|
||||
*
|
||||
* @param task Tâche concernée.
|
||||
*/
|
||||
void background_task_unref(
|
||||
BackgroundTask *task
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Démarre l'exécution asynchrone d'une tâche.
|
||||
*
|
||||
* Si le démarrage réussit, la tâche devient propriétaire de worker_data
|
||||
* et completion_data.
|
||||
*
|
||||
* Si le démarrage échoue, l'appelant conserve leur propriété.
|
||||
*
|
||||
* Le futur résultat produit par le worker sera détruit avec
|
||||
* result_destroy lors de la destruction de la tâche.
|
||||
*
|
||||
* @param task Tâche à démarrer.
|
||||
* @param worker Fonction exécutée dans un thread secondaire.
|
||||
* @param worker_data Données transmises au worker.
|
||||
* @param worker_data_destroy Fonction de destruction de worker_data.
|
||||
* @param result_destroy Fonction de destruction du futur résultat.
|
||||
* @param completion_callback Callback final facultatif.
|
||||
* @param completion_data Données transmises au callback final.
|
||||
* @param completion_data_destroy Fonction de destruction associée.
|
||||
* @param error Emplacement facultatif recevant une erreur.
|
||||
*
|
||||
* @return TRUE si la tâche a été lancée, sinon FALSE.
|
||||
*/
|
||||
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
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Demande l'annulation coopérative de la tâche.
|
||||
*
|
||||
* @param task Tâche concernée.
|
||||
*/
|
||||
void background_task_cancel(
|
||||
BackgroundTask *task
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Indique si une annulation a été demandée.
|
||||
*
|
||||
* @param task Tâche concernée.
|
||||
*
|
||||
* @return TRUE si l'annulation a été demandée, sinon FALSE.
|
||||
*/
|
||||
gboolean background_task_is_cancelled(
|
||||
const BackgroundTask *task
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Met à jour la progression d'une tâche en cours.
|
||||
*
|
||||
* La progression est automatiquement limitée à l'intervalle
|
||||
* compris entre 0.0 et 1.0.
|
||||
*
|
||||
* @param task Tâche concernée.
|
||||
* @param progress Nouvelle progression.
|
||||
* @param status_message Message facultatif, copié par le module.
|
||||
*/
|
||||
void background_task_report_progress(
|
||||
BackgroundTask *task,
|
||||
double progress,
|
||||
const char *status_message
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne le titre immuable d'une tâche.
|
||||
*
|
||||
* Le pointeur retourné est emprunté.
|
||||
*
|
||||
* @param task Tâche concernée.
|
||||
*
|
||||
* @return Titre de la tâche, ou NULL.
|
||||
*/
|
||||
const char *background_task_get_title(
|
||||
const BackgroundTask *task
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne l'état courant d'une tâche.
|
||||
*
|
||||
* @param task Tâche concernée.
|
||||
*
|
||||
* @return État courant de la tâche.
|
||||
*/
|
||||
BackgroundTaskState background_task_get_state(
|
||||
const BackgroundTask *task
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne la progression courante.
|
||||
*
|
||||
* @param task Tâche concernée.
|
||||
*
|
||||
* @return Progression comprise entre 0.0 et 1.0.
|
||||
*/
|
||||
double background_task_get_progress(
|
||||
const BackgroundTask *task
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Copie le message de progression courant.
|
||||
*
|
||||
* L'appelant doit libérer la chaîne avec g_free().
|
||||
*
|
||||
* @param task Tâche concernée.
|
||||
*
|
||||
* @return Nouvelle chaîne, ou NULL.
|
||||
*/
|
||||
char *background_task_dup_status_message(
|
||||
const BackgroundTask *task
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne la date de démarrage monotone en microsecondes.
|
||||
*
|
||||
* @param task Tâche concernée.
|
||||
*
|
||||
* @return Date de démarrage, ou 0 si la tâche n'a pas démarré.
|
||||
*/
|
||||
gint64 background_task_get_started_at_us(
|
||||
const BackgroundTask *task
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne la date de fin monotone en microsecondes.
|
||||
*
|
||||
* @param task Tâche concernée.
|
||||
*
|
||||
* @return Date de fin, ou 0 si la tâche n'est pas terminée.
|
||||
*/
|
||||
gint64 background_task_get_finished_at_us(
|
||||
const BackgroundTask *task
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne une copie de l'erreur finale.
|
||||
*
|
||||
* L'appelant doit libérer l'erreur avec g_error_free().
|
||||
*
|
||||
* @param task Tâche concernée.
|
||||
*
|
||||
* @return Nouvelle copie de l'erreur, ou NULL.
|
||||
*/
|
||||
GError *background_task_dup_error(
|
||||
const BackgroundTask *task
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Retourne le résultat final de la tâche.
|
||||
*
|
||||
* Le pointeur retourné est emprunté et ne doit pas être libéré.
|
||||
* Il n'est exploitable qu'après l'état BACKGROUND_TASK_STATE_COMPLETED.
|
||||
*
|
||||
* @param task Tâche concernée.
|
||||
*
|
||||
* @return Résultat final, ou NULL.
|
||||
*/
|
||||
gpointer background_task_get_result(
|
||||
const BackgroundTask *task
|
||||
);
|
||||
|
||||
#endif
|
||||
Binary file not shown.
1072
src/core/background_task.c
Normal file
1072
src/core/background_task.c
Normal file
File diff suppressed because it is too large
Load diff
BIN
tests/test_background_task
Executable file
BIN
tests/test_background_task
Executable file
Binary file not shown.
1469
tests/test_background_task.c
Normal file
1469
tests/test_background_task.c
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue