diff --git a/Makefile.dep b/Makefile.dep index b909cf125f..bde52cff28 100644 --- a/Makefile.dep +++ b/Makefile.dep @@ -601,6 +601,10 @@ ifneq (,$(filter constfs,$(USEMODULE))) USEMODULE += vfs endif +ifneq (,$(filter devfs,$(USEMODULE))) + USEMODULE += vfs +endif + # include package dependencies -include $(USEPKG:%=$(RIOTPKG)/%/Makefile.dep) diff --git a/sys/Makefile b/sys/Makefile index 8e4f043577..78a120f685 100644 --- a/sys/Makefile +++ b/sys/Makefile @@ -116,6 +116,10 @@ ifneq (,$(filter constfs,$(USEMODULE))) DIRS += fs/constfs endif +ifneq (,$(filter devfs,$(USEMODULE))) + DIRS += fs/devfs +endif + DIRS += $(dir $(wildcard $(addsuffix /Makefile, ${USEMODULE}))) include $(RIOTBASE)/Makefile.base diff --git a/sys/auto_init/auto_init.c b/sys/auto_init/auto_init.c index c7813baf1e..5169fa97d9 100644 --- a/sys/auto_init/auto_init.c +++ b/sys/auto_init/auto_init.c @@ -152,6 +152,11 @@ void auto_init(void) DEBUG("Auto init gcoap module.\n"); gcoap_init(); #endif +#ifdef MODULE_DEVFS + DEBUG("Mounting /dev\n"); + extern void auto_init_devfs(void); + auto_init_devfs(); +#endif /* initialize network devices */ #ifdef MODULE_AUTO_INIT_GNRC_NETIF diff --git a/sys/fs/devfs/Makefile b/sys/fs/devfs/Makefile new file mode 100644 index 0000000000..67d6ef1eb3 --- /dev/null +++ b/sys/fs/devfs/Makefile @@ -0,0 +1,2 @@ +MODULE=devfs +include $(RIOTBASE)/Makefile.base diff --git a/sys/fs/devfs/auto_init_devfs.c b/sys/fs/devfs/auto_init_devfs.c new file mode 100644 index 0000000000..9de9b57057 --- /dev/null +++ b/sys/fs/devfs/auto_init_devfs.c @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 Eistec AB + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + * + */ + +/** + * @ingroup auto_init_fs + * @{ + * + * @file + * @brief Automatic mount of DevFS on /dev + * + * @author Joakim Nohlgård + * + * @} + */ + +#include "vfs.h" +#include "fs/devfs.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +static vfs_mount_t _devfs_auto_init_mount = { + .fs = &devfs_file_system, + .mount_point = "/dev", +}; + +void auto_init_devfs(void) +{ + DEBUG("auto_init_devfs: mounting /dev\n"); + vfs_mount(&_devfs_auto_init_mount); +} diff --git a/sys/fs/devfs/devfs.c b/sys/fs/devfs/devfs.c new file mode 100644 index 0000000000..07dc70b826 --- /dev/null +++ b/sys/fs/devfs/devfs.c @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2016 Eistec AB + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup fs_devfs + * @{ + * + * @file + * @brief DevFS implementation + * + * @author Joakim Nohlgård + * + * @} + */ + +/* Required for strnlen in string.h, when building with -std=c99 */ +#define _DEFAULT_SOURCE 1 +#include +#include +#include +#include + +#include "fs/devfs.h" +#include "vfs.h" +#include "mutex.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +/** + * @internal + * @brief DevFS list head + * + * DevFS operates as a singleton, the same files show up in all mounted instances. + */ +static clist_node_t _devfs_list; +/** + * @internal + * @brief mutex to protect the DevFS list from corruption + */ +static mutex_t _devfs_mutex = MUTEX_INIT; + +/* No need for file system operations, no extra work to be done on + * mount/umount. unlink is not permitted, use devfs_unregister instead */ + +/* File operations */ +/* open is overloaded to allow searching for the correct device */ +static int devfs_open(vfs_file_t *filp, const char *name, int flags, mode_t mode, const char *abs_path); +/* A minimal fcntl is also provided to enable SETFL handling */ +static int devfs_fcntl(vfs_file_t *filp, int cmd, int arg); + +/* Directory operations */ +static int devfs_opendir(vfs_DIR *dirp, const char *dirname, const char *abs_path); +static int devfs_readdir(vfs_DIR *dirp, vfs_dirent_t *entry); +static int devfs_closedir(vfs_DIR *dirp); + +static const vfs_file_ops_t devfs_file_ops = { + .open = devfs_open, + .fcntl = devfs_fcntl, +}; + +static const vfs_dir_ops_t devfs_dir_ops = { + .opendir = devfs_opendir, + .readdir = devfs_readdir, + .closedir = devfs_closedir, +}; + +const vfs_file_system_t devfs_file_system = { + .f_op = &devfs_file_ops, + .d_op = &devfs_dir_ops, +}; + +static int devfs_open(vfs_file_t *filp, const char *name, int flags, mode_t mode, const char *abs_path) +{ + DEBUG("devfs_open: %p, \"%s\", 0x%x, 0%03lo, \"%s\"\n", (void *)filp, name, flags, (unsigned long)mode, abs_path); + /* linear search through the device list */ + mutex_lock(&_devfs_mutex); + clist_node_t *it = _devfs_list.next; + if (it == NULL) { + /* list empty */ + mutex_unlock(&_devfs_mutex); + return -ENOENT; + } + do { + it = it->next; + devfs_t *devp = container_of(it, devfs_t, list_entry); + if (strcmp(devp->path, name) == 0) { + mutex_unlock(&_devfs_mutex); + DEBUG("devfs_open: Found :)\n"); + /* Add private data from DevFS node */ + filp->private_data.ptr = devp->private_data; + /* Replace f_op with the operations provided by the device driver */ + filp->f_op = devp->f_op; + /* Chain the open() method for the specific device */ + if (filp->f_op->open != NULL) { + return filp->f_op->open(filp, name, flags, mode, abs_path); + } + return 0; + } + } while (it != _devfs_list.next); + mutex_unlock(&_devfs_mutex); + DEBUG("devfs_open: Not found :(\n"); + return -ENOENT; +} + +static int devfs_fcntl(vfs_file_t *filp, int cmd, int arg) +{ + DEBUG("devfs_fcntl: %p, 0x%x, 0x%x\n", (void *)filp, cmd, arg); + switch (cmd) { + /* F_GETFL is handled directly by vfs_fcntl */ + case F_SETFL: + DEBUG("devfs_fcntl: SETFL: %d\n", arg); + filp->flags = arg; + return filp->flags; + default: + return -EINVAL; + } +} + +static int devfs_opendir(vfs_DIR *dirp, const char *dirname, const char *abs_path) +{ + (void) abs_path; + DEBUG("devfs_opendir: %p, \"%s\", \"%s\"\n", (void *)dirp, dirname, abs_path); + if (strncmp(dirname, "/", 2) != 0) { + /* We keep it simple and only support a flat file system, only a root directory */ + return -ENOENT; + } + dirp->private_data.ptr = NULL; + return 0; +} + +static int devfs_readdir(vfs_DIR *dirp, vfs_dirent_t *entry) +{ + DEBUG("devfs_readdir: %p, %p\n", (void *)dirp, (void *)entry); + mutex_lock(&_devfs_mutex); + clist_node_t *it = dirp->private_data.ptr; + if (it == _devfs_list.next) { + /* end of list was reached */ + mutex_unlock(&_devfs_mutex); + return 0; + } + if (it == NULL) { + /* first readdir after opendir */ + it = _devfs_list.next; + if (it == NULL) { + /* empty list */ + mutex_unlock(&_devfs_mutex); + return 0; + } + } + it = it->next; + dirp->private_data.ptr = it; + mutex_unlock(&_devfs_mutex); + devfs_t *devp = container_of(it, devfs_t, list_entry); + if (devp->path == NULL) { + /* skip past the broken entry and try again */ + return -EAGAIN; + } + size_t len = strnlen(devp->path, VFS_NAME_MAX + 1); + if (len > VFS_NAME_MAX) { + /* name does not fit in vfs_dirent_t buffer */ + /* skip past the broken entry and try again */ + return -EAGAIN; + } + /* clear the dirent */ + memset(entry, 0, sizeof(*entry)); + /* copy the string, including terminating null */ + memcpy(&entry->d_name[0], devp->path, len + 1); + return 1; +} + +static int devfs_closedir(vfs_DIR *dirp) +{ + /* Just an example, it's not necessary to define closedir if there is + * nothing to clean up */ + (void) dirp; + DEBUG("devfs_closedir: %p\n", (void *)dirp); + return 0; +} + +int devfs_register(devfs_t *devp) +{ + DEBUG("devfs_register: %p\n", (void *)devp); + if (devp == NULL) { + return -EINVAL; + } + DEBUG("devfs_register: \"%s\" -> (%p, %p)\n", devp->path, (void *)devp->f_op, devp->private_data); + if (devp->path == NULL) { + return -EINVAL; + } + if (devp->f_op == NULL) { + return -EINVAL; + } + mutex_lock(&_devfs_mutex); + clist_node_t *it = _devfs_list.next; + if (it != NULL) { + /* list not empty */ + do { + it = it->next; + if (it == &devp->list_entry) { + /* Already registered */ + mutex_unlock(&_devfs_mutex); + DEBUG("devfs_register: %p already registered\n", (void *)devp); + return -EEXIST; + } + devfs_t *devit = container_of(it, devfs_t, list_entry); + if (strcmp(devit->path, devp->path) == 0) { + /* Path already registered */ + mutex_unlock(&_devfs_mutex); + DEBUG("devfs_register: \"%s\" occupied\n", devp->path); + return -EEXIST; + } + } while(it != _devfs_list.next); + } + /* insert last in list */ + clist_rpush(&_devfs_list, &devp->list_entry); + mutex_unlock(&_devfs_mutex); + return 0; +} + +int devfs_unregister(devfs_t *devp) +{ + DEBUG("devfs_unregister: %p\n", (void *)devp); + if (devp == NULL) { + return -EINVAL; + } + mutex_lock(&_devfs_mutex); + /* find devp in the list and remove it */ + clist_node_t *node = clist_remove(&_devfs_list, &devp->list_entry); + mutex_unlock(&_devfs_mutex); + if (node == NULL) { + /* not found */ + DEBUG("devfs_unregister: ERR not registered!\n"); + return -ENOENT; + } + return 0; +} diff --git a/sys/include/fs/devfs.h b/sys/include/fs/devfs.h new file mode 100644 index 0000000000..f5c4260edd --- /dev/null +++ b/sys/include/fs/devfs.h @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2016 Eistec AB + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup fs_devfs DevFS device file system + * @ingroup fs + * @brief Dynamic device file system + * + * This file system implementation allows devices to register file names for + * easier access to device drivers from shell commands etc. + * + * The idea is similar to the /dev directory on Unix. + * + * @{ + * @file + * @brief DevFS public API + * @author Joakim Nohlgård + */ + +#ifndef DEVFS_H_ +#define DEVFS_H_ + +#include "clist.h" +#include "vfs.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief DevFS node typedef + */ +typedef struct devfs devfs_t; + +/** + * @brief A device "file" consists of a file name and an opaque pointer to device driver private data + * + * The file system is implemented as a linked list. + */ +struct devfs { + clist_node_t list_entry; /**< List item entry */ + const char *path; /**< File system relative path to this node */ + const vfs_file_ops_t *f_op; /**< Pointer to file operations table for this device */ + void *private_data; /**< Pointer to device driver specific data */ +}; + +/** + * @brief DevFS file system driver + * + * For use with vfs_mount + */ +extern const vfs_file_system_t devfs_file_system; + +/** + * @brief Register a node in DevFS + * + * The node will immediately become available to @c vfs_open, if DevFS is already + * mounted somewhere. + * + * If DevFS is not mounted, the node will be registered and will become + * available to @c vfs_open when DevFS is mounted. + * + * @param[in] node DevFS node to register + * + * @return 0 on success + * @return <0 on error + */ +int devfs_register(devfs_t *node); + +/** + * @brief Remove a registration from DevFS + * + * The node will no longer be available to @c vfs_open, but any already opened FDs + * will remain open. + * + * @param[in] node DevFS node to unregister + * + * @return 0 on success + * @return <0 on error + */ +int devfs_unregister(devfs_t *node); + +#ifdef __cplusplus +} +#endif + +#endif + +/** @} */ diff --git a/tests/unittests/tests-devfs/Makefile b/tests/unittests/tests-devfs/Makefile new file mode 100644 index 0000000000..48422e909a --- /dev/null +++ b/tests/unittests/tests-devfs/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/tests/unittests/tests-devfs/Makefile.include b/tests/unittests/tests-devfs/Makefile.include new file mode 100644 index 0000000000..ded525dbfa --- /dev/null +++ b/tests/unittests/tests-devfs/Makefile.include @@ -0,0 +1,2 @@ +USEMODULE += vfs +USEMODULE += devfs diff --git a/tests/unittests/tests-devfs/tests-devfs.c b/tests/unittests/tests-devfs/tests-devfs.c new file mode 100644 index 0000000000..cb9dd321c7 --- /dev/null +++ b/tests/unittests/tests-devfs/tests-devfs.c @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2016 Eistec AB + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @{ + * + * @file + * @brief Unittests for DevFS + * + * @author Joakim Nohlgård + */ +#include +#include +#include +#include + +#include "fs/devfs.h" + +#include "embUnit/embUnit.h" + +#include "tests-devfs.h" + +static int _mock_open(vfs_file_t *filp, const char *name, int flags, mode_t mode, const char *abs_path); +static ssize_t _mock_read(vfs_file_t *filp, void *dest, size_t nbytes); +static ssize_t _mock_write(vfs_file_t *filp, const void *src, size_t nbytes); + +static volatile int _mock_open_calls = 0; +static volatile int _mock_read_calls = 0; +static volatile int _mock_write_calls = 0; + +static int _mock_private_data; + +static const vfs_file_ops_t _mock_devfs_ops = { + .open = _mock_open, + .read = _mock_read, + .write = _mock_write, +}; + +static int _mock_private_data_tag = 4321; + +static devfs_t _mock_devfs_node = { + .path = "/mock0", + .f_op = &_mock_devfs_ops, + .private_data = &_mock_private_data_tag, +}; + +static vfs_mount_t _test_devfs_mount = { + .fs = &devfs_file_system, + .mount_point = "/test", + .private_data = &_mock_private_data, +}; + +static int _mock_open(vfs_file_t *filp, const char *name, int flags, mode_t mode, const char *abs_path) +{ + (void) name; + (void) flags; + (void) mode; + (void) abs_path; + if (filp->private_data.ptr != &_mock_private_data_tag) { + return -4321; + } + int *np = filp->mp->private_data; + ++(*np); + ++_mock_open_calls; + return 0; +} + +static ssize_t _mock_read(vfs_file_t *filp, void *dest, size_t nbytes) +{ + (void) dest; + (void) nbytes; + if (filp->private_data.ptr != &_mock_private_data_tag) { + return -4321; + } + int *np = filp->mp->private_data; + ++(*np); + ++_mock_read_calls; + return 0; +} + +static ssize_t _mock_write(vfs_file_t *filp, const void *src, size_t nbytes) +{ + (void) src; + (void) nbytes; + if (filp->private_data.ptr != &_mock_private_data_tag) { + return -4321; + } + int *np = filp->mp->private_data; + ++(*np); + ++_mock_write_calls; + return 0; +} + +static void test_devfs_register(void) +{ + int res = devfs_register(NULL); + TEST_ASSERT(res < 0); + + res = devfs_register(&_mock_devfs_node); + TEST_ASSERT(res == 0); + + res = devfs_register(&_mock_devfs_node); + TEST_ASSERT(res < 0); + + res = devfs_unregister(&_mock_devfs_node); + TEST_ASSERT(res == 0); + + res = devfs_unregister(&_mock_devfs_node); + TEST_ASSERT(res < 0); +} + +static void test_devfs_mount_open(void) +{ + _mock_private_data = 12345; + int res; + res = vfs_mount(&_test_devfs_mount); + TEST_ASSERT_EQUAL_INT(0, res); + TEST_ASSERT_EQUAL_INT(_mock_private_data, 12345); + + res = devfs_register(&_mock_devfs_node); + TEST_ASSERT_EQUAL_INT(0, res); + + int count = _mock_open_calls; + int fd = vfs_open("/test/mock0", O_RDWR, 0); + TEST_ASSERT(fd >= 0); + TEST_ASSERT_EQUAL_INT(count + 1, _mock_open_calls); + TEST_ASSERT_EQUAL_INT(_mock_private_data, 12346); + + res = vfs_close(fd); + TEST_ASSERT_EQUAL_INT(0, res); + + res = devfs_unregister(&_mock_devfs_node); + TEST_ASSERT_EQUAL_INT(0, res); + + res = vfs_umount(&_test_devfs_mount); + TEST_ASSERT_EQUAL_INT(0, res); +} + +Test *tests_devfs_tests(void) +{ + EMB_UNIT_TESTFIXTURES(fixtures) { + new_TestFixture(test_devfs_register), + new_TestFixture(test_devfs_mount_open), + }; + + EMB_UNIT_TESTCALLER(devfs_tests, NULL, NULL, fixtures); + + return (Test *)&devfs_tests; +} + +void tests_devfs(void) +{ + TESTS_RUN(tests_devfs_tests()); +} +/** @} */ diff --git a/tests/unittests/tests-devfs/tests-devfs.h b/tests/unittests/tests-devfs/tests-devfs.h new file mode 100644 index 0000000000..1d9a45e66a --- /dev/null +++ b/tests/unittests/tests-devfs/tests-devfs.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 Eistec AB + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @addtogroup unittests + * @{ + * + * @file + * @brief Unittests for DevFS + * + * @author Joakim Nohlgård + */ +#ifndef TESTS_DEVFS_H +#define TESTS_DEVFS_H + +#include "embUnit.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief The entry point of this test suite. + */ +void tests_devfs(void); + +#ifdef __cplusplus +} +#endif + +#endif /* TESTS_DEVFS_H */ +/** @} */