| // SPDX-License-Identifier: GPL-2.0 |
| #ifndef NO_BCACHEFS_FS |
| |
| #include "bcachefs.h" |
| #include "thread_with_file.h" |
| |
| #include <linux/anon_inodes.h> |
| #include <linux/file.h> |
| #include <linux/kthread.h> |
| #include <linux/pagemap.h> |
| #include <linux/poll.h> |
| #include <linux/sched/sysctl.h> |
| |
| void bch2_thread_with_file_exit(struct thread_with_file *thr) |
| { |
| if (thr->task) { |
| kthread_stop(thr->task); |
| put_task_struct(thr->task); |
| } |
| } |
| |
| int bch2_run_thread_with_file(struct thread_with_file *thr, |
| const struct file_operations *fops, |
| int (*fn)(void *)) |
| { |
| struct file *file = NULL; |
| int ret, fd = -1; |
| unsigned fd_flags = O_CLOEXEC; |
| |
| if (fops->read && fops->write) |
| fd_flags |= O_RDWR; |
| else if (fops->read) |
| fd_flags |= O_RDONLY; |
| else if (fops->write) |
| fd_flags |= O_WRONLY; |
| |
| char name[TASK_COMM_LEN]; |
| get_task_comm(name, current); |
| |
| thr->ret = 0; |
| thr->task = kthread_create(fn, thr, "%s", name); |
| ret = PTR_ERR_OR_ZERO(thr->task); |
| if (ret) |
| return ret; |
| |
| ret = get_unused_fd_flags(fd_flags); |
| if (ret < 0) |
| goto err; |
| fd = ret; |
| |
| file = anon_inode_getfile(name, fops, thr, fd_flags); |
| ret = PTR_ERR_OR_ZERO(file); |
| if (ret) |
| goto err; |
| |
| get_task_struct(thr->task); |
| wake_up_process(thr->task); |
| fd_install(fd, file); |
| return fd; |
| err: |
| if (fd >= 0) |
| put_unused_fd(fd); |
| if (thr->task) |
| kthread_stop(thr->task); |
| return ret; |
| } |
| |
| /* stdio_redirect */ |
| |
| static bool stdio_redirect_has_more_input(struct stdio_redirect *stdio, size_t seen) |
| { |
| return stdio->input.buf.nr > seen || stdio->done; |
| } |
| |
| static bool stdio_redirect_has_input(struct stdio_redirect *stdio) |
| { |
| return stdio_redirect_has_more_input(stdio, 0); |
| } |
| |
| static bool stdio_redirect_has_output(struct stdio_redirect *stdio) |
| { |
| return stdio->output.buf.nr || stdio->done; |
| } |
| |
| #define STDIO_REDIRECT_BUFSIZE 4096 |
| |
| static bool stdio_redirect_has_input_space(struct stdio_redirect *stdio) |
| { |
| return stdio->input.buf.nr < STDIO_REDIRECT_BUFSIZE || stdio->done; |
| } |
| |
| static bool stdio_redirect_has_output_space(struct stdio_redirect *stdio) |
| { |
| return stdio->output.buf.nr < STDIO_REDIRECT_BUFSIZE || stdio->done; |
| } |
| |
| static void stdio_buf_init(struct stdio_buf *buf) |
| { |
| spin_lock_init(&buf->lock); |
| init_waitqueue_head(&buf->wait); |
| darray_init(&buf->buf); |
| } |
| |
| /* thread_with_stdio */ |
| |
| static void thread_with_stdio_done(struct thread_with_stdio *thr) |
| { |
| thr->thr.done = true; |
| thr->stdio.done = true; |
| wake_up(&thr->stdio.input.wait); |
| wake_up(&thr->stdio.output.wait); |
| } |
| |
| static ssize_t thread_with_stdio_read(struct file *file, char __user *ubuf, |
| size_t len, loff_t *ppos) |
| { |
| struct thread_with_stdio *thr = |
| container_of(file->private_data, struct thread_with_stdio, thr); |
| struct stdio_buf *buf = &thr->stdio.output; |
| size_t copied = 0, b; |
| int ret = 0; |
| |
| if (!(file->f_flags & O_NONBLOCK)) { |
| ret = wait_event_interruptible(buf->wait, stdio_redirect_has_output(&thr->stdio)); |
| if (ret) |
| return ret; |
| } else if (!stdio_redirect_has_output(&thr->stdio)) |
| return -EAGAIN; |
| |
| while (len && buf->buf.nr) { |
| if (fault_in_writeable(ubuf, len) == len) { |
| ret = -EFAULT; |
| break; |
| } |
| |
| spin_lock_irq(&buf->lock); |
| b = min_t(size_t, len, buf->buf.nr); |
| |
| if (b && !copy_to_user_nofault(ubuf, buf->buf.data, b)) { |
| ubuf += b; |
| len -= b; |
| copied += b; |
| buf->buf.nr -= b; |
| memmove(buf->buf.data, |
| buf->buf.data + b, |
| buf->buf.nr); |
| } |
| spin_unlock_irq(&buf->lock); |
| } |
| |
| return copied ?: ret; |
| } |
| |
| static int thread_with_stdio_release(struct inode *inode, struct file *file) |
| { |
| struct thread_with_stdio *thr = |
| container_of(file->private_data, struct thread_with_stdio, thr); |
| |
| thread_with_stdio_done(thr); |
| bch2_thread_with_file_exit(&thr->thr); |
| darray_exit(&thr->stdio.input.buf); |
| darray_exit(&thr->stdio.output.buf); |
| thr->ops->exit(thr); |
| return 0; |
| } |
| |
| static ssize_t thread_with_stdio_write(struct file *file, const char __user *ubuf, |
| size_t len, loff_t *ppos) |
| { |
| struct thread_with_stdio *thr = |
| container_of(file->private_data, struct thread_with_stdio, thr); |
| struct stdio_buf *buf = &thr->stdio.input; |
| size_t copied = 0; |
| ssize_t ret = 0; |
| |
| while (len) { |
| if (thr->thr.done) { |
| ret = -EPIPE; |
| break; |
| } |
| |
| size_t b = len - fault_in_readable(ubuf, len); |
| if (!b) { |
| ret = -EFAULT; |
| break; |
| } |
| |
| spin_lock(&buf->lock); |
| size_t makeroom = b; |
| if (!buf->waiting_for_line || memchr(buf->buf.data, '\n', buf->buf.nr)) |
| makeroom = min_t(ssize_t, makeroom, |
| max_t(ssize_t, STDIO_REDIRECT_BUFSIZE - buf->buf.nr, |
| 0)); |
| darray_make_room_gfp(&buf->buf, makeroom, GFP_NOWAIT); |
| |
| b = min(len, darray_room(buf->buf)); |
| |
| if (b && !copy_from_user_nofault(&darray_top(buf->buf), ubuf, b)) { |
| buf->buf.nr += b; |
| ubuf += b; |
| len -= b; |
| copied += b; |
| } |
| spin_unlock(&buf->lock); |
| |
| if (b) { |
| wake_up(&buf->wait); |
| } else { |
| if ((file->f_flags & O_NONBLOCK)) { |
| ret = -EAGAIN; |
| break; |
| } |
| |
| ret = wait_event_interruptible(buf->wait, |
| stdio_redirect_has_input_space(&thr->stdio)); |
| if (ret) |
| break; |
| } |
| } |
| |
| return copied ?: ret; |
| } |
| |
| static __poll_t thread_with_stdio_poll(struct file *file, struct poll_table_struct *wait) |
| { |
| struct thread_with_stdio *thr = |
| container_of(file->private_data, struct thread_with_stdio, thr); |
| |
| poll_wait(file, &thr->stdio.output.wait, wait); |
| poll_wait(file, &thr->stdio.input.wait, wait); |
| |
| __poll_t mask = 0; |
| |
| if (stdio_redirect_has_output(&thr->stdio)) |
| mask |= EPOLLIN; |
| if (stdio_redirect_has_input_space(&thr->stdio)) |
| mask |= EPOLLOUT; |
| if (thr->thr.done) |
| mask |= EPOLLHUP|EPOLLERR; |
| return mask; |
| } |
| |
| static __poll_t thread_with_stdout_poll(struct file *file, struct poll_table_struct *wait) |
| { |
| struct thread_with_stdio *thr = |
| container_of(file->private_data, struct thread_with_stdio, thr); |
| |
| poll_wait(file, &thr->stdio.output.wait, wait); |
| |
| __poll_t mask = 0; |
| |
| if (stdio_redirect_has_output(&thr->stdio)) |
| mask |= EPOLLIN; |
| if (thr->thr.done) |
| mask |= EPOLLHUP|EPOLLERR; |
| return mask; |
| } |
| |
| static int thread_with_stdio_flush(struct file *file, fl_owner_t id) |
| { |
| struct thread_with_stdio *thr = |
| container_of(file->private_data, struct thread_with_stdio, thr); |
| |
| return thr->thr.ret; |
| } |
| |
| static long thread_with_stdio_ioctl(struct file *file, unsigned int cmd, unsigned long p) |
| { |
| struct thread_with_stdio *thr = |
| container_of(file->private_data, struct thread_with_stdio, thr); |
| |
| if (thr->ops->unlocked_ioctl) |
| return thr->ops->unlocked_ioctl(thr, cmd, p); |
| return -ENOTTY; |
| } |
| |
| static const struct file_operations thread_with_stdio_fops = { |
| .read = thread_with_stdio_read, |
| .write = thread_with_stdio_write, |
| .poll = thread_with_stdio_poll, |
| .flush = thread_with_stdio_flush, |
| .release = thread_with_stdio_release, |
| .unlocked_ioctl = thread_with_stdio_ioctl, |
| }; |
| |
| static const struct file_operations thread_with_stdout_fops = { |
| .read = thread_with_stdio_read, |
| .poll = thread_with_stdout_poll, |
| .flush = thread_with_stdio_flush, |
| .release = thread_with_stdio_release, |
| .unlocked_ioctl = thread_with_stdio_ioctl, |
| }; |
| |
| static int thread_with_stdio_fn(void *arg) |
| { |
| struct thread_with_stdio *thr = arg; |
| |
| thr->thr.ret = thr->ops->fn(thr); |
| |
| thread_with_stdio_done(thr); |
| return 0; |
| } |
| |
| void bch2_thread_with_stdio_init(struct thread_with_stdio *thr, |
| const struct thread_with_stdio_ops *ops) |
| { |
| stdio_buf_init(&thr->stdio.input); |
| stdio_buf_init(&thr->stdio.output); |
| thr->ops = ops; |
| } |
| |
| int __bch2_run_thread_with_stdio(struct thread_with_stdio *thr) |
| { |
| return bch2_run_thread_with_file(&thr->thr, &thread_with_stdio_fops, thread_with_stdio_fn); |
| } |
| |
| int bch2_run_thread_with_stdio(struct thread_with_stdio *thr, |
| const struct thread_with_stdio_ops *ops) |
| { |
| bch2_thread_with_stdio_init(thr, ops); |
| |
| return __bch2_run_thread_with_stdio(thr); |
| } |
| |
| int bch2_run_thread_with_stdout(struct thread_with_stdio *thr, |
| const struct thread_with_stdio_ops *ops) |
| { |
| stdio_buf_init(&thr->stdio.input); |
| stdio_buf_init(&thr->stdio.output); |
| thr->ops = ops; |
| |
| return bch2_run_thread_with_file(&thr->thr, &thread_with_stdout_fops, thread_with_stdio_fn); |
| } |
| EXPORT_SYMBOL_GPL(bch2_run_thread_with_stdout); |
| |
| int bch2_stdio_redirect_read(struct stdio_redirect *stdio, char *ubuf, size_t len) |
| { |
| struct stdio_buf *buf = &stdio->input; |
| |
| /* |
| * we're waiting on user input (or for the file descriptor to be |
| * closed), don't want a hung task warning: |
| */ |
| do { |
| wait_event_timeout(buf->wait, stdio_redirect_has_input(stdio), |
| sysctl_hung_task_timeout_secs * HZ / 2); |
| } while (!stdio_redirect_has_input(stdio)); |
| |
| if (stdio->done) |
| return -1; |
| |
| spin_lock(&buf->lock); |
| int ret = min(len, buf->buf.nr); |
| buf->buf.nr -= ret; |
| memcpy(ubuf, buf->buf.data, ret); |
| memmove(buf->buf.data, |
| buf->buf.data + ret, |
| buf->buf.nr); |
| spin_unlock(&buf->lock); |
| |
| wake_up(&buf->wait); |
| return ret; |
| } |
| |
| int bch2_stdio_redirect_readline_timeout(struct stdio_redirect *stdio, |
| darray_char *line, |
| unsigned long timeout) |
| { |
| unsigned long until = jiffies + timeout, t; |
| struct stdio_buf *buf = &stdio->input; |
| size_t seen = 0; |
| again: |
| t = timeout != MAX_SCHEDULE_TIMEOUT |
| ? max_t(long, until - jiffies, 0) |
| : timeout; |
| |
| t = min(t, sysctl_hung_task_timeout_secs * HZ / 2); |
| |
| wait_event_timeout(buf->wait, stdio_redirect_has_more_input(stdio, seen), t); |
| |
| if (stdio->done) |
| return -1; |
| |
| spin_lock(&buf->lock); |
| seen = buf->buf.nr; |
| char *n = memchr(buf->buf.data, '\n', seen); |
| |
| if (!n && timeout != MAX_SCHEDULE_TIMEOUT && time_after_eq(jiffies, until)) { |
| spin_unlock(&buf->lock); |
| return -ETIME; |
| } |
| |
| if (!n) { |
| buf->waiting_for_line = true; |
| spin_unlock(&buf->lock); |
| goto again; |
| } |
| |
| size_t b = n + 1 - buf->buf.data; |
| if (b > line->size) { |
| spin_unlock(&buf->lock); |
| int ret = darray_resize(line, b); |
| if (ret) |
| return ret; |
| seen = 0; |
| goto again; |
| } |
| |
| buf->buf.nr -= b; |
| memcpy(line->data, buf->buf.data, b); |
| memmove(buf->buf.data, |
| buf->buf.data + b, |
| buf->buf.nr); |
| line->nr = b; |
| |
| buf->waiting_for_line = false; |
| spin_unlock(&buf->lock); |
| |
| wake_up(&buf->wait); |
| return 0; |
| } |
| |
| int bch2_stdio_redirect_readline(struct stdio_redirect *stdio, darray_char *line) |
| { |
| return bch2_stdio_redirect_readline_timeout(stdio, line, MAX_SCHEDULE_TIMEOUT); |
| } |
| |
| __printf(3, 0) |
| static ssize_t bch2_darray_vprintf(darray_char *out, gfp_t gfp, const char *fmt, va_list args) |
| { |
| ssize_t ret; |
| |
| do { |
| va_list args2; |
| size_t len; |
| |
| va_copy(args2, args); |
| len = vsnprintf(out->data + out->nr, darray_room(*out), fmt, args2); |
| va_end(args2); |
| |
| if (len + 1 <= darray_room(*out)) { |
| out->nr += len; |
| return len; |
| } |
| |
| ret = darray_make_room_gfp(out, len + 1, gfp); |
| } while (ret == 0); |
| |
| return ret; |
| } |
| |
| ssize_t bch2_stdio_redirect_vprintf(struct stdio_redirect *stdio, bool nonblocking, |
| const char *fmt, va_list args) |
| { |
| struct stdio_buf *buf = &stdio->output; |
| unsigned long flags; |
| ssize_t ret; |
| |
| again: |
| spin_lock_irqsave(&buf->lock, flags); |
| ret = bch2_darray_vprintf(&buf->buf, GFP_NOWAIT, fmt, args); |
| spin_unlock_irqrestore(&buf->lock, flags); |
| |
| if (ret < 0) { |
| if (nonblocking) |
| return -EAGAIN; |
| |
| ret = wait_event_interruptible(buf->wait, |
| stdio_redirect_has_output_space(stdio)); |
| if (ret) |
| return ret; |
| goto again; |
| } |
| |
| wake_up(&buf->wait); |
| return ret; |
| } |
| |
| ssize_t bch2_stdio_redirect_printf(struct stdio_redirect *stdio, bool nonblocking, |
| const char *fmt, ...) |
| { |
| va_list args; |
| ssize_t ret; |
| |
| va_start(args, fmt); |
| ret = bch2_stdio_redirect_vprintf(stdio, nonblocking, fmt, args); |
| va_end(args); |
| |
| return ret; |
| } |
| |
| #endif /* NO_BCACHEFS_FS */ |