| // 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 <errno.h> |
| #include <fcntl.h> |
| #include <fuchsia/hardware/pty/llcpp/fidl.h> |
| #include <lib/fdio/cpp/caller.h> |
| #include <lib/fdio/fd.h> |
| #include <lib/fdio/io.h> |
| #include <lib/service/llcpp/service.h> |
| #include <lib/zx/time.h> |
| #include <poll.h> |
| #include <unistd.h> |
| #include <zircon/status.h> |
| |
| #include <zxtest/zxtest.h> |
| |
| namespace fpty = fuchsia_hardware_pty; |
| |
| zx::status<uint32_t> fd_signals(const fbl::unique_fd& fd, uint32_t wait_for_any, |
| zx::time deadline) { |
| uint32_t signals = 0; |
| zx_status_t status = fdio_wait_fd(fd.get(), wait_for_any, &signals, deadline.get()); |
| if (status != ZX_OK && status != ZX_ERR_TIMED_OUT) { |
| return zx::error(status); |
| } |
| return zx::ok(signals); |
| } |
| |
| #define ASSERT_SIGNALS(val1, val2) \ |
| ASSERT_OK(val1); \ |
| ASSERT_EQ(val1.value(), val2) |
| |
| static ssize_t write_full(const fbl::unique_fd& fd) { |
| char tmp[300]; |
| memset(tmp, 0x33, sizeof(tmp)); |
| ssize_t total = 0; |
| for (;;) { |
| ssize_t r = write(fd.get(), tmp, sizeof(tmp)); |
| if (r < 0) { |
| if (errno == EAGAIN) { |
| break; |
| } |
| return r; |
| } |
| if (r == 0) { |
| break; |
| } |
| total += r; |
| } |
| return total; |
| } |
| |
| static ssize_t read_all(const fbl::unique_fd& fd) { |
| char tmp[700]; |
| ssize_t total = 0; |
| for (;;) { |
| ssize_t r = read(fd.get(), tmp, sizeof(tmp)); |
| if (r < 0) { |
| if (errno == EAGAIN) { |
| break; |
| } |
| return r; |
| } |
| if (r == 0) { |
| break; |
| } |
| for (ssize_t n = 0; n < r; n++) { |
| if (tmp[n] != 0x33) { |
| return -EFAULT; |
| } |
| } |
| total += r; |
| } |
| return total; |
| } |
| |
| static zx_status_t open_client(const fbl::unique_fd& fd, uint32_t client_id, int* out_fd) { |
| if (!out_fd) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| fdio_cpp::UnownedFdioCaller io(fd.get()); |
| |
| zx::status endpoints = fidl::CreateEndpoints<fpty::Device>(); |
| if (endpoints.is_error()) { |
| return endpoints.status_value(); |
| } |
| |
| auto result = fidl::WireCall(io.borrow_as<fpty::Device>()) |
| .OpenClient(client_id, std::move(endpoints->server)); |
| |
| if (result.status() != ZX_OK) { |
| return result.status(); |
| } |
| if (result->s != ZX_OK) { |
| return result->s; |
| } |
| |
| zx_status_t status = fdio_fd_create(endpoints->client.channel().release(), out_fd); |
| if (status != ZX_OK) { |
| return status; |
| } |
| return fcntl(*out_fd, F_SETFL, O_NONBLOCK); |
| } |
| |
| TEST(PtyTests, pty_test) { |
| // Connect to the PTY service. We have to do this dance rather than just |
| // using open() because open() uses the DESCRIBE flag internally, and the |
| // plumbing of the PTY service through svchost causes the DESCRIBE to get |
| // consumed by the wrong code, resulting in the wrong NodeInfo being provided. |
| // This manifests as a loss of fd signals. |
| fbl::unique_fd ps; |
| { |
| zx::status client_end = service::Connect<fpty::Device>(); |
| ASSERT_OK(client_end.status_value()); |
| |
| ASSERT_OK(fdio_fd_create(client_end->channel().release(), ps.reset_and_get_address())); |
| ASSERT_TRUE(ps.is_valid()); |
| int flags; |
| ASSERT_GE(flags = fcntl(ps.get(), F_GETFL), 0, "%s", strerror(errno)); |
| ASSERT_EQ(fcntl(ps.get(), F_SETFL, flags | O_NONBLOCK), 0, "%s", strerror(errno)); |
| } |
| |
| fdio_cpp::UnownedFdioCaller ps_io(ps.get()); |
| |
| fbl::unique_fd pc; |
| ASSERT_OK(open_client(ps, 0, pc.reset_and_get_address())); |
| ASSERT_TRUE(pc.is_valid()); |
| |
| fdio_cpp::UnownedFdioCaller pc_io(pc.get()); |
| |
| char tmp[32]; |
| |
| ASSERT_SIGNALS(fd_signals(ps, POLLOUT, zx::time{}), POLLOUT); |
| ASSERT_SIGNALS(fd_signals(pc, POLLOUT, zx::time{}), POLLOUT); |
| |
| // nothing to read |
| ASSERT_EQ(read(ps.get(), tmp, 32), -1); |
| ASSERT_EQ(errno, EAGAIN, "%s", strerror(errno)); |
| ASSERT_EQ(read(pc.get(), tmp, 32), -1); |
| ASSERT_EQ(errno, EAGAIN, "%s", strerror(errno)); |
| |
| // write server, read client |
| ASSERT_EQ(write(ps.get(), "xyzzy", 5), 5, "%s", strerror(errno)); |
| ASSERT_SIGNALS(fd_signals(pc, POLLIN | POLLOUT, zx::time{}), POLLIN | POLLOUT); |
| |
| memset(tmp, 0, 6); |
| ASSERT_EQ(read(pc.get(), tmp, 5), 5, "%s", strerror(errno)); |
| ASSERT_SUBSTR(tmp, "xyzzy"); |
| ASSERT_SIGNALS(fd_signals(pc, POLLOUT, zx::time{}), POLLOUT); |
| |
| // write client, read server |
| ASSERT_EQ(write(pc.get(), "xyzzy", 5), 5, "%s", strerror(errno)); |
| ASSERT_SIGNALS(fd_signals(ps, POLLIN | POLLOUT, zx::time{}), POLLIN | POLLOUT); |
| |
| memset(tmp, 0, 6); |
| ASSERT_EQ(read(ps.get(), tmp, 5), 5, "%s", strerror(errno)); |
| ASSERT_SUBSTR(tmp, "xyzzy"); |
| ASSERT_SIGNALS(fd_signals(ps, POLLOUT, zx::time{}), POLLOUT); |
| |
| // write server until full, then drain |
| ASSERT_EQ(write_full(ps), 4096, "%s", strerror(errno)); |
| ASSERT_SIGNALS(fd_signals(ps, 0, zx::time{}), 0); |
| ASSERT_EQ(read_all(pc), 4096, "%s", strerror(errno)); |
| ASSERT_SIGNALS(fd_signals(ps, POLLOUT, zx::time{}), POLLOUT); |
| |
| // write client until full, then drain |
| ASSERT_EQ(write_full(pc), 4096, "%s", strerror(errno)); |
| ASSERT_SIGNALS(fd_signals(pc, 0, zx::time{}), 0); |
| ASSERT_EQ(read_all(ps), 4096, "%s", strerror(errno)); |
| ASSERT_SIGNALS(fd_signals(pc, POLLOUT, zx::time{}), POLLOUT); |
| |
| // verify no events pending |
| auto result1 = fidl::WireCall(pc_io.borrow_as<fpty::Device>()).ReadEvents(); |
| |
| ASSERT_OK(result1.status()); |
| ASSERT_OK(result1->status); |
| ASSERT_EQ(result1->events, 0u); |
| |
| // write a ctrl-c |
| ASSERT_EQ(write(ps.get(), "\x03", 1), 1, "%s", strerror(errno)); |
| |
| // should be an event now |
| auto result2 = fidl::WireCall(pc_io.borrow_as<fpty::Device>()).ReadEvents(); |
| ASSERT_OK(result2.status()); |
| ASSERT_OK(result2->status); |
| ASSERT_EQ(result2->events, fpty::wire::EVENT_INTERRUPT); |
| |
| // should vanish once we read it |
| auto result3 = fidl::WireCall(pc_io.borrow_as<fpty::Device>()).ReadEvents(); |
| ASSERT_OK(result3.status()); |
| ASSERT_OK(result3->status); |
| ASSERT_EQ(result3->events, 0u); |
| |
| // write something containing a special char |
| // should write up to and including the special char |
| // converting the special char to a signal |
| ASSERT_EQ(write(ps.get(), "hello\x03world", 11), 6, "%s", strerror(errno)); |
| ASSERT_EQ(read(pc.get(), tmp, 6), 5, "%s", strerror(errno)); |
| ASSERT_SUBSTR(tmp, "hello"); |
| auto result4 = fidl::WireCall(pc_io.borrow_as<fpty::Device>()).ReadEvents(); |
| ASSERT_OK(result4.status()); |
| ASSERT_OK(result4->status); |
| ASSERT_EQ(result4->events, fpty::wire::EVENT_INTERRUPT); |
| |
| auto ws_result1 = fidl::WireCall(pc_io.borrow_as<fpty::Device>()).GetWindowSize(); |
| ASSERT_OK(ws_result1.status()); |
| ASSERT_OK(ws_result1->status); |
| ASSERT_EQ(ws_result1->size.width, 0u, "%s", strerror(errno)); |
| ASSERT_EQ(ws_result1->size.height, 0u, "%s", strerror(errno)); |
| |
| fpty::wire::WindowSize ws; |
| ws.width = 80; |
| ws.height = 25; |
| auto result5 = fidl::WireCall(pc_io.borrow_as<fpty::Device>()).SetWindowSize(ws); |
| ASSERT_OK(result5.status()); |
| ASSERT_OK(result5->status); |
| auto ws_result2 = fidl::WireCall(pc_io.borrow_as<fpty::Device>()).GetWindowSize(); |
| ASSERT_OK(ws_result2.status()); |
| ASSERT_OK(ws_result2->status); |
| ASSERT_EQ(ws_result2->size.width, 80u, "%s", strerror(errno)); |
| ASSERT_EQ(ws_result2->size.height, 25u, "%s", strerror(errno)); |
| |
| // verify that we don't get events for special chars in raw mode |
| auto result6 = |
| fidl::WireCall(pc_io.borrow_as<fpty::Device>()).ClrSetFeature(0, fpty::wire::FEATURE_RAW); |
| ASSERT_OK(result6.status()); |
| ASSERT_OK(result6->status); |
| ASSERT_EQ(result6->features & fpty::wire::FEATURE_RAW, fpty::wire::FEATURE_RAW); |
| ASSERT_EQ(write(ps.get(), "\x03", 1), 1, "%s", strerror(errno)); |
| ASSERT_EQ(read(pc.get(), tmp, 1), 1, "%s", strerror(errno)); |
| ASSERT_EQ(tmp[0], '\x03', "%s", strerror(errno)); |
| auto result7 = fidl::WireCall(pc_io.borrow_as<fpty::Device>()).ReadEvents(); |
| ASSERT_OK(result7.status()); |
| ASSERT_OK(result7->status); |
| ASSERT_EQ(result7->events, 0u); |
| |
| // create a second client |
| fbl::unique_fd pc1; |
| ASSERT_OK(open_client(pc, 1, pc1.reset_and_get_address())); |
| ASSERT_TRUE(pc1.is_valid()); |
| |
| fdio_cpp::UnownedFdioCaller pc1_io(pc1.get()); |
| |
| // reads/writes to non-active client should block |
| ASSERT_SIGNALS(fd_signals(pc1, 0, zx::time{}), 0); |
| ASSERT_EQ(write(pc1.get(), "test", 4), -1); |
| ASSERT_EQ(errno, EAGAIN, "%s", strerror(errno)); |
| ASSERT_EQ(read(pc1.get(), tmp, 4), -1); |
| ASSERT_EQ(errno, EAGAIN, "%s", strerror(errno)); |
| |
| uint32_t n = 2; |
| auto result8 = fidl::WireCall(pc_io.borrow_as<fpty::Device>()).MakeActive(n); |
| ASSERT_EQ(result8.status(), ZX_OK); |
| ASSERT_STATUS(result8->status, ZX_ERR_NOT_FOUND); |
| |
| // non-controlling client cannot change active client |
| auto result9 = fidl::WireCall(pc1_io.borrow_as<fpty::Device>()).MakeActive(n); |
| ASSERT_EQ(result9.status(), ZX_OK); |
| ASSERT_STATUS(result9->status, ZX_ERR_ACCESS_DENIED); |
| |
| // but controlling client can |
| n = 1; |
| auto result10 = fidl::WireCall(pc_io.borrow_as<fpty::Device>()).MakeActive(n); |
| ASSERT_OK(result10.status()); |
| ASSERT_OK(result10->status); |
| ASSERT_SIGNALS(fd_signals(pc, 0, zx::time{}), 0); |
| ASSERT_SIGNALS(fd_signals(pc1, POLLOUT, zx::time{}), POLLOUT); |
| ASSERT_EQ(write(pc1.get(), "test", 4), 4, "%s", strerror(errno)); |
| ASSERT_EQ(read(ps.get(), tmp, 4), 4, "%s", strerror(errno)); |
| ASSERT_SUBSTR(tmp, "test"); |
| |
| // make sure controlling client observes departing active client |
| pc1_io.reset(); |
| pc1.reset(); |
| ASSERT_SIGNALS(fd_signals(pc, POLLHUP | POLLPRI, zx::time::infinite()), POLLHUP | POLLPRI); |
| auto result11 = fidl::WireCall(pc_io.borrow_as<fpty::Device>()).ReadEvents(); |
| ASSERT_OK(result11.status()); |
| ASSERT_OK(result11->status); |
| ASSERT_EQ(result11->events, fpty::wire::EVENT_HANGUP); |
| |
| // verify that server observes departure of last client |
| pc_io.reset(); |
| pc.reset(); |
| ASSERT_SIGNALS(fd_signals(ps, POLLHUP | POLLIN, zx::time::infinite()), POLLHUP | POLLIN); |
| |
| ps_io.reset(); |
| ps.reset(); |
| } |
| |
| TEST(PtyTests, not_a_pty_test) { |
| fbl::unique_fd root_dir; |
| ASSERT_TRUE(root_dir = fbl::unique_fd(open("/", O_DIRECTORY | O_RDONLY)), "%s", strerror(errno)); |
| |
| fdio_cpp::UnownedFdioCaller io(root_dir.get()); |
| |
| // Sending pty messages such as 'get window size' should fail |
| // properly on things that are not ptys. |
| auto result = fidl::WireCall(io.borrow_as<fpty::Device>()).GetWindowSize(); |
| ASSERT_STATUS(result.status(), ZX_ERR_BAD_HANDLE); |
| |
| io.reset(); |
| } |