| // 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; |
| } |