Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/include/libvirtualhid/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,10 @@ namespace lvh {
DeviceProfile profile;

/**
* @brief Held-key repeat interval in milliseconds, or `0` to disable repeat.
* @brief Requested held-key repeat interval in milliseconds, or `0` to disable backend-generated repeat.
*
* The uinput backend ignores this value, including zero, and leaves key
* repeat to the compositor or X server.
*/
std::uint32_t auto_repeat_interval_ms = 50;

Expand Down
62 changes: 5 additions & 57 deletions src/platform/linux/uhid_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
#include <mutex>
#include <numbers>
#include <optional>
#include <set>
#include <span>
#include <sstream>
#include <stop_token>
Expand Down Expand Up @@ -1596,6 +1595,9 @@ namespace lvh::detail {

/**
* @brief Backend keyboard backed by one Linux uinput file descriptor.
*
* Emits only caller-submitted key transitions. The compositor or X server
* handles held-key repeat; replaying presses here can re-press released keys.
*/
class UinputKeyboard final: public BackendKeyboard, private UinputDevice {
public:
Expand All @@ -1608,23 +1610,15 @@ namespace lvh::detail {

OperationStatus create(DeviceId id, const CreateKeyboardOptions &options) {
device_name_ = options.profile.name;
auto status = create_uinput_device(options.profile, id);
if (status.ok() && options.auto_repeat_interval_ms > 0) {
start_repeat_thread(options.auto_repeat_interval_ms);
}
return status;
return create_uinput_device(options.profile, id);
}

OperationStatus submit(const KeyboardEvent &event) override {
if (!is_open()) {
return OperationStatus::failure(ErrorCode::device_closed, "uinput keyboard is closed");
}

auto status = emit_keyboard_event(event);
if (status.ok()) {
update_pressed_keys(event);
}
return status;
return emit_keyboard_event(event);
}

OperationStatus type_text(const KeyboardTextEvent &event) override {
Expand All @@ -1634,7 +1628,6 @@ namespace lvh::detail {
}

OperationStatus close() override {
stop_repeat_thread();
return close_uinput("uinput keyboard");
}

Expand All @@ -1655,52 +1648,7 @@ namespace lvh::detail {
return sync();
}

void update_pressed_keys(const KeyboardEvent &event) {
std::lock_guard lock {pressed_keys_mutex_};
if (event.pressed) {
pressed_keys_.insert(event.key_code);
} else {
pressed_keys_.erase(event.key_code);
}
}

std::vector<KeyboardKeyCode> pressed_keys_snapshot() const {
std::lock_guard lock {pressed_keys_mutex_};
return {pressed_keys_.begin(), pressed_keys_.end()};
}

void start_repeat_thread(std::uint32_t interval_ms) {
repeat_running_ = true;
repeat_thread_ = std::jthread {[this, interval_ms](std::stop_token stop_token) {
while (!stop_token.stop_requested() && repeat_running_) {
std::this_thread::sleep_for(std::chrono::milliseconds {interval_ms});
if (stop_token.stop_requested() || !repeat_running_ || !is_open()) {
break;
}

for (const auto key_code : pressed_keys_snapshot()) {
if (stop_token.stop_requested() || !repeat_running_ || !is_open()) {
break;
}
static_cast<void>(emit_keyboard_event({.key_code = key_code, .pressed = true}));
}
}
}};
}

void stop_repeat_thread() {
repeat_running_ = false;
if (repeat_thread_.joinable()) {
repeat_thread_.request_stop();
repeat_thread_.join();
}
}

std::string device_name_;
std::atomic_bool repeat_running_ = false;
std::jthread repeat_thread_;
mutable std::mutex pressed_keys_mutex_;
std::set<KeyboardKeyCode> pressed_keys_;
};

/**
Expand Down
13 changes: 7 additions & 6 deletions tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
namespace lvh::detail::test {

/**
* @brief Linux input event captured by a pipe-backed backend test.
* @brief Linux input event captured by a uinput backend test.
*/
struct LinuxInputEventRecord {
/**
Expand All @@ -38,7 +38,7 @@ namespace lvh::detail::test {
};

/**
* @brief Result from a pipe-backed uinput submission.
* @brief Result from a captured uinput submission.
*/
struct LinuxInputSubmissionResult {
/**
Expand All @@ -47,7 +47,7 @@ namespace lvh::detail::test {
OperationStatus status;

/**
* @brief Events written to the pipe.
* @brief Events emitted by the backend.
*/
std::vector<LinuxInputEventRecord> events;
};
Expand Down Expand Up @@ -1410,11 +1410,12 @@ namespace lvh::detail::test {
OperationStatus linux_uinput_keyboard_close_fake_close_failure();

/**
* @brief Create a fake uinput keyboard with auto repeat enabled.
* @brief Capture uinput events while holding and releasing a key across repeat intervals.
*
* @return Close status after the repeat thread runs.
* @param interval_ms Requested keyboard repeat interval in milliseconds.
* @return Operation status and events emitted before closing the keyboard.
*/
OperationStatus linux_uinput_keyboard_auto_repeat_fake_success();
LinuxInputSubmissionResult linux_uinput_keyboard_held_key_events(std::uint32_t interval_ms);

/**
* @brief Submit a mouse event while fake write fails.
Expand Down
34 changes: 27 additions & 7 deletions tests/fixtures/linux_backend_test_hooks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
#include <mutex>
#include <numbers>
#include <optional>
#include <set>
#include <span>
#include <sstream>
#include <string>
Expand Down Expand Up @@ -126,6 +125,8 @@ namespace lvh::detail::test {
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;
Expand Down Expand Up @@ -235,6 +236,15 @@ std::ptrdiff_t lvh_linux_test_write(int fd, const std::byte *buffer, std::size_t
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));
Expand Down Expand Up @@ -2831,28 +2841,38 @@ namespace lvh::detail::test {
return keyboard.close();
}

OperationStatus linux_uinput_keyboard_auto_repeat_fake_success() {
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 = 1;
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);
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 = 0x41, .pressed = true});
status = keyboard.submit({.key_code = 0x46, .pressed = true});
}
std::this_thread::sleep_for(std::chrono::milliseconds {10});
return keyboard.close();
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) {
Expand Down
21 changes: 20 additions & 1 deletion tests/unit/test_linux_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,26 @@ TEST_F(LinuxBackendTest, PipeBackedUinputKeyboardEmitsComposeKey) {
}
}

TEST_F(LinuxBackendTest, UinputKeyboardEmitsOnlySubmittedKeyTransitions) {
const auto default_interval = lvh::CreateKeyboardOptions {}.auto_repeat_interval_ms;
for (const auto interval_ms : {0U, 1U, default_interval}) {
SCOPED_TRACE(interval_ms);
const auto result = lvh::detail::test::linux_uinput_keyboard_held_key_events(interval_ms);
ASSERT_TRUE(result.status.ok()) << result.status.message();
ASSERT_EQ(result.events.size(), 4U);
for (std::size_t index = 0; index < 2; ++index) {
const auto &key = result.events[index * 2];
EXPECT_EQ(key.type, EV_KEY);
EXPECT_EQ(key.code, KEY_F);
EXPECT_EQ(key.value, index == 0 ? 1 : 0);
const auto &sync = result.events[index * 2 + 1];
EXPECT_EQ(sync.type, EV_SYN);
EXPECT_EQ(sync.code, SYN_REPORT);
EXPECT_EQ(sync.value, 0);
}
}
}

TEST_F(LinuxBackendTest, PipeBackedUinputGamepadsUseCanonicalLinuxEvents) {
using enum lvh::GamepadButton;
using enum lvh::GamepadProfileKind;
Expand Down Expand Up @@ -1464,7 +1484,6 @@ TEST_F(LinuxBackendTest, FakeUinputConstructionCoversCapabilitiesAndFailureBranc
lvh::ErrorCode::backend_failure
);
}
EXPECT_TRUE(lvh::detail::test::linux_uinput_keyboard_auto_repeat_fake_success().ok());
EXPECT_EQ(
lvh::detail::test::linux_uinput_keyboard_close_fake_close_failure().code(),
lvh::ErrorCode::backend_failure
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/test_linux_consumers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include <cerrno>
#include <fcntl.h>
#include <linux/input.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
Expand Down Expand Up @@ -1285,6 +1286,49 @@ TEST_F(LinuxConsumerTest, LibinputSeesUinputKeyboardKeys) {
EXPECT_EQ(libinput_event_keyboard_get_key_state(keyboard_event), LIBINPUT_KEY_STATE_RELEASED);
}

TEST_F(LinuxConsumerTest, UinputKeyboardKeepsReleasedKeyUpUnderRapidTransitions) {
ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uinput"));

lvh::RuntimeOptions runtime_options;
runtime_options.backend = lvh::BackendKind::platform_default;
auto runtime = lvh::Runtime::create(runtime_options);
lvh::CreateKeyboardOptions options;
options.profile = lvh::profiles::keyboard();
options.profile.name = unique_device_name("Keyboard Release Stress");
options.auto_repeat_interval_ms = 1;
auto created = runtime->create_keyboard(options);
ASSERT_TRUE(created) << created.status.message();

const auto node = wait_for_readable_event_node(options.profile.name);
ASSERT_TRUE(node) << "Keyboard event node was not readable";
const auto descriptor = ::open(node->c_str(), O_RDONLY | O_NONBLOCK | O_CLOEXEC);
ASSERT_GE(descriptor, 0) << errno_message(errno);
ScopeExit close_keyboard {[descriptor, &created]() {
static_cast<void>(created.keyboard->close());
static_cast<void>(::close(descriptor));
}};
ASSERT_EQ(::ioctl(descriptor, EVIOCGRAB, 1), 0) << errno_message(errno);

std::array<unsigned char, KEY_MAX / 8 + 1> keys {};
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds {2};
std::size_t cycles = 0;
do {
ASSERT_TRUE(created.keyboard->press(0x46).ok());
ASSERT_GE(::ioctl(descriptor, EVIOCGKEY(keys.size()), keys.data()), 0) << errno_message(errno);
ASSERT_NE(keys[KEY_F / 8] & (1U << (KEY_F % 8)), 0U);
ASSERT_TRUE(created.keyboard->release(0x46).ok());
ASSERT_GE(::ioctl(descriptor, EVIOCGKEY(keys.size()), keys.data()), 0) << errno_message(errno);
ASSERT_EQ(keys[KEY_F / 8] & (1U << (KEY_F % 8)), 0U)
<< "KEY_F remained pressed after release on cycle " << cycles;
++cycles;
} while (std::chrono::steady_clock::now() < deadline);

std::this_thread::sleep_for(std::chrono::milliseconds {20});
ASSERT_GE(::ioctl(descriptor, EVIOCGKEY(keys.size()), keys.data()), 0) << errno_message(errno);
EXPECT_EQ(keys[KEY_F / 8] & (1U << (KEY_F % 8)), 0U);
EXPECT_TRUE(created.keyboard->close().ok());
}

TEST_F(LinuxConsumerTest, LibinputSeesSplitUinputMouseMotionAndButtons) {
ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uinput"));

Expand Down
Loading