Files
salmanoff/senseApis/xcbXorg/xcbXorg.cpp
T
hayodea 20154d1e95 xcbXorg: Use apiParams to choose match method for window attachment
We now use param keys in the API params to choose what type of ID
the deviceSelector holds, and how to match it.

* dev-id/devid: matches by ID.
* dev-substr/dev-substring/devsubstr: Matches window name by substring.
* dev-string/devstr/dev-str: matches window name by exact whole string.
2025-01-14 20:59:28 -04:00

527 lines
17 KiB
C++

#include <iostream>
#include <algorithm>
#include <stdexcept>
#include <memory>
#include <vector>
#include <sstream>
#include <map>
#include <atomic>
#include <user/senseDeviceSpec.h>
#include <user/senseApiDesc.h>
#include <xcb/xcb.h>
#include "xcbXorg.h"
// Key for identifying unique X server connections
struct XcbConnection
{
struct XConnectionIdentifier
{
int display;
int screen;
bool operator<(const XConnectionIdentifier& other) const
{
if (display != other.display) return display < other.display;
return screen < other.screen;
}
std::string stringify() const
{
std::ostringstream os;
os << "display=" << display << ", screen=" << screen;
return os.str();
}
};
/**
* @brief RAII guard for managing X server connection lifetime
*
* This guard ensures proper cleanup of X server connections in case of
* errors during attachDeviceReq. If a connection is created but the device
* attachment fails, and the connection's refcount is 0, this guard will
* automatically remove the connection from the connections map.
*
* The guard can be "committed" using commit() to indicate successful
* device attachment, in which case it will not perform cleanup on destruction.
*/
class ConnectionGuard
{
std::shared_ptr<XcbConnection> conn;
bool committed = false;
public:
explicit ConnectionGuard(std::shared_ptr<XcbConnection> c)
: conn(std::move(c))
{}
void commit(void) { committed = true; }
~ConnectionGuard()
{
if (!committed && conn && conn->refCount == 0) {
XcbConnection::connections.erase(conn->connectionIdentifier);
}
}
};
XcbConnection(const XConnectionIdentifier& id)
: connection(nullptr, &xcb_disconnect),
connectionIdentifier(id), refCount(0)
{
// Convert to X display string format (e.g., ":0.1")
std::string displayString = ":" + std::to_string(id.display)
+ "." + std::to_string(id.screen);
int screenNum;
connection.reset(xcb_connect(displayString.c_str(), &screenNum));
if (xcb_connection_has_error(connection.get()))
{
throw std::runtime_error(
std::string(__func__) + ": Failed to connect to X server "
+ connectionIdentifier.stringify());
}
// Verify we got the screen we asked for
if (screenNum != id.screen)
{
throw std::runtime_error(
std::string(__func__) + ": Connected to wrong screen. "
"Requested " + connectionIdentifier.stringify()
+ " but got screen " + std::to_string(screenNum));
}
}
// Delete copy/move operations - we'll manage instances through pointers
XcbConnection(const XcbConnection&) = delete;
XcbConnection& operator=(const XcbConnection&) = delete;
XcbConnection(XcbConnection&&) = delete;
XcbConnection& operator=(XcbConnection&&) = delete;
std::unique_ptr<xcb_connection_t, decltype(&xcb_disconnect)> connection;
XConnectionIdentifier connectionIdentifier;
std::atomic<int> refCount;
public:
static std::map<XConnectionIdentifier, std::shared_ptr<XcbConnection>>
connections;
static std::shared_ptr<XcbConnection> getOrCreateConnection(
const XConnectionIdentifier& id)
{
auto it = connections.find(id);
if (it != connections.end()) {
return it->second;
}
auto conn = std::make_shared<XcbConnection>(id);
connections.emplace(id, conn);
return conn;
}
};
namespace xcb_window_search {
// Custom deleters for XCB reply types
struct XcbReplyDeleter {
void operator()(xcb_query_tree_reply_t* p) { free(p); }
void operator()(xcb_get_property_reply_t* p) { free(p); }
};
enum class MatchType { SUBSTRING, EXACT, ID };
static xcb_window_t findById(
xcb_connection_t* conn, xcb_window_t root, uint32_t targetId);
static xcb_window_t findByName(
xcb_connection_t* conn, xcb_window_t root,
const std::string& targetName, std::string& outWindowName,
MatchType matchType);
}
class AttachedDevice
{
public:
struct WindowSelector
{
xcb_window_search::MatchType matchType;
XcbConnection::XConnectionIdentifier xconn;
struct
{
uint32_t id;
std::string name;
} window;
std::string stringify() const
{
std::ostringstream os;
os << "Display: " << xconn.display
<< ", Screen: " << xconn.screen << ", Window: ";
if (matchType == xcb_window_search::MatchType::ID) {
os << window.id;
} else {
os << "\"" << window.name << "\"";
}
os << " (matchType="
<< (matchType == xcb_window_search::MatchType::EXACT
? "exact" :
(matchType == xcb_window_search::MatchType::SUBSTRING)
? "substring" : "id")
<< ")";
return os.str();
}
};
AttachedDevice(const hk::device::SenseDeviceSpec &spec,
std::shared_ptr<XcbConnection> conn)
: deviceSpec(spec)
// This std::move is moving ownership from a shared_ptr to a shared_ptr
, connection(std::move(conn))
{
windowSelector.xconn.display = getRequiredParamAsInt(spec, "display");
windowSelector.xconn.screen = getRequiredParamAsInt(spec, "screen");
parseWindowSelector(spec, windowSelector);
// Get the root window
const xcb_setup_t* setup = xcb_get_setup(connection->connection.get());
xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup);
for (int i = 0; i < windowSelector.xconn.screen; ++i) {
xcb_screen_next(&iter);
}
xcb_window_t root = iter.data->root;
// Search for window
xcb_window_t targetWindow = 0;
if (windowSelector.matchType == xcb_window_search::MatchType::ID)
{
targetWindow = xcb_window_search::findById(
connection->connection.get(), root, windowSelector.window.id);
}
else
{
targetWindow = xcb_window_search::findByName(
connection->connection.get(), root,
windowSelector.window.name, actualWindowName,
windowSelector.matchType);
}
if (!targetWindow)
{
throw std::runtime_error(
"Failed to find window "
+ (windowSelector.matchType == xcb_window_search::MatchType::ID
? std::to_string(windowSelector.window.id)
: "\"" + windowSelector.window.name + "\"")
+ " on display " + std::to_string(windowSelector.xconn.display)
+ ", screen " + std::to_string(windowSelector.xconn.screen));
}
}
hk::device::SenseDeviceSpec deviceSpec;
WindowSelector windowSelector;
std::shared_ptr<XcbConnection> connection;
std::string actualWindowName;
public:
static int getRequiredParamAsInt(
const hk::device::SenseDeviceSpec& spec,
const std::string& paramName)
{
auto it = std::find_if(
spec.providerParams.begin(),
spec.providerParams.end(),
[&paramName](const auto& param) {
return param.first == paramName;
}
);
if (it == spec.providerParams.end())
{
throw std::runtime_error(
"No " + paramName + " specified in provider params");
}
try {
return std::stoi(it->second);
} catch (const std::exception& e) {
throw std::runtime_error(
"Failed to parse '" + paramName + "' param value '"
+ it->second + "' as integer: " + e.what());
}
}
static void parseWindowSelector(
const hk::device::SenseDeviceSpec& spec,
WindowSelector& windowSelector)
{
// Default match type
windowSelector.matchType = xcb_window_search::MatchType::SUBSTRING;
// Check if 'dev-id', 'dev-string', or 'dev-substring' is specified
for (const auto& param : spec.apiParams)
{
if (param.first == "dev-id" || param.first == "devid")
{
windowSelector.matchType = xcb_window_search::MatchType::ID;
break;
}
if (param.first == "dev-string"
|| param.first == "dev-str" || param.first == "devstr")
{
windowSelector.matchType = xcb_window_search::MatchType::EXACT;
}
if (param.first == "dev-substring"
|| param.first == "dev-substr" || param.first == "devsubstr")
{
windowSelector.matchType = xcb_window_search::
MatchType::SUBSTRING;
}
}
if (windowSelector.matchType == xcb_window_search::MatchType::ID)
{
try {
windowSelector.window.id = std::stoul(
spec.deviceSelector, nullptr, 0);
} catch (const std::exception&) {
throw std::runtime_error(
"Window selector: 'dev-id' present, but selector is not "
"numeric");
}
}
else {
windowSelector.window.name = spec.deviceSelector;
}
}
};
// Define the static member
std::map<XcbConnection::XConnectionIdentifier, std::shared_ptr<XcbConnection>>
XcbConnection::connections;
static std::vector<AttachedDevice> attachedDevices;
namespace xcb_window_search {
static xcb_window_t findById(
xcb_connection_t* conn, xcb_window_t root, uint32_t targetId)
{
xcb_query_tree_cookie_t cookie = xcb_query_tree(conn, root);
std::unique_ptr<xcb_query_tree_reply_t, XcbReplyDeleter> reply(
xcb_query_tree_reply(conn, cookie, nullptr));
if (!reply) return 0;
// First check if current window is the target
if (root == targetId) {
return root;
}
// Then check all children
xcb_window_t* children = xcb_query_tree_children(reply.get());
int num_children = xcb_query_tree_children_length(reply.get());
for (int i = 0; i < num_children; ++i)
{
if (children[i] == targetId) {
return children[i];
}
// Recursively search this child's subtree
if (xcb_window_t result = findById(
conn, children[i], targetId))
{
return result;
}
}
return 0;
}
static xcb_window_t findByName(
xcb_connection_t* conn, xcb_window_t root,
const std::string& targetName, std::string& outWindowName,
MatchType matchType)
{
xcb_query_tree_cookie_t cookie = xcb_query_tree(conn, root);
std::unique_ptr<xcb_query_tree_reply_t, XcbReplyDeleter> reply(
xcb_query_tree_reply(conn, cookie, nullptr));
if (!reply) return 0;
// First check current window
xcb_get_property_cookie_t prop_cookie = xcb_get_property(
conn, 0, root, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0, 1024);
std::unique_ptr<xcb_get_property_reply_t, XcbReplyDeleter> prop_reply(
xcb_get_property_reply(conn, prop_cookie, nullptr));
if (prop_reply)
{
int len = xcb_get_property_value_length(prop_reply.get());
char* name = static_cast<char*>(
xcb_get_property_value(prop_reply.get()));
if (len > 0)
{
std::string windowName(name, len);
if ((matchType == MatchType::EXACT
&& windowName == targetName)
|| (matchType == MatchType::SUBSTRING
&& windowName.find(targetName) != std::string::npos))
{
outWindowName = windowName;
return root;
}
}
}
// Then check all children
xcb_window_t* children = xcb_query_tree_children(reply.get());
int num_children = xcb_query_tree_children_length(reply.get());
for (int i = 0; i < num_children; ++i)
{
prop_cookie = xcb_get_property(
conn, 0, children[i],
XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0, 1024);
prop_reply.reset(xcb_get_property_reply(conn, prop_cookie, nullptr));
if (prop_reply)
{
int len = xcb_get_property_value_length(prop_reply.get());
char* name = static_cast<char*>(xcb_get_property_value(
prop_reply.get()));
if (len > 0)
{
std::string windowName(name, len);
if ((matchType == MatchType::EXACT
&& windowName == targetName)
|| (matchType == MatchType::SUBSTRING
&& windowName.find(targetName) != std::string::npos))
{
outWindowName = windowName;
return children[i];
}
}
}
// Recursively search this child's subtree
if (xcb_window_t result = findByName(
conn, children[i], targetName, outWindowName, matchType))
{
return result;
}
}
return 0;
}
} // namespace xcb_window_search
static hk::sense_api::sal_mlo_initializeIndFn xcbXorg_initializeInd;
static hk::sense_api::sal_mlo_finalizeIndFn xcbXorg_finalizeInd;
static hk::sense_api::sal_mlo_attachDeviceReqFn xcbXorg_attachDeviceReq;
static hk::sense_api::sal_mlo_detachDeviceReqFn xcbXorg_detachDeviceReq;
static hk::sense_api::SenseApiDesc xcbXorgApiDesc =
{
.name = "xcb-xorg",
.exportedImplexorApis = { { "video-implexor" } },
.sal_mgmt_libOps = {
.initializeInd = xcbXorg_initializeInd,
.finalizeInd = xcbXorg_finalizeInd,
.attachDeviceReq = xcbXorg_attachDeviceReq,
.detachDeviceReq = xcbXorg_detachDeviceReq
}
};
extern HK_UNMANGLED hk::sense_api::getSenseApiDescFn
HK_GET_SENSE_API_DESC_FN_NAME;
const hk::sense_api::SenseApiDesc &HK_GET_SENSE_API_DESC_FN_NAME(void)
{
return xcbXorgApiDesc;
}
int xcbXorg_initializeInd(void)
{
return 0;
}
int xcbXorg_finalizeInd(void)
{
XcbConnection::connections.clear();
return 0;
}
int xcbXorg_attachDeviceReq(const hk::device::SenseDeviceSpec &desc)
{
// Ensure connection exists before creating device. Create conn'tion if not.
XcbConnection::XConnectionIdentifier id{
AttachedDevice::getRequiredParamAsInt(desc, "display"),
AttachedDevice::getRequiredParamAsInt(desc, "screen")
};
auto conn = XcbConnection::getOrCreateConnection(id);
// RAII protection in case AttachDevice construction below fails
XcbConnection::ConnectionGuard guard(conn);
// Create device and increment connection refcount
attachedDevices.emplace_back(desc, conn);
// Successfully attached device, so decouple guard from RAII cleanup
conn->refCount++;
guard.commit();
std::cout << "Attaching X11 window:\n "
<< attachedDevices.back().windowSelector.stringify() << "\n"
<< " Actual window name: \""
<< attachedDevices.back().actualWindowName << "\"\n"
<< " Using " << (conn->refCount > 1 ? "existing" : "new")
<< " connection to X server\n";
return 0;
}
int xcbXorg_detachDeviceReq(const hk::device::SenseDeviceSpec &spec)
{
auto it = std::find_if(attachedDevices.begin(), attachedDevices.end(),
[&spec](const AttachedDevice &device) {
return device.deviceSpec == spec;
}
);
if (it == attachedDevices.end())
{
auto displayIt = std::find_if(
spec.providerParams.begin(), spec.providerParams.end(),
[](const auto& param) { return param.first == "display"; });
auto screenIt = std::find_if(
spec.providerParams.begin(), spec.providerParams.end(),
[](const auto& param) { return param.first == "screen"; });
std::cerr << __func__ << ": Device not attached: "
<< "display=" << (displayIt != spec.providerParams.end()
? displayIt->second : "not specified")
<< ", screen=" << (screenIt != spec.providerParams.end()
? screenIt->second : "not specified")
<< ", selector=" << spec.deviceSelector << "\n";
return 0;
}
XcbConnection::XConnectionIdentifier id{
it->windowSelector.xconn.display,
it->windowSelector.xconn.screen
};
// Atomic decrement refcount
int newCount = --it->connection->refCount;
// If no more references, close the connection
if (newCount == 0) {
XcbConnection::connections.erase(id);
std::cout << "Closed X server connection (display="
<< id.display << ", screen=" << id.screen << ")\n";
}
attachedDevices.erase(it);
std::cout << __func__ << ": Detached device\n";
return 0;
}