/* * Copyright (c) 2020 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include #include #include #include #define MAX_PATH_LEN 256 #define MAX_FLASH_WRITE_SIZE 256 #define LOG_PREFIX_LEN (sizeof(CONFIG_LOG_BACKEND_FS_FILE_PREFIX) - 1) #define MAX_FILE_NUMERAL 9999 #define FILE_NUMERAL_LEN 4 enum backend_fs_state { BACKEND_FS_NOT_INITIALIZED = 0, BACKEND_FS_CORRUPTED, BACKEND_FS_OK }; static struct fs_file_t file; static enum backend_fs_state backend_state = BACKEND_FS_NOT_INITIALIZED; static int file_ctr, newest, oldest; static int allocate_new_file(struct fs_file_t *file); static int del_oldest_log(void); static int get_log_file_id(struct fs_dirent *ent); static int check_log_volumen_available(void) { int index = 0; char const *name; int rc = 0; while (rc == 0) { rc = fs_readmount(&index, &name); if (rc == 0) { if (strncmp(CONFIG_LOG_BACKEND_FS_DIR, name, strlen(name)) == 0) { return 0; } } } return -ENOENT; } static int create_log_dir(const char *path) { const char *next; const char *last = path + (strlen(path) - 1); char w_path[MAX_PATH_LEN]; int rc, len; struct fs_dir_t dir; fs_dir_t_init(&dir); /* the fist directory name is the mount point*/ /* the firs path's letter might be meaningless `/`, let's skip it */ next = strchr(path + 1, '/'); if (!next) { return 0; } while (true) { next++; if (next > last) { return 0; } next = strchr(next, '/'); if (!next) { next = last; len = last - path + 1; } else { len = next - path; } memcpy(w_path, path, len); w_path[len] = 0; rc = fs_opendir(&dir, w_path); if (rc) { /* assume directory doesn't exist */ rc = fs_mkdir(w_path); if (rc) { break; } } rc = fs_closedir(&dir); if (rc) { break; } } return rc; } static int check_log_file_exist(int num) { struct fs_dir_t dir; struct fs_dirent ent; int rc; fs_dir_t_init(&dir); rc = fs_opendir(&dir, CONFIG_LOG_BACKEND_FS_DIR); if (rc) { return -EIO; } while (true) { rc = fs_readdir(&dir, &ent); if (rc < 0) { (void) fs_closedir(&dir); return -EIO; } if (ent.name[0] == 0) { break; } rc = get_log_file_id(&ent); if (rc == num) { return 1; } } (void) fs_closedir(&dir); return 0; } int write_log_to_file(uint8_t *data, size_t length, void *ctx) { int rc; struct fs_file_t *f = &file; if (backend_state == BACKEND_FS_NOT_INITIALIZED) { if (check_log_volumen_available()) { return length; } rc = create_log_dir(CONFIG_LOG_BACKEND_FS_DIR); if (!rc) { rc = allocate_new_file(&file); } backend_state = (rc ? BACKEND_FS_CORRUPTED : BACKEND_FS_OK); } if (backend_state == BACKEND_FS_OK) { /* Check if new data overwrites max file size. * If so, create new log file. */ int size = fs_tell(f); if (size < 0) { backend_state = BACKEND_FS_CORRUPTED; return length; } else if ((size + length) > CONFIG_LOG_BACKEND_FS_FILE_SIZE) { rc = allocate_new_file(f); if (rc < 0) { goto on_error; } } rc = fs_write(f, data, length); if (rc >= 0) { if (IS_ENABLED(CONFIG_LOG_BACKEND_FS_OVERWRITE) && (rc != length)) { del_oldest_log(); return 0; } /* If overwrite is disabled, full memory * cause the log record abandonment. */ length = rc; } else { rc = check_log_file_exist(newest); if (rc == 0) { /* file was lost somehow * try to get a new one */ file_ctr--; rc = allocate_new_file(f); if (rc < 0) { goto on_error; } } else if (rc < 0) { /* fs is corrupted*/ goto on_error; } length = 0; } rc = fs_sync(f); if (rc < 0) { /* Something is wrong */ goto on_error; } } return length; on_error: backend_state = BACKEND_FS_CORRUPTED; return length; } static int get_log_file_id(struct fs_dirent *ent) { size_t len; int num; if (ent->type != FS_DIR_ENTRY_FILE) { return -1; } len = strlen(ent->name); if (len != LOG_PREFIX_LEN + FILE_NUMERAL_LEN) { return -1; } if (memcmp(ent->name, CONFIG_LOG_BACKEND_FS_FILE_PREFIX, LOG_PREFIX_LEN) != 0) { return -1; } num = atoi(ent->name + LOG_PREFIX_LEN); if (num <= MAX_FILE_NUMERAL && num > 0) { return num; } return -1; } static int allocate_new_file(struct fs_file_t *file) { /* In case of no log file or current file fills up * create new log file. */ int rc; struct fs_statvfs stat; int curr_file_num; struct fs_dirent ent; char fname[MAX_PATH_LEN]; assert(file); if (backend_state == BACKEND_FS_NOT_INITIALIZED) { /* Search for the last used log number. */ struct fs_dir_t dir; int file_num = 0; fs_dir_t_init(&dir); curr_file_num = 0; int max = 0, min = MAX_FILE_NUMERAL; rc = fs_opendir(&dir, CONFIG_LOG_BACKEND_FS_DIR); while (rc >= 0) { rc = fs_readdir(&dir, &ent); if ((rc < 0) || (ent.name[0] == 0)) { break; } file_num = get_log_file_id(&ent); if (file_num > 0) { if (file_num > max) { max = file_num; } if (file_num < min) { min = file_num; } ++file_ctr; } } oldest = min; if ((file_ctr > 1) && ((max - min) > 2 * CONFIG_LOG_BACKEND_FS_FILES_LIMIT)) { /* oldest log is in the range around the min */ newest = min; oldest = max; (void)fs_closedir(&dir); rc = fs_opendir(&dir, CONFIG_LOG_BACKEND_FS_DIR); while (rc == 0) { rc = fs_readdir(&dir, &ent); if ((rc < 0) || (ent.name[0] == 0)) { break; } file_num = get_log_file_id(&ent); if (file_num < min + CONFIG_LOG_BACKEND_FS_FILES_LIMIT) { if (newest < file_num) { newest = file_num; } } if (file_num > max - CONFIG_LOG_BACKEND_FS_FILES_LIMIT) { if (oldest > file_num) { oldest = file_num; } } } } else { newest = max; oldest = min; } (void)fs_closedir(&dir); if (rc < 0) { goto out; } curr_file_num = newest; if (file_ctr >= 1) { curr_file_num++; if (curr_file_num > MAX_FILE_NUMERAL) { curr_file_num = 0; } } backend_state = BACKEND_FS_OK; } else { fs_close(file); curr_file_num = newest; curr_file_num++; if (curr_file_num > MAX_FILE_NUMERAL) { curr_file_num = 0; } } rc = fs_statvfs(CONFIG_LOG_BACKEND_FS_DIR, &stat); /* Check if there is enough space to write file or max files number * is not exceeded. */ while ((file_ctr >= CONFIG_LOG_BACKEND_FS_FILES_LIMIT) || ((stat.f_bfree * stat.f_frsize) <= CONFIG_LOG_BACKEND_FS_FILE_SIZE)) { if (IS_ENABLED(CONFIG_LOG_BACKEND_FS_OVERWRITE)) { rc = del_oldest_log(); if (rc < 0) { goto out; } rc = fs_statvfs(CONFIG_LOG_BACKEND_FS_DIR, &stat); if (rc < 0) { goto out; } } else { return -ENOSPC; } } snprintf(fname, sizeof(fname), "%s/%s%04d", CONFIG_LOG_BACKEND_FS_DIR, CONFIG_LOG_BACKEND_FS_FILE_PREFIX, curr_file_num); rc = fs_open(file, fname, FS_O_CREATE | FS_O_WRITE); if (rc < 0) { goto out; } ++file_ctr; newest = curr_file_num; out: return rc; } static int del_oldest_log(void) { int rc; static char dellname[MAX_PATH_LEN]; while (true) { snprintf(dellname, sizeof(dellname), "%s/%s%04d", CONFIG_LOG_BACKEND_FS_DIR, CONFIG_LOG_BACKEND_FS_FILE_PREFIX, oldest); rc = fs_unlink(dellname); if ((rc == 0) || (rc == -ENOENT)) { oldest++; if (oldest > MAX_FILE_NUMERAL) { oldest = 0; } if (rc == 0) { --file_ctr; break; } } else { break; } } return rc; } BUILD_ASSERT(!IS_ENABLED(CONFIG_LOG_MODE_IMMEDIATE), "Immediate logging is not supported by LOG FS backend."); #ifndef CONFIG_LOG_BACKEND_FS_TESTSUITE static uint8_t __aligned(4) buf[MAX_FLASH_WRITE_SIZE]; LOG_OUTPUT_DEFINE(log_output, write_log_to_file, buf, MAX_FLASH_WRITE_SIZE); static void put(const struct log_backend *const backend, struct log_msg *msg) { log_backend_std_put(&log_output, 0, msg); } static void log_backend_fs_init(void) { } static void panic(struct log_backend const *const backend) { /* In case of panic deinitialize backend. It is better to keep * current data rather than log new and risk of failure. */ log_backend_deactivate(backend); } static void dropped(const struct log_backend *const backend, uint32_t cnt) { ARG_UNUSED(backend); log_backend_std_dropped(&log_output, cnt); } static const struct log_backend_api log_backend_fs_api = { .put = put, .put_sync_string = NULL, .put_sync_hexdump = NULL, .panic = panic, .init = log_backend_fs_init, .dropped = dropped, }; LOG_BACKEND_DEFINE(log_backend_fs, log_backend_fs_api, true); #endif