mirror of
https://github.com/LizardByte/libvirtualhid.git
synced 2026-11-04 02:57:35 +09:00
3071 lines
114 KiB
C++
3071 lines
114 KiB
C++
/**
|
|
* @file tests/fixtures/linux_backend_test_hooks.cpp
|
|
* @brief Linux backend test hook definitions.
|
|
*/
|
|
|
|
// standard includes
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <bit>
|
|
#include <cerrno>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <numbers>
|
|
#include <optional>
|
|
#include <span>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <system_error>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
// platform includes
|
|
#if defined(__linux__)
|
|
#include <fcntl.h>
|
|
#ifndef __user
|
|
#define __user
|
|
#endif
|
|
#include <linux/input.h>
|
|
#include <linux/uhid.h>
|
|
#include <linux/uinput.h>
|
|
#include <poll.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
#include <X11/extensions/XTest.h>
|
|
#include <X11/keysym.h>
|
|
#include <X11/Xlib.h>
|
|
#include <X11/Xutil.h>
|
|
#endif
|
|
#endif
|
|
|
|
// lib includes
|
|
#if defined(__linux__)
|
|
#include <libevdev/libevdev-uinput.h>
|
|
#include <libevdev/libevdev.h>
|
|
#endif
|
|
|
|
// local includes
|
|
#include "fixtures/linux_backend_test_hooks.hpp"
|
|
|
|
#if defined(__linux__)
|
|
namespace lvh::detail::test {
|
|
namespace {
|
|
|
|
struct FakeLibevdevDevice {
|
|
std::string name;
|
|
std::uint16_t bustype = 0;
|
|
std::uint16_t vendor = 0;
|
|
std::uint16_t product = 0;
|
|
std::uint16_t version = 0;
|
|
std::vector<std::uint32_t> event_types;
|
|
std::vector<LinuxLibevdevEventCode> event_codes;
|
|
std::vector<std::uint32_t> properties;
|
|
};
|
|
|
|
struct FakeLibevdevUinput {
|
|
FakeLibevdevDevice device;
|
|
};
|
|
|
|
template<typename Target, typename Source>
|
|
Target opaque_test_handle(Source *source) noexcept {
|
|
static_assert(sizeof(Target) == sizeof(source), "opaque test handles must preserve pointer size");
|
|
return std::bit_cast<Target>(source);
|
|
}
|
|
|
|
libevdev *libevdev_handle(FakeLibevdevDevice *device) noexcept {
|
|
return opaque_test_handle<libevdev *>(device);
|
|
}
|
|
|
|
FakeLibevdevDevice *fake_libevdev_device(libevdev *device) noexcept {
|
|
return opaque_test_handle<FakeLibevdevDevice *>(device);
|
|
}
|
|
|
|
const FakeLibevdevDevice *fake_libevdev_device(const libevdev *device) noexcept {
|
|
return opaque_test_handle<const FakeLibevdevDevice *>(device);
|
|
}
|
|
|
|
libevdev_uinput *libevdev_uinput_handle(FakeLibevdevUinput *device) noexcept {
|
|
return opaque_test_handle<libevdev_uinput *>(device);
|
|
}
|
|
|
|
FakeLibevdevUinput *fake_libevdev_uinput(libevdev_uinput *device) noexcept {
|
|
return opaque_test_handle<FakeLibevdevUinput *>(device);
|
|
}
|
|
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
Display *fake_display_handle() noexcept {
|
|
static std::byte display_handle {};
|
|
return opaque_test_handle<Display *>(&display_handle);
|
|
}
|
|
#endif
|
|
|
|
struct LinuxTestSyscalls { // NOSONAR(cpp:S1820): Test-only fake syscall state keeps related Linux hook controls visible at call sites.
|
|
bool override_access = false;
|
|
int access_result = 0;
|
|
bool override_open = false;
|
|
int open_result = 100000;
|
|
int last_open_flags = 0;
|
|
bool override_write = false;
|
|
std::atomic_int write_call_count = 0;
|
|
int fail_write_call = -1;
|
|
int short_write_call = -1;
|
|
std::size_t short_write_size = 1;
|
|
bool capture_input_events = false;
|
|
std::vector<LinuxInputEventRecord> written_input_events;
|
|
bool override_ioctl = false;
|
|
std::atomic_int ioctl_call_count = 0;
|
|
int fail_ioctl_call = -1;
|
|
bool override_poll = false;
|
|
std::atomic_int poll_call_count = 0;
|
|
std::vector<int> poll_results;
|
|
std::vector<short> poll_revents;
|
|
std::vector<int> poll_errors;
|
|
bool override_read = false;
|
|
std::atomic_int read_call_count = 0;
|
|
std::vector<std::ptrdiff_t> read_results;
|
|
std::vector<int> read_errors;
|
|
uhid_event read_event {};
|
|
std::vector<uhid_event> read_events;
|
|
std::vector<input_event> read_input_events;
|
|
ff_effect uploaded_ff_effect {};
|
|
std::vector<ff_effect> uploaded_ff_effects;
|
|
std::atomic_size_t uploaded_ff_effect_index = 0;
|
|
bool provide_uploaded_ff_effect = false;
|
|
bool override_xtest_query = false;
|
|
bool xtest_query_result = true;
|
|
bool override_x_keycode = false;
|
|
std::atomic_int x_keycode_call_count = 0;
|
|
int fail_x_keycode_call = -1;
|
|
bool override_libevdev = false;
|
|
bool libevdev_new_returns_null = false;
|
|
bool fail_libevdev_event_type = false;
|
|
bool fail_libevdev_event_code = false;
|
|
bool fail_libevdev_property = false;
|
|
bool fail_libevdev_create = false;
|
|
std::vector<std::unique_ptr<FakeLibevdevDevice>> owned_libevdev_devices;
|
|
std::vector<std::unique_ptr<FakeLibevdevUinput>> owned_libevdev_uinput_devices;
|
|
std::vector<FakeLibevdevDevice> libevdev_devices;
|
|
std::size_t libevdev_destroy_count = 0;
|
|
};
|
|
|
|
LinuxTestSyscalls *&active_test_syscalls() {
|
|
static LinuxTestSyscalls *syscalls = nullptr;
|
|
return syscalls;
|
|
}
|
|
|
|
class ScopedLinuxTestSyscalls {
|
|
public:
|
|
explicit ScopedLinuxTestSyscalls(LinuxTestSyscalls &syscalls):
|
|
previous_ {active_test_syscalls()} {
|
|
active_test_syscalls() = &syscalls;
|
|
}
|
|
|
|
ScopedLinuxTestSyscalls(const ScopedLinuxTestSyscalls &) = delete;
|
|
ScopedLinuxTestSyscalls &operator=(const ScopedLinuxTestSyscalls &) = delete;
|
|
ScopedLinuxTestSyscalls(ScopedLinuxTestSyscalls &&) noexcept = delete;
|
|
ScopedLinuxTestSyscalls &operator=(ScopedLinuxTestSyscalls &&) noexcept = delete;
|
|
|
|
~ScopedLinuxTestSyscalls() {
|
|
active_test_syscalls() = previous_;
|
|
}
|
|
|
|
private:
|
|
LinuxTestSyscalls *previous_ = nullptr;
|
|
};
|
|
|
|
} // namespace
|
|
} // namespace lvh::detail::test
|
|
|
|
namespace {
|
|
|
|
constexpr auto free_real_libevdev = &::libevdev_free;
|
|
constexpr auto destroy_real_libevdev_uinput = &::libevdev_uinput_destroy;
|
|
|
|
} // namespace
|
|
|
|
int lvh_linux_test_access(const char *path, int mode) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_access) {
|
|
if (lvh::detail::test::active_test_syscalls()->access_result < 0) {
|
|
errno = EACCES;
|
|
}
|
|
return lvh::detail::test::active_test_syscalls()->access_result;
|
|
}
|
|
return ::access(path, mode);
|
|
}
|
|
|
|
int lvh_linux_test_open(const char *path, int flags) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr) {
|
|
lvh::detail::test::active_test_syscalls()->last_open_flags = flags;
|
|
}
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_open) {
|
|
if (lvh::detail::test::active_test_syscalls()->open_result < 0) {
|
|
errno = ENOENT;
|
|
return lvh::detail::test::active_test_syscalls()->open_result;
|
|
}
|
|
const auto fd = ::open("/dev/null", O_RDWR);
|
|
if (fd < 0) {
|
|
errno = EIO;
|
|
}
|
|
return fd;
|
|
}
|
|
return ::open(path, flags);
|
|
}
|
|
|
|
std::ptrdiff_t lvh_linux_test_write(int fd, const std::byte *buffer, std::size_t size) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_write) {
|
|
const auto call_count = ++lvh::detail::test::active_test_syscalls()->write_call_count;
|
|
if (lvh::detail::test::active_test_syscalls()->fail_write_call == call_count) {
|
|
errno = EIO;
|
|
return -1;
|
|
}
|
|
if (lvh::detail::test::active_test_syscalls()->short_write_call == call_count) {
|
|
return static_cast<std::ptrdiff_t>(lvh::detail::test::active_test_syscalls()->short_write_size);
|
|
}
|
|
if (lvh::detail::test::active_test_syscalls()->capture_input_events && size == sizeof(input_event)) {
|
|
input_event event {};
|
|
std::memcpy(&event, buffer, sizeof(event));
|
|
lvh::detail::test::active_test_syscalls()->written_input_events.push_back({
|
|
.type = event.type,
|
|
.code = event.code,
|
|
.value = event.value,
|
|
});
|
|
}
|
|
return static_cast<std::ptrdiff_t>(size);
|
|
}
|
|
return static_cast<std::ptrdiff_t>(::write(fd, buffer, size));
|
|
}
|
|
|
|
int lvh_linux_test_ioctl(int fd, unsigned long request, unsigned long argument) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_ioctl) {
|
|
if (const auto call_count = ++lvh::detail::test::active_test_syscalls()->ioctl_call_count; lvh::detail::test::active_test_syscalls()->fail_ioctl_call == call_count) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
if (
|
|
request == UI_BEGIN_FF_UPLOAD &&
|
|
lvh::detail::test::active_test_syscalls()->provide_uploaded_ff_effect
|
|
) {
|
|
auto *upload = std::bit_cast<uinput_ff_upload *>(argument);
|
|
auto &syscalls = *lvh::detail::test::active_test_syscalls();
|
|
const auto upload_index = syscalls.uploaded_ff_effect_index++;
|
|
upload->effect = upload_index < syscalls.uploaded_ff_effects.size() ?
|
|
syscalls.uploaded_ff_effects[upload_index] :
|
|
syscalls.uploaded_ff_effect;
|
|
}
|
|
return 0;
|
|
}
|
|
return ::ioctl(fd, request, argument);
|
|
}
|
|
|
|
int lvh_linux_test_poll(pollfd *descriptors, nfds_t descriptor_count, int timeout) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_poll) {
|
|
const auto call_index = static_cast<std::size_t>(lvh::detail::test::active_test_syscalls()->poll_call_count++);
|
|
auto result = 0;
|
|
if (call_index < lvh::detail::test::active_test_syscalls()->poll_results.size()) {
|
|
result = lvh::detail::test::active_test_syscalls()->poll_results[call_index];
|
|
}
|
|
|
|
if (descriptor_count > 0) {
|
|
descriptors[0].revents = 0;
|
|
if (result > 0 && call_index < lvh::detail::test::active_test_syscalls()->poll_revents.size()) {
|
|
descriptors[0].revents = lvh::detail::test::active_test_syscalls()->poll_revents[call_index];
|
|
}
|
|
}
|
|
|
|
if (result < 0) {
|
|
errno = call_index < lvh::detail::test::active_test_syscalls()->poll_errors.size() ?
|
|
lvh::detail::test::active_test_syscalls()->poll_errors[call_index] :
|
|
EIO;
|
|
}
|
|
return result;
|
|
}
|
|
return ::poll(descriptors, descriptor_count, timeout);
|
|
}
|
|
|
|
std::ptrdiff_t lvh_linux_test_read(int fd, std::byte *buffer, std::size_t size) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_read) {
|
|
const auto call_index = static_cast<std::size_t>(lvh::detail::test::active_test_syscalls()->read_call_count++);
|
|
auto result = std::ptrdiff_t {0};
|
|
if (call_index < lvh::detail::test::active_test_syscalls()->read_results.size()) {
|
|
result = lvh::detail::test::active_test_syscalls()->read_results[call_index];
|
|
}
|
|
|
|
if (result < 0) {
|
|
errno = call_index < lvh::detail::test::active_test_syscalls()->read_errors.size() ?
|
|
lvh::detail::test::active_test_syscalls()->read_errors[call_index] :
|
|
EIO;
|
|
return result;
|
|
}
|
|
|
|
if (result > 0) {
|
|
if (const auto &input_events = lvh::detail::test::active_test_syscalls()->read_input_events; call_index < input_events.size()) {
|
|
const auto bytes = std::min<std::size_t>(static_cast<std::size_t>(result), std::min(size, sizeof(input_event)));
|
|
std::memcpy(buffer, &input_events[call_index], bytes);
|
|
return static_cast<std::ptrdiff_t>(bytes);
|
|
}
|
|
|
|
const auto bytes = std::min<std::size_t>(static_cast<std::size_t>(result), std::min(size, sizeof(uhid_event)));
|
|
const auto &read_events = lvh::detail::test::active_test_syscalls()->read_events;
|
|
const auto &event = call_index < read_events.size() ?
|
|
read_events[call_index] :
|
|
lvh::detail::test::active_test_syscalls()->read_event;
|
|
std::memcpy(buffer, &event, bytes);
|
|
return static_cast<std::ptrdiff_t>(bytes);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
return static_cast<std::ptrdiff_t>(::read(fd, buffer, size));
|
|
}
|
|
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
Display *lvh_linux_test_x_open_display(const char *) {
|
|
return lvh::detail::test::fake_display_handle();
|
|
}
|
|
|
|
int lvh_linux_test_x_close_display(Display *) {
|
|
return 0;
|
|
}
|
|
|
|
Bool lvh_linux_test_xtest_query_extension(Display *, int *, int *, int *, int *) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_xtest_query) {
|
|
return lvh::detail::test::active_test_syscalls()->xtest_query_result ? True : False;
|
|
}
|
|
return True;
|
|
}
|
|
|
|
KeyCode lvh_linux_test_x_keysym_to_keycode(Display *, KeySym keysym) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_x_keycode) {
|
|
const auto call_count = ++lvh::detail::test::active_test_syscalls()->x_keycode_call_count;
|
|
if (lvh::detail::test::active_test_syscalls()->fail_x_keycode_call == call_count) {
|
|
return 0;
|
|
}
|
|
}
|
|
return keysym == NoSymbol ? 0 : 1;
|
|
}
|
|
|
|
int lvh_linux_test_x_flush(Display *) {
|
|
return 0;
|
|
}
|
|
|
|
int lvh_linux_test_default_screen(Display *) {
|
|
return 0;
|
|
}
|
|
|
|
int lvh_linux_test_display_width(Display *, int) {
|
|
return 1920;
|
|
}
|
|
|
|
int lvh_linux_test_display_height(Display *, int) {
|
|
return 1080;
|
|
}
|
|
|
|
int lvh_linux_test_xtest_fake_key_event(Display *, unsigned int, Bool, unsigned long) {
|
|
return 1;
|
|
}
|
|
|
|
int lvh_linux_test_xtest_fake_button_event(Display *, unsigned int, Bool, unsigned long) {
|
|
return 1;
|
|
}
|
|
|
|
int lvh_linux_test_xtest_fake_motion_event(Display *, int, int, int, unsigned long) {
|
|
return 1;
|
|
}
|
|
|
|
int lvh_linux_test_xtest_fake_relative_motion_event(Display *, int, int, unsigned long) {
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
extern "C" libevdev *lvh_linux_test_libevdev_new() {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) {
|
|
if (lvh::detail::test::active_test_syscalls()->libevdev_new_returns_null) {
|
|
return nullptr;
|
|
}
|
|
auto fake_device = std::make_unique<lvh::detail::test::FakeLibevdevDevice>();
|
|
auto *created_device = fake_device.get();
|
|
lvh::detail::test::active_test_syscalls()->owned_libevdev_devices.push_back(std::move(fake_device));
|
|
return lvh::detail::test::libevdev_handle(created_device);
|
|
}
|
|
return ::libevdev_new();
|
|
}
|
|
|
|
extern "C" void lvh_linux_test_libevdev_free(libevdev *raw_libevdev_handle) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) {
|
|
auto *fake_device_handle = lvh::detail::test::fake_libevdev_device(raw_libevdev_handle);
|
|
static_cast<void>(std::erase_if(
|
|
lvh::detail::test::active_test_syscalls()->owned_libevdev_devices,
|
|
[fake_device_handle](const auto &owned_device) {
|
|
return owned_device.get() == fake_device_handle;
|
|
}
|
|
));
|
|
return;
|
|
}
|
|
free_real_libevdev(raw_libevdev_handle);
|
|
}
|
|
|
|
extern "C" void lvh_linux_test_libevdev_set_name(libevdev *device, const char *name) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) {
|
|
lvh::detail::test::fake_libevdev_device(device)->name = name == nullptr ? "" : name;
|
|
return;
|
|
}
|
|
::libevdev_set_name(device, name);
|
|
}
|
|
|
|
extern "C" void lvh_linux_test_libevdev_set_id_bustype(libevdev *device, int bustype) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) {
|
|
lvh::detail::test::fake_libevdev_device(device)->bustype = static_cast<std::uint16_t>(bustype);
|
|
return;
|
|
}
|
|
::libevdev_set_id_bustype(device, bustype);
|
|
}
|
|
|
|
extern "C" void lvh_linux_test_libevdev_set_id_vendor(libevdev *device, int vendor) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) {
|
|
lvh::detail::test::fake_libevdev_device(device)->vendor = static_cast<std::uint16_t>(vendor);
|
|
return;
|
|
}
|
|
::libevdev_set_id_vendor(device, vendor);
|
|
}
|
|
|
|
extern "C" void lvh_linux_test_libevdev_set_id_product(libevdev *device, int product) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) {
|
|
lvh::detail::test::fake_libevdev_device(device)->product = static_cast<std::uint16_t>(product);
|
|
return;
|
|
}
|
|
::libevdev_set_id_product(device, product);
|
|
}
|
|
|
|
extern "C" void lvh_linux_test_libevdev_set_id_version(libevdev *device, int version) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) {
|
|
lvh::detail::test::fake_libevdev_device(device)->version = static_cast<std::uint16_t>(version);
|
|
return;
|
|
}
|
|
::libevdev_set_id_version(device, version);
|
|
}
|
|
|
|
extern "C" int lvh_linux_test_libevdev_enable_event_type(libevdev *device, unsigned int type) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) {
|
|
if (lvh::detail::test::active_test_syscalls()->fail_libevdev_event_type) {
|
|
return -EIO;
|
|
}
|
|
lvh::detail::test::fake_libevdev_device(device)->event_types.push_back(type);
|
|
return 0;
|
|
}
|
|
return ::libevdev_enable_event_type(device, type);
|
|
}
|
|
|
|
extern "C" int lvh_linux_test_libevdev_enable_event_code(
|
|
libevdev *device,
|
|
unsigned int type,
|
|
unsigned int code,
|
|
const void *data
|
|
) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) {
|
|
if (lvh::detail::test::active_test_syscalls()->fail_libevdev_event_code) {
|
|
return -EIO;
|
|
}
|
|
|
|
lvh::detail::test::LinuxLibevdevEventCode event_code {
|
|
.type = type,
|
|
.code = code,
|
|
};
|
|
if (type == EV_ABS && data != nullptr) {
|
|
const auto *absinfo = static_cast<const input_absinfo *>(data);
|
|
event_code.has_absinfo = true;
|
|
event_code.minimum = absinfo->minimum;
|
|
event_code.maximum = absinfo->maximum;
|
|
event_code.fuzz = absinfo->fuzz;
|
|
event_code.flat = absinfo->flat;
|
|
event_code.resolution = absinfo->resolution;
|
|
}
|
|
lvh::detail::test::fake_libevdev_device(device)->event_codes.push_back(event_code);
|
|
return 0;
|
|
}
|
|
return ::libevdev_enable_event_code(device, type, code, data);
|
|
}
|
|
|
|
extern "C" int lvh_linux_test_libevdev_enable_property(libevdev *device, unsigned int property) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) {
|
|
if (lvh::detail::test::active_test_syscalls()->fail_libevdev_property) {
|
|
return -EIO;
|
|
}
|
|
lvh::detail::test::fake_libevdev_device(device)->properties.push_back(property);
|
|
return 0;
|
|
}
|
|
return ::libevdev_enable_property(device, property);
|
|
}
|
|
|
|
extern "C" int lvh_linux_test_libevdev_uinput_create_from_device(
|
|
const libevdev *device,
|
|
int uinput_fd,
|
|
libevdev_uinput **uinput_device
|
|
) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) {
|
|
if (lvh::detail::test::active_test_syscalls()->fail_libevdev_create || uinput_fd < 0) {
|
|
return -EIO;
|
|
}
|
|
|
|
const auto &source_device = *lvh::detail::test::fake_libevdev_device(device);
|
|
lvh::detail::test::active_test_syscalls()->libevdev_devices.push_back(source_device);
|
|
auto owned_uinput_device =
|
|
std::make_unique<lvh::detail::test::FakeLibevdevUinput>(lvh::detail::test::FakeLibevdevUinput {source_device});
|
|
auto *created_uinput_device = owned_uinput_device.get();
|
|
lvh::detail::test::active_test_syscalls()->owned_libevdev_uinput_devices.push_back(std::move(owned_uinput_device));
|
|
*uinput_device = lvh::detail::test::libevdev_uinput_handle(created_uinput_device);
|
|
return 0;
|
|
}
|
|
return ::libevdev_uinput_create_from_device(device, uinput_fd, uinput_device);
|
|
}
|
|
|
|
extern "C" void lvh_linux_test_libevdev_uinput_destroy(libevdev_uinput *raw_uinput_handle) {
|
|
if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) {
|
|
++lvh::detail::test::active_test_syscalls()->libevdev_destroy_count;
|
|
auto *fake_uinput_handle = lvh::detail::test::fake_libevdev_uinput(raw_uinput_handle);
|
|
static_cast<void>(std::erase_if(
|
|
lvh::detail::test::active_test_syscalls()->owned_libevdev_uinput_devices,
|
|
[fake_uinput_handle](const auto &owned_device) {
|
|
return owned_device.get() == fake_uinput_handle;
|
|
}
|
|
));
|
|
return;
|
|
}
|
|
destroy_real_libevdev_uinput(raw_uinput_handle);
|
|
}
|
|
|
|
#define access lvh_linux_test_access
|
|
#define ioctl lvh_linux_test_ioctl
|
|
#define open lvh_linux_test_open
|
|
#define poll lvh_linux_test_poll
|
|
#define read lvh_linux_test_read
|
|
#define write lvh_linux_test_write
|
|
#define libevdev_enable_event_code lvh_linux_test_libevdev_enable_event_code
|
|
#define libevdev_enable_event_type lvh_linux_test_libevdev_enable_event_type
|
|
#define libevdev_enable_property lvh_linux_test_libevdev_enable_property
|
|
#define libevdev_free lvh_linux_test_libevdev_free
|
|
#define libevdev_new lvh_linux_test_libevdev_new
|
|
#define libevdev_set_id_bustype lvh_linux_test_libevdev_set_id_bustype
|
|
#define libevdev_set_id_product lvh_linux_test_libevdev_set_id_product
|
|
#define libevdev_set_id_vendor lvh_linux_test_libevdev_set_id_vendor
|
|
#define libevdev_set_id_version lvh_linux_test_libevdev_set_id_version
|
|
#define libevdev_set_name lvh_linux_test_libevdev_set_name
|
|
#define libevdev_uinput_create_from_device lvh_linux_test_libevdev_uinput_create_from_device
|
|
#define libevdev_uinput_destroy lvh_linux_test_libevdev_uinput_destroy
|
|
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
#if defined(DefaultScreen)
|
|
#undef DefaultScreen // NOSONAR(cpp:S959): X11 exposes this as a macro; the test hook replaces it while including the backend.
|
|
#endif
|
|
#if defined(DisplayHeight)
|
|
#undef DisplayHeight // NOSONAR(cpp:S959): X11 exposes this as a macro; the test hook replaces it while including the backend.
|
|
#endif
|
|
#if defined(DisplayWidth)
|
|
#undef DisplayWidth // NOSONAR(cpp:S959): X11 exposes this as a macro; the test hook replaces it while including the backend.
|
|
#endif
|
|
|
|
#define DefaultScreen lvh_linux_test_default_screen
|
|
#define DisplayHeight lvh_linux_test_display_height
|
|
#define DisplayWidth lvh_linux_test_display_width
|
|
#define XCloseDisplay lvh_linux_test_x_close_display
|
|
#define XFlush lvh_linux_test_x_flush
|
|
#define XKeysymToKeycode lvh_linux_test_x_keysym_to_keycode
|
|
#define XOpenDisplay lvh_linux_test_x_open_display
|
|
#define XTestFakeButtonEvent lvh_linux_test_xtest_fake_button_event
|
|
#define XTestFakeKeyEvent lvh_linux_test_xtest_fake_key_event
|
|
#define XTestFakeMotionEvent lvh_linux_test_xtest_fake_motion_event
|
|
#define XTestFakeRelativeMotionEvent lvh_linux_test_xtest_fake_relative_motion_event
|
|
#define XTestQueryExtension lvh_linux_test_xtest_query_extension
|
|
#endif
|
|
|
|
#define create_platform_backend create_platform_backend_for_linux_backend_test_hooks
|
|
#include "../../src/platform/linux/uhid_backend.cpp"
|
|
#undef create_platform_backend
|
|
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
#undef XTestQueryExtension
|
|
#undef XTestFakeRelativeMotionEvent
|
|
#undef XTestFakeMotionEvent
|
|
#undef XTestFakeKeyEvent
|
|
#undef XTestFakeButtonEvent
|
|
#undef XOpenDisplay
|
|
#undef XKeysymToKeycode
|
|
#undef XFlush
|
|
#undef XCloseDisplay
|
|
#undef DisplayWidth
|
|
#undef DisplayHeight
|
|
#undef DefaultScreen
|
|
#endif
|
|
|
|
#undef write
|
|
#undef read
|
|
#undef poll
|
|
#undef open
|
|
#undef ioctl
|
|
#undef access
|
|
#undef libevdev_uinput_destroy
|
|
#undef libevdev_uinput_create_from_device
|
|
#undef libevdev_set_name
|
|
#undef libevdev_set_id_version
|
|
#undef libevdev_set_id_vendor
|
|
#undef libevdev_set_id_product
|
|
#undef libevdev_set_id_bustype
|
|
#undef libevdev_new
|
|
#undef libevdev_free
|
|
#undef libevdev_enable_property
|
|
#undef libevdev_enable_event_type
|
|
#undef libevdev_enable_event_code
|
|
|
|
namespace lvh::detail::test {
|
|
namespace {
|
|
|
|
constexpr auto fake_fd = 100000;
|
|
|
|
int open_test_fd() {
|
|
return ::open("/dev/null", O_RDWR);
|
|
}
|
|
|
|
std::vector<LinuxInputEventRecord> read_input_events_until_eof(int fd) {
|
|
std::vector<LinuxInputEventRecord> records;
|
|
input_event event {};
|
|
while (::read(fd, &event, sizeof(event)) == sizeof(event)) {
|
|
records.push_back({
|
|
.type = event.type,
|
|
.code = event.code,
|
|
.value = event.value,
|
|
});
|
|
}
|
|
return records;
|
|
}
|
|
|
|
bool write_uhid_event(int fd, const uhid_event &event) {
|
|
const auto data = std::as_bytes(std::span {&event, 1U});
|
|
std::size_t written = 0;
|
|
while (written < data.size()) {
|
|
const auto result = ::write(fd, data.data() + written, data.size() - written);
|
|
if (result <= 0) {
|
|
return false;
|
|
}
|
|
written += static_cast<std::size_t>(result);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool read_uhid_event(int fd, uhid_event &event) {
|
|
pollfd descriptor {};
|
|
descriptor.fd = fd;
|
|
descriptor.events = POLLIN;
|
|
if (const auto poll_result = ::poll(&descriptor, 1, 1000); poll_result <= 0 || (descriptor.revents & POLLIN) == 0) {
|
|
return false;
|
|
}
|
|
|
|
const auto data = std::as_writable_bytes(std::span {&event, 1U});
|
|
std::size_t read_size = 0;
|
|
while (read_size < data.size()) {
|
|
const auto result = ::read(fd, data.data() + read_size, data.size() - read_size);
|
|
if (result <= 0) {
|
|
return false;
|
|
}
|
|
read_size += static_cast<std::size_t>(result);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
template<class Predicate>
|
|
bool read_uhid_event_matching(int fd, Predicate matches, uhid_event &event) {
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds {1};
|
|
while (std::chrono::steady_clock::now() < deadline) {
|
|
if (!read_uhid_event(fd, event)) {
|
|
return false;
|
|
}
|
|
if (matches(event)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool read_uhid_event_type(int fd, unsigned int event_type, uhid_event &event) {
|
|
return read_uhid_event_matching(fd, [event_type](const uhid_event &candidate) {
|
|
return candidate.type == event_type;
|
|
},
|
|
event);
|
|
}
|
|
|
|
std::vector<std::vector<std::uint8_t>> read_switch_input_reports(int fd, const std::vector<std::uint8_t> &expected) {
|
|
std::vector<std::vector<std::uint8_t>> reports;
|
|
uhid_event event {};
|
|
for (auto index = 0; index < 2; ++index) {
|
|
const auto received = read_uhid_event_matching(fd, [&expected](const uhid_event &candidate) {
|
|
return candidate.type == UHID_INPUT2 && candidate.u.input2.size == expected.size() &&
|
|
candidate.u.input2.data[0] == expected[0] &&
|
|
std::equal(expected.begin() + 2, expected.end(), candidate.u.input2.data + 2U);
|
|
},
|
|
event);
|
|
if (!received) {
|
|
break;
|
|
}
|
|
const auto *data = event.u.input2.data;
|
|
reports.emplace_back(data, data + event.u.input2.size);
|
|
}
|
|
return reports;
|
|
}
|
|
|
|
OperationStatus create_started_uhid_gamepad(
|
|
UhidGamepad &gamepad,
|
|
DeviceId id,
|
|
const CreateGamepadOptions &options,
|
|
int peer_fd,
|
|
uhid_event &create_event,
|
|
bool &saw_create,
|
|
bool &waited_for_start
|
|
) {
|
|
auto create_status = OperationStatus::failure(ErrorCode::backend_failure, "UHID create thread did not run");
|
|
std::atomic_bool create_returned = false;
|
|
std::jthread create_thread {[&]() {
|
|
create_status = gamepad.create(id, options);
|
|
create_returned = true;
|
|
}};
|
|
|
|
saw_create = read_uhid_event_type(peer_fd, UHID_CREATE2, create_event);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds {20});
|
|
waited_for_start = !create_returned;
|
|
|
|
uhid_event start_event {};
|
|
start_event.type = UHID_START;
|
|
static_cast<void>(write_uhid_event(peer_fd, start_event));
|
|
create_thread.join();
|
|
return create_status;
|
|
}
|
|
|
|
uhid_event create_started_profile_uhid_gamepad(
|
|
UhidGamepad &gamepad,
|
|
DeviceId id,
|
|
const CreateGamepadOptions &options,
|
|
int peer_fd,
|
|
std::uint16_t expected_bus,
|
|
LinuxUhidRoundTripResult &result
|
|
) {
|
|
uhid_event event {};
|
|
result.create_status = create_started_uhid_gamepad(
|
|
gamepad,
|
|
id,
|
|
options,
|
|
peer_fd,
|
|
event,
|
|
result.creation.saw_create,
|
|
result.creation.waited_for_start
|
|
);
|
|
if (result.creation.saw_create) {
|
|
const auto transport_profile = uhid_transport_profile(options.profile, options.metadata.has_battery);
|
|
result.creation.saw_create = event.u.create2.vendor == uhid_vendor_id(transport_profile) &&
|
|
event.u.create2.product == transport_profile.product_id &&
|
|
event.u.create2.version == transport_profile.version &&
|
|
event.u.create2.bus == expected_bus;
|
|
result.creation.bus = event.u.create2.bus;
|
|
result.creation.vendor_id = event.u.create2.vendor;
|
|
result.creation.product_id = event.u.create2.product;
|
|
result.creation.version = event.u.create2.version;
|
|
result.creation.name = reinterpret_cast<const char *>(event.u.create2.name);
|
|
result.creation.physical_id = reinterpret_cast<const char *>(event.u.create2.phys);
|
|
result.creation.unique_id = reinterpret_cast<const char *>(event.u.create2.uniq);
|
|
}
|
|
return event;
|
|
}
|
|
|
|
std::uint32_t read_u32_le(const std::uint8_t *buffer) {
|
|
return static_cast<std::uint32_t>(buffer[0]) |
|
|
(static_cast<std::uint32_t>(buffer[1]) << 8U) |
|
|
(static_cast<std::uint32_t>(buffer[2]) << 16U) |
|
|
(static_cast<std::uint32_t>(buffer[3]) << 24U);
|
|
}
|
|
|
|
void enable_fake_device_syscalls(LinuxTestSyscalls &syscalls) {
|
|
syscalls.override_access = true;
|
|
syscalls.override_open = true;
|
|
syscalls.override_write = true;
|
|
syscalls.override_ioctl = true;
|
|
syscalls.override_libevdev = true;
|
|
}
|
|
|
|
bool wait_for_poll_calls(const LinuxTestSyscalls &syscalls, int expected_calls) {
|
|
using namespace std::chrono_literals;
|
|
|
|
for (auto attempt = 0; attempt < 100; ++attempt) {
|
|
if (syscalls.poll_call_count.load() >= expected_calls) {
|
|
return true;
|
|
}
|
|
std::this_thread::sleep_for(1ms);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
OperationStatus run_fake_uhid_read_loop(LinuxTestSyscalls &syscalls, int expected_poll_calls) {
|
|
syscalls.override_write = true;
|
|
syscalls.override_poll = true;
|
|
syscalls.override_read = true;
|
|
syscalls.poll_results.insert(syscalls.poll_results.begin(), 1);
|
|
syscalls.poll_revents.insert(syscalls.poll_revents.begin(), POLLIN);
|
|
syscalls.poll_errors.insert(syscalls.poll_errors.begin(), 0);
|
|
syscalls.read_results.insert(syscalls.read_results.begin(), static_cast<std::ptrdiff_t>(sizeof(uhid_event)));
|
|
syscalls.read_errors.insert(syscalls.read_errors.begin(), 0);
|
|
uhid_event start_event {};
|
|
start_event.type = UHID_START;
|
|
syscalls.read_events.insert(syscalls.read_events.begin(), start_event);
|
|
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
CreateGamepadOptions options;
|
|
options.profile = profiles::generic_gamepad();
|
|
|
|
const auto fd = open_test_fd();
|
|
if (fd < 0) {
|
|
return system_error_status(ErrorCode::backend_failure, "failed to open test file descriptor", errno);
|
|
}
|
|
|
|
UhidGamepad gamepad {fd};
|
|
if (const auto status = gamepad.create(1, options); !status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
const auto saw_expected_polls = wait_for_poll_calls(syscalls, expected_poll_calls + 1);
|
|
const auto close_status = gamepad.close();
|
|
if (!saw_expected_polls) {
|
|
return OperationStatus::failure(ErrorCode::backend_failure, "fake UHID read loop did not consume the scripted poll calls");
|
|
}
|
|
return close_status;
|
|
}
|
|
|
|
DeviceProfile profile_for_uinput_device_type(DeviceType device_type) {
|
|
switch (device_type) {
|
|
case DeviceType::keyboard:
|
|
return profiles::keyboard();
|
|
case DeviceType::mouse:
|
|
return profiles::mouse();
|
|
case DeviceType::touchscreen:
|
|
return profiles::touchscreen();
|
|
case DeviceType::trackpad:
|
|
return profiles::trackpad();
|
|
case DeviceType::pen_tablet:
|
|
return profiles::pen_tablet();
|
|
case DeviceType::gamepad:
|
|
return profiles::xbox_series();
|
|
}
|
|
|
|
return profiles::mouse();
|
|
}
|
|
|
|
OperationStatus create_uinput_device_by_type(
|
|
int fd,
|
|
DeviceType device_type,
|
|
std::optional<GamepadProfileKind> gamepad_kind = std::nullopt,
|
|
UinputMouseDeviceKind mouse_kind = UinputMouseDeviceKind::relative
|
|
) {
|
|
switch (device_type) {
|
|
case DeviceType::keyboard:
|
|
{
|
|
CreateKeyboardOptions options;
|
|
options.profile = profile_for_uinput_device_type(device_type);
|
|
UinputKeyboard keyboard {fd};
|
|
return keyboard.create(1, options);
|
|
}
|
|
case DeviceType::mouse:
|
|
{
|
|
CreateMouseOptions options;
|
|
options.profile = profile_for_uinput_device_type(device_type);
|
|
UinputMouseDevice mouse {fd};
|
|
return mouse.create(1, options.profile, mouse_kind);
|
|
}
|
|
case DeviceType::touchscreen:
|
|
{
|
|
CreateTouchscreenOptions options;
|
|
options.profile = profile_for_uinput_device_type(device_type);
|
|
UinputTouchscreen touchscreen {fd};
|
|
return touchscreen.create(1, options);
|
|
}
|
|
case DeviceType::trackpad:
|
|
{
|
|
CreateTrackpadOptions options;
|
|
options.profile = profile_for_uinput_device_type(device_type);
|
|
UinputTrackpad trackpad {fd};
|
|
return trackpad.create(1, options);
|
|
}
|
|
case DeviceType::pen_tablet:
|
|
{
|
|
CreatePenTabletOptions options;
|
|
options.profile = profile_for_uinput_device_type(device_type);
|
|
UinputPenTablet pen_tablet {fd};
|
|
return pen_tablet.create(1, options);
|
|
}
|
|
case DeviceType::gamepad:
|
|
{
|
|
CreateGamepadOptions options;
|
|
options.profile = gamepad_kind ? *profiles::gamepad_profile(*gamepad_kind) : profile_for_uinput_device_type(device_type);
|
|
UinputGamepad gamepad {fd, options.profile.gamepad_kind};
|
|
return gamepad.create(1, options);
|
|
}
|
|
}
|
|
|
|
return OperationStatus::failure(ErrorCode::unsupported_profile, "unsupported fake uinput device type");
|
|
}
|
|
|
|
template<typename ConfigureFailure>
|
|
LinuxLibevdevCreationResult create_fake_libevdev_device(
|
|
DeviceType device_type,
|
|
ConfigureFailure configure_failure,
|
|
std::optional<GamepadProfileKind> gamepad_kind = std::nullopt,
|
|
UinputMouseDeviceKind mouse_kind = UinputMouseDeviceKind::relative
|
|
) {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_libevdev = true;
|
|
configure_failure(syscalls);
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxLibevdevCreationResult result;
|
|
const auto fd = open_test_fd();
|
|
if (fd < 0) {
|
|
result.status = system_error_status(ErrorCode::backend_failure, "failed to open test file descriptor", errno);
|
|
return result;
|
|
}
|
|
|
|
result.status = create_uinput_device_by_type(fd, device_type, gamepad_kind, mouse_kind);
|
|
if (!syscalls.libevdev_devices.empty()) {
|
|
const auto &device = syscalls.libevdev_devices.back();
|
|
result.name = device.name;
|
|
result.bustype = device.bustype;
|
|
result.vendor = device.vendor;
|
|
result.product = device.product;
|
|
result.version = device.version;
|
|
result.event_types = device.event_types;
|
|
result.event_codes = device.event_codes;
|
|
result.properties = device.properties;
|
|
}
|
|
result.destroy_count = syscalls.libevdev_destroy_count;
|
|
if (result.status.ok()) {
|
|
result.close_status = OperationStatus::success();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void keep_fake_libevdev_successful(const LinuxTestSyscalls &syscalls) {
|
|
static_cast<void>(syscalls);
|
|
}
|
|
|
|
LinuxLibevdevCreationResult create_fake_libevdev_device(DeviceType device_type) {
|
|
return create_fake_libevdev_device(device_type, keep_fake_libevdev_successful);
|
|
}
|
|
|
|
LinuxLibevdevCreationResult create_fake_libevdev_gamepad(GamepadProfileKind kind) {
|
|
return create_fake_libevdev_device(DeviceType::gamepad, keep_fake_libevdev_successful, kind);
|
|
}
|
|
|
|
template<typename TouchDevice>
|
|
LinuxInputSubmissionResult touch_contact_reuse_pipe() {
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::pipe(descriptors.data()) != 0) {
|
|
return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}};
|
|
}
|
|
|
|
TouchDevice device {descriptors[1]};
|
|
auto status = device.place_contact({.id = 0, .x = 0.1F, .y = 0.1F, .pressure = 0.5F});
|
|
if (status.ok()) {
|
|
status = device.place_contact({.id = 1, .x = 0.2F, .y = 0.2F, .pressure = 0.5F});
|
|
}
|
|
if (status.ok()) {
|
|
status = device.release_contact(0, PointerTransition::release);
|
|
}
|
|
if (status.ok()) {
|
|
status = device.place_contact({.id = 0, .x = 0.3F, .y = 0.3F, .pressure = 0.5F});
|
|
}
|
|
if (status.ok()) {
|
|
status = device.place_contact({.id = 1, .x = 0.25F, .y = 0.25F, .pressure = 0.5F});
|
|
}
|
|
static_cast<void>(device.close());
|
|
auto records = read_input_events_until_eof(descriptors[0]);
|
|
static_cast<void>(::close(descriptors[0]));
|
|
return {std::move(status), std::move(records)};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::string linux_copy_string_char_buffer(const std::string &source) {
|
|
std::array<char, 5> destination {};
|
|
copy_string(destination, source);
|
|
return destination.data();
|
|
}
|
|
|
|
int linux_key_code(KeyboardKeyCode key_code) {
|
|
return key_code_to_linux(key_code);
|
|
}
|
|
|
|
int linux_mouse_button(MouseButton button) {
|
|
return mouse_button_to_linux(button);
|
|
}
|
|
|
|
std::uint16_t linux_uhid_bus(BusType bus_type) {
|
|
return to_uhid_bus(bus_type);
|
|
}
|
|
|
|
std::uint16_t linux_gamepad_uhid_bus(GamepadProfileKind kind) {
|
|
const auto profile = profiles::gamepad_profile(kind);
|
|
return profile ? to_uhid_bus(*profile) : to_uhid_bus(BusType::unknown);
|
|
}
|
|
|
|
std::uint16_t linux_uinput_bus(BusType bus_type) {
|
|
return to_uinput_bus(bus_type);
|
|
}
|
|
|
|
int linux_absolute_axis(std::int32_t value, std::int32_t limit) {
|
|
return scale_absolute_axis(value, limit);
|
|
}
|
|
|
|
int linux_absolute_axis_to_viewport(
|
|
float value,
|
|
std::int32_t source_dimension,
|
|
std::int32_t viewport_offset,
|
|
std::int32_t viewport_dimension,
|
|
std::int32_t desktop_offset,
|
|
std::int32_t desktop_dimension
|
|
) {
|
|
return scale_absolute_axis_to_viewport(
|
|
value,
|
|
source_dimension,
|
|
viewport_offset,
|
|
viewport_dimension,
|
|
desktop_offset,
|
|
desktop_dimension
|
|
);
|
|
}
|
|
|
|
std::vector<std::uint32_t> linux_decode_utf8(const std::string &text) {
|
|
return decode_utf8(text);
|
|
}
|
|
|
|
std::string linux_uppercase_hex(std::uint32_t codepoint) {
|
|
return uppercase_hex(codepoint);
|
|
}
|
|
|
|
KeyboardKeyCode linux_hex_digit_key_code(char digit) {
|
|
return hex_digit_key_code(digit);
|
|
}
|
|
|
|
int linux_legacy_scroll_steps(std::int32_t distance) {
|
|
return legacy_scroll_steps(distance);
|
|
}
|
|
|
|
int linux_pen_tool(PenToolType tool) {
|
|
return pen_tool_to_linux(tool);
|
|
}
|
|
|
|
int linux_pen_button(PenButton button) {
|
|
return pen_button_to_linux(button);
|
|
}
|
|
|
|
std::string linux_dualsense_mac_address(const std::string &stable_id, DeviceId id) {
|
|
return format_mac_address(parse_mac_address(stable_id).value_or(generated_mac_address(id)));
|
|
}
|
|
|
|
bool linux_first_line_matches(std::string_view path, std::string_view expected) {
|
|
const auto line = read_first_line(std::filesystem::path {std::string {path}});
|
|
return line && *line == expected;
|
|
}
|
|
|
|
bool linux_first_line_missing(std::string_view path) {
|
|
return !read_first_line(std::filesystem::path {std::string {path}});
|
|
}
|
|
|
|
bool linux_hidraw_name_matches(std::string_view path, std::string_view name) {
|
|
return hidraw_name_matches(std::filesystem::path {std::string {path}}, name);
|
|
}
|
|
|
|
std::vector<DeviceNode> linux_discover_hidraw_nodes_by_metadata(
|
|
std::string_view name,
|
|
std::string_view physical_id,
|
|
std::string_view unique_id,
|
|
const std::string &hidraw_root
|
|
) {
|
|
return discover_hidraw_nodes_by_metadata(name, physical_id, unique_id, hidraw_root);
|
|
}
|
|
|
|
std::vector<DeviceNode> linux_discover_nodes_by_name(const std::string &name) {
|
|
return discover_input_nodes_by_name(name);
|
|
}
|
|
|
|
std::vector<DeviceNode> linux_discover_nodes_by_name(
|
|
const std::string &name,
|
|
const std::string &input_root,
|
|
const std::string &hidraw_root
|
|
) {
|
|
return discover_input_nodes_by_name(name, input_root, hidraw_root);
|
|
}
|
|
|
|
std::size_t linux_empty_device_nodes_count() {
|
|
UhidGamepad gamepad {-1};
|
|
UinputKeyboard keyboard {-1};
|
|
UinputMouse mouse {-1, -1};
|
|
UinputTouchscreen touchscreen {-1};
|
|
UinputTrackpad trackpad {-1};
|
|
UinputPenTablet pen_tablet {-1};
|
|
|
|
return gamepad.device_nodes().size() + keyboard.device_nodes().size() + mouse.device_nodes().size() +
|
|
touchscreen.device_nodes().size() + trackpad.device_nodes().size() + pen_tablet.device_nodes().size();
|
|
}
|
|
|
|
std::size_t linux_uhid_descriptor_limit() {
|
|
uhid_event event {};
|
|
return sizeof(event.u.create2.rd_data);
|
|
}
|
|
|
|
std::size_t linux_uhid_input_limit() {
|
|
uhid_event event {};
|
|
return sizeof(event.u.input2.data);
|
|
}
|
|
|
|
OperationStatus linux_uhid_create_with_descriptor_size(std::size_t descriptor_size) {
|
|
auto profile = profiles::generic_gamepad();
|
|
profile.report_descriptor.assign(descriptor_size, 0);
|
|
|
|
CreateGamepadOptions options;
|
|
options.profile = std::move(profile);
|
|
|
|
UhidGamepad gamepad {-1};
|
|
return gamepad.create(1, options);
|
|
}
|
|
|
|
OperationStatus linux_uhid_submit_report_size(std::size_t report_size) {
|
|
UhidGamepad gamepad {-1};
|
|
return gamepad.submit({}, std::vector<std::uint8_t>(report_size, 0));
|
|
}
|
|
|
|
OperationStatus linux_uhid_submit_after_close() {
|
|
UhidGamepad gamepad {-1};
|
|
static_cast<void>(gamepad.close());
|
|
return gamepad.submit({}, {0});
|
|
}
|
|
|
|
OperationStatus linux_uinput_keyboard_create_invalid_fd() {
|
|
CreateKeyboardOptions options;
|
|
options.profile = profiles::keyboard();
|
|
|
|
UinputKeyboard keyboard {-1};
|
|
return keyboard.create(1, options);
|
|
}
|
|
|
|
OperationStatus linux_uinput_keyboard_submit_invalid_fd(const KeyboardEvent &event) {
|
|
UinputKeyboard keyboard {-1};
|
|
return keyboard.submit(event);
|
|
}
|
|
|
|
OperationStatus linux_uinput_keyboard_type_text_invalid_fd(const std::string &text) {
|
|
UinputKeyboard keyboard {-1};
|
|
return keyboard.type_text({.text = text});
|
|
}
|
|
|
|
OperationStatus linux_uinput_keyboard_submit_after_close() {
|
|
UinputKeyboard keyboard {-1};
|
|
static_cast<void>(keyboard.close());
|
|
return keyboard.submit({.key_code = 0x41, .pressed = true});
|
|
}
|
|
|
|
LinuxInputSubmissionResult linux_uinput_keyboard_submit_pipe(const KeyboardEvent &event) {
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::pipe(descriptors.data()) != 0) {
|
|
return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}};
|
|
}
|
|
|
|
UinputKeyboard keyboard {descriptors[1]};
|
|
auto status = keyboard.submit(event);
|
|
static_cast<void>(keyboard.close());
|
|
auto records = read_input_events_until_eof(descriptors[0]);
|
|
static_cast<void>(::close(descriptors[0]));
|
|
return {std::move(status), std::move(records)};
|
|
}
|
|
|
|
LinuxInputSubmissionResult linux_uinput_gamepad_submit_pipe(GamepadProfileKind kind, const GamepadState &state) {
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::pipe(descriptors.data()) != 0) {
|
|
return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}};
|
|
}
|
|
|
|
const auto profile = profiles::gamepad_profile(kind);
|
|
if (!profile) {
|
|
static_cast<void>(::close(descriptors[0]));
|
|
static_cast<void>(::close(descriptors[1]));
|
|
return {OperationStatus::failure(ErrorCode::unsupported_profile, "unknown gamepad profile"), {}};
|
|
}
|
|
|
|
UinputGamepad gamepad {descriptors[1], kind};
|
|
auto status = gamepad.submit(state, reports::pack_input_report(*profile, state));
|
|
static_cast<void>(gamepad.close());
|
|
auto records = read_input_events_until_eof(descriptors[0]);
|
|
static_cast<void>(::close(descriptors[0]));
|
|
return {std::move(status), std::move(records)};
|
|
}
|
|
|
|
LinuxUinputRumbleResult linux_uinput_gamepad_fake_rumble(
|
|
GamepadProfileKind kind,
|
|
std::uint16_t effect_type,
|
|
std::uint16_t replay_length,
|
|
bool send_stop,
|
|
std::optional<std::uint16_t> reupload_length
|
|
) {
|
|
LinuxTestSyscalls syscalls;
|
|
enable_fake_device_syscalls(syscalls);
|
|
syscalls.override_poll = true;
|
|
syscalls.poll_results = {1, 1};
|
|
syscalls.poll_revents = {POLLIN, POLLIN};
|
|
syscalls.override_read = true;
|
|
syscalls.read_results = {
|
|
static_cast<std::ptrdiff_t>(sizeof(input_event)),
|
|
static_cast<std::ptrdiff_t>(sizeof(input_event)),
|
|
};
|
|
syscalls.read_input_events = {
|
|
input_event {.type = EV_UINPUT, .code = UI_FF_UPLOAD, .value = 7},
|
|
input_event {.type = EV_FF, .code = 3, .value = 1},
|
|
};
|
|
if (reupload_length.has_value()) {
|
|
syscalls.poll_results.push_back(1);
|
|
syscalls.poll_revents.push_back(POLLIN);
|
|
syscalls.read_results.push_back(static_cast<std::ptrdiff_t>(sizeof(input_event)));
|
|
syscalls.read_input_events.push_back(input_event {.type = EV_UINPUT, .code = UI_FF_UPLOAD, .value = 8});
|
|
}
|
|
if (send_stop) {
|
|
syscalls.poll_results.push_back(1);
|
|
syscalls.poll_revents.push_back(POLLIN);
|
|
syscalls.read_results.push_back(static_cast<std::ptrdiff_t>(sizeof(input_event)));
|
|
syscalls.read_input_events.push_back(input_event {.type = EV_FF, .code = 3, .value = 0});
|
|
}
|
|
syscalls.poll_results.push_back(0);
|
|
syscalls.poll_revents.push_back(0);
|
|
syscalls.provide_uploaded_ff_effect = true;
|
|
syscalls.uploaded_ff_effect.type = effect_type;
|
|
syscalls.uploaded_ff_effect.id = 3;
|
|
switch (effect_type) {
|
|
case FF_RUMBLE:
|
|
syscalls.uploaded_ff_effect.u.rumble.weak_magnitude = 0x1234;
|
|
syscalls.uploaded_ff_effect.u.rumble.strong_magnitude = 0x5678;
|
|
break;
|
|
case FF_CONSTANT:
|
|
syscalls.uploaded_ff_effect.u.constant.level = 0x1234;
|
|
break;
|
|
case FF_PERIODIC:
|
|
syscalls.uploaded_ff_effect.u.periodic.waveform = FF_SINE;
|
|
syscalls.uploaded_ff_effect.u.periodic.magnitude = 0x1234;
|
|
break;
|
|
case FF_RAMP:
|
|
syscalls.uploaded_ff_effect.u.ramp.start_level = 0x1234;
|
|
syscalls.uploaded_ff_effect.u.ramp.end_level = 0x2345;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
syscalls.uploaded_ff_effect.replay.length = replay_length;
|
|
if (reupload_length.has_value()) {
|
|
auto replacement = syscalls.uploaded_ff_effect;
|
|
replacement.replay.length = *reupload_length;
|
|
syscalls.uploaded_ff_effects = {syscalls.uploaded_ff_effect, replacement};
|
|
}
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUinputRumbleResult result;
|
|
const auto fd = open_test_fd();
|
|
if (fd < 0) {
|
|
result.create_status = system_error_status(ErrorCode::backend_failure, "failed to open test file descriptor", errno);
|
|
result.close_status = result.create_status;
|
|
return result;
|
|
}
|
|
|
|
std::atomic_size_t callback_count = 0;
|
|
std::atomic_size_t nonzero_callback_count = 0;
|
|
std::atomic_size_t zero_callback_count = 0;
|
|
std::mutex output_mutex;
|
|
GamepadOutput last_output;
|
|
UinputGamepad gamepad {fd, kind};
|
|
gamepad.set_output_callback([&callback_count, &nonzero_callback_count, &zero_callback_count, &last_output, &output_mutex](
|
|
const GamepadOutput &output
|
|
) {
|
|
if (output.low_frequency_rumble != 0 || output.high_frequency_rumble != 0) {
|
|
std::lock_guard lock {output_mutex};
|
|
last_output = output;
|
|
++nonzero_callback_count;
|
|
} else {
|
|
++zero_callback_count;
|
|
}
|
|
++callback_count;
|
|
});
|
|
|
|
CreateGamepadOptions options;
|
|
const auto profile = profiles::gamepad_profile(kind);
|
|
if (!profile.has_value()) {
|
|
result.create_status = OperationStatus::failure(ErrorCode::unsupported_profile, "unknown gamepad profile");
|
|
result.close_status = result.create_status;
|
|
static_cast<void>(system_close(fd));
|
|
return result;
|
|
}
|
|
options.profile = *profile;
|
|
result.create_status = gamepad.create(8, options);
|
|
const auto expect_zero_callback = send_stop || reupload_length.has_value();
|
|
const auto callbacks_complete = [&]() {
|
|
return nonzero_callback_count.load() > 0 && (!expect_zero_callback || zero_callback_count.load() > 0);
|
|
};
|
|
for (auto attempt = 0; attempt < 500 && !callbacks_complete(); ++attempt) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds {1});
|
|
}
|
|
result.nonzero_callback_count = nonzero_callback_count.load();
|
|
result.zero_callback_count_before_close = zero_callback_count.load();
|
|
result.close_status = gamepad.close();
|
|
result.callback_count = callback_count.load();
|
|
{
|
|
std::lock_guard lock {output_mutex};
|
|
result.last_output = last_output;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
LinuxRumbleCycleTiming linux_uinput_rumble_cycle_timing(std::uint64_t elapsed, std::uint64_t length) {
|
|
const auto [cycle_elapsed, cycle_remaining] = rumble_effect_cycle_timing(elapsed, length);
|
|
return {.elapsed = cycle_elapsed, .remaining = cycle_remaining};
|
|
}
|
|
|
|
OperationStatus linux_uinput_user_device_invalid_fd() {
|
|
return create_libevdev_uinput_device(-1, profiles::mouse(), 1).status;
|
|
}
|
|
|
|
OperationStatus linux_uinput_user_device_pipe() {
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::pipe(descriptors.data()) != 0) {
|
|
return system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno);
|
|
}
|
|
|
|
auto status = create_libevdev_uinput_device(descriptors[1], profiles::mouse(), 1).status;
|
|
static_cast<void>(::close(descriptors[0]));
|
|
static_cast<void>(::close(descriptors[1]));
|
|
return status;
|
|
}
|
|
|
|
OperationStatus linux_uinput_mouse_create_invalid_fd() {
|
|
CreateMouseOptions options;
|
|
options.profile = profiles::mouse();
|
|
|
|
UinputMouse mouse {-1, -1};
|
|
return mouse.create(1, options);
|
|
}
|
|
|
|
OperationStatus linux_uinput_mouse_submit_invalid_fd(const MouseEvent &event) {
|
|
UinputMouse mouse {-1, -1};
|
|
return mouse.submit(event);
|
|
}
|
|
|
|
OperationStatus linux_uinput_mouse_submit_after_close() {
|
|
UinputMouse mouse {-1, -1};
|
|
static_cast<void>(mouse.close());
|
|
return mouse.submit({.kind = MouseEventKind::relative_motion, .x = 1, .y = 1});
|
|
}
|
|
|
|
namespace {
|
|
/**
|
|
* @brief Submit mouse events through pipe-backed uinput devices.
|
|
*
|
|
* @param events Mouse events to submit.
|
|
* @param options Mouse creation options containing optional viewport bounds.
|
|
* @return Submission status and captured input events.
|
|
*/
|
|
LinuxInputSubmissionResult submit_uinput_mouse_pipe_sequence(
|
|
const std::vector<MouseEvent> &events,
|
|
const CreateMouseOptions &options
|
|
) {
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::pipe(descriptors.data()) != 0) {
|
|
return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}};
|
|
}
|
|
|
|
const auto absolute_descriptor = ::dup(descriptors[1]);
|
|
if (absolute_descriptor < 0) {
|
|
static_cast<void>(::close(descriptors[0]));
|
|
static_cast<void>(::close(descriptors[1]));
|
|
return {system_error_status(ErrorCode::backend_failure, "failed to duplicate pipe", errno), {}};
|
|
}
|
|
|
|
UinputMouse mouse {descriptors[1], absolute_descriptor, options.desktop, options.viewport};
|
|
auto status = OperationStatus::success();
|
|
for (const auto &event : events) {
|
|
status = mouse.submit(event);
|
|
if (!status.ok()) {
|
|
break;
|
|
}
|
|
}
|
|
static_cast<void>(mouse.close());
|
|
auto records = read_input_events_until_eof(descriptors[0]);
|
|
static_cast<void>(::close(descriptors[0]));
|
|
return {std::move(status), std::move(records)};
|
|
}
|
|
} // namespace
|
|
|
|
LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe(const MouseEvent &event) {
|
|
return submit_uinput_mouse_pipe_sequence(std::vector<MouseEvent> {event}, {});
|
|
}
|
|
|
|
LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe(
|
|
const MouseEvent &event,
|
|
const CreateMouseOptions &options
|
|
) {
|
|
return submit_uinput_mouse_pipe_sequence(std::vector<MouseEvent> {event}, options);
|
|
}
|
|
|
|
LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe_sequence(const std::vector<MouseEvent> &events) {
|
|
return submit_uinput_mouse_pipe_sequence(events, {});
|
|
}
|
|
|
|
LinuxMouseInputSubmissionResult linux_uinput_mouse_submit_split_pipe_sequence(const std::vector<MouseEvent> &events) {
|
|
std::array<int, 2> relative_descriptors {-1, -1};
|
|
if (::pipe(relative_descriptors.data()) != 0) {
|
|
return {system_error_status(ErrorCode::backend_failure, "failed to create relative mouse pipe", errno), {}, {}};
|
|
}
|
|
|
|
std::array<int, 2> absolute_descriptors {-1, -1};
|
|
if (::pipe(absolute_descriptors.data()) != 0) {
|
|
static_cast<void>(::close(relative_descriptors[0]));
|
|
static_cast<void>(::close(relative_descriptors[1]));
|
|
return {system_error_status(ErrorCode::backend_failure, "failed to create absolute mouse pipe", errno), {}, {}};
|
|
}
|
|
|
|
UinputMouse mouse {relative_descriptors[1], absolute_descriptors[1]};
|
|
auto status = OperationStatus::success();
|
|
for (const auto &event : events) {
|
|
status = mouse.submit(event);
|
|
if (!status.ok()) {
|
|
break;
|
|
}
|
|
}
|
|
static_cast<void>(mouse.close());
|
|
auto relative_records = read_input_events_until_eof(relative_descriptors[0]);
|
|
auto absolute_records = read_input_events_until_eof(absolute_descriptors[0]);
|
|
static_cast<void>(::close(relative_descriptors[0]));
|
|
static_cast<void>(::close(absolute_descriptors[0]));
|
|
return {std::move(status), std::move(relative_records), std::move(absolute_records)};
|
|
}
|
|
|
|
LinuxInputSubmissionResult linux_uinput_touchscreen_contact_pipe(const TouchContact &contact) {
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::pipe(descriptors.data()) != 0) {
|
|
return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}};
|
|
}
|
|
|
|
UinputTouchscreen touchscreen {descriptors[1]};
|
|
auto status = touchscreen.place_contact(contact);
|
|
if (status.ok()) {
|
|
status = touchscreen.release_contact(contact.id, PointerTransition::release);
|
|
}
|
|
static_cast<void>(touchscreen.close());
|
|
auto records = read_input_events_until_eof(descriptors[0]);
|
|
static_cast<void>(::close(descriptors[0]));
|
|
return {std::move(status), std::move(records)};
|
|
}
|
|
|
|
LinuxInputSubmissionResult linux_uinput_trackpad_contact_pipe(const TouchContact &contact) {
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::pipe(descriptors.data()) != 0) {
|
|
return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}};
|
|
}
|
|
|
|
UinputTrackpad trackpad {descriptors[1]};
|
|
auto status = trackpad.place_contact(contact);
|
|
if (status.ok()) {
|
|
status = trackpad.button(true);
|
|
}
|
|
if (status.ok()) {
|
|
status = trackpad.button(false);
|
|
}
|
|
if (status.ok()) {
|
|
status = trackpad.release_contact(contact.id, PointerTransition::release);
|
|
}
|
|
static_cast<void>(trackpad.close());
|
|
auto records = read_input_events_until_eof(descriptors[0]);
|
|
static_cast<void>(::close(descriptors[0]));
|
|
return {std::move(status), std::move(records)};
|
|
}
|
|
|
|
LinuxInputSubmissionResult linux_uinput_pen_tablet_tool_pipe(const PenToolState &state) {
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::pipe(descriptors.data()) != 0) {
|
|
return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}};
|
|
}
|
|
|
|
UinputPenTablet pen_tablet {descriptors[1]};
|
|
auto status = pen_tablet.place_tool(state);
|
|
if (status.ok()) {
|
|
status = pen_tablet.button(PenButton::primary, true);
|
|
}
|
|
if (status.ok()) {
|
|
status = pen_tablet.button(PenButton::primary, false);
|
|
}
|
|
static_cast<void>(pen_tablet.close());
|
|
auto records = read_input_events_until_eof(descriptors[0]);
|
|
static_cast<void>(::close(descriptors[0]));
|
|
return {std::move(status), std::move(records)};
|
|
}
|
|
|
|
LinuxInputSubmissionResult linux_uinput_trackpad_multi_contact_pipe() {
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::pipe(descriptors.data()) != 0) {
|
|
return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}};
|
|
}
|
|
|
|
UinputTrackpad trackpad {descriptors[1]};
|
|
auto status = OperationStatus::success();
|
|
for (auto id = 0; id < 5 && status.ok(); ++id) {
|
|
status = trackpad.place_contact({
|
|
.id = id,
|
|
.x = static_cast<float>(id) / 5.0F,
|
|
.y = 0.5F,
|
|
.pressure = 0.5F,
|
|
.orientation = id * 10,
|
|
});
|
|
}
|
|
for (auto id = 4; id >= 0 && status.ok(); --id) {
|
|
status = trackpad.release_contact(id, PointerTransition::release);
|
|
}
|
|
static_cast<void>(trackpad.close());
|
|
auto records = read_input_events_until_eof(descriptors[0]);
|
|
static_cast<void>(::close(descriptors[0]));
|
|
return {std::move(status), std::move(records)};
|
|
}
|
|
|
|
LinuxInputSubmissionResult linux_uinput_touch_contact_reuse_pipe(DeviceType device_type) {
|
|
switch (device_type) {
|
|
case DeviceType::touchscreen:
|
|
return touch_contact_reuse_pipe<UinputTouchscreen>();
|
|
case DeviceType::trackpad:
|
|
return touch_contact_reuse_pipe<UinputTrackpad>();
|
|
default:
|
|
return {
|
|
OperationStatus::failure(ErrorCode::invalid_argument, "device type must be touchscreen or trackpad"),
|
|
{},
|
|
};
|
|
}
|
|
}
|
|
|
|
OperationStatus linux_uinput_touchscreen_invalid_contacts() {
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::pipe(descriptors.data()) != 0) {
|
|
return system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno);
|
|
}
|
|
|
|
UinputTouchscreen touchscreen {descriptors[1]};
|
|
static_cast<void>(touchscreen.place_contact({.id = -1}));
|
|
|
|
auto status = OperationStatus::success();
|
|
for (auto id = 0; id <= touch_max_contacts && status.ok(); ++id) {
|
|
status = touchscreen.place_contact({
|
|
.id = id,
|
|
.x = 0.5F,
|
|
.y = 0.5F,
|
|
.pressure = 0.5F,
|
|
.orientation = 0,
|
|
});
|
|
}
|
|
|
|
static_cast<void>(touchscreen.close());
|
|
static_cast<void>(read_input_events_until_eof(descriptors[0]));
|
|
static_cast<void>(::close(descriptors[0]));
|
|
return status;
|
|
}
|
|
|
|
LinuxInputSubmissionResult linux_uinput_pen_tablet_transition_pipe() {
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::pipe(descriptors.data()) != 0) {
|
|
return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}};
|
|
}
|
|
|
|
UinputPenTablet pen_tablet {descriptors[1]};
|
|
auto status = OperationStatus::success();
|
|
using enum PenToolType;
|
|
constexpr std::array tools {
|
|
pen,
|
|
eraser,
|
|
brush,
|
|
pencil,
|
|
airbrush,
|
|
touch,
|
|
unchanged,
|
|
};
|
|
for (const auto tool : tools) {
|
|
if (!status.ok()) {
|
|
break;
|
|
}
|
|
status = pen_tablet.place_tool({
|
|
.tool = tool,
|
|
.x = 0.25F,
|
|
.y = 0.75F,
|
|
.pressure = tool == eraser ? -1.0F : 0.25F,
|
|
.distance = tool == eraser ? 0.5F : -1.0F,
|
|
.tilt_x = 120.0F,
|
|
.tilt_y = -120.0F,
|
|
});
|
|
}
|
|
if (status.ok()) {
|
|
status = pen_tablet.button(PenButton::secondary, true);
|
|
}
|
|
if (status.ok()) {
|
|
status = pen_tablet.button(PenButton::secondary, false);
|
|
}
|
|
if (status.ok()) {
|
|
status = pen_tablet.button(PenButton::tertiary, true);
|
|
}
|
|
if (status.ok()) {
|
|
status = pen_tablet.button(PenButton::tertiary, false);
|
|
}
|
|
static_cast<void>(pen_tablet.close());
|
|
auto records = read_input_events_until_eof(descriptors[0]);
|
|
static_cast<void>(::close(descriptors[0]));
|
|
return {std::move(status), std::move(records)};
|
|
}
|
|
|
|
OperationStatus linux_uinput_pen_tablet_closed_status() {
|
|
UinputPenTablet pen_tablet {-1};
|
|
static_cast<void>(pen_tablet.close());
|
|
static_cast<void>(pen_tablet.place_tool({}));
|
|
return pen_tablet.button(PenButton::primary, true);
|
|
}
|
|
|
|
LinuxUhidRoundTripResult linux_uhid_socketpair_roundtrip() {
|
|
LinuxUhidRoundTripResult result;
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) {
|
|
result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno);
|
|
result.submit_status = result.create_status;
|
|
result.close_status = result.create_status;
|
|
return result;
|
|
}
|
|
|
|
auto profile = profiles::xbox_360();
|
|
CreateGamepadOptions options;
|
|
options.profile = profile;
|
|
options.metadata.stable_id = "linux-uhid-roundtrip";
|
|
|
|
UhidGamepad gamepad {descriptors[0]};
|
|
auto event = create_started_profile_uhid_gamepad(gamepad, 7, options, descriptors[1], BUS_USB, result);
|
|
|
|
gamepad.set_output_callback([&result](const GamepadOutput &output) {
|
|
++result.output.callback_count;
|
|
result.output.last = output;
|
|
});
|
|
|
|
event = {};
|
|
event.type = UHID_OUTPUT;
|
|
event.u.output.size = static_cast<__u16>(profile.output_report_size);
|
|
event.u.output.data[0] = profile.report_id;
|
|
event.u.output.data[1] = 0x34;
|
|
event.u.output.data[2] = 0x12;
|
|
event.u.output.data[3] = 0x78;
|
|
event.u.output.data[4] = 0x56;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
|
|
event = {};
|
|
event.type = UHID_GET_REPORT;
|
|
event.u.get_report.id = 9;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event(descriptors[1], event)) {
|
|
result.saw_get_report_reply = event.type == UHID_GET_REPORT_REPLY && event.u.get_report_reply.id == 9;
|
|
}
|
|
|
|
event = {};
|
|
event.type = UHID_SET_REPORT;
|
|
event.u.set_report.id = 10;
|
|
event.u.set_report.rnum = profile.report_id;
|
|
event.u.set_report.rtype = UHID_OUTPUT_REPORT;
|
|
event.u.set_report.size = static_cast<__u16>(profile.output_report_size);
|
|
event.u.set_report.data[0] = profile.report_id;
|
|
event.u.set_report.data[1] = 0x78;
|
|
event.u.set_report.data[2] = 0x56;
|
|
event.u.set_report.data[3] = 0x34;
|
|
event.u.set_report.data[4] = 0x12;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event(descriptors[1], event)) {
|
|
result.saw_set_report_reply = event.type == UHID_SET_REPORT_REPLY && event.u.set_report_reply.id == 10;
|
|
}
|
|
|
|
event = {};
|
|
event.type = UHID_OPEN;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
|
|
lvh::GamepadState state;
|
|
state.buttons.set(GamepadButton::a);
|
|
const auto report = reports::pack_input_report(profile, state);
|
|
result.submit_status = gamepad.submit(state, report);
|
|
if (read_uhid_event(descriptors[1], event)) {
|
|
result.saw_input = event.type == UHID_INPUT2 && event.u.input2.size == report.size();
|
|
}
|
|
|
|
result.close_status = gamepad.close();
|
|
if (read_uhid_event(descriptors[1], event)) {
|
|
result.saw_destroy = event.type == UHID_DESTROY;
|
|
}
|
|
|
|
static_cast<void>(::close(descriptors[1]));
|
|
return result;
|
|
}
|
|
|
|
LinuxUhidRoundTripResult linux_xbox_bluetooth_uhid_socketpair_reports(
|
|
GamepadProfileKind kind,
|
|
bool advertises_battery,
|
|
bool submits_battery
|
|
) {
|
|
LinuxUhidRoundTripResult result;
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) {
|
|
result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno);
|
|
result.submit_status = result.create_status;
|
|
result.close_status = result.create_status;
|
|
return result;
|
|
}
|
|
|
|
const auto profile = profiles::gamepad_profile(kind).value();
|
|
CreateGamepadOptions options;
|
|
options.profile = profile;
|
|
options.metadata.stable_id = "xbox-bluetooth-uhid-roundtrip";
|
|
options.metadata.has_battery = advertises_battery;
|
|
|
|
UhidGamepad gamepad {descriptors[0]};
|
|
auto event = create_started_profile_uhid_gamepad(gamepad, 13, options, descriptors[1], BUS_BLUETOOTH, result);
|
|
if (result.creation.saw_create) {
|
|
const auto series = kind == GamepadProfileKind::xbox_series;
|
|
const auto descriptor = std::span<const std::uint8_t> {
|
|
event.u.create2.rd_data,
|
|
event.u.create2.rd_size,
|
|
};
|
|
constexpr std::array<std::uint8_t, 16> battery_report_descriptor {
|
|
0x05U,
|
|
0x06U,
|
|
0x09U,
|
|
0x20U,
|
|
0x85U,
|
|
xbox_bluetooth_battery_report_id,
|
|
0x15U,
|
|
0x04U,
|
|
0x25U,
|
|
0x07U,
|
|
0x75U,
|
|
0x08U,
|
|
0x95U,
|
|
0x01U,
|
|
0x81U,
|
|
0x02U,
|
|
};
|
|
result.xbox.saw_transport_descriptor = descriptor.size() == (advertises_battery ? 299U : 283U);
|
|
result.xbox.saw_battery_descriptor =
|
|
std::ranges::search(descriptor, battery_report_descriptor).begin() != descriptor.end();
|
|
constexpr std::array<std::uint8_t, 4> right_stick_usages {
|
|
0x09U,
|
|
0x33U,
|
|
0x09U,
|
|
0x34U,
|
|
};
|
|
constexpr std::array<std::uint8_t, 6> left_trigger_usage {
|
|
0x05U,
|
|
0x01U,
|
|
0x09U,
|
|
0x32U,
|
|
0x15U,
|
|
0x00U,
|
|
};
|
|
constexpr std::array<std::uint8_t, 6> right_trigger_usage {
|
|
0x05U,
|
|
0x01U,
|
|
0x09U,
|
|
0x35U,
|
|
0x15U,
|
|
0x00U,
|
|
};
|
|
result.xbox.saw_canonical_evdev_axes =
|
|
std::ranges::search(descriptor, right_stick_usages).begin() != descriptor.end() &&
|
|
std::ranges::search(descriptor, left_trigger_usage).begin() != descriptor.end() &&
|
|
std::ranges::search(descriptor, right_trigger_usage).begin() != descriptor.end();
|
|
result.xbox.saw_gamepad_application_usage =
|
|
event.u.create2.rd_size >= 4U && event.u.create2.rd_data[0] == 0x05U &&
|
|
event.u.create2.rd_data[1] == 0x01U && event.u.create2.rd_data[2] == 0x09U &&
|
|
event.u.create2.rd_data[3] == 0x05U;
|
|
result.xbox.saw_bluetooth_identity =
|
|
static_cast<std::uint16_t>(event.u.create2.vendor) == 0x045EU &&
|
|
event.u.create2.product == (series ? xbox_series_uinput_product_id : xbox_wireless_uinput_product_id) &&
|
|
event.u.create2.version == xbox_bluetooth_version && event.u.create2.bus == BUS_BLUETOOTH;
|
|
}
|
|
|
|
std::atomic_size_t callback_count {0};
|
|
gamepad.set_output_callback([&result, &callback_count](const GamepadOutput &output) {
|
|
result.output.last = output;
|
|
if (output.kind == GamepadOutputKind::rumble) {
|
|
result.output.rumble = output;
|
|
} else if (output.kind == GamepadOutputKind::trigger_rumble) {
|
|
result.output.trigger_rumble = output;
|
|
}
|
|
++callback_count;
|
|
});
|
|
|
|
event = {};
|
|
event.type = UHID_OPEN;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
|
|
GamepadState state;
|
|
state.buttons.set(GamepadButton::a);
|
|
state.buttons.set(GamepadButton::start);
|
|
state.buttons.set(GamepadButton::dpad_right);
|
|
state.buttons.set(GamepadButton::left_shoulder);
|
|
state.buttons.set(GamepadButton::left_stick);
|
|
state.buttons.set(GamepadButton::guide);
|
|
if (kind == GamepadProfileKind::xbox_series) {
|
|
state.buttons.set(GamepadButton::misc1);
|
|
}
|
|
state.left_stick = {.x = 1.0F, .y = -1.0F};
|
|
state.right_stick = {.x = 0.5F, .y = -0.5F};
|
|
state.left_trigger = 0.25F;
|
|
state.right_trigger = 0.75F;
|
|
if (submits_battery) {
|
|
state.battery = GamepadBattery {
|
|
.state = GamepadBatteryState::discharging,
|
|
.percentage = 50,
|
|
};
|
|
}
|
|
const auto report = reports::pack_input_report(profile, state);
|
|
result.submit_status = gamepad.submit(state, report);
|
|
|
|
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
|
|
const auto input = std::span {event.u.input2.data, event.u.input2.size};
|
|
result.xbox.saw_input =
|
|
input.size() == xbox_bluetooth_input_report_size && input[0] == xbox_bluetooth_input_report_id &&
|
|
std::equal(report.begin(), report.begin() + 8, input.begin() + 1) &&
|
|
std::equal(report.begin() + 8, report.begin() + 12, input.begin() + 9) && input[13] == report[14] &&
|
|
input[14] == 0x41U && input[15] == 0x38U;
|
|
result.xbox.saw_guide = input.size() == xbox_bluetooth_input_report_size && (input[15] & 0x10U) != 0U;
|
|
result.xbox.saw_profile_consumer_button =
|
|
input.size() == xbox_bluetooth_input_report_size &&
|
|
input[16] == (kind == GamepadProfileKind::xbox_series ? 0x01U : 0x00U);
|
|
}
|
|
result.saw_input = result.xbox.saw_input;
|
|
|
|
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
|
|
const auto input = std::span {event.u.input2.data, event.u.input2.size};
|
|
result.xbox.saw_battery_input =
|
|
input.size() == xbox_bluetooth_battery_report_size && input[0] == xbox_bluetooth_battery_report_id &&
|
|
input[1] == 0x05U;
|
|
}
|
|
|
|
constexpr std::array<std::uint8_t, xbox_bluetooth_rumble_report_size> motor_report {
|
|
xbox_bluetooth::xbox_bluetooth_rumble_report_id,
|
|
0x0F,
|
|
25,
|
|
50,
|
|
75,
|
|
100,
|
|
10,
|
|
0,
|
|
0,
|
|
};
|
|
event = {};
|
|
event.type = UHID_OUTPUT;
|
|
event.u.output.rtype = UHID_OUTPUT_REPORT;
|
|
event.u.output.size = static_cast<__u16>(motor_report.size());
|
|
std::copy(motor_report.begin(), motor_report.end(), event.u.output.data);
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
|
|
const auto output_deadline = std::chrono::steady_clock::now() + std::chrono::seconds {1};
|
|
while (std::chrono::steady_clock::now() < output_deadline && callback_count.load() < 2U) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds {10});
|
|
}
|
|
result.output.callback_count = callback_count.load();
|
|
|
|
result.close_status = gamepad.close();
|
|
if (read_uhid_event_type(descriptors[1], UHID_DESTROY, event)) {
|
|
result.saw_destroy = true;
|
|
}
|
|
|
|
static_cast<void>(::close(descriptors[1]));
|
|
return result;
|
|
}
|
|
|
|
bool linux_gamepad_prefers_uhid(GamepadProfileKind kind) {
|
|
return prefers_uhid_gamepad_profile(kind);
|
|
}
|
|
|
|
bool linux_gamepad_uses_uinput(GamepadProfileKind kind) {
|
|
return uses_uinput_gamepad_profile(kind);
|
|
}
|
|
|
|
DeviceProfile linux_uinput_effective_gamepad_profile(GamepadProfileKind kind) {
|
|
const auto requested_profile = profiles::gamepad_profile(kind).value();
|
|
return effective_uinput_profile(requested_profile).value_or(requested_profile);
|
|
}
|
|
|
|
LinuxUhidRoundTripResult linux_switch_pro_uhid_socketpair_reports() {
|
|
LinuxUhidRoundTripResult result;
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) {
|
|
result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno);
|
|
result.submit_status = result.create_status;
|
|
result.close_status = result.create_status;
|
|
return result;
|
|
}
|
|
|
|
CreateGamepadOptions options;
|
|
options.profile = profiles::switch_pro();
|
|
options.metadata.stable_id = "streaming-host-gamepad-0";
|
|
|
|
UhidGamepad gamepad {descriptors[0]};
|
|
auto event = create_started_profile_uhid_gamepad(gamepad, 12, options, descriptors[1], BUS_BLUETOOTH, result);
|
|
result.creation.saw_create = result.creation.saw_create &&
|
|
event.u.create2.rd_size == options.profile.report_descriptor.size();
|
|
|
|
const auto idle_report = reports::pack_input_report(options.profile, {});
|
|
result.switch_pro.idle_input_reports = read_switch_input_reports(descriptors[1], idle_report);
|
|
|
|
gamepad.set_output_callback([&result](const GamepadOutput &output) {
|
|
++result.output.callback_count;
|
|
result.output.last = output;
|
|
if (
|
|
output.kind == GamepadOutputKind::player_leds &&
|
|
output.player_leds == std::array {true, false, true, false} &&
|
|
output.flashing_player_leds == std::array {false, true, false, true}
|
|
) {
|
|
result.switch_pro.saw_player_leds = true;
|
|
}
|
|
});
|
|
|
|
event = {};
|
|
event.type = UHID_OUTPUT;
|
|
event.u.output.rtype = UHID_OUTPUT_REPORT;
|
|
event.u.output.size = static_cast<__u16>(options.profile.output_report_size);
|
|
event.u.output.data[0] = 0x01;
|
|
event.u.output.data[1] = 0x07;
|
|
const std::array<std::uint8_t, 8> neutral_rumble {0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40};
|
|
std::ranges::copy(neutral_rumble, event.u.output.data + 2U);
|
|
event.u.output.data[10] = 0x30;
|
|
event.u.output.data[11] = 0xA5;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event_matching(descriptors[1], [](const uhid_event &candidate) {
|
|
return candidate.type == UHID_INPUT2 && candidate.u.input2.size > 0U && candidate.u.input2.data[0] == 0x21;
|
|
},
|
|
event)) {
|
|
if (event.u.input2.size > 1U) {
|
|
result.switch_pro.subcommand_reply_packet_timer = static_cast<std::uint8_t>(event.u.input2.data[1]);
|
|
}
|
|
result.switch_pro.saw_subcommand_reply = event.u.input2.size == options.profile.input_report_size &&
|
|
event.u.input2.data[0] == 0x21 &&
|
|
event.u.input2.data[13] == 0x80 &&
|
|
event.u.input2.data[14] == 0x30;
|
|
}
|
|
|
|
GamepadState state;
|
|
state.buttons.set(GamepadButton::a);
|
|
state.acceleration = Vector3 {.x = 9.80665F, .y = 19.6133F, .z = -9.80665F};
|
|
state.gyroscope = Vector3 {.x = 1.0F, .y = 2.0F, .z = -3.0F};
|
|
const auto report = reports::pack_input_report(options.profile, state);
|
|
result.submit_status = gamepad.submit(state, report);
|
|
result.switch_pro.repeated_input_reports = read_switch_input_reports(descriptors[1], report);
|
|
if (!result.switch_pro.repeated_input_reports.empty()) {
|
|
const auto &motion_report = result.switch_pro.repeated_input_reports.front();
|
|
result.switch_pro.motion_input_packet_timer = motion_report[1];
|
|
result.switch_pro.saw_motion_input = motion_report[0] == 0x30 && (motion_report[3] & 0x08U) != 0U &&
|
|
std::equal(report.begin() + 13, report.begin() + 49, motion_report.begin() + 13);
|
|
}
|
|
|
|
result.switch_pro.clear_status = gamepad.submit({}, reports::pack_input_report(options.profile, {}));
|
|
result.switch_pro.cleared_input_reports = read_switch_input_reports(descriptors[1], idle_report);
|
|
|
|
result.close_status = gamepad.close();
|
|
result.saw_destroy = read_uhid_event_type(descriptors[1], UHID_DESTROY, event);
|
|
result.switch_pro.stopped_after_destroy = result.saw_destroy && !read_uhid_event(descriptors[1], event);
|
|
static_cast<void>(::close(descriptors[1]));
|
|
return result;
|
|
}
|
|
|
|
LinuxUhidRoundTripResult linux_dualsense_uhid_socketpair_reports() {
|
|
LinuxUhidRoundTripResult result;
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) {
|
|
result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno);
|
|
result.submit_status = result.create_status;
|
|
result.close_status = result.create_status;
|
|
return result;
|
|
}
|
|
|
|
CreateGamepadOptions options;
|
|
options.profile = profiles::dualsense_usb();
|
|
options.profile.name = "libvirtualhid PS5 Controller";
|
|
options.metadata.stable_id = "02:03:04:05:06:07";
|
|
|
|
UhidGamepad gamepad {descriptors[0]};
|
|
auto event = create_started_profile_uhid_gamepad(gamepad, 8, options, descriptors[1], BUS_USB, result);
|
|
|
|
gamepad.set_output_callback([&result](const GamepadOutput &output) {
|
|
if (output.kind == GamepadOutputKind::rumble) {
|
|
++result.output.callback_count;
|
|
result.output.last = output;
|
|
}
|
|
});
|
|
|
|
event = {};
|
|
event.type = UHID_OUTPUT;
|
|
event.u.output.rtype = UHID_OUTPUT_REPORT;
|
|
event.u.output.size = 63;
|
|
event.u.output.data[0] = 0x02;
|
|
event.u.output.data[1] = 0x03;
|
|
event.u.output.data[3] = 0x12;
|
|
event.u.output.data[4] = 0x56;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
|
|
event = {};
|
|
event.type = UHID_SET_REPORT;
|
|
event.u.set_report.id = 20;
|
|
event.u.set_report.rnum = 0x02;
|
|
event.u.set_report.rtype = UHID_OUTPUT_REPORT;
|
|
event.u.set_report.size = 62;
|
|
event.u.set_report.data[0] = 0x03;
|
|
event.u.set_report.data[2] = 0x12;
|
|
event.u.set_report.data[3] = 0x56;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event_type(descriptors[1], UHID_SET_REPORT_REPLY, event)) {
|
|
result.saw_set_report_reply = event.u.set_report_reply.id == 20 && event.u.set_report_reply.err == 0;
|
|
}
|
|
|
|
event = {};
|
|
event.type = UHID_GET_REPORT;
|
|
event.u.get_report.id = 11;
|
|
event.u.get_report.rnum = 0x05;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
|
result.dualsense.saw_calibration = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 41U &&
|
|
event.u.get_report_reply.data[0] == 0x05;
|
|
}
|
|
|
|
event = {};
|
|
event.type = UHID_GET_REPORT;
|
|
event.u.get_report.id = 12;
|
|
event.u.get_report.rnum = 0x09;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
|
result.dualsense.saw_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size > 7 &&
|
|
event.u.get_report_reply.data[0] == 0x09 &&
|
|
event.u.get_report_reply.data[1] == 0x07 &&
|
|
event.u.get_report_reply.data[6] == 0x02;
|
|
}
|
|
|
|
event = {};
|
|
event.type = UHID_GET_REPORT;
|
|
event.u.get_report.id = 13;
|
|
event.u.get_report.rnum = 0x20;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
|
result.dualsense.saw_firmware = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size > 0 &&
|
|
event.u.get_report_reply.data[0] == 0x20;
|
|
}
|
|
|
|
event = {};
|
|
event.type = UHID_GET_REPORT;
|
|
event.u.get_report.id = 15;
|
|
event.u.get_report.rnum = 0x7F;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
static_cast<void>(read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event));
|
|
|
|
result.close_status = gamepad.close();
|
|
static_cast<void>(::close(descriptors[1]));
|
|
result.submit_status = OperationStatus::success();
|
|
return result;
|
|
}
|
|
|
|
LinuxUhidRoundTripResult linux_dualsense_bluetooth_uhid_socketpair_reports() {
|
|
LinuxUhidRoundTripResult result;
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) {
|
|
result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno);
|
|
result.submit_status = result.create_status;
|
|
result.close_status = result.create_status;
|
|
return result;
|
|
}
|
|
|
|
CreateGamepadOptions options;
|
|
options.profile = profiles::dualsense_bluetooth();
|
|
options.profile.name = "libvirtualhid PS5 Controller";
|
|
options.metadata.stable_id = "02:03:04:05:06:07";
|
|
|
|
UhidGamepad gamepad {descriptors[0]};
|
|
auto event = create_started_profile_uhid_gamepad(gamepad, 9, options, descriptors[1], BUS_BLUETOOTH, result);
|
|
|
|
GamepadState motion_state;
|
|
motion_state.acceleration = Vector3 {.x = 1.0F, .y = 2.0F, .z = 3.0F};
|
|
motion_state.gyroscope = Vector3 {.x = 4.0F, .y = 5.0F, .z = 6.0F};
|
|
const auto submitted_report = reports::pack_input_report(options.profile, motion_state);
|
|
result.submit_status = gamepad.submit(motion_state, submitted_report);
|
|
|
|
bool first_input_report_valid = false;
|
|
std::uint8_t first_sequence = 0;
|
|
std::uint32_t first_sensor_timestamp = 0;
|
|
std::array<std::uint8_t, 12> first_sensor_values {};
|
|
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
|
|
const auto report_size = static_cast<std::size_t>(event.u.input2.size);
|
|
if (report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x31) {
|
|
const auto crc_offset = report_size - 4U;
|
|
const auto expected_crc = crc32(std::span<const std::uint8_t> {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1));
|
|
const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset);
|
|
std::copy_n(event.u.input2.data + 17U, first_sensor_values.size(), first_sensor_values.begin());
|
|
first_input_report_valid = expected_crc == actual_crc && std::equal(
|
|
first_sensor_values.begin(),
|
|
first_sensor_values.end(),
|
|
submitted_report.begin() + 17
|
|
);
|
|
first_sequence = event.u.input2.data[8];
|
|
first_sensor_timestamp = read_u32_le(event.u.input2.data + 29U);
|
|
}
|
|
}
|
|
|
|
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
|
|
const auto report_size = static_cast<std::size_t>(event.u.input2.size);
|
|
if (report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x31) {
|
|
const auto crc_offset = report_size - 4U;
|
|
const auto expected_crc = crc32(std::span<const std::uint8_t> {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1));
|
|
const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset);
|
|
result.dualsense.saw_bluetooth_input_with_live_sensor_metadata =
|
|
first_input_report_valid && expected_crc == actual_crc && event.u.input2.data[8] != first_sequence &&
|
|
read_u32_le(event.u.input2.data + 29U) != first_sensor_timestamp && std::equal(first_sensor_values.begin(), first_sensor_values.end(), event.u.input2.data + 17U);
|
|
}
|
|
}
|
|
|
|
event = {};
|
|
event.type = UHID_GET_REPORT;
|
|
event.u.get_report.id = 14;
|
|
event.u.get_report.rnum = 0x09;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
|
const auto report_size = static_cast<std::size_t>(event.u.get_report_reply.size);
|
|
result.dualsense.saw_pairing = event.u.get_report_reply.err == 0 && report_size > 7U &&
|
|
event.u.get_report_reply.data[0] == 0x09 &&
|
|
event.u.get_report_reply.data[1] == 0x07 &&
|
|
event.u.get_report_reply.data[6] == 0x02;
|
|
if (report_size >= 4U) {
|
|
const auto crc_offset = report_size - 4U;
|
|
const auto expected_crc = crc32(
|
|
std::span<const std::uint8_t> {event.u.get_report_reply.data, crc_offset},
|
|
playstation_crc_seed(playstation_feature_reports::playstation_feature_crc_seed)
|
|
);
|
|
const auto actual_crc = read_u32_le(event.u.get_report_reply.data + crc_offset);
|
|
result.dualsense.saw_feature_crc = expected_crc == actual_crc;
|
|
}
|
|
}
|
|
|
|
result.close_status = gamepad.close();
|
|
static_cast<void>(::close(descriptors[1]));
|
|
return result;
|
|
}
|
|
|
|
LinuxUhidRoundTripResult linux_dualshock4_uhid_socketpair_reports() {
|
|
LinuxUhidRoundTripResult result;
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) {
|
|
result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno);
|
|
result.submit_status = result.create_status;
|
|
result.close_status = result.create_status;
|
|
return result;
|
|
}
|
|
|
|
CreateGamepadOptions options;
|
|
options.profile = profiles::dualshock4_usb();
|
|
options.profile.name = "libvirtualhid PS4 Controller";
|
|
options.metadata.stable_id = "02:03:04:05:06:07";
|
|
|
|
UhidGamepad gamepad {descriptors[0]};
|
|
auto event = create_started_profile_uhid_gamepad(gamepad, 10, options, descriptors[1], BUS_USB, result);
|
|
result.creation.saw_create = result.creation.saw_create &&
|
|
event.u.create2.rd_size == options.profile.report_descriptor.size();
|
|
|
|
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
|
|
result.dualshock4.saw_usb_input =
|
|
event.u.input2.size == options.profile.input_report_size && event.u.input2.data[0] == options.profile.report_id;
|
|
}
|
|
|
|
gamepad.set_output_callback([&result](const GamepadOutput &output) {
|
|
if (output.kind == GamepadOutputKind::rumble) {
|
|
++result.output.callback_count;
|
|
result.output.last = output;
|
|
}
|
|
});
|
|
|
|
event = {};
|
|
event.type = UHID_SET_REPORT;
|
|
event.u.set_report.id = 21;
|
|
event.u.set_report.rnum = 0x05;
|
|
event.u.set_report.rtype = UHID_OUTPUT_REPORT;
|
|
event.u.set_report.size = 31;
|
|
event.u.set_report.data[0] = 0x03;
|
|
event.u.set_report.data[3] = 0x12;
|
|
event.u.set_report.data[4] = 0x56;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event_type(descriptors[1], UHID_SET_REPORT_REPLY, event)) {
|
|
result.saw_set_report_reply = event.u.set_report_reply.id == 21 && event.u.set_report_reply.err == 0;
|
|
}
|
|
|
|
event = {};
|
|
event.type = UHID_GET_REPORT;
|
|
event.u.get_report.id = 16;
|
|
event.u.get_report.rnum = 0x02;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
|
result.dualshock4.saw_calibration = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 37 &&
|
|
event.u.get_report_reply.data[0] == 0x02;
|
|
}
|
|
|
|
event = {};
|
|
event.type = UHID_GET_REPORT;
|
|
event.u.get_report.id = 17;
|
|
event.u.get_report.rnum = 0x12;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
|
result.dualshock4.saw_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 16 &&
|
|
event.u.get_report_reply.data[0] == 0x12 &&
|
|
event.u.get_report_reply.data[1] == 0x07 &&
|
|
event.u.get_report_reply.data[6] == 0x02;
|
|
}
|
|
|
|
event = {};
|
|
event.type = UHID_GET_REPORT;
|
|
event.u.get_report.id = 18;
|
|
event.u.get_report.rnum = 0xA3;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
|
result.dualshock4.saw_firmware = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 49 &&
|
|
event.u.get_report_reply.data[0] == 0xA3;
|
|
}
|
|
|
|
result.close_status = gamepad.close();
|
|
static_cast<void>(::close(descriptors[1]));
|
|
result.submit_status = OperationStatus::success();
|
|
return result;
|
|
}
|
|
|
|
LinuxUhidRoundTripResult linux_dualshock4_bluetooth_uhid_socketpair_reports() {
|
|
LinuxUhidRoundTripResult result;
|
|
std::array<int, 2> descriptors {-1, -1};
|
|
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) {
|
|
result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno);
|
|
result.submit_status = result.create_status;
|
|
result.close_status = result.create_status;
|
|
return result;
|
|
}
|
|
|
|
CreateGamepadOptions options;
|
|
options.profile = profiles::dualshock4_bluetooth();
|
|
options.profile.name = "libvirtualhid PS4 Controller";
|
|
options.metadata.stable_id = "02:03:04:05:06:07";
|
|
|
|
UhidGamepad gamepad {descriptors[0]};
|
|
auto event = create_started_profile_uhid_gamepad(gamepad, 11, options, descriptors[1], BUS_BLUETOOTH, result);
|
|
|
|
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
|
|
const auto report_size = static_cast<std::size_t>(event.u.input2.size);
|
|
if (
|
|
report_size == options.profile.input_report_size &&
|
|
event.u.input2.data[0] == 0x11 &&
|
|
(event.u.input2.data[1] & 0x80U) != 0U
|
|
) {
|
|
const auto crc_offset = report_size - 4U;
|
|
const auto expected_crc = crc32(std::span<const std::uint8_t> {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1));
|
|
const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset);
|
|
result.dualshock4.saw_bluetooth_input = expected_crc == actual_crc;
|
|
}
|
|
}
|
|
|
|
event = {};
|
|
event.type = UHID_GET_REPORT;
|
|
event.u.get_report.id = 19;
|
|
event.u.get_report.rnum = 0x05;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
|
const auto report_size = static_cast<std::size_t>(event.u.get_report_reply.size);
|
|
result.dualshock4.saw_calibration = event.u.get_report_reply.err == 0 && report_size == 41U &&
|
|
event.u.get_report_reply.data[0] == 0x05;
|
|
if (report_size >= 4U) {
|
|
const auto crc_offset = report_size - 4U;
|
|
const auto expected_crc = crc32(
|
|
std::span<const std::uint8_t> {event.u.get_report_reply.data, crc_offset},
|
|
playstation_crc_seed(playstation_feature_reports::playstation_feature_crc_seed)
|
|
);
|
|
const auto actual_crc = read_u32_le(event.u.get_report_reply.data + crc_offset);
|
|
result.dualshock4.saw_feature_crc = expected_crc == actual_crc;
|
|
}
|
|
}
|
|
|
|
event = {};
|
|
event.type = UHID_GET_REPORT;
|
|
event.u.get_report.id = 20;
|
|
event.u.get_report.rnum = 0x12;
|
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
|
result.dualshock4.saw_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 16 &&
|
|
event.u.get_report_reply.data[0] == 0x12 &&
|
|
event.u.get_report_reply.data[1] == 0x07 &&
|
|
event.u.get_report_reply.data[6] == 0x02;
|
|
}
|
|
|
|
result.close_status = gamepad.close();
|
|
static_cast<void>(::close(descriptors[1]));
|
|
result.submit_status = OperationStatus::success();
|
|
return result;
|
|
}
|
|
|
|
LinuxBackendFakeCreationResult linux_backend_create_all_fake_success() {
|
|
LinuxTestSyscalls syscalls;
|
|
enable_fake_device_syscalls(syscalls);
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxBackendFakeCreationResult result;
|
|
LinuxUhidBackend backend;
|
|
result.capabilities = backend.capabilities();
|
|
|
|
CreateGamepadOptions gamepad_options;
|
|
gamepad_options.profile = profiles::xbox_360();
|
|
gamepad_options.metadata.stable_id = "fake-linux-gamepad";
|
|
auto gamepad = backend.create_gamepad(1, gamepad_options);
|
|
result.gamepad_status = gamepad.status;
|
|
if (gamepad) {
|
|
result.gamepad_close_status = gamepad.gamepad->close();
|
|
}
|
|
|
|
CreateKeyboardOptions keyboard_options;
|
|
keyboard_options.profile = profiles::keyboard();
|
|
keyboard_options.stable_id = "fake-linux-keyboard";
|
|
auto keyboard = backend.create_keyboard(2, keyboard_options);
|
|
result.keyboard_status = keyboard.status;
|
|
if (keyboard) {
|
|
result.keyboard_close_status = keyboard.keyboard->close();
|
|
}
|
|
|
|
CreateMouseOptions mouse_options;
|
|
mouse_options.profile = profiles::mouse();
|
|
mouse_options.stable_id = "fake-linux-mouse";
|
|
auto mouse = backend.create_mouse(3, mouse_options);
|
|
result.mouse_status = mouse.status;
|
|
if (mouse) {
|
|
result.mouse_close_status = mouse.mouse->close();
|
|
}
|
|
|
|
CreateTouchscreenOptions touchscreen_options;
|
|
touchscreen_options.profile = profiles::touchscreen();
|
|
touchscreen_options.stable_id = "fake-linux-touchscreen";
|
|
auto touchscreen = backend.create_touchscreen(4, touchscreen_options);
|
|
result.touchscreen_status = touchscreen.status;
|
|
if (touchscreen) {
|
|
result.touchscreen_close_status = touchscreen.touchscreen->close();
|
|
}
|
|
|
|
CreateTrackpadOptions trackpad_options;
|
|
trackpad_options.profile = profiles::trackpad();
|
|
trackpad_options.stable_id = "fake-linux-trackpad";
|
|
auto trackpad = backend.create_trackpad(5, trackpad_options);
|
|
result.trackpad_status = trackpad.status;
|
|
if (trackpad) {
|
|
result.trackpad_close_status = trackpad.trackpad->close();
|
|
}
|
|
|
|
CreatePenTabletOptions pen_tablet_options;
|
|
pen_tablet_options.profile = profiles::pen_tablet();
|
|
pen_tablet_options.stable_id = "fake-linux-pen-tablet";
|
|
auto pen_tablet = backend.create_pen_tablet(6, pen_tablet_options);
|
|
result.pen_tablet_status = pen_tablet.status;
|
|
if (pen_tablet) {
|
|
result.pen_tablet_close_status = pen_tablet.pen_tablet->close();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
BackendCapabilities linux_backend_fake_unavailable_capabilities() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_access = true;
|
|
syscalls.access_result = -1;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
return backend.capabilities();
|
|
}
|
|
|
|
OperationStatus linux_backend_gamepad_fake_open_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_access = true;
|
|
syscalls.override_open = true;
|
|
syscalls.open_result = -1;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateGamepadOptions options;
|
|
options.profile = profiles::xbox_360();
|
|
return backend.create_gamepad(1, options).status;
|
|
}
|
|
|
|
int linux_backend_gamepad_open_flags() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_access = true;
|
|
syscalls.override_open = true;
|
|
syscalls.open_result = -1;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateGamepadOptions options;
|
|
options.profile = profiles::dualshock4_usb();
|
|
static_cast<void>(backend.create_gamepad(1, options));
|
|
return syscalls.last_open_flags;
|
|
}
|
|
|
|
OperationStatus linux_backend_gamepad_fake_create_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
enable_fake_device_syscalls(syscalls);
|
|
syscalls.fail_write_call = 1;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateGamepadOptions options;
|
|
options.profile = profiles::dualshock4_usb();
|
|
return backend.create_gamepad(1, options).status;
|
|
}
|
|
|
|
OperationStatus linux_backend_keyboard_fake_open_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_access = true;
|
|
syscalls.override_open = true;
|
|
syscalls.open_result = -1;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateKeyboardOptions options;
|
|
options.profile = profiles::keyboard();
|
|
return backend.create_keyboard(1, options).status;
|
|
}
|
|
|
|
OperationStatus linux_backend_keyboard_fake_create_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
enable_fake_device_syscalls(syscalls);
|
|
syscalls.fail_libevdev_create = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateKeyboardOptions options;
|
|
options.profile = profiles::keyboard();
|
|
return backend.create_keyboard(1, options).status;
|
|
}
|
|
|
|
OperationStatus linux_backend_keyboard_fake_fallback_success() {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
LinuxTestSyscalls syscalls;
|
|
enable_fake_device_syscalls(syscalls);
|
|
syscalls.fail_libevdev_create = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateKeyboardOptions options;
|
|
options.profile = profiles::keyboard();
|
|
auto keyboard = backend.create_keyboard(1, options);
|
|
if (!keyboard) {
|
|
return keyboard.status;
|
|
}
|
|
return keyboard.keyboard->close();
|
|
#else
|
|
return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled");
|
|
#endif
|
|
}
|
|
|
|
OperationStatus linux_backend_mouse_fake_open_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_access = true;
|
|
syscalls.override_open = true;
|
|
syscalls.open_result = -1;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateMouseOptions options;
|
|
options.profile = profiles::mouse();
|
|
return backend.create_mouse(1, options).status;
|
|
}
|
|
|
|
OperationStatus linux_backend_mouse_fake_create_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
enable_fake_device_syscalls(syscalls);
|
|
syscalls.fail_libevdev_create = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateMouseOptions options;
|
|
options.profile = profiles::mouse();
|
|
return backend.create_mouse(1, options).status;
|
|
}
|
|
|
|
OperationStatus linux_backend_mouse_fake_fallback_success() {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
LinuxTestSyscalls syscalls;
|
|
enable_fake_device_syscalls(syscalls);
|
|
syscalls.fail_libevdev_create = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateMouseOptions options;
|
|
options.profile = profiles::mouse();
|
|
auto mouse = backend.create_mouse(1, options);
|
|
if (!mouse) {
|
|
return mouse.status;
|
|
}
|
|
return mouse.mouse->close();
|
|
#else
|
|
return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled");
|
|
#endif
|
|
}
|
|
|
|
OperationStatus linux_backend_keyboard_fake_create_failure_without_fallback() {
|
|
LinuxTestSyscalls syscalls;
|
|
enable_fake_device_syscalls(syscalls);
|
|
syscalls.fail_libevdev_create = true;
|
|
syscalls.override_xtest_query = true;
|
|
syscalls.xtest_query_result = false;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateKeyboardOptions options;
|
|
options.profile = profiles::keyboard();
|
|
return backend.create_keyboard(1, options).status;
|
|
}
|
|
|
|
OperationStatus linux_backend_mouse_fake_create_failure_without_fallback() {
|
|
LinuxTestSyscalls syscalls;
|
|
enable_fake_device_syscalls(syscalls);
|
|
syscalls.fail_libevdev_create = true;
|
|
syscalls.override_xtest_query = true;
|
|
syscalls.xtest_query_result = false;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateMouseOptions options;
|
|
options.profile = profiles::mouse();
|
|
return backend.create_mouse(1, options).status;
|
|
}
|
|
|
|
OperationStatus linux_backend_touchscreen_fake_open_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_access = true;
|
|
syscalls.override_open = true;
|
|
syscalls.open_result = -1;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateTouchscreenOptions options;
|
|
options.profile = profiles::touchscreen();
|
|
return backend.create_touchscreen(1, options).status;
|
|
}
|
|
|
|
OperationStatus linux_backend_touchscreen_fake_create_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
enable_fake_device_syscalls(syscalls);
|
|
syscalls.fail_libevdev_create = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateTouchscreenOptions options;
|
|
options.profile = profiles::touchscreen();
|
|
return backend.create_touchscreen(1, options).status;
|
|
}
|
|
|
|
OperationStatus linux_backend_trackpad_fake_open_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_access = true;
|
|
syscalls.override_open = true;
|
|
syscalls.open_result = -1;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateTrackpadOptions options;
|
|
options.profile = profiles::trackpad();
|
|
return backend.create_trackpad(1, options).status;
|
|
}
|
|
|
|
OperationStatus linux_backend_trackpad_fake_create_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
enable_fake_device_syscalls(syscalls);
|
|
syscalls.fail_libevdev_create = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreateTrackpadOptions options;
|
|
options.profile = profiles::trackpad();
|
|
return backend.create_trackpad(1, options).status;
|
|
}
|
|
|
|
OperationStatus linux_backend_pen_tablet_fake_open_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_access = true;
|
|
syscalls.override_open = true;
|
|
syscalls.open_result = -1;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreatePenTabletOptions options;
|
|
options.profile = profiles::pen_tablet();
|
|
return backend.create_pen_tablet(1, options).status;
|
|
}
|
|
|
|
OperationStatus linux_backend_pen_tablet_fake_create_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
enable_fake_device_syscalls(syscalls);
|
|
syscalls.fail_libevdev_create = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
LinuxUhidBackend backend;
|
|
|
|
CreatePenTabletOptions options;
|
|
options.profile = profiles::pen_tablet();
|
|
return backend.create_pen_tablet(1, options).status;
|
|
}
|
|
|
|
OperationStatus linux_uhid_submit_fake_write_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_write = true;
|
|
syscalls.fail_write_call = 1;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
UhidGamepad gamepad {fake_fd};
|
|
return gamepad.submit({}, {0});
|
|
}
|
|
|
|
OperationStatus linux_uhid_submit_fake_short_write() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_write = true;
|
|
syscalls.short_write_call = 1;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
UhidGamepad gamepad {fake_fd};
|
|
return gamepad.submit({}, {0});
|
|
}
|
|
|
|
OperationStatus linux_uhid_close_fake_write_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_write = true;
|
|
syscalls.fail_write_call = 1;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
UhidGamepad gamepad {fake_fd};
|
|
return gamepad.close();
|
|
}
|
|
|
|
OperationStatus linux_uhid_close_fake_close_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_write = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
UhidGamepad gamepad {fake_fd};
|
|
return gamepad.close();
|
|
}
|
|
|
|
OperationStatus linux_uhid_read_loop_fake_retry_branches() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_poll = true;
|
|
syscalls.poll_results = {-1, 0, 1, 1, 1};
|
|
syscalls.poll_revents = {0, 0, 0, POLLIN, POLLIN};
|
|
syscalls.poll_errors = {EINTR};
|
|
syscalls.override_read = true;
|
|
syscalls.read_results = {-1, 0};
|
|
syscalls.read_errors = {EAGAIN};
|
|
return run_fake_uhid_read_loop(syscalls, 5);
|
|
}
|
|
|
|
OperationStatus linux_uhid_read_loop_fake_poll_errors() {
|
|
LinuxTestSyscalls syscall_failure;
|
|
syscall_failure.override_poll = true;
|
|
syscall_failure.poll_results = {-1};
|
|
syscall_failure.poll_errors = {EIO};
|
|
if (const auto status = run_fake_uhid_read_loop(syscall_failure, 1); !status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
LinuxTestSyscalls event_failure;
|
|
event_failure.override_poll = true;
|
|
event_failure.poll_results = {1};
|
|
event_failure.poll_revents = {POLLERR};
|
|
return run_fake_uhid_read_loop(event_failure, 1);
|
|
}
|
|
|
|
OperationStatus linux_uhid_read_loop_fake_read_error() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_poll = true;
|
|
syscalls.poll_results = {1};
|
|
syscalls.poll_revents = {POLLIN};
|
|
syscalls.override_read = true;
|
|
syscalls.read_results = {-1};
|
|
syscalls.read_errors = {EIO};
|
|
return run_fake_uhid_read_loop(syscalls, 1);
|
|
}
|
|
|
|
OperationStatus linux_uhid_read_loop_fake_output_without_callback() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_poll = true;
|
|
syscalls.poll_results = {1, 1};
|
|
syscalls.poll_revents = {POLLIN, POLLIN};
|
|
syscalls.override_read = true;
|
|
syscalls.read_results = {static_cast<std::ptrdiff_t>(sizeof(uhid_event)), 0};
|
|
syscalls.read_event.type = UHID_OUTPUT;
|
|
return run_fake_uhid_read_loop(syscalls, 2);
|
|
}
|
|
|
|
LinuxLibevdevCreationResult linux_uinput_create_fake_libevdev_device(DeviceType device_type) {
|
|
return create_fake_libevdev_device(device_type);
|
|
}
|
|
|
|
LinuxLibevdevCreationResult linux_uinput_create_fake_absolute_mouse_device() {
|
|
return create_fake_libevdev_device(
|
|
DeviceType::mouse,
|
|
keep_fake_libevdev_successful,
|
|
std::nullopt,
|
|
UinputMouseDeviceKind::absolute
|
|
);
|
|
}
|
|
|
|
LinuxLibevdevCreationResult linux_uinput_create_fake_gamepad(GamepadProfileKind kind) {
|
|
return create_fake_libevdev_gamepad(kind);
|
|
}
|
|
|
|
OperationStatus linux_uinput_create_fake_libevdev_allocation_failure(DeviceType device_type) {
|
|
return create_fake_libevdev_device(device_type, [](LinuxTestSyscalls &syscalls) {
|
|
syscalls.libevdev_new_returns_null = true;
|
|
})
|
|
.status;
|
|
}
|
|
|
|
OperationStatus linux_uinput_create_fake_libevdev_event_type_failure(DeviceType device_type) {
|
|
return create_fake_libevdev_device(device_type, [](LinuxTestSyscalls &syscalls) {
|
|
syscalls.fail_libevdev_event_type = true;
|
|
})
|
|
.status;
|
|
}
|
|
|
|
OperationStatus linux_uinput_create_fake_libevdev_event_code_failure(DeviceType device_type) {
|
|
return create_fake_libevdev_device(device_type, [](LinuxTestSyscalls &syscalls) {
|
|
syscalls.fail_libevdev_event_code = true;
|
|
})
|
|
.status;
|
|
}
|
|
|
|
OperationStatus linux_uinput_create_fake_libevdev_property_failure(DeviceType device_type) {
|
|
return create_fake_libevdev_device(device_type, [](LinuxTestSyscalls &syscalls) {
|
|
syscalls.fail_libevdev_property = true;
|
|
})
|
|
.status;
|
|
}
|
|
|
|
OperationStatus linux_uinput_create_fake_libevdev_create_failure(DeviceType device_type) {
|
|
return create_fake_libevdev_device(device_type, [](LinuxTestSyscalls &syscalls) {
|
|
syscalls.fail_libevdev_create = true;
|
|
})
|
|
.status;
|
|
}
|
|
|
|
OperationStatus linux_uinput_keyboard_submit_fake_write_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_write = true;
|
|
syscalls.fail_write_call = 1;
|
|
syscalls.override_ioctl = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
UinputKeyboard keyboard {fake_fd};
|
|
return keyboard.submit({.key_code = 0x41, .pressed = true});
|
|
}
|
|
|
|
OperationStatus linux_uinput_keyboard_submit_fake_short_write() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_write = true;
|
|
syscalls.short_write_call = 1;
|
|
syscalls.override_ioctl = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
UinputKeyboard keyboard {fake_fd};
|
|
return keyboard.submit({.key_code = 0x41, .pressed = true});
|
|
}
|
|
|
|
OperationStatus linux_uinput_keyboard_type_text_fake_success() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_write = true;
|
|
syscalls.override_ioctl = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
UinputKeyboard keyboard {fake_fd};
|
|
return keyboard.type_text({.text = "A"});
|
|
}
|
|
|
|
OperationStatus linux_uinput_keyboard_type_text_fake_write_failure(int fail_write_call) {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_write = true;
|
|
syscalls.fail_write_call = fail_write_call;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
UinputKeyboard keyboard {fake_fd};
|
|
return keyboard.type_text({.text = "A"});
|
|
}
|
|
|
|
OperationStatus linux_uinput_keyboard_close_fake_close_failure() {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_ioctl = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
UinputKeyboard keyboard {fake_fd};
|
|
return keyboard.close();
|
|
}
|
|
|
|
LinuxInputSubmissionResult linux_uinput_keyboard_held_key_events(std::uint32_t interval_ms) {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_write = true;
|
|
syscalls.override_ioctl = true;
|
|
syscalls.override_libevdev = true;
|
|
syscalls.capture_input_events = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
CreateKeyboardOptions options;
|
|
options.profile = profiles::keyboard();
|
|
options.auto_repeat_interval_ms = interval_ms;
|
|
|
|
const auto fd = open_test_fd();
|
|
if (fd < 0) {
|
|
return {system_error_status(ErrorCode::backend_failure, "failed to open test file descriptor", errno), {}};
|
|
}
|
|
|
|
UinputKeyboard keyboard {fd};
|
|
auto status = keyboard.create(1, options);
|
|
if (status.ok()) {
|
|
status = keyboard.submit({.key_code = 0x46, .pressed = true});
|
|
}
|
|
const auto observation_time = std::chrono::milliseconds {std::max(interval_ms * 2, 20U)};
|
|
std::this_thread::sleep_for(observation_time);
|
|
if (status.ok()) {
|
|
status = keyboard.submit({.key_code = 0x46, .pressed = false});
|
|
}
|
|
std::this_thread::sleep_for(observation_time);
|
|
if (const auto close_status = keyboard.close(); status.ok()) {
|
|
status = close_status;
|
|
}
|
|
return {std::move(status), std::move(syscalls.written_input_events)};
|
|
}
|
|
|
|
OperationStatus linux_uinput_mouse_submit_fake_write_failure(const MouseEvent &event) {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_write = true;
|
|
syscalls.fail_write_call = 1;
|
|
syscalls.override_ioctl = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
UinputMouse mouse {fake_fd, fake_fd};
|
|
return mouse.submit(event);
|
|
}
|
|
|
|
OperationStatus linux_uinput_mouse_submit_fake_short_write(const MouseEvent &event) {
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_write = true;
|
|
syscalls.short_write_call = 1;
|
|
syscalls.override_ioctl = true;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
UinputMouse mouse {fake_fd, fake_fd};
|
|
return mouse.submit(event);
|
|
}
|
|
|
|
OperationStatus linux_xtest_keyboard_submit_success() {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
XTestKeyboard keyboard;
|
|
if (const auto status = keyboard.create(); !status.ok()) {
|
|
return status;
|
|
}
|
|
return keyboard.submit({.key_code = 0x41, .pressed = true});
|
|
#else
|
|
return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled");
|
|
#endif
|
|
}
|
|
|
|
OperationStatus linux_xtest_keyboard_submit_compose_fallback() {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_x_keycode = true;
|
|
syscalls.fail_x_keycode_call = 1;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
XTestKeyboard keyboard;
|
|
if (const auto status = keyboard.create(); !status.ok()) {
|
|
return status;
|
|
}
|
|
return keyboard.submit({.key_code = 0x5D, .pressed = true});
|
|
#else
|
|
return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled");
|
|
#endif
|
|
}
|
|
|
|
OperationStatus linux_xtest_keyboard_submit_invalid() {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
XTestKeyboard keyboard;
|
|
if (const auto status = keyboard.create(); !status.ok()) {
|
|
return status;
|
|
}
|
|
return keyboard.submit({.key_code = 0x2C, .pressed = true});
|
|
#else
|
|
return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled");
|
|
#endif
|
|
}
|
|
|
|
OperationStatus linux_xtest_keyboard_submit_closed() {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
XTestKeyboard keyboard;
|
|
if (const auto status = keyboard.create(); !status.ok()) {
|
|
return status;
|
|
}
|
|
static_cast<void>(keyboard.close());
|
|
return keyboard.submit({.key_code = 0x41, .pressed = true});
|
|
#else
|
|
return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled");
|
|
#endif
|
|
}
|
|
|
|
OperationStatus linux_xtest_keyboard_type_text_success() {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
XTestKeyboard keyboard;
|
|
if (const auto status = keyboard.create(); !status.ok()) {
|
|
return status;
|
|
}
|
|
return keyboard.type_text({.text = "A"});
|
|
#else
|
|
return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled");
|
|
#endif
|
|
}
|
|
|
|
OperationStatus linux_xtest_keyboard_type_text_fake_keycode_failure(int fail_keycode_call) {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_x_keycode = true;
|
|
syscalls.fail_x_keycode_call = fail_keycode_call;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
XTestKeyboard keyboard;
|
|
if (const auto status = keyboard.create(); !status.ok()) {
|
|
return status;
|
|
}
|
|
return keyboard.type_text({.text = "A"});
|
|
#else
|
|
static_cast<void>(fail_keycode_call);
|
|
return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled");
|
|
#endif
|
|
}
|
|
|
|
OperationStatus linux_xtest_keyboard_create_query_failure() {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_xtest_query = true;
|
|
syscalls.xtest_query_result = false;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
XTestKeyboard keyboard;
|
|
return keyboard.create();
|
|
#else
|
|
return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled");
|
|
#endif
|
|
}
|
|
|
|
OperationStatus linux_xtest_mouse_submit_success() {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
XTestMouse mouse;
|
|
if (const auto status = mouse.create(); !status.ok()) {
|
|
return status;
|
|
}
|
|
|
|
using enum MouseEventKind;
|
|
if (const auto status = mouse.submit({.kind = relative_motion, .x = 1, .y = -1}); !status.ok()) {
|
|
return status;
|
|
}
|
|
if (const auto status = mouse.submit({.kind = absolute_motion, .x = 1, .y = 1, .width = 2, .height = 2}); !status.ok()) {
|
|
return status;
|
|
}
|
|
if (const auto status = mouse.submit({.kind = button, .button = MouseButton::extra, .pressed = true}); !status.ok()) {
|
|
return status;
|
|
}
|
|
if (const auto status = mouse.submit({.kind = vertical_scroll, .high_resolution_scroll = 120}); !status.ok()) {
|
|
return status;
|
|
}
|
|
return mouse.submit({.kind = horizontal_scroll, .high_resolution_scroll = -120});
|
|
#else
|
|
return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled");
|
|
#endif
|
|
}
|
|
|
|
OperationStatus linux_xtest_mouse_submit_closed() {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
XTestMouse mouse;
|
|
if (const auto status = mouse.create(); !status.ok()) {
|
|
return status;
|
|
}
|
|
static_cast<void>(mouse.close());
|
|
return mouse.submit({.kind = MouseEventKind::relative_motion, .x = 1, .y = 1});
|
|
#else
|
|
return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled");
|
|
#endif
|
|
}
|
|
|
|
OperationStatus linux_xtest_mouse_create_query_failure() {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
LinuxTestSyscalls syscalls;
|
|
syscalls.override_xtest_query = true;
|
|
syscalls.xtest_query_result = false;
|
|
ScopedLinuxTestSyscalls scoped_syscalls {syscalls};
|
|
|
|
XTestMouse mouse;
|
|
return mouse.create();
|
|
#else
|
|
return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled");
|
|
#endif
|
|
}
|
|
|
|
unsigned long linux_xtest_keysym(KeyboardKeyCode key_code) {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
return key_code_to_keysym(key_code);
|
|
#else
|
|
static_cast<void>(key_code);
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
int linux_xtest_mouse_button(MouseButton button) {
|
|
#if defined(LIBVIRTUALHID_HAVE_XTEST)
|
|
return mouse_button_to_xtest(button);
|
|
#else
|
|
static_cast<void>(button);
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
} // namespace lvh::detail::test
|
|
#endif
|