From a9699f5c05cdcb07c693f638ba5b4937976a2cd9 Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Fri, 28 Aug 2026 16:01:10 -0700 Subject: [PATCH 1/3] vhu_input: Add an endpoint to capture events The vhost-user-input backend accepts connections on an extra unix socket. Events received through the main server are sent to any client connected to this server in addition to the frontend. At most one connection is allowed through this server at a time. Bug: b/554190370 --- .../run_cvd/launch/input_paths_provider.cc | 29 ++++++-- .../vhost_user_input/src/event_source.rs | 74 ++++++++++++++++++- .../commands/vhost_user_input/src/main.rs | 31 +++++++- .../libs/config/config_instance_derived.cc | 25 +++++++ .../libs/config/config_instance_derived.h | 7 ++ .../host/libs/config/cuttlefish_config.h | 2 + .../config/cuttlefish_config_instance.cpp | 6 ++ 7 files changed, 161 insertions(+), 13 deletions(-) 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")); From 6ed52dfeac343fdb1bf133af6578e0d2c5be412e Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Thu, 3 Sep 2026 14:51:07 -0700 Subject: [PATCH 2/3] Add ReadExactOrEof and ReadExactBinaryOrEof --- base/cvd/cuttlefish/io/read_exact.cc | 13 +++++++++++++ base/cvd/cuttlefish/io/read_exact.h | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) 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 From bba88ffb475965555b3c7e891a2e45dbe465c93e Mon Sep 17 00:00:00 2001 From: "Jorge E. Moreira" Date: Mon, 31 Aug 2026 17:29:22 -0700 Subject: [PATCH 3/3] cvd: Add cvd event_devices subcommand The new subcommand allows listing input devices as well as capturing and injecting input events. Bug: b/554190370 --- .../host/commands/cvd/cli/BUILD.bazel | 1 + .../commands/cvd/cli/commands/BUILD.bazel | 22 ++ .../cvd/cli/commands/event_devices.cpp | 268 ++++++++++++++++++ .../commands/cvd/cli/commands/event_devices.h | 57 ++++ .../host/commands/cvd/cli/request_context.cpp | 3 + .../host/commands/cvd/instances/BUILD.bazel | 1 + .../commands/cvd/instances/local_instance.cpp | 60 ++++ .../commands/cvd/instances/local_instance.h | 9 + 8 files changed, 421 insertions(+) create mode 100644 base/cvd/cuttlefish/host/commands/cvd/cli/commands/event_devices.cpp create mode 100644 base/cvd/cuttlefish/host/commands/cvd/cli/commands/event_devices.h 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.