diff --git a/README.md b/README.md index ccf00a9..0d04211 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,8 @@ La touche `S` fait défiler les tris par ordre d'import, nom ou taille, dans les deux directions. `/` filtre les noms d'images sans distinction de casse ASCII et `X` efface le filtre. Le tri et le filtre restent entièrement en mémoire et ne modifient ni le manifeste ni les images. + +Un moteur générique exécute les tâches en file FIFO sur un worker unique. Il +prend en charge progression, pause, reprise et annulation coopérative sans +dépendre de la TUI ni des modules métier. L'écran `F5` affiche l'état courant de +la file et de ses tâches. diff --git a/include/lardon3d/app_state.h b/include/lardon3d/app_state.h index e59d316..740d483 100644 --- a/include/lardon3d/app_state.h +++ b/include/lardon3d/app_state.h @@ -5,13 +5,15 @@ #include typedef struct Lardon3DImageCatalog Lardon3DImageCatalog; typedef struct Lardon3DImageView Lardon3DImageView; +typedef struct Lardon3DTaskQueue Lardon3DTaskQueue; typedef enum { LARDON3D_SCREEN_HOME = 0, LARDON3D_SCREEN_PROJECTS, LARDON3D_SCREEN_IMPORT, LARDON3D_SCREEN_VIEWER, - LARDON3D_SCREEN_HELP + LARDON3D_SCREEN_HELP, + LARDON3D_SCREEN_TASKS } Lardon3DScreen; typedef struct { @@ -23,6 +25,7 @@ typedef struct { char status_message[256]; Lardon3DImageCatalog *image_catalog; Lardon3DImageView *image_view; + Lardon3DTaskQueue *task_queue; } Lardon3DAppState; void lardon3d_app_state_init(Lardon3DAppState *state); diff --git a/include/lardon3d/layout.h b/include/lardon3d/layout.h index e65ceed..358e55d 100644 --- a/include/lardon3d/layout.h +++ b/include/lardon3d/layout.h @@ -3,12 +3,16 @@ #include #include +#include void lardon3d_layout_draw( const Lardon3DAppState *state, const char *input_text, const char *input_label, const Lardon3DImportTaskSnapshot *import_snapshot, + const Lardon3DTaskSnapshot *task_snapshots, + size_t task_count, + const Lardon3DTaskQueueSummary *task_summary, int rows, int cols ); diff --git a/include/lardon3d/task.h b/include/lardon3d/task.h new file mode 100644 index 0000000..2531c74 --- /dev/null +++ b/include/lardon3d/task.h @@ -0,0 +1,62 @@ +#ifndef LARDON3D_TASK_H +#define LARDON3D_TASK_H + +#include +#include +#include + +enum { + LARDON3D_TASK_NAME_CAPACITY = 128, + LARDON3D_TASK_MESSAGE_CAPACITY = 256, +}; + +typedef enum { + TASK_PENDING = 0, + TASK_RUNNING, + TASK_PAUSED, + TASK_CANCELLED, + TASK_FAILED, + TASK_COMPLETED +} Lardon3DTaskState; + +typedef struct Lardon3DTask Lardon3DTask; +typedef bool (*Lardon3DTaskCallback)(Lardon3DTask *task, void *userdata); + +typedef struct { + uint64_t id; + char name[LARDON3D_TASK_NAME_CAPACITY]; + unsigned int progress; + Lardon3DTaskState state; + char message[LARDON3D_TASK_MESSAGE_CAPACITY]; + struct timespec started_at; + struct timespec finished_at; +} Lardon3DTaskSnapshot; + +Lardon3DTask *lardon3d_task_create( + const char *name, + Lardon3DTaskCallback callback, + void *userdata +); +void lardon3d_task_destroy(Lardon3DTask *task); +/* Exécute le callback dans le thread appelant. */ +bool lardon3d_task_start(Lardon3DTask *task); +void lardon3d_task_request_cancel(Lardon3DTask *task); +bool lardon3d_task_pause(Lardon3DTask *task); +bool lardon3d_task_resume(Lardon3DTask *task); +bool lardon3d_task_join(Lardon3DTask *task); +bool lardon3d_task_checkpoint(Lardon3DTask *task); +bool lardon3d_task_set_progress( + Lardon3DTask *task, + unsigned int progress, + const char *message +); +bool lardon3d_task_fail(Lardon3DTask *task, const char *message); +bool lardon3d_task_snapshot( + const Lardon3DTask *task, + Lardon3DTaskSnapshot *snapshot +); +uint64_t lardon3d_task_id(const Lardon3DTask *task); +bool lardon3d_task_assign_id(Lardon3DTask *task, uint64_t id); +const char *lardon3d_task_state_name(Lardon3DTaskState state); + +#endif diff --git a/include/lardon3d/task_queue.h b/include/lardon3d/task_queue.h new file mode 100644 index 0000000..094592b --- /dev/null +++ b/include/lardon3d/task_queue.h @@ -0,0 +1,46 @@ +#ifndef LARDON3D_TASK_QUEUE_H +#define LARDON3D_TASK_QUEUE_H + +#include +#include +#include + +#include + +typedef struct Lardon3DTaskQueue Lardon3DTaskQueue; + +typedef struct { + size_t running; + size_t pending; + size_t completed; + size_t total; +} Lardon3DTaskQueueSummary; + +Lardon3DTaskQueue *lardon3d_task_queue_create(void); +void lardon3d_task_queue_destroy(Lardon3DTaskQueue *queue); +/* La file devient propriétaire de task uniquement en cas de succès. */ +bool lardon3d_task_queue_add( + Lardon3DTaskQueue *queue, + Lardon3DTask *task, + uint64_t *task_id +); +bool lardon3d_task_queue_remove(Lardon3DTaskQueue *queue, uint64_t task_id); +size_t lardon3d_task_queue_count(Lardon3DTaskQueue *queue); +bool lardon3d_task_queue_get( + Lardon3DTaskQueue *queue, + uint64_t task_id, + Lardon3DTaskSnapshot *snapshot +); +bool lardon3d_task_queue_get_at( + Lardon3DTaskQueue *queue, + size_t index, + Lardon3DTaskSnapshot *snapshot +); +size_t lardon3d_task_queue_snapshot( + Lardon3DTaskQueue *queue, + Lardon3DTaskSnapshot *snapshots, + size_t capacity, + Lardon3DTaskQueueSummary *summary +); + +#endif diff --git a/meson.build b/meson.build index 45e9937..29a7adf 100644 --- a/meson.build +++ b/meson.build @@ -34,6 +34,8 @@ executable( 'src/image_catalog.c', 'src/image_view.c', 'src/project.c', + 'src/task.c', + 'src/task_queue.c', ], include_directories: include_directories('include'), dependencies: [ncursesw, threads], @@ -89,3 +91,28 @@ image_view_test = executable( ) test('image-view', image_view_test, timeout: 30) + +task_test = executable( + 'test-task', + sources: [ + 'tests/test_task.c', + 'src/task.c', + ], + include_directories: include_directories('include'), + dependencies: [threads], +) + +test('task', task_test, timeout: 30) + +task_queue_test = executable( + 'test-task-queue', + sources: [ + 'tests/test_task_queue.c', + 'src/task.c', + 'src/task_queue.c', + ], + include_directories: include_directories('include'), + dependencies: [threads], +) + +test('task-queue', task_queue_test, timeout: 30) diff --git a/src/app.c b/src/app.c index 7f440be..e013383 100644 --- a/src/app.c +++ b/src/app.c @@ -5,6 +5,7 @@ #include #include #include +#include #include int @@ -16,8 +17,13 @@ lardon3d_app_run(void) if (!setlocale(LC_ALL, "")) { return EXIT_FAILURE; } + state.task_queue = lardon3d_task_queue_create(); + if (!state.task_queue) { + return EXIT_FAILURE; + } if (!lardon3d_tui_init()) { + lardon3d_task_queue_destroy(state.task_queue); return EXIT_FAILURE; } @@ -25,6 +31,7 @@ lardon3d_app_run(void) lardon3d_image_view_destroy(state.image_view); lardon3d_image_catalog_destroy(state.image_catalog); lardon3d_tui_shutdown(); + lardon3d_task_queue_destroy(state.task_queue); return success ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/src/layout.c b/src/layout.c index a3d05ba..82345ad 100644 --- a/src/layout.c +++ b/src/layout.c @@ -61,7 +61,7 @@ screen_texts( const char **footer ) { - *footer = "ESC Accueil F1 Aide F2 Projets F3 Import F4 Viewer Q Quit"; + *footer = "ESC Accueil F1 Aide F2 Projets F3 Import F4 Viewer F5 Tâches Q"; switch (screen) { case LARDON3D_SCREEN_PROJECTS: @@ -82,11 +82,16 @@ screen_texts( *title = "Aide"; *content = "Raccourcis clavier."; break; + case LARDON3D_SCREEN_TASKS: + *title = "Tâches"; + *content = "Tâches en arrière-plan"; + *footer = "F5 Tâches ESC Accueil Q Quit"; + break; case LARDON3D_SCREEN_HOME: default: *title = "Accueil"; *content = "Bienvenue dans Lardon3D"; - *footer = "F1 Aide F2 Projets F3 Import F4 Viewer Q Quit"; + *footer = "F1 Aide F2 Projets F3 Import F4 Viewer F5 Tâches Q Quit"; break; } } @@ -296,12 +301,58 @@ draw_import_screen( draw_catalog(state, rows, columns); } +static void +draw_tasks( + const Lardon3DTaskSnapshot *snapshots, + size_t count, + const Lardon3DTaskQueueSummary *summary, + int rows, + int columns +) +{ + char line[512]; + (void)snprintf( + line, + sizeof(line), + "Nombre de tâches : %zu En cours : %zu En attente : %zu Terminées : %zu", + summary->total, + summary->running, + summary->pending, + summary->completed + ); + draw_text(6, 2, columns - 4, line); + draw_text(8, 2, columns - 4, "ID Nom Etat Progression Message"); + int journal_row = rows - 7; + size_t visible = journal_row > 9 ? (size_t)(journal_row - 9) : 0; + if (count == 0) { + draw_text(10, 4, columns - 6, "Aucune tâche."); + return; + } + size_t displayed = count < visible ? count : visible; + for (size_t index = 0; index < displayed; ++index) { + (void)snprintf( + line, + sizeof(line), + "%-6llu %-20.20s %-12.12s %3u %% %s", + (unsigned long long)snapshots[index].id, + snapshots[index].name, + lardon3d_task_state_name(snapshots[index].state), + snapshots[index].progress, + snapshots[index].message + ); + draw_text(9 + (int)index, 2, columns - 4, line); + } +} + static void draw_content( const Lardon3DAppState *state, const char *input_text, const char *input_label, const Lardon3DImportTaskSnapshot *import_snapshot, + const Lardon3DTaskSnapshot *task_snapshots, + size_t task_count, + const Lardon3DTaskQueueSummary *task_summary, int rows, int columns ) @@ -343,6 +394,14 @@ draw_content( rows, columns ); + } else if (state->screen == LARDON3D_SCREEN_TASKS) { + draw_tasks( + task_snapshots, + task_count, + task_summary, + rows, + columns + ); } else { draw_text( (3 + journal_row) / 2, @@ -367,6 +426,9 @@ lardon3d_layout_draw( const char *input_text, const char *input_label, const Lardon3DImportTaskSnapshot *import_snapshot, + const Lardon3DTaskSnapshot *task_snapshots, + size_t task_count, + const Lardon3DTaskQueueSummary *task_summary, int rows, int columns ) @@ -382,6 +444,9 @@ lardon3d_layout_draw( input_text, input_label, import_snapshot, + task_snapshots, + task_count, + task_summary, rows, columns ); diff --git a/src/task.c b/src/task.c new file mode 100644 index 0000000..740e8f7 --- /dev/null +++ b/src/task.c @@ -0,0 +1,355 @@ +#include +#include +#include +#include + +#include + +struct Lardon3DTask { + pthread_mutex_t mutex; + pthread_cond_t condition; + uint64_t id; + char name[LARDON3D_TASK_NAME_CAPACITY]; + unsigned int progress; + Lardon3DTaskState state; + char message[LARDON3D_TASK_MESSAGE_CAPACITY]; + struct timespec started_at; + struct timespec finished_at; + Lardon3DTaskCallback callback; + void *userdata; + bool pause_requested; + bool cancel_requested; + bool executing; +}; + +static bool +is_terminal(Lardon3DTaskState state) +{ + return state == TASK_CANCELLED || state == TASK_FAILED + || state == TASK_COMPLETED; +} + +static void +copy_text(char *destination, size_t capacity, const char *text) +{ + (void)snprintf(destination, capacity, "%s", text ? text : ""); +} + +static void +finish_locked( + Lardon3DTask *task, + Lardon3DTaskState state, + const char *message +) +{ + task->state = state; + if (state == TASK_COMPLETED) { + task->progress = 100; + } + if (message) { + copy_text(task->message, sizeof(task->message), message); + } + (void)clock_gettime(CLOCK_REALTIME, &task->finished_at); + task->executing = false; + (void)pthread_cond_broadcast(&task->condition); +} + +Lardon3DTask * +lardon3d_task_create( + const char *name, + Lardon3DTaskCallback callback, + void *userdata +) +{ + if (!name || !name[0] || !callback) { + return NULL; + } + Lardon3DTask *task = calloc(1, sizeof(*task)); + if (!task) { + return NULL; + } + int written = snprintf(task->name, sizeof(task->name), "%s", name); + if (written < 0 || (size_t)written >= sizeof(task->name) + || pthread_mutex_init(&task->mutex, NULL) != 0) { + free(task); + return NULL; + } + if (pthread_cond_init(&task->condition, NULL) != 0) { + (void)pthread_mutex_destroy(&task->mutex); + free(task); + return NULL; + } + task->state = TASK_PENDING; + task->callback = callback; + task->userdata = userdata; + copy_text(task->message, sizeof(task->message), "En attente."); + return task; +} + +void +lardon3d_task_destroy(Lardon3DTask *task) +{ + if (!task) { + return; + } + lardon3d_task_request_cancel(task); + (void)lardon3d_task_join(task); + (void)pthread_cond_destroy(&task->condition); + (void)pthread_mutex_destroy(&task->mutex); + free(task); +} + +bool +lardon3d_task_start(Lardon3DTask *task) +{ + if (!task) { + return false; + } + (void)pthread_mutex_lock(&task->mutex); + if (task->executing || is_terminal(task->state)) { + (void)pthread_mutex_unlock(&task->mutex); + return false; + } + task->executing = true; + (void)clock_gettime(CLOCK_REALTIME, &task->started_at); + if (task->cancel_requested) { + finish_locked(task, TASK_CANCELLED, "Tâche annulée."); + (void)pthread_mutex_unlock(&task->mutex); + return true; + } + while (task->pause_requested && !task->cancel_requested) { + task->state = TASK_PAUSED; + copy_text(task->message, sizeof(task->message), "Tâche en pause."); + (void)pthread_cond_wait(&task->condition, &task->mutex); + } + if (task->cancel_requested) { + finish_locked(task, TASK_CANCELLED, "Tâche annulée."); + (void)pthread_mutex_unlock(&task->mutex); + return true; + } + task->state = TASK_RUNNING; + copy_text(task->message, sizeof(task->message), "Tâche en cours."); + (void)pthread_mutex_unlock(&task->mutex); + + bool succeeded = task->callback(task, task->userdata); + + (void)pthread_mutex_lock(&task->mutex); + if (task->cancel_requested) { + finish_locked(task, TASK_CANCELLED, "Tâche annulée."); + } else if (task->state == TASK_FAILED || !succeeded) { + finish_locked( + task, + TASK_FAILED, + task->message[0] ? NULL : "Échec de la tâche." + ); + } else { + finish_locked(task, TASK_COMPLETED, "Tâche terminée."); + } + (void)pthread_mutex_unlock(&task->mutex); + return true; +} + +void +lardon3d_task_request_cancel(Lardon3DTask *task) +{ + if (!task) { + return; + } + (void)pthread_mutex_lock(&task->mutex); + if (!is_terminal(task->state)) { + task->cancel_requested = true; + copy_text(task->message, sizeof(task->message), "Annulation demandée."); + if (!task->executing) { + finish_locked(task, TASK_CANCELLED, "Tâche annulée."); + } + (void)pthread_cond_broadcast(&task->condition); + } + (void)pthread_mutex_unlock(&task->mutex); +} + +bool +lardon3d_task_pause(Lardon3DTask *task) +{ + if (!task) { + return false; + } + (void)pthread_mutex_lock(&task->mutex); + bool accepted = !is_terminal(task->state) && !task->cancel_requested; + if (accepted) { + task->pause_requested = true; + if (!task->executing) { + task->state = TASK_PAUSED; + copy_text(task->message, sizeof(task->message), "Tâche en pause."); + } + } + (void)pthread_mutex_unlock(&task->mutex); + return accepted; +} + +bool +lardon3d_task_resume(Lardon3DTask *task) +{ + if (!task) { + return false; + } + (void)pthread_mutex_lock(&task->mutex); + bool accepted = !is_terminal(task->state) && task->pause_requested; + if (accepted) { + task->pause_requested = false; + if (!task->executing) { + task->state = TASK_PENDING; + copy_text(task->message, sizeof(task->message), "En attente."); + } + (void)pthread_cond_broadcast(&task->condition); + } + (void)pthread_mutex_unlock(&task->mutex); + return accepted; +} + +bool +lardon3d_task_join(Lardon3DTask *task) +{ + if (!task) { + return false; + } + (void)pthread_mutex_lock(&task->mutex); + while (task->executing) { + (void)pthread_cond_wait(&task->condition, &task->mutex); + } + bool terminal = is_terminal(task->state); + (void)pthread_mutex_unlock(&task->mutex); + return terminal; +} + +bool +lardon3d_task_checkpoint(Lardon3DTask *task) +{ + if (!task) { + return false; + } + (void)pthread_mutex_lock(&task->mutex); + while (task->pause_requested && !task->cancel_requested) { + task->state = TASK_PAUSED; + copy_text(task->message, sizeof(task->message), "Tâche en pause."); + (void)pthread_cond_broadcast(&task->condition); + (void)pthread_cond_wait(&task->condition, &task->mutex); + } + if (!task->cancel_requested && task->executing) { + task->state = TASK_RUNNING; + } + bool continuing = !task->cancel_requested; + (void)pthread_mutex_unlock(&task->mutex); + return continuing; +} + +bool +lardon3d_task_set_progress( + Lardon3DTask *task, + unsigned int progress, + const char *message +) +{ + if (!task || progress > 100) { + return false; + } + (void)pthread_mutex_lock(&task->mutex); + bool accepted = !is_terminal(task->state); + if (accepted) { + task->progress = progress; + if (message) { + copy_text(task->message, sizeof(task->message), message); + } + } + (void)pthread_mutex_unlock(&task->mutex); + return accepted; +} + +bool +lardon3d_task_fail(Lardon3DTask *task, const char *message) +{ + if (!task) { + return false; + } + (void)pthread_mutex_lock(&task->mutex); + bool accepted = task->executing && !is_terminal(task->state); + if (accepted) { + task->state = TASK_FAILED; + copy_text( + task->message, + sizeof(task->message), + message ? message : "Échec de la tâche." + ); + } + (void)pthread_mutex_unlock(&task->mutex); + return accepted; +} + +bool +lardon3d_task_snapshot( + const Lardon3DTask *task, + Lardon3DTaskSnapshot *snapshot +) +{ + if (!task || !snapshot) { + return false; + } + Lardon3DTask *mutable_task = (Lardon3DTask *)task; + (void)pthread_mutex_lock(&mutable_task->mutex); + *snapshot = (Lardon3DTaskSnapshot) { + .id = task->id, + .progress = task->progress, + .state = task->state, + .started_at = task->started_at, + .finished_at = task->finished_at, + }; + copy_text(snapshot->name, sizeof(snapshot->name), task->name); + copy_text(snapshot->message, sizeof(snapshot->message), task->message); + (void)pthread_mutex_unlock(&mutable_task->mutex); + return true; +} + +uint64_t +lardon3d_task_id(const Lardon3DTask *task) +{ + if (!task) { + return 0; + } + Lardon3DTaskSnapshot snapshot; + return lardon3d_task_snapshot(task, &snapshot) ? snapshot.id : 0; +} + +bool +lardon3d_task_assign_id(Lardon3DTask *task, uint64_t id) +{ + if (!task || id == 0) { + return false; + } + (void)pthread_mutex_lock(&task->mutex); + bool accepted = task->id == 0 && task->state == TASK_PENDING; + if (accepted) { + task->id = id; + } + (void)pthread_mutex_unlock(&task->mutex); + return accepted; +} + +const char * +lardon3d_task_state_name(Lardon3DTaskState state) +{ + switch (state) { + case TASK_PENDING: + return "En attente"; + case TASK_RUNNING: + return "En cours"; + case TASK_PAUSED: + return "En pause"; + case TASK_CANCELLED: + return "Annulée"; + case TASK_FAILED: + return "Échec"; + case TASK_COMPLETED: + return "Terminée"; + default: + return "Inconnu"; + } +} diff --git a/src/task_queue.c b/src/task_queue.c new file mode 100644 index 0000000..8a89201 --- /dev/null +++ b/src/task_queue.c @@ -0,0 +1,309 @@ +#include +#include +#include +#include + +#include + +typedef struct TaskNode { + Lardon3DTask *task; + struct TaskNode *next_all; + struct TaskNode *next_pending; +} TaskNode; + +struct Lardon3DTaskQueue { + pthread_mutex_t mutex; + pthread_cond_t condition; + pthread_t worker; + bool worker_started; + bool stopping; + uint64_t next_id; + TaskNode *all_head; + TaskNode *all_tail; + TaskNode *pending_head; + TaskNode *pending_tail; + Lardon3DTask *active; + size_t count; +}; + +static bool +terminal_state(Lardon3DTaskState state) +{ + return state == TASK_CANCELLED || state == TASK_FAILED + || state == TASK_COMPLETED; +} + +static void * +queue_worker(void *context) +{ + Lardon3DTaskQueue *queue = context; + for (;;) { + (void)pthread_mutex_lock(&queue->mutex); + while (!queue->stopping && !queue->pending_head) { + (void)pthread_cond_wait(&queue->condition, &queue->mutex); + } + if (queue->stopping) { + (void)pthread_mutex_unlock(&queue->mutex); + return NULL; + } + TaskNode *node = queue->pending_head; + queue->pending_head = node->next_pending; + if (!queue->pending_head) { + queue->pending_tail = NULL; + } + node->next_pending = NULL; + queue->active = node->task; + (void)pthread_mutex_unlock(&queue->mutex); + + (void)lardon3d_task_start(node->task); + + (void)pthread_mutex_lock(&queue->mutex); + queue->active = NULL; + (void)pthread_cond_broadcast(&queue->condition); + (void)pthread_mutex_unlock(&queue->mutex); + } +} + +Lardon3DTaskQueue * +lardon3d_task_queue_create(void) +{ + Lardon3DTaskQueue *queue = calloc(1, sizeof(*queue)); + if (!queue) { + return NULL; + } + if (pthread_mutex_init(&queue->mutex, NULL) != 0) { + free(queue); + return NULL; + } + if (pthread_cond_init(&queue->condition, NULL) != 0) { + (void)pthread_mutex_destroy(&queue->mutex); + free(queue); + return NULL; + } + queue->next_id = 1; + if (pthread_create(&queue->worker, NULL, queue_worker, queue) != 0) { + (void)pthread_cond_destroy(&queue->condition); + (void)pthread_mutex_destroy(&queue->mutex); + free(queue); + return NULL; + } + queue->worker_started = true; + return queue; +} + +void +lardon3d_task_queue_destroy(Lardon3DTaskQueue *queue) +{ + if (!queue) { + return; + } + (void)pthread_mutex_lock(&queue->mutex); + queue->stopping = true; + for (TaskNode *node = queue->all_head; node; node = node->next_all) { + lardon3d_task_request_cancel(node->task); + } + (void)pthread_cond_broadcast(&queue->condition); + (void)pthread_mutex_unlock(&queue->mutex); + if (queue->worker_started) { + (void)pthread_join(queue->worker, NULL); + } + TaskNode *node = queue->all_head; + while (node) { + TaskNode *next = node->next_all; + lardon3d_task_destroy(node->task); + free(node); + node = next; + } + (void)pthread_cond_destroy(&queue->condition); + (void)pthread_mutex_destroy(&queue->mutex); + free(queue); +} + +bool +lardon3d_task_queue_add( + Lardon3DTaskQueue *queue, + Lardon3DTask *task, + uint64_t *task_id +) +{ + if (!queue || !task) { + return false; + } + TaskNode *node = calloc(1, sizeof(*node)); + if (!node) { + return false; + } + (void)pthread_mutex_lock(&queue->mutex); + if (queue->stopping || queue->next_id == 0 + || !lardon3d_task_assign_id(task, queue->next_id)) { + (void)pthread_mutex_unlock(&queue->mutex); + free(node); + return false; + } + uint64_t id = queue->next_id++; + node->task = task; + if (queue->all_tail) { + queue->all_tail->next_all = node; + } else { + queue->all_head = node; + } + queue->all_tail = node; + if (queue->pending_tail) { + queue->pending_tail->next_pending = node; + } else { + queue->pending_head = node; + } + queue->pending_tail = node; + ++queue->count; + if (task_id) { + *task_id = id; + } + (void)pthread_cond_signal(&queue->condition); + (void)pthread_mutex_unlock(&queue->mutex); + return true; +} + +bool +lardon3d_task_queue_remove(Lardon3DTaskQueue *queue, uint64_t task_id) +{ + if (!queue || task_id == 0) { + return false; + } + (void)pthread_mutex_lock(&queue->mutex); + TaskNode *previous = NULL; + TaskNode *node = queue->all_head; + while (node && lardon3d_task_id(node->task) != task_id) { + previous = node; + node = node->next_all; + } + Lardon3DTaskSnapshot snapshot; + if (!node || !lardon3d_task_snapshot(node->task, &snapshot) + || !terminal_state(snapshot.state)) { + (void)pthread_mutex_unlock(&queue->mutex); + return false; + } + while (node->task == queue->active) { + (void)pthread_cond_wait(&queue->condition, &queue->mutex); + } + if (previous) { + previous->next_all = node->next_all; + } else { + queue->all_head = node->next_all; + } + if (queue->all_tail == node) { + queue->all_tail = previous; + } + TaskNode *pending_previous = NULL; + TaskNode *pending = queue->pending_head; + while (pending && pending != node) { + pending_previous = pending; + pending = pending->next_pending; + } + if (pending) { + if (pending_previous) { + pending_previous->next_pending = pending->next_pending; + } else { + queue->pending_head = pending->next_pending; + } + if (queue->pending_tail == pending) { + queue->pending_tail = pending_previous; + } + } + --queue->count; + (void)pthread_mutex_unlock(&queue->mutex); + lardon3d_task_destroy(node->task); + free(node); + return true; +} + +size_t +lardon3d_task_queue_count(Lardon3DTaskQueue *queue) +{ + if (!queue) { + return 0; + } + (void)pthread_mutex_lock(&queue->mutex); + size_t count = queue->count; + (void)pthread_mutex_unlock(&queue->mutex); + return count; +} + +bool +lardon3d_task_queue_get( + Lardon3DTaskQueue *queue, + uint64_t task_id, + Lardon3DTaskSnapshot *snapshot +) +{ + if (!queue || task_id == 0 || !snapshot) { + return false; + } + (void)pthread_mutex_lock(&queue->mutex); + TaskNode *node = queue->all_head; + while (node && lardon3d_task_id(node->task) != task_id) { + node = node->next_all; + } + bool found = node && lardon3d_task_snapshot(node->task, snapshot); + (void)pthread_mutex_unlock(&queue->mutex); + return found; +} + +bool +lardon3d_task_queue_get_at( + Lardon3DTaskQueue *queue, + size_t index, + Lardon3DTaskSnapshot *snapshot +) +{ + if (!queue || !snapshot) { + return false; + } + (void)pthread_mutex_lock(&queue->mutex); + TaskNode *node = queue->all_head; + while (node && index > 0) { + node = node->next_all; + --index; + } + bool found = node && lardon3d_task_snapshot(node->task, snapshot); + (void)pthread_mutex_unlock(&queue->mutex); + return found; +} + +size_t +lardon3d_task_queue_snapshot( + Lardon3DTaskQueue *queue, + Lardon3DTaskSnapshot *snapshots, + size_t capacity, + Lardon3DTaskQueueSummary *summary +) +{ + if (summary) { + *summary = (Lardon3DTaskQueueSummary) {0}; + } + if (!queue || (!snapshots && capacity > 0)) { + return 0; + } + (void)pthread_mutex_lock(&queue->mutex); + size_t copied = 0; + for (TaskNode *node = queue->all_head; node; node = node->next_all) { + Lardon3DTaskSnapshot snapshot; + if (!lardon3d_task_snapshot(node->task, &snapshot)) { + continue; + } + if (summary) { + ++summary->total; + if (snapshot.state == TASK_RUNNING || snapshot.state == TASK_PAUSED) { + ++summary->running; + } else if (snapshot.state == TASK_PENDING) { + ++summary->pending; + } else { + ++summary->completed; + } + } + if (copied < capacity) { + snapshots[copied++] = snapshot; + } + } + (void)pthread_mutex_unlock(&queue->mutex); + return copied; +} diff --git a/src/tui.c b/src/tui.c index 5988402..46a83a1 100644 --- a/src/tui.c +++ b/src/tui.c @@ -10,11 +10,13 @@ #include #include #include +#include #include enum { MINIMUM_ROWS = 20, MINIMUM_COLUMNS = 72, + DISPLAYED_TASK_CAPACITY = 64, }; typedef enum { @@ -170,11 +172,22 @@ redraw( if (task && lardon3d_import_task_snapshot(task, &snapshot)) { displayed_snapshot = &snapshot; } + Lardon3DTaskSnapshot task_snapshots[DISPLAYED_TASK_CAPACITY]; + Lardon3DTaskQueueSummary task_summary; + size_t task_count = lardon3d_task_queue_snapshot( + state->task_queue, + task_snapshots, + DISPLAYED_TASK_CAPACITY, + &task_summary + ); lardon3d_layout_draw( state, text, label, displayed_snapshot, + task_snapshots, + task_count, + &task_summary, rows, columns ); @@ -396,6 +409,9 @@ handle_normal_input( case KEY_F(4): state->screen = LARDON3D_SCREEN_VIEWER; return true; + case KEY_F(5): + state->screen = LARDON3D_SCREEN_TASKS; + return true; case 27: state->screen = LARDON3D_SCREEN_HOME; return true; @@ -634,7 +650,8 @@ lardon3d_tui_run(Lardon3DAppState *state) while (state->running) { int key = getch(); - bool should_redraw = task != NULL; + bool should_redraw = task != NULL + || state->screen == LARDON3D_SCREEN_TASKS; if (task && lardon3d_import_task_is_finished(task)) { Lardon3DImportTaskSnapshot snapshot; if (!lardon3d_import_task_join(task) diff --git a/tests/test_task.c b/tests/test_task.c new file mode 100644 index 0000000..bc43cd3 --- /dev/null +++ b/tests/test_task.c @@ -0,0 +1,156 @@ +#include +#include +#include +#include +#include +#include + +#include + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + (void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \ + return false; \ + } \ + } while (0) + +typedef struct { + size_t steps; + long pause_ns; +} Work; + +static void +short_pause(long nanoseconds) +{ + struct timespec duration = {.tv_sec = 0, .tv_nsec = nanoseconds}; + (void)nanosleep(&duration, NULL); +} + +static bool +work_callback(Lardon3DTask *task, void *userdata) +{ + Work *work = userdata; + for (size_t step = 0; step < work->steps; ++step) { + if (!lardon3d_task_checkpoint(task)) { + return false; + } + unsigned int progress = (unsigned int)(((step + 1) * 100) / work->steps); + if (!lardon3d_task_set_progress(task, progress, "Traitement.")) { + return false; + } + short_pause(work->pause_ns); + } + return true; +} + +static bool +failure_callback(Lardon3DTask *task, void *userdata) +{ + (void)userdata; + return lardon3d_task_fail(task, "Erreur contrôlée.") && false; +} + +static void * +start_task(void *context) +{ + Lardon3DTask *task = context; + return (void *)(uintptr_t)(lardon3d_task_start(task) ? 1 : 0); +} + +static bool +wait_for_state(Lardon3DTask *task, Lardon3DTaskState expected) +{ + for (size_t attempt = 0; attempt < 5000; ++attempt) { + Lardon3DTaskSnapshot snapshot; + if (!lardon3d_task_snapshot(task, &snapshot)) { + return false; + } + if (snapshot.state == expected) { + return true; + } + short_pause(1000000); + } + return false; +} + +static bool +run_test(void) +{ + CHECK(!lardon3d_task_create(NULL, work_callback, NULL)); + CHECK(!lardon3d_task_create("", work_callback, NULL)); + CHECK(!lardon3d_task_create("invalide", NULL, NULL)); + lardon3d_task_destroy(NULL); + CHECK(!lardon3d_task_join(NULL)); + + Work work = {.steps = 100, .pause_ns = 1000000}; + Lardon3DTask *task = lardon3d_task_create("Tâche de test", work_callback, &work); + CHECK(task); + CHECK(lardon3d_task_assign_id(task, 42)); + CHECK(!lardon3d_task_assign_id(task, 43)); + CHECK(lardon3d_task_id(task) == 42); + Lardon3DTaskSnapshot snapshot; + CHECK(lardon3d_task_snapshot(task, &snapshot)); + CHECK(snapshot.state == TASK_PENDING); + CHECK(snapshot.progress == 0); + CHECK(strcmp(snapshot.name, "Tâche de test") == 0); + + pthread_t thread; + CHECK(pthread_create(&thread, NULL, start_task, task) == 0); + CHECK(wait_for_state(task, TASK_RUNNING)); + CHECK(lardon3d_task_pause(task)); + CHECK(wait_for_state(task, TASK_PAUSED)); + CHECK(lardon3d_task_snapshot(task, &snapshot)); + unsigned int paused_progress = snapshot.progress; + short_pause(5000000); + CHECK(lardon3d_task_snapshot(task, &snapshot)); + CHECK(snapshot.progress == paused_progress); + CHECK(lardon3d_task_resume(task)); + CHECK(wait_for_state(task, TASK_RUNNING)); + CHECK(lardon3d_task_join(task)); + void *thread_result; + CHECK(pthread_join(thread, &thread_result) == 0); + CHECK((uintptr_t)thread_result == 1); + CHECK(lardon3d_task_snapshot(task, &snapshot)); + CHECK(snapshot.state == TASK_COMPLETED); + CHECK(snapshot.progress == 100); + CHECK(snapshot.started_at.tv_sec > 0); + CHECK(snapshot.finished_at.tv_sec > 0); + CHECK(!lardon3d_task_start(task)); + lardon3d_task_destroy(task); + + work = (Work) {.steps = 1000, .pause_ns = 1000000}; + task = lardon3d_task_create("Annulation", work_callback, &work); + CHECK(task && pthread_create(&thread, NULL, start_task, task) == 0); + CHECK(wait_for_state(task, TASK_RUNNING)); + lardon3d_task_request_cancel(task); + CHECK(lardon3d_task_join(task)); + CHECK(pthread_join(thread, NULL) == 0); + CHECK(lardon3d_task_snapshot(task, &snapshot)); + CHECK(snapshot.state == TASK_CANCELLED); + CHECK(snapshot.progress < 100); + lardon3d_task_destroy(task); + + task = lardon3d_task_create("Échec", failure_callback, NULL); + CHECK(task && lardon3d_task_start(task)); + CHECK(lardon3d_task_snapshot(task, &snapshot)); + CHECK(snapshot.state == TASK_FAILED); + CHECK(strcmp(snapshot.message, "Erreur contrôlée.") == 0); + lardon3d_task_destroy(task); + + task = lardon3d_task_create("Pause avant départ", work_callback, &work); + CHECK(task && lardon3d_task_pause(task)); + CHECK(lardon3d_task_snapshot(task, &snapshot)); + CHECK(snapshot.state == TASK_PAUSED); + CHECK(lardon3d_task_resume(task)); + lardon3d_task_request_cancel(task); + CHECK(lardon3d_task_join(task)); + lardon3d_task_destroy(task); + return true; +} + +int +main(void) +{ + return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_task_queue.c b/tests/test_task_queue.c new file mode 100644 index 0000000..31dc9fa --- /dev/null +++ b/tests/test_task_queue.c @@ -0,0 +1,196 @@ +#include +#include +#include +#include +#include + +#include + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + (void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \ + return false; \ + } \ + } while (0) + +enum { + TASK_COUNT = 400, +}; + +typedef struct { + pthread_mutex_t mutex; + size_t order[TASK_COUNT]; + size_t count; +} OrderLog; + +typedef struct { + OrderLog *log; + size_t value; + size_t steps; +} QueueWork; + +static void +short_pause(void) +{ + const struct timespec duration = {.tv_sec = 0, .tv_nsec = 1000000}; + (void)nanosleep(&duration, NULL); +} + +static bool +queue_callback(Lardon3DTask *task, void *userdata) +{ + QueueWork *work = userdata; + (void)pthread_mutex_lock(&work->log->mutex); + work->log->order[work->log->count++] = work->value; + (void)pthread_mutex_unlock(&work->log->mutex); + for (size_t step = 0; step < work->steps; ++step) { + if (!lardon3d_task_checkpoint(task)) { + return false; + } + unsigned int progress = (unsigned int)(((step + 1) * 100) / work->steps); + (void)lardon3d_task_set_progress(task, progress, "File en cours."); + if (work->steps > 1) { + short_pause(); + } + } + return true; +} + +static bool +wait_terminal(Lardon3DTaskQueue *queue, uint64_t id, Lardon3DTaskSnapshot *result) +{ + for (size_t attempt = 0; attempt < 10000; ++attempt) { + if (!lardon3d_task_queue_get(queue, id, result)) { + return false; + } + if (result->state == TASK_CANCELLED || result->state == TASK_FAILED + || result->state == TASK_COMPLETED) { + return true; + } + short_pause(); + } + return false; +} + +static bool +run_test(void) +{ + lardon3d_task_queue_destroy(NULL); + CHECK(lardon3d_task_queue_count(NULL) == 0); + Lardon3DTaskQueue *queue = lardon3d_task_queue_create(); + CHECK(queue); + short_pause(); + + OrderLog log = {0}; + CHECK(pthread_mutex_init(&log.mutex, NULL) == 0); + QueueWork work[TASK_COUNT]; + Lardon3DTask *tasks[TASK_COUNT]; + uint64_t ids[TASK_COUNT]; + for (size_t index = 0; index < TASK_COUNT; ++index) { + work[index] = (QueueWork) { + .log = &log, + .value = index, + .steps = 1, + }; + tasks[index] = lardon3d_task_create("FIFO", queue_callback, &work[index]); + CHECK(tasks[index]); + CHECK(lardon3d_task_queue_add(queue, tasks[index], &ids[index])); + CHECK(ids[index] == index + 1); + } + CHECK(lardon3d_task_queue_count(queue) == TASK_COUNT); + Lardon3DTaskSnapshot snapshot; + CHECK(wait_terminal(queue, ids[TASK_COUNT - 1], &snapshot)); + CHECK(snapshot.state == TASK_COMPLETED); + CHECK(log.count == TASK_COUNT); + for (size_t index = 0; index < TASK_COUNT; ++index) { + CHECK(log.order[index] == index); + } + Lardon3DTaskQueueSummary summary; + Lardon3DTaskSnapshot listed[8]; + CHECK(lardon3d_task_queue_snapshot(queue, listed, 8, &summary) == 8); + CHECK(summary.total == TASK_COUNT); + CHECK(summary.running == 0); + CHECK(summary.pending == 0); + CHECK(summary.completed == TASK_COUNT); + CHECK(lardon3d_task_queue_get_at(queue, 0, &snapshot)); + CHECK(snapshot.id == ids[0]); + CHECK(!lardon3d_task_queue_get_at(queue, TASK_COUNT, &snapshot)); + CHECK(lardon3d_task_queue_remove(queue, ids[0])); + CHECK(!lardon3d_task_queue_get(queue, ids[0], &snapshot)); + CHECK(lardon3d_task_queue_count(queue) == TASK_COUNT - 1); + lardon3d_task_queue_destroy(queue); + CHECK(pthread_mutex_destroy(&log.mutex) == 0); + + queue = lardon3d_task_queue_create(); + CHECK(queue); + OrderLog control_log = {0}; + CHECK(pthread_mutex_init(&control_log.mutex, NULL) == 0); + QueueWork slow = {.log = &control_log, .value = 1, .steps = 500}; + QueueWork cancelled = {.log = &control_log, .value = 2, .steps = 1}; + Lardon3DTask *slow_task = lardon3d_task_create("Longue", queue_callback, &slow); + Lardon3DTask *cancelled_task = lardon3d_task_create( + "Annulée en attente", + queue_callback, + &cancelled + ); + uint64_t slow_id; + uint64_t cancelled_id; + CHECK(slow_task && cancelled_task); + CHECK(lardon3d_task_queue_add(queue, slow_task, &slow_id)); + CHECK(lardon3d_task_queue_add(queue, cancelled_task, &cancelled_id)); + for (size_t attempt = 0; attempt < 1000; ++attempt) { + CHECK(lardon3d_task_queue_get(queue, slow_id, &snapshot)); + if (snapshot.state == TASK_RUNNING) { + break; + } + short_pause(); + } + CHECK(lardon3d_task_pause(slow_task)); + for (size_t attempt = 0; attempt < 1000; ++attempt) { + CHECK(lardon3d_task_queue_get(queue, slow_id, &snapshot)); + if (snapshot.state == TASK_PAUSED) { + break; + } + short_pause(); + } + CHECK(snapshot.state == TASK_PAUSED); + lardon3d_task_request_cancel(cancelled_task); + CHECK(lardon3d_task_resume(slow_task)); + CHECK(wait_terminal(queue, slow_id, &snapshot)); + CHECK(snapshot.state == TASK_COMPLETED); + CHECK(wait_terminal(queue, cancelled_id, &snapshot)); + CHECK(snapshot.state == TASK_CANCELLED); + CHECK(control_log.count == 1); + CHECK(lardon3d_task_queue_remove(queue, cancelled_id)); + CHECK(lardon3d_task_queue_remove(queue, slow_id)); + lardon3d_task_queue_destroy(queue); + CHECK(pthread_mutex_destroy(&control_log.mutex) == 0); + + queue = lardon3d_task_queue_create(); + CHECK(queue); + OrderLog destruction_log = {0}; + CHECK(pthread_mutex_init(&destruction_log.mutex, NULL) == 0); + QueueWork destruction_work = { + .log = &destruction_log, + .value = 0, + .steps = 10000, + }; + Lardon3DTask *destruction_task = lardon3d_task_create( + "Destruction sûre", + queue_callback, + &destruction_work + ); + CHECK(destruction_task); + CHECK(lardon3d_task_queue_add(queue, destruction_task, NULL)); + short_pause(); + lardon3d_task_queue_destroy(queue); + CHECK(pthread_mutex_destroy(&destruction_log.mutex) == 0); + return true; +} + +int +main(void) +{ + return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; +}