ref:f60daa42a1ef380cdae54c3b45472abcd7e1b4fc

fix(linux): say a VM is unreachable once, not several times a second

Capture, audio and input each retry the session while a VM is away, so a VM that stops mid-stream or hasn't booted yet wrote "no QEMU owns [org.qemu]" at error level a few times a second: 673 lines in the twelve seconds after killing a VM under test. The first failure is the one worth an error; the retries after it go to the debug log until the VM answers again, which also resets the report for the next outage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPNw4PCgkEfhyCjQT19wsb
SHA: f60daa42a1ef380cdae54c3b45472abcd7e1b4fc
Author: Cole Christensen <cole.christensen@gmail.com>
Date: 2026-09-17 05:58
Parents: 4fe9720
3 files changed +55 -1
Type
src/platform/linux/qemu/session.cpp +19 −1
@@ -14,6 +14,7 @@
#include <functional>
#include <future>
#include <map>
#include <set>
#include <mutex>
#include <thread>
#include <utility>
@@ -813,7 +814,8 @@
if (!peer_to_peer) {
auto owner = g_dbus_proxy_get_name_owner(G_DBUS_PROXY(vm_proxy));
if (!owner) {
auto &severity = first_failure(where) ? error : debug;
BOOST_LOG(severity) << "qemu: no QEMU owns ["sv << bus_name << "] on "sv << where;
BOOST_LOG(error) << "qemu: no QEMU owns ["sv << bus_name << "] on "sv << where;
return false;
}
g_free(owner);
@@ -1661,6 +1663,21 @@
}
}
namespace {
std::mutex unreachable_mutex; ///< Guards `unreachable`.
std::set<std::string> unreachable; ///< Addresses whose last connection failed, already reported.
} // namespace
bool first_failure(const std::string &address) {
std::scoped_lock lock {unreachable_mutex};
return unreachable.insert(address).second;
}
void connection_succeeded(const std::string &address) {
std::scoped_lock lock {unreachable_mutex};
unreachable.erase(address);
}
std::shared_ptr<session_t> shared_session(const std::string &address) {
static std::mutex mutex;
static std::weak_ptr<session_t> cached;
@@ -1689,6 +1706,7 @@
if (session) {
cached = session;
cached_address = address;
connection_succeeded(address);
}
connecting.erase(address);
lock.unlock();
src/platform/linux/qemu/session.h +17 −0
@@ -610,6 +610,23 @@
std::shared_ptr<session_t> shared_session(const std::string &address);
/**
* @brief Report whether a failure to reach an address is the first since it last worked.
* @details Capture, audio and input retry a VM that isn't there several times a second. The first
* failure is worth an error in the log; the ones after it aren't, until the VM answers again.
*
* @param address Address that couldn't be reached.
* @return True the first time, false while it stays unreachable.
*/
bool first_failure(const std::string &address);
/**
* @brief Forget that an address was unreachable, so the next failure is reported again.
*
* @param address Address that answered.
*/
void connection_succeeded(const std::string &address);
/**
* @brief Ask a console for a size and remember the one it had, so it can be put back.
* @details The session is held until the size is restored: the display that asked for the change is
* usually gone by the time streaming stops, and a console whose size nobody remembers stays resized.
tests/unit/platform/linux/qemu/test_session.cpp +19 −0
@@ -232,6 +232,25 @@
}
// @tag requirements: [REQ-CAP-001]
TEST(QemuSessionRetryTest, ReportsAnUnreachableVmOnceUntilItAnswers) {
const std::string address = "unix:path=/nonexistent/sunshine-qemu-retry-test.sock";
// capture, audio and input retry several times a second while a VM is away or still booting
EXPECT_TRUE(qemu::first_failure(address));
EXPECT_FALSE(qemu::first_failure(address));
EXPECT_FALSE(qemu::first_failure(address));
qemu::connection_succeeded(address);
EXPECT_TRUE(qemu::first_failure(address));
// addresses are tracked apart, so one VM's outage doesn't silence another's
const std::string other = address + ".other";
EXPECT_TRUE(qemu::first_failure(other));
qemu::connection_succeeded(other);
qemu::connection_succeeded(address);
}
// @tag requirements: [REQ-CAP-001]
TEST_F(QemuSessionTest, FindConsoleByIdLabelOrDefault) {
qemu::vm_info_t vm;
vm.consoles = {