blob: 492a30a5ade78a91bdf3bc7f5ff2b3ea2b7dde3b [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 <utility>
#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/driver.h>
#include <fbl/auto_lock.h>
#include <fuchsia/device/c/fidl.h>
#include <fuchsia/hardware/pty/c/fidl.h>
#include <lib/zx/channel.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_open_client(pty_server_t* ps, uint32_t id, zx::channel channel);
// 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_chunk_locked(pty_client_t* pc, pty_server_t* ps,
const void* buf, size_t count,
size_t* actual) {
size_t length;
zx_status_t 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);
}
return status;
}
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;
if (count == 0) {
*actual = 0;
return ZX_OK;
}
fbl::AutoLock lock(&ps->lock);
if (!(pc->flags & PTY_CLI_ACTIVE)) {
return (pc->flags & PTY_CLI_PEER_CLOSED) ? ZX_ERR_PEER_CLOSED : ZX_ERR_SHOULD_WAIT;
}
if (pc->flags & fuchsia_hardware_pty_FEATURE_RAW) {
return pty_client_write_chunk_locked(pc, ps, buf, count, actual);
}
// newline translation time
auto chunk_start = static_cast<const char*>(buf);
auto chunk_end = chunk_start;
size_t chunk_length;
size_t chunk_actual;
size_t sent = 0;
auto partial_result = [&sent, actual](zx_status_t status) {
if (sent) {
*actual = sent;
return ZX_OK;
}
return status;
};
for (size_t i = 0; i < count; i++) {
// just iterate until there's a linefeed character
if (*chunk_end != '\n') {
chunk_end++;
continue;
}
// send up to (but not including) the linefeed
chunk_length = chunk_end - chunk_start;
status = pty_client_write_chunk_locked(pc, ps, chunk_start, chunk_length, &chunk_actual);
if (status != ZX_OK) {
return partial_result(status);
}
sent += chunk_actual;
if (chunk_actual != chunk_length) {
return partial_result(status);
}
// send the line ending
status = pty_client_write_chunk_locked(pc, ps, "\r\n", 2, &chunk_actual);
if (status != ZX_OK) {
return partial_result(status);
}
// this case means only the \r of the \r\n was sent; report to the caller
// as if it didn't work at all
if (chunk_actual != 2) {
return partial_result(status);
}
// don't increment for the \r
sent++;
chunk_start = chunk_end + 1;
chunk_end = chunk_start;
}
// finish up the buffer if necessary
chunk_length = chunk_end - chunk_start;
status = pty_client_write_chunk_locked(pc, ps, chunk_start, chunk_length, &chunk_actual);
if (status == ZX_OK) {
sent += chunk_actual;
}
return partial_result(status);
}
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 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,
fuchsia_hardware_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_fidl_OpenClient(void* ctx, uint32_t id, zx_handle_t handle,
fidl_txn_t* txn) {
auto pc = static_cast<pty_client_t*>(ctx);
auto ps = static_cast<pty_server_t*>(pc->srv);
zx::channel channel(handle);
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;
}
zx_status_t status = pty_open_client(ps, id, std::move(channel));
return fuchsia_hardware_pty_DeviceOpenClient_reply(txn, status);
}
#define fuchsia_hardware_pty_FEATURE_BAD (~fuchsia_hardware_pty_FEATURE_RAW)
zx_status_t pty_client_ClrSetFeature(void* ctx, uint32_t clr, uint32_t set, fidl_txn_t* txn) {
auto pc = static_cast<pty_client_t*>(ctx);
auto ps = static_cast<pty_server_t*>(pc->srv);
zx_status_t status = ZX_OK;
zxlogf(TRACE, "PTY Client %p (id=%u) message: clear and/or set feature\n", pc, pc->id);
if ((clr & fuchsia_hardware_pty_FEATURE_BAD) ||
(set & fuchsia_hardware_pty_FEATURE_BAD)) {
status = ZX_ERR_NOT_SUPPORTED;
} else {
fbl::AutoLock(&ps->lock);
pc->flags = (pc->flags & ~clr) | set;
}
return fuchsia_hardware_pty_DeviceClrSetFeature_reply(txn, status, pc->flags);
}
zx_status_t pty_client_GetWindowSize(void* ctx, fidl_txn_t* txn) {
auto pc = static_cast<pty_client_t*>(ctx);
auto ps = static_cast<pty_server_t*>(pc->srv);
zxlogf(TRACE, "PTY Client %p (id=%u) message: get window size\n", pc, pc->id);
mtx_lock(&ps->lock);
fuchsia_hardware_pty_WindowSize wsz;
wsz.width = ps->width;
wsz.height = ps->height;
mtx_unlock(&ps->lock);
return fuchsia_hardware_pty_DeviceGetWindowSize_reply(txn, ZX_OK, &wsz);
}
zx_status_t pty_client_MakeActive(void* ctx, uint32_t client_pty_id, fidl_txn_t* txn) {
auto pc = static_cast<pty_client_t*>(ctx);
auto ps = static_cast<pty_server_t*>(pc->srv);
zxlogf(TRACE, "PTY Client %p (id=%u) message: make active\n", pc, pc->id);
if (!(pc->flags & PTY_CLI_CONTROL)) {
return fuchsia_hardware_pty_DeviceMakeActive_reply(txn, ZX_ERR_ACCESS_DENIED);
}
mtx_lock(&ps->lock);
pty_client_t* c;
list_for_every_entry (&ps->clients, c, pty_client_t, node) {
if (c->id == client_pty_id) {
pty_make_active_locked(ps, c);
mtx_unlock(&ps->lock);
return fuchsia_hardware_pty_DeviceMakeActive_reply(txn, ZX_OK);
}
}
mtx_unlock(&ps->lock);
return fuchsia_hardware_pty_DeviceMakeActive_reply(txn, ZX_ERR_NOT_FOUND);
}
zx_status_t pty_client_ReadEvents(void* ctx, fidl_txn_t* txn) {
auto pc = static_cast<pty_client_t*>(ctx);
auto ps = static_cast<pty_server_t*>(pc->srv);
uint32_t events = 0;
zxlogf(TRACE, "PTY Client %p (id=%u) message: read events\n", pc, pc->id);
if (!(pc->flags & PTY_CLI_CONTROL)) {
return fuchsia_hardware_pty_DeviceReadEvents_reply(txn, ZX_ERR_ACCESS_DENIED, events);
}
mtx_lock(&ps->lock);
events = ps->events;
ps->events = 0;
if (ps->active == NULL) {
events |= fuchsia_hardware_pty_EVENT_HANGUP;
}
device_state_clr(pc->zxdev, fuchsia_hardware_pty_SIGNAL_EVENT);
mtx_unlock(&ps->lock);
return fuchsia_hardware_pty_DeviceReadEvents_reply(txn, ZX_OK, events);
}
zx_status_t pty_client_SetWindowSize(void* ctx, const fuchsia_hardware_pty_WindowSize* size,
fidl_txn_t* txn) {
auto pc = static_cast<pty_client_t*>(ctx);
auto ps = static_cast<pty_server_t*>(pc->srv);
if (ps->set_window_size != NULL) {
return ps->set_window_size(ps, size, txn);
} else {
return fuchsia_hardware_pty_DeviceSetWindowSize_reply(txn, ZX_ERR_NOT_SUPPORTED);
}
}
static fuchsia_hardware_pty_Device_ops_t fidl_ops = {
.OpenClient = pty_client_fidl_OpenClient,
.ClrSetFeature = pty_client_ClrSetFeature,
.GetWindowSize = pty_client_GetWindowSize,
.MakeActive = pty_client_MakeActive,
.ReadEvents = pty_client_ReadEvents,
.SetWindowSize = pty_client_SetWindowSize
};
zx_status_t pty_client_message(void* ctx, fidl_msg_t* msg, fidl_txn_t* txn) {
return fuchsia_hardware_pty_Device_dispatch(ctx, txn, msg, &fidl_ops);
}
zx_protocol_device_t pc_ops = []() {
zx_protocol_device_t ops = {};
ops.version = DEVICE_OPS_VERSION;
// ops.open = default, allow cloning
ops.release = pty_client_release;
ops.read = pty_client_read;
ops.write = pty_client_write;
ops.message = pty_client_message;
return ops;
}();
static zx_status_t pty_open_client(pty_server_t* ps, uint32_t id, zx::channel channel) {
auto pc = static_cast<pty_client_t*>(calloc(1, sizeof(pty_client_t)));
if (!pc) {
return ZX_ERR_NO_MEMORY;
}
if (!channel) {
return ZX_ERR_INVALID_ARGS;
}
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;
args.client_remote = channel.release();
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);
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 = fuchsia_hardware_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) {
static_assert(fuchsia_hardware_pty_SIGNAL_EVENT == DEV_STATE_OOB);
device_state_set(ps->control->zxdev, fuchsia_hardware_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_fidl_OpenClient(void* ctx, uint32_t id, zx_handle_t handle,
fidl_txn_t* txn) {
auto ps = static_cast<pty_server_t*>(ctx);
zx::channel channel(handle);
zxlogf(TRACE, "PTY Server %p OpenClient %u\n", ps, id);
zx_status_t status = pty_open_client(ps, id, std::move(channel));
return fuchsia_hardware_pty_DeviceOpenClient_reply(txn, status);
}
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;
}