blob: a5e897d94a119b36920625ab375bf82f8cfd46ab [file] [log] [blame]
// Copyright 2016 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 <assert.h>
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <memory>
#include <fbl/algorithm.h>
#include <fbl/string_piece.h>
#include <fbl/unique_fd.h>
#include <fs/handler.h>
#include <fuchsia/hardware/pty/c/fidl.h>
#include <fuchsia/io/c/fidl.h>
#include <fuchsia/virtualconsole/c/fidl.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/fdio.h>
#include <lib/fdio/io.h>
#include <lib/fdio/spawn.h>
#include <lib/fdio/watcher.h>
#include <lib/fzl/fdio.h>
#include <lib/zx/channel.h>
#include <port/port.h>
#include <zircon/device/vfs.h>
#include <zircon/listnode.h>
#include <zircon/process.h>
#include <zircon/processargs.h>
#include <zircon/status.h>
#include <zircon/syscalls.h>
#include <zircon/syscalls/log.h>
#include <zircon/syscalls/object.h>
#include <utility>
#include "vc.h"
port_t port;
static port_handler_t log_ph;
static port_handler_t new_vc_ph;
static port_handler_t input_ph;
static int input_dir_fd;
static vc_t* log_vc;
static zx_koid_t proc_koid;
static zx_status_t log_reader_cb(port_handler_t* ph, zx_signals_t signals, uint32_t evt) {
char buf[ZX_LOG_RECORD_MAX];
zx_log_record_t* rec = (zx_log_record_t*)buf;
zx_status_t status;
for (;;) {
if ((status = zx_debuglog_read(ph->handle, 0, rec, ZX_LOG_RECORD_MAX)) < 0) {
if (status == ZX_ERR_SHOULD_WAIT) {
return ZX_OK;
}
break;
}
// don't print log messages from ourself
if (rec->pid == proc_koid) {
continue;
}
char tmp[64];
snprintf(tmp, 64, "\033[32m%05d.%03d\033[39m] \033[31m%05" PRIu64 ".\033[36m%05" PRIu64 "\033[39m> ",
(int)(rec->timestamp / 1000000000ULL),
(int)((rec->timestamp / 1000000ULL) % 1000ULL),
rec->pid, rec->tid);
vc_write(log_vc, tmp, strlen(tmp), 0);
vc_write(log_vc, rec->data, rec->datalen, 0);
if ((rec->datalen == 0) ||
(rec->data[rec->datalen - 1] != '\n')) {
vc_write(log_vc, "\n", 1, 0);
}
}
const char* oops = "<<LOG ERROR>>\n";
vc_write(log_vc, oops, strlen(oops), 0);
// Error reading the log, no point in continuing to try to read
// log messages.
port_cancel(&port, &log_ph);
return status;
}
static zx_status_t launch_shell(vc_t* vc, int fd, const char* cmd) {
const char* argv[] = { "/boot/bin/sh", nullptr, nullptr, nullptr };
if (cmd) {
argv[1] = "-c";
argv[2] = cmd;
}
fdio_spawn_action_t actions[2] = {};
actions[0].action = FDIO_SPAWN_ACTION_SET_NAME;
actions[0].name.data = "vc:sh";
actions[1].action = FDIO_SPAWN_ACTION_TRANSFER_FD;
actions[1].fd = {.local_fd = fd, .target_fd = FDIO_FLAG_USE_FOR_STDIO};
uint32_t flags = FDIO_SPAWN_CLONE_ALL & ~FDIO_SPAWN_CLONE_STDIO;
char err_msg[FDIO_SPAWN_ERR_MSG_MAX_LENGTH];
zx_status_t status = fdio_spawn_etc(ZX_HANDLE_INVALID, flags, argv[0], argv,
nullptr, fbl::count_of(actions), actions,
&vc->proc, err_msg);
if (status != ZX_OK) {
printf("vc: cannot spawn shell: %s: %d (%s)\n", err_msg, status,
zx_status_get_string(status));
}
return status;
}
static void session_destroy(vc_t* vc) {
if (vc->fd >= 0) {
port_fd_handler_done(&vc->fh);
// vc_destroy() closes the fd
}
if (vc->proc != ZX_HANDLE_INVALID) {
zx_task_kill(vc->proc);
}
vc_destroy(vc);
}
static zx_status_t session_io_cb(port_fd_handler_t* fh, unsigned pollevt, uint32_t evt) {
vc_t* vc = containerof(fh, vc_t, fh);
if (pollevt & POLLIN) {
char data[1024];
ssize_t r = read(vc->fd, data, sizeof(data));
if (r > 0) {
vc_write(vc, data, r, 0);
return ZX_OK;
}
}
if (pollevt & (POLLRDHUP | POLLHUP)) {
// shell sessions get restarted on exit
if (vc->is_shell) {
zx_task_kill(vc->proc);
vc->proc = ZX_HANDLE_INVALID;
int fd = openat(vc->fd, "0", O_RDWR);
if (fd < 0) {
goto fail;
}
if (launch_shell(vc, fd, NULL) < 0) {
goto fail;
}
return ZX_OK;
}
}
fail:
session_destroy(vc);
return ZX_ERR_STOP;
}
static zx_status_t remote_session_create(vc_t** out, zx::channel session, bool make_active, bool special) {
// The ptmx device can start later than these threads
int retry = 30;
int raw_fd;
while ((raw_fd = open("/dev/misc/ptmx", O_RDWR | O_NONBLOCK)) < 0) {
if (--retry == 0) {
return ZX_ERR_IO;
}
usleep(100000);
}
fbl::unique_fd fd(raw_fd);
fdio_t* io = fdio_unsafe_fd_to_io(fd.get());
if (io == nullptr) {
return ZX_ERR_INTERNAL;
}
zx_status_t status;
zx_status_t fidl_status = fuchsia_hardware_pty_DeviceOpenClient(
fdio_unsafe_borrow_channel(io), 0, session.release(), &status);
fdio_unsafe_release(io);
if (fidl_status != ZX_OK) {
return fidl_status;
}
if (status != ZX_OK) {
return status;
}
vc_t* vc;
if (vc_create(&vc, special)) {
return ZX_ERR_INTERNAL;
}
zx_status_t r;
if ((r = port_fd_handler_init(&vc->fh, fd.get(), POLLIN | POLLRDHUP | POLLHUP)) < 0) {
vc_destroy(vc);
return r;
}
if (isatty(fd.get())) {
fuchsia_hardware_pty_WindowSize wsz = {
.width = vc->columns,
.height = vc->rows,
};
io = fdio_unsafe_fd_to_io(fd.get());
fuchsia_hardware_pty_DeviceSetWindowSize(fdio_unsafe_borrow_channel(io), &wsz, &status);
fdio_unsafe_release(io);
}
vc->fd = fd.release();
if (make_active) {
vc_set_active(-1, vc);
}
vc->fh.func = session_io_cb;
*out = vc;
return ZX_OK;
}
static zx_status_t session_create(vc_t** out, int* out_fd, bool make_active, bool special) {
zx::channel device_channel, client_channel;
zx_status_t status = zx::channel::create(0, &device_channel, &client_channel);
if (status != ZX_OK) {
return status;
}
status = remote_session_create(out, std::move(device_channel), make_active, special);
if (status != ZX_OK) {
return status;
}
int raw_client_fd;
status = fdio_fd_create(client_channel.release(), &raw_client_fd);
if (status != ZX_OK) {
return status;
}
fbl::unique_fd client_fd(raw_client_fd);
*out_fd = client_fd.release();
return ZX_OK;
}
static void start_shell(bool make_active, const char* cmd) {
vc_t* vc = nullptr;
int fd = 0;
if (session_create(&vc, &fd, make_active, cmd != NULL) < 0) {
return;
}
vc->is_shell = true;
if (launch_shell(vc, fd, cmd) < 0) {
session_destroy(vc);
} else {
port_wait(&port, &vc->fh.ph);
}
}
static zx_status_t new_vc_cb(void*, zx_handle_t session, fidl_txn_t* txn) {
zx::channel session_channel(session);
vc_t* vc = nullptr;
if (remote_session_create(&vc, std::move(session_channel), true, false) < 0) {
return ZX_OK;
}
port_wait(&port, &vc->fh.ph);
return fuchsia_virtualconsole_SessionManagerCreateSession_reply(txn, ZX_OK);
}
static zx_status_t fidl_message_cb(port_handler_t* ph, zx_signals_t signals, uint32_t evt) {
if ((signals & ZX_CHANNEL_PEER_CLOSED) &&
!(signals & ZX_CHANNEL_READABLE)) {
zx_handle_close(ph->handle);
delete ph;
return ZX_ERR_STOP;
}
auto status = fs::ReadMessage(ph->handle, [](fidl_msg_t* message, fs::FidlConnection* txn) {
static constexpr fuchsia_virtualconsole_SessionManager_ops_t kOps {
.CreateSession = new_vc_cb,
};
return fuchsia_virtualconsole_SessionManager_dispatch(nullptr,
reinterpret_cast<fidl_txn_t*>(txn),
message,
&kOps);
});
if (status != ZX_OK) {
printf("Failed to dispatch fidl message from client: %s\n", zx_status_get_string(status));
zx_handle_close(ph->handle);
delete ph;
return ZX_ERR_STOP;
}
return ZX_OK;
}
static zx_status_t fidl_connection_cb(port_handler_t* ph, zx_signals_t signals, uint32_t evt) {
constexpr size_t kBufferSize = 256;
char buffer[kBufferSize];
uint32_t bytes_read, handles_read;
zx_handle_t client_raw;
auto status = zx_channel_read(ph->handle, 0,
buffer, &client_raw,
kBufferSize, 1,
&bytes_read, &handles_read);
if (status != ZX_OK) {
printf("Failed to read from channel: %s\n", zx_status_get_string(status));
return ZX_OK;
}
if (handles_read < 1) {
printf("Fidl connection with no channel.\n");
return ZX_OK;
}
zx::channel client(client_raw);
if (fbl::StringPiece(fuchsia_virtualconsole_SessionManager_Name) ==
fbl::StringPiece(buffer, bytes_read)) {
auto handler = std::unique_ptr<port_handler_t>(new port_handler_t {
.handle = client.release(),
.waitfor = ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED,
.func = fidl_message_cb,
});
port_wait(&port, handler.release());
} else {
printf("Unsupported fidl interface: %.*s\n", bytes_read, buffer);
}
return ZX_OK;
}
static zx_status_t input_dir_event(unsigned evt, const char* name) {
if ((evt != fuchsia_io_WATCH_EVENT_EXISTING) && (evt != fuchsia_io_WATCH_EVENT_ADDED)) {
return ZX_OK;
}
printf("vc: new input device /dev/class/input/%s\n", name);
int fd;
if ((fd = openat(input_dir_fd, name, O_RDONLY)) < 0) {
return ZX_OK;
}
new_input_device(fd, handle_key_press);
return ZX_OK;
}
static void setup_dir_watcher(const char* dir,
zx_status_t (*cb)(port_handler_t*, zx_signals_t, uint32_t),
port_handler_t* ph,
int* fd_out) {
*fd_out = -1;
fbl::unique_fd fd(open(dir, O_DIRECTORY | O_RDONLY));
if (!fd) {
return;
}
zx::channel client, server;
if (zx::channel::create(0, &client, &server) != ZX_OK) {
return;
}
fzl::FdioCaller caller(std::move(fd));
zx_status_t status;
zx_status_t io_status = fuchsia_io_DirectoryWatch(caller.borrow_channel(),
fuchsia_io_WATCH_MASK_ALL, 0,
server.release(),
&status);
if (io_status != ZX_OK || status != ZX_OK) {
return;
}
*fd_out = caller.release().release();
ph->handle = client.release();
ph->waitfor = ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED;
ph->func = cb;
port_wait(&port, ph);
}
zx_status_t handle_device_dir_event(port_handler_t* ph, zx_signals_t signals,
zx_status_t (*event_handler)(unsigned event, const char* msg)) {
if (!(signals & ZX_CHANNEL_READABLE)) {
printf("vc: device directory died\n");
return ZX_ERR_STOP;
}
// Buffer contains events { Opcode, Len, Name[Len] }
// See zircon/device/vfs.h for more detail
// extra byte is for temporary NUL
uint8_t buf[fuchsia_io_MAX_BUF + 1];
uint32_t len;
if (zx_channel_read(ph->handle, 0, buf, NULL, sizeof(buf) - 1, 0, &len, NULL) < 0) {
printf("vc: failed to read from device directory\n");
return ZX_ERR_STOP;
}
uint8_t* msg = buf;
while (len >= 2) {
uint8_t event = *msg++;
uint8_t namelen = *msg++;
if (len < (namelen + 2u)) {
printf("vc: malformed device directory message\n");
return ZX_ERR_STOP;
}
// add temporary nul
uint8_t tmp = msg[namelen];
msg[namelen] = 0;
zx_status_t status = event_handler(event, (char*)msg);
if (status != ZX_OK) {
return status;
}
msg[namelen] = tmp;
msg += namelen;
len -= (namelen + 2u);
}
return ZX_OK;
}
static zx_status_t input_cb(port_handler_t* ph, zx_signals_t signals, uint32_t evt) {
return handle_device_dir_event(ph, signals, input_dir_event);
}
void set_log_listener_active(bool active) {
if (active) {
port_wait(&port, &log_ph);
} else {
port_cancel(&port, &log_ph);
}
}
int main(int argc, char** argv) {
// NOTE: devmgr has getenv_bool. when more options are added, consider
// sharing that.
bool keep_log = false;
const char* value = getenv("virtcon.keep-log-visible");
if (value == NULL ||
((strcmp(value, "0") == 0) ||
(strcmp(value, "false") == 0) ||
(strcmp(value, "off") == 0))) {
keep_log = false;
} else {
keep_log = true;
}
const char* cmd = NULL;
int shells = 0;
while (argc > 1) {
if (!strcmp(argv[1], "--run")) {
if (argc > 2) {
argc--;
argv++;
cmd = argv[1];
if (shells < 1)
shells = 1;
printf("CMD: %s\n", cmd);
}
} else if (!strcmp(argv[1], "--shells")) {
if (argc > 2) {
argc--;
argv++;
shells = atoi(argv[1]);
}
}
argc--;
argv++;
}
if (port_init(&port) < 0) {
return -1;
}
// create initial console for debug log
if (vc_create(&log_vc, false) != ZX_OK) {
return -1;
}
snprintf(log_vc->title, sizeof(log_vc->title), "debuglog");
// Get our process koid so the log reader can
// filter out our own debug messages from the log
zx_info_handle_basic_t info;
if (zx_object_get_info(zx_process_self(), ZX_INFO_HANDLE_BASIC, &info,
sizeof(info), NULL, NULL) == ZX_OK) {
proc_koid = info.koid;
}
log_ph.handle = zx_take_startup_handle(PA_HND(PA_USER0, 1));
if (log_ph.handle == ZX_HANDLE_INVALID) {
printf("vc log listener: did not receive log startup handle\n");
return -1;
}
log_ph.func = log_reader_cb;
log_ph.waitfor = ZX_LOG_READABLE;
if ((new_vc_ph.handle = zx_take_startup_handle(PA_HND(PA_USER0, 0))) != ZX_HANDLE_INVALID) {
new_vc_ph.func = fidl_connection_cb;
new_vc_ph.waitfor = ZX_CHANNEL_READABLE;
port_wait(&port, &new_vc_ph);
}
setup_dir_watcher("/dev/class/input", input_cb, &input_ph, &input_dir_fd);
if (!vc_display_init()) {
return -1;
}
setenv("TERM", "xterm", 1);
for (int i = 0; i < shells; ++i) {
if (i == 0)
start_shell(!keep_log, cmd);
else
start_shell(false, NULL);
}
zx_status_t r = port_dispatch(&port, ZX_TIME_INFINITE, false);
printf("vc: port failure: %d\n", r);
return -1;
}