blob: 1f977403d6542086af2bdb3ca4a52ed096828442 [file] [log] [blame]
/*
* Copyright (c) 2020 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <zephyr/logging/log_backend.h>
#include <zephyr/logging/log_output_dict.h>
#include <zephyr/logging/log_backend_std.h>
#include <assert.h>
#include <zephyr/fs/fs.h>
#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 fs_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);
#ifndef CONFIG_LOG_BACKEND_FS_TESTSUITE
static uint32_t log_format_current = CONFIG_LOG_BACKEND_FS_OUTPUT_DEFAULT;
#endif
static int check_log_volume_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) {
rc = -EIO;
goto close_dir;
}
if (ent.name[0] == 0) {
break;
}
rc = get_log_file_id(&ent);
if (rc == num) {
rc = 1;
goto close_dir;
}
}
rc = 0;
close_dir:
(void) fs_closedir(&dir);
return rc;
}
int write_log_to_file(uint8_t *data, size_t length, void *ctx)
{
int rc;
struct fs_file_t *f = &fs_file;
if (backend_state == BACKEND_FS_NOT_INITIALIZED) {
if (check_log_volume_available()) {
return length;
}
rc = create_log_dir(CONFIG_LOG_BACKEND_FS_DIR);
if (!rc) {
rc = allocate_new_file(&fs_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];
off_t file_size;
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;
/* Is there space left in the newest file? */
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 | FS_O_APPEND);
if (rc < 0) {
goto out;
}
file_size = fs_tell(file);
if (IS_ENABLED(CONFIG_LOG_BACKEND_FS_APPEND_TO_NEWEST_FILE) &&
file_size < CONFIG_LOG_BACKEND_FS_FILE_SIZE) {
/* There is space left to log to the latest file, no need to create
* a new one or delete old ones at this point.
*/
if (file_ctr == 0) {
++file_ctr;
}
backend_state = BACKEND_FS_OK;
goto out;
} else {
fs_close(file);
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 log_backend_fs_init(const struct log_backend *const backend)
{
}
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);
if (IS_ENABLED(CONFIG_LOG_BACKEND_FS_OUTPUT_DICTIONARY)) {
log_dict_output_dropped_process(&log_output, cnt);
} else {
log_backend_std_dropped(&log_output, cnt);
}
}
static void process(const struct log_backend *const backend,
union log_msg_generic *msg)
{
uint32_t flags = log_backend_std_get_flags();
log_format_func_t log_output_func = log_format_func_t_get(log_format_current);
log_output_func(&log_output, &msg->log, flags);
}
static int format_set(const struct log_backend *const backend, uint32_t log_type)
{
log_format_current = log_type;
return 0;
}
static const struct log_backend_api log_backend_fs_api = {
.process = process,
.panic = panic,
.init = log_backend_fs_init,
.dropped = dropped,
.format_set = format_set,
};
LOG_BACKEND_DEFINE(log_backend_fs, log_backend_fs_api,
IS_ENABLED(CONFIG_LOG_BACKEND_FS_AUTOSTART));
#endif