Add export screen
This commit is contained in:
		
							
								
								
									
										25
									
								
								src/config.c
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								src/config.c
									
									
									
									
									
								
							| @ -93,7 +93,7 @@ static int parse_uint_arr_callback(struct figpar_config *opt, uint32_t line, | ||||
|         if (last_char && !isdigit(last_char)) { | ||||
|             warnx("line %" PRIu32 ": not a valid number array \"%s\"", | ||||
|                   line, value); | ||||
|             FREE_CHECKED(arr->arr); | ||||
|             free(arr->arr); | ||||
|             free(arr); | ||||
|             return 1; | ||||
|         } | ||||
| @ -118,7 +118,7 @@ static int parse_uint_arr_callback(struct figpar_config *opt, uint32_t line, | ||||
|  | ||||
| static int parse_str_callback(struct figpar_config *opt, uint32_t line, | ||||
|                               char *directive, char *value) { | ||||
|     FREE_CHECKED(opt->value.str); | ||||
|     free(opt->value.str); | ||||
|     if (!value[0]) { | ||||
|         opt->type = FIGPAR_TYPE_STR; | ||||
|         opt->value.str = NULL; | ||||
| @ -200,6 +200,11 @@ static void set_options_from_entries(struct figpar_config *entries, | ||||
|     GLOBAL_OPTS.temp_unit = entries[15].value.num; | ||||
|  | ||||
|     GLOBAL_OPTS.bl_pin = entries[16].value.num; | ||||
|  | ||||
|     entries[17].type = FIGPAR_TYPE_NONE; | ||||
|     GLOBAL_OPTS.export_file_name = steal_opt_if_set(entries[17].value.str); | ||||
|     LOG_VERBOSE("Using export_file_name: \"%s\"\n", | ||||
|                 GLOBAL_OPTS.export_file_name); | ||||
| } | ||||
|  | ||||
| static char *strdup_default_opt(const char *def) { | ||||
| @ -298,13 +303,19 @@ void parse_config_file(const char *path) { | ||||
|             .directive = "temp_unit", | ||||
|             .type = FIGPAR_TYPE_INT, | ||||
|             .action = parse_temp_unit_callback, | ||||
|             .value = {.num = DEFAULT_TEMP_UNIT} | ||||
|             .value = {.num = DEFAULT_TEMP_UNIT}, | ||||
|         }, | ||||
|         { | ||||
|             .directive = "bl_pin", | ||||
|             .type = FIGPAR_TYPE_INT, | ||||
|             .action = parse_int_callback, | ||||
|             .value = {.num = -1} | ||||
|             .value = {.num = -1}, | ||||
|         }, | ||||
|         { | ||||
|             .directive = "export_file_name", | ||||
|             .type = FIGPAR_TYPE_STR, | ||||
|             .action = parse_str_callback, | ||||
|             .value = {.str = strdup_default_opt(DEFAULT_EXPORT_FILE_NAME)}, | ||||
|         }, | ||||
|         { .directive = NULL }, | ||||
|     }; | ||||
| @ -321,11 +332,11 @@ void parse_config_file(const char *path) { | ||||
|     for (size_t i = 0; i < entry_count; ++i) { | ||||
|         switch (entries[i].type) { | ||||
|         case FIGPAR_TYPE_STR: | ||||
|             FREE_CHECKED(entries[i].value.str); | ||||
|             free(entries[i].value.str); | ||||
|             break; | ||||
|         case CONFIG_UINT_ARR_TYPE: | ||||
|             FREE_CHECKED(((struct UIntArr *) entries[i].value.data)->arr); | ||||
|             FREE_CHECKED(entries[i].value.data); | ||||
|             free(((struct UIntArr *) entries[i].value.data)->arr); | ||||
|             free(entries[i].value.data); | ||||
|             break; | ||||
|         default: ; // ignore | ||||
|         } | ||||
|  | ||||
							
								
								
									
										228
									
								
								src/drive.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								src/drive.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,228 @@ | ||||
| /* | ||||
|  * drive.c - Simple abstractions for working with file systems and drives | ||||
|  * 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 "drive.h" | ||||
| #include "util.h" | ||||
|  | ||||
| #include <ctype.h> | ||||
| #include <err.h> | ||||
| #include <stdlib.h> | ||||
| #include <unistd.h> | ||||
| #include <sys/wait.h> | ||||
| #include <string.h> | ||||
| #include <dirent.h> | ||||
| #include <sys/param.h> | ||||
| #include <sys/mount.h> | ||||
| #include <pthread.h> | ||||
|  | ||||
| static int compare_drives(const Drive *d1, const Drive *d2) { | ||||
|     return strcmp(d1->name, d2->name); | ||||
| } | ||||
|  | ||||
| Drive *drive_list_usb_drives(size_t *count) { | ||||
|     DIR *dir = opendir("/dev"); | ||||
|     if (!dir) { | ||||
|         warn("failed to open /dev"); | ||||
|         return NULL; | ||||
|     } | ||||
|     Drive *drives = NULL; | ||||
|     *count = 0; | ||||
|     struct dirent *sd; | ||||
|     while ((sd = readdir(dir))) { | ||||
|         if (strncmp(sd->d_name, "da", 2) == 0 && isdigit(sd->d_name[2])) { | ||||
|             drives = realloc_checked(drives, sizeof(Drive) * ++(*count)); | ||||
|             Drive *cur = &drives[*count - 1]; | ||||
|             cur->name = strdup_checked(sd->d_name); | ||||
|             asprintf_checked(&cur->path, "/dev/%s", sd->d_name); | ||||
|             cur->fs = drive_guess_fs(cur->path); | ||||
|             cur->mnt = NULL; | ||||
|         } | ||||
|     } | ||||
|     closedir(dir); | ||||
|     if (*count) { | ||||
|         qsort(drives, *count, sizeof(Drive), | ||||
|               (int(*)(const void *, const void *))compare_drives); | ||||
|     } | ||||
|     return drives; | ||||
| } | ||||
|  | ||||
| void drive_free_drives(Drive *drives, size_t count) { | ||||
|     for (size_t i = 0; i < count; ++i) { | ||||
|         free(drives[i].name); | ||||
|         free(drives[i].path); | ||||
|         free(drives[i].fs); | ||||
|         free(drives[i].mnt); | ||||
|     } | ||||
|     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; | ||||
|     } | ||||
|     pid_t pid = fork(); | ||||
|     if (pid < 0) { | ||||
|         warn("fork(2) failed"); | ||||
|         goto cleanup; | ||||
|     } else if (pid == 0) { | ||||
|         // child | ||||
|         close(ctop[0]); // close read end | ||||
|         if (dup2(ctop[1], 1) < 0) { | ||||
|             warn("dup2(2) failed"); | ||||
|             close(ctop[1]); | ||||
|             exit(1); | ||||
|         } | ||||
|         if (!GLOBAL_OPTS.verbose) { | ||||
|             freopen("/dev/null", "w", stderr); // prevent warnings for unknown fs | ||||
|         } | ||||
|         execl("/usr/sbin/fstyp", "/usr/sbin/fstyp", dev, NULL); | ||||
|         close(ctop[1]); | ||||
|         exit(1); | ||||
|     }  | ||||
|     // parent | ||||
|     int status; | ||||
|     if (waitpid(pid, &status, 0) < 0) { | ||||
|         warn("waiting for child failed"); | ||||
|         goto cleanup; | ||||
|     } | ||||
|     if (WEXITSTATUS(status) != 0) { | ||||
|         // no error, we probably just don't know the FS type | ||||
|         goto cleanup; | ||||
|     } | ||||
|     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; | ||||
|     } | ||||
|     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; | ||||
| } | ||||
|  | ||||
| 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); | ||||
|         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; | ||||
| } | ||||
							
								
								
									
										60
									
								
								src/drive.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/drive.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| /* | ||||
|  * drive.h - Simple abstractions for working with file systems and drives | ||||
|  * 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_DRIVE_H | ||||
| #define INCLUDED_DRIVE_H | ||||
|  | ||||
| #include <stddef.h> | ||||
| #include <stdbool.h> | ||||
|  | ||||
| typedef struct { | ||||
|     char *name; | ||||
|     char *path; | ||||
|     char *fs; | ||||
|     char *mnt; | ||||
| } Drive; | ||||
|  | ||||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
| Drive *drive_list_usb_drives(size_t *count); | ||||
|  | ||||
| /* | ||||
|  * Free the array of drives DRIVES. | ||||
|  */ | ||||
| void drive_free_drives(Drive *drives, size_t count); | ||||
|  | ||||
| /* | ||||
|  * Use the fstyp(8) utility to try to find the FS for device file DEV.  | ||||
|  * Return: The FS name allocated with malloc(3), or NULL if it could not be | ||||
|  *         found. | ||||
|  */ | ||||
| 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 | ||||
|  */ | ||||
| bool 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 | ||||
|  */ | ||||
| bool drive_mount(Drive *drive); | ||||
|  | ||||
| /* | ||||
|  * Unmount DRIVE. Force the operation if FORCE. Also rmdir(2) the mount point. | ||||
|  * Return: true of success, false on failure. | ||||
|  */ | ||||
| bool drive_unmount(Drive *drive, bool force); | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										19
									
								
								src/main.c
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								src/main.c
									
									
									
									
									
								
							| @ -18,6 +18,7 @@ | ||||
| #include "ui/datapoints.h" | ||||
| #include "ui/statrange.h" | ||||
| #include "ui/blankscreen.h" | ||||
| #include "ui/exportscreen.h" | ||||
|  | ||||
| #include <unistd.h> | ||||
| #include <err.h> | ||||
| @ -105,7 +106,12 @@ int main(int argc, char *const *argv) { | ||||
|     screen_manager_add(screen_manager, (Screen *) stats_by_screen_new()); | ||||
|     screen_manager_add(screen_manager, (Screen *) data_points_screen_new()); | ||||
|     screen_manager_add(screen_manager, (Screen *) stat_range_screen_new()); | ||||
|     screen_manager_add(screen_manager, blank_screen_new()); | ||||
|     if (GLOBAL_OPTS.export_file_name) { | ||||
|         screen_manager_add(screen_manager, (Screen *) export_screen_new()); | ||||
|     } | ||||
|     if (GLOBAL_OPTS.bl_pin >= 0) { | ||||
|         screen_manager_add(screen_manager, blank_screen_new()); | ||||
|     } | ||||
|     while (RUNNING) { | ||||
|         lock_stat_globals(); | ||||
|         uint32_t temp = LAST_TEMP; | ||||
| @ -142,7 +148,7 @@ void parse_arguments(int argc, char *const *argv) { | ||||
|             print_help(argv[0]); | ||||
|             exit(0); | ||||
|         case 'f': | ||||
|             FREE_CHECKED(GLOBAL_OPTS.config_path); | ||||
|             free(GLOBAL_OPTS.config_path); | ||||
|             GLOBAL_OPTS.config_path = optarg; | ||||
|             LOG_VERBOSE("Config file path set: \"%s\"\n", optarg); | ||||
|             break; | ||||
| @ -174,13 +180,14 @@ static void exit_signal_callback(int sig) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #define SIGNAL_SETUP_CHECKED(sig) \ | ||||
| if (signal(sig, exit_signal_callback) == SIG_ERR) {\ | ||||
| #define SIGNAL_SETUP_CHECKED(sig, func) \ | ||||
| if (signal(sig, func) == SIG_ERR) {\ | ||||
| err(1, "failed to setup signal %s", #sig);\ | ||||
| } | ||||
| void setup_signals() { | ||||
|     SIGNAL_SETUP_CHECKED(SIGTERM); | ||||
|     SIGNAL_SETUP_CHECKED(SIGINT); | ||||
|     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 | ||||
| } | ||||
|  | ||||
| void open_database() { | ||||
|  | ||||
							
								
								
									
										403
									
								
								src/ui/exportscreen.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										403
									
								
								src/ui/exportscreen.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,403 @@ | ||||
| /* | ||||
|  * exportscreen.c - Screen for exporting to USB drives | ||||
|  * 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 "exportscreen.h" | ||||
|  | ||||
| #include <err.h> | ||||
| #include <errno.h> | ||||
| #include <sys/types.h> | ||||
| #include <signal.h> | ||||
| #include <stdarg.h> | ||||
| #include <pthread.h> | ||||
| #include <pthread_np.h> | ||||
| #include <unistd.h> | ||||
| #include <sys/stat.h> | ||||
|  | ||||
| static void export_screen_reset(ExportScreen *screen, | ||||
|                                 SensorState *state) { | ||||
|     screen->format = EXPORT_FORMAT_SQLITE; | ||||
|     screen->stage = EXPORT_SCREEN_FORMAT; | ||||
|     screen->need_redraw = true; | ||||
|     drive_free_drives(screen->drives, screen->ndrive); | ||||
|     screen->read_drives = true; | ||||
|     screen->confirm_state = false; | ||||
|     screen->need_overwrite = false; | ||||
|     lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, | ||||
|                         LCD_DISPLAY_ON); | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * 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"); | ||||
|         } | ||||
|         screen->stage = (intptr_t) status; | ||||
|         screen->need_redraw = true; | ||||
|         return true; | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| static const char *FORMAT_STRINGS[] = { | ||||
|     "SQLITE", | ||||
|     "CSV" | ||||
| }; | ||||
| static const char *EXTENSION_STRINGS[] = { | ||||
|     "sqlite3", | ||||
|     "csv" | ||||
| }; | ||||
|  | ||||
| static char *export_build_cur_path(ExportScreen *screen) { | ||||
|     char *path = NULL; | ||||
|     pthread_cleanup_push(free, path); | ||||
|     asprintf_checked(&path, "%s/%s.%s", screen->drives[screen->cur_drive].mnt, | ||||
|                      GLOBAL_OPTS.export_file_name, | ||||
|                      EXTENSION_STRINGS[screen->format]); | ||||
|     pthread_cleanup_pop(false); | ||||
|     return path; | ||||
| } | ||||
|  | ||||
| static bool export_select_format(ExportScreen *screen, | ||||
|                                  SensorState *state) { | ||||
|     if (state->back_down) { | ||||
|         export_screen_reset(screen, state); | ||||
|         return true; | ||||
|     } | ||||
|     if (state->up_down || state->down_down) { | ||||
|         screen->need_redraw = true; | ||||
|         screen->format = abs((screen->format + state->up_down - | ||||
|                               state->down_down) % EXPORT_NFORMAT); | ||||
|     } | ||||
|     if (screen->need_redraw) { | ||||
|         screen->need_redraw = false; | ||||
|         lcd_display_control(state->lcd, LCD_CURSOR_BLINK, LCD_CURSOR_ON, | ||||
|                             LCD_DISPLAY_ON); | ||||
|         lcd_clear(state->lcd); | ||||
|         lcd_move_to(state->lcd, 0, 0); | ||||
|         lcd_write_string(state->lcd, "Format:"); | ||||
|         lcd_move_to(state->lcd, 1, 0); | ||||
|         lcd_write_char(state->lcd, '>'); | ||||
|         lcd_write_string(state->lcd, FORMAT_STRINGS[screen->format]); | ||||
|         lcd_move_to(state->lcd, 1, 0); | ||||
|     } | ||||
|     if (state->sel_down) { | ||||
|         ++screen->stage; | ||||
|         screen->need_redraw = true; | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| static void export_select_drive(ExportScreen *screen, | ||||
|                                 SensorState *state) { | ||||
|     if (state->back_down) { | ||||
|         --screen->stage; | ||||
|         screen->need_redraw = true; | ||||
|         return; | ||||
|     } | ||||
|     if (state->down_down && screen->cur_drive > 0) { | ||||
|         --screen->cur_drive; | ||||
|         screen->need_redraw = true; | ||||
|     } | ||||
|     if (state->up_down && screen->cur_drive < screen->ndrive - 1) { | ||||
|         ++screen->cur_drive; | ||||
|         screen->need_redraw = true; | ||||
|     } | ||||
|     if (screen->need_redraw) { | ||||
|         screen->need_redraw = false; | ||||
|         lcd_clear(state->lcd); | ||||
|         lcd_move_to(state->lcd, 0, 0); | ||||
|         lcd_write_string(state->lcd, "Drive:"); | ||||
|         lcd_move_to(state->lcd, 1, 0); | ||||
|         lcd_write_char(state->lcd, '>'); | ||||
|         lcd_write_string(state->lcd, screen->drives[screen->cur_drive].name); | ||||
|         lcd_write_char(state->lcd, ' '); | ||||
|         lcd_write_string(state->lcd, screen->drives[screen->cur_drive].fs); | ||||
|         lcd_move_to(state->lcd, 1, 0); | ||||
|     } | ||||
|     if (state->sel_down) { | ||||
|         screen->need_redraw = true; | ||||
|         if (!screen->drives[screen->cur_drive].fs) { | ||||
|             screen->stage = EXPORT_SCREEN_CONFIRM_NEWFS; | ||||
|         } else { | ||||
|             screen->stage = EXPORT_SCREEN_MOUNTING; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| __attribute__((format(printf, 3, 4))) | ||||
| static enum { | ||||
|     EXPORT_CONFIRM_CONTINUE = 0, | ||||
|     EXPORT_CONFIRM_NO, | ||||
|     EXPORT_CONFIRM_YES, | ||||
| } 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; | ||||
|     } | ||||
|     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) { | ||||
|         done_status = EXPORT_CONFIRM_YES; | ||||
|         goto confirm_done; | ||||
|     } | ||||
|     if (state->up_down || state->down_down) { | ||||
|         screen->confirm_state = abs((screen->confirm_state + state->up_down - | ||||
|                                      state->down_down) % 2); | ||||
|         screen->need_redraw = true; | ||||
|     } | ||||
|     if (screen->need_redraw) { | ||||
|         screen->need_redraw = false; | ||||
|         char message[17]; | ||||
|         va_list args; | ||||
|         va_start(args, fmt); | ||||
|         vsnprintf(message, 17, fmt, args); | ||||
|         va_end(args); | ||||
|         lcd_clear(state->lcd); | ||||
|         lcd_move_to(state->lcd, 0, 0); | ||||
|         lcd_write_string(state->lcd, message); | ||||
|         lcd_move_to(state->lcd, 1, 0); | ||||
|         lcd_write_string(state->lcd, ">No >Yes"); | ||||
|         lcd_display_control(state->lcd, LCD_CURSOR_BLINK, LCD_CURSOR_ON, | ||||
|                             LCD_DISPLAY_ON); | ||||
|         lcd_move_to(state->lcd, 1, 4 * screen->confirm_state); | ||||
|     } | ||||
|     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; | ||||
|     return done_status; | ||||
| } | ||||
|  | ||||
| static bool export_show_message(ExportScreen *screen, | ||||
|                                 SensorState *state, | ||||
|                                 const char *message) { | ||||
|     if (state->back_down || state->up_down || state->down_down || | ||||
|         state->sel_down) { | ||||
|         export_screen_reset(screen, state); | ||||
|         return true; | ||||
|     } | ||||
|     if (screen->need_redraw) { | ||||
|         lcd_clear(state->lcd); | ||||
|         lcd_move_to(state->lcd, 0, 0); | ||||
|         lcd_write_string(state->lcd, message); | ||||
|         screen->need_redraw = false; | ||||
|     } | ||||
|     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; | ||||
|         screen->need_redraw = true; | ||||
|         return; | ||||
|     } | ||||
|     time_t now = time(NULL); | ||||
|     if (export_check_bg_task(screen)) { | ||||
|         // bg task done | ||||
|         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]); | ||||
|     } | ||||
| } | ||||
|  | ||||
| static bool export_screen_dispatch(ExportScreen *screen, | ||||
|                                    SensorState *state) { | ||||
|     screen->db_cache = state->db; | ||||
|     if (state->force_draw) { | ||||
|         screen->need_redraw = true; | ||||
|     } | ||||
|     if (screen->read_drives) { | ||||
|         screen->drives = drive_list_usb_drives(&screen->ndrive); | ||||
|         if (screen->ndrive == 0) { | ||||
|             screen->stage = EXPORT_SCREEN_NO_DRIVES; | ||||
|             screen->need_redraw =true; | ||||
|         } else { | ||||
|             screen->cur_drive = 0; | ||||
|             screen->read_drives = false; | ||||
|         } | ||||
|     } | ||||
|     Drive *cur_drive = &screen->drives[screen->cur_drive]; | ||||
|     int confirm_status; | ||||
|     switch (screen->stage) { | ||||
|     case EXPORT_SCREEN_FORMAT: | ||||
|         return export_select_format(screen, state); | ||||
|     case EXPORT_SCREEN_DRIVE: | ||||
|         export_select_drive(screen, state); | ||||
|         break; | ||||
|     case EXPORT_SCREEN_CONFIRM_NEWFS: | ||||
|         confirm_status = export_confirm_action(screen, state, "Newfs? (%s)", | ||||
|                                                cur_drive->name); | ||||
|         if (confirm_status == EXPORT_CONFIRM_YES) { | ||||
|             screen->stage = EXPORT_SCREEN_WRITE_NEWFS; | ||||
|             screen->need_redraw = true; | ||||
|         } else if (confirm_status == EXPORT_CONFIRM_NO) { | ||||
|             screen->stage = EXPORT_SCREEN_DRIVE; | ||||
|             screen->need_redraw = true; | ||||
|         } | ||||
|         break; | ||||
|     case EXPORT_SCREEN_WRITE_NEWFS: | ||||
|         export_do_background_action(screen, state, export_newfs_action); | ||||
|         break; | ||||
|     case EXPORT_SCREEN_MOUNTING: | ||||
|         export_do_background_action(screen, state, export_mount_action); | ||||
|         break; | ||||
|     case EXPORT_SCREEN_CONFIRM_OVERWRITE: | ||||
|         confirm_status = export_confirm_action(screen, state, "Overwrite?"); | ||||
|         if (confirm_status == EXPORT_CONFIRM_YES) { | ||||
|             screen->stage = EXPORT_SCREEN_WRITING; | ||||
|             screen->need_redraw = true; | ||||
|         } else if (confirm_status == EXPORT_CONFIRM_NO) { | ||||
|             screen->stage = EXPORT_SCREEN_DRIVE; | ||||
|             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); | ||||
|         } | ||||
|         break; | ||||
|     case EXPORT_SCREEN_WRITING: | ||||
|         export_do_background_action(screen, state, export_write_action); | ||||
|         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); | ||||
|             drive_unmount(cur_drive, true); | ||||
|             screen->stage = EXPORT_SCREEN_CANCELED; | ||||
|             screen->need_redraw = true; | ||||
|         } else if (confirm_status == EXPORT_CONFIRM_NO) { | ||||
|             screen->stage = screen->last_cancel_stage; | ||||
|             screen->need_redraw = true; | ||||
|         } | ||||
|         break; | ||||
|     case EXPORT_SCREEN_CANCELED: | ||||
|         return export_show_message(screen, state, "Cancelled!"); | ||||
|     case EXPORT_SCREEN_FAILED: | ||||
|         return export_show_message(screen, state, "Failed!"); | ||||
|     case EXPORT_SCREEN_DONE: | ||||
|         return export_show_message(screen, state, "Export Done!"); | ||||
|     case EXPORT_SCREEN_NO_DRIVES: | ||||
|         return export_show_message(screen, state, "No drives found!"); | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| ExportScreen *export_screen_new() { | ||||
|     ExportScreen *s = malloc_checked(sizeof(ExportScreen)); | ||||
|     screen_init(&s->parent, "Export", | ||||
|                 (ScreenDispatchFunc) export_screen_dispatch, | ||||
|                 (ScreenCleanupFunc) free); | ||||
|     s->read_drives = true; | ||||
|     s->stage = EXPORT_SCREEN_FORMAT; | ||||
|     s->format = EXPORT_FORMAT_SQLITE; | ||||
|     return s; | ||||
| } | ||||
							
								
								
									
										61
									
								
								src/ui/exportscreen.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/ui/exportscreen.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | ||||
| /* | ||||
|  * exportscreen.h - Screen for exporting to USB drives | ||||
|  * 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_EXPORTSCREEN_H | ||||
| #define INCLUDED_EXPORTSCREEN_H | ||||
|  | ||||
| #include "screen.h" | ||||
| #include "../drive.h" | ||||
|  | ||||
| #include <pthread.h> | ||||
| #include <time.h> | ||||
| #include <stdatomic.h> | ||||
| #include <sqlite3.h> | ||||
|  | ||||
| typedef struct { | ||||
|     Screen parent; | ||||
|     enum { | ||||
|         EXPORT_SCREEN_FORMAT = 0, | ||||
|         EXPORT_SCREEN_DRIVE, | ||||
|         EXPORT_SCREEN_CONFIRM_NEWFS, | ||||
|         EXPORT_SCREEN_WRITE_NEWFS, | ||||
|         EXPORT_SCREEN_MOUNTING, | ||||
|         EXPORT_SCREEN_CONFIRM_OVERWRITE, | ||||
|         EXPORT_SCREEN_WRITING, | ||||
|         EXPORT_SCREEN_DONE, | ||||
|         EXPORT_SCREEN_CONFIRM_CANCEL, | ||||
|         EXPORT_SCREEN_CANCELED, | ||||
|         EXPORT_SCREEN_FAILED, | ||||
|         EXPORT_SCREEN_NO_DRIVES, | ||||
|     } stage; | ||||
|     enum { | ||||
|         EXPORT_FORMAT_SQLITE = 0, | ||||
|         EXPORT_FORMAT_CSV, | ||||
|         EXPORT_NFORMAT, | ||||
|     }; | ||||
|     int format; | ||||
|     bool need_redraw; | ||||
|  | ||||
|     bool read_drives; | ||||
|     Drive *drives; | ||||
|     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; | ||||
| } ExportScreen; | ||||
|  | ||||
| ExportScreen *export_screen_new(void); | ||||
|  | ||||
| #endif | ||||
| @ -24,7 +24,7 @@ void screen_init(Screen *screen, const char *name, | ||||
| } | ||||
|  | ||||
| void screen_delete(Screen *screen) { | ||||
|     FREE_CHECKED(screen->name); | ||||
|     free(screen->name); | ||||
|     if (screen->cleanup_func) { | ||||
|         screen->cleanup_func(screen); | ||||
|     } | ||||
| @ -53,7 +53,7 @@ void screen_manager_delete(ScreenManager *sm) { | ||||
|     for (size_t i = 0; i < sm->screen_count; ++i) { | ||||
|         screen_delete(sm->screens[i]); | ||||
|     } | ||||
|     FREE_CHECKED(sm->screens); | ||||
|     free(sm->screens); | ||||
|     button_delete(sm->up_btn); | ||||
|     button_delete(sm->down_btn); | ||||
|     button_delete(sm->back_btn); | ||||
| @ -115,6 +115,7 @@ void screen_manager_dispatch(ScreenManager *sm, uint32_t temp, uint32_t humid) { | ||||
|         if (cs->dispatch_func) { | ||||
|             SensorState state = { | ||||
|                 .lcd = sm->lcd, | ||||
|                 .db = sm->db, | ||||
|                 .temp = temp, | ||||
|                 .humid = humid, | ||||
|                 .back_down = button_pressed(sm->back_btn), | ||||
|  | ||||
							
								
								
									
										160
									
								
								src/util.c
									
									
									
									
									
								
							
							
						
						
									
										160
									
								
								src/util.c
									
									
									
									
									
								
							| @ -12,6 +12,12 @@ | ||||
| #include <err.h> | ||||
| #include <string.h> | ||||
| #include <stdlib.h> | ||||
| #include <stdarg.h> | ||||
| #include <sys/stat.h> | ||||
| #include <dirent.h> | ||||
| #include <unistd.h> | ||||
| #include <fcntl.h> | ||||
| #include <pthread.h> | ||||
|  | ||||
| const char *PERIOD_LABELS[] = { | ||||
|     "HOUR", | ||||
| @ -25,12 +31,13 @@ const size_t NPERIOD = sizeof(PERIOD_LABELS) / sizeof(char *); | ||||
| Options GLOBAL_OPTS; | ||||
|  | ||||
| void cleanup_options(Options *opts) { | ||||
|     FREE_CHECKED(opts->config_path); | ||||
|     FREE_CHECKED(opts->gpio_path); | ||||
|     FREE_CHECKED(opts->temp_key); | ||||
|     FREE_CHECKED(opts->humid_key); | ||||
|     FREE_CHECKED(opts->fail_key); | ||||
|     FREE_CHECKED(opts->database_location); | ||||
|     free(opts->config_path); | ||||
|     free(opts->gpio_path); | ||||
|     free(opts->temp_key); | ||||
|     free(opts->humid_key); | ||||
|     free(opts->fail_key); | ||||
|     free(opts->database_location); | ||||
|     free(opts->export_file_name); | ||||
| } | ||||
|  | ||||
| void *malloc_checked(size_t n) { | ||||
| @ -56,6 +63,17 @@ void *strdup_checked(const char *str) { | ||||
|     return new_str; | ||||
| } | ||||
|  | ||||
| int asprintf_checked(char **str, const char *format, ...) { | ||||
|     va_list args; | ||||
|     va_start(args, format); | ||||
|     int retval = vasprintf(str, format, args); | ||||
|     if (!*str) { | ||||
|         errx(1, "out of memory!"); | ||||
|     } | ||||
|     va_end(args); | ||||
|     return retval; | ||||
| } | ||||
|  | ||||
| int days_in_month(int m, int y) { | ||||
|     switch (m) { | ||||
|     case 2: | ||||
| @ -160,6 +178,9 @@ static const char *AVG_FOR_RANGE_QUERY_STR = | ||||
|     "       max(temp), min(temp), max(humid), min(humid) FROM env_data\n" | ||||
|     "WHERE time >= ?1 AND time <= ?2;"; | ||||
| static sqlite3_stmt *AVG_FOR_RANGE_QUERY; | ||||
| static const char *EXPORT_CSV_QUERY_STR = | ||||
|     "select printf('%d,%d,%d', time, temp, humid) from env_data;"; | ||||
| static sqlite3_stmt *EXPORT_CSV_QUERY; | ||||
| void initialize_util_queries(sqlite3 *db) { | ||||
|     int status = sqlite3_prepare_v2(db, DB_LIMITS_QUERY_STR, -1, | ||||
|                                     &DB_LIMITS_QUERY, NULL); | ||||
| @ -181,6 +202,11 @@ void initialize_util_queries(sqlite3 *db) { | ||||
|     if (status != SQLITE_OK) { | ||||
|         errx(1, "failed to compile range average query: %s", sqlite3_errstr(status)); | ||||
|     } | ||||
|     status = sqlite3_prepare_v2(db, EXPORT_CSV_QUERY_STR, -1, | ||||
|                                 &EXPORT_CSV_QUERY, NULL); | ||||
|     if (status != SQLITE_OK) { | ||||
|         errx(1, "failed to compile CSV export query: %s", sqlite3_errstr(status)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void cleanup_util_queries() { | ||||
| @ -188,6 +214,7 @@ void cleanup_util_queries() { | ||||
|     sqlite3_finalize(AVG_FOR_PERIOD_QUERY); | ||||
|     sqlite3_finalize(DATA_POINT_QUERY); | ||||
|     sqlite3_finalize(AVG_FOR_RANGE_QUERY); | ||||
|     sqlite3_finalize(EXPORT_CSV_QUERY); | ||||
| } | ||||
|  | ||||
| bool get_database_limits(sqlite3 *db, UtilPeriod period, UtilDate *start, | ||||
| @ -379,3 +406,124 @@ 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); | ||||
|     } | ||||
|     struct stat statbuf; | ||||
|     if (fstatat(dfd, path, &statbuf, 0) < 0) { | ||||
|         warn("could not stat \"%s\" for removal", path); | ||||
|         return false; | ||||
|     } | ||||
|     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); | ||||
|         if (!dir) { | ||||
|             close(fd); | ||||
|             warn("fdopendir \"%s\"", path); | ||||
|             return false; | ||||
|         } | ||||
|         struct dirent *sd; | ||||
|         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; | ||||
|             } | ||||
|         } | ||||
|         pthread_cleanup_pop(true); | ||||
|         if (error) { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|     if (unlinkat(dfd, path, flag)< 0) { | ||||
|         warn("remove \"%s\"", path); | ||||
|         return false; | ||||
|     } | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| bool recursive_rm(const char *path) { | ||||
|     return recursive_rm_at(AT_FDCWD, 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; | ||||
|     } | ||||
|     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; | ||||
|     }  | ||||
|     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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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"); | ||||
|     if (!outfile) { | ||||
|         warn("fopen \"%s\"", dest); | ||||
|         status = false; | ||||
|         goto cleanup; | ||||
|     } | ||||
|     int res; | ||||
|     while ((res = sqlite3_step(EXPORT_CSV_QUERY)) == SQLITE_ROW) { | ||||
|         const unsigned char *test = sqlite3_column_text(EXPORT_CSV_QUERY, 0); | ||||
|         fputs((const char *) test, outfile); | ||||
|         fputc('\n', outfile); // avoid flush | ||||
|     } | ||||
|     if (res != SQLITE_DONE) { | ||||
|         warnx("CSV export to \"%s\" faield: %s\n", dest, | ||||
|               sqlite3_errstr(res)); | ||||
|         status = false; | ||||
|         goto cleanup; // readability | ||||
|     } | ||||
|  cleanup: | ||||
|     pthread_cleanup_pop(true); // reset query | ||||
|     pthread_cleanup_pop(true); // close file | ||||
|     return status; | ||||
| } | ||||
|  | ||||
							
								
								
									
										33
									
								
								src/util.h
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								src/util.h
									
									
									
									
									
								
							| @ -49,6 +49,8 @@ typedef struct { | ||||
|     gpio_pin_t sel_pin; | ||||
|  | ||||
|     TemperatureUnit temp_unit; | ||||
|  | ||||
|     char *export_file_name; // file to export to | ||||
| } Options; | ||||
| extern Options GLOBAL_OPTS; | ||||
|  | ||||
| @ -76,9 +78,11 @@ void *realloc_checked(void *old_ptr, size_t n); | ||||
| void *strdup_checked(const char *str); | ||||
|  | ||||
| /* | ||||
|  * Like free(3), but do nothing if P is NULL. | ||||
|  * Like asprintf(3), except that if allocation fails, an error will be written to | ||||
|  * standard error and abort(3) called | ||||
|  */ | ||||
| #define FREE_CHECKED(p) if (p) {free(p);} | ||||
| int asprintf_checked(char **str, const char *format, ...) | ||||
| __attribute__((format(printf, 2, 3))); | ||||
|  | ||||
| /* | ||||
|  * Call fprintf(3) to stderr only if verbose mode was enabled. | ||||
| @ -210,4 +214,29 @@ float convert_temperature(int dk); | ||||
|  */ | ||||
| char *pad_humid_str(int humid, char *buf, size_t buf_size); | ||||
|  | ||||
| /* | ||||
|  * Like recursive_rm (see below), except that PATH is resolved relative to DFD. | ||||
|  */ | ||||
| bool recursive_rm_at(int dfd, const char *path); | ||||
|  | ||||
| /* | ||||
|  * Call recursive_rm on all of FILE's children, the remove FILE | ||||
|  * Return: false on error, true otherwise | ||||
|  */ | ||||
| bool recursive_rm(const char *path); | ||||
|  | ||||
| /* | ||||
|  * Use the sqlite3 backup API to export DB to dest. This function is safe to | ||||
|  * call in a multi-threaded environment where pthread_cancel may be used. | ||||
|  * Return: true on success, false on error | ||||
|  */ | ||||
| bool export_database(sqlite3 *db, const char *dest); | ||||
|  | ||||
| /* | ||||
|  * Export DB as a CSV file to DEST. This function is safe to call in a | ||||
|  * multi-threaded environment where pthread_cancel may be used. | ||||
|  * Return: true on success, false on error | ||||
|  */ | ||||
| bool export_database_as_csv(sqlite3 *db, const char *dest); | ||||
|  | ||||
| #endif | ||||
|  | ||||
		Reference in New Issue
	
	Block a user