feat: add generic task engine and FIFO scheduler

This commit is contained in:
fy59 2026-08-06 20:27:49 +02:00
parent 2fe594cc51
commit 030994696b
13 changed files with 1256 additions and 4 deletions

View file

@ -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.

View file

@ -5,13 +5,15 @@
#include <limits.h>
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);

View file

@ -3,12 +3,16 @@
#include <lardon3d/app_state.h>
#include <lardon3d/import_task.h>
#include <lardon3d/task_queue.h>
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
);

62
include/lardon3d/task.h Normal file
View file

@ -0,0 +1,62 @@
#ifndef LARDON3D_TASK_H
#define LARDON3D_TASK_H
#include <stdbool.h>
#include <stdint.h>
#include <time.h>
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

View file

@ -0,0 +1,46 @@
#ifndef LARDON3D_TASK_QUEUE_H
#define LARDON3D_TASK_QUEUE_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/task.h>
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

View file

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

View file

@ -5,6 +5,7 @@
#include <lardon3d/app_state.h>
#include <lardon3d/image_catalog.h>
#include <lardon3d/image_view.h>
#include <lardon3d/task_queue.h>
#include <lardon3d/tui.h>
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;
}

View file

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

355
src/task.c Normal file
View file

@ -0,0 +1,355 @@
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <lardon3d/task.h>
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";
}
}

309
src/task_queue.c Normal file
View file

@ -0,0 +1,309 @@
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <lardon3d/task_queue.h>
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;
}

View file

@ -10,11 +10,13 @@
#include <lardon3d/image_view.h>
#include <lardon3d/layout.h>
#include <lardon3d/project.h>
#include <lardon3d/task_queue.h>
#include <lardon3d/tui.h>
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)

156
tests/test_task.c Normal file
View file

@ -0,0 +1,156 @@
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <lardon3d/task.h>
#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;
}

196
tests/test_task_queue.c Normal file
View file

@ -0,0 +1,196 @@
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <lardon3d/task_queue.h>
#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;
}