mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
Preserve native HID discovery and trace Deck enumeration
This commit is contained in:
1 parent
f1fb3026e9
commit
c6c74fb67d
2 files changed
+254
-16
No files matched your search
@@ -1304,11 +1304,14 @@ __attribute__((visibility("hidden"))) static bool process_is_steam_client() {
|
||||
}
|
||||
|
||||
// What the client starts with, so a pad log says whether it was offered a Deck at all.
|
||||
__attribute__((visibility("hidden"))) static void probe_deck_discovery();
|
||||
__attribute__((constructor)) static void log_client_start() {
|
||||
config();
|
||||
if (process_is_steam_client())
|
||||
if (process_is_steam_client()) {
|
||||
Logger::log("loaded in the Steam client: deck %s, ring slot 0 %s\n", fake_deck_enabled() ? "on" : "off",
|
||||
get_ring_path_for_slot(0).empty() ? "(none configured)" : get_ring_path_for_slot(0).c_str());
|
||||
if (fake_deck_enabled() && Logger::log_enabled) probe_deck_discovery();
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static bool is_deck_hidraw_path(const char *pathname) {
|
||||
@@ -1517,6 +1520,74 @@ __attribute__((visibility("hidden"))) static std::string fd_path(int fd) {
|
||||
return length > 0 ? std::string(path, length) : std::string();
|
||||
}
|
||||
|
||||
// A read-only view of the bound tree, independent of whether Steam ever tries to enumerate it.
|
||||
// Use the next libc functions so these lines cannot be mistaken for Steam's discovery calls.
|
||||
__attribute__((visibility("hidden"))) static void probe_deck_discovery() {
|
||||
auto real_opendir = reinterpret_cast<DIR *(*)(const char *)>(dlsym(RTLD_NEXT, "opendir"));
|
||||
auto real_readlink = reinterpret_cast<ssize_t (*)(const char *, char *, size_t)>(dlsym(RTLD_NEXT, "readlink"));
|
||||
auto real_realpath = reinterpret_cast<char *(*)(const char *, char *)>(dlsym(RTLD_NEXT, "realpath"));
|
||||
auto real_open = reinterpret_cast<int (*)(const char *, int, ...)>(dlsym(RTLD_NEXT, "open"));
|
||||
auto real_fstatfs = reinterpret_cast<int (*)(int, struct statfs *)>(dlsym(RTLD_NEXT, "fstatfs"));
|
||||
int saved_errno = errno;
|
||||
DIR *dir = real_opendir("/sys/class/hidraw");
|
||||
if (dir) {
|
||||
unsigned count = 0;
|
||||
bool deck = false;
|
||||
unsigned deck_type = 0;
|
||||
errno = 0;
|
||||
while (auto *entry = readdir(dir)) {
|
||||
if (entry->d_name[0] == '.') continue;
|
||||
++count;
|
||||
if (!strcmp(entry->d_name, "hidraw16")) {
|
||||
deck = true;
|
||||
deck_type = entry->d_type;
|
||||
}
|
||||
}
|
||||
int error = errno;
|
||||
closedir(dir);
|
||||
Logger::log("deck probe: /sys/class/hidraw entries=%u hidraw16=%s d_type=%u (%s)\n", count,
|
||||
deck ? "present" : "absent", deck_type, error ? strerror(error) : "ok");
|
||||
} else {
|
||||
Logger::log("deck probe: /sys/class/hidraw failed (%s)\n", strerror(errno));
|
||||
}
|
||||
const char *node = "/sys/class/hidraw/hidraw16";
|
||||
char resolved[PATH_MAX];
|
||||
ssize_t length = real_readlink(node, resolved, sizeof(resolved) - 1);
|
||||
if (length >= 0) {
|
||||
resolved[length] = '\0';
|
||||
Logger::log("deck probe: %s link=%s\n", node, resolved);
|
||||
} else {
|
||||
Logger::log("deck probe: %s readlink failed (%s)\n", node, strerror(errno));
|
||||
}
|
||||
if (real_realpath(node, resolved))
|
||||
Logger::log("deck probe: %s resolved=%s\n", node, resolved);
|
||||
else
|
||||
Logger::log("deck probe: %s realpath failed (%s)\n", node, strerror(errno));
|
||||
for (const char *path : {"/sys/class/hidraw/hidraw16/uevent", "/sys/class/hidraw/hidraw16/device/uevent",
|
||||
"/sys/class/hidraw/hidraw16/dev", "/run/udev/data/c240:16"}) {
|
||||
int fd = real_open(path, O_RDONLY | O_CLOEXEC);
|
||||
if (fd >= 0 && !strcmp(path, "/sys/class/hidraw/hidraw16/uevent")) {
|
||||
struct statfs fs;
|
||||
int result = real_fstatfs(fd, &fs);
|
||||
int error = errno;
|
||||
Logger::log("deck probe: uevent fd path=%s raw_fs_type=0x%lx (%s)\n", fd_path(fd).c_str(),
|
||||
result == 0 ? static_cast<long>(fs.f_type) : 0, result == 0 ? "ok" : strerror(error));
|
||||
}
|
||||
char value[384];
|
||||
ssize_t n = fd >= 0 ? syscall(SYS_read, fd, value, sizeof(value) - 1) : -1;
|
||||
int error = errno;
|
||||
if (fd >= 0) syscall(SYS_close, fd);
|
||||
if (n >= 0) {
|
||||
value[n] = '\0';
|
||||
for (ssize_t i = 0; i < n; ++i) if (value[i] == '\n') value[i] = ';';
|
||||
Logger::log("deck probe: %s value=%s\n", path, value);
|
||||
} else {
|
||||
Logger::log("deck probe: %s %s failed (%s)\n", path, fd < 0 ? "open" : "read", strerror(error));
|
||||
}
|
||||
}
|
||||
errno = saved_errno;
|
||||
}
|
||||
|
||||
// Once per process: which guest or backing path sd-device checked and the filesystem it found.
|
||||
__attribute__((visibility("hidden"))) static void note_deck_sysfs(const char *path, int result, long type) {
|
||||
static std::atomic<bool> noted{false};
|
||||
@@ -1710,9 +1781,34 @@ __attribute__((visibility("hidden"))) static std::string note_sys_failure(int di
|
||||
return path;
|
||||
}
|
||||
|
||||
// A bind does not add its name to a real parent's listing. Always show Steam the merged class
|
||||
// listing, even on a readable kernel whose real /sys/class lacks hidraw. /sys and /sys/bus keep
|
||||
// their real listings unless SELinux denies them. Other processes and Xbox mode keep real sysfs.
|
||||
// Check the guest's native class names once. Readable listings containing hidraw stay native;
|
||||
// a missing name needs the merge because a bind alone does not add it to the parent's listing.
|
||||
__attribute__((visibility("hidden"))) static bool deck_class_missing_hidraw() {
|
||||
static const bool missing = [] {
|
||||
auto real = reinterpret_cast<DIR *(*)(const char *)>(dlsym(RTLD_NEXT, "opendir"));
|
||||
int saved_errno = errno;
|
||||
DIR *dir = real("/sys/class");
|
||||
bool found = false;
|
||||
bool readable = false;
|
||||
int error = errno;
|
||||
if (dir) {
|
||||
errno = 0;
|
||||
while (auto *entry = readdir(dir)) found |= !strcmp(entry->d_name, "hidraw");
|
||||
error = errno;
|
||||
readable = error == 0;
|
||||
closedir(dir);
|
||||
}
|
||||
Logger::log("deck discovery: native /sys/class %s (%s)\n",
|
||||
readable ? (found ? "contains hidraw; retained" : "missing hidraw; merge needed") : "not listable",
|
||||
readable ? "ok" : strerror(error));
|
||||
errno = saved_errno;
|
||||
return readable && !found;
|
||||
}();
|
||||
return missing;
|
||||
}
|
||||
|
||||
// /sys, /sys/bus and a native /sys/class containing hidraw use the old EACCES-only fallback.
|
||||
// Other processes and Xbox mode keep real sysfs.
|
||||
__attribute__((visibility("hidden"))) static std::string deck_listing_for(const char *path, bool refused = false) {
|
||||
const char *listing = deck_listing_dir();
|
||||
if (!path || !listing || !fake_deck_enabled() || !process_is_steam_client()) return {};
|
||||
@@ -1722,7 +1818,8 @@ __attribute__((visibility("hidden"))) static std::string deck_listing_for(const
|
||||
if (p.compare(0, strlen(listing) + 5, std::string(listing) + "/sys/") == 0)
|
||||
p = "/sys/" + p.substr(strlen(listing) + 5);
|
||||
int which = p == "/sys" ? 0 : p == "/sys/class" ? 1 : p == "/sys/bus" ? 2 : -1;
|
||||
if (which < 0 || (which != 1 && (!refused || errno != EACCES))) return {};
|
||||
bool denied = refused && errno == EACCES;
|
||||
if (which < 0 || (!denied && (which != 1 || !deck_class_missing_hidraw()))) return {};
|
||||
static const char *const names[] = {"sys", "class", "bus"};
|
||||
static std::atomic<int> logged{0};
|
||||
if (!(logged.fetch_or(1 << which) & (1 << which)))
|
||||
@@ -1747,13 +1844,141 @@ __attribute__((visibility("hidden"))) static std::string path_at(int dirfd, cons
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Log only the discovery roots and our HID tree, including ENOENT. Ordinary sysfs failures keep
|
||||
// their existing log below; unrelated attribute traffic cannot consume this separate budget.
|
||||
__attribute__((visibility("hidden"))) static bool deck_discovery_path(const char *path) {
|
||||
if (!path) return false;
|
||||
auto under = [path](const std::string &root) {
|
||||
return !root.empty() && !strncmp(path, root.c_str(), root.size()) &&
|
||||
(path[root.size()] == '\0' || path[root.size()] == '/');
|
||||
};
|
||||
if (!strcmp(path, "/sys") || !strcmp(path, "/sys/") ||
|
||||
!strcmp(path, "/sys/class") || !strcmp(path, "/sys/class/") ||
|
||||
!strcmp(path, "/sys/bus") || !strcmp(path, "/sys/bus/") ||
|
||||
!strcmp(path, "/sys/dev/char/240:16") || !strcmp(path, "/run/udev/data/c240:16") ||
|
||||
under("/sys/class/hidraw") || under("/sys/devices/droiddeck")) return true;
|
||||
const char *root = getenv("FAKE_DECK_SYSFS_ROOT");
|
||||
if (root && (under(std::string(root) + "/devices") || under(std::string(root) + "/class-hidraw"))) return true;
|
||||
const char *listing = deck_listing_dir();
|
||||
return listing && (!strcmp(path, (std::string(listing) + "/class").c_str()) ||
|
||||
under(std::string(listing) + "/class/hidraw"));
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static bool deck_discovery_enabled() {
|
||||
return Logger::log_enabled && fake_deck_enabled() && process_is_steam_client();
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static void note_deck_discovery(
|
||||
const char *operation, const char *path, long result, int flags = 0,
|
||||
const char *detail = nullptr, bool descriptor = false) {
|
||||
int saved_errno = errno;
|
||||
if (Logger::log_enabled && fake_deck_enabled() && deck_discovery_path(path) && process_is_steam_client()) {
|
||||
// Descriptor numbers change on each scan. Deduplicate by operation/path/flags/outcome instead.
|
||||
int error = result < 0 ? saved_errno : 0;
|
||||
std::string key = std::string(operation) + ":" + path + ":" + std::to_string(flags) + ":" + std::to_string(error);
|
||||
static auto *seen = new std::vector<std::string>();
|
||||
bool first = false;
|
||||
bool limit = false;
|
||||
{
|
||||
// This mutex is already reset in forked Steam children.
|
||||
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
|
||||
if (seen->size() < 96 && std::find(seen->begin(), seen->end(), key) == seen->end()) {
|
||||
seen->push_back(key);
|
||||
first = true;
|
||||
limit = seen->size() == 96;
|
||||
}
|
||||
}
|
||||
if (first) {
|
||||
std::string extra = descriptor && result >= 0 ? fd_path(static_cast<int>(result)) : std::string();
|
||||
if (detail && *detail) extra += (extra.empty() ? "" : " ") + std::string(detail);
|
||||
Logger::log("deck discovery: %s %s flags=0x%x result=%ld (%s)%s%s\n", operation, path, flags, result,
|
||||
error ? strerror(error) : "ok", extra.empty() ? "" : " target=", extra.c_str());
|
||||
if (limit) Logger::log("deck discovery: trace limit reached (96 unique operations)\n");
|
||||
}
|
||||
}
|
||||
errno = saved_errno;
|
||||
}
|
||||
|
||||
// These libc entry points can bypass the public open() hook. Keep their behavior unchanged and
|
||||
// expose the symlink chase, statx/fstatat validation and stdio attribute opens that sd-device uses.
|
||||
EXPORT ssize_t readlink(const char *path, char *buf, size_t size) {
|
||||
static auto real = reinterpret_cast<decltype(&::readlink)>(dlsym(RTLD_NEXT, "readlink"));
|
||||
ssize_t result = real(path, buf, size);
|
||||
int saved_errno = errno;
|
||||
if (deck_discovery_path(path)) {
|
||||
std::string target = result >= 0 ? std::string(buf, result) : std::string();
|
||||
note_deck_discovery("readlink", path, result, 0, target.c_str());
|
||||
}
|
||||
errno = saved_errno;
|
||||
return result;
|
||||
}
|
||||
|
||||
EXPORT ssize_t readlinkat(int dirfd, const char *path, char *buf, size_t size) {
|
||||
static auto real = reinterpret_cast<decltype(&::readlinkat)>(dlsym(RTLD_NEXT, "readlinkat"));
|
||||
ssize_t result = real(dirfd, path, buf, size);
|
||||
int saved_errno = errno;
|
||||
if (deck_discovery_enabled()) {
|
||||
std::string resolved = path_at(dirfd, path);
|
||||
std::string target = result >= 0 ? std::string(buf, result) : std::string();
|
||||
note_deck_discovery("readlinkat", resolved.c_str(), result, 0, target.c_str());
|
||||
}
|
||||
errno = saved_errno;
|
||||
return result;
|
||||
}
|
||||
|
||||
EXPORT FILE *fopen(const char *path, const char *mode) {
|
||||
static auto real = reinterpret_cast<decltype(&::fopen)>(dlsym(RTLD_NEXT, "fopen"));
|
||||
FILE *file = real(path, mode);
|
||||
int saved_errno = errno;
|
||||
note_deck_discovery("fopen", path, file ? fileno(file) : -1, 0, mode, true);
|
||||
errno = saved_errno;
|
||||
return file;
|
||||
}
|
||||
|
||||
#ifdef __GLIBC__
|
||||
EXPORT FILE *fopen64(const char *path, const char *mode) {
|
||||
return fopen(path, mode);
|
||||
}
|
||||
|
||||
EXPORT int fstatat(int dirfd, const char *path, struct stat *buf, int flags) {
|
||||
static auto real = reinterpret_cast<decltype(&::fstatat)>(dlsym(RTLD_NEXT, "fstatat"));
|
||||
int result = real(dirfd, path, buf, flags);
|
||||
int saved_errno = errno;
|
||||
if (deck_discovery_enabled()) {
|
||||
std::string resolved = path_at(dirfd, path);
|
||||
char type[32];
|
||||
snprintf(type, sizeof(type), "mode=0%o", result == 0 ? buf->st_mode : 0);
|
||||
note_deck_discovery("fstatat", resolved.c_str(), result, flags, type);
|
||||
}
|
||||
errno = saved_errno;
|
||||
return result;
|
||||
}
|
||||
|
||||
EXPORT int fstatat64(int dirfd, const char *path, struct stat64 *buf, int flags) {
|
||||
return fstatat(dirfd, path, reinterpret_cast<struct stat *>(buf), flags);
|
||||
}
|
||||
|
||||
EXPORT int statx(int dirfd, const char *path, int flags, unsigned mask, struct statx *buf) {
|
||||
static auto real = reinterpret_cast<decltype(&::statx)>(dlsym(RTLD_NEXT, "statx"));
|
||||
int result = real(dirfd, path, flags, mask, buf);
|
||||
int saved_errno = errno;
|
||||
if (deck_discovery_enabled()) {
|
||||
std::string resolved = path_at(dirfd, path);
|
||||
char type[48];
|
||||
snprintf(type, sizeof(type), "mode=0%o mask=0x%x", result == 0 ? buf->stx_mode : 0, mask);
|
||||
note_deck_discovery("statx", resolved.c_str(), result, flags, type);
|
||||
}
|
||||
errno = saved_errno;
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
// The listing supplies names only. A relative open on its fd must descend into real sysfs (or
|
||||
// the bound hidraw class), not into an empty placeholder. In particular sd-device's chase uses
|
||||
// openat(O_PATH) on each component; symlinks back to /sys/class would loop through this listing.
|
||||
__attribute__((visibility("hidden"))) static std::string deck_listing_child(int dirfd, const char *path) {
|
||||
__attribute__((visibility("hidden"))) static std::string deck_listing_child_path(const std::string &p) {
|
||||
const char *listing = deck_listing_dir();
|
||||
if (!listing || !fake_deck_enabled() || !process_is_steam_client()) return {};
|
||||
std::string p = path_at(dirfd, path);
|
||||
for (const char *name : {"class", "sys"}) {
|
||||
std::string prefix = std::string(listing) + "/" + name + "/";
|
||||
if (p.compare(0, prefix.size(), prefix) == 0)
|
||||
@@ -1762,18 +1987,23 @@ __attribute__((visibility("hidden"))) static std::string deck_listing_child(int
|
||||
return {};
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static std::string deck_listing_child(int dirfd, const char *path) {
|
||||
if (!deck_listing_dir() || !fake_deck_enabled() || !process_is_steam_client()) return {};
|
||||
return deck_listing_child_path(path_at(dirfd, path));
|
||||
}
|
||||
|
||||
EXPORT DIR *opendir(const char *name) {
|
||||
static auto real = reinterpret_cast<DIR *(*)(const char *)>(dlsym(RTLD_NEXT, "opendir"));
|
||||
std::string child = deck_listing_child(AT_FDCWD, name);
|
||||
if (!child.empty()) name = child.c_str();
|
||||
std::string listing = deck_listing_for(name);
|
||||
if (!listing.empty()) return real(listing.c_str());
|
||||
DIR *dir = real(name);
|
||||
DIR *dir = real(listing.empty() ? name : listing.c_str());
|
||||
if (!dir) {
|
||||
note_sys_failure(AT_FDCWD, name);
|
||||
listing = deck_listing_for(name, true);
|
||||
if (!listing.empty()) dir = real(listing.c_str());
|
||||
}
|
||||
note_deck_discovery("opendir", name, dir ? dirfd(dir) : -1, 0, nullptr, true);
|
||||
return dir;
|
||||
}
|
||||
|
||||
@@ -1838,6 +2068,7 @@ EXPORT int open(const char *pathname, int flags, ...) {
|
||||
|
||||
std::string listing;
|
||||
std::string child;
|
||||
const char *requested = pathname;
|
||||
if ((flags & O_ACCMODE) == O_RDONLY && !(flags & (O_CREAT | O_TRUNC))) {
|
||||
child = deck_listing_child(AT_FDCWD, pathname);
|
||||
if (!child.empty()) pathname = child.c_str();
|
||||
@@ -1853,6 +2084,8 @@ EXPORT int open(const char *pathname, int flags, ...) {
|
||||
if (!fallback.empty()) fd = my_open(fallback.c_str(), flags);
|
||||
}
|
||||
|
||||
note_deck_discovery("open", requested, fd, flags, nullptr, true);
|
||||
|
||||
if (fake_path)
|
||||
free(fake_path);
|
||||
|
||||
@@ -1920,14 +2153,16 @@ EXPORT int openat(int dirfd, const char *pathname, int flags, ...) {
|
||||
|
||||
std::string listing;
|
||||
std::string child;
|
||||
std::string requested;
|
||||
if (fake_deck_enabled() && process_is_steam_client() &&
|
||||
(flags & O_ACCMODE) == O_RDONLY && !(flags & (O_CREAT | O_TRUNC))) {
|
||||
child = deck_listing_child(dirfd, pathname);
|
||||
requested = path_at(dirfd, pathname);
|
||||
child = deck_listing_child_path(requested);
|
||||
if (!child.empty()) {
|
||||
dirfd = AT_FDCWD;
|
||||
pathname = child.c_str();
|
||||
}
|
||||
listing = deck_listing_for(path_at(dirfd, pathname).c_str());
|
||||
listing = deck_listing_for(child.empty() ? requested.c_str() : child.c_str());
|
||||
}
|
||||
if (!listing.empty()) {
|
||||
dirfd = AT_FDCWD;
|
||||
@@ -1943,6 +2178,8 @@ EXPORT int openat(int dirfd, const char *pathname, int flags, ...) {
|
||||
if (!fallback.empty()) fd = my_openat(AT_FDCWD, fallback.c_str(), flags);
|
||||
}
|
||||
|
||||
if (!requested.empty()) note_deck_discovery("openat", requested.c_str(), fd, flags, nullptr, true);
|
||||
|
||||
if (fake_path)
|
||||
free(fake_path);
|
||||
|
||||
@@ -2109,13 +2346,13 @@ EXPORT int scandir(const char *dirp, struct dirent ***namelist,
|
||||
std::string child = deck_listing_child(AT_FDCWD, dirp);
|
||||
if (!child.empty()) dirp = child.c_str();
|
||||
std::string listing = deck_listing_for(dirp);
|
||||
if (!listing.empty()) return my_scandir(listing.c_str(), namelist, filter, compar);
|
||||
int result = my_scandir(dirp, namelist, filter, compar);
|
||||
int result = my_scandir(listing.empty() ? dirp : listing.c_str(), namelist, filter, compar);
|
||||
if (result < 0) {
|
||||
note_sys_failure(AT_FDCWD, dirp);
|
||||
listing = deck_listing_for(dirp, true);
|
||||
if (!listing.empty()) result = my_scandir(listing.c_str(), namelist, filter, compar);
|
||||
}
|
||||
note_deck_discovery("scandir", dirp, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@ import java.io.File
|
||||
* - [listingDir] - a merged `/sys/class` listing, with the host's class names plus `hidraw`.
|
||||
* A proot bind is reachable by path but does not add its name to the parent's directory listing;
|
||||
* a kernel without a native `hidraw` class therefore hid our bound class from libudev. The
|
||||
* libfakeinput routes relative opens back to the real guest paths. `/sys` and `/sys/bus` also have minimal
|
||||
* libfakeinput uses this only when `hidraw` is missing or the native listing is denied, and routes
|
||||
* relative opens back to the real guest paths. `/sys` and `/sys/bus` also have minimal
|
||||
* stand-ins for a client whose SELinux policy refuses those listings.
|
||||
*
|
||||
* Symlinks are absolute guest paths; proot resolves them inside the session.
|
||||
@@ -111,8 +112,8 @@ object SteamDeckPad {
|
||||
File(listing, "class/$name").mkdirs()
|
||||
}
|
||||
Log.i(TAG, "deck pad: native /sys/class " + when (nativeClasses) {
|
||||
null -> "not listable; merged listing contains hidraw only"
|
||||
else -> "${nativeClasses.size} classes, hidraw ${if ("hidraw" in nativeClasses) "present" else "absent"}; merged listing includes hidraw"
|
||||
null -> "not listable; hidraw listing fallback prepared"
|
||||
else -> "${nativeClasses.size} classes, hidraw ${if ("hidraw" in nativeClasses) "present; native listing retained" else "absent; merged listing prepared"}"
|
||||
})
|
||||
// The runtime binds this directory over /sys/dev/char for the GPU (LinuxRuntime.bindGpuNode).
|
||||
link(File(context.cacheDir, "drm/sys/$MAJOR:$MINOR"), HIDRAW)
|
||||
|
||||
Reference in new issue
Block a user