diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel index 40d79f9f811..576292db46e 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/BUILD.bazel @@ -133,6 +133,7 @@ cf_cc_library( "//cuttlefish/host/commands/cvd/cli/commands:command_handler", "//cuttlefish/host/commands/cvd/cli/commands:display", "//cuttlefish/host/commands/cvd/cli/commands:env", + "//cuttlefish/host/commands/cvd/cli/commands:event_devices", "//cuttlefish/host/commands/cvd/cli/commands:fetch", "//cuttlefish/host/commands/cvd/cli/commands:fleet", "//cuttlefish/host/commands/cvd/cli/commands:host_tool_target", diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel index 7286fe902a1..3ace1b6295a 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel @@ -190,6 +190,28 @@ cf_cc_library( ], ) +cf_cc_library( + name = "event_devices", + srcs = ["event_devices.cpp"], + hdrs = ["event_devices.h"], + deps = [ + "//cuttlefish/common/libs/fs:fd", + "//cuttlefish/flag_parser", + "//cuttlefish/host/commands/cvd/cli:command_request", + "//cuttlefish/host/commands/cvd/cli:help_format", + "//cuttlefish/host/commands/cvd/cli/commands:command_handler", + "//cuttlefish/host/commands/cvd/cli/selector", + "//cuttlefish/host/commands/cvd/instances", + "//cuttlefish/host/commands/cvd/instances:instance_manager", + "//cuttlefish/io", + "//cuttlefish/io:read_exact", + "//cuttlefish/io:write_exact", + "//cuttlefish/result", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/strings", + ], +) + cf_cc_library( name = "lint", srcs = ["lint.cpp"], diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/event_devices.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/event_devices.cpp new file mode 100644 index 00000000000..06c13a018c1 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/event_devices.cpp @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cuttlefish/host/commands/cvd/cli/commands/event_devices.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/log/log.h" +#include "absl/strings/str_join.h" + +#include "cuttlefish/common/libs/fs/fd.h" +#include "cuttlefish/flag_parser/flag.h" +#include "cuttlefish/flag_parser/gflags_compat.h" +#include "cuttlefish/host/commands/cvd/cli/command_request.h" +#include "cuttlefish/host/commands/cvd/cli/help_format.h" +#include "cuttlefish/host/commands/cvd/cli/selector/selector.h" +#include "cuttlefish/host/commands/cvd/instances/instance_manager.h" +#include "cuttlefish/host/commands/cvd/instances/local_instance.h" +#include "cuttlefish/io/io.h" +#include "cuttlefish/io/read_exact.h" +#include "cuttlefish/io/write_exact.h" +#include "cuttlefish/result/result.h" + +namespace cuttlefish { + +namespace { +const char* kListCommand = "list"; +const char* kLsAlias = "ls"; +const char* kCaptureCommand = "capture"; +const char* kInjectCommand = "inject"; + +struct __attribute__((packed)) Event { + // The virtio_input_event structure actually contains 2 16 bits integers and + // one 32 bits one, in little endian representation. However the only + // operation on these events is going to be comparing the first two integers + // with zero, for which endianness doesn't matter and can be done in a single + // comparisson using 32 bits. + uint32_t ev_type; + uint32_t value; +}; +static_assert(sizeof(Event) == 8, + "Event structure doesn't match virtio_input_event size"); + +Result SaveEventsToFile(Reader& conn, Writer& out) { + std::chrono::steady_clock::time_point start = + std::chrono::steady_clock::now(); + std::vector events; + for (;;) { + Event event = CF_EXPECT(ReadExactBinary(conn), + "Failed to read complete event"); + events.push_back(event); + if (event.ev_type == 0) { + uint64_t timestamp = + std::chrono::duration_cast( + std::chrono::steady_clock::now() - start) + .count(); + CF_EXPECT(WriteExactBinary(out, timestamp), + "Unable to write timestamp to file"); + CF_EXPECT(WriteExact(out, reinterpret_cast(events.data()), + events.size() * sizeof(Event)), + "Unable to write events to file"); + events.clear(); + } + // The loop ends when the user interrupts the command with CTRL-C + } + return {}; +} + +Result InjectEventsFromFile(Writer& conn, Reader& in) { + std::chrono::steady_clock::time_point start = + std::chrono::steady_clock::now(); + for (;;) { + std::optional delay = + CF_EXPECT(ReadExactBinaryOrEof(in)); + if (!delay) { + break; + } + std::this_thread::sleep_until(start + std::chrono::milliseconds(*delay)); + Event event; + do { + event = CF_EXPECT(ReadExactBinary(in)); + CF_EXPECT(WriteExactBinary(conn, event)); + } while (event.ev_type != 0); + } + return {}; +} + +} // namespace + +CvdInputDevicesHandler::CvdInputDevicesHandler( + InstanceManager& instance_manager) + : instance_manager_(instance_manager) {} + +std::vector CvdInputDevicesHandler::CmdList() const { + return {"event_devices"}; +} + +std::string CvdInputDevicesHandler::SummaryHelp() const { + return "List, inject events to or capture events from input devices"; +} + +std::vector CvdInputDevicesHandler::Description() const { + return { + HelpParagraph("Usage"), + + HelpParagraph::Raw( + R"( cvd [SELECTOR_ARGS] event_devices list + cvd [SELECTOR_ARGS] event_devices capture --device=DEVICE_NAME EVENTS_FILE) + cvd [SELECTOR_ARGS] event_devices inject --device=DEVICE_NAME EVENTS_FILE)"), + + HelpParagraph("The `list` subcommand accepts no flags and can target " + "multiple instances and groups. For each selected instance " + "it prints the instance's group and name, followed by a " + "space separated list of input device names. For example:"), + + HelpParagraph::Raw( + R"( cvd-1/instance1: keyboard touch_0 + cvd-1/instance2: keyboard touch_0)"), + + HelpParagraph( + "The `capture` and `inject` subcommands require an events file and " + "the device name. These subcommands arget a single instance. The " + "events file is in binary should not be edited directly. Event " + "files should always be created via the `capture` subcommand."), + + HelpParagraph("Events capture ends when the process is interrupted, " + "typically by pressing CTRL+C in the terminal."), + + HelpParagraph( + "When the special name \"-\" is given as the events file, events " + "will be written to/read from standard output/input."), + }; +} + +Result> CvdInputDevicesHandler::Flags( + const CommandRequest& request) { + return std::vector{ + GflagsCompatFlag("device_name", flags_.device_name) + .ValueNameHint("DEVICE") + .Alias("d") + .Help("Name of input device to capture events from/inject events to. " + "Valid only with `capture` and `inject`. Available device " + "names can be obtained with the `list` subcommand."), + }; +} + +Result CvdInputDevicesHandler::Handle(const CommandRequest& request) { + std::vector args = request.SubcommandArguments(); + std::string cmd = kListCommand; + if (!args.empty()) { + cmd = std::move(args[0]); + args.erase(args.begin()); + } + if (cmd == kListCommand || cmd == kLsAlias) { + CF_EXPECT(ListDevices(request, std::move(args))); + } else if (cmd == kCaptureCommand) { + CF_EXPECT(CaptureEvents(request, std::move(args))); + } else if (cmd == kInjectCommand) { + CF_EXPECT(InjectEvents(request, std::move(args))); + } else { + return CF_ERRF( + "Unknown event_devices subcommand: `{}`. Must be one of `ls`, " + "`inject` or `capture`.", + cmd); + } + return {}; +} + +Result CvdInputDevicesHandler::ListDevices( + const CommandRequest& request, std::vector args) { + CF_EXPECTF(args.empty(), "Unknown arguments to `event_devices ls`: {}", + absl::StrJoin(args, " ")); + const std::vector>> + found_instances = + CF_EXPECT(selector::SelectInstances(instance_manager_, request)); + + if (found_instances.empty()) { + LOG(INFO) << "No devices found"; + return {}; + } + + for (const auto& [group, instances] : found_instances) { + for (const LocalInstance& instance : instances) { + std::cout << group.GroupName() << "/" << instance.Name() << ": " + << absl::StrJoin(CF_EXPECT(instance.InputDevices()), " ") + << std::endl; + } + } + + return {}; +} + +Result CvdInputDevicesHandler::CaptureEvents( + const CommandRequest& request, std::vector args) { + std::vector flags = CF_EXPECT(Flags(request)); + CF_EXPECT(ConsumeFlags(flags, args)); + CF_EXPECTF(args.size() < 2, + "Too many standalone arguments provided ({}), expected " + "`EVENTS_FILE` only", + absl::StrJoin(args, " ")); + CF_EXPECT_EQ(args.size(), 1, "Missing events file name"); + CF_EXPECT(!flags_.device_name.empty(), + "A device name is required to capture events"); + if (args[0] == "-") { + args[0] = "/proc/self/fd/1"; + } + const auto [instance, group] = + CF_EXPECT(selector::SelectInstance(instance_manager_, request), + "Unable to select an instance"); + Fd conn = + CF_EXPECT(instance.NewCaptureInputDeviceEventsConn(flags_.device_name)); + Fd out = CF_EXPECT(Fd::Creat(args[0], 0600), "Unable to create events file"); + std::cerr << "Capturing events from '" << flags_.device_name + << "'. Press CTRL+C to stop" << std::endl; + CF_EXPECT(SaveEventsToFile(conn, out)); + return {}; +} + +Result CvdInputDevicesHandler::InjectEvents( + const CommandRequest& request, std::vector args) { + std::vector flags = CF_EXPECT(Flags(request)); + CF_EXPECT(ConsumeFlags(flags, args)); + CF_EXPECTF(args.size() < 2, + "Too many standalone arguments provided ({}), expected " + "`EVENTS_FILE` only", + absl::StrJoin(args, " ")); + CF_EXPECT_EQ(args.size(), 1, "Missing events file name"); + CF_EXPECT(!flags_.device_name.empty(), + "A device name is required to inject events"); + if (args[0] == "-") { + args[0] = "/proc/self/fd/0"; + } + const auto [instance, _] = + CF_EXPECT(selector::SelectInstance(instance_manager_, request), + "Unable to select an instance"); + Fd conn = + CF_EXPECT(instance.NewInjectInputDeviceEventsConn(flags_.device_name)); + Fd in = + CF_EXPECT(Fd::Open(args[0], O_RDONLY), "Unable to create events file"); + std::cerr << "Injecting events to '" << flags_.device_name << "'." + << std::endl; + CF_EXPECT(InjectEventsFromFile(conn, in)); + return {}; +} +} // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/commands/event_devices.h b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/event_devices.h new file mode 100644 index 00000000000..b90e40dcc1e --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/commands/event_devices.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "cuttlefish/flag_parser/flag.h" +#include "cuttlefish/host/commands/cvd/cli/command_request.h" +#include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h" +#include "cuttlefish/host/commands/cvd/cli/help_format.h" +#include "cuttlefish/host/commands/cvd/instances/instance_manager.h" +#include "cuttlefish/result/result.h" + +namespace cuttlefish { + +class CvdInputDevicesHandler : public CvdCommandHandler { + public: + CvdInputDevicesHandler(InstanceManager& instance_manager); + + Result Handle(const CommandRequest& request) override; + std::vector CmdList() const override; + + bool RequiresDeviceExists() const override { return true; } + std::string SummaryHelp() const override; + std::vector Description() const override; + Result> Flags(const CommandRequest& request) override; + + private: + Result ListDevices(const CommandRequest& request, + std::vector args); + Result CaptureEvents(const CommandRequest& request, + std::vector args); + Result InjectEvents(const CommandRequest& request, + std::vector args); + + InstanceManager& instance_manager_; + struct { + std::string device_name; + } flags_; +}; + +} // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp b/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp index a507901f056..ae98f203c18 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/cli/request_context.cpp @@ -34,6 +34,7 @@ #include "cuttlefish/host/commands/cvd/cli/commands/create.h" #include "cuttlefish/host/commands/cvd/cli/commands/display.h" #include "cuttlefish/host/commands/cvd/cli/commands/env.h" +#include "cuttlefish/host/commands/cvd/cli/commands/event_devices.h" #include "cuttlefish/host/commands/cvd/cli/commands/fetch.h" #include "cuttlefish/host/commands/cvd/cli/commands/fleet.h" #include "cuttlefish/host/commands/cvd/cli/commands/help.h" @@ -149,6 +150,8 @@ RequestContext::RequestContext(InstanceManager& instance_manager, std::make_unique(instance_manager)); request_handlers_.emplace_back( std::make_unique(this->request_handlers_)); + request_handlers_.emplace_back( + std::make_unique(instance_manager)); request_handlers_.emplace_back(std::make_unique()); request_handlers_.emplace_back( std::make_unique(instance_manager)); diff --git a/base/cvd/cuttlefish/host/commands/cvd/instances/BUILD.bazel b/base/cvd/cuttlefish/host/commands/cvd/instances/BUILD.bazel index a56ff1e05c7..89003830768 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/instances/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/cvd/instances/BUILD.bazel @@ -113,6 +113,7 @@ cf_cc_library( deps = [ ":cvd_persistent_data", "//cuttlefish/common/libs/fs", + "//cuttlefish/common/libs/fs:fd", "//cuttlefish/common/libs/utils:contains", "//cuttlefish/common/libs/utils:files", "//cuttlefish/common/libs/utils:gflags_xml_parser", diff --git a/base/cvd/cuttlefish/host/commands/cvd/instances/local_instance.cpp b/base/cvd/cuttlefish/host/commands/cvd/instances/local_instance.cpp index 1686f05d09c..09d35947445 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/instances/local_instance.cpp +++ b/base/cvd/cuttlefish/host/commands/cvd/instances/local_instance.cpp @@ -21,10 +21,14 @@ #include #include +#include #include +#include #include #include "absl/log/log.h" +#include "absl/strings/str_join.h" +#include "absl/strings/strip.h" #include "cuttlefish/common/libs/utils/files.h" #include "cuttlefish/files/directory_contents.h" @@ -255,6 +259,19 @@ LocalInstance::GetInstanceConfig() { return config->ForInstance(Id()); } +Result LocalInstance::UDSDirectory() const { + // Newer cuttlefish instances put launcher monitor socket in a directory + // under /tmp, and store this path in the config. Older instances just put + // them in the instance directory. + Json::Value config = CF_EXPECT(ReadJsonConfig()); + if (config.isMember("instances_uds_dir") && + config["instances_uds_dir"].isString()) { + return fmt::format("{}/cvd-{}", config["instances_uds_dir"].asString(), + Id()); + } + return InstanceDirectory(); +} + Result LocalInstance::GetLauncherMonitor( std::chrono::seconds timeout) const { // Newer cuttlefish instances put launcher monitor socket in a directory @@ -296,4 +313,47 @@ Result> LocalInstance::LogsFilenames() const { return result; } +Result> LocalInstance::InputDevices() const { + std::string uds_dir = + CF_EXPECT(UDSDirectory(), "Unable to get instance's UDS directory") + + "/internal"; + std::vector files = CF_EXPECT(DirectoryContents(uds_dir)); + std::set event_devices; + for (std::string_view file : files) { + if (absl::ConsumeSuffix(&file, ".events.in") || + absl::ConsumeSuffix(&file, ".events.out")) { + event_devices.emplace(file); + } + } + return event_devices; +} + +Result LocalInstance::NewCaptureInputDeviceEventsConn( + const std::string& device_name) const { + std::set devices = CF_EXPECT(InputDevices()); + CF_EXPECTF(devices.find(device_name) != devices.end(), + "'{}' not found among existing input devices ({})", device_name, + absl::StrJoin(devices, ", ")); + std::string uds_dir = + CF_EXPECT(UDSDirectory(), "Unable to get instance's UDS directory"); + std::string socket = + fmt::format("{}/internal/{}.events.out", uds_dir, device_name); + return CF_EXPECTF(Fd::SocketLocalClient(socket, false, SOCK_STREAM), + "Unable to connect to {}'s capture socket", device_name); +} + +Result LocalInstance::NewInjectInputDeviceEventsConn( + const std::string& device_name) const { + std::set devices = CF_EXPECT(InputDevices()); + CF_EXPECTF(devices.find(device_name) != devices.end(), + "'{}' not found among existing input devices ({})", device_name, + absl::StrJoin(devices, ", ")); + std::string uds_dir = + CF_EXPECT(UDSDirectory(), "Unable to get instance's UDS directory"); + std::string socket = + fmt::format("{}/internal/{}.events.in", uds_dir, device_name); + return CF_EXPECTF(Fd::SocketLocalClient(socket, false, SOCK_STREAM), + "Unable to connect to {}'s inject socket", device_name); +} + } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/cvd/instances/local_instance.h b/base/cvd/cuttlefish/host/commands/cvd/instances/local_instance.h index 87c75991c3e..da97d991761 100644 --- a/base/cvd/cuttlefish/host/commands/cvd/instances/local_instance.h +++ b/base/cvd/cuttlefish/host/commands/cvd/instances/local_instance.h @@ -20,10 +20,12 @@ #include #include +#include #include #include #include +#include "cuttlefish/common/libs/fs/fd.h" #include "cuttlefish/common/libs/fs/shared_fd.h" #include "cuttlefish/host/commands/cvd/instances/cvd_persistent_data.pb.h" #include "cuttlefish/host/libs/config/cuttlefish_config.h" @@ -76,6 +78,12 @@ class LocalInstance { // Return list of filenames of instance-level log files. Result> LogsFilenames() const; + // List names of the instance's input devices. + Result> InputDevices() const; + Result NewCaptureInputDeviceEventsConn( + const std::string& device_name) const; + Result NewInjectInputDeviceEventsConn( + const std::string& device_name) const; private: LocalInstance(std::shared_ptr group_proto, @@ -86,6 +94,7 @@ class LocalInstance { Result ReadJsonConfig() const; Result LoadConfig(); Result GetInstanceConfig(); + Result UDSDirectory() const; // Sharing ownership of the group proto ensures the instance proto reference // doesn't invalidate. diff --git a/base/cvd/cuttlefish/host/commands/run_cvd/launch/input_paths_provider.cc b/base/cvd/cuttlefish/host/commands/run_cvd/launch/input_paths_provider.cc index 99af65abcd1..1c475837448 100644 --- a/base/cvd/cuttlefish/host/commands/run_cvd/launch/input_paths_provider.cc +++ b/base/cvd/cuttlefish/host/commands/run_cvd/launch/input_paths_provider.cc @@ -48,6 +48,8 @@ namespace { // Holds all sockets related to a single vhost user input device process. struct DeviceSockets { + // Path to the events capture server socket. + std::string capture_server_path; // Path to the events server socket. std::string events_server_path; // The events server fd. It's created and held at the CommandSource level to @@ -59,9 +61,11 @@ struct DeviceSockets { SharedFD vhu_server; }; -Result NewDeviceSockets(const std::string& evt_server_path, +Result NewDeviceSockets(const std::string& capture_server_path, + const std::string& evt_server_path, const std::string& vhu_server_path) { DeviceSockets ret{ + .capture_server_path = capture_server_path, .events_server_path = evt_server_path, .events_server = CF_EXPECT( Fd::SocketLocalServer(evt_server_path, false, SOCK_STREAM, 0600), @@ -81,6 +85,8 @@ Command NewVhostUserInputCommand(const DeviceSockets& device_sockets, cmd.AddParameter("--socket-fd=", device_sockets.vhu_server); cmd.AddParameter("--device-config=", spec); cmd.AddParameter("--server-fd=", device_sockets.events_server); + cmd.AddParameter("--capture-server-path=", + device_sockets.capture_server_path); return cmd; } @@ -266,33 +272,39 @@ class VhostInputDevices : public CommandSource, public InputPathsProvider { std::unordered_set Dependencies() const override { return {}; } Result ResultSetup() override { rotary_sockets_ = - CF_EXPECT(NewDeviceSockets(RotaryEventsServerPath(instance_), + CF_EXPECT(NewDeviceSockets(RotaryCaptureServerPath(instance_), + RotaryEventsServerPath(instance_), RotarySocketPath(instance_)), "Failed to setup sockets for rotary device"); if (instance_.enable_mouse()) { mouse_sockets_ = - CF_EXPECT(NewDeviceSockets(MouseEventsServerPath(instance_), + CF_EXPECT(NewDeviceSockets(MouseCaptureServerPath(instance_), + MouseEventsServerPath(instance_), MouseSocketPath(instance_)), "Failed to setup sockets for mouse device"); } if (instance_.enable_gamepad()) { gamepad_sockets_ = - CF_EXPECT(NewDeviceSockets(GamepadEventsServerPath(instance_), + CF_EXPECT(NewDeviceSockets(GamepadCaptureServerPath(instance_), + GamepadEventsServerPath(instance_), GamepadSocketPath(instance_)), "Failed to setup sockets for gamepad device"); } keyboard_sockets_ = - CF_EXPECT(NewDeviceSockets(KeyboardEventsServerPath(instance_), + CF_EXPECT(NewDeviceSockets(KeyboardCaptureServerPath(instance_), + KeyboardEventsServerPath(instance_), KeyboardSocketPath(instance_)), "Failed to setup sockets for keyboard device"); switches_sockets_ = - CF_EXPECT(NewDeviceSockets(SwitchesEventsServerPath(instance_), + CF_EXPECT(NewDeviceSockets(SwitchesCaptureServerPath(instance_), + SwitchesEventsServerPath(instance_), SwitchesSocketPath(instance_)), "Failed to setup sockets for switches device"); touchscreen_sockets_.reserve(instance_.display_configs().size()); for (int i = 0; i < instance_.display_configs().size(); ++i) { touchscreen_sockets_.emplace_back( - CF_EXPECTF(NewDeviceSockets(instance_.touch_events_server_path(i), + CF_EXPECTF(NewDeviceSockets(instance_.touch_capture_server_path(i), + instance_.touch_events_server_path(i), instance_.touch_socket_path(i)), "Failed to setup sockets for touchscreen {}", i)); } @@ -300,7 +312,8 @@ class VhostInputDevices : public CommandSource, public InputPathsProvider { for (int i = 0; i < instance_.touchpad_configs().size(); ++i) { int idx = touchscreen_sockets_.size() + i; touchpad_sockets_.emplace_back( - CF_EXPECTF(NewDeviceSockets(instance_.touch_events_server_path(idx), + CF_EXPECTF(NewDeviceSockets(instance_.touch_capture_server_path(i), + instance_.touch_events_server_path(idx), instance_.touch_socket_path(idx)), "Failed to setup sockets for touchpad {}", i)); } diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_input/src/event_source.rs b/base/cvd/cuttlefish/host/commands/vhost_user_input/src/event_source.rs index 21d3521ab53..f57aee4e1db 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_input/src/event_source.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_input/src/event_source.rs @@ -6,6 +6,8 @@ use std::sync::{Arc, Mutex}; use anyhow::{bail, Context, Result}; use log::{error, warn}; +use nix::errno::Errno; +use nix::poll::{poll, PollFd, PollFlags, PollTimeout}; use vmm_sys_util::epoll::{ControlOperation, Epoll, EpollEvent, EventSet}; use crate::buf_reader::EventReader; @@ -76,6 +78,7 @@ pub struct UnixSocketEventSource { events_fd: UnixStream, events: Arc>>, status: Arc>>, + capture_sink: Arc>>, } impl UnixSocketEventSource { @@ -92,13 +95,34 @@ impl UnixSocketEventSource { let events_clone = events.clone(); let status = Arc::new(Mutex::new(Vec::new())); let status_clone = events.clone(); - std::thread::spawn(move || server_loop(listener, bg_events_fd, events_clone, status_clone)); + let capture_sink = Arc::new(Mutex::new(None)); + let capture_sink_clone = capture_sink.clone(); + std::thread::spawn(move || { + server_loop( + listener, + bg_events_fd, + events_clone, + status_clone, + capture_sink_clone, + ) + }); Ok(Self { events_fd: fg_events_fd, events, status, + capture_sink, }) } + + pub fn with_capture_server( + listener: UnixListener, + capture_server: UnixListener, + ) -> Result { + let res = Self::new(listener)?; + let sink_clone = res.capture_sink.clone(); + std::thread::spawn(move || capture_loop(capture_server, sink_clone)); + Ok(res) + } } impl AsFd for UnixSocketEventSource { @@ -131,6 +155,7 @@ impl EventSource for UnixSocketEventSource { events_fd: self.events_fd.try_clone()?, events: self.events.clone(), status: self.status.clone(), + capture_sink: self.capture_sink.clone(), }) } } @@ -140,6 +165,7 @@ fn server_loop( mut events_fd: UnixStream, events: Arc>>, status: Arc>>, + capture_sink: Arc>>, ) { const SERVER_TOKEN: u64 = 0; const EVENT_TOKEN: u64 = 1; @@ -230,6 +256,11 @@ fn server_loop( .expect("epoll token is not associated to any fd"); match client.read_events() { Ok(mut v) => { + if let Some(ref mut s) = *capture_sink.lock().unwrap() { + if let Err(e) = s.write_all(&v) { + error!("Failed to write events to capture client: {:?}", e); + } + } events.lock().unwrap().append(&mut v); events_fd .write_all(&[0u8; 1]) @@ -254,3 +285,44 @@ fn server_loop( } } } + +fn capture_loop(server: UnixListener, sink: Arc>>) { + loop { + match server.accept() { + Err(e) => { + error!("Failed to accept connection on capture server: {:?}", e); + } + Ok((client, _)) => { + if let Err(e) = client.set_nonblocking(true) { + error!( + "Failed to set capture client connection non-blocking: {:?}", + e + ); + continue; + } + let client_clone = match client.try_clone() { + Err(e) => { + error!("Failed to clone capture client connection: {:?}", e); + continue; + } + Ok(c) => c, + }; + let _ = sink.lock().unwrap().insert(client_clone); + // Don't include POLLIN here, the peer could have closed its write channel + let mut poll_fds = [PollFd::new(client.as_fd(), PollFlags::POLLHUP)]; + match poll(&mut poll_fds, PollTimeout::NONE) { + Ok(_) => { + continue; + } + Err(e) if e == Errno::EINTR => { + continue; + } + Err(e) => { + error!("Failed to poll capture client connection: {:?}", e); + } + } + let _ = sink.lock().unwrap().take(); + } + } + } +} diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_input/src/main.rs b/base/cvd/cuttlefish/host/commands/vhost_user_input/src/main.rs index 903fe553d34..502aea02b50 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_input/src/main.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_input/src/main.rs @@ -7,6 +7,7 @@ mod vhu_input; mod vio_input; use std::fs; +use std::io::ErrorKind; use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd}; use std::os::unix::net::UnixListener; use std::str::FromStr; @@ -14,7 +15,7 @@ use std::sync::{Arc, Mutex}; use anyhow::{anyhow, bail, Context, Result}; use clap::Parser; -use log::{info, LevelFilter}; +use log::{error, info, LevelFilter}; use vhost::vhost_user::{Error as VError, Listener}; use vhost_user_backend::{Error as VHUError, VhostUserDaemon}; use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap}; @@ -36,9 +37,12 @@ struct Args { /// Path to a file specifying the device's config in JSON format. #[arg(short, long, required = true)] device_config: String, - /// A file descriptor for the unix socket event sources will connect to. + /// File descriptor for the unix socket event sources will connect to. #[arg(long, default_value_t = -1i32)] server_fd: i32, + /// Path to the event capture unix socket. + #[arg(long, default_value_t = String::from(""))] + capture_server_path: String, } fn init_logging(verbosity: &str) -> Result<()> { @@ -58,7 +62,9 @@ fn create_and_run_device( server_fd: OwnedFd, ) -> Result<()> { loop { - let event_source_clone = event_source.try_clone().context("Failed to clone event source")?; + let event_source_clone = event_source + .try_clone() + .context("Failed to clone event source")?; let event_source_fd = event_source_clone.as_fd().as_raw_fd(); // vhost::vhost_user::Listener and UnixListener take ownership of the underlying fds and // close them when dropped, so dups of the original fds are used in each iteration. @@ -138,9 +144,26 @@ fn main() -> Result<()> { if args.server_fd >= 0 { let server_fd = inherited_fd::take_fd_ownership(args.server_fd) .context("Failed to take ownership of socket fd")?; - let event_source = UnixSocketEventSource::new(UnixListener::from(server_fd))?; + let event_source = if args.capture_server_path.is_empty() { + UnixSocketEventSource::new(UnixListener::from(server_fd))? + } else { + match fs::remove_file(&args.capture_server_path) { + Err(e) if e.kind() != ErrorKind::NotFound => { + error!("Failed to remove existing capture socket: {:?}", e); + } + _ => {} + } + UnixSocketEventSource::with_capture_server( + UnixListener::from(server_fd), + UnixListener::bind(&args.capture_server_path) + .context("Failed to create capture server unix socket")?, + )? + }; create_and_run_device(event_source, device_config, socket_fd)?; } else { + if !args.capture_server_path.is_empty() { + bail!("--capture_server_path is only supported with --server_fd"); + } let event_source = StdioEventSource::new(); create_and_run_device(event_source, device_config, socket_fd)?; }; diff --git a/base/cvd/cuttlefish/host/libs/config/config_instance_derived.cc b/base/cvd/cuttlefish/host/libs/config/config_instance_derived.cc index 50ddaea43e0..372cd64bee6 100644 --- a/base/cvd/cuttlefish/host/libs/config/config_instance_derived.cc +++ b/base/cvd/cuttlefish/host/libs/config/config_instance_derived.cc @@ -52,6 +52,11 @@ std::string GamepadEventsServerPath( return ins.PerInstanceInternalUdsPath("gamepad.events.in"); } +std::string GamepadCaptureServerPath( + const CuttlefishConfig::InstanceSpecific& ins) { + return ins.PerInstanceInternalUdsPath("gamepad.events.out"); +} + std::string HwcomposerPmemPath(const CuttlefishConfig::InstanceSpecific& ins) { return AbsolutePath(ins.PerInstanceInternalPath("hwcomposer-pmem")); } @@ -69,6 +74,11 @@ std::string KeyboardEventsServerPath( return ins.PerInstanceInternalUdsPath("keyboard.events.in"); } +std::string KeyboardCaptureServerPath( + const CuttlefishConfig::InstanceSpecific& ins) { + return ins.PerInstanceInternalUdsPath("keyboard.events.out"); +} + std::string LauncherMonitorSocketPath( const CuttlefishConfig::InstanceSpecific& ins) { return AbsolutePath(ins.PerInstanceUdsPath("launcher_monitor.sock")); @@ -91,6 +101,11 @@ std::string MouseEventsServerPath( return ins.PerInstanceInternalUdsPath("mouse.events.in"); } +std::string MouseCaptureServerPath( + const CuttlefishConfig::InstanceSpecific& ins) { + return ins.PerInstanceInternalUdsPath("mouse.events.out"); +} + std::string PflashPath(const CuttlefishConfig::InstanceSpecific& ins) { return AbsolutePath(ins.PerInstancePath("pflash.img")); } @@ -108,6 +123,11 @@ std::string RotaryEventsServerPath( return ins.PerInstanceInternalUdsPath("rotary.events.in"); } +std::string RotaryCaptureServerPath( + const CuttlefishConfig::InstanceSpecific& ins) { + return ins.PerInstanceInternalUdsPath("rotary.events.out"); +} + std::string SwitchesSocketPath(const CuttlefishConfig::InstanceSpecific& ins) { return ins.PerInstanceInternalUdsPath("switches.sock"); } @@ -117,6 +137,11 @@ std::string SwitchesEventsServerPath( return ins.PerInstanceInternalUdsPath("switches.events.in"); } +std::string SwitchesCaptureServerPath( + const CuttlefishConfig::InstanceSpecific& ins) { + return ins.PerInstanceInternalUdsPath("switches.events.out"); +} + std::string RestoreAdbdPipeName(const CuttlefishConfig::InstanceSpecific& ins) { return AbsolutePath(ins.PerInstanceInternalPath("restore-pipe-adbd")); } diff --git a/base/cvd/cuttlefish/host/libs/config/config_instance_derived.h b/base/cvd/cuttlefish/host/libs/config/config_instance_derived.h index d03eecdfa01..82de7dd0683 100644 --- a/base/cvd/cuttlefish/host/libs/config/config_instance_derived.h +++ b/base/cvd/cuttlefish/host/libs/config/config_instance_derived.h @@ -28,22 +28,29 @@ std::string ConsolePath(const CuttlefishConfig::InstanceSpecific&); std::string ConsolePipePrefix(const CuttlefishConfig::InstanceSpecific&); std::string GamepadSocketPath(const CuttlefishConfig::InstanceSpecific&); std::string GamepadEventsServerPath(const CuttlefishConfig::InstanceSpecific&); +std::string GamepadCaptureServerPath(const CuttlefishConfig::InstanceSpecific&); std::string HwcomposerPmemPath(const CuttlefishConfig::InstanceSpecific&); std::string KernelLogPipeName(const CuttlefishConfig::InstanceSpecific& ins); std::string KeyboardSocketPath(const CuttlefishConfig::InstanceSpecific&); std::string KeyboardEventsServerPath(const CuttlefishConfig::InstanceSpecific&); +std::string KeyboardCaptureServerPath( + const CuttlefishConfig::InstanceSpecific&); std::string LauncherMonitorSocketPath( const CuttlefishConfig::InstanceSpecific&); std::string LogcatPath(const CuttlefishConfig::InstanceSpecific&); std::string LogcatPipeName(const CuttlefishConfig::InstanceSpecific&); std::string MouseSocketPath(const CuttlefishConfig::InstanceSpecific&); std::string MouseEventsServerPath(const CuttlefishConfig::InstanceSpecific&); +std::string MouseCaptureServerPath(const CuttlefishConfig::InstanceSpecific&); std::string PflashPath(const CuttlefishConfig::InstanceSpecific&); std::string PstorePath(const CuttlefishConfig::InstanceSpecific&); std::string RotarySocketPath(const CuttlefishConfig::InstanceSpecific&); std::string RotaryEventsServerPath(const CuttlefishConfig::InstanceSpecific&); +std::string RotaryCaptureServerPath(const CuttlefishConfig::InstanceSpecific&); std::string SwitchesSocketPath(const CuttlefishConfig::InstanceSpecific&); std::string SwitchesEventsServerPath(const CuttlefishConfig::InstanceSpecific&); +std::string SwitchesCaptureServerPath( + const CuttlefishConfig::InstanceSpecific&); std::string RestoreAdbdPipeName(const CuttlefishConfig::InstanceSpecific&); } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/libs/config/cuttlefish_config.h b/base/cvd/cuttlefish/host/libs/config/cuttlefish_config.h index d1eb831fa29..b5968115130 100644 --- a/base/cvd/cuttlefish/host/libs/config/cuttlefish_config.h +++ b/base/cvd/cuttlefish/host/libs/config/cuttlefish_config.h @@ -346,6 +346,8 @@ class CuttlefishConfig { std::string touch_events_server_path(int touch_dev_idx) const; + std::string touch_capture_server_path(int touch_dev_idx) const; + std::string media_socket_path(int index) const; std::string launcher_log_path() const; diff --git a/base/cvd/cuttlefish/host/libs/config/cuttlefish_config_instance.cpp b/base/cvd/cuttlefish/host/libs/config/cuttlefish_config_instance.cpp index 02cad7f2991..01cdf6d1b32 100644 --- a/base/cvd/cuttlefish/host/libs/config/cuttlefish_config_instance.cpp +++ b/base/cvd/cuttlefish/host/libs/config/cuttlefish_config_instance.cpp @@ -1931,6 +1931,12 @@ std::string CuttlefishConfig::InstanceSpecific::touch_events_server_path( return PerInstanceInternalUdsPath(name); } +std::string CuttlefishConfig::InstanceSpecific::touch_capture_server_path( + int touch_dev_idx) const { + std::string name = absl::StrCat("touch_", touch_dev_idx, ".events.out"); + return PerInstanceInternalUdsPath(name); +} + std::string CuttlefishConfig::InstanceSpecific::media_socket_path( int index) const { return PerInstanceInternalUdsPath(absl::StrCat("media_", index, ".sock")); diff --git a/base/cvd/cuttlefish/io/read_exact.cc b/base/cvd/cuttlefish/io/read_exact.cc index 82092a9c4e5..1e329159200 100644 --- a/base/cvd/cuttlefish/io/read_exact.cc +++ b/base/cvd/cuttlefish/io/read_exact.cc @@ -46,4 +46,17 @@ Result PReadExact(const ReaderSeeker& reader, char* buf, size_t size, return {}; } +Result ReadExactOrEof(Reader& reader, char* buf, size_t size) { + size_t read = 0; + while (read < size) { + size_t data_read = CF_EXPECT(reader.Read((void*)(buf + read), size - read)); + if (data_read == 0 && read == 0) { + return false; + } + CF_EXPECT_GT(data_read, 0, "EOF, still want to read " << size - read); + read += data_read; + } + return true; +} + } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/io/read_exact.h b/base/cvd/cuttlefish/io/read_exact.h index fb78e563b78..7561f568eed 100644 --- a/base/cvd/cuttlefish/io/read_exact.h +++ b/base/cvd/cuttlefish/io/read_exact.h @@ -17,6 +17,8 @@ #include +#include + #include "cuttlefish/io/io.h" #include "cuttlefish/result/expect.h" #include "cuttlefish/result/result_type.h" @@ -36,6 +38,10 @@ Result ReadExactBinary(Reader& reader) { Result PReadExact(const ReaderSeeker&, char* buf, size_t size, uint64_t offset); +// Similar to ReadExact, but returns false instead of an error if zero bytes are +// read because of EOF. A partial read is still an error. +Result ReadExactOrEof(Reader&, char* buf, size_t size); + template Result PReadExactBinary(const ReaderSeeker& reader, uint64_t offset) { T data; @@ -44,4 +50,14 @@ Result PReadExactBinary(const ReaderSeeker& reader, uint64_t offset) { return data; } +template +Result> ReadExactBinaryOrEof(Reader& reader) { + T data; + char* data_char = reinterpret_cast(&data); + if (!CF_EXPECT(ReadExactOrEof(reader, data_char, sizeof(data)))) { + return std::nullopt; + } + return data; +} + } // namespace cuttlefish