feat: add task manager and activity panel

This commit is contained in:
grayTerminal-sh 2026-07-17 14:35:21 +02:00
parent 2a8ed10034
commit 1c391335c6
23 changed files with 4569 additions and 25 deletions

View file

@ -0,0 +1,790 @@
# Ticket #035 — File de tâches et panneau dactivité
## 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 lannulation ;
- 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 linterface.
## 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 quun 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 dune référence supplémentaire ;
- lappelant 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 lappelant doit libérer.
### Retirer une tâche
```c
gboolean task_manager_remove(
TaskManager *task_manager,
BackgroundTask *task
);
```
La tâche nest 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 lappel.
Remplacer le callback existant doit détruire les anciennes données exactement une fois.
---
## 4. Domaine derreur
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 laffichage 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 dobservation 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 dannulation.
### 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 derreur sous forme courte.
Une tâche en cours doit afficher son message de progression lorsquil existe.
---
## 10. Bouton dannulation
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 lapplication.
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.
Lordre 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 linterface, ajouter un bouton temporaire :
```text
Tâche de test
```
Il lance une `BackgroundTask` qui :
- dure environ deux secondes ;
- progresse de 0 à 100 % ;
- accepte lannulation ;
- 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 ;
- dhistorique permanent ;
- dexécution de commandes ;
- de recherche DNS ;
- dExifTool ;
- de notifications système ;
- de tri avancé ;
- de pagination.
---
# Critères dacceptation
- [ ] `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.
- [ ] Lordre 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 lindépendance du manager :
```bash
rg -n \
'#include <gtk|sqlite3_|Database|Investigation' \
include/core/task_manager.h \
src/core/task_manager.c
```
Résultat attendu :
```text
aucune sortie
```
Vérifier les références :
```bash
rg -n \
'background_task_ref|background_task_unref' \
src/core/task_manager.c
```
Vérifier le rafraîchissement GTK :
```bash
rg -n \
'g_timeout_add|g_source_remove|task_panel_refresh' \
src/widgets/task_panel.c
```
Vérifier labsence de threads bruts :
```bash
rg -n \
'pthread_|pthread.h' \
include/core/task_manager.h \
src/core/task_manager.c \
src/widgets/task_panel.c
```
Résultat attendu :
```text
aucune sortie
```
---
# Fichiers concernés
```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
include/views/main_window.h
src/views/main_window.c
src/core/application.c
Makefile
```
---
# Commit attendu
```bash
make clean
make
make test
git diff --check
git status --short
```
```bash
git add \
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 \
include/views/main_window.h \
src/views/main_window.c \
src/core/application.c \
Makefile
```
```bash
git diff --cached --stat
git diff --cached
```
```bash
git commit -m "feat(ui): add task manager and activity panel"
git push
```
---
# Résultat attendu
Après ce ticket, Labfy possédera une infrastructure visible pour tous les futurs traitements longs :
```text
Utilisateur
Action GTK
BackgroundTask
TaskManager
TaskPanel
```
Le ticket suivant pourra ajouter le registre des dépendances et lancer les premiers contrôles doutils externes sans bloquer linterface.

198
include/core/task_manager.h Normal file
View file

@ -0,0 +1,198 @@
/******************************************************************************
* @file task_manager.h
* @brief Gestionnaire central des tâches exécutées en arrière-plan.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_TASK_MANAGER_H
#define LABFY_INVESTIGATION_TASK_MANAGER_H
#include "core/background_task.h"
#include <glib.h>
/**
* @brief Représentation opaque du gestionnaire de tâches.
*/
typedef struct TaskManager TaskManager;
/**
* @brief Codes derreur du gestionnaire de tâches.
*/
typedef enum
{
TASK_MANAGER_ERROR_INVALID_ARGUMENT,
TASK_MANAGER_ERROR_ALREADY_ADDED
} TaskManagerError;
/**
* @brief Domaine derreur 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é lorsquune tâche est ajoutée, retirée,
* nettoyée ou lorsquune demande globale dannulation 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 derreur du module.
*
* @return Quark GLib du domaine derreur.
*/
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 dexister si dautres 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.
* Lappelant 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.
*
* Lappelant 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 lindex est invalide.
*/
BackgroundTask *task_manager_get_task(
const TaskManager *task_manager,
gsize index
);
/**
* @brief Retire une tâche du gestionnaire.
*
* La tâche nest 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 é 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 lannulation de toutes les tâches en cours.
*
* Lannulation 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.
*
* Lorsquun nouveau callback remplace lancien, 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

View file

@ -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 <gtk/gtk.h>
@ -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.
*

View file

@ -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 <gtk/gtk.h>
/**
* @brief Représentation opaque du panneau dactivité.
*/
typedef struct TaskPanel TaskPanel;
/**
* @brief Crée un panneau affichant les tâches dun 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 laffichage à 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é nest pas libéré.
*
* @param task_panel Panneau à libérer.
*/
void task_panel_free(
TaskPanel *task_panel
);
#endif

Binary file not shown.

View file

@ -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 <gtk/gtk.h>
@ -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
);
}

736
src/core/task_manager.c Normal file
View file

@ -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
);
}

View file

@ -7,6 +7,7 @@
#include "widgets/sidebar.h"
#include "widgets/workspace.h"
#include "widgets/investigation_tree_view.h"
#include "widgets/task_panel.h"
#include <glib.h>
@ -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);
}

1105
src/widgets/task_panel.c Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
tests/test_task_manager Executable file

Binary file not shown.

1116
tests/test_task_manager.c Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.