// Copyright 2017 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #include #include #include #include #include "pty-core.h" #include "pty-fifo.h" #include #include #if 0 #define xprintf(fmt...) printf(fmt) #else #define xprintf(fmt...) do {} while (0) #endif #define CTRL_(n) ((n) - 'A' + 1) #define CTRL_C CTRL_('C') #define CTRL_S CTRL_('S') #define CTRL_Z CTRL_('Z') #define PTY_CLI_RAW_MODE (0x00000001u) #define PTY_CLI_CONTROL (0x00010000u) #define PTY_CLI_ACTIVE (0x00020000u) #define PTY_CLI_PEER_CLOSED (0x00040000u) struct pty_client { zx_device_t* zxdev; pty_server_t* srv; uint32_t id; uint32_t flags; pty_fifo_t fifo; list_node_t node; }; static zx_status_t pty_openat(pty_server_t* ps, zx_device_t** out, uint32_t id, uint32_t flags); // pty client device operations static zx_status_t pty_client_read(void* ctx, void* buf, size_t count, zx_off_t off, size_t* actual) { pty_client_t* pc = ctx; pty_server_t* ps = pc->srv; mtx_lock(&ps->lock); bool was_full = pty_fifo_is_full(&pc->fifo); size_t length = pty_fifo_read(&pc->fifo, buf, count); if (pty_fifo_is_empty(&pc->fifo)) { device_state_clr(pc->zxdev, DEV_STATE_READABLE); } if (was_full && length) { device_state_set(ps->zxdev, DEV_STATE_WRITABLE); } mtx_unlock(&ps->lock); if (length > 0) { *actual =length; return ZX_OK; } else { return (pc->flags & PTY_CLI_PEER_CLOSED) ? ZX_ERR_PEER_CLOSED : ZX_ERR_SHOULD_WAIT; } } static zx_status_t pty_client_write(void* ctx, const void* buf, size_t count, zx_off_t off, size_t* actual) { pty_client_t* pc = ctx; pty_server_t* ps = pc->srv; ssize_t r; mtx_lock(&ps->lock); if (pc->flags & PTY_CLI_ACTIVE) { size_t length; r = ps->recv(ps, buf, count, &length); if (r == ZX_OK) { *actual = length; } else if (r == ZX_ERR_SHOULD_WAIT) { device_state_clr(pc->zxdev, DEV_STATE_WRITABLE); } } else { r = (pc->flags & PTY_CLI_PEER_CLOSED) ? ZX_ERR_PEER_CLOSED : ZX_ERR_SHOULD_WAIT; } mtx_unlock(&ps->lock); return r; } // mask of invalid features #define PTY_FEATURE_BAD (~PTY_FEATURE_RAW) static void pty_make_active_locked(pty_server_t* ps, pty_client_t* pc) { xprintf("pty cli %p (id=%u) becomes active\n", pc, pc->id); if (ps->active != pc) { if (ps->active) { ps->active->flags &= (~PTY_CLI_ACTIVE); device_state_clr(ps->active->zxdev, DEV_STATE_WRITABLE); } ps->active = pc; pc->flags |= PTY_CLI_ACTIVE; device_state_set(pc->zxdev, DEV_STATE_WRITABLE); if (pty_fifo_is_full(&pc->fifo)) { device_state_clr_set(ps->zxdev, DEV_STATE_WRITABLE | DEV_STATE_HANGUP, 0); } else { device_state_clr_set(ps->zxdev, DEV_STATE_HANGUP, DEV_STATE_WRITABLE); } } } static void pty_adjust_signals_locked(pty_client_t* pc) { uint32_t set = 0; uint32_t clr = 0; if (pc->flags & PTY_CLI_ACTIVE) { set = DEV_STATE_WRITABLE; } else { clr = DEV_STATE_WRITABLE; } if (pty_fifo_is_empty(&pc->fifo)) { clr = DEV_STATE_READABLE; } else { set = DEV_STATE_READABLE; } device_state_clr_set(pc->zxdev, clr, set); } static zx_status_t pty_client_ioctl(void* ctx, uint32_t op, const void* in_buf, size_t in_len, void* out_buf, size_t out_len, size_t* out_actual) { pty_client_t* pc = ctx; pty_server_t* ps = pc->srv; switch (op) { case IOCTL_PTY_CLR_SET_FEATURE: { const pty_clr_set_t* cs = in_buf; if ((in_len != sizeof(pty_clr_set_t)) || (cs->clr & PTY_FEATURE_BAD) || (cs->set & PTY_FEATURE_BAD)) { return ZX_ERR_INVALID_ARGS; } mtx_lock(&ps->lock); pc->flags = (pc->flags & (~cs->clr)) | cs->set; mtx_unlock(&ps->lock); return ZX_OK; } case IOCTL_PTY_GET_WINDOW_SIZE: { pty_window_size_t* wsz = out_buf; if (out_len != sizeof(pty_window_size_t)) { return ZX_ERR_INVALID_ARGS; } mtx_lock(&ps->lock); wsz->width = ps->width; wsz->height = ps->height; mtx_unlock(&ps->lock); *out_actual = sizeof(pty_window_size_t); return ZX_OK; } case IOCTL_PTY_MAKE_ACTIVE: { if (in_len != sizeof(uint32_t)) { return ZX_ERR_INVALID_ARGS; } if (!(pc->flags & PTY_CLI_CONTROL)) { return ZX_ERR_ACCESS_DENIED; } uint32_t id = *((uint32_t*)in_buf); mtx_lock(&ps->lock); pty_client_t* c; list_for_every_entry(&ps->clients, c, pty_client_t, node) { if (c->id == id) { pty_make_active_locked(ps, c); mtx_unlock(&ps->lock); return ZX_OK; } } mtx_unlock(&ps->lock); return ZX_ERR_NOT_FOUND; } case IOCTL_PTY_READ_EVENTS: { if (!(pc->flags & PTY_CLI_CONTROL)) { return ZX_ERR_ACCESS_DENIED; } if (out_len != sizeof(uint32_t)) { return ZX_ERR_INVALID_ARGS; } mtx_lock(&ps->lock); uint32_t events = ps->events; ps->events = 0; if (ps->active == NULL) { events |= PTY_EVENT_HANGUP; } *((uint32_t*) out_buf) = events; device_state_clr(pc->zxdev, PTY_SIGNAL_EVENT); mtx_unlock(&ps->lock); *out_actual = sizeof(uint32_t); return ZX_OK; } default: if (ps->ioctl != NULL) { return ps->ioctl(ps, op, in_buf, in_len, out_buf, out_len, out_actual); } else { return ZX_ERR_NOT_SUPPORTED; } } } static void pty_client_release(void* ctx) { pty_client_t* pc = ctx; pty_server_t* ps = pc->srv; mtx_lock(&ps->lock); // remove client from list of clients and downref server list_delete(&pc->node); pc->srv = NULL; int refcount = --ps->refcount; if (ps->control == pc) { ps->control = NULL; } if (ps->active == pc) { // signal controlling client as well, if there is one if (ps->control) { device_state_set(ps->control->zxdev, PTY_SIGNAL_EVENT | DEV_STATE_HANGUP); } ps->active = NULL; } // signal server, if the last client has gone away if (list_is_empty(&ps->clients)) { device_state_clr_set(ps->zxdev, DEV_STATE_WRITABLE, DEV_STATE_READABLE | DEV_STATE_HANGUP); } mtx_unlock(&ps->lock); if (refcount == 0) { xprintf("pty srv %p release (from client)\n", ps); if (ps->release) { ps->release(ps); } else { free(ps); } } xprintf("pty cli %p (id=%u) release\n", pc, pc->id); free(pc); } zx_status_t pty_client_openat(void* ctx, zx_device_t** out, const char* path, uint32_t flags) { pty_client_t* pc = ctx; pty_server_t* ps = pc->srv; uint32_t id = strtoul(path, NULL, 0); // only controlling clients may create additional clients if (!(pc->flags & PTY_CLI_CONTROL)) { return ZX_ERR_ACCESS_DENIED; } // clients may not create controlling clients if (id == 0) { return ZX_ERR_INVALID_ARGS; } return pty_openat(ps, out, id, flags); } zx_protocol_device_t pc_ops = { .version = DEVICE_OPS_VERSION, // .open = default, allow cloning .open_at = pty_client_openat, .release = pty_client_release, .read = pty_client_read, .write = pty_client_write, .ioctl = pty_client_ioctl, }; // used by both client and server ptys to create new client ptys static zx_status_t pty_openat(pty_server_t* ps, zx_device_t** out, uint32_t id, uint32_t flags) { pty_client_t* pc; if ((pc = calloc(1, sizeof(pty_client_t))) == NULL) { return ZX_ERR_NO_MEMORY; } pc->id = id; pc->flags = 0; pc->fifo.head = 0; pc->fifo.tail = 0; zx_status_t status; unsigned num_clients = 0; mtx_lock(&ps->lock); // require that client ID is unique pty_client_t* c; list_for_every_entry(&ps->clients, c, pty_client_t, node) { if (c->id == id) { mtx_unlock(&ps->lock); free(pc); return ZX_ERR_INVALID_ARGS; } num_clients++; } list_add_tail(&ps->clients, &pc->node); ps->refcount++; mtx_unlock(&ps->lock); pc->srv = ps; device_add_args_t args = { .version = DEVICE_ADD_ARGS_VERSION, .name = "pty", .ctx = pc, .ops = &pc_ops, .flags = DEVICE_ADD_INSTANCE, }; status = device_add(ps->zxdev, &args, &pc->zxdev); if (status < 0) { pty_client_release(pc->zxdev); return status; } if (ps->active == NULL) { pty_make_active_locked(ps, pc); } if (id == 0) { ps->control = pc; pc->flags |= PTY_CLI_CONTROL; } xprintf("pty cli %p (id=%u) created (srv %p)\n", pc, pc->id, ps); mtx_lock(&ps->lock); if (num_clients == 0) { // if there were no clients, make sure we take server // out of HANGUP and READABLE, where it landed if all // its clients had closed device_state_clr(ps->zxdev, DEV_STATE_READABLE | DEV_STATE_HANGUP); } pty_adjust_signals_locked(pc); mtx_unlock(&ps->lock); *out = pc->zxdev; return ZX_OK; } // pty server device operations void pty_server_resume_locked(pty_server_t* ps) { if (ps->active) { device_state_set(ps->active->zxdev, DEV_STATE_WRITABLE); } } zx_status_t pty_server_send(pty_server_t* ps, const void* data, size_t len, bool atomic, size_t* actual) { //TODO: rw signals zx_status_t status; mtx_lock(&ps->lock); if (ps->active) { pty_client_t* pc = ps->active; bool was_empty = pty_fifo_is_empty(&pc->fifo); if (atomic || (pc->flags & PTY_CLI_RAW_MODE)) { *actual = pty_fifo_write(&pc->fifo, data, len, atomic); } else { if (len > PTY_FIFO_SIZE) { len = PTY_FIFO_SIZE; } const uint8_t *ch = data; unsigned n = 0; unsigned evt = 0; while (n < len) { if (*ch++ == CTRL_C) { evt = PTY_EVENT_INTERRUPT; break; } n++; } size_t r = pty_fifo_write(&pc->fifo, data, n, false); if ((r == n) && evt) { // consume the event r++; ps->events |= evt; xprintf("pty cli %p evt %x\n", pc, evt); if (ps->control) { device_state_set(ps->control->zxdev, PTY_SIGNAL_EVENT); } } *actual = r; } if (was_empty && *actual) { device_state_set(pc->zxdev, DEV_STATE_READABLE); } if (pty_fifo_is_full(&pc->fifo)) { device_state_clr(ps->zxdev, DEV_STATE_WRITABLE); } status = ZX_OK; } else { *actual = 0; status = ZX_ERR_PEER_CLOSED; } mtx_unlock(&ps->lock); return status; } void pty_server_set_window_size(pty_server_t* ps, uint32_t w, uint32_t h) { mtx_lock(&ps->lock); ps->width = w; ps->height = h; //TODO signal? mtx_unlock(&ps->lock); } zx_status_t pty_server_openat(void* ctx, zx_device_t** out, const char* path, uint32_t flags) { pty_server_t* ps = ctx; uint32_t id = strtoul(path, NULL, 0); return pty_openat(ps, out, id, flags); } void pty_server_release(void* ctx) { pty_server_t* ps = ctx; mtx_lock(&ps->lock); // inform clients that server is gone pty_client_t* pc; list_for_every_entry(&ps->clients, pc, pty_client_t, node) { pc->flags = (pc->flags & (~PTY_CLI_ACTIVE)) | PTY_CLI_PEER_CLOSED; device_state_set(pc->zxdev, DEV_STATE_HANGUP); } int32_t refcount = --ps->refcount; mtx_unlock(&ps->lock); if (refcount == 0) { xprintf("pty srv %p release (from server)\n", ps); if (ps->release) { ps->release(ps); } else { free(ps); } } } void pty_server_init(pty_server_t* ps) { mtx_init(&ps->lock, mtx_plain); ps->refcount = 1; list_initialize(&ps->clients); ps->active = NULL; ps->control = NULL; ps->events = 0; ps->width = 0; ps->height = 0; }