From 2ffbdec8888270f384def37d0037ac5972f78c65 Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Fri, 17 Jul 2026 16:47:46 +0200 Subject: [PATCH] feat: run external tools in background tasks --- Makefile | 15 +- docs/tickets/open/TICKET-038.md | 1083 ++++++++++++++ include/core/tool_task.h | 299 ++++ labfy-investigation | Bin 275472 -> 289536 bytes src/core/tool_task.c | 981 ++++++++++++ tests/test_tool_task | Bin 0 -> 75680 bytes tests/test_tool_task.c | 2461 +++++++++++++++++++++++++++++++ 7 files changed, 4837 insertions(+), 2 deletions(-) create mode 100644 docs/tickets/open/TICKET-038.md create mode 100644 include/core/tool_task.h create mode 100644 src/core/tool_task.c create mode 100755 tests/test_tool_task create mode 100644 tests/test_tool_task.c diff --git a/Makefile b/Makefile index 6c48d4e..88c5ded 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ TEST_BACKGROUND_TASK := tests/test_background_task TEST_TASK_MANAGER := tests/test_task_manager TEST_TOOL_REGISTRY := tests/test_tool_registry TEST_TOOL_PROCESS := tests/test_tool_process +TEST_TOOL_TASK := tests/test_tool_task all: $(TARGET) @@ -162,6 +163,13 @@ $(TEST_TOOL_PROCESS): \ src/core/tool_process.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) +$(TEST_TOOL_TASK): \ + tests/test_tool_task.c \ + src/core/tool_task.c \ + src/core/tool_registry.c \ + src/core/tool_process.c \ + src/core/background_task.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) test: \ $(TEST_NODE) \ @@ -178,7 +186,8 @@ test: \ $(TEST_BACKGROUND_TASK) \ $(TEST_TASK_MANAGER) \ $(TEST_TOOL_REGISTRY) \ - $(TEST_TOOL_PROCESS) + $(TEST_TOOL_PROCESS) \ + $(TEST_TOOL_TASK) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -195,6 +204,7 @@ test: \ @$(TEST_TASK_MANAGER) @$(TEST_TOOL_REGISTRY) @$(TEST_TOOL_PROCESS) + @$(TEST_TOOL_TASK) @echo "Tous les tests sont valides." %.o: %.c @@ -219,6 +229,7 @@ clean: $(TEST_BACKGROUND_TASK) \ $(TEST_TASK_MANAGER) \ $(TEST_TOOL_REGISTRY) \ - $(TEST_TOOL_PROCESS) + $(TEST_TOOL_PROCESS) \ + $(TEST_TOOL_TASK) .PHONY: clean run test diff --git a/docs/tickets/open/TICKET-038.md b/docs/tickets/open/TICKET-038.md new file mode 100644 index 0000000..ef5c1a0 --- /dev/null +++ b/docs/tickets/open/TICKET-038.md @@ -0,0 +1,1083 @@ +# TICKET-038 — Exécution d’un outil externe dans une BackgroundTask + +## Statut + +À faire + +## Priorité + +Haute + +## Objectif + +Créer une couche d’intégration permettant d’exécuter un outil externe dans une `BackgroundTask`, puis de suivre cette exécution avec le `TaskManager` et le panneau d’activité. + +Ce ticket doit relier proprement les modules déjà validés : + +```text +ToolRegistry +ToolProcess +BackgroundTask +TaskManager +TaskPanel +``` + +Le résultat attendu est une abstraction réutilisable capable de : + +- sélectionner un outil enregistré ; +- vérifier sa disponibilité ; +- préparer une exécution structurée ; +- lancer cette exécution dans un thread secondaire ; +- transmettre l’annulation ; +- publier un statut compréhensible ; +- conserver le résultat brut de l’outil ; +- signaler proprement les erreurs ; +- rester indépendante de GTK. + +--- + +## Contexte + +Les tickets précédents ont fourni : + +### Ticket #034 + +```text +BackgroundTask +``` + +pour exécuter un travail asynchrone, publier une progression et gérer l’annulation. + +### Ticket #035 + +```text +TaskManager +TaskPanel +``` + +pour conserver, afficher et annuler les tâches. + +### Ticket #036 + +```text +ToolRegistry +ToolInfo +``` + +pour détecter les exécutables disponibles sur la machine. + +### Ticket #037 + +```text +ToolProcess +ToolProcessResult +``` + +pour lancer un exécutable sans shell, capturer ses sorties et gérer son annulation. + +Il manque maintenant une couche métier qui assemble ces briques sans obliger `Application`, les futurs adaptateurs OSINT ou les widgets GTK à connaître tous les détails de leur fonctionnement interne. + +--- + +## Nom proposé du module + +```text +ToolTask +``` + +Fichiers attendus : + +```text +include/core/tool_task.h +src/core/tool_task.c +tests/test_tool_task.c +``` + +Le nom peut être ajusté avant implémentation s’il existe une meilleure proposition cohérente avec le projet. + +--- + +## Responsabilité du module + +`ToolTask` doit représenter une exécution externe préparée pour être lancée dans une `BackgroundTask`. + +Le module devra : + +1. recevoir un `ToolRegistry` ; +2. rechercher un outil par identifiant ; +3. vérifier que l’outil est disponible ; +4. copier les arguments reçus ; +5. copier le dossier de travail facultatif ; +6. créer une `BackgroundTask` ; +7. utiliser `ToolProcess` dans son worker ; +8. transmettre le `GCancellable` fourni par `BackgroundTask` ; +9. publier un statut avant le lancement ; +10. publier un statut après la fin ; +11. transférer un résultat structuré au propriétaire de la tâche ; +12. convertir les erreurs de préparation en erreurs `ToolTask` ; +13. laisser les erreurs d’exécution de l’outil disponibles dans le résultat ou dans la tâche selon leur nature. + +--- + +## Hors périmètre + +Ce ticket ne doit pas encore : + +- ajouter un outil OSINT réel à l’interface ; +- créer un adaptateur DNS ; +- analyser la sortie d’un outil ; +- enregistrer une preuve ; +- écrire les sorties dans SQLite ; +- sauvegarder les résultats bruts sur disque ; +- afficher directement les sorties dans GTK ; +- gérer une limite de temps ; +- gérer stdin ; +- relancer automatiquement une commande ; +- gérer un pipeline entre plusieurs outils ; +- déterminer automatiquement les arguments d’un outil ; +- modifier le `ToolRegistry`. + +Le premier adaptateur OSINT concret viendra après ce ticket. + +--- + +## Contraintes générales + +- C17. +- GLib et GIO. +- Aucune dépendance GTK. +- Compilation stricte : + +```text +-Wall -Wextra -Wpedantic -Werror +``` + +- Aucun shell. +- Aucune concaténation de commande. +- Les arguments doivent rester séparés. +- Les structures publiques doivent être opaques. +- Les fonctions doivent être préfixées par : + +```text +tool_task_ +tool_task_result_ +``` + +- Aucun état global mutable. +- Les règles de propriété doivent être documentées. +- Le module doit être testable sans dépendre d’un outil réellement installé. + +--- + +## Modèle public + +### ToolTaskResult + +Créer une structure opaque : + +```c +typedef struct ToolTaskResult ToolTaskResult; +``` + +Cette structure doit conserver au minimum : + +```text +tool_identifier +executable_path +arguments +working_directory +process_result +``` + +Le champ `process_result` doit contenir le `ToolProcessResult` produit par `ToolProcess`. + +Le résultat doit être indépendant du `ToolRegistry` après sa création. + +### ToolTask + +Deux approches sont possibles. + +#### Approche recommandée + +Ne pas créer une structure persistante `ToolTask`. + +Créer directement une `BackgroundTask` configurée à partir d’une requête. + +Exemple : + +```c +BackgroundTask *tool_task_create( + const ToolRegistry *tool_registry, + const char *tool_identifier, + const char *task_title, + const char *const arguments[], + const char *working_directory, + GError **error +); +``` + +Le `BackgroundTask` retourné n’est pas encore démarré. + +Une seconde fonction démarre la tâche : + +```c +gboolean tool_task_start( + BackgroundTask *background_task, + BackgroundTaskCompletedCallback completed_callback, + gpointer completed_user_data, + GDestroyNotify completed_user_data_destroy, + GError **error +); +``` + +#### Approche alternative + +Créer une structure opaque `ToolTask` possédant sa `BackgroundTask`. + +Cette approche n’est acceptable que si elle simplifie clairement la propriété et les tests. + +--- + +## Domaine d’erreur + +Créer : + +```c +#define TOOL_TASK_ERROR \ + tool_task_error_quark() +``` + +Énumération minimale : + +```c +typedef enum +{ + TOOL_TASK_ERROR_INVALID_ARGUMENT, + TOOL_TASK_ERROR_TOOL_NOT_FOUND, + TOOL_TASK_ERROR_TOOL_NOT_CHECKED, + TOOL_TASK_ERROR_TOOL_MISSING, + TOOL_TASK_ERROR_TASK_CREATION, + TOOL_TASK_ERROR_TASK_START, + TOOL_TASK_ERROR_PROCESS +} ToolTaskError; +``` + +Fonction : + +```c +GQuark tool_task_error_quark(void); +``` + +--- + +## Règles de préparation + +### Outil inconnu + +Si l’identifiant n’existe pas dans le registre : + +```text +retour : NULL +erreur : TOOL_TASK_ERROR_TOOL_NOT_FOUND +``` + +### Outil non vérifié + +Si l’état vaut : + +```text +TOOL_AVAILABILITY_UNKNOWN +``` + +la création doit échouer avec : + +```text +TOOL_TASK_ERROR_TOOL_NOT_CHECKED +``` + +Le module ne doit pas appeler automatiquement `tool_registry_refresh()`. + +Cette décision garde les responsabilités séparées. + +### Outil absent + +Si l’état vaut : + +```text +TOOL_AVAILABILITY_MISSING +``` + +la création doit échouer avec : + +```text +TOOL_TASK_ERROR_TOOL_MISSING +``` + +### Outil disponible sans chemin + +Un outil marqué disponible mais sans chemin résolu représente un état incohérent. + +La création doit échouer. + +### Arguments + +Le tableau d’arguments : + +```c +const char *const arguments[] +``` + +peut être `NULL`. + +Lorsqu’il est fourni : + +- chaque chaîne doit être non `NULL` jusqu’au terminateur ; +- les chaînes doivent être dupliquées ; +- leur ordre doit être conservé ; +- aucune interprétation ne doit être faite ; +- le tableau interne doit se terminer par `NULL`. + +### Titre + +Le titre de la tâche doit être non vide. + +Exemple : + +```text +Interrogation DNS de example.org +``` + +Le titre ne doit pas être fabriqué automatiquement par le module. + +--- + +## Cycle de vie recommandé + +### Création + +```c +BackgroundTask *tool_task_create( + const ToolRegistry *tool_registry, + const char *tool_identifier, + const char *task_title, + const char *const arguments[], + const char *working_directory, + GError **error +); +``` + +Comportement : + +1. valider les arguments ; +2. rechercher `ToolInfo` ; +3. vérifier l’état ; +4. récupérer le chemin résolu ; +5. dupliquer : + - identifiant ; + - chemin ; + - arguments ; + - dossier de travail ; +6. créer les données du worker ; +7. créer une `BackgroundTask` ; +8. attacher les données au futur lancement. + +### Démarrage + +```c +gboolean tool_task_start( + BackgroundTask *background_task, + BackgroundTaskCompletedCallback completed_callback, + gpointer completed_user_data, + GDestroyNotify completed_user_data_destroy, + GError **error +); +``` + +Cette fonction doit : + +1. vérifier que la tâche provient bien de `ToolTask` ; +2. appeler `background_task_start()` ; +3. transmettre : + - le worker ; + - les données du worker ; + - leur destructeur ; + - le destructeur du résultat ; + - le callback de fin ; + - ses données utilisateur. + +La fonction ne doit pas ajouter elle-même la tâche dans un `TaskManager`. + +Cette responsabilité reste à l’appelant. + +--- + +## Worker + +Le worker doit respecter la signature actuelle de `BackgroundTask`. + +Comportement attendu : + +1. publier : + +```text +Préparation de l’exécution +``` + +2. publier : + +```text +Exécution de +``` + +3. appeler `tool_process_run()` avec : + - le chemin détecté ; + - les arguments copiés ; + - le dossier de travail ; + - le `GCancellable` reçu ; +4. si l’appel échoue : + - propager une erreur exploitable ; + - ne produire aucun résultat ; +5. si l’appel réussit : + - créer `ToolTaskResult` ; + - y transférer `ToolProcessResult` ; + - publier un statut final ; +6. placer le résultat dans : + +```c +*result +``` + +### Progression + +Une commande externe ne fournit pas toujours une progression mesurable. + +Pour ce ticket, utiliser une progression qualitative : + +```text +0.0 Préparation +0.1 Lancement +0.9 Exécution terminée, traitement du résultat +1.0 Terminé +``` + +Il ne faut pas simuler une progression régulière avec un minuteur. + +--- + +## Code de sortie non nul + +Un programme lancé correctement mais terminant avec un code non nul ne doit pas faire échouer la `BackgroundTask`. + +Dans ce cas : + +```text +BackgroundTask : COMPLETED +ToolTaskResult : disponible +ToolProcessResult : is_success == FALSE +``` + +Le résultat doit conserver : + +- le code de sortie ; +- stdout ; +- stderr. + +Cette distinction est essentielle pour les futurs adaptateurs. + +Exemple : + +```text +dig retourne 9 +``` + +La tâche technique est terminée, mais le résultat fonctionnel signale un échec. + +--- + +## Annulation + +Le `GCancellable` reçu par le worker doit être transmis directement à : + +```c +tool_process_run() +``` + +Si `ToolProcess` retourne : + +```text +TOOL_PROCESS_ERROR_CANCELLED +``` + +le worker doit permettre à `BackgroundTask` d’aboutir à l’état : + +```text +BACKGROUND_TASK_STATE_CANCELLED +``` + +Il ne doit pas transformer l’annulation en erreur métier générique. + +Aucun processus enfant ne doit rester actif. + +--- + +## Résultat + +### Cycle de vie + +```c +void tool_task_result_free( + ToolTaskResult *result +); +``` + +La fonction doit accepter `NULL`. + +### Identifiant + +```c +const char *tool_task_result_get_tool_identifier( + const ToolTaskResult *result +); +``` + +Chaîne empruntée. + +### Chemin de l’exécutable + +```c +const char *tool_task_result_get_executable_path( + const ToolTaskResult *result +); +``` + +Chaîne empruntée. + +### Arguments + +```c +gsize tool_task_result_get_argument_count( + const ToolTaskResult *result +); +``` + +```c +const char *tool_task_result_get_argument( + const ToolTaskResult *result, + gsize index +); +``` + +Chaînes empruntées. + +### Dossier de travail + +```c +const char *tool_task_result_get_working_directory( + const ToolTaskResult *result +); +``` + +Retourne `NULL` si aucun dossier n’a été défini. + +### Résultat du processus + +```c +const ToolProcessResult *tool_task_result_get_process_result( + const ToolTaskResult *result +); +``` + +Pointeur emprunté. + +Une fonction de transfert ou de référence n’est pas nécessaire pour ce ticket. + +--- + +## Propriété des données + +### Données de préparation + +Le module doit copier toutes les données nécessaires avant le démarrage. + +Il ne doit pas dépendre de la durée de vie : + +- du `ToolRegistry` ; +- du `ToolInfo` ; +- du tableau d’arguments d’origine ; +- du titre d’origine ; +- du dossier de travail d’origine. + +### Résultat + +`ToolTaskResult` devient propriétaire de : + +- l’identifiant copié ; +- le chemin copié ; +- la copie des arguments ; +- la copie du dossier de travail ; +- `ToolProcessResult`. + +Le destructeur doit tout libérer. + +### BackgroundTask + +La propriété de la `BackgroundTask` reste conforme au ticket #034. + +--- + +## API publique suggérée + +```c +typedef struct ToolTaskResult ToolTaskResult; + +typedef enum +{ + TOOL_TASK_ERROR_INVALID_ARGUMENT, + TOOL_TASK_ERROR_TOOL_NOT_FOUND, + TOOL_TASK_ERROR_TOOL_NOT_CHECKED, + TOOL_TASK_ERROR_TOOL_MISSING, + TOOL_TASK_ERROR_TASK_CREATION, + TOOL_TASK_ERROR_TASK_START, + TOOL_TASK_ERROR_PROCESS +} ToolTaskError; + +#define TOOL_TASK_ERROR \ + tool_task_error_quark() + +GQuark tool_task_error_quark(void); + +BackgroundTask *tool_task_create( + const ToolRegistry *tool_registry, + const char *tool_identifier, + const char *task_title, + const char *const arguments[], + const char *working_directory, + GError **error +); + +gboolean tool_task_start( + BackgroundTask *background_task, + BackgroundTaskCompletedCallback completed_callback, + gpointer completed_user_data, + GDestroyNotify completed_user_data_destroy, + GError **error +); + +void tool_task_result_free( + ToolTaskResult *result +); + +const char *tool_task_result_get_tool_identifier( + const ToolTaskResult *result +); + +const char *tool_task_result_get_executable_path( + const ToolTaskResult *result +); + +gsize tool_task_result_get_argument_count( + const ToolTaskResult *result +); + +const char *tool_task_result_get_argument( + const ToolTaskResult *result, + gsize index +); + +const char *tool_task_result_get_working_directory( + const ToolTaskResult *result +); + +const ToolProcessResult *tool_task_result_get_process_result( + const ToolTaskResult *result +); +``` + +L’API peut évoluer si l’implémentation de `BackgroundTask` rend une autre forme plus sûre. + +--- + +## Marquage d’une BackgroundTask + +`tool_task_start()` doit pouvoir vérifier que la tâche reçue a été créée par `tool_task_create()`. + +Approches possibles : + +### Structure privée associée + +Conserver les données du worker dans une structure privée connue uniquement de `ToolTask`. + +### Extension de BackgroundTask + +Ajouter un pointeur de contexte privé à `BackgroundTask` uniquement si cela reste générique et justifié. + +### Wrapper opaque + +Créer un `ToolTask` opaque contenant la `BackgroundTask`. + +Cette solution peut être préférable si la vérification devient fragile. + +Le choix final doit privilégier : + +- sécurité de propriété ; +- lisibilité ; +- testabilité ; +- absence de cast dangereux. + +--- + +## Tests unitaires obligatoires + +Les tests doivent créer un faux outil temporaire. + +Ils ne doivent pas dépendre d’un outil réel. + +--- + +### 1. Arguments invalides + +Tester au minimum : + +- registre `NULL` ; +- identifiant `NULL` ; +- identifiant vide ; +- titre `NULL` ; +- titre vide ; +- dossier de travail vide ; +- `GError` déjà initialisé si la convention est vérifiée. + +Résultat attendu : + +```text +TOOL_TASK_ERROR_INVALID_ARGUMENT +``` + +--- + +### 2. Outil inconnu + +Registre valide mais identifiant absent. + +Résultat : + +```text +TOOL_TASK_ERROR_TOOL_NOT_FOUND +``` + +--- + +### 3. Outil non vérifié + +Enregistrer un outil sans appeler `tool_registry_refresh()`. + +Résultat : + +```text +TOOL_TASK_ERROR_TOOL_NOT_CHECKED +``` + +--- + +### 4. Outil absent + +Enregistrer un faux outil absent, puis rafraîchir. + +Résultat : + +```text +TOOL_TASK_ERROR_TOOL_MISSING +``` + +--- + +### 5. Création valide + +Créer un faux outil dans un `PATH` temporaire. + +Vérifier : + +- registre rafraîchi ; +- création de la `BackgroundTask` ; +- titre correct ; +- état initial `PENDING`. + +--- + +### 6. Copie des arguments + +Créer les arguments avec des chaînes dynamiques. + +Créer la tâche, puis libérer les chaînes originales. + +Démarrer la tâche. + +Le résultat doit toujours contenir les bonnes valeurs. + +--- + +### 7. Exécution réussie + +Le faux outil écrit dans stdout et retourne zéro. + +Vérifier : + +- tâche terminée ; +- état `COMPLETED` ; +- résultat non `NULL` ; +- identifiant correct ; +- chemin correct ; +- arguments corrects ; +- `ToolProcessResult` disponible ; +- `is_success == TRUE`. + +--- + +### 8. Code de sortie non nul + +Le faux outil retourne : + +```text +exit 7 +``` + +Vérifier : + +- tâche `COMPLETED` ; +- résultat disponible ; +- code `7` ; +- succès fonctionnel `FALSE`. + +--- + +### 9. Erreur de lancement + +Créer la tâche avec un exécutable détecté, puis supprimer le fichier avant le démarrage. + +Vérifier : + +- tâche `FAILED` ; +- erreur exploitable ; +- aucun résultat. + +--- + +### 10. Annulation + +Créer un faux outil long. + +Démarrer la tâche puis l’annuler. + +Vérifier : + +- état final `CANCELLED` ; +- aucun résultat ; +- aucun processus enfant actif. + +--- + +### 11. Dossier de travail + +Le faux outil affiche son dossier courant. + +Vérifier que le résultat correspond au dossier demandé. + +--- + +### 12. Plusieurs tâches simultanées + +Créer deux tâches à partir du même outil avec des arguments différents. + +Les lancer simultanément. + +Vérifier : + +- deux résultats indépendants ; +- aucun mélange des arguments ; +- aucun partage incorrect des sorties ; +- aucun état global mutable. + +--- + +### 13. Destruction avant démarrage + +Créer une tâche puis la libérer sans la lancer. + +Vérifier : + +- aucune fuite ; +- destruction des copies d’arguments ; +- destruction des données privées. + +--- + +### 14. Destruction après exécution + +Exécuter une tâche, lire le résultat, puis libérer la tâche. + +Vérifier la libération complète. + +--- + +## Noms de tests suggérés + +```text +/tool_task/invalid_arguments +/tool_task/tool_not_found +/tool_task/tool_not_checked +/tool_task/tool_missing +/tool_task/create +/tool_task/argument_ownership +/tool_task/success +/tool_task/nonzero_exit +/tool_task/spawn_failure +/tool_task/cancellation +/tool_task/working_directory +/tool_task/concurrent_tasks +/tool_task/free_before_start +/tool_task/free_after_completion +``` + +--- + +## Synchronisation des tests + +Les tests doivent utiliser une `GMainLoop` pour attendre la fin des `BackgroundTask`. + +Ils ne doivent pas utiliser une attente active infinie. + +Prévoir un timeout de sécurité afin qu’un test défectueux ne bloque pas toute la suite. + +Exemple : + +```text +5 secondes maximum pour un test normal +``` + +Le test d’annulation doit se terminer rapidement. + +--- + +## Makefile + +Ajouter : + +```make +TEST_TOOL_TASK := tests/test_tool_task +``` + +Règle attendue : + +```make +$(TEST_TOOL_TASK): \ + tests/test_tool_task.c \ + src/core/tool_task.c \ + src/core/tool_registry.c \ + src/core/tool_process.c \ + src/core/background_task.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) +``` + +Ajouter la cible à : + +```text +make test +make clean +``` + +--- + +## Vérifications manuelles + +```bash +make clean +make +make tests/test_tool_task +./tests/test_tool_task +make test +``` + +--- + +## Vérification mémoire + +```bash +G_DEBUG=gc-friendly \ +G_SLICE=always-malloc \ +valgrind \ + --leak-check=full \ + --show-leak-kinds=all \ + ./tests/test_tool_task +``` + +Surveiller particulièrement : + +- création sans démarrage ; +- échec de démarrage ; +- annulation ; +- résultat avec code non nul ; +- tâches simultanées. + +--- + +## Critères d’acceptation + +Le ticket est validé lorsque : + +- le module compile en C17 strict ; +- aucune dépendance GTK n’est ajoutée ; +- un outil est recherché par identifiant dans `ToolRegistry` ; +- les états `UNKNOWN` et `MISSING` sont correctement refusés ; +- le chemin résolu est copié ; +- les arguments sont copiés ; +- le dossier de travail est copié ; +- la tâche utilise `ToolProcess` dans un thread secondaire ; +- le `GCancellable` est transmis ; +- l’annulation aboutit à `CANCELLED` ; +- un code de sortie non nul produit une tâche `COMPLETED` ; +- le résultat brut est accessible ; +- les résultats sont indépendants du registre après création ; +- plusieurs tâches peuvent fonctionner simultanément ; +- aucun shell n’est utilisé ; +- aucun processus enfant n’est abandonné ; +- aucune fuite mémoire n’est détectée ; +- tous les tests passent ; +- le test est intégré au `Makefile`. + +--- + +## Démonstration finale prévue + +Après validation du module, remplacer temporairement le bouton actuel : + +```text +Tâche de test +``` + +par une vraie démonstration basée sur un faux outil ou un outil stable détecté. + +Cette démonstration devra passer par : + +```text +ToolRegistry +→ ToolTask +→ BackgroundTask +→ TaskManager +→ TaskPanel +``` + +Le bouton temporaire sera supprimé dès que le premier adaptateur OSINT réel sera disponible. + +--- + +## Suite prévue + +Après ce ticket : + +1. ticket #039 — catalogue initial des outils externes ; +2. interrogation de leurs versions ; +3. ticket d’adaptateur DNS ; +4. conservation structurée des exécutions ; +5. stockage des sorties brutes ; +6. création d’observations et de preuves ; +7. affichage dans le workspace ; +8. packaging Ubuntu avec dépendances optionnelles. diff --git a/include/core/tool_task.h b/include/core/tool_task.h new file mode 100644 index 0000000..cdc3e7b --- /dev/null +++ b/include/core/tool_task.h @@ -0,0 +1,299 @@ +/****************************************************************************** + * @file tool_task.h + * @brief Exécution d'un outil externe dans une tâche d'arrière-plan. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_TOOL_TASK_H +#define LABFY_INVESTIGATION_TOOL_TASK_H + +#include "core/background_task.h" +#include "core/tool_process.h" +#include "core/tool_registry.h" + +#include +#include + +G_BEGIN_DECLS + +/** + * @brief Erreurs produites par ToolTask. + */ +typedef enum +{ + /** + * Un argument transmis au module est invalide. + */ + TOOL_TASK_ERROR_INVALID_ARGUMENT, + + /** + * L'identifiant demandé n'existe pas dans le registre. + */ + TOOL_TASK_ERROR_TOOL_NOT_FOUND, + + /** + * La disponibilité de l'outil n'a pas encore été vérifiée. + */ + TOOL_TASK_ERROR_TOOL_NOT_CHECKED, + + /** + * L'outil a été vérifié mais son exécutable est absent. + */ + TOOL_TASK_ERROR_TOOL_MISSING, + + /** + * L'outil est disponible, mais son chemin résolu est invalide. + */ + TOOL_TASK_ERROR_INVALID_TOOL_STATE, + + /** + * La BackgroundTask n'a pas pu être créée. + */ + TOOL_TASK_ERROR_TASK_CREATION, + + /** + * La BackgroundTask n'a pas pu être démarrée. + */ + TOOL_TASK_ERROR_TASK_START, + + /** + * L'exécution externe a échoué. + */ + TOOL_TASK_ERROR_PROCESS +} ToolTaskError; + +/** + * @brief Domaine d'erreur de ToolTask. + */ +#define TOOL_TASK_ERROR \ + tool_task_error_quark() + +/** + * @brief Exécution préparée d'un outil externe. + * + * Cette structure possède : + * + * - la BackgroundTask avant son transfert éventuel à d'autres modules ; + * - les données du worker avant le démarrage ; + * - les copies de l'identifiant, du chemin et des arguments. + */ +typedef struct ToolTask ToolTask; + +/** + * @brief Résultat opaque produit par une ToolTask. + */ +typedef struct ToolTaskResult ToolTaskResult; + +/** + * @brief Retourne le domaine d'erreur de ToolTask. + * + * @return Quark GLib du domaine d'erreur. + */ +GQuark tool_task_error_quark(void); + +/** + * @brief Prépare une tâche exécutant un outil enregistré. + * + * Cette fonction ne démarre pas la tâche. + * + * Toutes les informations nécessaires à l'exécution sont copiées. Le + * ToolTask créé ne dépend donc plus de la durée de vie du registre ni des + * chaînes fournies. + * + * L'outil doit avoir été vérifié avec tool_registry_refresh() et être dans + * l'état TOOL_AVAILABILITY_AVAILABLE. + * + * @param tool_registry Registre contenant l'outil. + * @param tool_identifier Identifiant interne de l'outil. + * @param task_title Titre affiché pour la BackgroundTask. + * @param arguments Tableau d'arguments terminé par NULL, ou NULL. + * @param working_directory Dossier de travail facultatif, ou NULL. + * @param error Emplacement facultatif pour l'erreur. + * + * @return Nouveau ToolTask, ou NULL en cas d'échec. + */ +ToolTask *tool_task_new( + const ToolRegistry *tool_registry, + const char *tool_identifier, + const char *task_title, + const char *const arguments[], + const char *working_directory, + GError **error +); + +/** + * @brief Libère une ToolTask. + * + * Avant le démarrage, cette fonction libère également les données préparées + * pour le worker. + * + * Après un démarrage réussi, les données du worker appartiennent à la + * BackgroundTask. + * + * La fonction accepte NULL. + * + * @param tool_task Tâche à libérer. + */ +void tool_task_free( + ToolTask *tool_task +); + +/** + * @brief Retourne la BackgroundTask associée. + * + * Le pointeur retourné est emprunté. Il reste valide tant que ToolTask + * conserve sa propre référence ou qu'un autre propriétaire, par exemple + * TaskManager, conserve une référence. + * + * Cette fonction permet notamment d'ajouter la tâche au TaskManager avant + * son démarrage. + * + * @param tool_task ToolTask consultée. + * + * @return BackgroundTask associée, ou NULL. + */ +BackgroundTask *tool_task_get_background_task( + const ToolTask *tool_task +); + +/** + * @brief Démarre l'exécution asynchrone de l'outil. + * + * Si le démarrage réussit : + * + * - la BackgroundTask devient propriétaire des données du worker ; + * - ToolTask ne doit plus les libérer ; + * - ToolTask peut être libérée dès qu'un autre propriétaire conserve une + * référence sur la BackgroundTask. + * + * Si le démarrage échoue, ToolTask conserve la propriété de ses données. + * + * @param tool_task ToolTask à démarrer. + * @param completion_callback Callback final facultatif. + * @param completion_data Données facultatives du callback. + * @param completion_data_destroy Destructeur facultatif des données. + * @param error Emplacement facultatif pour l'erreur. + * + * @return TRUE si la tâche a été démarrée, sinon FALSE. + */ +gboolean tool_task_start( + ToolTask *tool_task, + BackgroundTaskCompletionCallback completion_callback, + gpointer completion_data, + GDestroyNotify completion_data_destroy, + GError **error +); + +/** + * @brief Retourne le résultat d'une BackgroundTask créée par ToolTask. + * + * Cette fonction doit uniquement être appelée avec une BackgroundTask + * obtenue par tool_task_get_background_task(). + * + * Le pointeur retourné est emprunté et appartient à la BackgroundTask. + * Il n'est disponible qu'après l'état BACKGROUND_TASK_STATE_COMPLETED. + * + * @param background_task Tâche créée par ToolTask. + * + * @return Résultat ToolTask emprunté, ou NULL. + */ +const ToolTaskResult *tool_task_result_from_background_task( + const BackgroundTask *background_task +); + +/** + * @brief Libère un résultat ToolTask. + * + * Cette fonction est normalement transmise comme destructeur du résultat à + * background_task_start(). + * + * La fonction accepte NULL. + * + * @param result Résultat à libérer. + */ +void tool_task_result_free( + ToolTaskResult *result +); + +/** + * @brief Retourne l'identifiant de l'outil exécuté. + * + * La chaîne retournée est empruntée. + * + * @param result Résultat consulté. + * + * @return Identifiant de l'outil, ou NULL. + */ +const char *tool_task_result_get_tool_identifier( + const ToolTaskResult *result +); + +/** + * @brief Retourne le chemin de l'exécutable utilisé. + * + * La chaîne retournée est empruntée. + * + * @param result Résultat consulté. + * + * @return Chemin de l'exécutable, ou NULL. + */ +const char *tool_task_result_get_executable_path( + const ToolTaskResult *result +); + +/** + * @brief Retourne le nombre d'arguments transmis à l'outil. + * + * Le chemin de l'exécutable, utilisé comme argv[0], n'est pas compté. + * + * @param result Résultat consulté. + * + * @return Nombre d'arguments. + */ +gsize tool_task_result_get_argument_count( + const ToolTaskResult *result +); + +/** + * @brief Retourne un argument transmis à l'outil. + * + * La chaîne retournée est empruntée. + * + * @param result Résultat consulté. + * @param index Index de l'argument. + * + * @return Argument correspondant, ou NULL si l'index est invalide. + */ +const char *tool_task_result_get_argument( + const ToolTaskResult *result, + gsize index +); + +/** + * @brief Retourne le dossier de travail utilisé. + * + * La chaîne retournée est empruntée. + * + * @param result Résultat consulté. + * + * @return Dossier de travail, ou NULL. + */ +const char *tool_task_result_get_working_directory( + const ToolTaskResult *result +); + +/** + * @brief Retourne le résultat brut du processus externe. + * + * Le pointeur retourné est emprunté et ne doit pas être libéré. + * + * @param result Résultat consulté. + * + * @return Résultat ToolProcess, ou NULL. + */ +const ToolProcessResult *tool_task_result_get_process_result( + const ToolTaskResult *result +); + +G_END_DECLS + +#endif diff --git a/labfy-investigation b/labfy-investigation index a960d38724647dbdac21a27e88b656c00d7c5c65..65db09d5dfefacdaf72b4bf88036c2b4202ac929 100755 GIT binary patch delta 101202 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 Up`+Z_)^(+rQ6#@?>uU1<0PF~&?*IS* delta 86981 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 sC{VYyRT;AH7->;>$t^ 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 BIphEU diff --git a/src/core/tool_task.c b/src/core/tool_task.c new file mode 100644 index 0000000..3780107 --- /dev/null +++ b/src/core/tool_task.c @@ -0,0 +1,981 @@ +/****************************************************************************** + * @file tool_task.c + * @brief Exécution d'un outil externe dans une tâche d'arrière-plan. + ******************************************************************************/ + +#include "core/tool_task.h" + +/** + * @struct ToolTaskWorkerData + * @brief Données privées transmises au worker. + */ +typedef struct +{ + char *tool_identifier; + char *tool_display_name; + char *executable_path; + + char **arguments; + gsize argument_count; + + char *working_directory; +} ToolTaskWorkerData; + +/** + * @struct ToolTask + * @brief Exécution externe préparée. + */ +struct ToolTask +{ + BackgroundTask *background_task; + ToolTaskWorkerData *worker_data; + + gboolean started; +}; + +/** + * @struct ToolTaskResult + * @brief Résultat d'une exécution externe en arrière-plan. + */ +struct ToolTaskResult +{ + char *tool_identifier; + char *executable_path; + + char **arguments; + gsize argument_count; + + char *working_directory; + + ToolProcessResult *process_result; +}; + +/** + * @brief Vérifie qu'une chaîne est définie et non vide. + * + * @param text Chaîne à vérifier. + * + * @return TRUE si la chaîne est valide. + */ +static gboolean tool_task_string_is_valid( + const char *text +) +{ + return text != NULL && + text[0] != '\0'; +} + +/** + * @brief Duplique un tableau d'arguments terminé par NULL. + * + * Un tableau vide terminé par NULL est créé lorsque arguments vaut NULL. + * + * @param arguments Arguments à copier, ou NULL. + * @param out_argument_count Emplacement recevant le nombre d'arguments. + * + * @return Nouveau tableau terminé par NULL. + */ +static char **tool_task_duplicate_arguments( + const char *const arguments[], + gsize *out_argument_count +) +{ + char **arguments_copy = NULL; + + gsize argument_count = 0; + gsize argument_index = 0; + + if (out_argument_count != NULL) + { + *out_argument_count = 0; + } + + if (arguments != NULL) + { + while (arguments[argument_count] != NULL) + { + argument_count++; + } + } + + arguments_copy = g_new0( + char *, + argument_count + 1 + ); + + for (argument_index = 0; + argument_index < argument_count; + argument_index++) + { + arguments_copy[argument_index] = + g_strdup( + arguments[argument_index] + ); + } + + arguments_copy[argument_count] = NULL; + + if (out_argument_count != NULL) + { + *out_argument_count = + argument_count; + } + + return arguments_copy; +} + +/** + * @brief Libère les données privées du worker. + * + * @param user_data Pointeur vers ToolTaskWorkerData. + */ +static void tool_task_worker_data_free( + gpointer user_data +) +{ + ToolTaskWorkerData *worker_data = + user_data; + + if (worker_data == NULL) + { + return; + } + + g_clear_pointer( + &worker_data->tool_identifier, + g_free + ); + + g_clear_pointer( + &worker_data->tool_display_name, + g_free + ); + + g_clear_pointer( + &worker_data->executable_path, + g_free + ); + + g_clear_pointer( + &worker_data->arguments, + g_strfreev + ); + + g_clear_pointer( + &worker_data->working_directory, + g_free + ); + + g_free( + worker_data + ); +} + +/** + * @brief Adapte tool_task_result_free() à GDestroyNotify. + * + * @param user_data Pointeur vers ToolTaskResult. + */ +static void tool_task_result_destroy( + gpointer user_data +) +{ + tool_task_result_free( + user_data + ); +} + +/** + * @brief Produit une erreur ToolTask avec une cause facultative. + * + * @param error Emplacement facultatif pour l'erreur. + * @param error_code Code ToolTask. + * @param context_message Description de l'échec. + * @param cause Erreur d'origine facultative. + */ +static void tool_task_set_wrapped_error( + GError **error, + ToolTaskError error_code, + const char *context_message, + const GError *cause +) +{ + const char *cause_message = NULL; + + if (cause == NULL || + cause->message == NULL) + { + g_set_error_literal( + error, + TOOL_TASK_ERROR, + error_code, + context_message + ); + + return; + } + + cause_message = cause->message; + + g_set_error( + error, + TOOL_TASK_ERROR, + error_code, + "%s : %s", + context_message, + cause_message + ); +} + +/** + * @brief Crée le résultat ToolTask à partir des données du worker. + * + * La fonction devient propriétaire de process_result. + * + * @param worker_data Données de l'exécution. + * @param process_result Résultat brut du processus. + * + * @return Nouveau résultat. + */ +static ToolTaskResult *tool_task_result_new( + const ToolTaskWorkerData *worker_data, + ToolProcessResult *process_result +) +{ + ToolTaskResult *task_result = NULL; + + if (worker_data == NULL || + process_result == NULL) + { + return NULL; + } + + task_result = g_new0( + ToolTaskResult, + 1 + ); + + task_result->tool_identifier = + g_strdup( + worker_data->tool_identifier + ); + + task_result->executable_path = + g_strdup( + worker_data->executable_path + ); + + task_result->arguments = + tool_task_duplicate_arguments( + (const char *const *) + worker_data->arguments, + &task_result->argument_count + ); + + task_result->working_directory = + g_strdup( + worker_data->working_directory + ); + + task_result->process_result = + process_result; + + return task_result; +} + +/** + * @brief Exécute l'outil externe dans le thread secondaire. + * + * @param background_task Tâche en cours. + * @param cancellable Objet d'annulation. + * @param worker_data_pointer Données privées du worker. + * @param result Emplacement recevant ToolTaskResult. + * @param error Emplacement recevant une erreur. + * + * @return TRUE lorsqu'un résultat exploitable a été produit. + */ +static gboolean tool_task_worker( + BackgroundTask *background_task, + GCancellable *cancellable, + gpointer worker_data_pointer, + gpointer *result, + GError **error +) +{ + ToolTaskWorkerData *worker_data = + worker_data_pointer; + + ToolProcessResult *process_result = NULL; + ToolTaskResult *task_result = NULL; + + GError *process_error = NULL; + + char *execution_status = NULL; + char *final_status = NULL; + + gboolean process_success = FALSE; + + if (background_task == NULL || + cancellable == NULL || + worker_data == NULL || + result == NULL || + error == NULL) + { + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT, + "Les arguments du worker ToolTask sont invalides." + ); + + return FALSE; + } + + *result = NULL; + + background_task_report_progress( + background_task, + 0.0, + "Préparation de l'exécution" + ); + + /* + * Une annulation demandée avant le lancement doit être transmise + * sous la forme attendue par BackgroundTask. + */ + if (g_cancellable_set_error_if_cancelled( + cancellable, + error + )) + { + return FALSE; + } + + execution_status = g_strdup_printf( + "Exécution de %s", + tool_task_string_is_valid( + worker_data->tool_display_name + ) + ? worker_data->tool_display_name + : worker_data->tool_identifier + ); + + background_task_report_progress( + background_task, + 0.1, + execution_status + ); + + g_free( + execution_status + ); + + execution_status = NULL; + + process_success = tool_process_run( + worker_data->executable_path, + (const char *const *) + worker_data->arguments, + worker_data->working_directory, + cancellable, + &process_result, + &process_error + ); + + if (!process_success) + { + /* + * BackgroundTask reconnaît une annulation lorsque le domaine + * d'erreur est G_IO_ERROR et le code G_IO_ERROR_CANCELLED. + */ + if (process_error != NULL && + g_error_matches( + process_error, + TOOL_PROCESS_ERROR, + TOOL_PROCESS_ERROR_CANCELLED + )) + { + g_set_error( + error, + G_IO_ERROR, + G_IO_ERROR_CANCELLED, + "%s", + process_error->message + ); + } + else + { + tool_task_set_wrapped_error( + error, + TOOL_TASK_ERROR_PROCESS, + "L'exécution de l'outil externe a échoué", + process_error + ); + } + + g_clear_error( + &process_error + ); + + return FALSE; + } + + if (process_result == NULL) + { + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_PROCESS, + "ToolProcess n'a produit aucun résultat exploitable." + ); + + return FALSE; + } + + background_task_report_progress( + background_task, + 0.9, + "Traitement du résultat" + ); + + task_result = tool_task_result_new( + worker_data, + process_result + ); + + if (task_result == NULL) + { + tool_process_result_free( + process_result + ); + + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_PROCESS, + "Le résultat de l'outil n'a pas pu être construit." + ); + + return FALSE; + } + + /* + * ToolTaskResult possède maintenant ToolProcessResult. + */ + process_result = NULL; + + if (tool_process_result_is_success( + task_result->process_result + )) + { + final_status = g_strdup( + "Exécution terminée avec succès" + ); + } + else if (tool_process_result_exited_normally( + task_result->process_result + )) + { + final_status = g_strdup_printf( + "Exécution terminée avec le code %d", + tool_process_result_get_exit_status( + task_result->process_result + ) + ); + } + else if (tool_process_result_was_signaled( + task_result->process_result + )) + { + final_status = g_strdup_printf( + "Exécution terminée par le signal %d", + tool_process_result_get_termination_signal( + task_result->process_result + ) + ); + } + else + { + final_status = g_strdup( + "Exécution terminée" + ); + } + + background_task_report_progress( + background_task, + 1.0, + final_status + ); + + g_free( + final_status + ); + + *result = task_result; + + return TRUE; +} + +GQuark tool_task_error_quark(void) +{ + return g_quark_from_static_string( + "labfy-investigation-tool-task-error" + ); +} + +ToolTask *tool_task_new( + const ToolRegistry *tool_registry, + const char *tool_identifier, + const char *task_title, + const char *const arguments[], + const char *working_directory, + GError **error +) +{ + ToolTask *tool_task = NULL; + ToolTaskWorkerData *worker_data = NULL; + + const ToolInfo *tool_info = NULL; + + const char *tool_display_name = NULL; + const char *executable_path = NULL; + + ToolAvailability availability; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (tool_registry == NULL || + !tool_task_string_is_valid( + tool_identifier + ) || + !tool_task_string_is_valid( + task_title + ) || + (working_directory != NULL && + working_directory[0] == '\0')) + { + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT, + "Les arguments fournis à ToolTask sont invalides." + ); + + return NULL; + } + + tool_info = tool_registry_find( + tool_registry, + tool_identifier + ); + + if (tool_info == NULL) + { + g_set_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TOOL_NOT_FOUND, + "Aucun outil portant l'identifiant '%s' n'est enregistré.", + tool_identifier + ); + + return NULL; + } + + availability = tool_info_get_availability( + tool_info + ); + + if (availability == + TOOL_AVAILABILITY_UNKNOWN) + { + g_set_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TOOL_NOT_CHECKED, + "La disponibilité de l'outil '%s' n'a pas été vérifiée.", + tool_identifier + ); + + return NULL; + } + + if (availability == + TOOL_AVAILABILITY_MISSING) + { + g_set_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TOOL_MISSING, + "L'outil '%s' est absent de la machine.", + tool_identifier + ); + + return NULL; + } + + executable_path = + tool_info_get_resolved_path( + tool_info + ); + + if (!tool_task_string_is_valid( + executable_path + )) + { + g_set_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_TOOL_STATE, + "L'outil '%s' est disponible mais ne possède aucun chemin.", + tool_identifier + ); + + return NULL; + } + + tool_display_name = + tool_info_get_display_name( + tool_info + ); + + worker_data = g_new0( + ToolTaskWorkerData, + 1 + ); + + worker_data->tool_identifier = + g_strdup( + tool_identifier + ); + + worker_data->tool_display_name = + g_strdup( + tool_display_name + ); + + worker_data->executable_path = + g_strdup( + executable_path + ); + + worker_data->arguments = + tool_task_duplicate_arguments( + arguments, + &worker_data->argument_count + ); + + worker_data->working_directory = + g_strdup( + working_directory + ); + + tool_task = g_new0( + ToolTask, + 1 + ); + + tool_task->background_task = + background_task_new( + task_title + ); + + if (tool_task->background_task == NULL) + { + tool_task_worker_data_free( + worker_data + ); + + g_free( + tool_task + ); + + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TASK_CREATION, + "La tâche d'arrière-plan n'a pas pu être créée." + ); + + return NULL; + } + + tool_task->worker_data = + worker_data; + + tool_task->started = + FALSE; + + return tool_task; +} + +void tool_task_free( + ToolTask *tool_task +) +{ + if (tool_task == NULL) + { + return; + } + + /* + * Avant le démarrage, ToolTask possède les données du worker. + * + * Après un démarrage réussi, le pointeur vaut NULL car la propriété + * a été transférée à BackgroundTask. + */ + if (tool_task->worker_data != NULL) + { + tool_task_worker_data_free( + tool_task->worker_data + ); + + tool_task->worker_data = NULL; + } + + background_task_unref( + tool_task->background_task + ); + + tool_task->background_task = NULL; + + g_free( + tool_task + ); +} + +BackgroundTask *tool_task_get_background_task( + const ToolTask *tool_task +) +{ + if (tool_task == NULL) + { + return NULL; + } + + return tool_task->background_task; +} + +gboolean tool_task_start( + ToolTask *tool_task, + BackgroundTaskCompletionCallback completion_callback, + gpointer completion_data, + GDestroyNotify completion_data_destroy, + GError **error +) +{ + GError *start_error = NULL; + gboolean start_success = FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (tool_task == NULL || + tool_task->background_task == NULL || + tool_task->worker_data == NULL) + { + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT, + "La ToolTask fournie est invalide." + ); + + return FALSE; + } + + if (tool_task->started || + background_task_get_state( + tool_task->background_task + ) != BACKGROUND_TASK_STATE_PENDING) + { + g_set_error_literal( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TASK_START, + "La ToolTask a déjà été démarrée." + ); + + return FALSE; + } + + start_success = background_task_start( + tool_task->background_task, + tool_task_worker, + tool_task->worker_data, + tool_task_worker_data_free, + tool_task_result_destroy, + completion_callback, + completion_data, + completion_data_destroy, + &start_error + ); + + if (!start_success) + { + tool_task_set_wrapped_error( + error, + TOOL_TASK_ERROR_TASK_START, + "La tâche d'arrière-plan n'a pas pu être démarrée", + start_error + ); + + g_clear_error( + &start_error + ); + + /* + * BackgroundTask n'a pas pris possession des données. + */ + return FALSE; + } + + /* + * La propriété des données du worker appartient désormais à + * BackgroundTask. + */ + tool_task->worker_data = NULL; + tool_task->started = TRUE; + + return TRUE; +} + +const ToolTaskResult *tool_task_result_from_background_task( + const BackgroundTask *background_task +) +{ + if (background_task == NULL || + background_task_get_state( + background_task + ) != BACKGROUND_TASK_STATE_COMPLETED) + { + return NULL; + } + + /* + * Précondition publique : + * background_task doit provenir de ToolTask. + */ + return background_task_get_result( + background_task + ); +} + +void tool_task_result_free( + ToolTaskResult *result +) +{ + if (result == NULL) + { + return; + } + + g_clear_pointer( + &result->tool_identifier, + g_free + ); + + g_clear_pointer( + &result->executable_path, + g_free + ); + + g_clear_pointer( + &result->arguments, + g_strfreev + ); + + g_clear_pointer( + &result->working_directory, + g_free + ); + + g_clear_pointer( + &result->process_result, + tool_process_result_free + ); + + g_free( + result + ); +} + +const char *tool_task_result_get_tool_identifier( + const ToolTaskResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->tool_identifier; +} + +const char *tool_task_result_get_executable_path( + const ToolTaskResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->executable_path; +} + +gsize tool_task_result_get_argument_count( + const ToolTaskResult *result +) +{ + if (result == NULL) + { + return 0; + } + + return result->argument_count; +} + +const char *tool_task_result_get_argument( + const ToolTaskResult *result, + gsize index +) +{ + if (result == NULL || + result->arguments == NULL || + index >= result->argument_count) + { + return NULL; + } + + return result->arguments[index]; +} + +const char *tool_task_result_get_working_directory( + const ToolTaskResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->working_directory; +} + +const ToolProcessResult *tool_task_result_get_process_result( + const ToolTaskResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->process_result; +} diff --git a/tests/test_tool_task b/tests/test_tool_task new file mode 100755 index 0000000000000000000000000000000000000000..ad9a91adb67b6cb450fa54154f13255b54bd3492 GIT binary patch literal 75680 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 +#include + +#include +#include + +/** + * @struct ToolTaskFixture + * @brief Environnement temporaire contenant un faux outil. + */ +typedef struct +{ + char *temporary_directory; + char *fake_tool_path; + char *original_path; +} ToolTaskFixture; + +/** + * @struct ToolTaskWaitContext + * @brief Synchronisation entre une BackgroundTask et le test principal. + */ +typedef struct +{ + GMainLoop *main_loop; + + BackgroundTask *completed_task; + + guint timeout_source_id; + + gboolean completion_called; + gboolean timed_out; +} ToolTaskWaitContext; + +/** + * @struct ToolTaskCancellationContext + * @brief Contexte utilisé pendant un test d'annulation. + */ +typedef struct +{ + GMainLoop *main_loop; + BackgroundTask *background_task; + + guint cancel_source_id; + guint timeout_source_id; + + gboolean completion_called; + gboolean timed_out; +} ToolTaskCancellationContext; + +/** + * @struct ToolTaskConcurrentContext + * @brief Contexte d'attente de deux tâches simultanées. + */ +typedef struct +{ + GMainLoop *main_loop; + + BackgroundTask *first_task; + BackgroundTask *second_task; + + guint completed_count; + guint timeout_source_id; + + gboolean timed_out; +} ToolTaskConcurrentContext; + +static char *test_tool_task_shared_directory = NULL; + +static void test_tool_task_write_script( + ToolTaskFixture *fixture, + const char *script_content +); + +static void test_tool_task_fixture_setup( + ToolTaskFixture *fixture, + gconstpointer user_data +); + +static ToolRegistry *test_tool_task_create_registry( + ToolTaskFixture *fixture, + gboolean create_executable, + gboolean refresh_registry +); + +static gboolean test_tool_task_on_timeout( + gpointer user_data +); + +static void test_tool_task_on_completed( + BackgroundTask *background_task, + gpointer user_data +); + +static gboolean test_tool_task_start_and_wait( + ToolTask *tool_task, + ToolTaskWaitContext *wait_context, + GError **error +); + +static void test_tool_task_assert_bytes_equal_text( + GBytes *bytes, + const char *expected_text +); + +static ToolRegistry *test_tool_task_create_registry( + ToolTaskFixture *fixture, + gboolean create_executable, + gboolean refresh_registry +); + +static gboolean test_tool_task_cancel_task( + gpointer user_data +); + +static gboolean test_tool_task_cancellation_timeout( + gpointer user_data +); + +static void test_tool_task_on_cancellation_completed( + BackgroundTask *background_task, + gpointer user_data +); + +static gboolean test_tool_task_concurrent_timeout( + gpointer user_data +); + +static void test_tool_task_on_concurrent_completed( + BackgroundTask *background_task, + gpointer user_data +); + +static void test_tool_task_cancellation( + ToolTaskFixture *fixture, + gconstpointer user_data +); + +static void test_tool_task_concurrent_tasks( + ToolTaskFixture *fixture, + gconstpointer user_data +); + +/** + * @brief Demande l'annulation de la tâche testée. + */ +static gboolean test_tool_task_cancel_task( + gpointer user_data +) +{ + ToolTaskCancellationContext *cancellation_context = + user_data; + + if (cancellation_context == NULL) + { + return G_SOURCE_REMOVE; + } + + cancellation_context->cancel_source_id = 0; + + background_task_cancel( + cancellation_context->background_task + ); + + return G_SOURCE_REMOVE; +} + +/** + * @brief Interrompt le test d'annulation en cas de blocage. + */ +static gboolean test_tool_task_cancellation_timeout( + gpointer user_data +) +{ + ToolTaskCancellationContext *cancellation_context = + user_data; + + if (cancellation_context == NULL) + { + return G_SOURCE_REMOVE; + } + + cancellation_context->timeout_source_id = 0; + cancellation_context->timed_out = TRUE; + + if (cancellation_context->main_loop != NULL) + { + g_main_loop_quit( + cancellation_context->main_loop + ); + } + + return G_SOURCE_REMOVE; +} + +/** + * @brief Reçoit la fin de la tâche annulée. + */ +static void test_tool_task_on_cancellation_completed( + BackgroundTask *background_task, + gpointer user_data +) +{ + ToolTaskCancellationContext *cancellation_context = + user_data; + + if (cancellation_context == NULL) + { + return; + } + + g_assert_true( + background_task == + cancellation_context->background_task + ); + + cancellation_context->completion_called = TRUE; + + if (cancellation_context->cancel_source_id != 0) + { + g_source_remove( + cancellation_context->cancel_source_id + ); + + cancellation_context->cancel_source_id = 0; + } + + if (cancellation_context->timeout_source_id != 0) + { + g_source_remove( + cancellation_context->timeout_source_id + ); + + cancellation_context->timeout_source_id = 0; + } + + if (cancellation_context->main_loop != NULL) + { + g_main_loop_quit( + cancellation_context->main_loop + ); + } +} + +/** + * @brief Interrompt un test resté bloqué trop longtemps. + * + * @param user_data Pointeur vers ToolTaskWaitContext. + * + * @return G_SOURCE_REMOVE. + */ +static gboolean test_tool_task_on_timeout( + gpointer user_data +) +{ + ToolTaskWaitContext *wait_context = + user_data; + + if (wait_context == NULL) + { + return G_SOURCE_REMOVE; + } + + wait_context->timeout_source_id = 0; + wait_context->timed_out = TRUE; + + if (wait_context->main_loop != NULL) + { + g_main_loop_quit( + wait_context->main_loop + ); + } + + return G_SOURCE_REMOVE; +} + +/** + * @brief Reçoit la notification finale d'une BackgroundTask. + * + * @param background_task Tâche terminée. + * @param user_data Pointeur vers ToolTaskWaitContext. + */ +static void test_tool_task_on_completed( + BackgroundTask *background_task, + gpointer user_data +) +{ + ToolTaskWaitContext *wait_context = + user_data; + + if (wait_context == NULL) + { + return; + } + + wait_context->completion_called = TRUE; + wait_context->completed_task = + background_task; + + if (wait_context->timeout_source_id != 0) + { + g_source_remove( + wait_context->timeout_source_id + ); + + wait_context->timeout_source_id = 0; + } + + if (wait_context->main_loop != NULL) + { + g_main_loop_quit( + wait_context->main_loop + ); + } +} + +/** + * @brief Démarre une ToolTask et attend sa terminaison. + * + * @param tool_task Tâche à démarrer. + * @param wait_context Contexte d'attente à initialiser. + * @param error Emplacement facultatif pour l'erreur de démarrage. + * + * @return TRUE si la tâche a été démarrée et terminée avant le timeout. + */ +static gboolean test_tool_task_start_and_wait( + ToolTask *tool_task, + ToolTaskWaitContext *wait_context, + GError **error +) +{ + gboolean start_success = FALSE; + + if (tool_task == NULL || + wait_context == NULL) + { + return FALSE; + } + + wait_context->main_loop = + g_main_loop_new( + NULL, + FALSE + ); + + wait_context->completed_task = NULL; + wait_context->completion_called = FALSE; + wait_context->timed_out = FALSE; + + wait_context->timeout_source_id = + g_timeout_add_seconds( + 5, + test_tool_task_on_timeout, + wait_context + ); + + start_success = tool_task_start( + tool_task, + test_tool_task_on_completed, + wait_context, + NULL, + error + ); + + if (!start_success) + { + if (wait_context->timeout_source_id != 0) + { + g_source_remove( + wait_context->timeout_source_id + ); + + wait_context->timeout_source_id = 0; + } + + g_main_loop_unref( + wait_context->main_loop + ); + + wait_context->main_loop = NULL; + + return FALSE; + } + + g_main_loop_run( + wait_context->main_loop + ); + + if (wait_context->timeout_source_id != 0) + { + g_source_remove( + wait_context->timeout_source_id + ); + + wait_context->timeout_source_id = 0; + } + + g_main_loop_unref( + wait_context->main_loop + ); + + wait_context->main_loop = NULL; + + return wait_context->completion_called && + !wait_context->timed_out; +} + +/** + * @brief Vérifie qu'un GBytes contient exactement le texte attendu. + * + * @param bytes Données à vérifier. + * @param expected_text Texte attendu. + */ +static void test_tool_task_assert_bytes_equal_text( + GBytes *bytes, + const char *expected_text +) +{ + gconstpointer bytes_data = NULL; + + gsize bytes_size = 0; + gsize expected_size = 0; + + g_assert_nonnull( + bytes + ); + + g_assert_nonnull( + expected_text + ); + + bytes_data = g_bytes_get_data( + bytes, + &bytes_size + ); + + expected_size = strlen( + expected_text + ); + + g_assert_cmpuint( + bytes_size, + ==, + expected_size + ); + + g_assert_cmpmem( + bytes_data, + bytes_size, + expected_text, + expected_size + ); +} + +/** + * @brief Vérifie que ToolTask copie toutes les données nécessaires. + */ +static void test_tool_task_argument_ownership( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + + BackgroundTask *background_task = NULL; + + const ToolTaskResult *task_result = NULL; + const ToolProcessResult *process_result = NULL; + + ToolTaskWaitContext wait_context = {0}; + + GBytes *stdout_bytes = NULL; + + char *first_argument = NULL; + char *second_argument = NULL; + char *working_directory = NULL; + + const char *arguments[3]; + + GError *error = NULL; + + (void) user_data; + + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "for argument in \"$@\"\n" + "do\n" + " printf '%s\\n' \"$argument\"\n" + "done\n" + ); + + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + first_argument = g_strdup( + "premier argument" + ); + + second_argument = g_strdup( + "second;non-interprete" + ); + + working_directory = g_strdup( + fixture->temporary_directory + ); + + arguments[0] = first_argument; + arguments[1] = second_argument; + arguments[2] = NULL; + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Test de propriété", + arguments, + working_directory, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + tool_task + ); + + /* + * Toutes ces données doivent maintenant être indépendantes. + */ + g_free( + first_argument + ); + + g_free( + second_argument + ); + + g_free( + working_directory + ); + + tool_registry_free( + tool_registry + ); + + tool_registry = NULL; + + background_task = + tool_task_get_background_task( + tool_task + ); + + g_assert_true( + test_tool_task_start_and_wait( + tool_task, + &wait_context, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_cmpint( + background_task_get_state( + background_task + ), + ==, + BACKGROUND_TASK_STATE_COMPLETED + ); + + task_result = + tool_task_result_from_background_task( + background_task + ); + + g_assert_nonnull( + task_result + ); + + g_assert_cmpuint( + tool_task_result_get_argument_count( + task_result + ), + ==, + 2 + ); + + g_assert_cmpstr( + tool_task_result_get_argument( + task_result, + 0 + ), + ==, + "premier argument" + ); + + g_assert_cmpstr( + tool_task_result_get_argument( + task_result, + 1 + ), + ==, + "second;non-interprete" + ); + + g_assert_cmpstr( + tool_task_result_get_working_directory( + task_result + ), + ==, + fixture->temporary_directory + ); + + process_result = + tool_task_result_get_process_result( + task_result + ); + + g_assert_nonnull( + process_result + ); + + stdout_bytes = + tool_process_result_ref_stdout( + process_result + ); + + test_tool_task_assert_bytes_equal_text( + stdout_bytes, + "premier argument\n" + "second;non-interprete\n" + ); + + g_bytes_unref( + stdout_bytes + ); + + tool_task_free( + tool_task + ); +} + +/** + * @brief Vérifie une exécution complète réussie. + */ +static void test_tool_task_success( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + + BackgroundTask *background_task = NULL; + + const ToolTaskResult *task_result = NULL; + const ToolProcessResult *process_result = NULL; + + ToolTaskWaitContext wait_context = {0}; + + GBytes *stdout_bytes = NULL; + GBytes *stderr_bytes = NULL; + + const char *arguments[] = + { + "example.org", + "nom avec espaces", + NULL + }; + + GError *error = NULL; + + (void) user_data; + + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "printf 'stdout:%s|%s' \"$1\" \"$2\"\n" + "printf 'stderr-test' >&2\n" + "exit 0\n" + ); + + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Exécution réussie", + arguments, + NULL, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + tool_task + ); + + background_task = + tool_task_get_background_task( + tool_task + ); + + g_assert_true( + test_tool_task_start_and_wait( + tool_task, + &wait_context, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_true( + wait_context.completion_called + ); + + g_assert_true( + wait_context.completed_task == + background_task + ); + + g_assert_cmpint( + background_task_get_state( + background_task + ), + ==, + BACKGROUND_TASK_STATE_COMPLETED + ); + + g_assert_cmpfloat( + background_task_get_progress( + background_task + ), + ==, + 1.0 + ); + + task_result = + tool_task_result_from_background_task( + background_task + ); + + g_assert_nonnull( + task_result + ); + + g_assert_cmpstr( + tool_task_result_get_tool_identifier( + task_result + ), + ==, + "test.fake" + ); + + g_assert_cmpstr( + tool_task_result_get_executable_path( + task_result + ), + ==, + fixture->fake_tool_path + ); + + g_assert_cmpuint( + tool_task_result_get_argument_count( + task_result + ), + ==, + 2 + ); + + process_result = + tool_task_result_get_process_result( + task_result + ); + + g_assert_nonnull( + process_result + ); + + g_assert_true( + tool_process_result_is_success( + process_result + ) + ); + + g_assert_cmpint( + tool_process_result_get_exit_status( + process_result + ), + ==, + 0 + ); + + stdout_bytes = + tool_process_result_ref_stdout( + process_result + ); + + stderr_bytes = + tool_process_result_ref_stderr( + process_result + ); + + test_tool_task_assert_bytes_equal_text( + stdout_bytes, + "stdout:example.org|nom avec espaces" + ); + + test_tool_task_assert_bytes_equal_text( + stderr_bytes, + "stderr-test" + ); + + g_bytes_unref( + stdout_bytes + ); + + g_bytes_unref( + stderr_bytes + ); + + tool_task_free( + tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie qu'un code non nul produit une tâche terminée. + */ +static void test_tool_task_nonzero_exit( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + + BackgroundTask *background_task = NULL; + + const ToolTaskResult *task_result = NULL; + const ToolProcessResult *process_result = NULL; + + ToolTaskWaitContext wait_context = {0}; + + GError *error = NULL; + + (void) user_data; + + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "printf 'erreur fonctionnelle' >&2\n" + "exit 7\n" + ); + + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Code non nul", + NULL, + NULL, + &error + ); + + g_assert_no_error( + error + ); + + background_task = + tool_task_get_background_task( + tool_task + ); + + g_assert_true( + test_tool_task_start_and_wait( + tool_task, + &wait_context, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_cmpint( + background_task_get_state( + background_task + ), + ==, + BACKGROUND_TASK_STATE_COMPLETED + ); + + task_result = + tool_task_result_from_background_task( + background_task + ); + + g_assert_nonnull( + task_result + ); + + process_result = + tool_task_result_get_process_result( + task_result + ); + + g_assert_nonnull( + process_result + ); + + g_assert_false( + tool_process_result_is_success( + process_result + ) + ); + + g_assert_cmpint( + tool_process_result_get_exit_status( + process_result + ), + ==, + 7 + ); + + tool_task_free( + tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie l'échec lorsque l'exécutable disparaît avant le lancement. + */ +static void test_tool_task_spawn_failure( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + + BackgroundTask *background_task = NULL; + + ToolTaskWaitContext wait_context = {0}; + + GError *task_error = NULL; + GError *stored_error = NULL; + + (void) user_data; + + tool_registry = test_tool_task_create_registry( + fixture, + TRUE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Échec de lancement", + NULL, + NULL, + &task_error + ); + + g_assert_no_error( + task_error + ); + + g_assert_nonnull( + tool_task + ); + + background_task = + tool_task_get_background_task( + tool_task + ); + + g_assert_cmpint( + g_remove( + fixture->fake_tool_path + ), + ==, + 0 + ); + + /* + * Le démarrage asynchrone réussit. C'est le worker qui découvrira + * ensuite que l'exécutable a disparu. + */ + g_assert_true( + test_tool_task_start_and_wait( + tool_task, + &wait_context, + &task_error + ) + ); + + g_assert_no_error( + task_error + ); + + g_assert_cmpint( + background_task_get_state( + background_task + ), + ==, + BACKGROUND_TASK_STATE_FAILED + ); + + g_assert_null( + tool_task_result_from_background_task( + background_task + ) + ); + + stored_error = + background_task_dup_error( + background_task + ); + + g_assert_nonnull( + stored_error + ); + + g_assert_error( + stored_error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_PROCESS + ); + + g_clear_error( + &stored_error + ); + + tool_task_free( + tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie la transmission du dossier de travail. + */ +static void test_tool_task_working_directory( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + + BackgroundTask *background_task = NULL; + + const ToolTaskResult *task_result = NULL; + const ToolProcessResult *process_result = NULL; + + ToolTaskWaitContext wait_context = {0}; + + GBytes *stdout_bytes = NULL; + + char *canonical_directory = NULL; + + GError *error = NULL; + + (void) user_data; + + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "printf '%s' \"$PWD\"\n" + ); + + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Dossier de travail", + NULL, + fixture->temporary_directory, + &error + ); + + g_assert_no_error( + error + ); + + background_task = + tool_task_get_background_task( + tool_task + ); + + g_assert_true( + test_tool_task_start_and_wait( + tool_task, + &wait_context, + &error + ) + ); + + g_assert_no_error( + error + ); + + task_result = + tool_task_result_from_background_task( + background_task + ); + + g_assert_nonnull( + task_result + ); + + process_result = + tool_task_result_get_process_result( + task_result + ); + + stdout_bytes = + tool_process_result_ref_stdout( + process_result + ); + + canonical_directory = + g_canonicalize_filename( + fixture->temporary_directory, + NULL + ); + + test_tool_task_assert_bytes_equal_text( + stdout_bytes, + canonical_directory + ); + + g_assert_cmpstr( + tool_task_result_get_working_directory( + task_result + ), + ==, + fixture->temporary_directory + ); + + g_bytes_unref( + stdout_bytes + ); + + g_free( + canonical_directory + ); + + tool_task_free( + tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Crée un faux exécutable dans le dossier temporaire. + * + * @param fixture Fixture du test. + * @param script_content Contenu complet du script. + */ +static void test_tool_task_write_script( + ToolTaskFixture *fixture, + const char *script_content +) +{ + GError *error = NULL; + gboolean write_success = FALSE; + int chmod_result = 0; + + g_assert_nonnull( + fixture + ); + + g_assert_nonnull( + fixture->fake_tool_path + ); + + g_assert_nonnull( + script_content + ); + + write_success = g_file_set_contents( + fixture->fake_tool_path, + script_content, + -1, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_true( + write_success + ); + + chmod_result = g_chmod( + fixture->fake_tool_path, + S_IRUSR | + S_IWUSR | + S_IXUSR + ); + + g_assert_cmpint( + chmod_result, + ==, + 0 + ); +} + +static void test_tool_task_fixture_setup( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + (void) user_data; + + g_assert_nonnull( + fixture + ); + + g_assert_nonnull( + test_tool_task_shared_directory + ); + + fixture->temporary_directory = + g_strdup( + test_tool_task_shared_directory + ); + + fixture->fake_tool_path = + g_build_filename( + fixture->temporary_directory, + "fake_tool", + NULL + ); + + fixture->original_path = NULL; +} + +/** + * @brief Supprime les fichiers propres au test. + * + * Le dossier temporaire et le PATH sont partagés par toute la suite de + * tests. Ils sont donc nettoyés uniquement à la fin de main(). + */ +static void test_tool_task_fixture_teardown( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + (void) user_data; + + if (fixture->fake_tool_path != NULL) + { + g_remove( + fixture->fake_tool_path + ); + } + + g_clear_pointer( + &fixture->fake_tool_path, + g_free + ); + + g_clear_pointer( + &fixture->temporary_directory, + g_free + ); + + g_clear_pointer( + &fixture->original_path, + g_free + ); +} + +/** + * @brief Crée un registre contenant le faux outil. + * + * @param fixture Fixture du test. + * @param create_executable TRUE pour créer le faux exécutable. + * @param refresh_registry TRUE pour détecter sa disponibilité. + * + * @return Nouveau registre. + */ +static ToolRegistry *test_tool_task_create_registry( + ToolTaskFixture *fixture, + gboolean create_executable, + gboolean refresh_registry +) +{ + ToolRegistry *tool_registry = NULL; + GError *error = NULL; + + if (create_executable) + { + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "exit 0\n" + ); + } + + tool_registry = tool_registry_new(); + + g_assert_nonnull( + tool_registry + ); + + g_assert_true( + tool_registry_register( + tool_registry, + "test.fake", + "Fake Tool", + "fake_tool", + TOOL_REQUIREMENT_OPTIONAL, + &error + ) + ); + + g_assert_no_error( + error + ); + + if (refresh_registry) + { + g_assert_true( + tool_registry_refresh( + tool_registry, + &error + ) + ); + + g_assert_no_error( + error + ); + } + + return tool_registry; +} + +/** + * @brief Vérifie le refus des arguments invalides. + */ +static void test_tool_task_invalid_arguments( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + GError *error = NULL; + + (void) user_data; + + tool_registry = test_tool_task_create_registry( + fixture, + TRUE, + TRUE + ); + + tool_task = tool_task_new( + NULL, + "test.fake", + "Tâche de test", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + tool_task = tool_task_new( + tool_registry, + NULL, + "Tâche de test", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + tool_task = tool_task_new( + tool_registry, + "", + "Tâche de test", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + NULL, + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Tâche de test", + NULL, + "", + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie le refus d'un identifiant inconnu. + */ +static void test_tool_task_tool_not_found( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + GError *error = NULL; + + (void) fixture; + (void) user_data; + + tool_registry = tool_registry_new(); + + g_assert_nonnull( + tool_registry + ); + + tool_task = tool_task_new( + tool_registry, + "unknown.tool", + "Tâche inconnue", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TOOL_NOT_FOUND + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie le refus d'un outil non encore contrôlé. + */ +static void test_tool_task_tool_not_checked( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + GError *error = NULL; + + (void) user_data; + + tool_registry = test_tool_task_create_registry( + fixture, + TRUE, + FALSE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Tâche non vérifiée", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TOOL_NOT_CHECKED + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie le refus d'un outil explicitement absent. + */ +static void test_tool_task_tool_missing( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + GError *error = NULL; + + (void) user_data; + + /* + * Aucun fichier fake_tool n'est créé avant le rafraîchissement. + */ + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Tâche impossible", + NULL, + NULL, + &error + ); + + g_assert_null( + tool_task + ); + + g_assert_error( + error, + TOOL_TASK_ERROR, + TOOL_TASK_ERROR_TOOL_MISSING + ); + + g_clear_error( + &error + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie la création d'une ToolTask valide. + */ +static void test_tool_task_create( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + BackgroundTask *background_task = NULL; + + const char *arguments[] = + { + "premier argument", + "second", + NULL + }; + + GError *error = NULL; + + (void) user_data; + + tool_registry = test_tool_task_create_registry( + fixture, + TRUE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Exécution du faux outil", + arguments, + fixture->temporary_directory, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + tool_task + ); + + background_task = + tool_task_get_background_task( + tool_task + ); + + g_assert_nonnull( + background_task + ); + + g_assert_cmpstr( + background_task_get_title( + background_task + ), + ==, + "Exécution du faux outil" + ); + + g_assert_cmpint( + background_task_get_state( + background_task + ), + ==, + BACKGROUND_TASK_STATE_PENDING + ); + + g_assert_cmpfloat( + background_task_get_progress( + background_task + ), + ==, + 0.0 + ); + + /* + * Vérifie aussi la destruction d'une tâche jamais démarrée. + */ + tool_task_free( + tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Vérifie l'annulation d'un outil externe en cours. + */ +static void test_tool_task_cancellation( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + ToolTask *tool_task = NULL; + BackgroundTask *background_task = NULL; + + ToolTaskCancellationContext cancellation_context = {0}; + + GError *error = NULL; + + gboolean start_success = FALSE; + + (void) user_data; + + /* + * Boucle entièrement interne au shell. + * Aucun programme externe comme sleep n'est lancé. + */ + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "while :\n" + "do\n" + " :\n" + "done\n" + ); + + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + tool_task = tool_task_new( + tool_registry, + "test.fake", + "Annulation du faux outil", + NULL, + NULL, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + tool_task + ); + + background_task = + tool_task_get_background_task( + tool_task + ); + + cancellation_context.main_loop = + g_main_loop_new( + NULL, + FALSE + ); + + cancellation_context.background_task = + background_task; + + cancellation_context.cancel_source_id = + g_timeout_add( + 200, + test_tool_task_cancel_task, + &cancellation_context + ); + + cancellation_context.timeout_source_id = + g_timeout_add_seconds( + 5, + test_tool_task_cancellation_timeout, + &cancellation_context + ); + + start_success = tool_task_start( + tool_task, + test_tool_task_on_cancellation_completed, + &cancellation_context, + NULL, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_true( + start_success + ); + + g_main_loop_run( + cancellation_context.main_loop + ); + + if (cancellation_context.cancel_source_id != 0) + { + g_source_remove( + cancellation_context.cancel_source_id + ); + } + + if (cancellation_context.timeout_source_id != 0) + { + g_source_remove( + cancellation_context.timeout_source_id + ); + } + + g_main_loop_unref( + cancellation_context.main_loop + ); + + cancellation_context.main_loop = NULL; + + g_assert_false( + cancellation_context.timed_out + ); + + g_assert_true( + cancellation_context.completion_called + ); + + g_assert_cmpint( + background_task_get_state( + background_task + ), + ==, + BACKGROUND_TASK_STATE_CANCELLED + ); + + g_assert_null( + tool_task_result_from_background_task( + background_task + ) + ); + + tool_task_free( + tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +/** + * @brief Interrompt le test concurrent en cas de blocage. + */ +static gboolean test_tool_task_concurrent_timeout( + gpointer user_data +) +{ + ToolTaskConcurrentContext *concurrent_context = + user_data; + + if (concurrent_context == NULL) + { + return G_SOURCE_REMOVE; + } + + concurrent_context->timeout_source_id = 0; + concurrent_context->timed_out = TRUE; + + if (concurrent_context->main_loop != NULL) + { + g_main_loop_quit( + concurrent_context->main_loop + ); + } + + return G_SOURCE_REMOVE; +} + +/** + * @brief Compte les tâches concurrentes terminées. + */ +static void test_tool_task_on_concurrent_completed( + BackgroundTask *background_task, + gpointer user_data +) +{ + ToolTaskConcurrentContext *concurrent_context = + user_data; + + if (concurrent_context == NULL) + { + return; + } + + g_assert_true( + background_task == + concurrent_context->first_task || + background_task == + concurrent_context->second_task + ); + + concurrent_context->completed_count++; + + if (concurrent_context->completed_count < 2) + { + return; + } + + if (concurrent_context->timeout_source_id != 0) + { + g_source_remove( + concurrent_context->timeout_source_id + ); + + concurrent_context->timeout_source_id = 0; + } + + if (concurrent_context->main_loop != NULL) + { + g_main_loop_quit( + concurrent_context->main_loop + ); + } +} + +/** + * @brief Vérifie l'indépendance de deux ToolTask simultanées. + */ +static void test_tool_task_concurrent_tasks( + ToolTaskFixture *fixture, + gconstpointer user_data +) +{ + ToolRegistry *tool_registry = NULL; + + ToolTask *first_tool_task = NULL; + ToolTask *second_tool_task = NULL; + + BackgroundTask *first_background_task = NULL; + BackgroundTask *second_background_task = NULL; + + const ToolTaskResult *first_task_result = NULL; + const ToolTaskResult *second_task_result = NULL; + + const ToolProcessResult *first_process_result = NULL; + const ToolProcessResult *second_process_result = NULL; + + ToolTaskConcurrentContext concurrent_context = {0}; + + GBytes *first_stdout = NULL; + GBytes *second_stdout = NULL; + + const char *first_arguments[] = + { + "premiere-tache", + NULL + }; + + const char *second_arguments[] = + { + "seconde-tache", + NULL + }; + + GError *error = NULL; + + (void) user_data; + + test_tool_task_write_script( + fixture, + "#!/bin/sh\n" + "counter=0\n" + "while [ \"$counter\" -lt 10000 ]\n" + "do\n" + " counter=$((counter + 1))\n" + "done\n" + "printf '%s' \"$1\"\n" + ); + + tool_registry = test_tool_task_create_registry( + fixture, + FALSE, + TRUE + ); + + first_tool_task = tool_task_new( + tool_registry, + "test.fake", + "Première tâche", + first_arguments, + NULL, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + first_tool_task + ); + + second_tool_task = tool_task_new( + tool_registry, + "test.fake", + "Seconde tâche", + second_arguments, + NULL, + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + second_tool_task + ); + + first_background_task = + tool_task_get_background_task( + first_tool_task + ); + + second_background_task = + tool_task_get_background_task( + second_tool_task + ); + + concurrent_context.main_loop = + g_main_loop_new( + NULL, + FALSE + ); + + concurrent_context.first_task = + first_background_task; + + concurrent_context.second_task = + second_background_task; + + concurrent_context.timeout_source_id = + g_timeout_add_seconds( + 5, + test_tool_task_concurrent_timeout, + &concurrent_context + ); + + g_assert_true( + tool_task_start( + first_tool_task, + test_tool_task_on_concurrent_completed, + &concurrent_context, + NULL, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_assert_true( + tool_task_start( + second_tool_task, + test_tool_task_on_concurrent_completed, + &concurrent_context, + NULL, + &error + ) + ); + + g_assert_no_error( + error + ); + + g_main_loop_run( + concurrent_context.main_loop + ); + + if (concurrent_context.timeout_source_id != 0) + { + g_source_remove( + concurrent_context.timeout_source_id + ); + } + + g_main_loop_unref( + concurrent_context.main_loop + ); + + concurrent_context.main_loop = NULL; + + g_assert_false( + concurrent_context.timed_out + ); + + g_assert_cmpuint( + concurrent_context.completed_count, + ==, + 2 + ); + + g_assert_cmpint( + background_task_get_state( + first_background_task + ), + ==, + BACKGROUND_TASK_STATE_COMPLETED + ); + + g_assert_cmpint( + background_task_get_state( + second_background_task + ), + ==, + BACKGROUND_TASK_STATE_COMPLETED + ); + + first_task_result = + tool_task_result_from_background_task( + first_background_task + ); + + second_task_result = + tool_task_result_from_background_task( + second_background_task + ); + + g_assert_nonnull( + first_task_result + ); + + g_assert_nonnull( + second_task_result + ); + + g_assert_cmpstr( + tool_task_result_get_argument( + first_task_result, + 0 + ), + ==, + "premiere-tache" + ); + + g_assert_cmpstr( + tool_task_result_get_argument( + second_task_result, + 0 + ), + ==, + "seconde-tache" + ); + + first_process_result = + tool_task_result_get_process_result( + first_task_result + ); + + second_process_result = + tool_task_result_get_process_result( + second_task_result + ); + + first_stdout = + tool_process_result_ref_stdout( + first_process_result + ); + + second_stdout = + tool_process_result_ref_stdout( + second_process_result + ); + + test_tool_task_assert_bytes_equal_text( + first_stdout, + "premiere-tache" + ); + + test_tool_task_assert_bytes_equal_text( + second_stdout, + "seconde-tache" + ); + + g_bytes_unref( + first_stdout + ); + + g_bytes_unref( + second_stdout + ); + + tool_task_free( + first_tool_task + ); + + tool_task_free( + second_tool_task + ); + + tool_registry_free( + tool_registry + ); +} + +int main( + int argc, + char **argv +) +{ + GError *error = NULL; + int test_result = 0; + + g_test_init( + &argc, + &argv, + NULL + ); + test_tool_task_shared_directory = + g_dir_make_tmp( + "labfy-tool-task-XXXXXX", + &error + ); + + g_assert_no_error( + error + ); + + g_assert_nonnull( + test_tool_task_shared_directory + ); + + g_assert_true( + g_setenv( + "PATH", + test_tool_task_shared_directory, + TRUE + ) + ); + + g_test_add( + "/tool_task/invalid_arguments", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_invalid_arguments, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/tool_not_found", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_tool_not_found, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/tool_not_checked", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_tool_not_checked, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/tool_missing", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_tool_missing, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/create", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_create, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/argument_ownership", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_argument_ownership, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/success", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_success, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/nonzero_exit", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_nonzero_exit, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/spawn_failure", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_spawn_failure, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/working_directory", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_working_directory, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/cancellation", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_cancellation, + test_tool_task_fixture_teardown + ); + + g_test_add( + "/tool_task/concurrent_tasks", + ToolTaskFixture, + NULL, + test_tool_task_fixture_setup, + test_tool_task_concurrent_tasks, + test_tool_task_fixture_teardown + ); + + test_result = g_test_run(); + + g_rmdir( + test_tool_task_shared_directory + ); + + g_clear_pointer( + &test_tool_task_shared_directory, + g_free + ); + + return test_result; +}