ref:6ca811e2e41c7dfba74f22dfd90aea41ea6e343c

fix(linux): send touches as touches only to VMs with a touch device; review fixes

Touch (REQ-INP-003, #5): QEMU exports MultiTouch on every console, so touches sent to a VM without a multi-touch device were lost unless native_pen_touch was turned off. Sunshine now finds the device on QEMU's command line (/proc/<pid>/cmdline, including libvirt's JSON -device form) or in libvirt's domain XML when it connects, and sends touches as mouse input otherwise or when it can't tell. example.conf no longer needs native_pen_touch = disabled. Review fixes: - mDNS: only capture = qemu advertises sunshine_name; every other capture method keeps the host name as upstream does (REQ-CMP-001). - The backend only builds on Linux; FreeBSD shares linux.cmake but lacks the Linux-only headers and /proc. - QMP connect is bounded by the timeout (SO_SNDTIMEO), so a monitor held by another client no longer hangs Sunshine, and shared_session no longer holds its lock while connecting: concurrent callers share one attempt. - A failed DMABUF descriptor duplicate re-creates the display instead of spinning the capture loop. - The packaging test is registered only when the backend is built. Refs #3 #5 #6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPNw4PCgkEfhyCjQT19wsb
SHA: 6ca811e2e41c7dfba74f22dfd90aea41ea6e343c
Author: Cole Christensen <cole.christensen@gmail.com>
Date: 2026-09-13 10:16
Parents: 2398a0a
22 files changed +441 -33
Type
cmake/compile_definitions/linux.cmake +3 −1
@@ -288,7 +288,7 @@
# QEMU D-Bus display (capture = qemu)
set(QEMU_FOUND OFF)
if(${SUNSHINE_ENABLE_QEMU})
if(${SUNSHINE_ENABLE_QEMU} AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
pkg_check_modules(QEMU_GIO gio-2.0>=2.64 gio-unix-2.0>=2.64)
find_program(GDBUS_CODEGEN_EXECUTABLE gdbus-codegen)
if(QEMU_GIO_FOUND AND GDBUS_CODEGEN_EXECUTABLE)
@@ -339,6 +339,8 @@
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/pixel_format.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/render_node.h"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/render_node.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/vm_devices.h"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/vm_devices.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/session.h"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/session.cpp"
"${CMAKE_SOURCE_DIR}/third-party/qemu-keycodemapdb/linux_to_qnum.cpp"
cmake/prep/options.cmake +1 −1
@@ -77,5 +77,5 @@
option(SUNSHINE_ENABLE_PORTAL
"Enable XDG portal grab if available" ON)
option(SUNSHINE_ENABLE_QEMU
"Enable QEMU D-Bus display capture (capture = qemu) if available" ON)
"Enable QEMU D-Bus display capture (capture = qemu) if available; Linux only" ON)
endif()
docs/configuration.md +9 −8
@@ -149,10 +149,10 @@
<tr>
<td>Description</td>
<td colspan="2">
The name displayed by Moonlight. On Linux it is also the name advertised over mDNS, so several
The name displayed by Moonlight. On Linux with [capture](#capture) = `qemu` it is also the name
advertised over mDNS, so the Sunshine instances of several VMs on one host are listed separately;
there it may be any text up to 63 bytes (control characters become spaces), and should differ,
ignoring case, between instances on the same network. Otherwise the host name is advertised.
Sunshine instances on one host are listed separately; there it may be any text up to 63 bytes
(control characters become spaces), and should differ, ignoring case, between instances on the
same network. When it is left at the host name, the host name is advertised as before.
</td>
</tr>
<tr>
@@ -2240,10 +2240,11 @@
(gamepads still use virtual host devices). An absolute pointer such as `-device virtio-tablet-pci`
works best; with a relative mouse (PS/2) Moonlight's absolute pointer positions only work while the
guest shows a hardware cursor. Horizontal scrolling and text input aren't supported.
Touches reach the VM as touches only when it has `-device virtio-multitouch-pci` (libvirt:
Native touch (see [native_pen_touch](#native_pen_touch)) needs `-device virtio-multitouch-pci` in the VM;
QEMU then sends mouse buttons to the touch device and drops wheel events, so add it only for touch-first
guests. QEMU can't tell Sunshine whether that device exists: for a VM without it, disable
native_pen_touch so Moonlight sends touches as mouse input. Pens always move the mouse.
`<input type='touch' bus='virtio'/>`), which Sunshine finds on QEMU's command line or in the domain
XML; otherwise, or when it can't tell, they move and click the mouse. QEMU sends mouse buttons to a
multi-touch device and drops wheel events, so add it only for touch-first guests. Pens always move
the mouse.
@note{Applies to Linux only.}</td>
</tr>
<tr>
docs/qemu.md +5 −5
@@ -168,6 +168,6 @@
web UI fails; edit `/etc/sunshine-qemu/<vm>.conf` as root and restart the unit.
The unit runs `sunshine /etc/sunshine-qemu/<vm>.conf` as `sunshine-qemu` with its state in
`/var/lib/sunshine-qemu/<vm>`, restarts it on failure, and has `After=` on libvirt and Avahi. It uses
`/var/lib/sunshine-qemu/<escaped vm>` (see `systemd-escape` above), restarts it on failure, and has `After=` on libvirt and Avahi. It uses
`PrivateTmp=yes` and `ProtectHome=yes`, so keep sockets Sunshine connects to under `/run`, not `/tmp` or home
directories.
@@ -334,9 +334,9 @@
Sunshine sends absolute positions when QEMU reports an absolute pointer (`IsAbsolute`). A VM with only a PS/2 mouse is
relative: Moonlight positions then only work while the guest shows a hardware cursor. Add a virtio or USB tablet.
QEMU offers native touch on every VM, but only a VM with `-device virtio-multitouch-pci` receives the touches, and
Sunshine can't tell the difference. So `example.conf` disables [native_pen_touch](configuration.md#native_pen_touch)
and Moonlight sends touches as mouse input; enable it for VMs with a multi-touch device. If touches do nothing, the
VM lacks that device and `native_pen_touch` is enabled.
QEMU offers native touch on every VM, but only a VM with `-device virtio-multitouch-pci` (libvirt:
`<input type='touch' bus='virtio'/>`) receives the touches. Sunshine looks for that device on QEMU's command line or
in the domain XML and otherwise sends touches as mouse input; the log says which route it took. A device added later
with `device_add` isn't seen, so touches then still move the mouse.
### No sound
packaging/linux/qemu/example.conf +0 −5
@@ -38,10 +38,5 @@
# Graphical console to stream, by id or label; empty streams the first one.
# output_name = 0
# Native touch goes to QEMU's multi-touch interface, which every VM has but only a VM with
# -device virtio-multitouch-pci turns into touches; elsewhere they are lost. Disabled, Moonlight sends
# touches as mouse input. Enable it for VMs with a multi-touch device.
native_pen_touch = disabled
# Several instances on one host must not fight over router port mappings.
upnp = disabled
src/network.cpp +2 −1
@@ -299,6 +299,7 @@
std::string mdns_service_name() {
const auto &name = config::nvhttp.sunshine_name;
const auto host_name = platf::get_host_name();
if (name.empty() || name == host_name) {
// only one Sunshine per VM (capture = qemu) advertises sunshine_name; everyone else keeps the host-name rules
if (config::video.capture != "qemu" || name.empty() || name == host_name) {
return mdns_instance_name(host_name);
}
src/network.h +3 −2
@@ -158,8 +158,9 @@
/**
* @brief Returns the mDNS service instance name this Sunshine advertises.
* @details Uses `sunshine_name`, so several Sunshine instances on one host (for example one per
* VM) are listed separately instead of colliding on the host name. The name is kept as UTF-8, as
* @details With `capture = qemu`, uses `sunshine_name`, so the Sunshine instances of several VMs on
* one host are listed separately instead of colliding on the host name. With any other capture
* method the host-name rules of `mdns_instance_name()` apply, as before. The name is kept as UTF-8, as
* DNS-SD allows (RFC 6763 section 4.1.1): control characters become spaces, surrounding spaces
* are removed, and it is cut to 63 bytes. When `sunshine_name` is empty or the default (the
* host name), or isn't valid UTF-8, the host-name rules of `mdns_instance_name()` apply.
src/platform/linux/qemu/frame_store.cpp +2 −1
@@ -443,7 +443,8 @@
copy.fds[plane] = fd_t {fcntl(dmabuf.fds[plane].get(), F_DUPFD_CLOEXEC, 3)};
if (copy.fds[plane].get() < 0) {
BOOST_LOG(error) << "qemu: couldn't duplicate DMABUF descriptor: "sv << std::strerror(errno);
return frame_status_e::unchanged;
// `unchanged` would leave the sequence behind and wake the capture loop again at once
return frame_status_e::size_changed;
}
}
copy.num_planes = dmabuf.num_planes;
src/platform/linux/qemu/input.cpp +10 −2
@@ -581,14 +581,22 @@
// Sunshine normalizes touch coordinates to the captured display
const double x = std::clamp(touch.x, 0.0f, 1.0f) * (double) device->width();
const double y = std::clamp(touch.y, 0.0f, 1.0f) * (double) device->height();
if (!device->has_multitouch() || device->max_slots() <= 0) {
// QEMU exports MultiTouch on every console; a VM without a multi-touch device would drop the touches
const auto touch_device = device->has_touch_device();
if (!device->has_multitouch() || device->max_slots() <= 0 || touch_device != true) {
if (!logged_touch) {
logged_touch = true;
BOOST_LOG(info) << "qemu: sending touches as mouse input: "sv
<< (touch_device == false ? "the VM has no multi-touch device (-device virtio-multitouch-pci)"sv : !device->has_multitouch() ? "QEMU has no MultiTouch interface"sv :
"can't tell whether the VM has a multi-touch device"sv);
}
emulate_mouse_locked(*device, touch.pointerId, touch.eventType, x, y);
return;
}
if (!logged_touch) {
logged_touch = true;
BOOST_LOG(info) << "qemu: sending touches to the VM's multi-touch device"sv;
BOOST_LOG(info) << "qemu: sending touch to QEMU's MultiTouch interface; the VM needs -device virtio-multitouch-pci, otherwise set native_pen_touch = disabled"sv;
}
const auto contact = touch_contacts.find(touch.pointerId);
switch (touch.eventType) {
src/platform/linux/qemu/input.h +1 −1
@@ -317,6 +317,6 @@
bool logged_hscroll {false}; ///< Whether horizontal scrolling was logged as unsupported.
bool logged_unicode {false}; ///< Whether text input was logged as unsupported.
bool logged_no_pointer {false}; ///< Whether a dropped absolute move for a relative guest was logged.
bool logged_touch {false}; ///< Whether the multi-touch device hint was logged.
bool logged_touch {false}; ///< Whether the touch route (multi-touch device or mouse) was logged.
};
} // namespace qemu
src/platform/linux/qemu/p2p.cpp +12 −1
@@ -5,7 +5,10 @@
// class header include
#include "p2p.h"
#include "vm_devices.h"
// standard includes
#include <algorithm>
#include <atomic>
#include <cctype>
#include <cerrno>
@@ -19,5 +22,6 @@
#include <dlfcn.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/un.h>
#include <unistd.h>
@@ -77,8 +81,14 @@
}
addr.sun_family = AF_UNIX;
std::memcpy(addr.sun_path, path.c_str(), path.size());
// QEMU accepts one monitor client at a time; while another is attached the listen queue fills and
// connect() would wait for it forever, so bound the wait (Linux honors SO_SNDTIMEO for AF_UNIX connect)
const auto ms = std::max<std::int64_t>(1, timeout.count());
timeval send_timeout {(time_t) (ms / 1000), (suseconds_t) ((ms % 1000) * 1000)};
setsockopt(socket.get(), SOL_SOCKET, SO_SNDTIMEO, &send_timeout, sizeof(send_timeout));
if (::connect(socket.get(), (sockaddr *) &addr, sizeof(addr)) != 0) {
error = "couldn't connect to QMP socket [" + path + "]: " + errno_text();
const bool timed_out = errno == EAGAIN || errno == EWOULDBLOCK || errno == ETIMEDOUT;
error = "couldn't connect to QMP socket [" + path + "]: " + (timed_out ? "timed out; is another client attached to the monitor?"s : errno_text());
return false;
}
return true;
@@ -471,6 +481,7 @@
}
libvirt_display_t display;
display.touch_device = domain_xml_has_touch_device(xml.get());
if (!graphics->p2p) {
if (graphics->address.empty()) {
error = "domain [" + domain + "] has <graphics type='dbus'> without p2p='yes' and no bus address; is it running?";
src/platform/linux/qemu/p2p.h +1 −0
@@ -103,6 +103,7 @@
struct libvirt_display_t {
std::optional<peer_socket_t> peer; ///< Socket from `virDomainOpenGraphicsFD`, for `p2p='yes'`.
std::string bus_address; ///< Bus address from the live domain XML, for bus mode.
std::optional<bool> touch_device; ///< Whether the domain has an `<input type='touch'>` device.
};
/**
src/platform/linux/qemu/session.cpp +68 −4
@@ -27,7 +27,9 @@
// local includes
#include "p2p.h"
#include "render_node.h"
#include "src/logging.h"
#include "vm_devices.h"
// generated includes
extern "C" {
@@ -430,6 +432,8 @@
return slots;
}
std::optional<bool> has_touch_device() const override;
std::uint32_t width() const override {
return console_width;
}
@@ -573,8 +577,32 @@
bool alive() const override {
return is_alive;
}
std::optional<bool> touch_device() const override {
std::lock_guard lock {touch_mutex};
return touch;
}
void set_touch_device(std::optional<bool> present) override {
std::lock_guard lock {touch_mutex};
touch = present;
}
/**
* @brief Look for a multi-touch device on QEMU's command line.
* @details Runs once after connecting, so input never waits for the process id lookup.
*/
void detect_touch_device() {
std::optional<bool> present;
if (const auto pid = qemu_pid()) {
if (const auto args = read_cmdline(*pid); !args.empty()) {
present = cmdline_has_touch_device(args);
}
}
set_touch_device(present);
}
std::optional<std::uint32_t> qemu_pid() const override {
if (peer_to_peer) {
return peer_pid;
@@ -936,6 +964,8 @@
std::atomic<bool> peer_to_peer {false}; ///< Whether the connection is a peer-to-peer `add_client` connection.
std::optional<std::uint32_t> peer_pid; ///< QEMU's process id given for a peer-to-peer connection.
mutable std::mutex touch_mutex; ///< Guards `touch`.
std::optional<bool> touch; ///< Whether the VM has a multi-touch device, when known.
std::chrono::milliseconds timeout; ///< Deadline for connection setup and registration.
int timeout_ms; ///< Timeout applied to each D-Bus call.
std::atomic<bool> is_alive {false}; ///< Whether QEMU is reachable.
@@ -1024,6 +1054,10 @@
return session->alive();
}
std::optional<bool> console_input_impl_t::has_touch_device() const {
return session->touch_device();
}
void console_input_impl_t::read_properties() {
console_width = qemu_dbus_display1_console_get_width(console);
console_height = qemu_dbus_display1_console_get_height(console);
@@ -1503,6 +1537,7 @@
std::shared_ptr<session_t> session_t::connect(const std::string &address, std::chrono::milliseconds timeout) {
std::string reason;
std::optional<bool> touch_hint;
auto parsed = parse_display_address(address, reason);
if (!parsed) {
BOOST_LOG(error) << "qemu: "sv << reason;
@@ -1527,10 +1562,15 @@
return nullptr;
}
if (display->peer) {
return connect_peer(std::move(display->peer->fd), timeout, display->peer->qemu_pid);
auto session = connect_peer(std::move(display->peer->fd), timeout, display->peer->qemu_pid);
if (session && display->touch_device) {
session->set_touch_device(display->touch_device);
}
return session;
}
BOOST_LOG(info) << "qemu: "sv << describe(*parsed) << " uses the bus ["sv << display->bus_address << ']';
parsed->bus = display->bus_address;
touch_hint = display->touch_device;
break;
}
case display_address_t::kind_e::bus:
@@ -1541,5 +1581,9 @@
if (!session->connect_bus(parsed->bus)) {
return nullptr;
}
session->detect_touch_device();
if (touch_hint) {
session->set_touch_device(touch_hint);
}
return session;
}
@@ -1549,5 +1593,6 @@
if (!session->connect_peer(std::move(fd), qemu_pid)) {
return nullptr;
}
session->detect_touch_device();
return session;
}
@@ -1556,14 +1601,33 @@
static std::mutex mutex;
static std::weak_ptr<session_t> cached;
static std::string cached_address;
static std::map<std::string, std::shared_future<std::shared_ptr<session_t>>> connecting;
std::lock_guard lock {mutex};
std::unique_lock lock {mutex};
if (auto session = cached.lock(); session && session->alive() && cached_address == address) {
return session;
}
if (const auto pending = connecting.find(address); pending != connecting.end()) {
// capture, audio and input connect at the same time; share one attempt instead of queuing
auto attempt = pending->second;
lock.unlock();
return attempt.get();
}
std::promise<std::shared_ptr<session_t>> promise;
connecting.emplace(address, promise.get_future().share());
lock.unlock();
// connecting can take up to the timeout (QMP, libvirt, a slow bus), so other callers aren't blocked meanwhile
auto session = session_t::connect(address);
cached = session;
cached_address = address;
lock.lock();
if (session) {
cached = session;
cached_address = address;
}
connecting.erase(address);
lock.unlock();
promise.set_value(session);
return session;
}
src/platform/linux/qemu/session.h +26 −0
@@ -397,6 +397,15 @@
[[nodiscard]] virtual int max_slots() const = 0;
/**
* @brief Report whether the VM has a multi-touch input device.
* @details QEMU exports `MultiTouch` on every console, but only a VM with a multi-touch device turns
* the events into touches. See `session_t::touch_device()`.
*
* @return True or false when known, nothing when Sunshine can't tell.
*/
[[nodiscard]] virtual std::optional<bool> has_touch_device() const = 0;
/**
* @brief Current console width, from the `Console.Width` property.
*
* @return Width in pixels.
@@ -526,6 +535,23 @@
* @return Process id, or nothing when the bus doesn't know it.
*/
[[nodiscard]] virtual std::optional<std::uint32_t> qemu_pid() const = 0;
/**
* @brief Report whether the VM has a multi-touch input device.
* @details Found when connecting: from QEMU's command line (`-device virtio-multitouch-*`) when its
* process id is known, or from libvirt's domain XML (`<input type='touch'>`). Devices hot-plugged
* later aren't seen.
*
* @return True or false when known, nothing when neither source was available.
*/
[[nodiscard]] virtual std::optional<bool> touch_device() const = 0;
/**
* @brief Override what `touch_device()` reports.
*
* @param present Whether the VM has a multi-touch device, or nothing for unknown.
*/
virtual void set_touch_device(std::optional<bool> present) = 0;
/**
* @brief Register a display listener on a console.
src/platform/linux/qemu/vm_devices.cpp +74 −0
@@ -1,0 +1,74 @@
/**
* @file src/platform/linux/qemu/vm_devices.cpp
* @brief Definitions for finding which input devices a VM has.
*/
// class header include
#include "vm_devices.h"
// standard includes
#include <cctype>
#include <regex>
using namespace std::literals;
namespace qemu {
namespace {
/**
* @brief Extract the driver name from a `-device` value.
*
* @param value `driver[,opts]`, `driver=name[,opts]`, or a JSON object.
* @return Driver name, or empty when none is found.
*/
std::string device_driver(const std::string &value) {
if (!value.empty() && value.front() == '{') {
static const std::regex json_driver {R"re("driver"\s*:\s*"([^"]*)")re"};
std::smatch match;
return std::regex_search(value, match, json_driver) ? match[1].str() : std::string {};
}
std::string_view first {value};
first = first.substr(0, first.find(','));
if (first.starts_with("driver="sv)) {
first.remove_prefix("driver="sv.size());
}
return std::string {first};
}
/**
* @brief Report whether a device driver is a multi-touch input device.
*
* @param driver Driver name.
* @return True for QEMU's virtio multi-touch devices.
*/
bool is_touch_driver(std::string_view driver) {
return driver.starts_with("virtio-multitouch"sv);
}
} // namespace
bool cmdline_has_touch_device(const std::vector<std::string> &args) {
for (std::size_t i = 0; i < args.size(); ++i) {
std::string_view arg {args[i]};
if (arg.starts_with('-')) {
arg.remove_prefix(arg.starts_with("--"sv) ? 2 : 1);
} else {
continue;
}
if (arg == "device"sv) {
if (i + 1 < args.size() && is_touch_driver(device_driver(args[i + 1]))) {
return true;
}
} else if (arg.starts_with("device="sv)) {
if (is_touch_driver(device_driver(std::string {arg.substr("device="sv.size())}))) {
return true;
}
}
}
return false;
}
bool domain_xml_has_touch_device(std::string_view domain_xml) {
static const std::regex touch_input {R"re(<input\s[^>]*\btype\s*=\s*['"]touch['"])re"};
return std::regex_search(domain_xml.begin(), domain_xml.end(), touch_input);
}
} // namespace qemu
src/platform/linux/qemu/vm_devices.h +34 −0
@@ -1,0 +1,34 @@
/**
* @file src/platform/linux/qemu/vm_devices.h
* @brief Declarations for finding which input devices a VM has.
* @details QEMU's D-Bus display exports `org.qemu.Display1.MultiTouch` on every console, whether or
* not the VM has a multi-touch device, so touches sent to a VM without one are lost. The device is
* found in QEMU's command line or in libvirt's domain XML instead.
*/
#pragma once
// standard includes
#include <string>
#include <string_view>
#include <vector>
namespace qemu {
/**
* @brief Report whether a QEMU command line adds a multi-touch device.
* @details Matches `-device virtio-multitouch-pci` and `virtio-multitouch-device`, given as a
* driver name, a `driver=` option, or libvirt's JSON form. Devices added later with `device_add`
* are not seen.
*
* @param args QEMU arguments, including the program name.
* @return True when a `-device` argument uses a multi-touch driver.
*/
bool cmdline_has_touch_device(const std::vector<std::string> &args);
/**
* @brief Report whether a libvirt domain has a touch input device.
*
* @param domain_xml Output of `virDomainGetXMLDesc`.
* @return True when the domain has an `<input type='touch'>` element.
*/
bool domain_xml_has_touch_device(std::string_view domain_xml);
} // namespace qemu
tests/CMakeLists.txt +1 −1
@@ -263,7 +263,7 @@
# systemd units, libvirt hook and helper scripts for per-VM instances (REQ-DEP-001, REQ-DEP-002):
# ctest --test-dir <build>/tests -R sunshine_qemu_packaging; set SUNSHINE_QEMU_PKG_REQUIRE_TOOLS=1 in CI so
# missing systemd-analyze or shellcheck fails instead of skipping
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND SUNSHINE_ENABLE_QEMU)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND QEMU_FOUND)
find_program(BASH_EXECUTABLE bash)
if(BASH_EXECUTABLE)
add_test(NAME sunshine_qemu_packaging
tests/unit/platform/linux/qemu/test_input.cpp +40 −0
@@ -93,6 +93,22 @@
}
/**
* @brief Share the input's session and set whether the VM has a multi-touch device.
* @details The fake QEMU runs in the test process, whose command line has no such device.
*
* @param present Value for `session_t::set_touch_device()`.
* @return The session; keep it so the input uses the same one.
*/
std::shared_ptr<qemu::session_t> with_touch_device(std::optional<bool> present) {
auto session = qemu::shared_session(bus->address());
EXPECT_NE(session, nullptr);
if (session) {
session->set_touch_device(present);
}
return session;
}
/**
* @brief Create the input and wait until it reaches a console.
*/
void connect_input() {
@@ -423,6 +439,7 @@
// @tag requirements: [REQ-INP-003]
TEST_F(QemuInputTest, TouchContactsGetTheLowestFreeSlot) {
const auto session = with_touch_device(true);
connect_input();
input->touch_update(port_1280x800, touch_event(LI_TOUCH_EVENT_DOWN, 7, 0.5f, 0.25f));
@@ -439,6 +456,7 @@
// @tag requirements: [REQ-INP-003]
TEST_F(QemuInputTest, TouchContactsBeyondMaxSlotsAreDropped) {
const auto session = with_touch_device(true);
connect_input();
for (std::uint32_t id = 0; id < 11; ++id) {
@@ -465,5 +483,25 @@
}
// @tag requirements: [REQ-INP-003]
TEST_F(QemuInputTest, TouchDrivesTheMouseWhenTheVmHasNoTouchDevice) {
const auto session = with_touch_device(false); // MultiTouch is exported, as QEMU always does
connect_input();
input->touch_update(port_640x400, touch_event(LI_TOUCH_EVENT_DOWN, 1, 0.5f, 0.5f));
input->touch_update(port_640x400, touch_event(LI_TOUCH_EVENT_UP, 1, 0.5f, 0.5f));
EXPECT_EQ(calls(), (std::vector<std::string> {"abs 320 200", "button press 0", "abs 320 200", "button release 0"}));
}
// @tag requirements: [REQ-INP-003]
TEST_F(QemuInputTest, TouchDrivesTheMouseWhenTheTouchDeviceIsUnknown) {
const auto session = with_touch_device(std::nullopt);
connect_input();
input->touch_update(port_640x400, touch_event(LI_TOUCH_EVENT_DOWN, 1, 0.25f, 0.5f));
input->touch_update(port_640x400, touch_event(LI_TOUCH_EVENT_UP, 1, 0.25f, 0.5f));
EXPECT_EQ(calls(), (std::vector<std::string> {"abs 160 200", "button press 0", "abs 160 200", "button release 0"}));
}
// @tag requirements: [REQ-INP-003]
TEST_F(QemuInputTest, PenDrivesTheMouse) {
connect_input();
@@ -482,6 +520,7 @@
// @tag requirements: [REQ-INP-001, REQ-INP-002, REQ-INP-003]
TEST_F(QemuInputTest, StreamingStopReleasesEverythingHeldInTheGuest) {
const auto session = with_touch_device(true);
connect_input();
input->keyboard_update(vk_lshift, false, 0);
@@ -549,6 +588,7 @@
// @tag requirements: [REQ-INP-001, REQ-INP-003, REQ-CMP-001]
TEST_F(QemuInputTest, PlatformInputDispatchesToQemuOnlyWithQemuCapture) {
const auto session = with_touch_device(true);
{
auto platform_input = platf::input();
ASSERT_TRUE(platform_input);
tests/unit/platform/linux/qemu/test_p2p.cpp +73 −0
@@ -518,6 +518,66 @@
}
// @tag requirements: [REQ-DEP-002]
TEST_F(QemuP2pTest, QmpAddClientTimesOutWhileAnotherClientHoldsTheMonitor) {
// a monitor that never accepts: after its queue is full, connect() waits for the listener
const auto path = (dir / "held.sock").string();
const int listener = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
ASSERT_GE(listener, 0);
sockaddr_un addr {};
addr.sun_family = AF_UNIX;
ASSERT_LT(path.size(), sizeof(addr.sun_path));
std::strncpy(addr.sun_path, path.c_str(), sizeof(addr.sun_path) - 1);
ASSERT_EQ(bind(listener, (sockaddr *) &addr, sizeof(addr)), 0);
ASSERT_EQ(listen(listener, 0), 0);
std::vector<int> queued;
for (int i = 0; i < 4; ++i) {
const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (connect(fd, (sockaddr *) &addr, sizeof(addr)) != 0) {
close(fd);
break;
}
queued.push_back(fd);
}
ASSERT_FALSE(queued.empty());
std::string error;
const auto start = std::chrono::steady_clock::now();
EXPECT_FALSE(qemu::qmp_add_client(path, 300ms, error));
EXPECT_LT(std::chrono::steady_clock::now() - start, 3s);
EXPECT_NE(error.find("held.sock"), std::string::npos) << error;
for (const int fd : queued) {
close(fd);
}
close(listener);
}
// @tag requirements: [REQ-DEP-002]
TEST_F(QemuP2pTest, SharedSessionCallersShareOneConnectionAttempt) {
fake_qmp_t qmp {(dir / "shared.sock").string(), fake.get()};
const auto address = "qmp:" + qmp.path;
std::atomic<int> started {0};
std::vector<std::shared_ptr<qemu::session_t>> sessions(4);
std::vector<std::thread> threads;
for (std::size_t i = 0; i < sessions.size(); ++i) {
threads.emplace_back([&, i]() {
started += 1;
sessions[i] = qemu::shared_session(address);
});
}
for (auto &thread : threads) {
thread.join();
}
ASSERT_NE(sessions[0], nullptr);
for (const auto &session : sessions) {
EXPECT_EQ(session, sessions[0]);
}
EXPECT_EQ(started, 4);
// QEMU serves one monitor client at a time; concurrent callers must not each add a display client
EXPECT_EQ(fake->clients(), 1);
}
// @tag requirements: [REQ-DEP-002]
TEST(QemuLibvirtTest, FindsTheDbusGraphicsDeviceInDomainXml) {
auto p2p = qemu::find_dbus_graphics(p2p_domain_xml);
ASSERT_TRUE(p2p);
@@ -572,9 +632,22 @@
ASSERT_TRUE(display) << error;
EXPECT_FALSE(display->peer);
EXPECT_EQ(display->bus_address, "unix:path=/run/user/1000/libvirt/qemu/run/dbus/4-desktop-dbus.sock");
EXPECT_EQ(display->touch_device, false);
EXPECT_EQ(std::string {libvirt.last_uri()}, "(null)");
EXPECT_EQ(libvirt.last_index(), -1);
EXPECT_EQ(libvirt.open_handles(), 0);
}
// @tag requirements: [REQ-DEP-002, REQ-INP-003]
TEST(QemuLibvirtTest, ReportsTheDomainsTouchDevice) {
fake_libvirt_t libvirt;
ASSERT_NE(libvirt.setup, nullptr) << "couldn't load " << fake_libvirt_path();
libvirt.setup("tablet", "<domain><devices><input type='touch' bus='virtio'/><graphics type='dbus' address='unix:path=/run/user/1000/tablet.sock'/></devices></domain>", nullptr, nullptr);
std::string error;
auto display = qemu::libvirt_open_display("tablet", "", error, fake_libvirt_path());
ASSERT_TRUE(display) << error;
EXPECT_EQ(display->touch_device, true);
}
// @tag requirements: [REQ-DEP-002]
tests/unit/platform/linux/qemu/test_session.cpp +15 −0
@@ -202,6 +202,21 @@
EXPECT_NE(std::ranges::find(vm.consoles[1].interfaces, "org.qemu.Display1.Keyboard"), vm.consoles[1].interfaces.end());
}
// @tag requirements: [REQ-INP-003]
TEST_F(QemuSessionTest, FindsTheTouchDeviceOnQemusCommandLine) {
start_fake();
auto session = qemu::session_t::connect(bus->address());
ASSERT_NE(session, nullptr);
// the fake QEMU is this test process, whose command line has no multi-touch device
ASSERT_TRUE(session->qemu_pid().has_value());
EXPECT_EQ(session->touch_device(), false);
session->set_touch_device(true);
EXPECT_EQ(session->touch_device(), true);
session->set_touch_device(std::nullopt);
EXPECT_FALSE(session->touch_device().has_value());
}
// @tag requirements: [REQ-CAP-001]
TEST_F(QemuSessionTest, ConnectFailsWhenQemuDoesNotOwnTheName) {
auto start = std::chrono::steady_clock::now();
tests/unit/platform/linux/qemu/test_vm_devices.cpp +43 −0
@@ -1,0 +1,43 @@
/**
* @file tests/unit/platform/linux/qemu/test_vm_devices.cpp
* @brief Test finding a VM's touch device in its QEMU command line or libvirt domain XML.
*/
#ifdef SUNSHINE_BUILD_QEMU
// test includes
#include "../../../../tests_common.h"
// local includes
#include <src/platform/linux/qemu/vm_devices.h>
// @tag requirements: [REQ-INP-003]
TEST(QemuVmDevicesTest, FindsMultitouchDeviceOnTheCommandLine) {
EXPECT_TRUE(qemu::cmdline_has_touch_device({"qemu-system-x86_64", "-device", "virtio-multitouch-pci"}));
EXPECT_TRUE(qemu::cmdline_has_touch_device({"qemu-system-x86_64", "-device", "virtio-multitouch-pci,id=touch0,bus=pcie.1"}));
EXPECT_TRUE(qemu::cmdline_has_touch_device({"qemu-system-aarch64", "-device", "driver=virtio-multitouch-device,id=t"}));
EXPECT_TRUE(qemu::cmdline_has_touch_device({"qemu-system-x86_64", "--device", "virtio-multitouch-pci"}));
EXPECT_TRUE(qemu::cmdline_has_touch_device({"qemu-system-x86_64", "-device=virtio-multitouch-pci"}));
// libvirt passes devices as JSON
EXPECT_TRUE(qemu::cmdline_has_touch_device({"/usr/bin/qemu-system-x86_64", "-device", R"({"driver":"virtio-multitouch-pci","id":"input2","bus":"pci.0","addr":"0x5"})"}));
EXPECT_TRUE(qemu::cmdline_has_touch_device({"/usr/bin/qemu-system-x86_64", "-device", R"({ "id": "input2", "driver" : "virtio-multitouch-pci" })"}));
}
// @tag requirements: [REQ-INP-003]
TEST(QemuVmDevicesTest, IgnoresOtherDevicesAndMentionsElsewhere) {
EXPECT_FALSE(qemu::cmdline_has_touch_device({}));
EXPECT_FALSE(qemu::cmdline_has_touch_device({"qemu-system-x86_64", "-device", "virtio-tablet-pci", "-device", "virtio-keyboard-pci"}));
EXPECT_FALSE(qemu::cmdline_has_touch_device({"qemu-system-x86_64", "-name", "virtio-multitouch-pci"}));
EXPECT_FALSE(qemu::cmdline_has_touch_device({"qemu-system-x86_64", "-device", "virtio-tablet-pci,id=virtio-multitouch-pci"}));
EXPECT_FALSE(qemu::cmdline_has_touch_device({"qemu-system-x86_64", "-device", R"({"driver":"virtio-tablet-pci","id":"virtio-multitouch-pci"})"}));
EXPECT_FALSE(qemu::cmdline_has_touch_device({"qemu-system-x86_64", "-device"})); // truncated
}
// @tag requirements: [REQ-INP-003]
TEST(QemuVmDevicesTest, FindsTouchInputInDomainXml) {
EXPECT_TRUE(qemu::domain_xml_has_touch_device(R"(<domain><devices><input type='touch' bus='virtio'/></devices></domain>)"));
EXPECT_TRUE(qemu::domain_xml_has_touch_device(R"(<domain><devices><input type="touch" bus="virtio"><alias name="input2"/></input></devices></domain>)"));
EXPECT_FALSE(qemu::domain_xml_has_touch_device(R"(<domain><devices><input type='tablet' bus='usb'/><input type='mouse' bus='ps2'/></devices></domain>)"));
EXPECT_FALSE(qemu::domain_xml_has_touch_device(R"(<domain><description>input type='touch'</description></domain>)"));
EXPECT_FALSE(qemu::domain_xml_has_touch_device(R"(<domain><devices><inputs type='touch'/></devices></domain>)"));
EXPECT_FALSE(qemu::domain_xml_has_touch_device(""));
}
#endif
tests/unit/test_network.cpp +18 −0
@@ -35,6 +35,8 @@
// @tag requirements: [REQ-DEP-001]
TEST(MdnsServiceNameTest, FollowsSunshineNameSoInstancesOnOneHostDiffer) {
const auto saved = config::nvhttp.sunshine_name;
const auto saved_capture = config::video.capture;
config::video.capture = "qemu";
config::nvhttp.sunshine_name = "sq-vm-1";
EXPECT_EQ(net::mdns_service_name(), "sq-vm-1");
@@ -65,6 +67,22 @@
EXPECT_EQ(net::mdns_service_name(), net::mdns_instance_name(platf::get_host_name()));
config::nvhttp.sunshine_name = saved;
config::video.capture = saved_capture;
}
// @tag requirements: [REQ-CMP-001]
TEST(MdnsServiceNameTest, OtherCaptureMethodsAdvertiseTheHostNameAsBefore) {
const auto saved = config::nvhttp.sunshine_name;
const auto saved_capture = config::video.capture;
config::nvhttp.sunshine_name = "Living Room PC";
for (const auto *capture : {"", "kms", "x11", "wlr", "portal", "kwin", "nvfbc"}) {
config::video.capture = capture;
EXPECT_EQ(net::mdns_service_name(), net::mdns_instance_name(platf::get_host_name())) << "capture = " << capture;
}
config::nvhttp.sunshine_name = saved;
config::video.capture = saved_capture;
}
/**