blob: ec310e3636706dcd8df20ef12a05a9c0794b75a5 [file] [log] [blame]
// 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 <stdio.h>
#include <string.h>
#include <threads.h>
#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/driver.h>
#include <zircon/device/pty.h>
#include <zircon/errors.h>
#include "pty-core.h"
#include "pty-fifo.h"
#define CTRL_(n) ((n) - 'A' + 1)
#define CTRL_C CTRL_('C')
#define CTRL_S CTRL_('S')
#define CTRL_Z CTRL_('Z')
// clang-format off
#define PTY_CLI_RAW_MODE (0x00000001u)
#define PTY_CLI_CONTROL (0x00010000u)
#define PTY_CLI_ACTIVE (0x00020000u)
#define PTY_CLI_PEER_CLOSED (0x00040000u)
// clang-format on
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) {
auto pc = static_cast<pty_client_t*>(ctx);
auto ps = static_cast<pty_server_t*>(pc->srv);
zxlogf(TRACE, "PTY Client %p (id=%u) read\n", pc, pc->id);
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) {
auto pc = static_cast<pty_client_t*>(ctx);
auto ps = static_cast<pty_server_t*>(pc->srv);
zxlogf(TRACE, "PTY Client %p (id=%u) write\n", pc, pc->id);
zx_status_t status;
mtx_lock(&ps->lock);
if (pc->flags & PTY_CLI_ACTIVE) {
size_t length;
status = ps->recv(ps, buf, count, &length);
if (status == ZX_OK) {
*actual = length;
} else if (status == ZX_ERR_SHOULD_WAIT) {
device_state_clr(pc->zxdev, DEV_STATE_WRITABLE);
}
} else {
status = (pc->flags & PTY_CLI_PEER_CLOSED) ? ZX_ERR_PEER_CLOSED : ZX_ERR_SHOULD_WAIT;
}
mtx_unlock(&ps->lock);
return status;
}
// 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) {
zxlogf(TRACE, "PTY Client %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) {
auto pc = static_cast<pty_client_t*>(ctx);
auto ps = static_cast<pty_server_t*>(pc->srv);
switch (op) {
case IOCTL_PTY_CLR_SET_FEATURE: {
zxlogf(TRACE, "PTY Client %p (id=%u) ioctl: clear and/or set feature\n", pc, pc->id);
auto cs = static_cast<const pty_clr_set_t*>(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: {
zxlogf(TRACE, "PTY Client %p (id=%u) ioctl: get window size\n", pc, pc->id);
auto wsz = static_cast<pty_window_size_t*>(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: {
zxlogf(TRACE, "PTY Client %p (id=%u) ioctl: make active\n", pc, pc->id);
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: {
zxlogf(TRACE, "PTY Client %p (id=%u) ioctl: read events\n", pc, pc->id);
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);
auto events = static_cast<uint32_t*>(out_buf);
*events = ps->events;
ps->events = 0;
if (ps->active == NULL) {
*events |= PTY_EVENT_HANGUP;
}
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) {
auto pc = static_cast<pty_client_t*>(ctx);
auto ps = static_cast<pty_server_t*>(pc->srv);
zxlogf(TRACE, "PTY Client %p (id=%u) release\n", pc, pc->id);
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) {
zxlogf(TRACE, "PTY Server %p release (from client)\n", ps);
if (ps->release) {
ps->release(ps);
} else {
free(ps);
}
}
free(pc);
}
zx_status_t pty_client_openat(void* ctx, zx_device_t** out, const char* path, uint32_t flags) {
auto pc = static_cast<pty_client_t*>(ctx);
auto ps = static_cast<pty_server_t*>(pc->srv);
uint32_t id = static_cast<uint32_t>(strtoul(path, NULL, 0));
zxlogf(TRACE, "PTY Client %p (id=%u) openat %u\n", pc, pc->id, id);
// 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 = []() {
zx_protocol_device_t ops = {};
ops.version = DEVICE_OPS_VERSION;
// ops.open = default, allow cloning
ops.open_at = pty_client_openat;
ops.release = pty_client_release;
ops.read = pty_client_read;
ops.write = pty_client_write;
ops.ioctl = pty_client_ioctl;
return ops;
}();
// 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) {
auto pc = static_cast<pty_client_t*>(calloc(1, sizeof(pty_client_t)));
if (!pc) {
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 = {};
args.version = DEVICE_ADD_ARGS_VERSION;
args.name = "pty";
args.ctx = pc;
args.ops = &pc_ops;
args.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;
}
zxlogf(TRACE, "PTY Client %p (id=%u) created (server %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
zxlogf(TRACE, "PTY Server %p send\n", ps);
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;
}
auto ch = static_cast<const uint8_t*>(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;
zxlogf(TRACE, "PTY Client %p event %x\n", ps, 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) {
zxlogf(TRACE, "PTY Server %p set window size %ux%u\n", ps, w, 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) {
auto ps = static_cast<pty_server_t*>(ctx);
uint32_t id = static_cast<uint32_t>(strtoul(path, NULL, 0));
zxlogf(TRACE, "PTY Server %p openat %u\n", ps, id);
return pty_openat(ps, out, id, flags);
}
void pty_server_release(void* ctx) {
auto ps = static_cast<pty_server_t*>(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) {
zxlogf(TRACE, "PTY Server %p release (from server)\n", ps);
if (ps->release) {
ps->release(ps);
} else {
free(ps);
}
}
}
void pty_server_init(pty_server_t* ps) {
zxlogf(TRACE, "PTY Server %p init\n", 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;
}