mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 19:41:10 +00:00
fix: Rewired games see DroidDeck's pads (udev listing, dlsym-resolved libc calls) (#1222)
This commit is contained in:
1 parent
9139524015
commit
06a6fd96db
5 files changed
+339
No files matched your search
@@ -21,6 +21,9 @@
|
||||
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#ifdef __GLIBC__
|
||||
#include <gnu/lib-names.h>
|
||||
#endif
|
||||
#include <fcntl.h>
|
||||
#include <linux/input.h>
|
||||
#include <linux/joystick.h>
|
||||
@@ -4103,4 +4106,92 @@ EXPORT int __fxstat64(int version, int fd, struct stat64 *buf) {
|
||||
if (!real) return fstat64(fd, buf);
|
||||
return fake_stat_fd(fd, buf, [version](int f, struct stat64 *b) { return real(version, f, b); });
|
||||
}
|
||||
|
||||
#if defined(__x86_64__) || defined(__i386__)
|
||||
using dlsym_fn = void *(*)(void *, const char *);
|
||||
|
||||
static const char *const interposed_names[] = {
|
||||
"open", "open64", "openat", "openat64", "close", "read", "write", "writev", "ioctl", "poll",
|
||||
"ppoll", "select", "stat", "stat64", "fstat", "fstat64", "__xstat", "__xstat64", "__fxstat",
|
||||
"__fxstat64", "statx", "statfs", "statfs64", "fstatfs", "fstatfs64", "access", "faccessat",
|
||||
"opendir", "scandir", "inotify_add_watch",
|
||||
};
|
||||
|
||||
static dlsym_fn next_dlsym() {
|
||||
static const dlsym_fn fn = [] {
|
||||
#ifdef __x86_64__
|
||||
const char *base = "GLIBC_2.2.5";
|
||||
#else
|
||||
const char *base = "GLIBC_2.0";
|
||||
#endif
|
||||
for (const char *version : {base, "GLIBC_2.34"}) {
|
||||
if (auto found = reinterpret_cast<dlsym_fn>(dlvsym(RTLD_NEXT, "dlsym", version))) return found;
|
||||
}
|
||||
return dlsym_fn{};
|
||||
}();
|
||||
return fn;
|
||||
}
|
||||
|
||||
static void *global_dlsym(void *, const char *name) { return next_dlsym()(RTLD_DEFAULT, name); }
|
||||
|
||||
static void *missing_dlsym(void *, const char *) { return nullptr; }
|
||||
|
||||
static bool interposed(const char *name) {
|
||||
for (const char *interposed_name : interposed_names) {
|
||||
if (strcmp(interposed_name, name) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" __attribute__((visibility("hidden"), used)) void *fakeinput_dlsym_target(void *handle, const char *name) {
|
||||
dlsym_fn next = next_dlsym();
|
||||
if (!next) return reinterpret_cast<void *>(missing_dlsym);
|
||||
if (handle == RTLD_DEFAULT || handle == RTLD_NEXT || name == nullptr) return reinterpret_cast<void *>(next);
|
||||
static void *const libc = dlopen(LIBC_SO, RTLD_LAZY | RTLD_NOLOAD);
|
||||
if (handle != libc || !interposed(name)) return reinterpret_cast<void *>(next);
|
||||
return reinterpret_cast<void *>(global_dlsym);
|
||||
}
|
||||
|
||||
#ifdef __x86_64__
|
||||
__asm__(".text\n"
|
||||
".globl dlsym\n"
|
||||
".type dlsym, @function\n"
|
||||
"dlsym:\n"
|
||||
".cfi_startproc\n"
|
||||
"pushq %rdi\n"
|
||||
".cfi_adjust_cfa_offset 8\n"
|
||||
"pushq %rsi\n"
|
||||
".cfi_adjust_cfa_offset 8\n"
|
||||
"subq $8, %rsp\n"
|
||||
".cfi_adjust_cfa_offset 8\n"
|
||||
"call fakeinput_dlsym_target\n"
|
||||
"addq $8, %rsp\n"
|
||||
".cfi_adjust_cfa_offset -8\n"
|
||||
"popq %rsi\n"
|
||||
".cfi_adjust_cfa_offset -8\n"
|
||||
"popq %rdi\n"
|
||||
".cfi_adjust_cfa_offset -8\n"
|
||||
"jmp *%rax\n"
|
||||
".cfi_endproc\n"
|
||||
".size dlsym, .-dlsym\n");
|
||||
#else
|
||||
__asm__(".text\n"
|
||||
".globl dlsym\n"
|
||||
".type dlsym, @function\n"
|
||||
"dlsym:\n"
|
||||
".cfi_startproc\n"
|
||||
"subl $12, %esp\n"
|
||||
".cfi_adjust_cfa_offset 12\n"
|
||||
"movl 20(%esp), %eax\n"
|
||||
"movl %eax, 4(%esp)\n"
|
||||
"movl 16(%esp), %eax\n"
|
||||
"movl %eax, (%esp)\n"
|
||||
"call fakeinput_dlsym_target\n"
|
||||
"addl $12, %esp\n"
|
||||
".cfi_adjust_cfa_offset -12\n"
|
||||
"jmp *%eax\n"
|
||||
".cfi_endproc\n"
|
||||
".size dlsym, .-dlsym\n");
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
@@ -835,6 +835,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
SessionPrefs.setSteamController(this@SessionActivity, id)
|
||||
com.droiddeck.launcher.session.SteamDeckPad.showPad(SessionPrefs.padType(id))
|
||||
com.droiddeck.launcher.input.FakeInputWriter.setPadType(SessionPrefs.padType(id))
|
||||
com.droiddeck.launcher.session.InputSysfsComponent.padTypeChanged(SessionPrefs.padType(id))
|
||||
padType = id
|
||||
},
|
||||
backActionsInverted = backActionsInverted,
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.os.FileObserver
|
||||
import android.os.SystemClock
|
||||
import android.system.Os
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.core.SessionPart
|
||||
import com.droiddeck.launcher.input.FakeInputWriter
|
||||
import java.io.File
|
||||
|
||||
class InputSysfsComponent(
|
||||
private val nodes: File,
|
||||
base: File,
|
||||
private val udevData: File,
|
||||
private val deckPad: Boolean,
|
||||
private val steamVirtual: Boolean,
|
||||
) : SessionPart() {
|
||||
private val devices = File(base, "devices")
|
||||
private val classDir = File(base, "class")
|
||||
private val shown = HashSet<Int>()
|
||||
private var observer: FileObserver? = null
|
||||
private var running = false
|
||||
|
||||
fun prepare(): Boolean = try {
|
||||
FileUtils.clear(devices)
|
||||
FileUtils.clear(classDir)
|
||||
devices.mkdirs()
|
||||
classDir.mkdirs()
|
||||
udevData.mkdirs()
|
||||
udevData.listFiles()?.forEach { if (isPadRecord(it.name)) it.delete() }
|
||||
devices.isDirectory && classDir.isDirectory
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "pads: could not prepare the input sysfs: $e")
|
||||
false
|
||||
}
|
||||
|
||||
fun binds() = listOf(devices.path + ":" + GUEST_DEVICES, classDir.path + ":" + GUEST_CLASS)
|
||||
|
||||
override fun start() {
|
||||
synchronized(this) { running = true }
|
||||
active = this
|
||||
@Suppress("DEPRECATION")
|
||||
observer = object : FileObserver(nodes.path, CREATE or DELETE or MOVED_TO or MOVED_FROM or CLOSE_WRITE) {
|
||||
override fun onEvent(event: Int, path: String?) {
|
||||
if (path != null && NODE_FILE.matches(path)) sync()
|
||||
}
|
||||
}.also { it.startWatching() }
|
||||
sync()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
observer?.stopWatching()
|
||||
observer = null
|
||||
if (active === this) active = null
|
||||
synchronized(this) {
|
||||
running = false
|
||||
for (node in shown.toList()) hide(node)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun sync() {
|
||||
if (!running) return
|
||||
val wanted = (0 until NODE_LIMIT).filter { node ->
|
||||
File(nodes, "event$node").exists() && if (node >= FIRST_VIRTUAL) File(nodes, ".uinput-event$node.uevent").isFile
|
||||
else !ringWithdrawn(deckPad, padType)
|
||||
}.toSet()
|
||||
for (node in shown - wanted) hide(node)
|
||||
for (node in wanted - shown) show(node)
|
||||
}
|
||||
|
||||
private fun show(node: Int) {
|
||||
val pad = (if (node >= FIRST_VIRTUAL) runCatching { File(nodes, ".uinput-event$node.uevent").readText() }.getOrNull()?.let(::virtualPad)
|
||||
else ringPad(node, steamVirtual)) ?: return
|
||||
try {
|
||||
val input = File(devices, "input$node")
|
||||
val event = File(input, "event$node")
|
||||
event.mkdirs()
|
||||
File(input, "uevent").writeText(pad.uevent())
|
||||
File(input, "name").writeText(pad.name + "\n")
|
||||
File(input, "phys").writeText(pad.phys + "\n")
|
||||
File(input, "uniq").writeText(pad.uniq + "\n")
|
||||
File(input, "properties").writeText("0\n")
|
||||
File(input, "id").mkdirs()
|
||||
File(input, "id/bustype").writeText("%04x\n".format(pad.bus))
|
||||
File(input, "id/vendor").writeText("%04x\n".format(pad.vendor))
|
||||
File(input, "id/product").writeText("%04x\n".format(pad.product))
|
||||
File(input, "id/version").writeText("%04x\n".format(pad.version))
|
||||
File(input, "capabilities").mkdirs()
|
||||
for (name in CAPABILITIES) File(input, "capabilities/$name").writeText("0\n")
|
||||
link(File(input, "subsystem"), GUEST_CLASS)
|
||||
File(event, "uevent").writeText("MAJOR=$MAJOR\nMINOR=${MINOR_BASE + node}\nDEVNAME=input/event$node\n")
|
||||
File(event, "dev").writeText("$MAJOR:${MINOR_BASE + node}\n")
|
||||
link(File(event, "subsystem"), GUEST_CLASS)
|
||||
link(File(event, "device"), "$GUEST_DEVICES/input$node")
|
||||
val record = udevRecord(SystemClock.elapsedRealtimeNanos() / 1000)
|
||||
File(udevData, "+input:input$node").writeText(record)
|
||||
File(udevData, "c$MAJOR:${MINOR_BASE + node}").writeText(record)
|
||||
link(File(classDir, "input$node"), "$GUEST_DEVICES/input$node")
|
||||
link(File(classDir, "event$node"), "$GUEST_DEVICES/input$node/event$node")
|
||||
shown.add(node)
|
||||
Log.i(TAG, "pads: /dev/input/event$node listed for udev as \"${pad.name}\" (%04x:%04x)".format(pad.vendor, pad.product))
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "pads: could not list /dev/input/event$node for udev: $e")
|
||||
hide(node)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hide(node: Int) {
|
||||
File(classDir, "event$node").delete()
|
||||
File(classDir, "input$node").delete()
|
||||
File(udevData, "c$MAJOR:${MINOR_BASE + node}").delete()
|
||||
File(udevData, "+input:input$node").delete()
|
||||
File(devices, "input$node").let { FileUtils.clear(it); it.delete() }
|
||||
if (shown.remove(node)) Log.i(TAG, "pads: /dev/input/event$node no longer listed for udev")
|
||||
}
|
||||
|
||||
private fun link(file: File, target: String) {
|
||||
file.delete()
|
||||
Os.symlink(target, file.path)
|
||||
}
|
||||
|
||||
data class Pad(
|
||||
val bus: Int,
|
||||
val vendor: Int,
|
||||
val product: Int,
|
||||
val version: Int,
|
||||
val name: String,
|
||||
val phys: String,
|
||||
val uniq: String,
|
||||
) {
|
||||
fun uevent(): String =
|
||||
"PRODUCT=%x/%x/%x/%x\nNAME=\"$name\"\nPHYS=\"$phys\"\nUNIQ=\"$uniq\"\nPROP=0\n".format(bus, vendor, product, version)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SessionService"
|
||||
const val GUEST_DEVICES = "/sys/devices/virtual/input"
|
||||
const val GUEST_CLASS = "/sys/class/input"
|
||||
private const val MAJOR = 13
|
||||
private const val MINOR_BASE = 64
|
||||
private const val FIRST_VIRTUAL = 16
|
||||
private const val NODE_LIMIT = 64
|
||||
private val NODE_FILE = Regex("(\\.uinput-)?event\\d+(\\.uevent)?")
|
||||
private val CAPABILITIES = listOf("ev", "key", "rel", "abs", "msc", "sw", "led", "snd", "ff")
|
||||
|
||||
@Volatile private var active: InputSysfsComponent? = null
|
||||
@Volatile private var padType = -1
|
||||
|
||||
fun padTypeChanged(type: Int) {
|
||||
padType = type
|
||||
active?.let { Thread(it::sync, "input-sysfs").start() }
|
||||
}
|
||||
|
||||
internal fun ringWithdrawn(deckPad: Boolean, type: Int) = deckPad && type != FakeInputWriter.PAD_TYPE_XBOX
|
||||
|
||||
internal fun isPadRecord(name: String) =
|
||||
Regex("c$MAJOR:(\\d+)").matchEntire(name)?.groupValues?.get(1)?.toInt()?.let { it in MINOR_BASE until MINOR_BASE + NODE_LIMIT } == true ||
|
||||
Regex("\\+input:input(\\d+)").matchEntire(name)?.groupValues?.get(1)?.toInt()?.let { it < NODE_LIMIT } == true
|
||||
|
||||
internal fun udevRecord(initializedUs: Long) =
|
||||
"I:$initializedUs\nE:ID_INPUT=1\nE:ID_INPUT_JOYSTICK=1\nE:ID_SERIAL=noserial\nG:seat\nG:uaccess\nQ:seat\nQ:uaccess\nV:1\n"
|
||||
|
||||
internal fun ringPad(node: Int, steamVirtual: Boolean) = if (steamVirtual) {
|
||||
Pad(3, 0x28de, 0x11ff, 0x110, "Microsoft X-Box 360 pad $node", "usb-fakeinput/input$node", "0000000000%02d".format(node))
|
||||
} else {
|
||||
Pad(3, 0x045e, 0x028e, 0x110, "Xbox 360 Controller ($node)", "usb-fakeinput/input$node", "0000000000%02d".format(node))
|
||||
}
|
||||
|
||||
internal fun virtualPad(uevent: String): Pad? {
|
||||
val values = uevent.lines().mapNotNull { line ->
|
||||
line.indexOf('=').takeIf { it > 0 }?.let { line.substring(0, it) to line.substring(it + 1).trim('"') }
|
||||
}.toMap()
|
||||
val id = values["PRODUCT"]?.split('/')?.mapNotNull { it.toIntOrNull(16) }?.takeIf { it.size == 4 } ?: return null
|
||||
return Pad(id[0], id[1], id[2], id[3], values["NAME"] ?: return null, values["PHYS"].orEmpty(), values["UNIQ"].orEmpty())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ class SessionService : Service() {
|
||||
|
||||
private val components = java.util.concurrent.CopyOnWriteArrayList<SessionPart>()
|
||||
private var deckBinds: List<String> = emptyList()
|
||||
private var inputSysfs: InputSysfsComponent? = null
|
||||
private val stopLock = Any()
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private var wifiLock: WifiManager.WifiLock? = null
|
||||
@@ -411,6 +412,7 @@ class SessionService : Service() {
|
||||
val controllersOn = !File(Environment.getExternalStorageDirectory(), NO_PAD_SWITCH).exists()
|
||||
SessionState.deckPad = false
|
||||
deckBinds = emptyList()
|
||||
inputSysfs = null
|
||||
if (controllersOn) {
|
||||
addControllerEnvironment(guest, fakeInputDir, sessionDir)
|
||||
SessionState.padBridge?.syncConnectedControllers()
|
||||
@@ -729,6 +731,7 @@ class SessionService : Service() {
|
||||
val padType = SessionPrefs.steamController(this)
|
||||
guest.add("FAKE_EVDEV_PAD_TYPE=$padType")
|
||||
com.droiddeck.launcher.input.FakeInputWriter.setPadType(SessionPrefs.padType(padType))
|
||||
InputSysfsComponent.padTypeChanged(SessionPrefs.padType(padType))
|
||||
if (wantsTouch) com.droiddeck.launcher.input.SteamTouchDevice.prepare(fakeInputDir)?.let {
|
||||
guest.add("FAKE_TOUCHCTL_RING=" + it.path)
|
||||
}
|
||||
@@ -742,6 +745,9 @@ class SessionService : Service() {
|
||||
if (!SessionState.deckPad) guest.add("SDL_JOYSTICK_HIDAPI=0")
|
||||
guest.add("FAKE_EVDEV_LOG=1")
|
||||
guest.add("FAKE_EVDEV_LOG_FILE=" + File(sessionDir, "pad.log").path)
|
||||
inputSysfs = InputSysfsComponent(fakeInputDir, File(fakeInputDir.parentFile!!.parentFile!!, "sys/input"),
|
||||
File(LinuxRuntime.rootDir(this), "run/udev/data"), SessionState.deckPad, !uinput && SessionState.mode == MODE_STEAM)
|
||||
.takeIf { it.prepare() }
|
||||
SessionState.fakeInputDir = fakeInputDir
|
||||
}
|
||||
|
||||
@@ -756,6 +762,11 @@ class SessionService : Service() {
|
||||
val binds = ArrayList<String>()
|
||||
if (controllersOn) binds.add(fakeInputDir.path + ":/dev/input")
|
||||
if (controllersOn) binds.addAll(deckBinds)
|
||||
if (controllersOn) inputSysfs?.let {
|
||||
it.attach(this)
|
||||
components.add(it)
|
||||
binds.addAll(it.binds())
|
||||
}
|
||||
val battery = BatteryComponent(
|
||||
File(filesDir, "session/sys/power_supply"),
|
||||
File(LinuxRuntime.rootDir(this), "run/vpower"),
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import com.droiddeck.launcher.input.FakeInputWriter
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class InputSysfsComponentTest {
|
||||
@Test fun steamInputsVirtualPadKeepsItsIdentity() {
|
||||
val pad = InputSysfsComponent.virtualPad("PRODUCT=3/28de/11ff/1\nNAME=\"Microsoft X-Box 360 pad 0\"\nPHYS=\"\"\n")!!
|
||||
assertEquals(listOf(3, 0x28de, 0x11ff, 1), listOf(pad.bus, pad.vendor, pad.product, pad.version))
|
||||
assertEquals("Microsoft X-Box 360 pad 0", pad.name)
|
||||
assertEquals("", pad.phys)
|
||||
assertTrue(pad.uevent().startsWith("PRODUCT=3/28de/11ff/1\nNAME=\"Microsoft X-Box 360 pad 0\"\n"))
|
||||
}
|
||||
|
||||
@Test fun anUnreadableDescriptionIsNotListed() {
|
||||
assertNull(InputSysfsComponent.virtualPad(""))
|
||||
assertNull(InputSysfsComponent.virtualPad("PRODUCT=3/28de\nNAME=\"pad\"\n"))
|
||||
assertNull(InputSysfsComponent.virtualPad("PRODUCT=3/28de/11ff/1\n"))
|
||||
}
|
||||
|
||||
@Test fun ringPadsMatchWhatTheirIoctlsAnswer() {
|
||||
val xbox = InputSysfsComponent.ringPad(1, steamVirtual = false)
|
||||
assertEquals("Xbox 360 Controller (1)", xbox.name)
|
||||
assertEquals(listOf(0x045e, 0x028e, 0x110), listOf(xbox.vendor, xbox.product, xbox.version))
|
||||
val steam = InputSysfsComponent.ringPad(0, steamVirtual = true)
|
||||
assertEquals("Microsoft X-Box 360 pad 0", steam.name)
|
||||
assertEquals(listOf(0x28de, 0x11ff), listOf(steam.vendor, steam.product))
|
||||
}
|
||||
|
||||
@Test fun ringPadsAreHiddenWhereGamesCannotOpenThem() {
|
||||
assertFalse(InputSysfsComponent.ringWithdrawn(deckPad = false, type = FakeInputWriter.PAD_TYPE_DECK))
|
||||
assertFalse(InputSysfsComponent.ringWithdrawn(deckPad = true, type = FakeInputWriter.PAD_TYPE_XBOX))
|
||||
assertTrue(InputSysfsComponent.ringWithdrawn(deckPad = true, type = FakeInputWriter.PAD_TYPE_DECK))
|
||||
assertTrue(InputSysfsComponent.ringWithdrawn(deckPad = true, type = -1))
|
||||
}
|
||||
|
||||
@Test fun onlyThePadsOwnUdevRecordsAreCleared() {
|
||||
assertTrue(InputSysfsComponent.isPadRecord("c13:64"))
|
||||
assertTrue(InputSysfsComponent.isPadRecord("c13:80"))
|
||||
assertTrue(InputSysfsComponent.isPadRecord("+input:input16"))
|
||||
assertFalse(InputSysfsComponent.isPadRecord("c13:63"))
|
||||
assertFalse(InputSysfsComponent.isPadRecord("c13:128"))
|
||||
assertFalse(InputSysfsComponent.isPadRecord("c240:16"))
|
||||
assertFalse(InputSysfsComponent.isPadRecord("+input:input64"))
|
||||
}
|
||||
|
||||
@Test fun theUdevRecordMarksAnInitializedJoystick() {
|
||||
val record = InputSysfsComponent.udevRecord(42).lines()
|
||||
assertEquals("I:42", record.first())
|
||||
assertTrue("E:ID_INPUT_JOYSTICK=1" in record)
|
||||
assertTrue("E:ID_INPUT=1" in record)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user