diff --git a/.gitignore b/.gitignore index a5bcd5b..77ad7a3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ bin/ compile_commands.json .cache/ config.conf +.clangd diff --git a/Makefile b/Makefile index b7b3cda..d472144 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,8 @@ SRCS=src/main.c src/util.c src/lcd.c src/ths.c src/button.c src/ui/screen.c\ src/ui/datesel.c src/ui/statsby.c src/ui/datapoints.c src/ui/timesel.c\ src/ui/statrange.c src/config.c src/ui/blankscreen.c src/drive.c\ src/ui/exportscreen.c src/ui/setdatescreen.c src/ui/viewdatescreen.c\ - src/ui/powerscreen.c src/ui/settzscreen.c src/ui/cleardatascreen.c + src/ui/powerscreen.c src/ui/settzscreen.c src/ui/cleardatascreen.c\ + src/async.c PROG=rpi4b-temp-humidity all: config.conf bin/${PROG} @@ -24,7 +25,7 @@ bin/main.o bin/util.o bin/lcd.o bin/ths.o bin/button.o: src/util.h bin/ui/screen.o bin/ui/statsby.o bin/ui/datesel.o: src/util.h bin/ui/datapoints.o bin/ui/timesel.o bin/ui/statrange.o: src/util.h bin/ui/viewdatescreen.o bin/ui/setdatescreen.o bin/ui/powerscreen.o: src/util.h -bin/ui/settzscreen.o bin/ui/cleardatascreen.o: src/util.h +bin/ui/settzscreen.o bin/ui/cleardatascreen.o bin/async.o: src/util.h bin/main.o bin/lcd.o bin/screen.o bin/ui/datesel.o: src/lcd.h bin/ui/statsby.o bin/ui/timesel.o bin/ui/statrange.o: src/lcd.h @@ -59,6 +60,7 @@ bin/main.o bin/ui/powerscreen.o: src/ui/powerscreen.h bin/main.o bin/ui/settzscreen.o: src/ui/settzscreen.h bin/main.o bin/ui/cleardatascreen.o: src/ui/cleardatascreen.h bin/main.o bin/drive.o bin/ui/exportscreen.o: src/drive.h +bin/main.o bin/drive.o bin/ui/exportscreen.o: src/async.h .if "${DEFAULT_TEMP_UNIT}" != "F" && "${DEFAULT_TEMP_UNIT}" != "C" &&\ "${DEFAULT_TEMP_UNIT}" != "c" && "${DEFAULT_TEMP_UNIT}" != "c" diff --git a/src/async.c b/src/async.c new file mode 100644 index 0000000..27c9418 --- /dev/null +++ b/src/async.c @@ -0,0 +1,111 @@ +/* + * async.c - Utilities for async operations + * Copyright (C) 2024 Alexander Rosenberg + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. See the LICENSE file for more information. + */ +#include "async.h" +#include "signal.h" +#include "util.h" + +#include +#include +#include +#include +#include + + +AsyncOperation *async_operation_start(AsyncOperationPollFunc poll, + AsyncOperationCleanupFunc cleanup, + void *data1, void *data2, + const char *path, char * const *args) { + pid_t pid = fork(); + if (pid < 0) { + warn("fork(2)"); + return NULL; + } else if (pid == 0) { + // child + if (!GLOBAL_OPTS.verbose) { + freopen("/dev/null", "w", stdout); + } + execvp(path, args); + err(1, "execvp(2) for \"%s\"", path); + } else { + // parent + AsyncOperation *op = malloc_checked(sizeof(AsyncOperation)); + op->pid = pid; + op->data1 = data1; + op->data2 = data2; + op->poll_func = poll; + op->cleanup_func = cleanup; + op->done = false; + return op; + } +} + +AsyncOperation *async_operation_take(AsyncOperationPollFunc poll, + AsyncOperationCleanupFunc cleanup, + void *data1, void *data2, pid_t pid) { + AsyncOperation *op = malloc_checked(sizeof(AsyncOperation)); + op->pid = pid; + op->data1 = data1; + op->data2 = data2; + op->poll_func = poll; + op->cleanup_func = cleanup; + op->done = false; + return op; +} + +AsyncOperationStatus async_operation_poll(AsyncOperation *op, bool hang) { + if (op->done) { + // polling a done process is a no-op + return ASYNC_OPERATION_DONE; + } + pid_t proc = waitpid(op->pid, &op->status, hang ? 0 : WNOHANG); + if (proc < 0) { + warn("waitpid(2)"); + async_operation_cancel(op, false); + return ASYNC_OPERATION_FAILED; + } else if (proc > 0) { + op->done = true; + } + if (op->poll_func && !op->poll_func(op)) { + async_operation_cancel(op, false); + return ASYNC_OPERATION_FAILED; + } + return op->done ? ASYNC_OPERATION_DONE : ASYNC_OPERATION_WORKING; +} + +void async_operation_cancel(AsyncOperation *op, bool force) { + if (op->done) { + // cancelling a done process is a no-op + return; + } + kill(op->pid, force ? SIGKILL : SIGTERM); + if (!force) { + int status; + if (!timed_wait(op->pid, &status, 1000 /* 1ms */)) { + warn("waitpid(2)"); + } + if (!WIFEXITED(status) && !WIFSIGNALED(status)) { + // force kill it if it refuses to exit itself + async_operation_cancel(op, true); + } + } + op->done = true; +} + +void async_operation_free(AsyncOperation *op) { + if (op) { + if (!op->done) { + async_operation_cancel(op, false); + } + if (op->cleanup_func) { + op->cleanup_func(op); + } + free(op); + } +} diff --git a/src/async.h b/src/async.h new file mode 100644 index 0000000..cd9f014 --- /dev/null +++ b/src/async.h @@ -0,0 +1,75 @@ +/* + * async.h - Utilities for async operations + * Copyright (C) 2024 Alexander Rosenberg + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. See the LICENSE file for more information. + */ +#ifndef INCLUDED_ASYNC_H +#define INCLUDED_ASYNC_H + +#include +#include + +typedef enum { + ASYNC_OPERATION_DONE, + ASYNC_OPERATION_FAILED, + ASYNC_OPERATION_WORKING +} AsyncOperationStatus; + +typedef struct _AsyncOperation AsyncOperation; + +// true on success, false otherwise +typedef bool(*AsyncOperationPollFunc)(AsyncOperation *op); +typedef void(*AsyncOperationCleanupFunc)(AsyncOperation *op); + +struct _AsyncOperation { + AsyncOperationPollFunc poll_func; + AsyncOperationCleanupFunc cleanup_func; + int status; + pid_t pid; + bool done; + void *data1; + void *data2; +}; +#define async_operation_failed(o) (WEXITSTATUS((o)->status) != 0) + +/* + * Start a new async operation using the executable PATH and the arguments + * ARGS. The first element of ARGS should be the name of the new process, by + * convention. DATA1 and DATA2 are arbitrary pointers for storing data needed by + * CLEANUP or POLL. + * Return: NULL on error, the operation otherwise + */ +AsyncOperation *async_operation_start(AsyncOperationPollFunc poll, + AsyncOperationCleanupFunc cleanup, + void *data1, void *data2, + const char *path, char * const *args); + +/* + * Create a new async operation from an already running process with PID. + */ +AsyncOperation *async_operation_take(AsyncOperationPollFunc poll, + AsyncOperationCleanupFunc cleanup, + void *data1, void *data2, pid_t pid); + +/* + * Poll and return the status of OP. If HANG, wait until OP is done. + */ +AsyncOperationStatus async_operation_poll(AsyncOperation *op, bool hang); + +/* + * Kill OP, by FORCE if necessary. This does NOT free the process, you need to + * do that yourself. + */ +void async_operation_cancel(AsyncOperation *op, bool force); + +/* + * Free OP by calling its cleanup function. This will cancel it if it's still + * running! + */ +void async_operation_free(AsyncOperation *op); + +#endif diff --git a/src/drive.c b/src/drive.c index 91d3353..16a17d2 100644 --- a/src/drive.c +++ b/src/drive.c @@ -19,7 +19,6 @@ #include #include #include -#include static int compare_drives(const Drive *d1, const Drive *d2) { return strcmp(d1->name, d2->name); @@ -62,29 +61,16 @@ void drive_free_drives(Drive *drives, size_t count) { free(drives); } -static void close_pipe(void *ptr) { - int *pipe = ptr; - if (pipe[0] >= 0) { - close(pipe[0]); - } - if (pipe[1] >= 0) { - close(pipe[1]); - } -} - char *drive_guess_fs(const char *dev) { - char *result_fs = NULL; int ctop[2] = {0, 0}; - pthread_cleanup_push(close_pipe, ctop); - pthread_cleanup_push(free, result_fs); if (pipe(ctop) < 0) { warn("failed to create pipe"); - goto cleanup; + return NULL; } pid_t pid = fork(); if (pid < 0) { warn("fork(2) failed"); - goto cleanup; + return NULL; } else if (pid == 0) { // child close(ctop[0]); // close read end @@ -104,125 +90,119 @@ char *drive_guess_fs(const char *dev) { int status; if (waitpid(pid, &status, 0) < 0) { warn("waiting for child failed"); - goto cleanup; + return NULL; } if (WEXITSTATUS(status) != 0) { // no error, we probably just don't know the FS type - goto cleanup; + return NULL; } char read_buf[32]; ssize_t read_count = read(ctop[0], read_buf, 32); if (read_count < 0) { warn("reading from pipe failed"); - goto cleanup; + return NULL; } read_buf[read_count - 1] = '\0'; - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); - result_fs = strdup_checked(read_buf); - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); - cleanup: - pthread_cleanup_pop(false); // dont free result - pthread_cleanup_pop(true); - return result_fs; + return strdup_checked(read_buf); } -bool drive_newfs_msdos(Drive *drive) { - pid_t pid = fork(); - if (pid < 0) { - warn("fork(2) failed"); - return false; - } else if (pid == 0) { - // child - if (!GLOBAL_OPTS.verbose) { - freopen("/dev/null", "w", stdout); - } - execl("/sbin/newfs_msdos", "/sbin/newfs_msdos", "-F32", drive->path, NULL); - warn("execl(2) failed"); - exit(1); - } else { - // parent - int status; - if (waitpid(pid, &status, 0) < 0) { - warn("waiting for child failed"); - return false; - } - if (WEXITSTATUS(status) != 0) { - warnx("creating FAT32 FS failed"); - return false; - } - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); +static bool newfs_operation_poll(AsyncOperation *op) { + if (op->done && !async_operation_failed(op)) { + Drive *drive = op->data1; free(drive->fs); drive->fs = strdup_checked("msdosfs"); - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); - return true; } -} - -bool drive_mount(Drive *drive) { - bool status = true; - char *name = NULL; - pthread_cleanup_push(free, name); - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); - name = strdup_checked("/tmp/mntXXXXXX"); - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); - if (!mkdtemp(name)) { - warn("failed to create temp mount point"); - status = false; - goto cleanup; - } - pid_t pid = fork(); - if (pid < 0) { - warn("fork(2) failed"); - status = false; - goto cleanup; - } else if (pid == 0) { - // child - if (!GLOBAL_OPTS.verbose) { - freopen("/dev/null", "w", stdout); - } - execl("/sbin/mount", "/sbin/mount", "-t", drive->fs, drive->path, name, NULL); - warn("execl(2) failed"); - abort(); - } - // parent - int exit_code; - if (waitpid(pid, &exit_code, 0) < 0) { - warn("waiting for child failed"); - rmdir(name); - status = false; - goto cleanup; - } - if (WEXITSTATUS(exit_code) != 0) { - warnx("mount(8) failed: \"%s\"", drive->path); - rmdir(name); - status = false; - goto cleanup; - } - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); - drive->mnt = strdup_checked(name); - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); - LOG_VERBOSE("Mounted \"%s\" with fs %s to \"%s\"\n", drive->path, drive->fs, - drive->mnt); - cleanup: - pthread_cleanup_pop(true); - return status; -} - -bool drive_unmount(Drive *drive, bool force) { - if (!drive->mnt) { - warnx("\"%s\" is not mounted", drive->path); - return false; - } - int flags = force ? MNT_FORCE : 0; - if (unmount(drive->mnt, flags) < 0) { - warn("unmount(2) failed for \"%s\"", drive->path); - return false; - } - rmdir(drive->mnt); - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); - free(drive->mnt); - drive->mnt = NULL; - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); - LOG_VERBOSE("Unmounted \"%s\"\n", drive->path); return true; } + +AsyncOperation *drive_newfs_msdos(Drive *drive) { + if (drive->mnt) { + warnx("\"%s\" is mounted", drive->path); + return NULL; + } + char * const args[] = {"newfs_msdos", "-F32", drive->path, NULL}; + AsyncOperation * op= async_operation_start(newfs_operation_poll, NULL, + drive, NULL, args[0], args); + if (!op) { + warnx("%s: failed to start newfs_msdos process", drive->path); + return NULL; + } + return op; +} + +static bool mount_operation_poll(AsyncOperation *op) { + Drive *drive = op->data1; + if (op->done && !async_operation_failed(op)) { + // steal the mount point string + drive->mnt = op->data2; + op->data2 = NULL; + LOG_VERBOSE("Mounted \"%s\" at \"%s\"\n", drive->path, drive->mnt); + } else if (op->done) { + warnx("mount of \"%s\" failed with status %d", drive->path, + WEXITSTATUS(op->status)); + } + return true; +} + +static void mount_operation_cleanup(AsyncOperation *op) { + free(op->data2); +} + +AsyncOperation *drive_mount(Drive *drive) { + if (drive->mnt) { + warnx("\"%s\" is already mounted", drive->path); + return NULL; + } + char *name = strdup_checked("/tmp/mntXXXXXX"); + if (!mkdtemp(name)) { + warn("failed to create temp mount point"); + return NULL; + } + char * const args[] = { "mount", "-t", drive->fs, drive->path, name, NULL }; + AsyncOperation *op = async_operation_start(mount_operation_poll, + mount_operation_cleanup, + drive, name, args[0], args); + if (!op) { + warnx("%s: failed to start mount process", drive->path); + free(name); + return NULL; + } + return op; +} + +bool unmount_operation_poll(AsyncOperation *op) { + if (op->done) { + Drive *drive = op->data1; + rmdir(drive->mnt); + free(drive->mnt); + drive->mnt = NULL; + LOG_VERBOSE("Unmounted \"%s\"\n", drive->path); + } + return true; +} + +AsyncOperation *drive_unmount(Drive *drive, bool force) { + if (!drive->mnt) { + warnx("\"%s\" is not mounted", drive->path); + return NULL; + } + const char *args[4]; + if (force) { + args[0] = "umount"; + args[1] = "-f"; + args[2] = drive->mnt; + args[3] = NULL; + } else { + args[0] = "umount"; + args[1] = drive->mnt; + args[2] = NULL; + } + AsyncOperation *op = async_operation_start(unmount_operation_poll, NULL, + drive, NULL, args[0], + (char *const *) args); + if (!op) { + warnx("%s: failed to unmount drive", drive->mnt); + return NULL; + } + return op; +} diff --git a/src/drive.h b/src/drive.h index 117ad81..341313c 100644 --- a/src/drive.h +++ b/src/drive.h @@ -10,8 +10,10 @@ #ifndef INCLUDED_DRIVE_H #define INCLUDED_DRIVE_H -#include +#include "async.h" + #include +#include typedef struct { char *name; @@ -23,7 +25,8 @@ typedef struct { /* * Attempt to list all connected USB drives and their file systems. The list is * sorted alphabetically. - * Return: all connected USB (actually any serial drive) and their file systems. + * Return: all connected USB (actually any serial drive, so SATA too) and their + * file systems. */ Drive *drive_list_usb_drives(size_t *count); @@ -40,21 +43,21 @@ void drive_free_drives(Drive *drives, size_t count); char *drive_guess_fs(const char *dev); /* - * Create an MSDOS (FAT32) FS on DRIVE. DRIVE MUST NOT BE MOUNTED! - * Return: true on success, false otherwise + * Create an MSDOS (FAT32) FS on DRIVE. + * Return: an operation on success, NULL otherwise */ -bool drive_newfs_msdos(Drive *drive); +AsyncOperation *drive_newfs_msdos(Drive *drive); /* - * Mount DRIVE onto a new temp. dir in /tmp. DRIVE MUST NOT BE MOUNTED! - * Return: true on success, false otherwise + * Mount DRIVE onto a new temp. dir in /tmp. + * Return: an operation on success, NULL otherwise */ -bool drive_mount(Drive *drive); +AsyncOperation *drive_mount(Drive *drive); /* * Unmount DRIVE. Force the operation if FORCE. Also rmdir(2) the mount point. - * Return: true of success, false on failure. + * Return: an operation on success, NULL otherwise */ -bool drive_unmount(Drive *drive, bool force); +AsyncOperation *drive_unmount(Drive *drive, bool force); #endif diff --git a/src/main.c b/src/main.c index 0ca7e05..5120cb3 100644 --- a/src/main.c +++ b/src/main.c @@ -69,7 +69,8 @@ void start_update_thread(pthread_t *thread); // cross thread variables sqlite3 *DATABASE; pthread_mutex_t STAT_MUTEX; -uint32_t LAST_TEMP, LAST_HUMID; +// these values mean that we haven't yet read the temperature +uint32_t LAST_TEMP = UINT32_MAX - 1, LAST_HUMID = UINT32_MAX - 1; _Atomic bool RUNNING = true; /* * Lock the cross thread variables above. @@ -92,7 +93,8 @@ int main(int argc, char *const *argv) { setenv("TZ", GLOBAL_OPTS.timezone, true); NEED_CLEAR_TZ = true; } else { - LOG_VERBOSE("Config timezone option shadowed by local environment variable\n"); + LOG_VERBOSE("Config timezone option shadowed by local environment " + "variable.\n"); } } tzset(); @@ -218,7 +220,6 @@ err(1, "failed to setup signal %s", #sig);\ void setup_signals() { SIGNAL_SETUP_CHECKED(SIGTERM, exit_signal_callback); SIGNAL_SETUP_CHECKED(SIGINT, exit_signal_callback); - SIGNAL_SETUP_CHECKED(SIGUSR1, SIG_IGN); // used to kill export operations } static const char *CREATE_DB_TABLE_QUERY = diff --git a/src/ui/exportscreen.c b/src/ui/exportscreen.c index 6deba6f..3a58510 100644 --- a/src/ui/exportscreen.c +++ b/src/ui/exportscreen.c @@ -10,14 +10,11 @@ #include "exportscreen.h" #include -#include #include -#include #include -#include -#include #include #include +#include static void export_screen_reset(ExportScreen *screen, SensorState *state) { @@ -36,19 +33,24 @@ static void export_screen_reset(ExportScreen *screen, * Retrun: true if task is done, false otherwise */ static bool export_check_bg_task(ExportScreen *screen) { - void *status; - if (pthread_peekjoin_np(screen->backgroud_oper, &status) != EBUSY) { - // done - screen->bg_inited = false; - if ((intptr_t) status == EXPORT_SCREEN_FAILED) { - // error happened - warnx("background export operation failed"); + switch (async_operation_poll(screen->current_op, false)) { + case ASYNC_OPERATION_DONE: + if (async_operation_failed(screen->current_op)) { + goto failed; } - screen->stage = (intptr_t) status; screen->need_redraw = true; + screen->current_op = NULL; return true; + case ASYNC_OPERATION_FAILED: + goto failed; + case ASYNC_OPERATION_WORKING: + return false; } - return false; + failed: + screen->stage = EXPORT_SCREEN_FAILED; + screen->need_redraw = true; + screen->current_op = NULL; + return true; } static const char *FORMAT_STRINGS[] = { @@ -150,15 +152,9 @@ static enum { } export_confirm_action(ExportScreen *screen, SensorState *state, const char *fmt, ...) { - int done_status = EXPORT_CONFIRM_CONTINUE; - if (screen->bg_inited && export_check_bg_task(screen)) { - // background task done - done_status = EXPORT_CONFIRM_CONTINUE; - goto confirm_done; - } + int done_status; if (state->back_down || (state->sel_down && !screen->confirm_state)) { done_status = EXPORT_CONFIRM_NO; - screen->need_redraw = true; goto confirm_done; } if (state->sel_down) { @@ -188,9 +184,10 @@ static enum { } return EXPORT_CONFIRM_CONTINUE; confirm_done: - lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, - LCD_DISPLAY_ON); - screen->confirm_state = false; + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); + screen->confirm_state = false; + screen->need_redraw = true; return done_status; } @@ -203,6 +200,8 @@ static bool export_show_message(ExportScreen *screen, return true; } if (screen->need_redraw) { + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); lcd_clear(state->lcd); lcd_move_to(state->lcd, 0, 0); lcd_write_string(state->lcd, message); @@ -211,106 +210,114 @@ static bool export_show_message(ExportScreen *screen, return false; } -static intptr_t export_newfs_action(ExportScreen *screen) { - if (drive_newfs_msdos(&screen->drives[screen->cur_drive])) { - return EXPORT_SCREEN_MOUNTING; - } else { - return EXPORT_SCREEN_FAILED; - } -} - -static intptr_t export_mount_action(ExportScreen *screen) { - Drive *drive = &screen->drives[screen->cur_drive]; - if (drive_mount(drive)) { - struct stat statbuf; - char *path = NULL; - int exist; - pthread_cleanup_push(free, path); - path = export_build_cur_path(screen); - exist = stat(path, &statbuf); - screen->need_overwrite = exist == 0; - pthread_cleanup_pop(true); // free(path) - return screen->need_overwrite ? EXPORT_SCREEN_CONFIRM_OVERWRITE : - EXPORT_SCREEN_WRITING; - } else { - return EXPORT_SCREEN_FAILED; - } -} - -static intptr_t export_write_action(ExportScreen *screen) { - char *path = NULL; - intptr_t next_target = EXPORT_SCREEN_DONE; - pthread_cleanup_push(free, path); - path = export_build_cur_path(screen); - if (screen->need_overwrite && !recursive_rm(path)) { - next_target = EXPORT_SCREEN_FAILED; - goto cleanup; - } - LOG_VERBOSE("Exporting to \"%s\"\n", path); - switch (screen->format) { - case EXPORT_FORMAT_SQLITE: - if (!export_database(screen->db_cache, path)) { - warnx("export to sqlite3 db failed: \"%s\"", path); - next_target = EXPORT_SCREEN_FAILED; - } - break; - case EXPORT_FORMAT_CSV: - if (!export_database_as_csv(screen->db_cache, path)) { - warnx("export to CSV failed: \"%s\"", path); - next_target = EXPORT_SCREEN_FAILED; - } - break; - default: - warnx("unknown export format"); - next_target = EXPORT_SCREEN_FAILED; - break; - } - LOG_VERBOSE("Export done\n"); - cleanup: - pthread_cleanup_pop(true); // free(path) - drive_unmount(&screen->drives[screen->cur_drive], true); - return next_target; -} - static const char BACKGROUND_CHARS[] = { '|', '-' }; #define NBACKGROUND_CHAR sizeof(BACKGROUND_CHARS) -static void export_do_background_action(ExportScreen *screen, - SensorState *state, - intptr_t(*action)(ExportScreen *screen)) { - if (!screen->bg_inited) { - if (pthread_create(&screen->backgroud_oper, NULL, - (void*(*)(void *)) action, screen)) { - warnx("could not start background export operation"); - screen->stage = EXPORT_SCREEN_FAILED; - } - screen->bg_inited = true; - screen->working_char = 0; - } - if (state->down_down || state->up_down || state->back_down || - state->sel_down) { - screen->last_cancel_stage = screen->stage; - screen->stage = EXPORT_SCREEN_CONFIRM_CANCEL; +/* + * True if done, false otherwise + */ +static bool export_do_background_action(ExportScreen *screen, SensorState *state) { + if (!screen->show_cancel_screen && (state->down_down || state->up_down || + state->back_down || state->sel_down)) { + screen->show_cancel_screen = true; screen->need_redraw = true; - return; + // we need to make sure that the confirm action screen ignores actions + // for this iteration of the UI loop + state->back_down = state->up_down = + state->sel_down = state->down_down = false; + } + if (!screen->current_op) { + screen->stage = EXPORT_SCREEN_FAILED; + screen->need_redraw = true; + return false; } - time_t now = time(NULL); if (export_check_bg_task(screen)) { - // bg task done + return true; + } + if (screen->show_cancel_screen) { + int confirm_status = export_confirm_action(screen, state, "Really cancel?"); + if (confirm_status == EXPORT_CONFIRM_YES) { + async_operation_free(screen->current_op); + screen->current_op = NULL; + AsyncOperation *op = drive_unmount(&screen->drives[screen->cur_drive], true); + async_operation_poll(op, true); + async_operation_free(op); + screen->stage = EXPORT_SCREEN_CANCELED; + screen->need_redraw = true; + } else if (confirm_status == EXPORT_CONFIRM_NO) { + screen->need_redraw = true; + screen->show_cancel_screen = false; + } + } else { + time_t now = time(NULL); + if (screen->need_redraw || now != screen->last_char_change) { + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); + screen->last_char_change = now; + screen->need_redraw = false; + lcd_clear(state->lcd); + lcd_move_to(state->lcd, 0, 0); + lcd_write_string(state->lcd, "Working"); + lcd_write_char(state->lcd, BACKGROUND_CHARS[screen->working_char++ % NBACKGROUND_CHAR]); + } + } + return false; +} + +static void export_async_write_action(ExportScreen *screen) { + signal(SIGINT, SIG_DFL); + signal(SIGTERM, SIG_DFL); + char *path = export_build_cur_path(screen); + if (screen->need_overwrite && !recursive_rm(path)) { + exit(1); + } + LOG_VERBOSE("Exporting to \"%s\"\n", path); + switch (screen->format) { + case EXPORT_FORMAT_SQLITE: + if (!export_database(screen->db_cache, path)) { + errx(1, "export to sqlite3 db failed: \"%s\"", path); + } + break; + case EXPORT_FORMAT_CSV: + if (!export_database_as_csv(screen->db_cache, path)) { + errx(1, "export to CSV failed: \"%s\"", path); + } + break; + default: + errx(1, "unknown export format"); + break; + } + free(path); + LOG_VERBOSE("Export done\n"); + exit(0); +} + +static void export_handle_write(ExportScreen *screen, SensorState *state) { + if (screen->current_op) { + if (export_do_background_action(screen, state)) { + screen->need_redraw = true; + screen->stage = EXPORT_SCREEN_UNMOUNT; + } return; } - if (screen->need_redraw || now != screen->last_char_change) { - lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, - LCD_DISPLAY_ON); - screen->last_char_change = now; - screen->need_redraw = false; - lcd_clear(state->lcd); - lcd_move_to(state->lcd, 0, 0); - lcd_write_string(state->lcd, "Working"); - lcd_write_char(state->lcd, BACKGROUND_CHARS[screen->working_char++ % NBACKGROUND_CHAR]); + // if we have no current op, start a new one + screen->show_cancel_screen = false; + pid_t pid = fork(); + if (pid < 0) { + warn("failed to start write background task"); + screen->need_redraw = true; + screen->stage = EXPORT_SCREEN_FAILED; + return; + } else if (pid == 0) { + // child + export_async_write_action(screen); + abort(); + } else { + // parent + screen->current_op = async_operation_take(NULL, NULL, NULL, NULL, pid); } } @@ -349,10 +356,31 @@ static bool export_screen_dispatch(ExportScreen *screen, } break; case EXPORT_SCREEN_WRITE_NEWFS: - export_do_background_action(screen, state, export_newfs_action); + if (!screen->current_op) { + screen->current_op = drive_newfs_msdos(cur_drive); + screen->show_cancel_screen = false; + } + if (export_do_background_action(screen, state)) { + screen->stage = EXPORT_SCREEN_MOUNTING; + screen->need_redraw = true; + } break; case EXPORT_SCREEN_MOUNTING: - export_do_background_action(screen, state, export_mount_action); + if (!screen->current_op) { + screen->current_op = drive_mount(cur_drive); + screen->show_cancel_screen = false; + } + if (export_do_background_action(screen, state)) { + char *path = export_build_cur_path(screen); + struct stat statbuf; + if (stat(path, &statbuf) == 0) { + screen->stage = EXPORT_SCREEN_CONFIRM_OVERWRITE; + } else { + screen->stage = EXPORT_SCREEN_WRITING; + } + screen->need_redraw = true; + free(path); + } break; case EXPORT_SCREEN_CONFIRM_OVERWRITE: confirm_status = export_confirm_action(screen, state, "Overwrite?"); @@ -364,24 +392,21 @@ static bool export_screen_dispatch(ExportScreen *screen, screen->need_redraw = true; // on the main thread so force unmount // also, nothing should be writing, so kill it if it is - drive_unmount(cur_drive, true); + AsyncOperation *op = drive_unmount(cur_drive, true); + async_operation_poll(op, true); + async_operation_free(op); } break; case EXPORT_SCREEN_WRITING: - export_do_background_action(screen, state, export_write_action); + export_handle_write(screen, state); break; - case EXPORT_SCREEN_CONFIRM_CANCEL: - confirm_status = export_confirm_action(screen, state, "Really cancel?"); - if (confirm_status == EXPORT_CONFIRM_YES) { - kill(0, SIGUSR1); - pthread_cancel(screen->backgroud_oper); - pthread_detach(screen->backgroud_oper); - drive_unmount(cur_drive, true); - screen->bg_inited = false; - screen->stage = EXPORT_SCREEN_CANCELED; - screen->need_redraw = true; - } else if (confirm_status == EXPORT_CONFIRM_NO) { - screen->stage = screen->last_cancel_stage; + case EXPORT_SCREEN_UNMOUNT: + if (!screen->current_op) { + screen->current_op = drive_unmount(cur_drive, false); + screen->show_cancel_screen = false; + } + if (export_do_background_action(screen, state)) { + screen->stage = EXPORT_SCREEN_DONE; screen->need_redraw = true; } break; @@ -398,10 +423,7 @@ static bool export_screen_dispatch(ExportScreen *screen, } static void export_screen_cleanup(ExportScreen *screen) { - if (screen->bg_inited) { - pthread_cancel(screen->backgroud_oper); - pthread_detach(screen->backgroud_oper); - } + async_operation_free(screen->current_op); } ExportScreen *export_screen_new() { @@ -412,5 +434,6 @@ ExportScreen *export_screen_new() { s->read_drives = true; s->stage = EXPORT_SCREEN_FORMAT; s->format = EXPORT_FORMAT_SQLITE; + s->current_op = NULL; return s; } diff --git a/src/ui/exportscreen.h b/src/ui/exportscreen.h index 5a7cd40..f96b090 100644 --- a/src/ui/exportscreen.h +++ b/src/ui/exportscreen.h @@ -11,11 +11,11 @@ #define INCLUDED_EXPORTSCREEN_H #include "screen.h" +#include "../async.h" #include "../drive.h" #include #include -#include #include typedef struct { @@ -28,8 +28,8 @@ typedef struct { EXPORT_SCREEN_MOUNTING, EXPORT_SCREEN_CONFIRM_OVERWRITE, EXPORT_SCREEN_WRITING, + EXPORT_SCREEN_UNMOUNT, EXPORT_SCREEN_DONE, - EXPORT_SCREEN_CONFIRM_CANCEL, EXPORT_SCREEN_CANCELED, EXPORT_SCREEN_FAILED, EXPORT_SCREEN_NO_DRIVES, @@ -47,13 +47,13 @@ typedef struct { size_t ndrive; size_t cur_drive; int confirm_state; - bool bg_inited; - pthread_t backgroud_oper; int working_char; time_t last_char_change; bool need_overwrite; sqlite3 *db_cache; int last_cancel_stage; + AsyncOperation *current_op; + bool show_cancel_screen; } ExportScreen; ExportScreen *export_screen_new(void); diff --git a/src/ui/screen.c b/src/ui/screen.c index b1aa39c..06b9d04 100644 --- a/src/ui/screen.c +++ b/src/ui/screen.c @@ -151,7 +151,9 @@ static bool stats_screen_dispatch(StatsScreen *screen, SensorState *state) { char buff[17]; int cur_len; if (state->temp == UINT32_MAX || state->humid == UINT32_MAX) { - cur_len = snprintf(buff, sizeof(buff), "ERROR! "); + cur_len = snprintf(buff, sizeof(buff), "ERROR! "); + } else if (state->temp == UINT_MAX - 1 || state->humid == UINT32_MAX - 1) { + cur_len = snprintf(buff, sizeof(buff), "LOADING! "); } else { cur_len = snprintf(buff, sizeof(buff), "%-4.1f%c %3" PRIu32 "%% ", convert_temperature(state->temp), diff --git a/src/util.c b/src/util.c index 933969a..78a68a8 100644 --- a/src/util.c +++ b/src/util.c @@ -17,9 +17,8 @@ #include #include #include -#include #include -#include +#include const char *PERIOD_LABELS[] = { "HOUR", @@ -414,12 +413,6 @@ char *pad_humid_str(int humid, char *buf, size_t buf_size) { return buf; } -static void close_dir_if_set(void *dir) { - if (dir) { - closedir(dir); - } -} - bool recursive_rm_at(int dfd, const char *path) { if (dfd != AT_FDCWD) { LOG_VERBOSE("Recursive delete: \"%s\"\n", path); @@ -432,18 +425,12 @@ bool recursive_rm_at(int dfd, const char *path) { int flag = 0; if (S_ISDIR(statbuf.st_mode)) { flag = AT_REMOVEDIR; - pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); int fd = openat(dfd, path, O_DIRECTORY | O_RDONLY); if (fd < 0) { - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); warn("open \"%s\"", path); return false; } - DIR *dir = NULL; - bool error = false; - pthread_cleanup_push(close_dir_if_set, dir); - dir = fdopendir(fd); - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + DIR *dir = fdopendir(fd); if (!dir) { close(fd); warn("fdopendir \"%s\"", path); @@ -453,14 +440,10 @@ bool recursive_rm_at(int dfd, const char *path) { while ((sd = readdir(dir))) { if (strcmp(sd->d_name, ".") != 0 && strcmp(sd->d_name, "..") != 0 && !recursive_rm_at(dirfd(dir), sd->d_name)) { - error = true; - break; + return false; } } - pthread_cleanup_pop(true); - if (error) { - return false; - } + closedir(dir); } if (unlinkat(dfd, path, flag)< 0) { warn("remove \"%s\"", path); @@ -474,48 +457,31 @@ bool recursive_rm(const char *path) { } bool export_database(sqlite3 *db, const char *dest) { - bool status = true; sqlite3 *new_db = NULL; sqlite3_backup *oper = NULL; - pthread_cleanup_push((void(*)(void*))sqlite3_close, new_db); - pthread_cleanup_push((void(*)(void*))sqlite3_backup_finish, oper); int res = sqlite3_open(dest, &new_db); if (res != SQLITE_OK) { warnx("could not open new db: \"%s\": %s", dest, sqlite3_errstr(res)); - status = false; - goto cleanup; + return false; } oper = sqlite3_backup_init(new_db, "main", db, "main"); if (!oper) { warnx("could not start backup: \"%s\": %s", dest, sqlite3_errstr(res)); - status = false; - goto cleanup; + return false; } sqlite3_backup_step(oper, -1); - cleanup: - pthread_cleanup_pop(true); // cleanup oper - pthread_cleanup_pop(true); // cleanup new_db - return status; -} - -static void cleanup_file_obj(void *obj) { - if (obj) { - fclose(obj); - } + sqlite3_backup_finish(oper); + sqlite3_close(new_db); + return true; } bool export_database_as_csv(sqlite3 *db, const char *dest) { - bool status = true; - FILE *outfile = NULL; - pthread_cleanup_push((void(*)(void *))sqlite3_reset, EXPORT_CSV_QUERY); - pthread_cleanup_push(cleanup_file_obj, outfile); - outfile = fopen(dest, "w"); + FILE *outfile = fopen(dest, "w"); if (!outfile) { warn("fopen \"%s\"", dest); - status = false; - goto cleanup; + return false; } int res; while ((res = sqlite3_step(EXPORT_CSV_QUERY)) == SQLITE_ROW) { @@ -526,14 +492,11 @@ bool export_database_as_csv(sqlite3 *db, const char *dest) { if (res != SQLITE_DONE) { warnx("CSV export to \"%s\" faield: %s\n", dest, sqlite3_errstr(res)); - status = false; - goto cleanup; // readability + return false; } - fflush(outfile); - cleanup: - pthread_cleanup_pop(true); // reset query - pthread_cleanup_pop(true); // close file - return status; + sqlite3_reset(EXPORT_CSV_QUERY); + fclose(outfile); + return true; } static bool ensure_dir_exists(const char *path, mode_t mode) { @@ -556,7 +519,7 @@ static bool ensure_dir_exists(const char *path, mode_t mode) { } bool mkdirs(const char *path, mode_t mode) { - LOG_VERBOSE("Creating directory: \"%s\"\n", path) + LOG_VERBOSE("Creating directory: \"%s\"\n", path); char *copy = strdup_checked(path); char *work_str = copy, *token; while ((token = strsep(&work_str, "/"))) { @@ -576,3 +539,32 @@ void request_restart() { RUNNING = false; SHOULD_RESTART = true; } + +bool timed_wait(pid_t pid, int *out_status, useconds_t micro) { + struct timespec waittime = { + .tv_sec = micro / 1000000, + .tv_nsec = micro % 1000000, + }; + int status; + struct timespec start, now, tmp; + clock_gettime(CLOCK_UPTIME, &start); + while (true) { + if (waitpid(pid, &status, WNOHANG) < 0) { + return false; + } + if (WIFEXITED(status) || WIFSIGNALED(status)) { + if (out_status) { + *out_status = status; + } + return true; + } + clock_gettime(CLOCK_UPTIME, &now); + timespecsub(&now, &start, &tmp); + if (timespeccmp(&tmp, &waittime, >=)) { + if (out_status) { + *out_status = 0; + } + return true; + } + } +} diff --git a/src/util.h b/src/util.h index 2a1bc4b..eab7a5c 100644 --- a/src/util.h +++ b/src/util.h @@ -18,6 +18,7 @@ #include #include #include +#include #define _STRINGIFY_LIT(s) (#s) #define STRINGIFY(v) _STRINGIFY_LIT(v) @@ -254,5 +255,12 @@ bool mkdirs(const char *path, mode_t mode); * re-initialize configuration values. */ void request_restart(void); +/* + * waitpid(2) for PID for MICRO microseconds. If STATUS is not NULL, store the + * exit status there. Use the various WIF* macros to test the status. This + * function sets errno to the errno value of waitpid(2) on error. + * Return: true if successful, false otherwise and the error is in errno + */ +bool timed_wait(pid_t pid, int *out_status, useconds_t micro); #endif