diff options
Diffstat (limited to 'src/datenklo/device.c')
-rw-r--r-- | src/datenklo/device.c | 559 |
1 files changed, 559 insertions, 0 deletions
diff --git a/src/datenklo/device.c b/src/datenklo/device.c new file mode 100644 index 0000000..462c08e --- /dev/null +++ b/src/datenklo/device.c @@ -0,0 +1,559 @@ +/* character device link to libfuse + * + * (C) 2019 by Andreas Eversberg <jolly@eversberg.eu> + * All Rights Reserved + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#define FUSE_USE_VERSION 30 + +#include <cuse_lowlevel.h> +#include <fuse_opt.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include "../libdebug/debug.h" +#define __USE_GNU +#include <pthread.h> +#include <signal.h> + +#include "fioc.h" +#include "device.h" + +/* enable to heavily debug poll process */ +//#define DEBUG_POLL + +typedef struct device { + struct device *next; + void *inst; + pthread_t thread; + int thread_started, thread_stopped; + const char *name; + int major, minor; + int (*open_cb)(void *inst, int flags); + void (*close_cb)(void *inst); + ssize_t (*read_cb)(void *inst, char *buf, size_t size, int flags); + ssize_t (*write_cb)(void *inst, const char *buf, size_t size, int flags); + ssize_t (*ioctl_get_cb)(void *inst, int cmd, void *buf, size_t out_bufsz); + ssize_t (*ioctl_set_cb)(void *inst, int cmd, const void *buf, size_t in_bufsz); + void (*flush_tx)(void *inst); + void (*lock_cb)(void); + void (*unlock_cb)(void); + short poll_revents; + struct fuse_pollhandle *poll_handle; + /* handle read blocking */ + fuse_req_t read_req; + size_t read_size; + int read_flags; + int read_locked; + /* handle write blocking */ + fuse_req_t write_req; + size_t write_size; + char *write_buf; + int write_flags; + int write_locked; +} device_t; + +static device_t *device_list = NULL; + +static device_t *get_device_by_thread(void) +{ + device_t *device = device_list; + pthread_t thread = pthread_self(); + + while (device) { + if (device->thread == thread) + return device; + device = device->next; + } + + fprintf(stderr, "Our thread is unknown, please fix!\n"); + abort(); +} + +static void cuse_device_open(fuse_req_t req, struct fuse_file_info *fi) +{ + (void)fi; + int rc; + device_t *device = get_device_by_thread(); + + device->lock_cb(); + rc = device->open_cb(device->inst, fi->flags); + device->unlock_cb(); + + if (rc < 0) + fuse_reply_err(req, -rc); + else + fuse_reply_open(req, fi); +} + +static void cuse_device_release(fuse_req_t req, struct fuse_file_info *fi) +{ + (void)fi; + device_t *device = get_device_by_thread(); + + device->lock_cb(); + device->close_cb(device->inst); + device->unlock_cb(); + + fuse_reply_err(req, 0); +} + +static void cuse_read_interrupt(fuse_req_t req, void *data) +{ + (void)req; + device_t *device = (device_t *)data; + + if (!device->read_locked) + device->lock_cb(); + + PDEBUG(DDEVICE, DEBUG_DEBUG, "%s received interrupt from client!\n", device->name); + + if (device->read_req) { + device->read_req = NULL; + fuse_reply_err(req, EINTR); + } + + if (!device->read_locked) + device->unlock_cb(); +} + +void device_read_available(void *inst) +{ + device_t *device = (device_t *)inst; + ssize_t count; + + // we are locked by caller + + /* if enough data or if buffer is full */ + if (device->read_req) { + char buf[device->read_size]; + count = device->read_cb(device->inst, buf, device->read_size, device->read_flags); + /* still blocking, waiting for more... */ + if (count == -EAGAIN) + return; + fuse_reply_buf(device->read_req, buf, count); + device->read_req = NULL; + } +} + +static void cuse_device_read(fuse_req_t req, size_t size, off_t off, struct fuse_file_info *fi) +{ + (void)off; + (void)fi; + ssize_t count; + device_t *device = get_device_by_thread(); + + if (size > 65536) + size = 65536; + char buf[size]; + + device->lock_cb(); + + if (device->read_req) { + device->unlock_cb(); + PDEBUG(DDEVICE, DEBUG_ERROR, "%s: Got another read(), while first read() has not been replied, please fix.\n", device->name); + fuse_reply_err(req, EBUSY); + return; + } + +#ifdef DEBUG_POLL + puts("read: before fn"); +#endif + count = device->read_cb(device->inst, buf, size, fi->flags); +#ifdef DEBUG_POLL + puts("read: after fn"); +#endif + + /* this means that we block until modem's read() returns 0 or positive value (in nonblocking io, we return -EAGAIN) */ + if (!(fi->flags & O_NONBLOCK) && count == -EAGAIN) { + PDEBUG(DDEVICE, DEBUG_DEBUG, "%s has no data available, waiting for data, timer or interrupt.\n", device->name); + + device->read_req = req; + device->read_size = size; + device->read_flags = fi->flags; + /* to prevent race condition, tell cuse_write_interrupt that we are already locked. + * (interrupt may have come before and will be processed by fuse_req_interrupt_func()) + */ + device->read_locked = 1; + fuse_req_interrupt_func(req, cuse_read_interrupt, device); + device->read_locked = 0; + device->unlock_cb(); + return; + } + + device->unlock_cb(); + + if (count < 0) + fuse_reply_err(req, -count); + else + fuse_reply_buf(req, buf, count); +#ifdef DEBUG_POLL + puts("read: after reply"); +#endif +} + +static void cuse_write_interrupt(fuse_req_t req, void *data) +{ + (void)req; + device_t *device = (device_t *)data; + + if (!device->write_locked) + device->lock_cb(); + + PDEBUG(DDEVICE, DEBUG_DEBUG, "%s received interrupt from client!\n", device->name); + + if (device->write_req) { + device->write_req = NULL; + free(device->write_buf); + device->write_buf = NULL; + /* flushing TX buffer */ + device->flush_tx(device->inst); + fuse_reply_err(req, EINTR); + } + + if (!device->write_locked) + device->unlock_cb(); +} + +void device_write_available(void *inst) +{ + device_t *device = (device_t *)inst; + ssize_t count; + + // we are locked by caller + + /* if enough space or buffer empty */ + if (device->write_req) { + count = device->write_cb(device->inst, device->write_buf, device->write_size, device->write_flags); + /* still blocking, waiting for more... */ + if (count == -EAGAIN) + return; + fuse_reply_write(device->write_req, count); + device->write_req = NULL; + free(device->write_buf); + device->write_buf = NULL; + } +} + +static void cuse_device_write(fuse_req_t req, const char *buf, size_t size, off_t off, struct fuse_file_info *fi) +{ + (void)off; + (void)fi; + ssize_t count; + device_t *device = get_device_by_thread(); + + device->lock_cb(); + + if (device->write_req) { + device->unlock_cb(); + PDEBUG(DDEVICE, DEBUG_ERROR, "%s: Got another write(), while first write() has not been replied, please fix.\n", device->name); + fuse_reply_err(req, EBUSY); + return; + } + + count = device->write_cb(device->inst, buf, size, fi->flags); + + /* this means that we block until modem's write() returns 0 or positive value (in nonblocking io, we return -EAGAIN) */ + if (!(fi->flags & O_NONBLOCK) && count == -EAGAIN) { + PDEBUG(DDEVICE, DEBUG_DEBUG, "%s has no buffer space available, waiting for space or interrupt.\n", device->name); + + device->write_req = req; + device->write_size = size; + device->write_buf = malloc(size); + if (!buf) { + PDEBUG(DDEVICE, DEBUG_ERROR, "No memory!\n"); + exit(0); + } + memcpy(device->write_buf, buf, size); + device->write_flags = fi->flags; + /* to prevent race condition, tell cuse_write_interrupt that we are already locked. + * (interrupt may have come before and will be processed by fuse_req_interrupt_func()) + */ + device->write_locked = 1; + fuse_req_interrupt_func(req, cuse_write_interrupt, device); + device->write_locked = 0; + device->unlock_cb(); + return; + } + + device->unlock_cb(); + + if (count < 0) + fuse_reply_err(req, -count); + else + fuse_reply_write(req, count); +} + +static void cuse_device_ioctl(fuse_req_t req, int cmd, void *arg, struct fuse_file_info *fi, unsigned flags, const void *in_buf, size_t in_bufsz, size_t out_bufsz) +{ + (void)fi; + ssize_t rc; + char out_buf[out_bufsz]; + device_t *device = get_device_by_thread(); + + if (flags & FUSE_IOCTL_COMPAT) { + fuse_reply_err(req, ENOSYS); + return; + } + + switch (cmd) { + case TCGETS: + case TIOCMGET: + case TIOCGWINSZ: + case FIONREAD: + case TIOCOUTQ: + device->lock_cb(); + rc = device->ioctl_get_cb(device->inst, cmd, out_buf, out_bufsz); + device->unlock_cb(); + if (rc < 0) { + fuse_reply_err(req, -rc); + break; + } + if (rc == 0) { + // do we need this ? + fuse_reply_ioctl(req, 0, NULL, 0); + break; + } + if (!out_bufsz) { + struct iovec iov = { arg, rc }; + + fuse_reply_ioctl_retry(req, NULL, 0, &iov, 1); + } else { + fuse_reply_ioctl(req, 0, out_buf, rc); + } + break; + case TCSETS: + case TCSETSW: + case TCSETSF: + case TIOCMBIS: + case TIOCMBIC: + case TIOCMSET: + case TCFLSH: + case TCSBRK: + case TCSBRKP: + case TIOCSBRK: + case TIOCCBRK: + case TIOCGSID: + case TIOCGPGRP: + case TIOCSCTTY: + case TIOCSPGRP: + case TIOCSWINSZ: + case TCXONC: + device->lock_cb(); + rc = device->ioctl_set_cb(device->inst, cmd, in_buf, in_bufsz); + device->unlock_cb(); + if (rc < 0) { + fuse_reply_err(req, -rc); + break; + } + if (rc == 0) { + /* empty control is replied */ + fuse_reply_ioctl(req, 0, NULL, 0); + break; + } + if (!in_bufsz) { + struct iovec iov = { arg, rc }; + + fuse_reply_ioctl_retry(req, &iov, 1, NULL, 0); + } else { + fuse_reply_ioctl(req, 0, NULL, 0); + } + break; + default: + PDEBUG(DDEVICE, DEBUG_NOTICE, "%s: receives unknown ioctl: 0x%x\n", device->name, cmd); + fuse_reply_err(req, EINVAL); + } +} + +void device_set_poll_events(void *inst, short revents) +{ + device_t *device = (device_t *)inst; + + // we are locked by caller + + if (revents == device->poll_revents) + return; + +#ifdef DEBUG_POLL + printf("new revents 0x%x\n", revents); +#endif + device->poll_revents = revents; + if (device->poll_handle) { +#ifdef DEBUG_POLL + printf("notify with handle %p\n", device->poll_handle); +#endif + fuse_lowlevel_notify_poll(device->poll_handle); + } +} + +static void cuse_device_poll(fuse_req_t req, struct fuse_file_info *fi, struct fuse_pollhandle *ph) +{ + (void)fi; + device_t *device = get_device_by_thread(); + +#ifdef DEBUG_POLL + printf("poll %p %p\n", ph, device->poll_handle); +#endif + if (ph) { + device->lock_cb(); + if (device->poll_handle) + fuse_pollhandle_destroy(device->poll_handle); + device->poll_handle = ph; +#ifdef DEBUG_POLL + printf("storing %p\n", device->poll_handle); +#endif + device->unlock_cb(); + } + +#ifdef DEBUG_POLL + printf("sending revents 0x%x\n", device->poll_revents); +#endif + fuse_reply_poll(req, device->poll_revents); +} + +static void cuse_device_flush(fuse_req_t req, struct fuse_file_info *fi) +{ + (void)req; + (void)fi; + device_t *device = get_device_by_thread(); + PDEBUG(DDEVICE, DEBUG_NOTICE, "%s: unhandled flush\n", device->name); +} + +static void cuse_device_fsync(fuse_req_t req, int datasync, struct fuse_file_info *fi) +{ + (void)req; + (void)datasync; + (void)fi; + device_t *device = get_device_by_thread(); + PDEBUG(DDEVICE, DEBUG_NOTICE, "%s: unhandled fsync\n", device->name); +} + + +static const struct cuse_lowlevel_ops cuse_device_clop = { + .open = cuse_device_open, + .release = cuse_device_release, + .read = cuse_device_read, + .write = cuse_device_write, + .ioctl = cuse_device_ioctl, + .poll = cuse_device_poll, + .fsync = cuse_device_fsync, + .flush = cuse_device_flush, +}; + +static void *device_child(void *arg) +{ + device_t *device = (device_t *)arg; + + int argc = 3; + /* use -f to run without debug, but -d to debug */ + char *argv[3] = { "datenklo", "-f", "-s" }; + char dev_name[128] = "DEVNAME="; + const char *dev_info_argv[] = { dev_name }; + struct cuse_info ci; + + strncat(dev_name, device->name, sizeof(dev_name) - strlen(device->name) - 1); + + memset(&ci, 0, sizeof(ci)); + ci.dev_major = device->major; + ci.dev_minor = device->minor; + ci.dev_info_argc = 1; + ci.dev_info_argv = dev_info_argv; + ci.flags = CUSE_UNRESTRICTED_IOCTL; + + device->thread_started = 1; + + PDEBUG(DDEVICE, DEBUG_INFO, "Device '%s' started.\n", device->name); + cuse_lowlevel_main(argc, argv, &ci, &cuse_device_clop, NULL); + PDEBUG(DDEVICE, DEBUG_INFO, "Device '%s' terminated.\n", device->name); + + device->thread_stopped = 1; + + return NULL; +} + +void *device_init(void *inst, const char *name, int (*open)(void *inst, int flags), void (*close)(void *inst), ssize_t (*read)(void *inst, char *buf, size_t size, int flags), ssize_t (*write)(void *inst, const char *buf, size_t size, int flags), ssize_t ioctl_get(void *inst, int cmd, void *buf, size_t out_bufsz), ssize_t ioctl_set(void *inst, int cmd, const void *buf, size_t in_bufsz), void (*flush_tx)(void *inst), void (*lock)(void), void (*unlock)(void)) +{ + int rc = -EINVAL; + char tname[64]; + device_t *device = NULL; + device_t **devicep; + + device = calloc(1, sizeof(*device)); + if (!device) { + PDEBUG(DDEVICE, DEBUG_ERROR, "No memory!\n"); + errno = ENOMEM; + goto error; + } + device->inst = inst; + device->name = name; + device->open_cb = open; + device->close_cb = close; + device->read_cb = read; + device->write_cb = write; + device->ioctl_get_cb = ioctl_get; + device->ioctl_set_cb = ioctl_set; + device->flush_tx = flush_tx; + device->lock_cb = lock; + device->unlock_cb = unlock; + + rc = pthread_create(&device->thread, NULL, device_child, device); + if (rc < 0) { + PDEBUG(DDEVICE, DEBUG_ERROR, "Failed to create device thread!\n"); + errno = -rc; + goto error; + } + + pthread_getname_np(device->thread, tname, sizeof(tname)); + strncat(tname, "-device", sizeof(tname) - 7 - 1); + tname[sizeof(tname) - 1] = '\0'; + pthread_setname_np(device->thread, tname); + + while (!device->thread_started) + usleep(100); + + /* attach to list */ + devicep = &device_list; + while (*devicep) + devicep = &((*devicep)->next); + *devicep = device; + + return device; + +error: + device_exit(device); + return NULL; +} + +void device_exit(void *inst) +{ + device_t *device = (device_t *)inst; + device_t **devicep; + + /* detach from list */ + devicep = &device_list; + while (*devicep && *devicep != device) + devicep = &((*devicep)->next); + if (*devicep) + *devicep = device->next; + + /* the device-thread is terminated when the program terminates, so no kill required (REALLY????) */ + + free(device); +} + |