summaryrefslogtreecommitdiff
path: root/src/uvt_cdev.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/uvt_cdev.c')
-rw-r--r--src/uvt_cdev.c485
1 files changed, 485 insertions, 0 deletions
diff --git a/src/uvt_cdev.c b/src/uvt_cdev.c
new file mode 100644
index 0000000..291857b
--- /dev/null
+++ b/src/uvt_cdev.c
@@ -0,0 +1,485 @@
+/*
+ * UVT - Userspace Virtual Terminals
+ *
+ * Copyright (c) 2011-2013 David Herrmann <dh.herrmann@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files
+ * (the "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+/*
+ * Character Devices
+ * This implements a VT character device entry point via the CUSE API. It does
+ * not implement the VT API on top of the character-device (cdev) but only
+ * provides the entry point. It is up to the user to bind open-files to VT and
+ * client objects.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/major.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include "shl_dlist.h"
+#include "shl_hook.h"
+#include "shl_llog.h"
+#include "uvt.h"
+#include "uvt_internal.h"
+
+#include <fuse/fuse.h>
+#include <fuse/fuse_common.h>
+#include <fuse/fuse_lowlevel.h>
+#include <fuse/fuse_opt.h>
+#include <fuse/cuse_lowlevel.h>
+
+#define LLOG_SUBSYSTEM "uvt_cdev"
+
+/*
+ * FUSE low-level ops
+ * This implements all the file-system operations on the character-device. It
+ * is important that we handle interrupts correctly (ENOENT) and never loose
+ * any data. This is all single threaded as it is not performance critical at
+ * all.
+ * We simply dispatch each call to uvt_client as this implements all the
+ * client-session related operations.
+ */
+
+static void ll_open(fuse_req_t req, struct fuse_file_info *fi)
+{
+ struct uvt_cdev *cdev = fuse_req_userdata(req);
+ struct uvt_client *client;
+ struct uvt_cdev_event ev;
+ int ret;
+
+ ret = uvt_client_ll_open(&client, cdev, req, fi);
+ if (ret)
+ return;
+
+ memset(&ev, 0, sizeof(ev));
+ ev.type = UVT_CDEV_OPEN;
+ ev.client = client;
+ shl_hook_call(cdev->hook, cdev, &ev);
+}
+
+static void ll_destroy(void *data) {
+ struct uvt_cdev *cdev = data;
+ struct uvt_client *client;
+
+ /* on unexpected shutdown this kills all open clients */
+ while (!shl_dlist_empty(&cdev->clients)) {
+ client = shl_dlist_entry(cdev->clients.next,
+ struct uvt_client, list);
+ uvt_client_kill(client);
+ uvt_client_unref(client);
+ }
+}
+
+static const struct cuse_lowlevel_ops ll_ops = {
+ .init = NULL,
+ .destroy = ll_destroy,
+ .open = ll_open,
+ .release = uvt_client_ll_release,
+ .read = uvt_client_ll_read,
+ .write = uvt_client_ll_write,
+ .poll = uvt_client_ll_poll,
+ .ioctl = uvt_client_ll_ioctl,
+ .flush = NULL,
+ .fsync = NULL,
+};
+
+/*
+ * FUSE channel ops
+ * The connection to the FUSE kernel module is done via a file-descriptor.
+ * Writing to it is synchronous, so the commands that we write are
+ * _immediately_ executed and return the result to us. Furthermore, write()
+ * is always non-blocking and always succeeds so no reason to watch for
+ * EAGAIN. Reading from the FD, on the other hand, may block if there is no
+ * data available so we mark it as O_NONBLOCK. The kernel maintains
+ * an event-queue that we read from. So there may be pending events that we
+ * haven't read but which affect the calls that we write to the kernel. This
+ * is important when handling interrupts.
+ * chan_receive() and chan_send() handle I/O to the kernel module and are
+ * hooked up into a fuse-channel.
+ */
+
+static int chan_receive(struct fuse_chan **chp, char *buf, size_t size)
+{
+ struct fuse_chan *ch = *chp;
+ struct uvt_cdev *cdev = fuse_chan_data(ch);
+ struct fuse_session *se = fuse_chan_session(ch);
+ int fd = fuse_chan_fd(ch);
+ ssize_t res;
+
+ if (!se || !cdev)
+ return -EINVAL;
+
+ if (!size)
+ return 0;
+
+restart:
+ if (fuse_session_exited(se))
+ return 0;
+
+ res = read(fd, buf, size);
+ if (!res) {
+ /* EOF on cuse file */
+ llog_error(cdev, "fuse channel shut down on cdev %p", cdev);
+ fuse_session_exit(se);
+ return 0;
+ } else if (res < 0) {
+ /* ENOENT is returned if the operation was interrupted, it's
+ * safe to restart */
+ if (errno == ENOENT)
+ goto restart;
+
+ /* ENODEV is returned if the FS got unmounted. This shouldn't
+ * occur for CUSE devices. Anyway, exit if this happens. */
+ if (errno == ENODEV) {
+ llog_error(cdev, "fuse channel unmounted on cdev %p",
+ cdev);
+ fuse_session_exit(se);
+ return 0;
+ }
+
+ /* EINTR and EAGAIN are simply forwarded to the caller. */
+ if (errno == EINTR || errno == EAGAIN)
+ return -errno;
+
+ cdev->error = -errno;
+ llog_error(cdev, "fuse channel read error on cdev %p (%d): %m",
+ cdev, errno);
+ fuse_session_exit(se);
+ return cdev->error;
+ }
+
+ return res;
+}
+
+static int chan_send(struct fuse_chan *ch, const struct iovec iov[],
+ size_t count)
+{
+ struct uvt_cdev *cdev = fuse_chan_data(ch);
+ struct fuse_session *se = fuse_chan_session(ch);
+ int fd = fuse_chan_fd(ch);
+ int ret;
+
+ if (!cdev || !se)
+ return -EINVAL;
+ if (!iov || !count)
+ return 0;
+
+ ret = writev(fd, iov, count);
+ if (ret < 0) {
+ /* ENOENT is returned on interrupts */
+ if (!fuse_session_exited(se) && errno != ENOENT) {
+ cdev->error = -errno;
+ llog_error(cdev, "cannot write to fuse-channel on cdev %p (%d): %m",
+ cdev, errno);
+ fuse_session_exit(se);
+ }
+ return cdev->error;
+ }
+
+ return 0;
+}
+
+static const struct fuse_chan_ops chan_ops = {
+ .receive = chan_receive,
+ .send = chan_send,
+ .destroy = NULL,
+};
+
+/*
+ * Character Device
+ * This creates the high-level character-device driver and registers a
+ * fake-session that is used to control each character file.
+ * channel_event() is a callback when I/O is possible on the FUSE FD and
+ * performs all outstanding tasks.
+ * On error, the fake-session is unregistered and deleted. This also stops all
+ * client sessions, obviously.
+ */
+
+static void uvt_cdev_hup(struct uvt_cdev *cdev, int error)
+{
+ struct uvt_cdev_event ev;
+
+ ev_eloop_rm_fd(cdev->efd);
+ cdev->efd = NULL;
+ cdev->error = error;
+
+ memset(&ev, 0, sizeof(ev));
+ ev.type = UVT_CDEV_HUP;
+
+ shl_hook_call(cdev->hook, cdev, &ev);
+}
+
+static void channel_event(struct ev_fd *fd, int mask, void *data)
+{
+ struct uvt_cdev *cdev = data;
+ int ret;
+ struct fuse_buf buf;
+ struct fuse_chan *ch;
+ struct shl_dlist *iter;
+ struct uvt_client *client;
+
+ if (!(mask & EV_READABLE)) {
+ if (mask & (EV_HUP | EV_ERR)) {
+ llog_error(cdev, "HUP/ERR on fuse channel on cdev %p",
+ cdev);
+ uvt_cdev_hup(cdev, -EPIPE);
+ }
+
+ return;
+ }
+
+ memset(&buf, 0, sizeof(buf));
+ buf.mem = cdev->buf;
+ buf.size = cdev->bufsize;
+ ch = cdev->channel;
+ ret = fuse_session_receive_buf(cdev->session, &buf, &ch);
+ if (ret == -EINTR || ret == -EAGAIN) {
+ return;
+ } else if (ret < 0) {
+ llog_error(cdev, "fuse channel read error on cdev %p: %d",
+ cdev, ret);
+ uvt_cdev_hup(cdev, ret);
+ return;
+ }
+
+ fuse_session_process_buf(cdev->session, &buf, ch);
+ if (fuse_session_exited(cdev->session)) {
+ llog_error(cdev, "fuse session exited on cdev %p", cdev);
+ uvt_cdev_hup(cdev, cdev->error ? : -EFAULT);
+ return;
+ }
+
+ /* Readers can get interrupted asynchronously. Due to heavy locking
+ * inside of FUSE, we cannot release them right away. So cleanup all
+ * killed readers after we processed all buffers. */
+ shl_dlist_for_each(iter, &cdev->clients) {
+ client = shl_dlist_entry(iter, struct uvt_client, list);
+ uvt_client_cleanup(client);
+ }
+}
+
+static int uvt_cdev_init(struct uvt_cdev *cdev, const char *name,
+ unsigned int major, unsigned int minor)
+{
+ const char *dev_info_argv[1];
+ struct cuse_info ci;
+ size_t bufsize;
+ char *nparam;
+ int ret;
+
+ /* TODO: libfuse makes sure that fd 0, 1 and 2 are available as
+ * standard streams, otherwise they fail. This is awkward and we
+ * should check whether this is really needed and _why_?
+ * If it is needed, fix upstream to stop that crazy! */
+
+ if (!major)
+ major = TTY_MAJOR;
+
+ if (!major || major > 255) {
+ llog_error(cdev, "invalid major %u on cdev %p",
+ major, cdev);
+ return -EINVAL;
+ }
+ if (!minor) {
+ llog_error(cdev, "invalid minor %u on cdev %p",
+ minor, cdev);
+ return -EINVAL;
+ }
+ if (!name || !*name) {
+ llog_error(cdev, "empty name on cdev %p",
+ cdev);
+ return -EINVAL;
+ }
+
+ llog_info(cdev, "creating device /dev/%s %u:%u on cdev %p",
+ name, major, minor, cdev);
+
+ ret = asprintf(&nparam, "DEVNAME=%s", name);
+ if (ret <= 0)
+ return llog_ENOMEM(cdev);
+
+ dev_info_argv[0] = nparam;
+ memset(&ci, 0, sizeof(ci));
+ ci.dev_major = major;
+ ci.dev_minor = minor;
+ ci.dev_info_argc = 1;
+ ci.dev_info_argv = dev_info_argv;
+ ci.flags = CUSE_UNRESTRICTED_IOCTL;
+
+ cdev->session = cuse_lowlevel_new(NULL, &ci, &ll_ops, cdev);
+ free(nparam);
+
+ if (!cdev->session) {
+ llog_error(cdev, "cannot create fuse-ll session on cdev %p",
+ cdev);
+ return -ENOMEM;
+ }
+
+ cdev->fd = open(cdev->ctx->cuse_file, O_RDWR | O_CLOEXEC | O_NONBLOCK);
+ if (cdev->fd < 0) {
+ llog_error(cdev, "cannot open cuse-file %s on cdev %p (%d): %m",
+ cdev->ctx->cuse_file, cdev, errno);
+ ret = -EFAULT;
+ goto err_session;
+ }
+
+ bufsize = getpagesize() + 0x1000;
+ if (bufsize < 0x21000)
+ bufsize = 0x21000;
+
+ cdev->bufsize = bufsize;
+ cdev->buf = malloc(bufsize);
+ if (!cdev->buf) {
+ ret = llog_ENOMEM(cdev);
+ goto err_fd;
+ }
+
+ /* Argh! libfuse does not use "const" for the "chan_ops" pointer so we
+ * actually have to cast it. Their implementation does not write into it
+ * so we can safely use a constant storage for it.
+ * TODO: Fix libfuse upstream! */
+ cdev->channel = fuse_chan_new((void*)&chan_ops, cdev->fd, bufsize,
+ cdev);
+ if (!cdev->channel) {
+ llog_error(cdev, "cannot allocate fuse-channel on cdev %p",
+ cdev);
+ ret = -ENOMEM;
+ goto err_buf;
+ }
+
+ ret = ev_eloop_new_fd(cdev->ctx->eloop, &cdev->efd, cdev->fd,
+ EV_READABLE, channel_event, cdev);
+ if (ret)
+ goto err_chan;
+
+ fuse_session_add_chan(cdev->session, cdev->channel);
+ return 0;
+
+err_chan:
+ fuse_chan_destroy(cdev->channel);
+err_buf:
+ free(cdev->buf);
+err_fd:
+ close(cdev->fd);
+err_session:
+ fuse_session_destroy(cdev->session);
+ return ret;
+}
+
+static void uvt_cdev_destroy(struct uvt_cdev *cdev)
+{
+ if (cdev->error)
+ llog_warning(cdev, "cdev %p failed with error %d",
+ cdev, cdev->error);
+
+ fuse_session_destroy(cdev->session);
+ ev_eloop_rm_fd(cdev->efd);
+ free(cdev->buf);
+ close(cdev->fd);
+}
+
+SHL_EXPORT
+int uvt_cdev_new(struct uvt_cdev **out, struct uvt_ctx *ctx,
+ const char *name, unsigned int major, unsigned int minor)
+{
+ struct uvt_cdev *cdev;
+ int ret;
+
+ if (!ctx)
+ return -EINVAL;
+ if (!out)
+ return llog_EINVAL(ctx);
+
+ cdev = malloc(sizeof(*cdev));
+ if (!cdev)
+ return llog_ENOMEM(ctx);
+ memset(cdev, 0, sizeof(*cdev));
+ cdev->ref = 1;
+ cdev->ctx = ctx;
+ cdev->llog = ctx->llog;
+ cdev->llog_data = ctx->llog_data;
+ shl_dlist_init(&cdev->clients);
+
+ llog_debug(cdev, "new cdev %p on ctx %p", cdev, cdev->ctx);
+
+ ret = shl_hook_new(&cdev->hook);
+ if (ret)
+ goto err_free;
+
+ ret = uvt_cdev_init(cdev, name, major, minor);
+ if (ret)
+ goto err_hook;
+
+ uvt_ctx_ref(cdev->ctx);
+ *out = cdev;
+ return 0;
+
+err_hook:
+ shl_hook_free(cdev->hook);
+err_free:
+ free(cdev);
+ return ret;
+}
+
+SHL_EXPORT
+void uvt_cdev_ref(struct uvt_cdev *cdev)
+{
+ if (!cdev || !cdev->ref)
+ return;
+
+ ++cdev->ref;
+}
+
+SHL_EXPORT
+void uvt_cdev_unref(struct uvt_cdev *cdev)
+{
+ if (!cdev || !cdev->ref || --cdev->ref)
+ return;
+
+ llog_debug(cdev, "free cdev %p", cdev);
+
+ uvt_cdev_destroy(cdev);
+ shl_hook_free(cdev->hook);
+ uvt_ctx_unref(cdev->ctx);
+ free(cdev);
+}
+
+SHL_EXPORT
+int uvt_cdev_register_cb(struct uvt_cdev *cdev, uvt_cdev_cb cb, void *data)
+{
+ if (!cdev)
+ return -EINVAL;
+
+ return shl_hook_add_cast(cdev->hook, cb, data, false);
+}
+
+SHL_EXPORT
+void uvt_cdev_unregister_cb(struct uvt_cdev *cdev, uvt_cdev_cb cb, void *data)
+{
+ if (!cdev)
+ return;
+
+ shl_hook_rm_cast(cdev->hook, cb, data);
+}