feat: add generic task engine and FIFO scheduler
This commit is contained in:
parent
2fe594cc51
commit
030994696b
13 changed files with 1256 additions and 4 deletions
|
|
@ -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
|
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
|
`X` efface le filtre. Le tri et le filtre restent entièrement en mémoire et ne
|
||||||
modifient ni le manifeste ni les images.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,15 @@
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
typedef struct Lardon3DImageCatalog Lardon3DImageCatalog;
|
typedef struct Lardon3DImageCatalog Lardon3DImageCatalog;
|
||||||
typedef struct Lardon3DImageView Lardon3DImageView;
|
typedef struct Lardon3DImageView Lardon3DImageView;
|
||||||
|
typedef struct Lardon3DTaskQueue Lardon3DTaskQueue;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
LARDON3D_SCREEN_HOME = 0,
|
LARDON3D_SCREEN_HOME = 0,
|
||||||
LARDON3D_SCREEN_PROJECTS,
|
LARDON3D_SCREEN_PROJECTS,
|
||||||
LARDON3D_SCREEN_IMPORT,
|
LARDON3D_SCREEN_IMPORT,
|
||||||
LARDON3D_SCREEN_VIEWER,
|
LARDON3D_SCREEN_VIEWER,
|
||||||
LARDON3D_SCREEN_HELP
|
LARDON3D_SCREEN_HELP,
|
||||||
|
LARDON3D_SCREEN_TASKS
|
||||||
} Lardon3DScreen;
|
} Lardon3DScreen;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
@ -23,6 +25,7 @@ typedef struct {
|
||||||
char status_message[256];
|
char status_message[256];
|
||||||
Lardon3DImageCatalog *image_catalog;
|
Lardon3DImageCatalog *image_catalog;
|
||||||
Lardon3DImageView *image_view;
|
Lardon3DImageView *image_view;
|
||||||
|
Lardon3DTaskQueue *task_queue;
|
||||||
} Lardon3DAppState;
|
} Lardon3DAppState;
|
||||||
|
|
||||||
void lardon3d_app_state_init(Lardon3DAppState *state);
|
void lardon3d_app_state_init(Lardon3DAppState *state);
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,16 @@
|
||||||
|
|
||||||
#include <lardon3d/app_state.h>
|
#include <lardon3d/app_state.h>
|
||||||
#include <lardon3d/import_task.h>
|
#include <lardon3d/import_task.h>
|
||||||
|
#include <lardon3d/task_queue.h>
|
||||||
|
|
||||||
void lardon3d_layout_draw(
|
void lardon3d_layout_draw(
|
||||||
const Lardon3DAppState *state,
|
const Lardon3DAppState *state,
|
||||||
const char *input_text,
|
const char *input_text,
|
||||||
const char *input_label,
|
const char *input_label,
|
||||||
const Lardon3DImportTaskSnapshot *import_snapshot,
|
const Lardon3DImportTaskSnapshot *import_snapshot,
|
||||||
|
const Lardon3DTaskSnapshot *task_snapshots,
|
||||||
|
size_t task_count,
|
||||||
|
const Lardon3DTaskQueueSummary *task_summary,
|
||||||
int rows,
|
int rows,
|
||||||
int cols
|
int cols
|
||||||
);
|
);
|
||||||
|
|
|
||||||
62
include/lardon3d/task.h
Normal file
62
include/lardon3d/task.h
Normal 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
|
||||||
46
include/lardon3d/task_queue.h
Normal file
46
include/lardon3d/task_queue.h
Normal 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
|
||||||
27
meson.build
27
meson.build
|
|
@ -34,6 +34,8 @@ executable(
|
||||||
'src/image_catalog.c',
|
'src/image_catalog.c',
|
||||||
'src/image_view.c',
|
'src/image_view.c',
|
||||||
'src/project.c',
|
'src/project.c',
|
||||||
|
'src/task.c',
|
||||||
|
'src/task_queue.c',
|
||||||
],
|
],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
dependencies: [ncursesw, threads],
|
dependencies: [ncursesw, threads],
|
||||||
|
|
@ -89,3 +91,28 @@ image_view_test = executable(
|
||||||
)
|
)
|
||||||
|
|
||||||
test('image-view', image_view_test, timeout: 30)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include <lardon3d/app_state.h>
|
#include <lardon3d/app_state.h>
|
||||||
#include <lardon3d/image_catalog.h>
|
#include <lardon3d/image_catalog.h>
|
||||||
#include <lardon3d/image_view.h>
|
#include <lardon3d/image_view.h>
|
||||||
|
#include <lardon3d/task_queue.h>
|
||||||
#include <lardon3d/tui.h>
|
#include <lardon3d/tui.h>
|
||||||
|
|
||||||
int
|
int
|
||||||
|
|
@ -16,8 +17,13 @@ lardon3d_app_run(void)
|
||||||
if (!setlocale(LC_ALL, "")) {
|
if (!setlocale(LC_ALL, "")) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
state.task_queue = lardon3d_task_queue_create();
|
||||||
|
if (!state.task_queue) {
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
if (!lardon3d_tui_init()) {
|
if (!lardon3d_tui_init()) {
|
||||||
|
lardon3d_task_queue_destroy(state.task_queue);
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,6 +31,7 @@ lardon3d_app_run(void)
|
||||||
lardon3d_image_view_destroy(state.image_view);
|
lardon3d_image_view_destroy(state.image_view);
|
||||||
lardon3d_image_catalog_destroy(state.image_catalog);
|
lardon3d_image_catalog_destroy(state.image_catalog);
|
||||||
lardon3d_tui_shutdown();
|
lardon3d_tui_shutdown();
|
||||||
|
lardon3d_task_queue_destroy(state.task_queue);
|
||||||
|
|
||||||
return success ? EXIT_SUCCESS : EXIT_FAILURE;
|
return success ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
69
src/layout.c
69
src/layout.c
|
|
@ -61,7 +61,7 @@ screen_texts(
|
||||||
const char **footer
|
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) {
|
switch (screen) {
|
||||||
case LARDON3D_SCREEN_PROJECTS:
|
case LARDON3D_SCREEN_PROJECTS:
|
||||||
|
|
@ -82,11 +82,16 @@ screen_texts(
|
||||||
*title = "Aide";
|
*title = "Aide";
|
||||||
*content = "Raccourcis clavier.";
|
*content = "Raccourcis clavier.";
|
||||||
break;
|
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:
|
case LARDON3D_SCREEN_HOME:
|
||||||
default:
|
default:
|
||||||
*title = "Accueil";
|
*title = "Accueil";
|
||||||
*content = "Bienvenue dans Lardon3D";
|
*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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -296,12 +301,58 @@ draw_import_screen(
|
||||||
draw_catalog(state, rows, columns);
|
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
|
static void
|
||||||
draw_content(
|
draw_content(
|
||||||
const Lardon3DAppState *state,
|
const Lardon3DAppState *state,
|
||||||
const char *input_text,
|
const char *input_text,
|
||||||
const char *input_label,
|
const char *input_label,
|
||||||
const Lardon3DImportTaskSnapshot *import_snapshot,
|
const Lardon3DImportTaskSnapshot *import_snapshot,
|
||||||
|
const Lardon3DTaskSnapshot *task_snapshots,
|
||||||
|
size_t task_count,
|
||||||
|
const Lardon3DTaskQueueSummary *task_summary,
|
||||||
int rows,
|
int rows,
|
||||||
int columns
|
int columns
|
||||||
)
|
)
|
||||||
|
|
@ -343,6 +394,14 @@ draw_content(
|
||||||
rows,
|
rows,
|
||||||
columns
|
columns
|
||||||
);
|
);
|
||||||
|
} else if (state->screen == LARDON3D_SCREEN_TASKS) {
|
||||||
|
draw_tasks(
|
||||||
|
task_snapshots,
|
||||||
|
task_count,
|
||||||
|
task_summary,
|
||||||
|
rows,
|
||||||
|
columns
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
draw_text(
|
draw_text(
|
||||||
(3 + journal_row) / 2,
|
(3 + journal_row) / 2,
|
||||||
|
|
@ -367,6 +426,9 @@ lardon3d_layout_draw(
|
||||||
const char *input_text,
|
const char *input_text,
|
||||||
const char *input_label,
|
const char *input_label,
|
||||||
const Lardon3DImportTaskSnapshot *import_snapshot,
|
const Lardon3DImportTaskSnapshot *import_snapshot,
|
||||||
|
const Lardon3DTaskSnapshot *task_snapshots,
|
||||||
|
size_t task_count,
|
||||||
|
const Lardon3DTaskQueueSummary *task_summary,
|
||||||
int rows,
|
int rows,
|
||||||
int columns
|
int columns
|
||||||
)
|
)
|
||||||
|
|
@ -382,6 +444,9 @@ lardon3d_layout_draw(
|
||||||
input_text,
|
input_text,
|
||||||
input_label,
|
input_label,
|
||||||
import_snapshot,
|
import_snapshot,
|
||||||
|
task_snapshots,
|
||||||
|
task_count,
|
||||||
|
task_summary,
|
||||||
rows,
|
rows,
|
||||||
columns
|
columns
|
||||||
);
|
);
|
||||||
|
|
|
||||||
355
src/task.c
Normal file
355
src/task.c
Normal 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
309
src/task_queue.c
Normal 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;
|
||||||
|
}
|
||||||
19
src/tui.c
19
src/tui.c
|
|
@ -10,11 +10,13 @@
|
||||||
#include <lardon3d/image_view.h>
|
#include <lardon3d/image_view.h>
|
||||||
#include <lardon3d/layout.h>
|
#include <lardon3d/layout.h>
|
||||||
#include <lardon3d/project.h>
|
#include <lardon3d/project.h>
|
||||||
|
#include <lardon3d/task_queue.h>
|
||||||
#include <lardon3d/tui.h>
|
#include <lardon3d/tui.h>
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
MINIMUM_ROWS = 20,
|
MINIMUM_ROWS = 20,
|
||||||
MINIMUM_COLUMNS = 72,
|
MINIMUM_COLUMNS = 72,
|
||||||
|
DISPLAYED_TASK_CAPACITY = 64,
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
|
@ -170,11 +172,22 @@ redraw(
|
||||||
if (task && lardon3d_import_task_snapshot(task, &snapshot)) {
|
if (task && lardon3d_import_task_snapshot(task, &snapshot)) {
|
||||||
displayed_snapshot = &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(
|
lardon3d_layout_draw(
|
||||||
state,
|
state,
|
||||||
text,
|
text,
|
||||||
label,
|
label,
|
||||||
displayed_snapshot,
|
displayed_snapshot,
|
||||||
|
task_snapshots,
|
||||||
|
task_count,
|
||||||
|
&task_summary,
|
||||||
rows,
|
rows,
|
||||||
columns
|
columns
|
||||||
);
|
);
|
||||||
|
|
@ -396,6 +409,9 @@ handle_normal_input(
|
||||||
case KEY_F(4):
|
case KEY_F(4):
|
||||||
state->screen = LARDON3D_SCREEN_VIEWER;
|
state->screen = LARDON3D_SCREEN_VIEWER;
|
||||||
return true;
|
return true;
|
||||||
|
case KEY_F(5):
|
||||||
|
state->screen = LARDON3D_SCREEN_TASKS;
|
||||||
|
return true;
|
||||||
case 27:
|
case 27:
|
||||||
state->screen = LARDON3D_SCREEN_HOME;
|
state->screen = LARDON3D_SCREEN_HOME;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -634,7 +650,8 @@ lardon3d_tui_run(Lardon3DAppState *state)
|
||||||
while (state->running) {
|
while (state->running) {
|
||||||
int key = getch();
|
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)) {
|
if (task && lardon3d_import_task_is_finished(task)) {
|
||||||
Lardon3DImportTaskSnapshot snapshot;
|
Lardon3DImportTaskSnapshot snapshot;
|
||||||
if (!lardon3d_import_task_join(task)
|
if (!lardon3d_import_task_join(task)
|
||||||
|
|
|
||||||
156
tests/test_task.c
Normal file
156
tests/test_task.c
Normal 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
196
tests/test_task_queue.c
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue