diff --git a/docs/tickets/closed/TICKET-035.md b/docs/tickets/closed/TICKET-035.md new file mode 100644 index 0000000..12d6487 --- /dev/null +++ b/docs/tickets/closed/TICKET-035.md @@ -0,0 +1,790 @@ +# Ticket #035 — File de tâches et panneau d’activité + +## Contexte + +Le ticket #034 a introduit `BackgroundTask`, une primitive asynchrone capable de : + +- lancer un worker dans un thread GLib ; +- suivre son état ; +- signaler sa progression ; +- gérer l’annulation ; +- conserver un résultat ou une erreur ; +- revenir sur le contexte principal à la fin ; +- rester vivante grâce au comptage de références. + +Cette primitive ne gère cependant pas encore plusieurs tâches ni leur présentation dans l’interface. + +## Objectif + +Créer un gestionnaire opaque : + +```c +TaskManager +``` + +chargé de suivre les tâches actives et terminées, puis ajouter un panneau GTK permettant de les consulter. + +Le ticket doit permettre de voir : + +```text +Titre +État +Progression +Message courant +Bouton Annuler +``` + +Le gestionnaire reste indépendant de GTK. + +--- + +# Architecture attendue + +Créer : + +```text +include/core/task_manager.h +src/core/task_manager.c +tests/test_task_manager.c + +include/widgets/task_panel.h +src/widgets/task_panel.c +``` + +Relations : + +```text +Application + ├── possède TaskManager + └── transmet TaskManager à MainWindow + +MainWindow + └── possède TaskPanel + +TaskPanel + └── observe TaskManager + +TaskManager + └── conserve des références vers BackgroundTask +``` + +--- + +# Phase A — `TaskManager` + +## 1. Type opaque + +Dans : + +```text +include/core/task_manager.h +``` + +déclarer : + +```c +typedef struct TaskManager TaskManager; +``` + +Le type doit rester indépendant de GTK, SQLite et des enquêtes. + +--- + +## 2. Callback de changement + +Définir : + +```c +typedef void (*TaskManagerChangedCallback)( + TaskManager *task_manager, + gpointer user_data +); +``` + +Le callback signale qu’un changement visible a eu lieu : + +- tâche ajoutée ; +- progression modifiée ; +- tâche terminée ; +- tâche retirée ; +- annulation demandée. + +Le callback est une notification globale. Il ne transmet pas directement une tâche particulière. + +--- + +## 3. API publique + +### Construction + +```c +TaskManager *task_manager_new(void); + +void task_manager_free( + TaskManager *task_manager +); +``` + +### Ajouter une tâche + +```c +gboolean task_manager_add( + TaskManager *task_manager, + BackgroundTask *task, + GError **error +); +``` + +Règles : + +- `task_manager` devient propriétaire d’une référence supplémentaire ; +- l’appelant conserve sa propre référence ; +- une même tâche ne peut pas être ajoutée deux fois ; +- une tâche déjà terminée peut être ajoutée, mais elle est immédiatement visible comme terminée ; +- `NULL` est refusé. + +### Consulter les tâches + +```c +gsize task_manager_get_count( + const TaskManager *task_manager +); + +BackgroundTask *task_manager_get_task( + const TaskManager *task_manager, + gsize index +); +``` + +`task_manager_get_task()` retourne une nouvelle référence que l’appelant doit libérer. + +### Retirer une tâche + +```c +gboolean task_manager_remove( + TaskManager *task_manager, + BackgroundTask *task +); +``` + +La tâche n’est pas annulée automatiquement. + +### Supprimer les tâches terminées + +```c +gsize task_manager_remove_finished( + TaskManager *task_manager +); +``` + +États concernés : + +```text +COMPLETED +FAILED +CANCELLED +``` + +### Annuler toutes les tâches actives + +```c +void task_manager_cancel_all( + TaskManager *task_manager +); +``` + +### Callback de changement + +```c +void task_manager_set_changed_callback( + TaskManager *task_manager, + TaskManagerChangedCallback callback, + gpointer user_data, + GDestroyNotify user_data_destroy +); +``` + +Le manager possède `user_data` après l’appel. + +Remplacer le callback existant doit détruire les anciennes données exactement une fois. + +--- + +## 4. Domaine d’erreur + +Définir : + +```c +typedef enum +{ + TASK_MANAGER_ERROR_INVALID_ARGUMENT, + TASK_MANAGER_ERROR_ALREADY_ADDED +} TaskManagerError; +``` + +Puis : + +```c +#define TASK_MANAGER_ERROR \ + task_manager_error_quark() + +GQuark task_manager_error_quark(void); +``` + +--- + +## 5. Structure interne recommandée + +```c +struct TaskManager +{ + GMutex mutex; + GPtrArray *tasks; + + TaskManagerChangedCallback changed_callback; + gpointer changed_user_data; + GDestroyNotify changed_user_data_destroy; +}; +``` + +`tasks` doit contenir des références `BackgroundTask *`. + +Configurer le `GPtrArray` avec : + +```c +background_task_unref +``` + +comme fonction de destruction. + +--- + +## 6. Notification périodique + +`BackgroundTask` ne possède pas encore de callback de progression. + +Pour ce ticket, `TaskPanel` peut rafraîchir périodiquement l’affichage avec : + +```c +g_timeout_add() +``` + +fréquence recommandée : + +```text +200 à 300 ms +``` + +`TaskManager` notifie immédiatement les changements structurels. + +La progression sera relue par le panneau. + +Une API d’observation plus fine pourra être ajoutée plus tard si nécessaire. + +--- + +# Phase B — Tests de `TaskManager` + +Créer : + +```text +tests/test_task_manager.c +``` + +## Tests minimaux + +### Construction + +Vérifier : + +```text +manager non NULL +compteur initial à zéro +``` + +### Ajout + +Ajouter une tâche et vérifier : + +```text +compteur à un +callback déclenché +tâche récupérable +référence indépendante +``` + +### Doublon + +Ajouter deux fois la même tâche : + +```text +FALSE +TASK_MANAGER_ERROR_ALREADY_ADDED +compteur inchangé +``` + +### Retrait + +Retirer une tâche : + +```text +TRUE +compteur décrémenté +callback déclenché +``` + +### Retrait inconnu + +Retirer une tâche absente : + +```text +FALSE +aucun crash +``` + +### Nettoyage des tâches terminées + +Ajouter : + +- une tâche en attente ; +- une tâche terminée ; +- une tâche échouée ; +- une tâche annulée. + +Vérifier que seules les tâches terminées sont supprimées. + +### Annulation globale + +Ajouter plusieurs tâches actives et appeler : + +```c +task_manager_cancel_all() +``` + +Vérifier que chaque `GCancellable` reçoit une demande d’annulation. + +### Durée de vie + +Vérifier que : + +- le manager conserve ses références ; +- la destruction du manager libère toutes les tâches ; +- le remplacement du callback détruit les anciennes données une seule fois. + +--- + +# Phase C — `TaskPanel` + +## 7. Type opaque + +Dans : + +```text +include/widgets/task_panel.h +``` + +déclarer : + +```c +typedef struct TaskPanel TaskPanel; +``` + +API : + +```c +TaskPanel *task_panel_new( + TaskManager *task_manager +); + +GtkWidget *task_panel_get_widget( + const TaskPanel *task_panel +); + +void task_panel_refresh( + TaskPanel *task_panel +); + +void task_panel_free( + TaskPanel *task_panel +); +``` + +`TaskPanel` ne devient pas propriétaire de `TaskManager`. + +`TaskPanel` doit rester valide tant que le manager existe. + +--- + +## 8. Interface recommandée + +Premier rendu simple : + +```text +┌──────────────────────────────────────────────────────────┐ +│ Activité [ Nettoyer ] │ +├──────────────────────────────────────────────────────────┤ +│ Extraction des métadonnées │ +│ En cours — 45 % │ +│ [████████░░░░░░░░░░] [ Annuler ] │ +├──────────────────────────────────────────────────────────┤ +│ Calcul SHA-256 │ +│ Terminé │ +│ [████████████████████] │ +└──────────────────────────────────────────────────────────┘ +``` + +Widgets GTK possibles : + +- `GtkBox` ; +- `GtkLabel` ; +- `GtkProgressBar` ; +- `GtkButton` ; +- `GtkScrolledWindow`. + +Ne pas utiliser encore de `GtkListView` si cela complexifie inutilement le ticket. + +--- + +## 9. État visuel + +Créer une fonction privée traduisant les états : + +```text +PENDING → En attente +RUNNING → En cours +COMPLETED → Terminée +FAILED → Échouée +CANCELLED → Annulée +``` + +Une tâche en erreur doit afficher son message d’erreur sous forme courte. + +Une tâche en cours doit afficher son message de progression lorsqu’il existe. + +--- + +## 10. Bouton d’annulation + +Le bouton `Annuler` doit être visible uniquement pour : + +```text +RUNNING +``` + +Son callback appelle : + +```c +background_task_cancel(task); +``` + +Le bouton ne doit pas retirer la tâche. + +--- + +## 11. Bouton de nettoyage + +Ajouter : + +```text +Nettoyer +``` + +Il appelle : + +```c +task_manager_remove_finished() +``` + +Les tâches actives restent visibles. + +--- + +## 12. Rafraîchissement périodique + +`TaskPanel` doit enregistrer une source GLib : + +```c +g_timeout_add() +``` + +Elle appelle : + +```c +task_panel_refresh() +``` + +Lors de `task_panel_free()` : + +- retirer la source avec `g_source_remove()` ; +- empêcher tout callback après destruction ; +- ne pas détruire `TaskManager`. + +--- + +# Phase D — Intégration GTK + +## 13. Ajouter le panneau à `MainWindow` + +Modifier : + +```text +include/views/main_window.h +src/views/main_window.c +``` + +Changer la construction : + +```c +MainWindow *main_window_new( + GtkApplication *application, + TaskManager *task_manager +); +``` + +`MainWindow` doit créer : + +```c +TaskPanel *task_panel; +``` + +Le panneau peut être placé : + +- sous la zone de travail ; +- dans un volet inférieur ; +- ou temporairement dans une colonne latérale secondaire. + +Pour ce ticket, un volet inférieur sous `GtkPaned` est acceptable. + +--- + +## 14. Ajouter `TaskManager` à `Application` + +Dans la structure privée : + +```c +TaskManager *task_manager; +``` + +Dans `application_new()` : + +```c +application->task_manager = + task_manager_new(); +``` + +En cas d’échec, nettoyer l’application. + +Dans `application_on_activate()` : + +```c +application->main_window = main_window_new( + gtk_application, + application->task_manager +); +``` + +Dans `application_free()` : + +1. fermer/détruire `MainWindow` ; +2. libérer `TaskManager` après le panneau ; +3. poursuivre le nettoyage existant. + +L’ordre doit éviter que `TaskPanel` lise un manager déjà détruit. + +--- + +# Phase E — Tâche de démonstration + +## 15. Ajouter temporairement une tâche de test + +Pour valider l’interface, ajouter un bouton temporaire : + +```text +Tâche de test +``` + +Il lance une `BackgroundTask` qui : + +- dure environ deux secondes ; +- progresse de 0 à 100 % ; +- accepte l’annulation ; +- retourne un résultat simple. + +Cette tâche doit être ajoutée au `TaskManager`. + +Le bouton pourra être supprimé lorsque le premier véritable traitement asynchrone sera disponible. + +Le code de démonstration doit rester clairement identifié : + +```c +/* Temporary demonstration task for ticket #035. */ +``` + +--- + +# Hors périmètre + +Ne pas ajouter encore : + +- de limite de concurrence ; +- de priorité ; +- de persistance SQLite ; +- de reprise après redémarrage ; +- d’historique permanent ; +- d’exécution de commandes ; +- de recherche DNS ; +- d’ExifTool ; +- de notifications système ; +- de tri avancé ; +- de pagination. + +--- + +# Critères d’acceptation + +- [ ] `TaskManager` est opaque. +- [ ] `TaskManager` ne dépend pas de GTK. +- [ ] `TaskManager` protège sa collection avec `GMutex`. +- [ ] Le manager conserve une référence par tâche. +- [ ] Une tâche ne peut pas être ajoutée deux fois. +- [ ] Une tâche peut être retirée. +- [ ] Les tâches terminées peuvent être nettoyées. +- [ ] Toutes les tâches actives peuvent être annulées. +- [ ] Le callback de changement fonctionne. +- [ ] `TaskPanel` affiche toutes les tâches. +- [ ] La progression est visible. +- [ ] L’état est lisible. +- [ ] Une tâche active peut être annulée. +- [ ] Les tâches terminées peuvent être supprimées. +- [ ] Le rafraîchissement périodique est correctement détruit. +- [ ] `MainWindow` ne devient pas propriétaire du manager. +- [ ] L’ordre de destruction ne provoque aucun crash. +- [ ] La tâche de démonstration fonctionne. +- [ ] `make` réussit. +- [ ] `make test` réussit. +- [ ] `git diff --check` ne retourne aucune erreur. + +--- + +# Audit attendu + +Vérifier l’indépendance du manager : + +```bash +rg -n \ + '#include + +/** + * @brief Représentation opaque du gestionnaire de tâches. + */ +typedef struct TaskManager TaskManager; + +/** + * @brief Codes d’erreur du gestionnaire de tâches. + */ +typedef enum +{ + TASK_MANAGER_ERROR_INVALID_ARGUMENT, + TASK_MANAGER_ERROR_ALREADY_ADDED +} TaskManagerError; + +/** + * @brief Domaine d’erreur du gestionnaire de tâches. + */ +#define TASK_MANAGER_ERROR \ + task_manager_error_quark() + +/** + * @brief Callback appelé lorsque la collection de tâches change. + * + * Ce callback est utilisé lorsqu’une tâche est ajoutée, retirée, + * nettoyée ou lorsqu’une demande globale d’annulation est effectuée. + * + * Il est appelé sans que le mutex interne du gestionnaire soit verrouillé. + * + * @param task_manager Gestionnaire ayant changé. + * @param user_data Données associées au callback. + */ +typedef void (*TaskManagerChangedCallback)( + TaskManager *task_manager, + gpointer user_data +); + +/** + * @brief Retourne le domaine d’erreur du module. + * + * @return Quark GLib du domaine d’erreur. + */ +GQuark task_manager_error_quark(void); + +/** + * @brief Crée un gestionnaire de tâches vide. + * + * @return Nouveau gestionnaire, ou NULL en cas d’échec. + */ +TaskManager *task_manager_new(void); + +/** + * @brief Libère le gestionnaire et ses références sur les tâches. + * + * Cette fonction accepte task_manager == NULL. + * + * Les tâches continuent d’exister si d’autres composants possèdent + * encore leurs propres références. + * + * @param task_manager Gestionnaire à libérer. + */ +void task_manager_free( + TaskManager *task_manager +); + +/** + * @brief Ajoute une tâche au gestionnaire. + * + * Le gestionnaire prend une référence supplémentaire sur la tâche. + * L’appelant conserve la propriété de sa propre référence. + * + * Une même instance de BackgroundTask ne peut pas être ajoutée deux fois. + * + * @param task_manager Gestionnaire concerné. + * @param task Tâche à ajouter. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE si la tâche est ajoutée, sinon FALSE. + */ +gboolean task_manager_add( + TaskManager *task_manager, + BackgroundTask *task, + GError **error +); + +/** + * @brief Retourne le nombre de tâches actuellement suivies. + * + * @param task_manager Gestionnaire concerné. + * + * @return Nombre de tâches. + */ +gsize task_manager_get_count( + const TaskManager *task_manager +); + +/** + * @brief Retourne une référence vers une tâche selon son index. + * + * L’appelant doit libérer la référence retournée avec : + * + * @code + * background_task_unref(task); + * @endcode + * + * @param task_manager Gestionnaire concerné. + * @param index Index de la tâche. + * + * @return Nouvelle référence vers la tâche, ou NULL si l’index est invalide. + */ +BackgroundTask *task_manager_get_task( + const TaskManager *task_manager, + gsize index +); + +/** + * @brief Retire une tâche du gestionnaire. + * + * La tâche n’est pas annulée automatiquement. + * + * La référence détenue par le gestionnaire est libérée. + * + * @param task_manager Gestionnaire concerné. + * @param task Tâche à retirer. + * + * @return TRUE si la tâche était présente et a été retirée, sinon FALSE. + */ +gboolean task_manager_remove( + TaskManager *task_manager, + BackgroundTask *task +); + +/** + * @brief Retire toutes les tâches terminées. + * + * États concernés : + * + * - BACKGROUND_TASK_STATE_COMPLETED ; + * - BACKGROUND_TASK_STATE_FAILED ; + * - BACKGROUND_TASK_STATE_CANCELLED. + * + * Les tâches en attente ou en cours sont conservées. + * + * @param task_manager Gestionnaire concerné. + * + * @return Nombre de tâches retirées. + */ +gsize task_manager_remove_finished( + TaskManager *task_manager +); + +/** + * @brief Demande l’annulation de toutes les tâches en cours. + * + * L’annulation reste coopérative. + * + * Les tâches ne sont pas retirées du gestionnaire. + * + * @param task_manager Gestionnaire concerné. + */ +void task_manager_cancel_all( + TaskManager *task_manager +); + +/** + * @brief Définit le callback signalant un changement du gestionnaire. + * + * Le gestionnaire devient propriétaire de user_data après cet appel. + * + * Lorsqu’un nouveau callback remplace l’ancien, les anciennes données + * sont détruites avec leur fonction de destruction. + * + * Le callback peut être désactivé en fournissant callback == NULL. + * + * @param task_manager Gestionnaire concerné. + * @param callback Nouveau callback, ou NULL. + * @param user_data Données transmises au callback. + * @param user_data_destroy Fonction de destruction associée. + */ +void task_manager_set_changed_callback( + TaskManager *task_manager, + TaskManagerChangedCallback callback, + gpointer user_data, + GDestroyNotify user_data_destroy +); + +#endif diff --git a/include/views/main_window.h b/include/views/main_window.h index 112f07c..4c96478 100644 --- a/include/views/main_window.h +++ b/include/views/main_window.h @@ -9,6 +9,7 @@ #include "core/investigation_tree_model.h" #include "widgets/investigation_tree_view.h" #include "core/investigation_node.h" +#include "core/task_manager.h" #include @@ -45,11 +46,17 @@ void main_window_set_new_investigation_callback( /** * @brief Crée une nouvelle fenêtre principale. * - * @param application Application GTK. + * MainWindow ne devient pas propriétaire de task_manager. * - * @return Une nouvelle fenêtre ou NULL en cas d'échec. + * @param application Application GTK. + * @param task_manager Gestionnaire de tâches de l'application. + * + * @return Nouvelle fenêtre, ou NULL en cas d'échec. */ -MainWindow *main_window_new(GtkApplication *application); +MainWindow *main_window_new( + GtkApplication *application, + TaskManager *task_manager +); /** * @brief Affiche la fenêtre principale. @@ -156,6 +163,16 @@ typedef void (*MainWindowQuitCallback)( gpointer user_data ); +/** + * @brief Callback appelé lorsque l'utilisateur demande une tâche + * de démonstration. + * + * @param user_data Données utilisateur associées au callback. + */ +typedef void (*MainWindowDemoTaskCallback)( + gpointer user_data +); + /** * @brief Définit le callback du bouton « Ouvrir une enquête ». * @@ -186,6 +203,21 @@ void main_window_set_quit_callback( gpointer user_data ); +/** + * @brief Définit le callback du bouton « Tâche de test ». + * + * Ce bouton est temporaire et sert à valider le panneau d'activité. + * + * @param main_window Fenêtre principale. + * @param callback Fonction appelée lors du clic. + * @param user_data Données transmises au callback. + */ +void main_window_set_demo_task_callback( + MainWindow *main_window, + MainWindowDemoTaskCallback callback, + gpointer user_data +); + /** * @brief Libère les ressources de la fenêtre. * diff --git a/include/widgets/task_panel.h b/include/widgets/task_panel.h new file mode 100644 index 0000000..3f07317 --- /dev/null +++ b/include/widgets/task_panel.h @@ -0,0 +1,66 @@ +/****************************************************************************** + * @file task_panel.h + * @brief Panneau GTK affichant les tâches suivies par TaskManager. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_TASK_PANEL_H +#define LABFY_INVESTIGATION_TASK_PANEL_H + +#include "core/task_manager.h" + +#include + +/** + * @brief Représentation opaque du panneau d’activité. + */ +typedef struct TaskPanel TaskPanel; + +/** + * @brief Crée un panneau affichant les tâches d’un gestionnaire. + * + * Le panneau ne devient pas propriétaire de task_manager. + * Le gestionnaire doit rester valide pendant toute la durée de vie + * du panneau. + * + * @param task_manager Gestionnaire de tâches observé. + * + * @return Nouveau panneau, ou NULL si le gestionnaire est invalide. + */ +TaskPanel *task_panel_new( + TaskManager *task_manager +); + +/** + * @brief Retourne le widget racine du panneau. + * + * Le pointeur retourné est emprunté. + * + * @param task_panel Panneau concerné. + * + * @return Widget racine, ou NULL. + */ +GtkWidget *task_panel_get_widget( + const TaskPanel *task_panel +); + +/** + * @brief Reconstruit l’affichage à partir du gestionnaire. + * + * @param task_panel Panneau à actualiser. + */ +void task_panel_refresh( + TaskPanel *task_panel +); + +/** + * @brief Libère le panneau et arrête son rafraîchissement périodique. + * + * Le gestionnaire associé n’est pas libéré. + * + * @param task_panel Panneau à libérer. + */ +void task_panel_free( + TaskPanel *task_panel +); + +#endif diff --git a/labfy-investigation b/labfy-investigation index 47f1d00..19ab421 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/src/core/application.c b/src/core/application.c index e7bc5df..2054ec3 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -15,6 +15,8 @@ #include "views/folder_dialog.h" #include "views/main_window.h" #include "views/application_message_dialog.h" +#include "core/task_manager.h" +#include "core/background_task.h" #include @@ -64,6 +66,7 @@ struct Application MainWindow *main_window; InvestigationSession *session; InvestigationTreeModel *tree_model; + TaskManager *task_manager; }; /** @@ -602,6 +605,269 @@ static void application_on_open_investigation_requested( ); } +/** + * @brief Exécute une tâche temporaire servant à tester le panneau. + * + * Cette fonction s'exécute dans un thread secondaire. + * + * @param task Tâche en cours. + * @param cancellable Objet d'annulation. + * @param worker_data Données inutilisées. + * @param result Emplacement recevant le résultat. + * @param error Emplacement recevant une erreur. + * + * @return TRUE en cas de succès, sinon FALSE. + */ +static gboolean application_demo_task_worker( + BackgroundTask *task, + GCancellable *cancellable, + gpointer worker_data, + gpointer *result, + GError **error +) +{ + guint step = 0; + char status_message[64]; + + (void) worker_data; + + if (task == NULL || + cancellable == NULL || + result == NULL || + error == NULL) + { + return FALSE; + } + + for (step = 0; step <= 20; step++) + { + if (g_cancellable_set_error_if_cancelled( + cancellable, + error + )) + { + return FALSE; + } + + g_snprintf( + status_message, + sizeof(status_message), + "Étape %u sur 20", + step + ); + + background_task_report_progress( + task, + (double) step / 20.0, + status_message + ); + + if (step < 20) + { + g_usleep( + 500000 + ); + } + } + + *result = g_strdup( + "Tâche de démonstration terminée." + ); + + return TRUE; +} + +/** + * @brief Journalise la fin de la tâche de démonstration. + * + * @param task Tâche terminée. + * @param user_data Données inutilisées. + */ +static void application_demo_task_completed( + BackgroundTask *task, + gpointer user_data +) +{ + BackgroundTaskState state; + const char *result = NULL; + GError *error = NULL; + + (void) user_data; + + if (task == NULL) + { + return; + } + + state = background_task_get_state( + task + ); + + if (state == BACKGROUND_TASK_STATE_COMPLETED) + { + result = background_task_get_result( + task + ); + + g_print( + "%s\n", + result != NULL + ? result + : "Tâche terminée." + ); + + return; + } + + if (state == BACKGROUND_TASK_STATE_CANCELLED) + { + g_print( + "Tâche de démonstration annulée.\n" + ); + + return; + } + + error = background_task_dup_error( + task + ); + + g_warning( + "La tâche de démonstration a échoué : %s", + error != NULL + ? error->message + : "erreur inconnue" + ); + + g_clear_error( + &error + ); +} + +/** + * @brief Crée et lance une tâche de démonstration. + * + * @param user_data Pointeur vers Application. + */ +static void application_on_demo_task_requested( + gpointer user_data +) +{ + Application *application = user_data; + BackgroundTask *task = NULL; + GError *error = NULL; + + if (application == NULL || + application->task_manager == NULL) + { + return; + } + + task = background_task_new( + "Tâche de démonstration" + ); + + if (task == NULL) + { + application_present_error( + application, + "Tâche impossible", + "La tâche de démonstration n'a pas pu être créée." + ); + + return; + } + + /* + * Le manager prend une référence supplémentaire. + */ + if (!task_manager_add( + application->task_manager, + task, + &error + )) + { + g_warning( + "Impossible d'ajouter la tâche : %s", + error != NULL + ? error->message + : "erreur inconnue" + ); + + application_present_error( + application, + "Tâche impossible", + error != NULL + ? error->message + : "La tâche n'a pas pu être ajoutée." + ); + + g_clear_error( + &error + ); + + background_task_unref( + task + ); + + return; + } + + if (!background_task_start( + task, + application_demo_task_worker, + NULL, + NULL, + g_free, + application_demo_task_completed, + NULL, + NULL, + &error + )) + { + g_warning( + "Impossible de démarrer la tâche : %s", + error != NULL + ? error->message + : "erreur inconnue" + ); + + application_present_error( + application, + "Démarrage impossible", + error != NULL + ? error->message + : "La tâche n'a pas pu être démarrée." + ); + + g_clear_error( + &error + ); + + task_manager_remove( + application->task_manager, + task + ); + + background_task_unref( + task + ); + + return; + } + + /* + * La référence locale n'est plus nécessaire : + * + * - TaskManager conserve une référence ; + * - BackgroundTask conserve une référence interne pendant + * l'exécution. + */ + background_task_unref( + task + ); +} + /** * @brief Traite la sélection d'un nœud dans l'arborescence. * @@ -675,6 +941,10 @@ static void application_on_quit_requested( return; } + task_manager_cancel_all( + application->task_manager + ); + g_application_quit( G_APPLICATION( application->gtk_application @@ -713,7 +983,8 @@ static void application_on_activate( } application->main_window = main_window_new( - gtk_application + gtk_application, + application->task_manager ); if (application->main_window == NULL) @@ -743,6 +1014,12 @@ static void application_on_activate( application ); + main_window_set_demo_task_callback( + application->main_window, + application_on_demo_task_requested, + application + ); + main_window_set_quit_callback( application->main_window, application_on_quit_requested, @@ -763,6 +1040,18 @@ Application *application_new(void) 1 ); + application->task_manager = + task_manager_new(); + + if (application->task_manager == NULL) + { + g_free( + application + ); + + return NULL; + } + application->gtk_application = gtk_application_new( APPLICATION_ID, G_APPLICATION_DEFAULT_FLAGS @@ -770,7 +1059,14 @@ Application *application_new(void) if (application->gtk_application == NULL) { - g_free(application); + task_manager_free( + application->task_manager + ); + + g_free( + application + ); + return NULL; } @@ -834,10 +1130,18 @@ void application_free( application->session ); + /* + * MainWindow contient TaskPanel. + * TaskPanel doit être détruit avant TaskManager. + */ main_window_free( application->main_window ); + task_manager_free( + application->task_manager + ); + if (application->gtk_application != NULL) { g_object_unref( @@ -845,5 +1149,7 @@ void application_free( ); } - g_free(application); + g_free( + application + ); } diff --git a/src/core/task_manager.c b/src/core/task_manager.c new file mode 100644 index 0000000..4f7ee6e --- /dev/null +++ b/src/core/task_manager.c @@ -0,0 +1,736 @@ +/****************************************************************************** + * @file task_manager.c + * @brief Implémentation du gestionnaire central des tâches. + ******************************************************************************/ + +#include "core/task_manager.h" + +/** + * @struct TaskManagerCallbackSlot + * @brief Stockage référencé du callback de changement. + * + * Le comptage de références permet d'appeler le callback hors du mutex + * sans risquer que ses données soient détruites pendant l'appel. + */ +typedef struct +{ + gatomicrefcount reference_count; + + TaskManagerChangedCallback callback; + gpointer user_data; + GDestroyNotify user_data_destroy; +} TaskManagerCallbackSlot; + +/** + * @struct TaskManager + * @brief État interne du gestionnaire de tâches. + */ +struct TaskManager +{ + GMutex mutex; + GPtrArray *tasks; + + TaskManagerCallbackSlot *changed_slot; +}; + +/** + * @brief Adapte background_task_unref() au type GDestroyNotify. + * + * @param user_data Pointeur vers BackgroundTask. + */ +static void task_manager_task_unref( + gpointer user_data +) +{ + background_task_unref( + user_data + ); +} + +/** + * @brief Crée un emplacement référencé pour un callback. + * + * @param callback Callback à conserver. + * @param user_data Données du callback. + * @param user_data_destroy Destructeur des données. + * + * @return Nouvel emplacement, ou NULL si aucune donnée n'est fournie. + */ +static TaskManagerCallbackSlot * +task_manager_callback_slot_new( + TaskManagerChangedCallback callback, + gpointer user_data, + GDestroyNotify user_data_destroy +) +{ + TaskManagerCallbackSlot *slot = NULL; + + if (callback == NULL && + user_data == NULL) + { + return NULL; + } + + slot = g_new0( + TaskManagerCallbackSlot, + 1 + ); + + g_atomic_ref_count_init( + &slot->reference_count + ); + + slot->callback = callback; + slot->user_data = user_data; + slot->user_data_destroy = + user_data_destroy; + + return slot; +} + +/** + * @brief Ajoute une référence à un emplacement de callback. + * + * @param slot Emplacement concerné. + * + * @return L'emplacement fourni, ou NULL. + */ +static TaskManagerCallbackSlot * +task_manager_callback_slot_ref( + TaskManagerCallbackSlot *slot +) +{ + if (slot == NULL) + { + return NULL; + } + + g_atomic_ref_count_inc( + &slot->reference_count + ); + + return slot; +} + +/** + * @brief Libère une référence à un emplacement de callback. + * + * @param slot Emplacement concerné. + */ +static void task_manager_callback_slot_unref( + TaskManagerCallbackSlot *slot +) +{ + gpointer user_data = NULL; + GDestroyNotify user_data_destroy = NULL; + + if (slot == NULL) + { + return; + } + + if (!g_atomic_ref_count_dec( + &slot->reference_count + )) + { + return; + } + + user_data = slot->user_data; + user_data_destroy = + slot->user_data_destroy; + + slot->callback = NULL; + slot->user_data = NULL; + slot->user_data_destroy = NULL; + + if (user_data != NULL && + user_data_destroy != NULL) + { + user_data_destroy( + user_data + ); + } + + g_free( + slot + ); +} + +/** + * @brief Signale un changement du gestionnaire. + * + * Le callback est appelé sans verrouiller le mutex du gestionnaire. + * + * @param task_manager Gestionnaire concerné. + */ +static void task_manager_notify_changed( + TaskManager *task_manager +) +{ + TaskManagerCallbackSlot *slot = NULL; + + if (task_manager == NULL) + { + return; + } + + g_mutex_lock( + &task_manager->mutex + ); + + slot = task_manager_callback_slot_ref( + task_manager->changed_slot + ); + + g_mutex_unlock( + &task_manager->mutex + ); + + if (slot != NULL && + slot->callback != NULL) + { + slot->callback( + task_manager, + slot->user_data + ); + } + + task_manager_callback_slot_unref( + slot + ); +} + +/** + * @brief Indique si un état représente une tâche terminée. + * + * @param state État à examiner. + * + * @return TRUE si la tâche est terminée, sinon FALSE. + */ +static gboolean task_manager_state_is_finished( + BackgroundTaskState state +) +{ + return + state == BACKGROUND_TASK_STATE_COMPLETED || + state == BACKGROUND_TASK_STATE_FAILED || + state == BACKGROUND_TASK_STATE_CANCELLED; +} + +GQuark task_manager_error_quark(void) +{ + return g_quark_from_static_string( + "labfy-investigation-task-manager-error" + ); +} + +TaskManager *task_manager_new(void) +{ + TaskManager *task_manager = NULL; + + task_manager = g_new0( + TaskManager, + 1 + ); + + g_mutex_init( + &task_manager->mutex + ); + + task_manager->tasks = + g_ptr_array_new_with_free_func( + task_manager_task_unref + ); + + if (task_manager->tasks == NULL) + { + g_mutex_clear( + &task_manager->mutex + ); + + g_free( + task_manager + ); + + return NULL; + } + + return task_manager; +} + +void task_manager_free( + TaskManager *task_manager +) +{ + GPtrArray *tasks = NULL; + + TaskManagerCallbackSlot *changed_slot = + NULL; + + if (task_manager == NULL) + { + return; + } + + g_mutex_lock( + &task_manager->mutex + ); + + tasks = task_manager->tasks; + task_manager->tasks = NULL; + + changed_slot = + task_manager->changed_slot; + + task_manager->changed_slot = NULL; + + g_mutex_unlock( + &task_manager->mutex + ); + + /* + * La libération des tâches et des données utilisateur se fait + * hors du mutex. + */ + if (tasks != NULL) + { + g_ptr_array_unref( + tasks + ); + } + + task_manager_callback_slot_unref( + changed_slot + ); + + g_mutex_clear( + &task_manager->mutex + ); + + g_free( + task_manager + ); +} + +gboolean task_manager_add( + TaskManager *task_manager, + BackgroundTask *task, + GError **error +) +{ + gsize index = 0; + gboolean already_added = FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (task_manager == NULL || + task == NULL) + { + g_set_error_literal( + error, + TASK_MANAGER_ERROR, + TASK_MANAGER_ERROR_INVALID_ARGUMENT, + "Le gestionnaire ou la tâche est invalide." + ); + + return FALSE; + } + + g_mutex_lock( + &task_manager->mutex + ); + + for (index = 0; + index < task_manager->tasks->len; + index++) + { + if (g_ptr_array_index( + task_manager->tasks, + index + ) == task) + { + already_added = TRUE; + break; + } + } + + if (!already_added) + { + g_ptr_array_add( + task_manager->tasks, + background_task_ref(task) + ); + } + + g_mutex_unlock( + &task_manager->mutex + ); + + if (already_added) + { + g_set_error_literal( + error, + TASK_MANAGER_ERROR, + TASK_MANAGER_ERROR_ALREADY_ADDED, + "Cette tâche est déjà suivie par le gestionnaire." + ); + + return FALSE; + } + + task_manager_notify_changed( + task_manager + ); + + return TRUE; +} + +gsize task_manager_get_count( + const TaskManager *task_manager +) +{ + TaskManager *mutable_task_manager = NULL; + gsize count = 0; + + if (task_manager == NULL) + { + return 0; + } + + mutable_task_manager = + (TaskManager *) task_manager; + + g_mutex_lock( + &mutable_task_manager->mutex + ); + + if (mutable_task_manager->tasks != NULL) + { + count = + mutable_task_manager->tasks->len; + } + + g_mutex_unlock( + &mutable_task_manager->mutex + ); + + return count; +} + +BackgroundTask *task_manager_get_task( + const TaskManager *task_manager, + gsize index +) +{ + TaskManager *mutable_task_manager = NULL; + BackgroundTask *task = NULL; + + if (task_manager == NULL) + { + return NULL; + } + + mutable_task_manager = + (TaskManager *) task_manager; + + g_mutex_lock( + &mutable_task_manager->mutex + ); + + if (mutable_task_manager->tasks != NULL && + index < mutable_task_manager->tasks->len) + { + task = background_task_ref( + g_ptr_array_index( + mutable_task_manager->tasks, + index + ) + ); + } + + g_mutex_unlock( + &mutable_task_manager->mutex + ); + + return task; +} + +gboolean task_manager_remove( + TaskManager *task_manager, + BackgroundTask *task +) +{ + BackgroundTask *removed_task = NULL; + + gsize index = 0; + + if (task_manager == NULL || + task == NULL) + { + return FALSE; + } + + g_mutex_lock( + &task_manager->mutex + ); + + for (index = 0; + index < task_manager->tasks->len; + index++) + { + if (g_ptr_array_index( + task_manager->tasks, + index + ) == task) + { + removed_task = g_ptr_array_steal_index( + task_manager->tasks, + index + ); + + break; + } + } + + g_mutex_unlock( + &task_manager->mutex + ); + + if (removed_task == NULL) + { + return FALSE; + } + + /* + * La référence du gestionnaire est libérée hors du mutex. + */ + background_task_unref( + removed_task + ); + + task_manager_notify_changed( + task_manager + ); + + return TRUE; +} + +gsize task_manager_remove_finished( + TaskManager *task_manager +) +{ + GPtrArray *removed_tasks = NULL; + + gsize index = 0; + gsize removed_count = 0; + + if (task_manager == NULL) + { + return 0; + } + + removed_tasks = + g_ptr_array_new_with_free_func( + task_manager_task_unref + ); + + g_mutex_lock( + &task_manager->mutex + ); + + /* + * Le parcours se fait à l'envers afin que la suppression d'un + * élément ne décale pas les index encore à examiner. + */ + index = task_manager->tasks->len; + + while (index > 0) + { + BackgroundTask *task = NULL; + BackgroundTaskState state; + + index--; + + task = g_ptr_array_index( + task_manager->tasks, + index + ); + + state = background_task_get_state( + task + ); + + if (task_manager_state_is_finished( + state + )) + { + BackgroundTask *removed_task = NULL; + + removed_task = g_ptr_array_steal_index( + task_manager->tasks, + index + ); + + g_ptr_array_add( + removed_tasks, + removed_task + ); + } + } + + removed_count = + removed_tasks->len; + + g_mutex_unlock( + &task_manager->mutex + ); + + /* + * Les références retirées sont libérées hors du mutex. + */ + g_ptr_array_unref( + removed_tasks + ); + + if (removed_count > 0) + { + task_manager_notify_changed( + task_manager + ); + } + + return removed_count; +} + +void task_manager_cancel_all( + TaskManager *task_manager +) +{ + GPtrArray *task_snapshot = NULL; + + gsize index = 0; + gboolean cancellation_requested = FALSE; + + if (task_manager == NULL) + { + return; + } + + task_snapshot = + g_ptr_array_new_with_free_func( + task_manager_task_unref + ); + + g_mutex_lock( + &task_manager->mutex + ); + + for (index = 0; + index < task_manager->tasks->len; + index++) + { + BackgroundTask *task = NULL; + + task = g_ptr_array_index( + task_manager->tasks, + index + ); + + g_ptr_array_add( + task_snapshot, + background_task_ref(task) + ); + } + + g_mutex_unlock( + &task_manager->mutex + ); + + /* + * L'annulation et la lecture des états sont effectuées hors + * du mutex du gestionnaire. + */ + for (index = 0; + index < task_snapshot->len; + index++) + { + BackgroundTask *task = NULL; + + task = g_ptr_array_index( + task_snapshot, + index + ); + + if (background_task_get_state(task) == + BACKGROUND_TASK_STATE_RUNNING) + { + background_task_cancel( + task + ); + + cancellation_requested = TRUE; + } + } + + g_ptr_array_unref( + task_snapshot + ); + + if (cancellation_requested) + { + task_manager_notify_changed( + task_manager + ); + } +} + +void task_manager_set_changed_callback( + TaskManager *task_manager, + TaskManagerChangedCallback callback, + gpointer user_data, + GDestroyNotify user_data_destroy +) +{ + TaskManagerCallbackSlot *new_slot = NULL; + TaskManagerCallbackSlot *old_slot = NULL; + + if (task_manager == NULL) + { + /* + * Aucun transfert de propriété n'a lieu lorsque le + * gestionnaire est invalide. + */ + return; + } + + new_slot = task_manager_callback_slot_new( + callback, + user_data, + user_data_destroy + ); + + g_mutex_lock( + &task_manager->mutex + ); + + old_slot = + task_manager->changed_slot; + + task_manager->changed_slot = + new_slot; + + g_mutex_unlock( + &task_manager->mutex + ); + + /* + * L'ancien destructeur utilisateur est appelé hors du mutex. + */ + task_manager_callback_slot_unref( + old_slot + ); +} diff --git a/src/views/main_window.c b/src/views/main_window.c index 4e395ab..c673c4a 100644 --- a/src/views/main_window.c +++ b/src/views/main_window.c @@ -7,6 +7,7 @@ #include "widgets/sidebar.h" #include "widgets/workspace.h" #include "widgets/investigation_tree_view.h" +#include "widgets/task_panel.h" #include @@ -25,6 +26,11 @@ */ #define MAIN_WINDOW_SIDEBAR_POSITION 250 +/** + * @brief Position initiale de la séparation verticale. + */ +#define MAIN_WINDOW_TASK_PANEL_POSITION 430 + /** * @brief Titre par défaut de la fenêtre. */ @@ -52,12 +58,15 @@ struct MainWindow GtkWidget *action_bar; GtkWidget *new_investigation_button; GtkWidget *open_investigation_button; + GtkWidget *demo_task_button; + GtkWidget *content_paned; GtkWidget *main_paned; GtkWidget *status_label; GtkWidget *quit_button; Sidebar *sidebar; Workspace *workspace; + TaskPanel *task_panel; MainWindowNewInvestigationCallback new_investigation_callback; @@ -76,6 +85,12 @@ struct MainWindow gpointer quit_user_data; + + MainWindowDemoTaskCallback + demo_task_callback; + + gpointer + demo_task_user_data; }; /** @@ -130,6 +145,32 @@ static void main_window_on_open_investigation_clicked( ); } +/** + * @brief Transmet la demande de tâche de démonstration au contrôleur. + * + * @param button Bouton ayant reçu le clic. + * @param user_data Pointeur vers MainWindow. + */ +static void main_window_on_demo_task_clicked( + GtkButton *button, + gpointer user_data +) +{ + MainWindow *main_window = user_data; + + (void) button; + + if (main_window == NULL || + main_window->demo_task_callback == NULL) + { + return; + } + + main_window->demo_task_callback( + main_window->demo_task_user_data + ); +} + /** * @brief Transmet la demande de fermeture au contrôleur. * @@ -156,13 +197,18 @@ static void main_window_on_quit_clicked( ); } -MainWindow *main_window_new(GtkApplication *application) +MainWindow *main_window_new( + GtkApplication *application, + TaskManager *task_manager +) { MainWindow *main_window = NULL; GtkWidget *sidebar_widget = NULL; GtkWidget *workspace_widget = NULL; + GtkWidget *task_panel_widget = NULL; - if (application == NULL) + if (application == NULL || + task_manager == NULL) { return NULL; } @@ -239,6 +285,11 @@ MainWindow *main_window_new(GtkApplication *application) "Ouvrir une enquête" ); + main_window->demo_task_button = + gtk_button_new_with_label( + "Tâche de test" + ); + main_window->quit_button = gtk_button_new_with_label( "Quitter" @@ -254,6 +305,11 @@ MainWindow *main_window_new(GtkApplication *application) main_window->open_investigation_button ); + gtk_box_append( + GTK_BOX(main_window->action_bar), + main_window->demo_task_button + ); + gtk_box_append( GTK_BOX(main_window->action_bar), main_window->quit_button @@ -277,6 +333,15 @@ MainWindow *main_window_new(GtkApplication *application) main_window ); + g_signal_connect( + main_window->demo_task_button, + "clicked", + G_CALLBACK( + main_window_on_demo_task_clicked + ), + main_window + ); + g_signal_connect( main_window->quit_button, "clicked", @@ -345,6 +410,32 @@ MainWindow *main_window_new(GtkApplication *application) return NULL; } + main_window->task_panel = task_panel_new( + task_manager + ); + + if (main_window->task_panel == NULL) + { + main_window_free( + main_window + ); + + return NULL; + } + + task_panel_widget = task_panel_get_widget( + main_window->task_panel + ); + + if (task_panel_widget == NULL) + { + main_window_free( + main_window + ); + + return NULL; + } + /* * Placement des deux composants dans GtkPaned. */ @@ -393,6 +484,55 @@ MainWindow *main_window_new(GtkApplication *application) TRUE ); + main_window->content_paned = gtk_paned_new( + GTK_ORIENTATION_VERTICAL + ); + + gtk_widget_set_hexpand( + main_window->content_paned, + TRUE + ); + + gtk_widget_set_vexpand( + main_window->content_paned, + TRUE + ); + + gtk_paned_set_start_child( + GTK_PANED(main_window->content_paned), + main_window->main_paned + ); + + gtk_paned_set_end_child( + GTK_PANED(main_window->content_paned), + task_panel_widget + ); + + gtk_paned_set_position( + GTK_PANED(main_window->content_paned), + MAIN_WINDOW_TASK_PANEL_POSITION + ); + + gtk_paned_set_resize_start_child( + GTK_PANED(main_window->content_paned), + TRUE + ); + + gtk_paned_set_resize_end_child( + GTK_PANED(main_window->content_paned), + FALSE + ); + + gtk_paned_set_shrink_start_child( + GTK_PANED(main_window->content_paned), + TRUE + ); + + gtk_paned_set_shrink_end_child( + GTK_PANED(main_window->content_paned), + TRUE + ); + main_window->status_label = gtk_label_new( MAIN_WINDOW_NO_INVESTIGATION_STATUS ); @@ -436,7 +576,7 @@ MainWindow *main_window_new(GtkApplication *application) gtk_box_append( GTK_BOX(main_window->main_box), - main_window->main_paned + main_window->content_paned ); gtk_box_append( @@ -449,6 +589,9 @@ MainWindow *main_window_new(GtkApplication *application) main_window->main_box ); + /* + * Ce volet vertical sépare la zone principale du panneau d'activité. + */ return main_window; } @@ -652,6 +795,21 @@ void main_window_set_open_investigation_callback( main_window->open_investigation_user_data = user_data; } +void main_window_set_demo_task_callback( + MainWindow *main_window, + MainWindowDemoTaskCallback callback, + gpointer user_data +) +{ + if (main_window == NULL) + { + return; + } + + main_window->demo_task_callback = callback; + main_window->demo_task_user_data = user_data; +} + void main_window_set_quit_callback( MainWindow *main_window, MainWindowQuitCallback callback, @@ -693,11 +851,57 @@ void main_window_free( } /* - * La fenêtre et tous ses widgets doivent être détruits pendant que - * les structures MainWindow, Sidebar et Workspace existent encore. - * - * Certains widgets possèdent des callbacks dont user_data pointe vers - * ces structures. + * TaskPanel possède un timer et un callback enregistré dans + * TaskManager. Ils doivent être retirés avant la destruction + * des widgets GTK. + */ + task_panel_free( + main_window->task_panel + ); + + main_window->task_panel = NULL; + + /* + * Empêche la destruction du modèle de transmettre une dernière + * sélection à Application. + */ + if (main_window->sidebar != NULL) + { + sidebar_set_selection_callback( + main_window->sidebar, + NULL, + NULL + ); + + /* + * Le modèle est détaché pendant que le GtkListView existe + * encore. + */ + sidebar_set_tree_model( + main_window->sidebar, + NULL + ); + } + + /* + * Les structures qui manipulent encore leurs widgets doivent être + * nettoyées avant gtk_window_destroy(). + */ + sidebar_free( + main_window->sidebar + ); + + workspace_free( + main_window->workspace + ); + + main_window->sidebar = NULL; + main_window->workspace = NULL; + + /* + * Les modules ne manipulent plus leurs widgets. + * GTK peut maintenant détruire tout l'arbre sans callback vers + * des structures déjà libérées. */ if (main_window->window != NULL) { @@ -708,16 +912,7 @@ void main_window_free( main_window->window = NULL; } - workspace_free( - main_window->workspace + g_free( + main_window ); - - sidebar_free( - main_window->sidebar - ); - - main_window->workspace = NULL; - main_window->sidebar = NULL; - - g_free(main_window); } diff --git a/src/widgets/task_panel.c b/src/widgets/task_panel.c new file mode 100644 index 0000000..c469699 --- /dev/null +++ b/src/widgets/task_panel.c @@ -0,0 +1,1105 @@ +/****************************************************************************** + * @file task_panel.c + * @brief Implémentation du panneau GTK affichant les tâches. + ******************************************************************************/ + +#include "widgets/task_panel.h" + +/** + * @brief Intervalle de rafraîchissement du panneau, en millisecondes. + */ +#define TASK_PANEL_REFRESH_INTERVAL_MS 250 + +/** + * @brief Texte affiché lorsqu’aucune tâche n’est suivie. + */ +#define TASK_PANEL_EMPTY_TEXT \ + "Aucune tâche en cours ou terminée" + +/** + * @struct TaskPanelRow + * @brief Widgets persistants associés à une tâche. + * + * Les widgets sont créés une seule fois. Le rafraîchissement périodique + * modifie seulement leur contenu. + */ +typedef struct +{ + BackgroundTask *task; + + GtkWidget *frame; + GtkWidget *state_label; + GtkWidget *progress_bar; + GtkWidget *detail_label; + GtkWidget *cancel_button; +} TaskPanelRow; + +/** + * @struct TaskPanel + * @brief État interne du panneau d’activité. + * + * TaskManager est emprunté et doit rester valide pendant toute la durée + * de vie du panneau. + */ +struct TaskPanel +{ + TaskManager *task_manager; + + GtkWidget *root_box; + GtkWidget *header_box; + GtkWidget *title_label; + GtkWidget *clear_button; + + GtkWidget *scrolled_window; + GtkWidget *task_list_box; + + GPtrArray *rows; + + guint refresh_source_id; +}; + +/** + * @brief Retourne le libellé correspondant à l’état d’une tâche. + * + * @param state État de la tâche. + * + * @return Libellé statique. + */ +static const char *task_panel_get_state_label( + BackgroundTaskState state +) +{ + switch (state) + { + case BACKGROUND_TASK_STATE_PENDING: + return "En attente"; + + case BACKGROUND_TASK_STATE_RUNNING: + return "En cours"; + + case BACKGROUND_TASK_STATE_COMPLETED: + return "Terminée"; + + case BACKGROUND_TASK_STATE_FAILED: + return "Échouée"; + + case BACKGROUND_TASK_STATE_CANCELLED: + return "Annulée"; + + default: + return "État inconnu"; + } +} + +/** + * @brief Supprime tous les widgets enfants d’une boîte GTK. + * + * @param box Boîte à vider. + */ +static void task_panel_clear_box( + GtkWidget *box +) +{ + GtkWidget *child = NULL; + + if (box == NULL) + { + return; + } + + child = gtk_widget_get_first_child( + box + ); + + while (child != NULL) + { + gtk_box_remove( + GTK_BOX(box), + child + ); + + child = gtk_widget_get_first_child( + box + ); + } +} + +/** + * @brief Demande l’annulation d’une tâche. + * + * La tâche est conservée dans les données du bouton avec une référence + * indépendante. + * + * @param button Bouton ayant reçu le clic. + * @param user_data Pointeur vers TaskPanel. + */ +static void task_panel_on_cancel_clicked( + GtkButton *button, + gpointer user_data +) +{ + TaskPanel *task_panel = user_data; + BackgroundTask *task = NULL; + + if (button == NULL || + task_panel == NULL) + { + return; + } + + task = g_object_get_data( + G_OBJECT(button), + "labfy-background-task" + ); + + if (task == NULL) + { + return; + } + + background_task_cancel( + task + ); +} + +/** + * @brief Retire les tâches terminées du gestionnaire. + * + * @param button Bouton ayant reçu le clic. + * @param user_data Pointeur vers TaskPanel. + */ +static void task_panel_on_clear_clicked( + GtkButton *button, + gpointer user_data +) +{ + TaskPanel *task_panel = user_data; + + (void) button; + + if (task_panel == NULL || + task_panel->task_manager == NULL) + { + return; + } + + task_manager_remove_finished( + task_panel->task_manager + ); +} + +/** + * @brief Réagit aux changements structurels du gestionnaire. + * + * @param task_manager Gestionnaire ayant changé. + * @param user_data Pointeur vers TaskPanel. + */ +static void task_panel_on_manager_changed( + TaskManager *task_manager, + gpointer user_data +) +{ + TaskPanel *task_panel = user_data; + + if (task_panel == NULL || + task_manager == NULL || + task_panel->task_manager != task_manager) + { + return; + } + + task_panel_refresh( + task_panel + ); +} + +/** + * @brief Met à jour les lignes existantes sans reconstruire la liste. + * + * @param task_panel Panneau à mettre à jour. + */ +static void task_panel_update_rows( + TaskPanel *task_panel +); + +/** + * @brief Rafraîchit périodiquement les progressions. + * + * @param user_data Pointeur vers TaskPanel. + * + * @return G_SOURCE_CONTINUE. + */ +static gboolean task_panel_on_refresh_timeout( + gpointer user_data +) +{ + TaskPanel *task_panel = user_data; + + if (task_panel == NULL) + { + return G_SOURCE_REMOVE; + } + + task_panel_update_rows( + task_panel + ); + + return G_SOURCE_CONTINUE; +} + +/** + * @brief Crée le texte secondaire d’une tâche. + * + * @param task Tâche concernée. + * @param state État courant. + * @param progress Progression courante. + * @param status_message Message de progression facultatif. + * @param error Erreur finale facultative. + * + * @return Nouvelle chaîne à libérer avec g_free(). + */ +static char *task_panel_create_detail_text( + const BackgroundTask *task, + BackgroundTaskState state, + double progress, + const char *status_message, + const GError *error +) +{ + const char *state_label = NULL; + + if (task == NULL) + { + return g_strdup( + "Tâche invalide" + ); + } + + state_label = task_panel_get_state_label( + state + ); + + if (state == BACKGROUND_TASK_STATE_RUNNING) + { + if (status_message != NULL && + status_message[0] != '\0') + { + return g_strdup_printf( + "%s — %.0f %% — %s", + state_label, + progress * 100.0, + status_message + ); + } + + return g_strdup_printf( + "%s — %.0f %%", + state_label, + progress * 100.0 + ); + } + + if (state == BACKGROUND_TASK_STATE_FAILED && + error != NULL) + { + return g_strdup_printf( + "%s — %s", + state_label, + error->message + ); + } + + if (status_message != NULL && + status_message[0] != '\0') + { + return g_strdup_printf( + "%s — %s", + state_label, + status_message + ); + } + + return g_strdup( + state_label + ); +} + +/** + * @brief Libère les données privées d'une ligne. + * + * @param user_data Pointeur vers TaskPanelRow. + */ +static void task_panel_row_free( + gpointer user_data +) +{ + TaskPanelRow *task_row = user_data; + + if (task_row == NULL) + { + return; + } + + background_task_unref( + task_row->task + ); + + task_row->task = NULL; + + g_free( + task_row + ); +} + +/** + * @brief Met à jour l'affichage d'une ligne existante. + * + * Aucun widget n'est détruit ici. + * + * @param task_row Ligne à mettre à jour. + */ +static void task_panel_row_update( + TaskPanelRow *task_row +) +{ + BackgroundTaskState state; + double progress = 0.0; + + char *status_message = NULL; + char *detail_text = NULL; + + GError *error = NULL; + + if (task_row == NULL || + task_row->task == NULL) + { + return; + } + + state = background_task_get_state( + task_row->task + ); + + progress = background_task_get_progress( + task_row->task + ); + + status_message = + background_task_dup_status_message( + task_row->task + ); + + error = background_task_dup_error( + task_row->task + ); + + detail_text = task_panel_create_detail_text( + task_row->task, + state, + progress, + status_message, + error + ); + + gtk_label_set_text( + GTK_LABEL(task_row->state_label), + task_panel_get_state_label( + state + ) + ); + + gtk_progress_bar_set_fraction( + GTK_PROGRESS_BAR( + task_row->progress_bar + ), + state == BACKGROUND_TASK_STATE_COMPLETED + ? 1.0 + : progress + ); + + gtk_label_set_text( + GTK_LABEL(task_row->detail_label), + detail_text != NULL + ? detail_text + : "" + ); + + /* + * Le bouton existe en permanence, mais n'est visible que lorsque + * la tâche peut être annulée. + */ + gtk_widget_set_visible( + task_row->cancel_button, + state == BACKGROUND_TASK_STATE_RUNNING + ); + + gtk_widget_set_sensitive( + task_row->cancel_button, + state == BACKGROUND_TASK_STATE_RUNNING + ); + + g_clear_error( + &error + ); + + g_free( + detail_text + ); + + g_free( + status_message + ); +} + +/** + * @brief Crée une ligne GTK persistante pour une tâche. + * + * @param task_panel Panneau propriétaire. + * @param task Tâche à afficher. + * + * @return Nouvelle ligne, ou NULL. + */ +static TaskPanelRow *task_panel_create_task_row( + TaskPanel *task_panel, + BackgroundTask *task +) +{ + TaskPanelRow *task_row = NULL; + + GtkWidget *row_box = NULL; + GtkWidget *header_box = NULL; + GtkWidget *title_label = NULL; + + const char *title = NULL; + + if (task_panel == NULL || + task == NULL) + { + return NULL; + } + + task_row = g_new0( + TaskPanelRow, + 1 + ); + + task_row->task = background_task_ref( + task + ); + + task_row->frame = gtk_frame_new( + NULL + ); + + row_box = gtk_box_new( + GTK_ORIENTATION_VERTICAL, + 6 + ); + + gtk_widget_set_margin_start( + row_box, + 10 + ); + + gtk_widget_set_margin_end( + row_box, + 10 + ); + + gtk_widget_set_margin_top( + row_box, + 8 + ); + + gtk_widget_set_margin_bottom( + row_box, + 8 + ); + + header_box = gtk_box_new( + GTK_ORIENTATION_HORIZONTAL, + 8 + ); + + title = background_task_get_title( + task + ); + + title_label = gtk_label_new( + title != NULL + ? title + : "Tâche sans titre" + ); + + gtk_widget_set_hexpand( + title_label, + TRUE + ); + + gtk_widget_set_halign( + title_label, + GTK_ALIGN_START + ); + + gtk_label_set_xalign( + GTK_LABEL(title_label), + 0.0F + ); + + gtk_label_set_ellipsize( + GTK_LABEL(title_label), + PANGO_ELLIPSIZE_END + ); + + task_row->state_label = gtk_label_new( + NULL + ); + + gtk_widget_set_halign( + task_row->state_label, + GTK_ALIGN_END + ); + + task_row->cancel_button = + gtk_button_new_with_label( + "Annuler" + ); + + gtk_widget_set_halign( + task_row->cancel_button, + GTK_ALIGN_END + ); + + gtk_widget_set_valign( + task_row->cancel_button, + GTK_ALIGN_CENTER + ); + + /* + * TaskPanelRow conserve déjà une référence sur la tâche. + * Le bouton emprunte donc simplement ce pointeur. + */ + g_object_set_data( + G_OBJECT(task_row->cancel_button), + "labfy-background-task", + task_row->task + ); + + g_signal_connect( + task_row->cancel_button, + "clicked", + G_CALLBACK( + task_panel_on_cancel_clicked + ), + task_panel + ); + + gtk_box_append( + GTK_BOX(header_box), + title_label + ); + + gtk_box_append( + GTK_BOX(header_box), + task_row->state_label + ); + + gtk_box_append( + GTK_BOX(header_box), + task_row->cancel_button + ); + + task_row->progress_bar = + gtk_progress_bar_new(); + + gtk_progress_bar_set_show_text( + GTK_PROGRESS_BAR( + task_row->progress_bar + ), + TRUE + ); + + task_row->detail_label = gtk_label_new( + NULL + ); + + gtk_widget_set_halign( + task_row->detail_label, + GTK_ALIGN_START + ); + + gtk_label_set_xalign( + GTK_LABEL(task_row->detail_label), + 0.0F + ); + + gtk_label_set_wrap( + GTK_LABEL(task_row->detail_label), + TRUE + ); + + gtk_label_set_wrap_mode( + GTK_LABEL(task_row->detail_label), + PANGO_WRAP_WORD_CHAR + ); + + gtk_label_set_selectable( + GTK_LABEL(task_row->detail_label), + TRUE + ); + + gtk_box_append( + GTK_BOX(row_box), + header_box + ); + + gtk_box_append( + GTK_BOX(row_box), + task_row->progress_bar + ); + + gtk_box_append( + GTK_BOX(row_box), + task_row->detail_label + ); + + gtk_frame_set_child( + GTK_FRAME(task_row->frame), + row_box + ); + + task_panel_row_update( + task_row + ); + + return task_row; +} + +/** + * @brief Supprime les widgets et les données de toutes les lignes. + * + * @param task_panel Panneau à vider. + */ +static void task_panel_clear_rows( + TaskPanel *task_panel +) +{ + if (task_panel == NULL) + { + return; + } + + task_panel_clear_box( + task_panel->task_list_box + ); + + if (task_panel->rows != NULL) + { + g_ptr_array_set_size( + task_panel->rows, + 0 + ); + } +} + +/** + * @brief Met à jour toutes les lignes persistantes. + * + * @param task_panel Panneau concerné. + */ +static void task_panel_update_rows( + TaskPanel *task_panel +) +{ + guint index = 0; + + if (task_panel == NULL || + task_panel->rows == NULL) + { + return; + } + + for (index = 0; + index < task_panel->rows->len; + index++) + { + TaskPanelRow *task_row = NULL; + + task_row = g_ptr_array_index( + task_panel->rows, + index + ); + + task_panel_row_update( + task_row + ); + } +} + +TaskPanel *task_panel_new( + TaskManager *task_manager +) +{ + TaskPanel *task_panel = NULL; + + if (task_manager == NULL) + { + return NULL; + } + + task_panel = g_new0( + TaskPanel, + 1 + ); + + task_panel->task_manager = + task_manager; + + task_panel->rows = + g_ptr_array_new_with_free_func( + task_panel_row_free + ); + + task_panel->root_box = gtk_box_new( + GTK_ORIENTATION_VERTICAL, + 8 + ); + + task_panel->header_box = gtk_box_new( + GTK_ORIENTATION_HORIZONTAL, + 8 + ); + + gtk_widget_set_margin_start( + task_panel->header_box, + 8 + ); + + gtk_widget_set_margin_end( + task_panel->header_box, + 8 + ); + + gtk_widget_set_margin_top( + task_panel->header_box, + 8 + ); + + task_panel->title_label = gtk_label_new( + "Activité" + ); + + gtk_widget_set_hexpand( + task_panel->title_label, + TRUE + ); + + gtk_widget_set_halign( + task_panel->title_label, + GTK_ALIGN_START + ); + + gtk_label_set_xalign( + GTK_LABEL(task_panel->title_label), + 0.0F + ); + + task_panel->clear_button = + gtk_button_new_with_label( + "Nettoyer" + ); + + g_signal_connect( + task_panel->clear_button, + "clicked", + G_CALLBACK( + task_panel_on_clear_clicked + ), + task_panel + ); + + gtk_box_append( + GTK_BOX(task_panel->header_box), + task_panel->title_label + ); + + gtk_box_append( + GTK_BOX(task_panel->header_box), + task_panel->clear_button + ); + + task_panel->task_list_box = gtk_box_new( + GTK_ORIENTATION_VERTICAL, + 8 + ); + + gtk_widget_set_margin_start( + task_panel->task_list_box, + 8 + ); + + gtk_widget_set_margin_end( + task_panel->task_list_box, + 8 + ); + + gtk_widget_set_margin_bottom( + task_panel->task_list_box, + 8 + ); + + task_panel->scrolled_window = + gtk_scrolled_window_new(); + + gtk_scrolled_window_set_policy( + GTK_SCROLLED_WINDOW( + task_panel->scrolled_window + ), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC + ); + + gtk_scrolled_window_set_child( + GTK_SCROLLED_WINDOW( + task_panel->scrolled_window + ), + task_panel->task_list_box + ); + + gtk_widget_set_vexpand( + task_panel->scrolled_window, + TRUE + ); + + gtk_box_append( + GTK_BOX(task_panel->root_box), + task_panel->header_box + ); + + gtk_box_append( + GTK_BOX(task_panel->root_box), + task_panel->scrolled_window + ); + + task_manager_set_changed_callback( + task_manager, + task_panel_on_manager_changed, + task_panel, + NULL + ); + + task_panel->refresh_source_id = + g_timeout_add( + TASK_PANEL_REFRESH_INTERVAL_MS, + task_panel_on_refresh_timeout, + task_panel + ); + + if (task_panel->refresh_source_id == 0) + { + task_manager_set_changed_callback( + task_manager, + NULL, + NULL, + NULL + ); + + g_ptr_array_unref( + task_panel->rows + ); + + task_panel->rows = NULL; + + g_free( + task_panel + ); + + return NULL; + } + + task_panel_refresh( + task_panel + ); + + return task_panel; +} + +GtkWidget *task_panel_get_widget( + const TaskPanel *task_panel +) +{ + if (task_panel == NULL) + { + return NULL; + } + + return task_panel->root_box; +} + +void task_panel_refresh( + TaskPanel *task_panel +) +{ + gsize task_count = 0; + gsize index = 0; + + if (task_panel == NULL || + task_panel->task_manager == NULL || + task_panel->task_list_box == NULL || + task_panel->rows == NULL) + { + return; + } + + /* + * La liste n'est reconstruite que lorsque TaskManager signale + * un ajout ou une suppression. + */ + task_panel_clear_rows( + task_panel + ); + + task_count = task_manager_get_count( + task_panel->task_manager + ); + + gtk_widget_set_sensitive( + task_panel->clear_button, + task_count > 0 + ); + + if (task_count == 0) + { + GtkWidget *empty_label = NULL; + + empty_label = gtk_label_new( + TASK_PANEL_EMPTY_TEXT + ); + + gtk_widget_set_margin_top( + empty_label, + 16 + ); + + gtk_widget_set_margin_bottom( + empty_label, + 16 + ); + + gtk_widget_set_halign( + empty_label, + GTK_ALIGN_CENTER + ); + + gtk_box_append( + GTK_BOX(task_panel->task_list_box), + empty_label + ); + + return; + } + + for (index = 0; + index < task_count; + index++) + { + BackgroundTask *task = NULL; + TaskPanelRow *task_row = NULL; + + task = task_manager_get_task( + task_panel->task_manager, + index + ); + + if (task == NULL) + { + continue; + } + + task_row = task_panel_create_task_row( + task_panel, + task + ); + + if (task_row != NULL) + { + gtk_box_append( + GTK_BOX( + task_panel->task_list_box + ), + task_row->frame + ); + + g_ptr_array_add( + task_panel->rows, + task_row + ); + } + + background_task_unref( + task + ); + } +} + +void task_panel_free( + TaskPanel *task_panel +) +{ + if (task_panel == NULL) + { + return; + } + + /* + * Le callback du manager ne doit plus pouvoir utiliser TaskPanel. + */ + if (task_panel->task_manager != NULL) + { + task_manager_set_changed_callback( + task_panel->task_manager, + NULL, + NULL, + NULL + ); + } + + /* + * Arrêt du rafraîchissement périodique. + */ + if (task_panel->refresh_source_id != 0) + { + g_source_remove( + task_panel->refresh_source_id + ); + + task_panel->refresh_source_id = 0; + } + + if (task_panel->clear_button != NULL) + { + g_signal_handlers_disconnect_by_data( + task_panel->clear_button, + task_panel + ); + } + + task_panel_clear_rows( + task_panel + ); + + if (task_panel->rows != NULL) + { + g_ptr_array_unref( + task_panel->rows + ); + + task_panel->rows = NULL; + } + + task_panel->task_manager = NULL; + + g_free( + task_panel + ); +} diff --git a/tests/test_background_task b/tests/test_background_task deleted file mode 100755 index 0c76ca0..0000000 Binary files a/tests/test_background_task and /dev/null differ diff --git a/tests/test_database b/tests/test_database deleted file mode 100755 index 2a1ae25..0000000 Binary files a/tests/test_database and /dev/null differ diff --git a/tests/test_error b/tests/test_error deleted file mode 100755 index 1185635..0000000 Binary files a/tests/test_error and /dev/null differ diff --git a/tests/test_investigation_dao b/tests/test_investigation_dao deleted file mode 100755 index 0234ddd..0000000 Binary files a/tests/test_investigation_dao and /dev/null differ diff --git a/tests/test_investigation_node b/tests/test_investigation_node deleted file mode 100755 index b7614ef..0000000 Binary files a/tests/test_investigation_node and /dev/null differ diff --git a/tests/test_investigation_project b/tests/test_investigation_project deleted file mode 100755 index 3bd6369..0000000 Binary files a/tests/test_investigation_project and /dev/null differ diff --git a/tests/test_investigation_record b/tests/test_investigation_record deleted file mode 100755 index ba2880b..0000000 Binary files a/tests/test_investigation_record and /dev/null differ diff --git a/tests/test_investigation_session b/tests/test_investigation_session deleted file mode 100755 index 04f8d66..0000000 Binary files a/tests/test_investigation_session and /dev/null differ diff --git a/tests/test_investigation_tree_builder b/tests/test_investigation_tree_builder deleted file mode 100755 index d6cd484..0000000 Binary files a/tests/test_investigation_tree_builder and /dev/null differ diff --git a/tests/test_investigation_tree_model b/tests/test_investigation_tree_model deleted file mode 100755 index 2c823e1..0000000 Binary files a/tests/test_investigation_tree_model and /dev/null differ diff --git a/tests/test_statement b/tests/test_statement deleted file mode 100755 index 79b937b..0000000 Binary files a/tests/test_statement and /dev/null differ diff --git a/tests/test_task_manager b/tests/test_task_manager new file mode 100755 index 0000000..9c71a73 Binary files /dev/null and b/tests/test_task_manager differ diff --git a/tests/test_task_manager.c b/tests/test_task_manager.c new file mode 100644 index 0000000..fe42ed5 --- /dev/null +++ b/tests/test_task_manager.c @@ -0,0 +1,1116 @@ +/****************************************************************************** + * @file test_task_manager.c + * @brief Tests du gestionnaire central des tâches. + ******************************************************************************/ + +#include "core/task_manager.h" + +#include +#include +#include + +#define TEST_TASK_MANAGER_TIMEOUT_SECONDS 5 + +/** + * @struct TestTaskManagerCallbackData + * @brief Données utilisées pour contrôler le callback du gestionnaire. + */ +typedef struct +{ + guint *changed_count; + guint *destroy_count; +} TestTaskManagerCallbackData; + +/** + * @struct TestTaskManagerAsyncGroup + * @brief Suit la fin de plusieurs tâches asynchrones. + */ +typedef struct +{ + GMainLoop *main_loop; + guint remaining_tasks; +} TestTaskManagerAsyncGroup; + +/** + * @brief Interrompt un test asynchrone trop long. + * + * @param user_data Boucle principale du test. + * + * @return G_SOURCE_REMOVE. + */ +static gboolean test_task_manager_timeout( + gpointer user_data +) +{ + GMainLoop *main_loop = user_data; + + if (main_loop != NULL) + { + g_main_loop_quit( + main_loop + ); + } + + return G_SOURCE_REMOVE; +} + +/** + * @brief Compte la fin d'une tâche d'un groupe. + * + * @param task Tâche terminée. + * @param user_data Groupe asynchrone. + */ +static void test_task_manager_group_completed( + BackgroundTask *task, + gpointer user_data +) +{ + TestTaskManagerAsyncGroup *group = + user_data; + + assert(task != NULL); + assert(group != NULL); + assert(group->remaining_tasks > 0); + + group->remaining_tasks--; + + if (group->remaining_tasks == 0) + { + g_main_loop_quit( + group->main_loop + ); + } +} + +/** + * @brief Compte les notifications émises par le gestionnaire. + * + * @param task_manager Gestionnaire ayant changé. + * @param user_data Données TestTaskManagerCallbackData. + */ +static void test_task_manager_changed( + TaskManager *task_manager, + gpointer user_data +) +{ + TestTaskManagerCallbackData *callback_data = + user_data; + + assert(task_manager != NULL); + assert(callback_data != NULL); + assert(callback_data->changed_count != NULL); + + (*callback_data->changed_count)++; +} + +/** + * @brief Détruit les données du callback et compte leur destruction. + * + * @param user_data Données TestTaskManagerCallbackData. + */ +static void test_task_manager_callback_data_free( + gpointer user_data +) +{ + TestTaskManagerCallbackData *callback_data = + user_data; + + assert(callback_data != NULL); + assert(callback_data->destroy_count != NULL); + + (*callback_data->destroy_count)++; + + g_free( + callback_data + ); +} + +/** + * @brief Installe un callback de test sur un gestionnaire. + * + * @param task_manager Gestionnaire concerné. + * @param changed_count Compteur de notifications. + * @param destroy_count Compteur de destructions. + */ +static void test_task_manager_install_callback( + TaskManager *task_manager, + guint *changed_count, + guint *destroy_count +) +{ + TestTaskManagerCallbackData *callback_data = + NULL; + + assert(task_manager != NULL); + assert(changed_count != NULL); + assert(destroy_count != NULL); + + callback_data = g_new0( + TestTaskManagerCallbackData, + 1 + ); + + callback_data->changed_count = + changed_count; + + callback_data->destroy_count = + destroy_count; + + task_manager_set_changed_callback( + task_manager, + test_task_manager_changed, + callback_data, + test_task_manager_callback_data_free + ); +} + +/** + * @brief Vérifie la construction d'un gestionnaire vide. + */ +static void test_task_manager_creation(void) +{ + TaskManager *task_manager = NULL; + + task_manager = task_manager_new(); + + assert(task_manager != NULL); + + assert( + task_manager_get_count( + task_manager + ) == 0 + ); + + assert( + task_manager_get_task( + task_manager, + 0 + ) == NULL + ); + + assert( + task_manager_get_count(NULL) == 0 + ); + + assert( + task_manager_get_task( + NULL, + 0 + ) == NULL + ); + + task_manager_cancel_all( + NULL + ); + + assert( + task_manager_remove_finished(NULL) == 0 + ); + + task_manager_free( + task_manager + ); + + task_manager_free( + NULL + ); +} + +/** + * @brief Vérifie l'ajout et la récupération d'une tâche. + */ +static void test_task_manager_add(void) +{ + TaskManager *task_manager = NULL; + + BackgroundTask *task = NULL; + BackgroundTask *retrieved_task = NULL; + + GError *error = NULL; + + guint changed_count = 0; + guint destroy_count = 0; + + task_manager = task_manager_new(); + + assert(task_manager != NULL); + + test_task_manager_install_callback( + task_manager, + &changed_count, + &destroy_count + ); + + task = background_task_new( + "Tâche ajoutée" + ); + + assert(task != NULL); + + assert( + task_manager_add( + task_manager, + task, + &error + ) + ); + + assert(error == NULL); + + assert( + task_manager_get_count( + task_manager + ) == 1 + ); + + assert(changed_count == 1); + + retrieved_task = task_manager_get_task( + task_manager, + 0 + ); + + assert(retrieved_task != NULL); + + /* + * Le manager retourne une nouvelle référence vers la même instance. + */ + assert(retrieved_task == task); + + assert( + strcmp( + background_task_get_title( + retrieved_task + ), + "Tâche ajoutée" + ) == 0 + ); + + background_task_unref( + retrieved_task + ); + + /* + * Le gestionnaire possède encore sa référence. + * L'appelant possède toujours la référence créée par new(). + */ + task_manager_free( + task_manager + ); + + assert(destroy_count == 1); + + assert( + strcmp( + background_task_get_title(task), + "Tâche ajoutée" + ) == 0 + ); + + background_task_unref( + task + ); +} + +/** + * @brief Vérifie qu'une même tâche ne peut pas être ajoutée deux fois. + */ +static void test_task_manager_duplicate(void) +{ + TaskManager *task_manager = NULL; + BackgroundTask *task = NULL; + + GError *error = NULL; + + guint changed_count = 0; + guint destroy_count = 0; + + task_manager = task_manager_new(); + task = background_task_new( + "Tâche unique" + ); + + assert(task_manager != NULL); + assert(task != NULL); + + test_task_manager_install_callback( + task_manager, + &changed_count, + &destroy_count + ); + + assert( + task_manager_add( + task_manager, + task, + &error + ) + ); + + assert(error == NULL); + + assert( + !task_manager_add( + task_manager, + task, + &error + ) + ); + + assert(error != NULL); + + assert( + error->domain == + TASK_MANAGER_ERROR + ); + + assert( + error->code == + TASK_MANAGER_ERROR_ALREADY_ADDED + ); + + g_clear_error( + &error + ); + + assert( + task_manager_get_count( + task_manager + ) == 1 + ); + + /* + * Seul le premier ajout constitue un changement. + */ + assert(changed_count == 1); + + task_manager_free( + task_manager + ); + + assert(destroy_count == 1); + + background_task_unref( + task + ); +} + +/** + * @brief Vérifie le retrait d'une tâche. + */ +static void test_task_manager_remove_task(void) +{ + TaskManager *task_manager = NULL; + BackgroundTask *task = NULL; + + GError *error = NULL; + + guint changed_count = 0; + guint destroy_count = 0; + + task_manager = task_manager_new(); + + task = background_task_new( + "Tâche retirée" + ); + + assert(task_manager != NULL); + assert(task != NULL); + + test_task_manager_install_callback( + task_manager, + &changed_count, + &destroy_count + ); + + assert( + task_manager_add( + task_manager, + task, + &error + ) + ); + + assert(error == NULL); + assert(changed_count == 1); + + assert( + task_manager_remove( + task_manager, + task + ) + ); + + assert( + task_manager_get_count( + task_manager + ) == 0 + ); + + assert(changed_count == 2); + + /* + * Une seconde tentative ne modifie rien. + */ + assert( + !task_manager_remove( + task_manager, + task + ) + ); + + assert(changed_count == 2); + + /* + * La référence de l'appelant reste valide après le retrait. + */ + assert( + strcmp( + background_task_get_title(task), + "Tâche retirée" + ) == 0 + ); + + task_manager_free( + task_manager + ); + + assert(destroy_count == 1); + + background_task_unref( + task + ); +} + +/** + * @brief Worker terminant normalement. + */ +static gboolean test_task_manager_success_worker( + BackgroundTask *task, + GCancellable *cancellable, + gpointer worker_data, + gpointer *result, + GError **error +) +{ + (void) task; + (void) worker_data; + + assert(cancellable != NULL); + assert(result != NULL); + assert(error != NULL); + assert(*error == NULL); + + *result = NULL; + + return TRUE; +} + +/** + * @brief Worker terminant avec une erreur. + */ +static gboolean test_task_manager_failure_worker( + BackgroundTask *task, + GCancellable *cancellable, + gpointer worker_data, + gpointer *result, + GError **error +) +{ + (void) task; + (void) cancellable; + (void) worker_data; + (void) result; + + assert(error != NULL); + assert(*error == NULL); + + g_set_error_literal( + error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Échec volontaire." + ); + + return FALSE; +} + +/** + * @brief Worker suffisamment lent pour tester l'annulation. + */ +static gboolean test_task_manager_wait_worker( + BackgroundTask *task, + GCancellable *cancellable, + gpointer worker_data, + gpointer *result, + GError **error +) +{ + guint step = 0; + + (void) worker_data; + + assert(task != NULL); + assert(cancellable != NULL); + assert(result != NULL); + assert(error != NULL); + assert(*error == NULL); + + for (step = 0; step < 100; step++) + { + if (g_cancellable_set_error_if_cancelled( + cancellable, + error + )) + { + return FALSE; + } + + background_task_report_progress( + task, + (double) step / 100.0, + "Traitement en cours" + ); + + g_usleep( + 10000 + ); + } + + *result = NULL; + + return TRUE; +} + +/** + * @brief Vérifie que seules les tâches terminées sont retirées. + */ +static void test_task_manager_remove_finished_tasks(void) +{ + TaskManager *task_manager = NULL; + + BackgroundTask *pending_task = NULL; + BackgroundTask *completed_task = NULL; + BackgroundTask *failed_task = NULL; + BackgroundTask *cancelled_task = NULL; + BackgroundTask *remaining_task = NULL; + + TestTaskManagerAsyncGroup group = {0}; + + GMainLoop *main_loop = NULL; + GError *error = NULL; + + guint timeout_source_id = 0; + guint changed_count = 0; + guint destroy_count = 0; + + main_loop = g_main_loop_new( + NULL, + FALSE + ); + + assert(main_loop != NULL); + + group.main_loop = main_loop; + group.remaining_tasks = 3; + + task_manager = task_manager_new(); + + pending_task = background_task_new( + "En attente" + ); + + completed_task = background_task_new( + "Terminée" + ); + + failed_task = background_task_new( + "Échouée" + ); + + cancelled_task = background_task_new( + "Annulée" + ); + + assert(task_manager != NULL); + assert(pending_task != NULL); + assert(completed_task != NULL); + assert(failed_task != NULL); + assert(cancelled_task != NULL); + + test_task_manager_install_callback( + task_manager, + &changed_count, + &destroy_count + ); + + assert( + task_manager_add( + task_manager, + pending_task, + &error + ) + ); + + assert( + task_manager_add( + task_manager, + completed_task, + &error + ) + ); + + assert( + task_manager_add( + task_manager, + failed_task, + &error + ) + ); + + assert( + task_manager_add( + task_manager, + cancelled_task, + &error + ) + ); + + assert(error == NULL); + assert(changed_count == 4); + + assert( + background_task_start( + completed_task, + test_task_manager_success_worker, + NULL, + NULL, + NULL, + test_task_manager_group_completed, + &group, + NULL, + &error + ) + ); + + assert( + background_task_start( + failed_task, + test_task_manager_failure_worker, + NULL, + NULL, + NULL, + test_task_manager_group_completed, + &group, + NULL, + &error + ) + ); + + assert( + background_task_start( + cancelled_task, + test_task_manager_wait_worker, + NULL, + NULL, + NULL, + test_task_manager_group_completed, + &group, + NULL, + &error + ) + ); + + assert(error == NULL); + + background_task_cancel( + cancelled_task + ); + + timeout_source_id = g_timeout_add_seconds( + TEST_TASK_MANAGER_TIMEOUT_SECONDS, + test_task_manager_timeout, + main_loop + ); + + assert(timeout_source_id != 0); + + g_main_loop_run( + main_loop + ); + + assert(group.remaining_tasks == 0); + + assert( + g_source_remove( + timeout_source_id + ) + ); + + assert( + background_task_get_state(completed_task) == + BACKGROUND_TASK_STATE_COMPLETED + ); + + assert( + background_task_get_state(failed_task) == + BACKGROUND_TASK_STATE_FAILED + ); + + assert( + background_task_get_state(cancelled_task) == + BACKGROUND_TASK_STATE_CANCELLED + ); + + assert( + background_task_get_state(pending_task) == + BACKGROUND_TASK_STATE_PENDING + ); + + assert( + task_manager_remove_finished( + task_manager + ) == 3 + ); + + assert( + task_manager_get_count( + task_manager + ) == 1 + ); + + /* + * Une seule notification est produite pour le nettoyage groupé. + */ + assert(changed_count == 5); + + remaining_task = task_manager_get_task( + task_manager, + 0 + ); + + assert(remaining_task == pending_task); + + background_task_unref( + remaining_task + ); + + task_manager_free( + task_manager + ); + + assert(destroy_count == 1); + + background_task_unref( + pending_task + ); + + background_task_unref( + completed_task + ); + + background_task_unref( + failed_task + ); + + background_task_unref( + cancelled_task + ); + + g_main_loop_unref( + main_loop + ); +} + +/** + * @brief Vérifie l'annulation de toutes les tâches actives. + */ +static void test_task_manager_cancel_all_tasks(void) +{ + TaskManager *task_manager = NULL; + + BackgroundTask *first_task = NULL; + BackgroundTask *second_task = NULL; + BackgroundTask *pending_task = NULL; + + TestTaskManagerAsyncGroup group = {0}; + + GMainLoop *main_loop = NULL; + GError *error = NULL; + + guint timeout_source_id = 0; + guint changed_count = 0; + guint destroy_count = 0; + + main_loop = g_main_loop_new( + NULL, + FALSE + ); + + assert(main_loop != NULL); + + group.main_loop = main_loop; + group.remaining_tasks = 2; + + task_manager = task_manager_new(); + + first_task = background_task_new( + "Première tâche active" + ); + + second_task = background_task_new( + "Seconde tâche active" + ); + + pending_task = background_task_new( + "Tâche en attente" + ); + + assert(task_manager != NULL); + assert(first_task != NULL); + assert(second_task != NULL); + assert(pending_task != NULL); + + test_task_manager_install_callback( + task_manager, + &changed_count, + &destroy_count + ); + + assert( + task_manager_add( + task_manager, + first_task, + &error + ) + ); + + assert( + task_manager_add( + task_manager, + second_task, + &error + ) + ); + + assert( + task_manager_add( + task_manager, + pending_task, + &error + ) + ); + + assert(error == NULL); + assert(changed_count == 3); + + assert( + background_task_start( + first_task, + test_task_manager_wait_worker, + NULL, + NULL, + NULL, + test_task_manager_group_completed, + &group, + NULL, + &error + ) + ); + + assert( + background_task_start( + second_task, + test_task_manager_wait_worker, + NULL, + NULL, + NULL, + test_task_manager_group_completed, + &group, + NULL, + &error + ) + ); + + assert(error == NULL); + + task_manager_cancel_all( + task_manager + ); + + /* + * Une notification globale supplémentaire signale la demande + * d'annulation. + */ + assert(changed_count == 4); + + timeout_source_id = g_timeout_add_seconds( + TEST_TASK_MANAGER_TIMEOUT_SECONDS, + test_task_manager_timeout, + main_loop + ); + + assert(timeout_source_id != 0); + + g_main_loop_run( + main_loop + ); + + assert(group.remaining_tasks == 0); + + assert( + g_source_remove( + timeout_source_id + ) + ); + + assert( + background_task_get_state(first_task) == + BACKGROUND_TASK_STATE_CANCELLED + ); + + assert( + background_task_get_state(second_task) == + BACKGROUND_TASK_STATE_CANCELLED + ); + + assert( + background_task_get_state(pending_task) == + BACKGROUND_TASK_STATE_PENDING + ); + + assert( + !background_task_is_cancelled( + pending_task + ) + ); + + task_manager_free( + task_manager + ); + + assert(destroy_count == 1); + + background_task_unref( + first_task + ); + + background_task_unref( + second_task + ); + + background_task_unref( + pending_task + ); + + g_main_loop_unref( + main_loop + ); +} + +/** + * @brief Vérifie que le remplacement du callback détruit les anciennes + * données exactement une fois. + */ +static void test_task_manager_replace_callback(void) +{ + TaskManager *task_manager = NULL; + BackgroundTask *task = NULL; + + GError *error = NULL; + + guint first_changed_count = 0; + guint first_destroy_count = 0; + + guint second_changed_count = 0; + guint second_destroy_count = 0; + + task_manager = task_manager_new(); + + assert(task_manager != NULL); + + test_task_manager_install_callback( + task_manager, + &first_changed_count, + &first_destroy_count + ); + + assert(first_destroy_count == 0); + + test_task_manager_install_callback( + task_manager, + &second_changed_count, + &second_destroy_count + ); + + assert(first_destroy_count == 1); + assert(second_destroy_count == 0); + + task = background_task_new( + "Notification du second callback" + ); + + assert(task != NULL); + + assert( + task_manager_add( + task_manager, + task, + &error + ) + ); + + assert(error == NULL); + + assert(first_changed_count == 0); + assert(second_changed_count == 1); + + task_manager_set_changed_callback( + task_manager, + NULL, + NULL, + NULL + ); + + assert(second_destroy_count == 1); + + assert( + task_manager_remove( + task_manager, + task + ) + ); + + /* + * Le callback étant désactivé, le compteur reste inchangé. + */ + assert(second_changed_count == 1); + + task_manager_free( + task_manager + ); + + assert(first_destroy_count == 1); + assert(second_destroy_count == 1); + + background_task_unref( + task + ); +} + +int main(void) +{ + test_task_manager_creation(); + test_task_manager_add(); + test_task_manager_duplicate(); + test_task_manager_remove_task(); + test_task_manager_remove_finished_tasks(); + test_task_manager_cancel_all_tasks(); + test_task_manager_replace_callback(); + + printf( + "TaskManager : tous les tests sont valides.\n" + ); + + return 0; +} diff --git a/tests/test_transaction b/tests/test_transaction deleted file mode 100755 index 3533f19..0000000 Binary files a/tests/test_transaction and /dev/null differ