mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-15 03:37:09 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a6877423e |
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -22,6 +22,8 @@ class App : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
CrashHandler.install(this)
|
||||
// Before anything else touches the session prefs, so the first session already runs in Deck mode.
|
||||
com.droiddeck.launcher.session.SessionPrefs.settleDeckModeDefault(this)
|
||||
com.droiddeck.launcher.session.SessionPrefs.settleGpuDriverMode(this)
|
||||
com.droiddeck.launcher.update.AppUpdates.init(this)
|
||||
GpuClockPin.clearLeftover(this)
|
||||
|
||||
@@ -121,6 +121,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var showNonAdreno by mutableStateOf<LinuxRuntimeInstaller.Release?>(null)
|
||||
private var glThread by mutableStateOf(true)
|
||||
private var noGlError by mutableStateOf(true)
|
||||
private var steamDeckMode by mutableStateOf(false)
|
||||
private var steamRepairQueued by mutableStateOf(false)
|
||||
private var mangoapp by mutableStateOf(true)
|
||||
private var steamController by mutableStateOf(SessionPrefs.CONTROLLER_DECK)
|
||||
@@ -343,6 +344,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var resolution by mutableStateOf(com.droiddeck.launcher.session.SessionDisplay.DEFAULT_RESOLUTION)
|
||||
private var fexPreset by mutableStateOf("")
|
||||
private var forceSsbs by mutableStateOf(false)
|
||||
private var steamChannel by mutableStateOf("steamdeck_publicbeta")
|
||||
private var runSteamAtStartup by mutableStateOf(false)
|
||||
private var theme by mutableStateOf("graphite")
|
||||
private var appScale by mutableStateOf(com.droiddeck.launcher.core.AppUiPrefs.DEFAULT_SCALE)
|
||||
@@ -1299,6 +1301,8 @@ class MainActivity : ComponentActivity() {
|
||||
fexPreset = if (mode == SessionService.MODE_STEAM) fexPreset else null,
|
||||
forceSsbs = forceSsbs,
|
||||
syncBackend = if (mode == SessionService.MODE_STEAM) SessionPrefs.syncBackendOf(fastSync, fsyncFirst, syncFallback) else null,
|
||||
steamChannel = if (mode == SessionService.MODE_STEAM) steamChannel else null,
|
||||
steamDeckMode = mode == SessionService.MODE_STEAM && steamDeckMode,
|
||||
steamRepairQueued = steamRepairQueued,
|
||||
mangoapp = mangoapp,
|
||||
steamController = if (mode == SessionService.MODE_STEAM) steamController else null,
|
||||
@@ -1376,7 +1380,13 @@ class MainActivity : ComponentActivity() {
|
||||
fsyncFirst = SessionPrefs.fsyncFirst(this)
|
||||
syncFallback = SessionPrefs.syncFallback(this)
|
||||
},
|
||||
onSteamChannel = { id -> SessionPrefs.setSteamChannel(this, id); steamChannel = id },
|
||||
onSteamRepair = { steamRepairQueued = SteamRepair.queue(this) },
|
||||
onSteamDeckMode = { on ->
|
||||
SessionPrefs.setSteamDeckMode(this, on)
|
||||
steamDeckMode = on
|
||||
steamChannel = SessionPrefs.steamChannel(this)
|
||||
},
|
||||
onMangoapp = { on -> SessionPrefs.setMangoapp(this, on); mangoapp = on },
|
||||
onSteamController = { id -> SessionPrefs.setSteamController(this, id); steamController = id },
|
||||
onRunSteamAtStartup = { on ->
|
||||
@@ -1509,6 +1519,8 @@ class MainActivity : ComponentActivity() {
|
||||
fastSync = SessionPrefs.fastSync(this)
|
||||
fsyncFirst = SessionPrefs.fsyncFirst(this)
|
||||
syncFallback = SessionPrefs.syncFallback(this)
|
||||
steamChannel = SessionPrefs.steamChannel(this)
|
||||
steamDeckMode = SessionPrefs.steamDeckMode(this)
|
||||
steamRepairQueued = SteamRepair.queued(this)
|
||||
mangoapp = SessionPrefs.mangoapp(this)
|
||||
steamController = SessionPrefs.steamController(this)
|
||||
@@ -1567,6 +1579,7 @@ class MainActivity : ComponentActivity() {
|
||||
clientOverride = SessionPrefs.clientCpusOverride(this)
|
||||
glThread = SessionPrefs.glThread(this)
|
||||
noGlError = SessionPrefs.noGlError(this)
|
||||
steamDeckMode = SessionPrefs.steamDeckMode(this)
|
||||
clientCores = CpuCores.parse(SessionPrefs.clientCpus(this)).ifEmpty { CpuCores.all.toSet() }
|
||||
gameCores = CpuCores.parse(SessionPrefs.gameCpus(this)).ifEmpty { CpuCores.all.toSet() }
|
||||
tuSysmem = SessionPrefs.tuSysmem(this)
|
||||
|
||||
@@ -138,8 +138,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private fun openDeckRing(why: String) {
|
||||
val deck = SessionState.deckPad
|
||||
val opened = if (deck) padBridge?.start() else null
|
||||
// Every pad already connected is a player from the start, before its first press.
|
||||
padBridge?.claimConnected()
|
||||
Log.i(TAG, "deck ring ($why): deck pad $deck" + when (opened) {
|
||||
null -> ", left for first input"
|
||||
true -> ", ring open"
|
||||
@@ -252,8 +250,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var drawerPage by mutableStateOf(SessionDrawerPage.CONTROLLER)
|
||||
private var drawerControllerActive by mutableStateOf(false)
|
||||
private var backActionsInverted by mutableStateOf(false)
|
||||
/** What the pad is to the Steam client (SessionPrefs.CONTROLLER_*), switched from the drawer. */
|
||||
private var padType by mutableStateOf(SessionPrefs.CONTROLLER_DECK)
|
||||
/** The on-screen PC keyboard (ui/PcKeyboard): real key presses, Esc and F1 included. */
|
||||
private var pcKeyboardOpen by mutableStateOf(false)
|
||||
private var hudOn by mutableStateOf(true)
|
||||
@@ -312,7 +308,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private val deviceListener = object : InputManager.InputDeviceListener {
|
||||
override fun onInputDeviceAdded(deviceId: Int) {
|
||||
logInputDevice("connected", deviceId)
|
||||
padBridge?.claimConnected()
|
||||
updateOnScreenControls()
|
||||
updatePointerCapture()
|
||||
}
|
||||
@@ -592,14 +587,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
onKeyboardButton = { on -> updateControllerPrefs { ControllerPrefs.setKeyboardButton(this@SessionActivity, on) } },
|
||||
onSteamMenu = if (SessionState.mode == SessionService.MODE_STEAM) ({ sendSteamGuide() }) else null,
|
||||
onQam = if (SessionState.mode == SessionService.MODE_STEAM) ({ triggerSteamQam() }) else null,
|
||||
// Only where the session set the pad up to be switched (SessionService: deckPad).
|
||||
padType = if (SessionState.mode == SessionService.MODE_STEAM && SessionState.deckPad) padType else null,
|
||||
onPadType = { id ->
|
||||
SessionPrefs.setSteamController(this@SessionActivity, id)
|
||||
com.droiddeck.launcher.session.SteamDeckPad.showPad(SessionPrefs.padType(id))
|
||||
com.droiddeck.launcher.input.FakeInputWriter.setPadType(SessionPrefs.padType(id))
|
||||
padType = id
|
||||
},
|
||||
backActionsInverted = backActionsInverted,
|
||||
onBackActionsInverted = { inverted ->
|
||||
SessionPrefs.setBackActionsInverted(this@SessionActivity, inverted)
|
||||
@@ -860,7 +847,6 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
steamDownloadsInBackground = SessionPrefs.steamDownloadsInBackground(this)
|
||||
oscMode = SessionPrefs.oscMode(this)
|
||||
backActionsInverted = SessionPrefs.backActionsInverted(this)
|
||||
padType = SessionPrefs.steamController(this)
|
||||
}
|
||||
|
||||
// ── Compositor ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -152,6 +152,7 @@ object DeviceReport {
|
||||
k("Zink lazy descriptors", SessionPrefs.zinkLazy(context))
|
||||
k("Threaded GL (glthread)", SessionPrefs.glThread(context))
|
||||
k("No GL error checks", SessionPrefs.noGlError(context))
|
||||
k("Steam Deck mode", SessionPrefs.steamDeckMode(context))
|
||||
k("Steam controller", SessionPrefs.steamController(context))
|
||||
k("Upscaler", (SessionPrefs.upscalerChoices.firstOrNull { it.first == SessionPrefs.upscaler(context) }?.second ?: "Off") +
|
||||
" (sharpness ${SessionPrefs.upscaleSharpness(context)}%)")
|
||||
|
||||
@@ -107,22 +107,8 @@ public class FakeInputWriter {
|
||||
private static final int DECK_PADS_OFFSET = IMU_OFFSET + 28; // short[4]: left X, Y, right X, Y
|
||||
private static final int DECK_PRESSURE_OFFSET = IMU_OFFSET + 36; // short[2]: left, right
|
||||
private static final int DECK_CONTROLS_OFFSET = IMU_OFFSET + 40; // int: DeckControls bits
|
||||
// Slot 0 only: what the pad is to the Steam client (libfakeinput's PAD_TYPE_*), | PAD_TYPE_SET.
|
||||
private static final int PAD_TYPE_OFFSET = IMU_OFFSET + 44;
|
||||
private static final int PAD_TYPE_SET = 0x80000000;
|
||||
public static final int PAD_TYPE_DECK = 0;
|
||||
public static final int PAD_TYPE_STEAM = 1;
|
||||
public static final int PAD_TYPE_XBOX = 2;
|
||||
public static final int PAD_TYPE_DS4 = 3;
|
||||
public static final int PAD_TYPE_DUALSENSE = 4;
|
||||
/** The type chosen this process, written into a ring made after the choice too; -1 for none. */
|
||||
private static volatile int padType = -1;
|
||||
/** The paired controller's state, likewise written into a ring made after it connected. */
|
||||
private static volatile boolean tritonConnected = false;
|
||||
private static final int TRITON_OFFSET = RING_SIZE + 64;
|
||||
private static final int TRITON_MAGIC = 0x4e525454; // TTRN
|
||||
// Whether the paired controller is connected: libfakeinput offers its device only then.
|
||||
private static final int TRITON_CONNECTED_OFFSET = TRITON_OFFSET + 4;
|
||||
private static final int TRITON_INPUT_SEQ_OFFSET = TRITON_OFFSET + 8;
|
||||
private static final int TRITON_REPORT_ID_OFFSET = TRITON_OFFSET + 16;
|
||||
private static final int TRITON_REPORT_LENGTH_OFFSET = TRITON_OFFSET + 20;
|
||||
@@ -255,10 +241,7 @@ public class FakeInputWriter {
|
||||
data.putShort(IMU_ACCEL_OFFSET + (i * 2), (short) 0);
|
||||
}
|
||||
data.putInt(DECK_CONTROLS_OFFSET, 0);
|
||||
int type = padType;
|
||||
data.putInt(PAD_TYPE_OFFSET, type >= 0 ? PAD_TYPE_SET | type : 0);
|
||||
data.putInt(TRITON_OFFSET, TRITON_MAGIC);
|
||||
data.putInt(TRITON_CONNECTED_OFFSET, tritonConnected ? 1 : 0);
|
||||
data.putLong(TRITON_INPUT_SEQ_OFFSET, 0L);
|
||||
data.putInt(TRITON_REPORT_ID_OFFSET, 0);
|
||||
data.putInt(TRITON_REPORT_LENGTH_OFFSET, 0);
|
||||
@@ -566,38 +549,6 @@ public class FakeInputWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches what the pad is to the Steam client while the session runs ([PAD_TYPE_DECK],
|
||||
* [PAD_TYPE_STEAM] or [PAD_TYPE_XBOX]); libfakeinput unplugs the old device and plugs in the new.
|
||||
*/
|
||||
public static void setPadType(int type) {
|
||||
padType = type;
|
||||
RingSlot ringSlot;
|
||||
synchronized (RING_LOCK) {
|
||||
ringSlot = RING_SLOTS[0];
|
||||
}
|
||||
if (ringSlot == null) return;
|
||||
synchronized (ringSlot) {
|
||||
ByteBuffer ring = ringSlot.data;
|
||||
if (ring == null) return;
|
||||
nativeStoreFence();
|
||||
ring.putInt(PAD_TYPE_OFFSET, PAD_TYPE_SET | type);
|
||||
}
|
||||
}
|
||||
|
||||
/** The paired Steam Controller came or went (SteamControllerBle): its device is plugged in or out. */
|
||||
public static void setTritonConnected(int slot, boolean connected) {
|
||||
tritonConnected = connected;
|
||||
RingSlot ringSlot = tritonSlot(slot);
|
||||
if (ringSlot == null) return;
|
||||
synchronized (ringSlot) {
|
||||
ByteBuffer ring = ringSlot.data;
|
||||
if (ring == null || ring.getInt(TRITON_OFFSET) != TRITON_MAGIC) return;
|
||||
nativeStoreFence();
|
||||
ring.putInt(TRITON_CONNECTED_OFFSET, connected ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
/** Publishes a native Triton input report (without the HID report ID byte). */
|
||||
public static void writeTritonState(int slot, int reportId, byte[] report) {
|
||||
if (report == null || report.length == 0 || report.length > TRITON_REPORT_MAX) return;
|
||||
|
||||
@@ -29,9 +29,7 @@ import java.util.concurrent.atomic.AtomicIntegerArray;
|
||||
* Guide-then-A chord the client itself answers.
|
||||
*
|
||||
* <p>Each further controller is another player: slots 1 to 3, an Xbox 360 pad each, which in a
|
||||
* Steam session only the client reads (libfakeinput) and hands games through Steam Input. Slot 0
|
||||
* is the device's own controls wherever it has them: a Bluetooth pad pressed first used to take
|
||||
* it, and played as the handheld while its built-in pad became player 2.
|
||||
* Steam session only the client reads (libfakeinput) and hands games through Steam Input.
|
||||
*/
|
||||
public final class PadBridge {
|
||||
private static final String TAG = "PadBridge";
|
||||
@@ -103,17 +101,6 @@ public final class PadBridge {
|
||||
|
||||
/** No physical controller is driving the pad: nothing yet, or the on-screen controls were last. */
|
||||
public static final int NO_CONTROLLER = -1;
|
||||
|
||||
// AYN's handhelds (the Thor) present their own pad as a USB device Android calls external,
|
||||
// beside the 2020:0111 it re-presents each Bluetooth Xbox pad as.
|
||||
private static final int AYN_VENDOR = 0x2020;
|
||||
private static final int AYN_BUILT_IN_PAD = 0x0112;
|
||||
|
||||
/** Whether this controller is the device's own controls rather than a pad connected to it. */
|
||||
static boolean isBuiltIn(InputDevice device) {
|
||||
return !device.isExternal()
|
||||
|| (device.getVendorId() == AYN_VENDOR && device.getProductId() == AYN_BUILT_IN_PAD);
|
||||
}
|
||||
/**
|
||||
* The Android input device id of the physical controller that last drove any slot, or
|
||||
* {@link #NO_CONTROLLER}: where rumble goes when it names no slot of ours (RumbleComponent).
|
||||
@@ -424,19 +411,6 @@ public final class PadBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives every connected controller its slot now, so the client lists each one as it is
|
||||
* connected instead of on its first press: a further player's pad is plugged in, at rest.
|
||||
*/
|
||||
public synchronized void claimConnected() {
|
||||
for (int id : InputDevice.getDeviceIds()) {
|
||||
InputDevice device = InputDevice.getDevice(id);
|
||||
if (!isFromController(device)) continue;
|
||||
int slot = slotFor(device);
|
||||
if (slot > 0 && playerWriters[slot] == null) publish(slot);
|
||||
}
|
||||
}
|
||||
|
||||
/** A controller went away: its player's buttons are let go, and a further player's pad is unplugged. */
|
||||
public synchronized void onDeviceRemoved(int deviceId) {
|
||||
for (int slot = 0; slot < SLOTS; slot++) {
|
||||
@@ -497,55 +471,23 @@ public final class PadBridge {
|
||||
publish();
|
||||
}
|
||||
|
||||
/**
|
||||
* The slot this controller plays in: the one it has, else slot 0 for the device's own controls
|
||||
* and a further player's slot for a connected pad (slot 0 too while the device has no controls
|
||||
* of its own), else player 1's.
|
||||
*/
|
||||
/** The slot this controller plays in: the one it has, else the first free one, else player 1's. */
|
||||
private int slotFor(InputDevice device) {
|
||||
int id = device.getId();
|
||||
for (int slot = 0; slot < SLOTS; slot++) {
|
||||
if (owners[0] == id) return 0;
|
||||
for (int slot = 1; slot < SLOTS; slot++) {
|
||||
if (owners[slot] == id) return slot;
|
||||
}
|
||||
for (int slot = 0; slot < SLOTS; slot++) {
|
||||
if (owners[slot] != NO_CONTROLLER && InputDevice.getDevice(owners[slot]) == null) free(slot);
|
||||
}
|
||||
if (isBuiltIn(device)) {
|
||||
// A connected pad playing as the device's own controls moves to a player's slot of its own.
|
||||
int was = owners[0];
|
||||
if (was != NO_CONTROLLER) {
|
||||
free(0);
|
||||
InputDevice other = InputDevice.getDevice(was);
|
||||
if (other != null && !isBuiltIn(other)) {
|
||||
int to = claim(other, 1);
|
||||
if (to > 0) publish(to);
|
||||
}
|
||||
}
|
||||
owners[0] = id;
|
||||
Log.i(TAG, "\"" + device.getName() + "\" (id " + id + ") is player 1, the device's own controls");
|
||||
return 0;
|
||||
}
|
||||
int slot = claim(device, builtInConnected() ? 1 : 0);
|
||||
return slot >= 0 ? slot : 0;
|
||||
}
|
||||
|
||||
/** Takes the first free slot from {@code from} for this controller, or -1 if none is free. */
|
||||
private int claim(InputDevice device, int from) {
|
||||
for (int slot = from; slot < SLOTS; slot++) {
|
||||
for (int slot = 0; slot < SLOTS; slot++) {
|
||||
if (owners[slot] != NO_CONTROLLER) continue;
|
||||
owners[slot] = device.getId();
|
||||
Log.i(TAG, "\"" + device.getName() + "\" (id " + device.getId() + ") is player " + (slot + 1));
|
||||
owners[slot] = id;
|
||||
Log.i(TAG, "\"" + device.getName() + "\" (id " + id + ") is player " + (slot + 1));
|
||||
return slot;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static boolean builtInConnected() {
|
||||
for (int id : InputDevice.getDeviceIds()) {
|
||||
InputDevice device = InputDevice.getDevice(id);
|
||||
if (isFromController(device) && isBuiltIn(device)) return true;
|
||||
}
|
||||
return false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private PadState stateFor(int slot) {
|
||||
|
||||
@@ -92,7 +92,6 @@ class SteamControllerBle(private val context: Context) {
|
||||
queuedWrites.clear()
|
||||
outputReports.clear()
|
||||
connected = false
|
||||
FakeInputWriter.setTritonConnected(SLOT, false)
|
||||
g.disconnect()
|
||||
g.close()
|
||||
release()
|
||||
@@ -130,7 +129,6 @@ class SteamControllerBle(private val context: Context) {
|
||||
if (g !== gatt || descriptor.characteristic.uuid != input) return
|
||||
Log.i(TAG, "subscribed to report 0x%02x (status %d)".format(reportId, status))
|
||||
connected = status == BluetoothGatt.GATT_SUCCESS
|
||||
FakeInputWriter.setTritonConnected(SLOT, connected)
|
||||
listener?.invoke()
|
||||
if (connected) {
|
||||
refreshLizardMode()
|
||||
|
||||
@@ -277,6 +277,12 @@ public final class LinuxRuntime {
|
||||
bind(cmd, fake.getPath() + ":" + entry[1]);
|
||||
}
|
||||
}
|
||||
// libpci prefers sysfs to procfs, and where the app may list /sys/bus/pci/devices but not
|
||||
// read the files of a device in it (a RedMagic's PCIe root port) it die()s the same way.
|
||||
// Every Chromium GPU process exits with it: the Steam client's, an Electron app's.
|
||||
if (unreadablePciDevice()) {
|
||||
bind(cmd, new File(root, "etc/droiddeck/empty").getPath() + ":/sys/bus/pci/devices");
|
||||
}
|
||||
bindGpuNode(context, cmd);
|
||||
bindAdrenoStats(cmd, extraBinds);
|
||||
bindCpuTemps(cmd, root);
|
||||
@@ -288,6 +294,16 @@ public final class LinuxRuntime {
|
||||
return specs;
|
||||
}
|
||||
|
||||
/** Whether /sys/bus/pci/devices lists a device whose vendor the app may not read. */
|
||||
private static boolean unreadablePciDevice() {
|
||||
File[] devices = new File("/sys/bus/pci/devices").listFiles();
|
||||
if (devices == null) return false;
|
||||
for (File device : devices) {
|
||||
if (!new File(device, "vendor").canRead()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* An app process may not open {@code /dev/dri} - the nodes exist but are labelled
|
||||
* {@code graphics_device}, which stock policy grants surfaceflinger and not us - yet libdrm and
|
||||
|
||||
@@ -19,9 +19,6 @@ object SessionPrefs {
|
||||
|
||||
const val CONTROLLER_DECK = "deck"
|
||||
const val CONTROLLER_XBOX360 = "xbox360"
|
||||
const val CONTROLLER_STEAM = "steamcontroller"
|
||||
const val CONTROLLER_DS4 = "dualshock4"
|
||||
const val CONTROLLER_DUALSENSE = "dualsense"
|
||||
const val OSC_AUTO = "auto"
|
||||
const val OSC_ALWAYS = "always"
|
||||
const val OSC_STEAM_QAM = "steam-qam"
|
||||
@@ -134,9 +131,9 @@ object SessionPrefs {
|
||||
|
||||
private fun hudKey(context: Context) = if (mangoappSession(context)) "hudDeck" else "hud"
|
||||
|
||||
/** A Steam session with Deck mode's performance overlay (mangoapp). */
|
||||
/** A Steam session that runs Deck mode with its performance overlay (mangoapp). */
|
||||
private fun mangoappSession(context: Context) =
|
||||
SessionState.mode == SessionService.MODE_STEAM && mangoapp(context)
|
||||
SessionState.mode == SessionService.MODE_STEAM && steamDeckMode(context) && mangoapp(context)
|
||||
|
||||
/** When enabled, a single Back opens Steam QAM and a double Back opens the session menu. */
|
||||
fun backActionsInverted(context: Context): Boolean = prefs(context).getBoolean("backActionsInverted", false)
|
||||
@@ -452,21 +449,27 @@ object SessionPrefs {
|
||||
|
||||
/**
|
||||
* What the pad is to the Steam client: [CONTROLLER_DECK], a Steam Deck controller (Quick Access
|
||||
* button, gyro, Steam Input's full treatment - SteamDeckPad), [CONTROLLER_STEAM], a 2025 Steam
|
||||
* Controller (the same, plus grip sense and its own haptics), [CONTROLLER_DS4] or
|
||||
* [CONTROLLER_DUALSENSE], a PlayStation pad with its touchpad fed by the trackpads, or
|
||||
* [CONTROLLER_XBOX360], the plain Xbox 360 pad of earlier versions (QAM by the Guide+A chord).
|
||||
* button, gyro, Steam Input's full treatment - SteamDeckPad), or [CONTROLLER_XBOX360], the plain
|
||||
* Xbox 360 pad of earlier versions (QAM by the Guide+A chord).
|
||||
*/
|
||||
fun steamController(context: Context): String =
|
||||
prefs(context).getString("steamController", CONTROLLER_DECK) ?: CONTROLLER_DECK
|
||||
fun setSteamController(context: Context, id: String) { prefs(context).edit().putString("steamController", id).apply() }
|
||||
/** [steamController]'s choice as libfakeinput's pad type (FakeInputWriter.PAD_TYPE_*). */
|
||||
fun padType(id: String): Int = when (id) {
|
||||
CONTROLLER_STEAM -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_STEAM
|
||||
CONTROLLER_XBOX360 -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_XBOX
|
||||
CONTROLLER_DS4 -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DS4
|
||||
CONTROLLER_DUALSENSE -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DUALSENSE
|
||||
else -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DECK
|
||||
|
||||
/** Runs the SteamOS gamepad client with its Quick Access performance controls. On by default (settleDeckModeDefault). */
|
||||
fun steamDeckMode(context: Context): Boolean = prefs(context).getBoolean("steamDeckMode", true)
|
||||
fun setSteamDeckMode(context: Context, on: Boolean) { prefs(context).edit().putBoolean("steamDeckMode", on).apply() }
|
||||
|
||||
/**
|
||||
* Deck mode is the default: every install, new or from before, is moved to it once. An install
|
||||
* from before 0.3.0 had off written down for it whether or not anyone chose it, so the move
|
||||
* can't tell a choice from that default and moves everyone; turning it off afterwards sticks.
|
||||
* Run once at process start, before anything reads or writes these prefs.
|
||||
*/
|
||||
fun settleDeckModeDefault(context: Context) {
|
||||
val p = prefs(context)
|
||||
if (p.getBoolean("deckModeMoved", false)) return
|
||||
p.edit().putBoolean("steamDeckMode", true).putBoolean("deckModeMoved", true).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -600,6 +603,24 @@ object SessionPrefs {
|
||||
.onFailure { android.util.Log.e("GameEnvironment", "Could not update game environment", it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* The Steam client branch used for the first download and forced on the command line:
|
||||
* "steamdeck_publicbeta" by default, or "publicbeta" when chosen with Deck mode off.
|
||||
* Deck mode always takes the Deck branch, whatever was chosen:
|
||||
* with -steamos3 the client picks its own branch as SteamOS does, and on publicbeta it settled
|
||||
* on steamdeck_stable - an older client it then offered as a "Software Update" in every session,
|
||||
* which applying turns into the exit-42 restart loop (seen on device 2026-09-30). Earlier, Deck
|
||||
* mode on publicbeta also reinstalled the same client at every start (2026-09-23). On
|
||||
* steamdeck_publicbeta the client finds no update. The choice applies with Deck mode off.
|
||||
*/
|
||||
fun steamChannel(context: Context): String =
|
||||
if (steamDeckMode(context)) "steamdeck_publicbeta"
|
||||
else prefs(context).getString("steamChannel", null) ?: "steamdeck_publicbeta"
|
||||
|
||||
fun setSteamChannel(context: Context, id: String) {
|
||||
prefs(context).edit().putString("steamChannel", id).apply()
|
||||
}
|
||||
|
||||
/** Whether opening DroidDeck starts a Steam session instead of showing the front end. */
|
||||
fun runSteamAtStartup(context: Context): Boolean = prefs(context).getBoolean("runSteamAtStartup", false)
|
||||
|
||||
|
||||
@@ -625,7 +625,8 @@ class SessionService : Service() {
|
||||
guest.add("ZINK_DEBUG=compact")
|
||||
}
|
||||
// The rest of the client-interface switches (SessionPrefs): GL marshalled off the calling
|
||||
// thread and no GL error checks.
|
||||
// thread, no GL error checks, and the client run as SteamOS runs it (the script reads
|
||||
// BL_STEAMDECK; it is the one that builds the command line).
|
||||
if (SessionPrefs.glThread(this)) guest.add("mesa_glthread=true")
|
||||
if (SessionPrefs.noGlError(this)) guest.add("MESA_NO_ERROR=1")
|
||||
// Mesa's shader cache as one database instead of a file per entry. Every lookup in the
|
||||
@@ -638,7 +639,9 @@ class SessionService : Service() {
|
||||
// every thread start paid a SIGSYS that proot answers; the malloc top pad grows the heap
|
||||
// 16 MB at a time instead of 128 KB, and every brk(2) is a proot stop too.
|
||||
guest.add("GLIBC_TUNABLES=glibc.pthread.rseq=0:glibc.malloc.top_pad=16777216")
|
||||
if (SessionState.mode == MODE_STEAM) guest.add("BL_STEAMDECK=" + (if (SessionPrefs.steamDeckMode(this)) "1" else "0"))
|
||||
if (SessionState.mode == MODE_STEAM) guest.add("BL_MANGOAPP=" + (if (SessionPrefs.mangoapp(this)) "1" else "0"))
|
||||
if (steamHere) guest.add("BL_STEAM_CHANNEL=" + SessionPrefs.steamChannel(this))
|
||||
if (SessionState.mode == MODE_STEAM) {
|
||||
guest.add("BL_GAMESCOPE_STRETCH_16X9=" + (if (SessionPrefs.stretch16x9(this)) "1" else "0"))
|
||||
}
|
||||
@@ -742,18 +745,16 @@ class SessionService : Service() {
|
||||
// hidraw (SteamDeckPad), which only works with Steam Input's virtual pad for games to read.
|
||||
// Decided only once its sysfs is in place: without it the client would find no Deck, and
|
||||
// with the Deck asked for the pad's own nodes are withdrawn - no controller at all.
|
||||
// Set up whatever the pad is to be, so the drawer can switch it between a Deck, a Steam
|
||||
// Controller and an Xbox 360 pad while the session runs (FakeInputWriter.setPadType).
|
||||
val wantsDeck = uinput && SessionState.mode == MODE_STEAM &&
|
||||
SessionState.steamUi != "desktop" &&
|
||||
SessionPrefs.steamController(this) == SessionPrefs.CONTROLLER_DECK &&
|
||||
!File(Environment.getExternalStorageDirectory(), NO_DECK_PAD_SWITCH).exists()
|
||||
// Steam's touch controller rides on the Deck's sysfs and is offered only beside it.
|
||||
val wantsTouch = wantsDeck && !File(Environment.getExternalStorageDirectory(), NO_STEAM_TOUCH_SWITCH).exists()
|
||||
// A paired Triton is exposed as a second native Steam Controller HID, with its original
|
||||
// report IDs and output path, beside whatever the handheld's own pad is.
|
||||
// report IDs and output path. The handheld's own pad remains the Deck target.
|
||||
val wantsTriton = wantsDeck && SteamControllerBle.hasPairedController(this)
|
||||
deckBinds = if (wantsDeck) SteamDeckPad.prepare(this, fakeInputDir.parentFile!!.parentFile!!, wantsTouch, wantsTriton,
|
||||
SessionPrefs.padType(SessionPrefs.steamController(this))) else emptyList()
|
||||
deckBinds = if (wantsDeck) SteamDeckPad.prepare(this, fakeInputDir.parentFile!!.parentFile!!, wantsTouch, wantsTriton) else emptyList()
|
||||
SessionState.deckPad = deckBinds.isNotEmpty()
|
||||
if (wantsDeck && !SessionState.deckPad) Log.w(TAG, "deck pad: not available this session; the pad stays an Xbox 360 controller")
|
||||
logControllersAtStart()
|
||||
@@ -764,10 +765,6 @@ class SessionService : Service() {
|
||||
if (SessionState.deckPad) {
|
||||
guest.add("FAKE_EVDEV_DECK=1")
|
||||
if (wantsTriton) guest.add("FAKE_EVDEV_TRITON=1")
|
||||
// What the pad starts as; the drawer's changes reach libfakeinput through the ring.
|
||||
val padType = SessionPrefs.steamController(this)
|
||||
guest.add("FAKE_EVDEV_PAD_TYPE=$padType")
|
||||
com.droiddeck.launcher.input.FakeInputWriter.setPadType(SessionPrefs.padType(padType))
|
||||
// Steam's touch controller (SteamTouchDevice): the file the app and libfakeinput share.
|
||||
if (wantsTouch) com.droiddeck.launcher.input.SteamTouchDevice.prepare(fakeInputDir)?.let {
|
||||
guest.add("FAKE_TOUCHCTL_RING=" + it.path)
|
||||
@@ -1782,7 +1779,7 @@ class SessionService : Service() {
|
||||
pads.add(String.format(java.util.Locale.ROOT, "\"%s\" (%04x:%04x)", d.name, d.vendorId, d.productId))
|
||||
}
|
||||
Log.i(TAG, "controllers at start: " + (if (pads.isEmpty()) "none" else pads.joinToString(", ")) +
|
||||
"; presented to the guest as " + if (SessionState.deckPad) "switchable, starting as " + SessionPrefs.steamController(this) else "an Xbox 360 controller")
|
||||
"; presented to the guest as " + if (SessionState.deckPad) "a Steam Deck controller" else "an Xbox 360 controller")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -47,32 +47,10 @@ object SteamDeckPad {
|
||||
// The paired Bluetooth Steam Controller is a separate Triton HID device. It must not inherit
|
||||
// the Deck's identity or report format. Must match fakeinput_steam.cpp (TRITON_HIDRAW_*).
|
||||
private const val TRITON_MINOR = 18
|
||||
private const val TRITON_NODE = "hidraw$TRITON_MINOR"
|
||||
private const val TRITON_HID = "$GUEST_DEVICES/bluetooth/0005:28DE:1303.0003"
|
||||
private const val TRITON_HIDRAW = "$TRITON_HID/hidraw/$TRITON_NODE"
|
||||
private const val TRITON_SERIAL = "DROIDDECKSC01"
|
||||
// The handheld's own pad as the Steam Controller (SessionPrefs.CONTROLLER_STEAM): the wired
|
||||
// controller (28de:1302) on a USB device of its own, since it is built in.
|
||||
private const val TRITON_USB = "$GUEST_DEVICES/usb3"
|
||||
private const val TRITON_INTERFACE = "$TRITON_USB/3-1:1.0"
|
||||
private const val TRITON_WIRED_HID = "$TRITON_INTERFACE/0003:28DE:1302.0004"
|
||||
/** Must match libfakeinput (DS4_HIDRAW_MINOR, DUALSENSE_HIDRAW_MINOR). */
|
||||
private const val SONY_DS4_MINOR = 20
|
||||
private const val SONY_DUALSENSE_MINOR = 21
|
||||
/** Must match libfakeinput's kDs4ReportDescriptor and kDualSenseReportDescriptor. */
|
||||
private val DS4_REPORT_DESCRIPTOR = intArrayOf(
|
||||
0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x06, 0x00, 0xff, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08,
|
||||
0x85, 0x01, 0x09, 0x20, 0x95, 0x3f, 0x81, 0x02, 0x85, 0x05, 0x09, 0x21, 0x95, 0x1f, 0x91, 0x02,
|
||||
0x85, 0x02, 0x09, 0x24, 0x95, 0x24, 0xb1, 0x02, 0x85, 0x12, 0x09, 0x25, 0x95, 0x0f, 0xb1, 0x02,
|
||||
0x85, 0xa3, 0x09, 0x26, 0x95, 0x30, 0xb1, 0x02, 0x85, 0x81, 0x09, 0x27, 0x95, 0x3f, 0xb1, 0x02,
|
||||
0xc0,
|
||||
).map { it.toByte() }.toByteArray()
|
||||
private val DUALSENSE_REPORT_DESCRIPTOR = intArrayOf(
|
||||
0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x06, 0x00, 0xff, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08,
|
||||
0x85, 0x01, 0x09, 0x20, 0x95, 0x3f, 0x81, 0x02, 0x85, 0x02, 0x09, 0x21, 0x95, 0x2f, 0x91, 0x02,
|
||||
0x85, 0x05, 0x09, 0x22, 0x95, 0x28, 0xb1, 0x02, 0x85, 0x09, 0x09, 0x23, 0x95, 0x13, 0xb1, 0x02,
|
||||
0x85, 0x20, 0x09, 0x24, 0x95, 0x3f, 0xb1, 0x02, 0xc0,
|
||||
).map { it.toByte() }.toByteArray()
|
||||
/** Must match libfakeinput (TRITON_PAD_HIDRAW_*): its own node, so a paired controller keeps 18. */
|
||||
private const val TRITON_PAD_MINOR = 19
|
||||
|
||||
// Steam's touch controller (SteamTouchDevice), a second device beside the Deck's: 0000:11fb,
|
||||
// "Mobile Touch Control", on a USB device of its own. Must match libfakeinput (TOUCH_*).
|
||||
@@ -115,49 +93,11 @@ object SteamDeckPad {
|
||||
0xc0,
|
||||
).map { it.toByte() }.toByteArray()
|
||||
|
||||
/** The wired controller's: its 0x42 state report in place of the Bluetooth 0x45. */
|
||||
private val TRITON_WIRED_REPORT_DESCRIPTOR = TRITON_REPORT_DESCRIPTOR.copyOf().also { it[15] = 0x42 }
|
||||
|
||||
/** Where [prepare] puts the stand-in listings; bound into the guest at the same path. */
|
||||
fun listingDir(sessionRoot: File) = File(sessionRoot, "sys/deck/listing")
|
||||
|
||||
/** Writes the tree and returns the `host:guest` binds for it, or none if it could not be made. */
|
||||
/** The pad's own devices by minor (the Deck, its Steam Controller, the PlayStation pads): guest paths. */
|
||||
private val padNodes = mutableMapOf<Int, String>()
|
||||
@Volatile private var hidrawClassDir: File? = null
|
||||
|
||||
/**
|
||||
* Lists only the device the pad is now ([padType], FakeInputWriter.PAD_TYPE_*) in
|
||||
* /sys/class/hidraw, none for an Xbox 360 pad. Device scans then find what is plugged in: SDL's
|
||||
* (the PlayStation pads' driver) keeps a device it found and could not open, and never tries it
|
||||
* again, while one that disappears from the listing is let go. Before the shim is told.
|
||||
*/
|
||||
@Synchronized
|
||||
fun showPad(padType: Int) {
|
||||
val dir = hidrawClassDir ?: return
|
||||
val shown = when (padType) {
|
||||
com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DECK -> MINOR
|
||||
com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_STEAM -> TRITON_PAD_MINOR
|
||||
com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DS4 -> SONY_DS4_MINOR
|
||||
com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DUALSENSE -> SONY_DUALSENSE_MINOR
|
||||
else -> -1
|
||||
}
|
||||
for ((minor, target) in padNodes) {
|
||||
val link = File(dir, "hidraw$minor")
|
||||
try {
|
||||
if (minor == shown) {
|
||||
if (!link.exists()) { link.delete(); Os.symlink(target, link.path) }
|
||||
} else {
|
||||
link.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "pad: could not list /dev/hidraw$minor: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun prepare(context: Context, sessionRoot: File, touch: Boolean = false, triton: Boolean = false,
|
||||
padType: Int = com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DECK): List<String> {
|
||||
fun prepare(context: Context, sessionRoot: File, touch: Boolean = false, triton: Boolean = false): List<String> {
|
||||
val base = File(sessionRoot, "sys/deck")
|
||||
val devices = File(base, "devices")
|
||||
val hidrawClass = File(base, "class-hidraw")
|
||||
@@ -197,8 +137,7 @@ object SteamDeckPad {
|
||||
link(File(dir(HIDRAW), "device"), HID)
|
||||
|
||||
hidrawClass.mkdirs()
|
||||
padNodes.clear()
|
||||
padNodes[MINOR] = HIDRAW
|
||||
link(File(hidrawClass, NODE), HIDRAW)
|
||||
|
||||
// Only the names matter: libudev reads a listing for the subsystems to descend into and
|
||||
// then opens /sys/class/hidraw itself, which is the bind above.
|
||||
@@ -238,87 +177,21 @@ object SteamDeckPad {
|
||||
Log.i(TAG, "deck pad: /dev/$TOUCH_NODE described as Steam's touch controller (0000:11fb)")
|
||||
}
|
||||
|
||||
// A Steam Controller's HID device: the handheld's own pad as the wired one, or the paired
|
||||
// one on Bluetooth. Both are always described; libfakeinput offers whichever is in use.
|
||||
fun describeTriton(wired: Boolean) {
|
||||
val hid = if (wired) TRITON_WIRED_HID else TRITON_HID
|
||||
val product = if (wired) "1302" else "1303"
|
||||
val bus = if (wired) "0003" else "0005"
|
||||
val minor = if (wired) TRITON_PAD_MINOR else TRITON_MINOR
|
||||
val node = "hidraw$minor"
|
||||
if (wired) {
|
||||
write(TRITON_USB, "uevent", "DEVTYPE=usb_device\nPRODUCT=28de/1302/100\nTYPE=0/0/0\nBUSNUM=003\nDEVNUM=002\n")
|
||||
write(TRITON_USB, "idVendor", "28de\n")
|
||||
write(TRITON_USB, "idProduct", "1302\n")
|
||||
write(TRITON_USB, "bcdDevice", "0100\n")
|
||||
write(TRITON_USB, "manufacturer", "Valve Software\n")
|
||||
write(TRITON_USB, "product", "Steam Controller\n")
|
||||
write(TRITON_USB, "serial", "$TRITON_SERIAL\n")
|
||||
link(File(dir(TRITON_USB), "subsystem"), "/sys/bus/usb")
|
||||
write(TRITON_INTERFACE, "uevent", "DEVTYPE=usb_interface\nPRODUCT=28de/1302/100\nINTERFACE=3/0/0\n")
|
||||
write(TRITON_INTERFACE, "bInterfaceNumber", "00\n")
|
||||
write(TRITON_INTERFACE, "bInterfaceClass", "03\n")
|
||||
link(File(dir(TRITON_INTERFACE), "subsystem"), "/sys/bus/usb")
|
||||
}
|
||||
write(hid, "uevent", "DRIVER=hid-generic\nHID_ID=$bus:000028DE:0000$product\n" +
|
||||
"HID_NAME=Valve Software Steam Controller\n" +
|
||||
"HID_PHYS=${if (wired) "usb-droiddeck-3/input0" else "bluetooth-droiddeck-1"}\n" +
|
||||
"HID_UNIQ=$TRITON_SERIAL\nMODALIAS=hid:b${bus}g0001v000028DEp0000$product\n")
|
||||
File(dir(hid), "report_descriptor").writeBytes(if (wired) TRITON_WIRED_REPORT_DESCRIPTOR else TRITON_REPORT_DESCRIPTOR)
|
||||
link(File(dir(hid), "subsystem"), "/sys/bus/hid")
|
||||
val hidraw = "$hid/hidraw/$node"
|
||||
write(hidraw, "uevent", "MAJOR=$MAJOR\nMINOR=$minor\nDEVNAME=$node\n")
|
||||
write(hidraw, "dev", "$MAJOR:$minor\n")
|
||||
link(File(dir(hidraw), "subsystem"), "/sys/class/hidraw")
|
||||
link(File(dir(hidraw), "device"), hid)
|
||||
if (wired) padNodes[minor] = hidraw else link(File(hidrawClass, node), hidraw)
|
||||
link(File(context.cacheDir, "drm/sys/$MAJOR:$minor"), hidraw)
|
||||
File(udevData, "c$MAJOR:$minor").writeText("I:1\nE:ID_INPUT=1\nE:ID_INPUT_JOYSTICK=1\n")
|
||||
Log.i(TAG, "steam controller: /dev/$node described as Triton (28de:$product)")
|
||||
if (triton) {
|
||||
write(TRITON_HID, "uevent", "DRIVER=hid-generic\nHID_ID=0005:000028DE:00001303\n" +
|
||||
"HID_NAME=Valve Software Steam Controller\nHID_PHYS=bluetooth-droiddeck-1\n" +
|
||||
"HID_UNIQ=$TRITON_SERIAL\nMODALIAS=hid:b0005g0001v000028DEp00001303\n")
|
||||
File(dir(TRITON_HID), "report_descriptor").writeBytes(TRITON_REPORT_DESCRIPTOR)
|
||||
link(File(dir(TRITON_HID), "subsystem"), "/sys/bus/hid")
|
||||
write(TRITON_HIDRAW, "uevent", "MAJOR=$MAJOR\nMINOR=$TRITON_MINOR\nDEVNAME=$TRITON_NODE\n")
|
||||
write(TRITON_HIDRAW, "dev", "$MAJOR:$TRITON_MINOR\n")
|
||||
link(File(dir(TRITON_HIDRAW), "subsystem"), "/sys/class/hidraw")
|
||||
link(File(dir(TRITON_HIDRAW), "device"), TRITON_HID)
|
||||
link(File(hidrawClass, TRITON_NODE), TRITON_HIDRAW)
|
||||
link(File(context.cacheDir, "drm/sys/$MAJOR:$TRITON_MINOR"), TRITON_HIDRAW)
|
||||
File(udevData, "c$MAJOR:$TRITON_MINOR").writeText("I:1\nE:ID_INPUT=1\nE:ID_INPUT_JOYSTICK=1\n")
|
||||
Log.i(TAG, "steam controller: /dev/$TRITON_NODE described as Triton (28de:1303)")
|
||||
}
|
||||
// The handheld's own pad as a PlayStation controller, each a USB device of its own with
|
||||
// the report descriptor libfakeinput serves (kDs4/kDualSenseReportDescriptor).
|
||||
fun describeSony(dualSense: Boolean) {
|
||||
val bus = if (dualSense) 5 else 4
|
||||
val product = if (dualSense) "0ce6" else "09cc"
|
||||
val minor = if (dualSense) SONY_DUALSENSE_MINOR else SONY_DS4_MINOR
|
||||
val node = "hidraw$minor"
|
||||
val name = if (dualSense) "DualSense Wireless Controller" else "Wireless Controller"
|
||||
val usb = "$GUEST_DEVICES/usb$bus"
|
||||
val iface = "$usb/$bus-1:1.3"
|
||||
val hid = "$iface/0003:054C:${product.uppercase()}.000${bus + 1}"
|
||||
write(usb, "uevent", "DEVTYPE=usb_device\nPRODUCT=54c/$product/100\nTYPE=0/0/0\nBUSNUM=00$bus\nDEVNUM=002\n")
|
||||
write(usb, "idVendor", "054c\n")
|
||||
write(usb, "idProduct", "$product\n")
|
||||
write(usb, "bcdDevice", "0100\n")
|
||||
write(usb, "manufacturer", "Sony Interactive Entertainment\n")
|
||||
write(usb, "product", "$name\n")
|
||||
link(File(dir(usb), "subsystem"), "/sys/bus/usb")
|
||||
write(iface, "uevent", "DEVTYPE=usb_interface\nPRODUCT=54c/$product/100\nINTERFACE=3/0/0\n")
|
||||
write(iface, "bInterfaceNumber", "03\n")
|
||||
write(iface, "bInterfaceClass", "03\n")
|
||||
link(File(dir(iface), "subsystem"), "/sys/bus/usb")
|
||||
write(hid, "uevent", "DRIVER=hid-generic\nHID_ID=0003:0000054C:0000${product.uppercase()}\n" +
|
||||
"HID_NAME=Sony Interactive Entertainment $name\nHID_PHYS=usb-droiddeck-$bus/input3\n" +
|
||||
"HID_UNIQ=5e:43:4b:44:44:02\nMODALIAS=hid:b0003g0001v0000054Cp0000${product.uppercase()}\n")
|
||||
File(dir(hid), "report_descriptor").writeBytes(if (dualSense) DUALSENSE_REPORT_DESCRIPTOR else DS4_REPORT_DESCRIPTOR)
|
||||
link(File(dir(hid), "subsystem"), "/sys/bus/hid")
|
||||
val hidraw = "$hid/hidraw/$node"
|
||||
write(hidraw, "uevent", "MAJOR=$MAJOR\nMINOR=$minor\nDEVNAME=$node\n")
|
||||
write(hidraw, "dev", "$MAJOR:$minor\n")
|
||||
link(File(dir(hidraw), "subsystem"), "/sys/class/hidraw")
|
||||
link(File(dir(hidraw), "device"), hid)
|
||||
padNodes[minor] = hidraw
|
||||
link(File(context.cacheDir, "drm/sys/$MAJOR:$minor"), hidraw)
|
||||
File(udevData, "c$MAJOR:$minor").writeText("I:1\nE:ID_INPUT=1\nE:ID_INPUT_JOYSTICK=1\n")
|
||||
Log.i(TAG, "playstation pad: /dev/$node described as 054c:$product")
|
||||
}
|
||||
describeSony(dualSense = false)
|
||||
describeSony(dualSense = true)
|
||||
describeTriton(wired = true)
|
||||
if (triton) describeTriton(wired = false)
|
||||
hidrawClassDir = hidrawClass
|
||||
showPad(padType)
|
||||
|
||||
Log.i(TAG, "deck pad: /dev/$NODE described as a Steam Deck controller (28de:1205)")
|
||||
listOf(devices.path + ":" + GUEST_DEVICES, hidrawClass.path + ":/sys/class/hidraw")
|
||||
|
||||
@@ -86,8 +86,12 @@ class ModeSettings(
|
||||
/** Steam only: SessionPrefs.SYNC_* chosen for Proton games; null outside Steam. */
|
||||
val syncBackend: String? = null,
|
||||
val stretch16x9: Boolean? = null,
|
||||
/** Steam only: the client branch forced on the command line. */
|
||||
val steamChannel: String? = null,
|
||||
/** Steam only: enable the SteamOS client interface and its performance controls. */
|
||||
val steamDeckMode: Boolean = false,
|
||||
val steamRepairQueued: Boolean = false,
|
||||
/** Steam: the QAM's performance overlay (mangoapp) is started. */
|
||||
/** Steam, Deck mode: the QAM's performance overlay (mangoapp) is started. */
|
||||
val mangoapp: Boolean = true,
|
||||
/** Steam only: what the pad is to the client (SessionPrefs.CONTROLLER_*); null outside Steam. */
|
||||
val steamController: String? = null,
|
||||
@@ -137,7 +141,9 @@ class ModeSettingsActions(
|
||||
val onForceSsbs: (Boolean) -> Unit = {},
|
||||
val onSyncBackend: (String) -> Unit = {},
|
||||
val onStretch16x9: (Boolean) -> Unit = {},
|
||||
val onSteamChannel: (String) -> Unit = {},
|
||||
val onSteamRepair: () -> Unit = {},
|
||||
val onSteamDeckMode: (Boolean) -> Unit = {},
|
||||
val onMangoapp: (Boolean) -> Unit = {},
|
||||
val onSteamController: (String) -> Unit = {},
|
||||
val onUnfoldedControls: (Boolean) -> Unit = {},
|
||||
@@ -263,9 +269,6 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
host, "controller", stringResource(R.string.mode_controller), stringResource(R.string.mode_controller_hint),
|
||||
listOf(
|
||||
SessionPrefs.CONTROLLER_DECK to stringResource(R.string.mode_controller_deck),
|
||||
SessionPrefs.CONTROLLER_STEAM to stringResource(R.string.mode_controller_steam),
|
||||
SessionPrefs.CONTROLLER_DS4 to stringResource(R.string.mode_controller_ds4),
|
||||
SessionPrefs.CONTROLLER_DUALSENSE to stringResource(R.string.mode_controller_dualsense),
|
||||
SessionPrefs.CONTROLLER_XBOX360 to stringResource(R.string.mode_controller_x360),
|
||||
), s.steamController,
|
||||
note = stringResource(R.string.mode_controller_note),
|
||||
@@ -378,12 +381,25 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
)
|
||||
}
|
||||
}
|
||||
if (steam && tab == ModeSettingsTab.STEAM) SettingsGroup(stringResource(R.string.mode_client)) {
|
||||
if (steam && tab == ModeSettingsTab.STEAM && s.steamChannel != null) SettingsGroup(stringResource(R.string.mode_client)) {
|
||||
ToggleRow(
|
||||
host, "steamdeck", stringResource(R.string.mode_deck_mode),
|
||||
stringResource(R.string.mode_deck_mode_hint),
|
||||
s.steamDeckMode, onChange = a.onSteamDeckMode,
|
||||
)
|
||||
if (s.steamDeckMode) ToggleRow(
|
||||
host, "mangoapp", stringResource(R.string.mode_mangoapp),
|
||||
stringResource(R.string.mode_mangoapp_hint),
|
||||
s.mangoapp, onChange = a.onMangoapp,
|
||||
)
|
||||
// Deck mode fixes the branch (SessionPrefs.steamChannel); the choice is for Deck mode off.
|
||||
if (s.steamDeckMode) SettingsRow(stringResource(R.string.mode_branch), stringResource(R.string.mode_branch_deck)) {}
|
||||
else ChoiceRow(
|
||||
host, "channel", stringResource(R.string.mode_branch), stringResource(R.string.mode_branch_hint),
|
||||
listOf("publicbeta" to stringResource(R.string.mode_branch_public), "steamdeck_publicbeta" to stringResource(R.string.mode_branch_deck_beta)), s.steamChannel,
|
||||
note = stringResource(R.string.mode_branch_note),
|
||||
onPick = a.onSteamChannel,
|
||||
)
|
||||
SettingsRow(
|
||||
stringResource(R.string.mode_steam_repair),
|
||||
stringResource(if (s.steamRepairQueued) R.string.mode_steam_repair_queued else R.string.mode_steam_repair_hint),
|
||||
|
||||
@@ -194,9 +194,6 @@ class DrawerActions(
|
||||
val suspendPolicy: String,
|
||||
val steamDownloadsInBackground: Boolean = false,
|
||||
val backActionsInverted: Boolean,
|
||||
/** What the pad is to Steam (SessionPrefs.CONTROLLER_*), switched live; null hides the row. */
|
||||
val padType: String? = null,
|
||||
val onPadType: (String) -> Unit = {},
|
||||
val touchMode: String,
|
||||
val touchAuto: String,
|
||||
val fexPreset: String,
|
||||
@@ -422,15 +419,6 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
|
||||
}
|
||||
SessionDrawerPage.CONTROLLER -> {
|
||||
SettingsGroup(stringResource(R.string.drawer_page_controller)) {
|
||||
a.padType?.let { type ->
|
||||
ChoiceRow(host, "pad-type", stringResource(R.string.mode_controller), null,
|
||||
listOf(SessionPrefs.CONTROLLER_DECK to stringResource(R.string.mode_controller_deck),
|
||||
SessionPrefs.CONTROLLER_STEAM to stringResource(R.string.mode_controller_steam),
|
||||
SessionPrefs.CONTROLLER_DS4 to stringResource(R.string.mode_controller_ds4),
|
||||
SessionPrefs.CONTROLLER_DUALSENSE to stringResource(R.string.mode_controller_dualsense),
|
||||
SessionPrefs.CONTROLLER_XBOX360 to stringResource(R.string.mode_controller_x360)),
|
||||
type, chipModifier = focus.track(page, "pad-type"), onPick = a.onPadType)
|
||||
}
|
||||
ChoiceRow(host, "touch", stringResource(R.string.mode_touch), null,
|
||||
listOf(SessionPrefs.TOUCH_AUTO to stringResource(R.string.drawer_touch_auto, a.touchAuto), SessionPrefs.TOUCH_PAD to stringResource(R.string.mode_touch_touchpad), SessionPrefs.TOUCH_DIRECT to stringResource(R.string.mode_touch_direct), SessionPrefs.TOUCH_OFF to stringResource(R.string.widgets_off)),
|
||||
a.touchMode, chipModifier = focus.track(page, "touch"), onPick = a.onTouch)
|
||||
|
||||
@@ -254,9 +254,6 @@
|
||||
<string name="mode_controller">Mando</string>
|
||||
<string name="mode_controller_hint">Cómo ve Steam tu mando. Se aplica en la próxima sesión.</string>
|
||||
<string name="mode_controller_deck">Mando de Steam Deck</string>
|
||||
<string name="mode_controller_steam">Steam Controller</string>
|
||||
<string name="mode_controller_ds4">DualShock 4</string>
|
||||
<string name="mode_controller_dualsense">DualSense</string>
|
||||
<string name="mode_controller_x360">Mando Xbox 360</string>
|
||||
<string name="mode_controller_note">Deck: botón de Quick Access y giroscopio. Xbox 360: abre Quick Access con Guide+A.</string>
|
||||
<string name="mode_back">Atrás</string>
|
||||
@@ -286,8 +283,16 @@
|
||||
<string name="mode_decky_remove_title">¿Desinstalar Decky Loader?</string>
|
||||
<string name="mode_decky_remove_text">Elimina el loader. Tus plugins y ajustes se conservan.</string>
|
||||
<string name="mode_client">Cliente</string>
|
||||
<string name="mode_deck_mode">Modo Steam Deck</string>
|
||||
<string name="mode_deck_mode_hint">La interfaz Deck de Steam y los controles de rendimiento de Quick Access. Se aplica en la próxima sesión.</string>
|
||||
<string name="mode_mangoapp">Superposición de rendimiento</string>
|
||||
<string name="mode_mangoapp_hint">Desactívalo si los juegos fallan o se quedan en negro en modo Deck. Se aplica en la próxima sesión.</string>
|
||||
<string name="mode_branch">Rama del cliente</string>
|
||||
<string name="mode_branch_deck">Beta pública de Steam Deck (requerida por el modo Deck).</string>
|
||||
<string name="mode_branch_hint">Se aplica en la próxima sesión. Steam puede actualizarse una vez.</string>
|
||||
<string name="mode_branch_public">Beta pública</string>
|
||||
<string name="mode_branch_deck_beta">Beta pública de Steam Deck</string>
|
||||
<string name="mode_branch_note">El modo Deck siempre usa la beta pública de Steam Deck.</string>
|
||||
<string name="mode_steam_repair">Reparar el cliente de Steam</string>
|
||||
<string name="mode_steam_repair_hint">Vuelve a descargar el cliente de Steam la próxima vez que se inicie Steam. Úsalo si Steam se queda en la pantalla de carga. Se conservan los juegos, la sesión iniciada y los ajustes.</string>
|
||||
<string name="mode_steam_repair_queued">El cliente de Steam se volverá a descargar la próxima vez que se inicie Steam.</string>
|
||||
|
||||
@@ -232,9 +232,6 @@
|
||||
<string name="mode_controller">Manette</string>
|
||||
<string name="mode_controller_hint">Comment Steam reconnaît votre manette. Prendra effet à la prochaine session.</string>
|
||||
<string name="mode_controller_deck">Manette Steam Deck</string>
|
||||
<string name="mode_controller_steam">Steam Controller</string>
|
||||
<string name="mode_controller_ds4">DualShock 4</string>
|
||||
<string name="mode_controller_dualsense">DualSense</string>
|
||||
<string name="mode_controller_x360">Manette Xbox 360</string>
|
||||
<string name="mode_controller_note">Deck : fournit un bouton d\'accès rapide et un gyroscope. Xbox 360 : utilisez Guide+A pour ouvrir l\'accès rapide.</string>
|
||||
<string name="mode_back">Retour</string>
|
||||
@@ -260,8 +257,16 @@
|
||||
<string name="mode_decky_remove_title">Désinstaller Decky Loader ?</string>
|
||||
<string name="mode_decky_remove_text">Supprime le chargeur. Vos plugins et paramètres seront conservés.</string>
|
||||
<string name="mode_client">Client</string>
|
||||
<string name="mode_deck_mode">Mode Steam Deck</string>
|
||||
<string name="mode_deck_mode_hint">Interface Steam Deck et contrôles de performance à accès rapide. Prendra effet à la prochaine session.</string>
|
||||
<string name="mode_mangoapp">Superposition de performance</string>
|
||||
<string name="mode_mangoapp_hint">Désactivez cette option si les jeux plantent ou affichent un écran noir en mode Deck. Prendra effet à la prochaine session.</string>
|
||||
<string name="mode_branch">Branche du client</string>
|
||||
<string name="mode_branch_deck">Bêta Steam Deck (requis pour le mode Deck).</string>
|
||||
<string name="mode_branch_hint">Prendra effet à la prochaine session. Steam peut se mettre à jour une fois de lui-même.</string>
|
||||
<string name="mode_branch_public">Bêta</string>
|
||||
<string name="mode_branch_deck_beta">Bêta Steam Deck</string>
|
||||
<string name="mode_branch_note">Le mode Deck utilise toujours la version bêta de Steam Deck.</string>
|
||||
<string name="mode_added_art">Illustrations</string>
|
||||
<string name="mode_sgdb_key">Clé API SteamGridDB</string>
|
||||
<string name="mode_sgdb_key_yours">Votre clé</string>
|
||||
|
||||
@@ -254,9 +254,6 @@
|
||||
<string name="mode_controller">コントローラー</string>
|
||||
<string name="mode_controller_hint">Steamがコントローラーを認識する方法。次回のセッションで有効になります。</string>
|
||||
<string name="mode_controller_deck">Steam Deckコントローラー</string>
|
||||
<string name="mode_controller_steam">Steam Controller</string>
|
||||
<string name="mode_controller_ds4">DualShock 4</string>
|
||||
<string name="mode_controller_dualsense">DualSense</string>
|
||||
<string name="mode_controller_x360">Xbox 360コントローラー</string>
|
||||
<string name="mode_controller_note">Deck:クイックアクセスボタンとジャイロを提供します。Xbox 360:Guide+Aでクイックアクセスを開きます。</string>
|
||||
<string name="mode_back">戻る</string>
|
||||
@@ -286,8 +283,16 @@
|
||||
<string name="mode_decky_remove_title">Decky Loaderをアンインストールしますか?</string>
|
||||
<string name="mode_decky_remove_text">Loaderを削除します。プラグインと設定は保持されます。</string>
|
||||
<string name="mode_client">クライアント</string>
|
||||
<string name="mode_deck_mode">Steam Deckモード</string>
|
||||
<string name="mode_deck_mode_hint">Steam DeckのUIとクイックアクセスパフォーマンスコントロール。次回のセッションで有効になります。</string>
|
||||
<string name="mode_mangoapp">パフォーマンスオーバーレイ</string>
|
||||
<string name="mode_mangoapp_hint">Deckモードでゲームがクラッシュしたり画面が真っ暗になる場合は、これをオフにしてください。次回のセッションで有効になります。</string>
|
||||
<string name="mode_branch">クライアントブランチ</string>
|
||||
<string name="mode_branch_deck">Steam Deckベータ版(Deckモードに必要)。</string>
|
||||
<string name="mode_branch_hint">次回のセッションで有効になります。Steamが自動的に一度アップデートされる場合があります。</string>
|
||||
<string name="mode_branch_public">ベータ版</string>
|
||||
<string name="mode_branch_deck_beta">Steam Deckベータ版</string>
|
||||
<string name="mode_branch_note">Deckモードは常にSteam Deckベータ版を使用します。</string>
|
||||
<string name="mode_steam_repair">Steamクライアントを修復</string>
|
||||
<string name="mode_steam_repair_hint">次回Steamの起動時にSteamクライアントを再ダウンロードします。Steamが読み込み画面のまま進まない場合に使用してください。ゲーム、ログイン情報、設定は保持されます。</string>
|
||||
<string name="mode_steam_repair_queued">次回Steamの起動時にSteamクライアントを再ダウンロードします。</string>
|
||||
|
||||
@@ -254,9 +254,6 @@
|
||||
<string name="mode_controller">컨트롤러</string>
|
||||
<string name="mode_controller_hint">Steam이 컨트롤러를 인식하는 방식입니다. 다음 세션에 적용됩니다.</string>
|
||||
<string name="mode_controller_deck">Steam Deck 컨트롤러</string>
|
||||
<string name="mode_controller_steam">Steam Controller</string>
|
||||
<string name="mode_controller_ds4">DualShock 4</string>
|
||||
<string name="mode_controller_dualsense">DualSense</string>
|
||||
<string name="mode_controller_x360">Xbox 360 컨트롤러</string>
|
||||
<string name="mode_controller_note">Deck: 빠른 액세스 버튼과 자이로스코프를 제공합니다. Xbox 360: Guide+A로 빠른 액세스를 엽니다.</string>
|
||||
<string name="mode_back">뒤로</string>
|
||||
@@ -286,8 +283,16 @@
|
||||
<string name="mode_decky_remove_title">Decky Loader를 제거하시겠습니까?</string>
|
||||
<string name="mode_decky_remove_text">로더를 제거합니다. 플러그인과 설정은 유지됩니다.</string>
|
||||
<string name="mode_client">클라이언트</string>
|
||||
<string name="mode_deck_mode">Steam Deck 모드</string>
|
||||
<string name="mode_deck_mode_hint">Steam Deck UI 및 빠른 액세스 성능 제어. 다음 세션에 적용됩니다.</string>
|
||||
<string name="mode_mangoapp">성능 오버레이</string>
|
||||
<string name="mode_mangoapp_hint">Deck 모드에서 게임이 충돌하거나 화면이 검게 변하면 이 옵션을 끄세요. 다음 세션에 적용됩니다.</string>
|
||||
<string name="mode_branch">클라이언트 브랜치</string>
|
||||
<string name="mode_branch_deck">Steam Deck 베타(Deck 모드에 필요).</string>
|
||||
<string name="mode_branch_hint">다음 세션에 적용됩니다. Steam이 자체적으로 한 번 업데이트될 수 있습니다.</string>
|
||||
<string name="mode_branch_public">베타</string>
|
||||
<string name="mode_branch_deck_beta">Steam Deck 베타</string>
|
||||
<string name="mode_branch_note">Deck 모드는 항상 Steam Deck 베타를 사용합니다.</string>
|
||||
<string name="mode_steam_repair">Steam 클라이언트 복구</string>
|
||||
<string name="mode_steam_repair_hint">다음에 Steam을 시작할 때 Steam 클라이언트를 다시 다운로드합니다. Steam이 로딩 화면에서 넘어가지 않을 때 사용하세요. 게임, 로그인 정보 및 설정은 유지됩니다.</string>
|
||||
<string name="mode_steam_repair_queued">다음에 Steam을 시작할 때 Steam 클라이언트를 다시 다운로드합니다.</string>
|
||||
|
||||
@@ -254,9 +254,6 @@
|
||||
<string name="mode_controller">Контроллер</string>
|
||||
<string name="mode_controller_hint">Как Steam распознает ваш контроллер. Вступит в силу в следующей сессии.</string>
|
||||
<string name="mode_controller_deck">Контроллер Steam Deck</string>
|
||||
<string name="mode_controller_steam">Steam Controller</string>
|
||||
<string name="mode_controller_ds4">DualShock 4</string>
|
||||
<string name="mode_controller_dualsense">DualSense</string>
|
||||
<string name="mode_controller_x360">Контроллер Xbox 360</string>
|
||||
<string name="mode_controller_note">Deck: кнопка быстрого доступа и гироскоп. Xbox 360: используйте Guide+A для быстрого доступа.</string>
|
||||
<string name="mode_back">Назад</string>
|
||||
@@ -284,8 +281,16 @@
|
||||
<string name="mode_decky_remove_title">Удалить Decky Loader?</string>
|
||||
<string name="mode_decky_remove_text">Удаляет загрузчик. Ваши плагины и настройки сохранятся.</string>
|
||||
<string name="mode_client">Клиент</string>
|
||||
<string name="mode_deck_mode">Режим Steam Deck</string>
|
||||
<string name="mode_deck_mode_hint">Интерфейс Steam Deck и управление производительностью через быстрый доступ. Вступит в силу в следующей сессии.</string>
|
||||
<string name="mode_mangoapp">Оверлей производительности</string>
|
||||
<string name="mode_mangoapp_hint">Отключите, если игры вылетают или показывают черный экран в режиме Deck. Вступит в силу в следующей сессии.</string>
|
||||
<string name="mode_branch">Ветка клиента</string>
|
||||
<string name="mode_branch_deck">Публичная бета Steam Deck (требуется для режима Deck).</string>
|
||||
<string name="mode_branch_hint">Вступит в силу в следующей сессии. Steam может обновиться самостоятельно один раз.</string>
|
||||
<string name="mode_branch_public">Публичная бета</string>
|
||||
<string name="mode_branch_deck_beta">Публичная бета Steam Deck</string>
|
||||
<string name="mode_branch_note">Режим Deck всегда использует публичную бету Steam Deck.</string>
|
||||
<string name="mode_steam_repair">Восстановить клиент Steam</string>
|
||||
<string name="mode_steam_repair_hint">При следующем запуске Steam клиент будет скачан заново. Используйте, если Steam зависает на экране загрузки. Игры, вход и настройки сохраняются.</string>
|
||||
<string name="mode_steam_repair_queued">Клиент Steam будет скачан заново при следующем запуске Steam.</string>
|
||||
|
||||
@@ -254,9 +254,6 @@
|
||||
<string name="mode_controller">手柄</string>
|
||||
<string name="mode_controller_hint">决定 Steam 把你的手柄识别为哪种类型,将在下次会话生效。</string>
|
||||
<string name="mode_controller_deck">Steam Deck 手柄</string>
|
||||
<string name="mode_controller_steam">Steam Controller</string>
|
||||
<string name="mode_controller_ds4">DualShock 4</string>
|
||||
<string name="mode_controller_dualsense">DualSense</string>
|
||||
<string name="mode_controller_x360">Xbox 360 手柄</string>
|
||||
<string name="mode_controller_note">Deck:快捷访问按钮与陀螺仪。Xbox 360:用 Guide+A 打开快捷访问。</string>
|
||||
<string name="mode_back">返回</string>
|
||||
@@ -286,8 +283,16 @@
|
||||
<string name="mode_decky_remove_title">卸载 Decky Loader?</string>
|
||||
<string name="mode_decky_remove_text">移除加载器。你的插件和设置会保留。</string>
|
||||
<string name="mode_client">客户端</string>
|
||||
<string name="mode_deck_mode">Steam Deck 模式</string>
|
||||
<string name="mode_deck_mode_hint">Steam 的 Deck 界面和快捷访问性能控制。将在下次会话生效。</string>
|
||||
<string name="mode_mangoapp">性能覆盖层</string>
|
||||
<string name="mode_mangoapp_hint">若游戏在 Deck 模式下崩溃或黑屏,请关闭性能覆盖层。将在下次会话生效。</string>
|
||||
<string name="mode_branch">客户端分支</string>
|
||||
<string name="mode_branch_deck">Steam Deck 公开测试版(Deck 模式所必需)。</string>
|
||||
<string name="mode_branch_hint">将在下次会话生效。Steam 可能会自行更新一次。</string>
|
||||
<string name="mode_branch_public">公开测试版</string>
|
||||
<string name="mode_branch_deck_beta">Steam Deck 公开测试版</string>
|
||||
<string name="mode_branch_note">Deck 模式始终使用 Steam Deck 公开测试版。</string>
|
||||
<string name="mode_steam_repair">修复 Steam 客户端</string>
|
||||
<string name="mode_steam_repair_hint">下次启动 Steam 时重新下载 Steam 客户端。如果 Steam 一直停在加载画面,请使用此项。游戏、登录信息和设置都会保留。</string>
|
||||
<string name="mode_steam_repair_queued">下次启动 Steam 时将重新下载 Steam 客户端。</string>
|
||||
|
||||
@@ -254,9 +254,6 @@
|
||||
<string name="mode_controller">控制器</string>
|
||||
<string name="mode_controller_hint">Steam 如何識別你的控制器。下次會話生效。</string>
|
||||
<string name="mode_controller_deck">Steam Deck 控制器</string>
|
||||
<string name="mode_controller_steam">Steam Controller</string>
|
||||
<string name="mode_controller_ds4">DualShock 4</string>
|
||||
<string name="mode_controller_dualsense">DualSense</string>
|
||||
<string name="mode_controller_x360">Xbox 360 控制器</string>
|
||||
<string name="mode_controller_note">Deck:提供快速存取按鈕和陀螺儀。Xbox 360:按 Guide+A 開啟快速存取。</string>
|
||||
<string name="mode_back">返回</string>
|
||||
@@ -286,8 +283,16 @@
|
||||
<string name="mode_decky_remove_title">解除安裝 Decky Loader?</string>
|
||||
<string name="mode_decky_remove_text">移除載入器,你的插件和設定會保留。</string>
|
||||
<string name="mode_client">用戶端</string>
|
||||
<string name="mode_deck_mode">Steam Deck 模式</string>
|
||||
<string name="mode_deck_mode_hint">Steam 的 Deck 介面及快速存取效能控制。下次會話生效。</string>
|
||||
<string name="mode_mangoapp">效能疊加層</string>
|
||||
<string name="mode_mangoapp_hint">如果在 Deck 模式下遊戲當機或黑畫面,請關閉此項。下次會話生效。</string>
|
||||
<string name="mode_branch">用戶端分支</string>
|
||||
<string name="mode_branch_deck">Steam Deck 公開測試版(Deck 模式需要)。</string>
|
||||
<string name="mode_branch_hint">下次會話生效。Steam 可能會自行更新一次。</string>
|
||||
<string name="mode_branch_public">公開測試版</string>
|
||||
<string name="mode_branch_deck_beta">Steam Deck 公開測試版</string>
|
||||
<string name="mode_branch_note">Deck 模式一律使用 Steam Deck 公開測試版。</string>
|
||||
<string name="mode_steam_repair">修復 Steam 用戶端</string>
|
||||
<string name="mode_steam_repair_hint">下次啟動 Steam 時重新下載 Steam 用戶端。如果 Steam 一直停留在載入畫面,請使用此功能。遊戲、登入資料和設定都會保留。</string>
|
||||
<string name="mode_steam_repair_queued">下次啟動 Steam 時會重新下載 Steam 用戶端。</string>
|
||||
|
||||
@@ -254,9 +254,6 @@
|
||||
<string name="mode_controller">控制器</string>
|
||||
<string name="mode_controller_hint">Steam 辨識你的控制器的方式。下次工作階段生效。</string>
|
||||
<string name="mode_controller_deck">Steam Deck 控制器</string>
|
||||
<string name="mode_controller_steam">Steam Controller</string>
|
||||
<string name="mode_controller_ds4">DualShock 4</string>
|
||||
<string name="mode_controller_dualsense">DualSense</string>
|
||||
<string name="mode_controller_x360">Xbox 360 控制器</string>
|
||||
<string name="mode_controller_note">Deck:提供快速存取按鈕和陀螺儀。Xbox 360:使用 Guide+A 開啟快速存取。</string>
|
||||
<string name="mode_back">返回</string>
|
||||
@@ -286,8 +283,16 @@
|
||||
<string name="mode_decky_remove_title">解除安裝 Decky Loader?</string>
|
||||
<string name="mode_decky_remove_text">移除載入器。你的外掛程式和設定會保留。</string>
|
||||
<string name="mode_client">用戶端</string>
|
||||
<string name="mode_deck_mode">Steam Deck 模式</string>
|
||||
<string name="mode_deck_mode_hint">Steam Deck 介面和快速存取效能控制。下次工作階段生效。</string>
|
||||
<string name="mode_mangoapp">效能重疊層</string>
|
||||
<string name="mode_mangoapp_hint">如果 Deck 模式下遊戲崩潰或黑畫面,請關閉此項。下次工作階段生效。</string>
|
||||
<string name="mode_branch">用戶端分支</string>
|
||||
<string name="mode_branch_deck">Steam Deck 公開測試版(Deck 模式所需)。</string>
|
||||
<string name="mode_branch_hint">下次工作階段生效。Steam 可能會自行更新一次。</string>
|
||||
<string name="mode_branch_public">公開測試版</string>
|
||||
<string name="mode_branch_deck_beta">Steam Deck 公開測試版</string>
|
||||
<string name="mode_branch_note">Deck 模式一律使用 Steam Deck 公開測試版。</string>
|
||||
<string name="mode_steam_repair">修復 Steam 用戶端</string>
|
||||
<string name="mode_steam_repair_hint">下次啟動 Steam 時重新下載 Steam 用戶端。如果 Steam 一直停在載入畫面,請使用此功能。遊戲、登入狀態和設定都會保留。</string>
|
||||
<string name="mode_steam_repair_queued">下次啟動 Steam 時會重新下載 Steam 用戶端。</string>
|
||||
|
||||
@@ -262,11 +262,8 @@
|
||||
<string name="mode_controller">Controller</string>
|
||||
<string name="mode_controller_hint">How Steam sees your controller. Applies next session.</string>
|
||||
<string name="mode_controller_deck">Steam Deck controller</string>
|
||||
<string name="mode_controller_steam">Steam Controller</string>
|
||||
<string name="mode_controller_ds4">DualShock 4</string>
|
||||
<string name="mode_controller_dualsense">DualSense</string>
|
||||
<string name="mode_controller_x360">Xbox 360 controller</string>
|
||||
<string name="mode_controller_note">Deck: Quick Access button and gyro. Steam Controller: the same, with grip sense on the back buttons. DualShock 4 and DualSense: gyro, and the trackpads as their touchpad. Xbox 360: open Quick Access with Guide+A.</string>
|
||||
<string name="mode_controller_note">Deck: Quick Access button and gyro. Xbox 360: open Quick Access with Guide+A.</string>
|
||||
<string name="mode_back">Back</string>
|
||||
<string name="back_menu_then_qam">Menu → QAM</string>
|
||||
<string name="back_qam_then_menu">QAM → Menu</string>
|
||||
@@ -296,8 +293,16 @@
|
||||
<string name="mode_decky_remove_title">Uninstall Decky Loader?</string>
|
||||
<string name="mode_decky_remove_text">Removes the loader. Your plugins and settings are kept.</string>
|
||||
<string name="mode_client">Client</string>
|
||||
<string name="mode_deck_mode">Steam Deck mode</string>
|
||||
<string name="mode_deck_mode_hint">Steam\'s Deck interface and Quick Access performance controls. Applies next session.</string>
|
||||
<string name="mode_mangoapp">Performance overlay</string>
|
||||
<string name="mode_mangoapp_hint">Turn off if games crash or go black in Deck mode. Applies next session.</string>
|
||||
<string name="mode_branch">Client branch</string>
|
||||
<string name="mode_branch_deck">Steam Deck public beta (required by Deck mode).</string>
|
||||
<string name="mode_branch_hint">Applies next session. Steam may update itself once.</string>
|
||||
<string name="mode_branch_public">Public beta</string>
|
||||
<string name="mode_branch_deck_beta">Steam Deck public beta</string>
|
||||
<string name="mode_branch_note">Deck mode always uses Steam Deck public beta.</string>
|
||||
<string name="mode_steam_repair">Repair Steam client</string>
|
||||
<string name="mode_steam_repair_hint">Downloads the Steam client again the next time Steam starts. Use it if Steam stays on its loading screen. Games, sign-in and settings are kept.</string>
|
||||
<string name="mode_steam_repair_queued">The Steam client will be downloaded again the next time Steam starts.</string>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], manifest = Config.NONE)
|
||||
class SteamChannelTest {
|
||||
private lateinit var context: Context
|
||||
|
||||
@Before fun setUp() {
|
||||
context = RuntimeEnvironment.getApplication()
|
||||
context.getSharedPreferences("session", Context.MODE_PRIVATE).edit().clear().commit()
|
||||
}
|
||||
|
||||
@Test fun freshSettingsDefaultToDeckBetaWithOrWithoutDeckMode() {
|
||||
assertEquals("steamdeck_publicbeta", SessionPrefs.steamChannel(context))
|
||||
SessionPrefs.setSteamDeckMode(context, false)
|
||||
assertEquals("steamdeck_publicbeta", SessionPrefs.steamChannel(context))
|
||||
}
|
||||
|
||||
@Test fun explicitChannelIsPreservedWhenDeckModeIsOff() {
|
||||
SessionPrefs.setSteamDeckMode(context, false)
|
||||
SessionPrefs.setSteamChannel(context, "publicbeta")
|
||||
assertEquals("publicbeta", SessionPrefs.steamChannel(context))
|
||||
SessionPrefs.setSteamChannel(context, "steamdeck_publicbeta")
|
||||
assertEquals("steamdeck_publicbeta", SessionPrefs.steamChannel(context))
|
||||
}
|
||||
|
||||
@Test fun deckModeUsesDeckBetaAndRestoresSavedSelectionWhenDisabled() {
|
||||
SessionPrefs.setSteamChannel(context, "publicbeta")
|
||||
assertEquals("steamdeck_publicbeta", SessionPrefs.steamChannel(context))
|
||||
SessionPrefs.setSteamDeckMode(context, false)
|
||||
assertEquals("publicbeta", SessionPrefs.steamChannel(context))
|
||||
}
|
||||
}
|
||||
@@ -428,7 +428,11 @@ def guest_environment(fex, env):
|
||||
result["FEX_APP_DATA_LOCATION"] = data + "/"
|
||||
result["FEX_APP_CACHE_LOCATION"] = CACHE_DIR + "/"
|
||||
result.setdefault("FEX_SILENTLOG", "0")
|
||||
result.setdefault("FEX_OUTPUTLOG", "stderr")
|
||||
# Empty: FEX writes to descriptor 2 itself. "stderr" has it write to its own duplicate, a low
|
||||
# number a program may dup2() its own descriptor onto: Chromium does so in each child it starts
|
||||
# and lists /proc/self/fd next, which FEX logs, into the child's IPC channel - the browser
|
||||
# never understood a child, and every Electron program hung without a window.
|
||||
result.setdefault("FEX_OUTPUTLOG", "")
|
||||
if fex["portable"]:
|
||||
result["FEX_PORTABLE"] = "1"
|
||||
else:
|
||||
|
||||
@@ -264,12 +264,12 @@ if [ -z "${BL_INSIDE:-}" ]; then
|
||||
[ "${BL_HDR:-0}" = 1 ] && args+=(--hdr-enabled)
|
||||
# Extra gamescope arguments for an experiment (--debug-focus, say), from Download/droiddeck-env.
|
||||
[ -n "${BL_GAMESCOPE_ARGS:-}" ] && read -r -a _bl_extra <<<"$BL_GAMESCOPE_ARGS" && args+=("${_bl_extra[@]}")
|
||||
# The performance overlay (Quick Access Menu → Performance → Overlay Level) is drawn by
|
||||
# Deck mode's performance overlay (Quick Access Menu → Performance → Overlay Level) is drawn by
|
||||
# mangoapp, which gamescope starts beside the client; the client sets the level by rewriting
|
||||
# MANGOHUD_CONFIGFILE. The apk stages Valve's aarch64 build (tools/mangoapp) - without it the menu
|
||||
# still offers the levels and nothing appears. Starts hidden until the client writes a level.
|
||||
# BL_MANGOAPP=0 turns it off (one community build found Valve's mangoapp crashing on Turnip).
|
||||
if [ "$mode" = steam ] && [ "${BL_MANGOAPP:-1}" = 1 ] \
|
||||
if [ "$mode" = steam ] && [ "${BL_STEAMDECK:-0}" = 1 ] && [ "${BL_MANGOAPP:-1}" = 1 ] \
|
||||
&& [ "${BL_STEAM_UI:-}" != desktop ] && command -v mangoapp >/dev/null 2>&1; then
|
||||
MANGOHUD_CONFIGFILE=$(mktemp /tmp/mangohud.XXXXXXXX)
|
||||
echo no_display > "$MANGOHUD_CONFIGFILE"
|
||||
@@ -821,7 +821,7 @@ BUSCONF
|
||||
# desktop's launchers ask for; gamescope shows one window at a time and the game takes over.
|
||||
_bl_ui=(-gamepadui)
|
||||
[ "${BL_STEAM_UI:-}" = desktop ] && _bl_ui=()
|
||||
# Deck mode, always outside the desktop UI: -steamdeck is the flag that puts
|
||||
# Deck mode (BL_STEAMDECK=1, the Steam page's Client settings): -steamdeck is the flag that puts
|
||||
# the Quick Access Menu and the Deck settings pages on screen; the QAM's performance overlay
|
||||
# controls stay greyed out without -steamos3, as SteamOS and Armada launch it. -steamos3 makes
|
||||
# the client manage itself as SteamOS does - it calls steamos-select-branch and writes
|
||||
@@ -829,7 +829,7 @@ BUSCONF
|
||||
# is an update that never installs: exit 42, a restart, for ever (Bannerlator 38996ea1). The
|
||||
# loop below puts package/beta back before every start. The SteamOS helpers the client calls
|
||||
# in this mode are staged as no-op stubs under /usr/bin.
|
||||
if [ "${BL_STEAM_UI:-}" != desktop ]; then
|
||||
if [ "${BL_STEAMDECK:-0}" = 1 ] && [ "${BL_STEAM_UI:-}" != desktop ]; then
|
||||
_bl_ui+=(-steamdeck -steamos3)
|
||||
# The QAM's Scaling Filter lists NVIDIA Image Scaling only when told gamescope has it (it does).
|
||||
export STEAM_GAMESCOPE_NIS_SUPPORTED=1
|
||||
@@ -851,16 +851,13 @@ BUSCONF
|
||||
[ -n "$_stale" ] && echo "== steam: cleared what an earlier session left:$_stale"
|
||||
unset _stale _f
|
||||
fi
|
||||
# The Deck's beta branch, the one the first-run download takes: with -steamos3 on publicbeta
|
||||
# the client settled on steamdeck_stable, an older client it then offered as a "Software
|
||||
# Update" every session (2026-09-30); on steamdeck_publicbeta it finds no update. Whatever
|
||||
# wrote package/beta, the branch on disk has to agree with the one forced here - a
|
||||
# disagreement is the restart loop above, and it is silent apart from rc=42 in this log. A
|
||||
# client installed from publicbeta by an older app moves over this way at its next start.
|
||||
# Use the same configured branch as the first-run download. Whatever wrote package/beta, the branch on
|
||||
# disk has to agree with the one forced here - a disagreement is the restart loop above, and
|
||||
# it is silent apart from rc=42 in this log.
|
||||
# Checked before every start, not once: the client rewrites package/beta during its own run,
|
||||
# and a restart below (rc=42, or the one after the ARM64 Proton lands) would otherwise start it
|
||||
# again on the branch it just wrote.
|
||||
steam_channel=steamdeck_publicbeta
|
||||
steam_channel=${BL_STEAM_CHANNEL:-steamdeck_publicbeta}
|
||||
# The app's language (Setup, or the system's): the client's interface takes -language over its
|
||||
# registry, and the registry is rewritten before each start below for its update window.
|
||||
_bl_lang=()
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
# The client is not redistributable, so the runtime ships this script instead of the client.
|
||||
set -eu
|
||||
cdn=https://client-update.fastly.steamstatic.com
|
||||
# The Deck's beta branch, which droiddeck-session forces on the command line too. A client already
|
||||
# on disk from another branch is left for the client to move (droiddeck-session's package/beta).
|
||||
steam_channel=steamdeck_publicbeta
|
||||
steam_channel=${BL_STEAM_CHANNEL:-steamdeck_publicbeta}
|
||||
case "$steam_channel" in
|
||||
publicbeta|steamdeck_publicbeta) ;;
|
||||
*) echo "droiddeck-steam-install: unsupported Steam channel: $steam_channel" >&2; exit 1 ;;
|
||||
esac
|
||||
manifest=steam_client_${steam_channel}_linuxarm64
|
||||
steam_root=${XDG_DATA_HOME:-$HOME/.local/share}/Steam
|
||||
stamp=$steam_root/package/droiddeck-installed
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
* and Qt ask before they start one. Only in the desktop session.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <elf.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
@@ -20,6 +19,8 @@
|
||||
#include <sys/statvfs.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
#ifndef BINFMT_HANDLER
|
||||
#define BINFMT_HANDLER "/usr/local/bin/droiddeck-open"
|
||||
#endif
|
||||
@@ -96,7 +97,7 @@ static void handler_args(char **args, const char *path, char *const argv[], int
|
||||
|
||||
static void hand_off(const char *path, char *const argv[], char *const envp[]) {
|
||||
static int (*real)(const char *, char *const[], char *const[]);
|
||||
if (!real) real = (int (*)(const char *, char *const[], char *const[]))dlsym(RTLD_NEXT, "execve");
|
||||
if (!real) real = (int (*)(const char *, char *const[], char *const[]))bl_next("execve");
|
||||
int n = count(argv);
|
||||
char *args[n + 4];
|
||||
handler_args(args, path, argv, n);
|
||||
@@ -154,7 +155,7 @@ __attribute__((visibility("hidden"))) int bl_binfmt_spawn(pid_t *pid, const char
|
||||
|
||||
int execvpe(const char *file, char *const argv[], char *const envp[]) {
|
||||
static int (*real)(const char *, char *const[], char *const[]);
|
||||
if (!real) real = (int (*)(const char *, char *const[], char *const[]))dlsym(RTLD_NEXT, "execvpe");
|
||||
if (!real) real = (int (*)(const char *, char *const[], char *const[]))bl_next("execvpe");
|
||||
bl_binfmt_before_search(file, argv, envp);
|
||||
real(file, argv, envp);
|
||||
return bl_binfmt_after_search(file, argv, envp, errno);
|
||||
@@ -163,8 +164,8 @@ int execvpe(const char *file, char *const argv[], char *const envp[]) {
|
||||
int posix_spawnp(pid_t *pid, const char *file, const posix_spawn_file_actions_t *fa, const posix_spawnattr_t *attr,
|
||||
char *const argv[], char *const envp[]) {
|
||||
static spawn_fn real, spawn;
|
||||
if (!real) real = (spawn_fn)dlsym(RTLD_NEXT, "posix_spawnp");
|
||||
if (!spawn) spawn = (spawn_fn)dlsym(RTLD_NEXT, "posix_spawn");
|
||||
if (!real) real = (spawn_fn)bl_next("posix_spawnp");
|
||||
if (!spawn) spawn = (spawn_fn)bl_next("posix_spawn");
|
||||
char path[PATH_MAX];
|
||||
if (!desktop() || !lookup(file, path, sizeof path)) return real(pid, file, fa, attr, argv, envp);
|
||||
return bl_binfmt_spawn(pid, path, fa, attr, argv, envp, spawn);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
@@ -25,6 +24,8 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
#define DIR_TTL_NS 5000000000LL
|
||||
#define MAX_LISTINGS 16
|
||||
#define MAX_ENTRIES 100000
|
||||
@@ -334,7 +335,7 @@ static struct handle *lookup(DIR *d) {
|
||||
|
||||
#define REAL(name) static __typeof__(&name) next_##name
|
||||
#define NEXT(name) \
|
||||
if (next_##name == NULL) next_##name = (__typeof__(&name))dlsym(RTLD_NEXT, #name)
|
||||
if (next_##name == NULL) next_##name = (__typeof__(&name))bl_next(#name)
|
||||
|
||||
REAL(opendir);
|
||||
REAL(readdir64);
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* descriptors stands in for the kernel's, and every other device is left to libdrm.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
@@ -17,6 +16,8 @@
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
#define MAX_HANDLES 4096
|
||||
|
||||
/* DRM_IOCTL_GEM_CLOSE from drm.h: _IOW('d', 0x09, struct drm_gem_close { u32 handle, pad; }). */
|
||||
@@ -50,7 +51,7 @@ static int is_kgsl(int fd) {
|
||||
}
|
||||
|
||||
static void *real(const char *name) {
|
||||
void *fn = dlsym(RTLD_NEXT, name);
|
||||
void *fn = bl_next(name);
|
||||
if (!fn) {
|
||||
errno = ENOSYS;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* Credit: fsync, the futex-based synchronization in Proton, by Elizabeth Figura and Paul Gofman;
|
||||
* futex_waitv(2) by André Almeida. */
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/futex.h>
|
||||
@@ -18,6 +17,8 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
#ifndef SYS_futex_waitv
|
||||
#define SYS_futex_waitv 449
|
||||
#endif
|
||||
@@ -112,12 +113,12 @@ static int fs_on(void) {
|
||||
}
|
||||
|
||||
static long fs_sys(long n, long a, long b, long c, long d, long e, long f) {
|
||||
if (!fs_real_syscall) fs_real_syscall = (long (*)(long, ...))dlsym(RTLD_NEXT, "syscall");
|
||||
if (!fs_real_syscall) fs_real_syscall = (long (*)(long, ...))bl_next("syscall");
|
||||
return fs_real_syscall(n, a, b, c, d, e, f);
|
||||
}
|
||||
|
||||
static void *fs_anon(size_t size) {
|
||||
if (!fs_real_mmap) fs_real_mmap = (void *(*)(void *, size_t, int, int, int, off_t))dlsym(RTLD_NEXT, "mmap");
|
||||
if (!fs_real_mmap) fs_real_mmap = (void *(*)(void *, size_t, int, int, int, off_t))bl_next("mmap");
|
||||
return fs_real_mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
}
|
||||
|
||||
@@ -341,9 +342,9 @@ static void fs_child(void) {
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void fs_init(void) {
|
||||
if (!fs_real_mmap) fs_real_mmap = (void *(*)(void *, size_t, int, int, int, off_t))dlsym(RTLD_NEXT, "mmap");
|
||||
if (!fs_real_mmap64) fs_real_mmap64 = (void *(*)(void *, size_t, int, int, int, off64_t))dlsym(RTLD_NEXT, "mmap64");
|
||||
if (!fs_real_munmap) fs_real_munmap = (int (*)(void *, size_t))dlsym(RTLD_NEXT, "munmap");
|
||||
if (!fs_real_mmap) fs_real_mmap = (void *(*)(void *, size_t, int, int, int, off_t))bl_next("mmap");
|
||||
if (!fs_real_mmap64) fs_real_mmap64 = (void *(*)(void *, size_t, int, int, int, off64_t))bl_next("mmap64");
|
||||
if (!fs_real_munmap) fs_real_munmap = (int (*)(void *, size_t))bl_next("munmap");
|
||||
if (fs_on()) pthread_atfork(fs_fork_prepare, fs_fork_parent, fs_child);
|
||||
}
|
||||
|
||||
@@ -571,7 +572,7 @@ static void fs_attach(const char *name, int fd, int create) {
|
||||
close(rfd);
|
||||
return;
|
||||
}
|
||||
if (!fs_real_mmap) fs_real_mmap = (void *(*)(void *, size_t, int, int, int, off_t))dlsym(RTLD_NEXT, "mmap");
|
||||
if (!fs_real_mmap) fs_real_mmap = (void *(*)(void *, size_t, int, int, int, off_t))bl_next("mmap");
|
||||
p = fs_real_mmap(NULL, FS_REGION, PROT_READ | PROT_WRITE, MAP_SHARED, rfd, 0);
|
||||
close(rfd);
|
||||
if (p == MAP_FAILED) {
|
||||
@@ -588,8 +589,8 @@ static void fs_attach(const char *name, int fd, int create) {
|
||||
|
||||
int shm_open(const char *name, int oflag, mode_t mode) {
|
||||
int fd, saved;
|
||||
if (!fs_real_shm_open) fs_real_shm_open = (int (*)(const char *, int, mode_t))dlsym(RTLD_NEXT, "shm_open");
|
||||
if (!fs_real_shm_unlink) fs_real_shm_unlink = (int (*)(const char *))dlsym(RTLD_NEXT, "shm_unlink");
|
||||
if (!fs_real_shm_open) fs_real_shm_open = (int (*)(const char *, int, mode_t))bl_next("shm_open");
|
||||
if (!fs_real_shm_unlink) fs_real_shm_unlink = (int (*)(const char *))bl_next("shm_unlink");
|
||||
if (!fs_real_shm_open) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
@@ -605,7 +606,7 @@ int shm_open(const char *name, int oflag, mode_t mode) {
|
||||
|
||||
int shm_unlink(const char *name) {
|
||||
int r, saved;
|
||||
if (!fs_real_shm_unlink) fs_real_shm_unlink = (int (*)(const char *))dlsym(RTLD_NEXT, "shm_unlink");
|
||||
if (!fs_real_shm_unlink) fs_real_shm_unlink = (int (*)(const char *))bl_next("shm_unlink");
|
||||
if (!fs_real_shm_unlink) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
@@ -622,7 +623,7 @@ int shm_unlink(const char *name) {
|
||||
|
||||
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t off) {
|
||||
void *p;
|
||||
if (!fs_real_mmap) fs_real_mmap = (void *(*)(void *, size_t, int, int, int, off_t))dlsym(RTLD_NEXT, "mmap");
|
||||
if (!fs_real_mmap) fs_real_mmap = (void *(*)(void *, size_t, int, int, int, off_t))bl_next("mmap");
|
||||
p = fs_real_mmap(addr, len, prot, flags, fd, off);
|
||||
if (p == MAP_FAILED && bl_noexec_retry(prot, flags, fd)) p = fs_real_mmap(addr, len, prot & ~PROT_EXEC, flags, fd, off);
|
||||
if (p != MAP_FAILED && __builtin_expect(fs_fd >= 0, 0)) fs_track(p, len, flags, fd, off);
|
||||
@@ -631,7 +632,7 @@ void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t off) {
|
||||
|
||||
void *mmap64(void *addr, size_t len, int prot, int flags, int fd, off64_t off) {
|
||||
void *p;
|
||||
if (!fs_real_mmap64) fs_real_mmap64 = (void *(*)(void *, size_t, int, int, int, off64_t))dlsym(RTLD_NEXT, "mmap64");
|
||||
if (!fs_real_mmap64) fs_real_mmap64 = (void *(*)(void *, size_t, int, int, int, off64_t))bl_next("mmap64");
|
||||
p = fs_real_mmap64(addr, len, prot, flags, fd, off);
|
||||
if (p == MAP_FAILED && bl_noexec_retry(prot, flags, fd)) p = fs_real_mmap64(addr, len, prot & ~PROT_EXEC, flags, fd, off);
|
||||
if (p != MAP_FAILED && __builtin_expect(fs_fd >= 0, 0)) fs_track(p, len, flags, fd, off);
|
||||
@@ -642,7 +643,7 @@ int munmap(void *addr, size_t len) {
|
||||
uintptr_t lo = (uintptr_t)addr, hi = lo + len;
|
||||
sigset_t old;
|
||||
int r, saved;
|
||||
if (!fs_real_munmap) fs_real_munmap = (int (*)(void *, size_t))dlsym(RTLD_NEXT, "munmap");
|
||||
if (!fs_real_munmap) fs_real_munmap = (int (*)(void *, size_t))bl_next("munmap");
|
||||
if (__builtin_expect(fs_fd < 0, 1) || !fs_overlaps(lo, hi)) return fs_real_munmap(addr, len);
|
||||
fs_block(&old);
|
||||
pthread_mutex_lock(&fs_mx);
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
#include <sys/sysmacros.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
/* The app opens one ring per configured controller; four is what its launcher offers. Nodes from
|
||||
* UINPUT_BASE up are the virtual pads made through libfakeinput's /dev/uinput stand-in. */
|
||||
#define RING_MAX 4
|
||||
@@ -208,8 +210,6 @@ static struct pad *pad_for_syslink(const char *path) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *next_symbol(const char *name) { return dlsym(RTLD_NEXT, name); }
|
||||
|
||||
char *bl_cached_realpath(const char *path, char *resolved, char *(*real)(const char *, char *))
|
||||
__attribute__((visibility("hidden")));
|
||||
|
||||
@@ -232,7 +232,7 @@ char *realpath(const char *path, char *resolved) {
|
||||
memcpy(resolved, pad->syspath, strlen(pad->syspath) + 1);
|
||||
return resolved;
|
||||
}
|
||||
if (real_realpath == NULL) real_realpath = next_symbol("realpath");
|
||||
if (real_realpath == NULL) real_realpath = bl_next("realpath");
|
||||
if (real_realpath == NULL) return NULL;
|
||||
return bl_cached_realpath(path, resolved, real_realpath);
|
||||
}
|
||||
@@ -243,7 +243,7 @@ void *udev_device_new_from_syspath(void *udev, const char *syspath) {
|
||||
struct pad *pad = caller_is_winebus(caller) ? pad_for_syspath(syspath) : NULL;
|
||||
|
||||
if (pad != NULL) return pad;
|
||||
if (real_new == NULL) real_new = next_symbol("udev_device_new_from_syspath");
|
||||
if (real_new == NULL) real_new = bl_next("udev_device_new_from_syspath");
|
||||
if (real_new == NULL) return NULL;
|
||||
return real_new(udev, syspath);
|
||||
}
|
||||
@@ -252,7 +252,7 @@ void *udev_device_ref(void *device) {
|
||||
static void *(*real_ref)(void *);
|
||||
|
||||
if (pad_for_device(device) != NULL) return device;
|
||||
if (real_ref == NULL) real_ref = next_symbol("udev_device_ref");
|
||||
if (real_ref == NULL) real_ref = bl_next("udev_device_ref");
|
||||
if (real_ref == NULL) return NULL;
|
||||
return real_ref(device);
|
||||
}
|
||||
@@ -261,7 +261,7 @@ void *udev_device_unref(void *device) {
|
||||
static void *(*real_unref)(void *);
|
||||
|
||||
if (pad_for_device(device) != NULL) return NULL;
|
||||
if (real_unref == NULL) real_unref = next_symbol("udev_device_unref");
|
||||
if (real_unref == NULL) real_unref = bl_next("udev_device_unref");
|
||||
if (real_unref == NULL) return NULL;
|
||||
return real_unref(device);
|
||||
}
|
||||
@@ -271,7 +271,7 @@ const char *udev_device_get_devnode(void *device) {
|
||||
struct pad *pad = pad_for_device(device);
|
||||
|
||||
if (pad != NULL) return pad->devnode;
|
||||
if (real_get == NULL) real_get = next_symbol("udev_device_get_devnode");
|
||||
if (real_get == NULL) real_get = bl_next("udev_device_get_devnode");
|
||||
if (real_get == NULL) return NULL;
|
||||
return real_get(device);
|
||||
}
|
||||
@@ -281,7 +281,7 @@ const char *udev_device_get_syspath(void *device) {
|
||||
struct pad *pad = pad_for_device(device);
|
||||
|
||||
if (pad != NULL) return pad->syspath;
|
||||
if (real_get == NULL) real_get = next_symbol("udev_device_get_syspath");
|
||||
if (real_get == NULL) real_get = bl_next("udev_device_get_syspath");
|
||||
if (real_get == NULL) return NULL;
|
||||
return real_get(device);
|
||||
}
|
||||
@@ -291,7 +291,7 @@ const char *udev_device_get_sysname(void *device) {
|
||||
struct pad *pad = pad_for_device(device);
|
||||
|
||||
if (pad != NULL) return pad->sysname;
|
||||
if (real_get == NULL) real_get = next_symbol("udev_device_get_sysname");
|
||||
if (real_get == NULL) real_get = bl_next("udev_device_get_sysname");
|
||||
if (real_get == NULL) return NULL;
|
||||
return real_get(device);
|
||||
}
|
||||
@@ -301,7 +301,7 @@ const char *udev_device_get_subsystem(void *device) {
|
||||
struct pad *pad = pad_for_device(device);
|
||||
|
||||
if (pad != NULL) return "input";
|
||||
if (real_get == NULL) real_get = next_symbol("udev_device_get_subsystem");
|
||||
if (real_get == NULL) real_get = bl_next("udev_device_get_subsystem");
|
||||
if (real_get == NULL) return NULL;
|
||||
return real_get(device);
|
||||
}
|
||||
@@ -311,7 +311,7 @@ const char *udev_device_get_devtype(void *device) {
|
||||
struct pad *pad = pad_for_device(device);
|
||||
|
||||
if (pad != NULL) return NULL;
|
||||
if (real_get == NULL) real_get = next_symbol("udev_device_get_devtype");
|
||||
if (real_get == NULL) real_get = bl_next("udev_device_get_devtype");
|
||||
if (real_get == NULL) return NULL;
|
||||
return real_get(device);
|
||||
}
|
||||
@@ -321,7 +321,7 @@ const char *udev_device_get_action(void *device) {
|
||||
struct pad *pad = pad_for_device(device);
|
||||
|
||||
if (pad != NULL) return NULL;
|
||||
if (real_get == NULL) real_get = next_symbol("udev_device_get_action");
|
||||
if (real_get == NULL) real_get = bl_next("udev_device_get_action");
|
||||
if (real_get == NULL) return NULL;
|
||||
return real_get(device);
|
||||
}
|
||||
@@ -331,7 +331,7 @@ dev_t udev_device_get_devnum(void *device) {
|
||||
struct pad *pad = pad_for_device(device);
|
||||
|
||||
if (pad != NULL) return pad->devnum;
|
||||
if (real_get == NULL) real_get = next_symbol("udev_device_get_devnum");
|
||||
if (real_get == NULL) real_get = bl_next("udev_device_get_devnum");
|
||||
if (real_get == NULL) return makedev(0, 0);
|
||||
return real_get(device);
|
||||
}
|
||||
@@ -349,7 +349,7 @@ void *udev_device_get_parent_with_subsystem_devtype(void *device, const char *su
|
||||
if (subsystem != NULL && strcmp(subsystem, "input") == 0 && devtype == NULL) return pad;
|
||||
return NULL;
|
||||
}
|
||||
if (real_get == NULL) real_get = next_symbol("udev_device_get_parent_with_subsystem_devtype");
|
||||
if (real_get == NULL) real_get = bl_next("udev_device_get_parent_with_subsystem_devtype");
|
||||
if (real_get == NULL) return NULL;
|
||||
return real_get(device, subsystem, devtype);
|
||||
}
|
||||
@@ -359,7 +359,7 @@ const char *udev_device_get_sysattr_value(void *device, const char *attr) {
|
||||
struct pad *pad = pad_for_device(device);
|
||||
|
||||
if (pad != NULL) return attr != NULL && strcmp(attr, "uevent") == 0 ? pad->uevent : NULL;
|
||||
if (real_get == NULL) real_get = next_symbol("udev_device_get_sysattr_value");
|
||||
if (real_get == NULL) real_get = bl_next("udev_device_get_sysattr_value");
|
||||
if (real_get == NULL) return NULL;
|
||||
return real_get(device, attr);
|
||||
}
|
||||
@@ -369,7 +369,7 @@ const char *udev_device_get_property_value(void *device, const char *key) {
|
||||
struct pad *pad = pad_for_device(device);
|
||||
|
||||
if (pad != NULL) return key != NULL && strcmp(key, "ID_INPUT_JOYSTICK") == 0 ? "1" : NULL;
|
||||
if (real_get == NULL) real_get = next_symbol("udev_device_get_property_value");
|
||||
if (real_get == NULL) real_get = bl_next("udev_device_get_property_value");
|
||||
if (real_get == NULL) return NULL;
|
||||
return real_get(device, key);
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
* becomes a rename that will not replace an existing name, which is all the link promised there.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
@@ -26,6 +25,8 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
static int refused(int error) { return error == EPERM || error == EACCES; }
|
||||
|
||||
static int moves_instead(void) {
|
||||
@@ -45,7 +46,7 @@ __attribute__((visibility("hidden"))) int bl_link_lock(const char *oldpath, cons
|
||||
if (result == 0 || !refused(errno)) return result;
|
||||
if (moves_instead()) return renameat2(AT_FDCWD, oldpath, AT_FDCWD, newpath, RENAME_NOREPLACE);
|
||||
if (!lock_name(newpath)) return result;
|
||||
if (real_symlink == NULL) real_symlink = dlsym(RTLD_NEXT, "symlink");
|
||||
if (real_symlink == NULL) real_symlink = bl_next("symlink");
|
||||
return real_symlink(oldpath, newpath);
|
||||
}
|
||||
|
||||
@@ -53,14 +54,14 @@ __attribute__((visibility("hidden"))) int bl_link_lock(const char *oldpath, cons
|
||||
#if __SIZEOF_POINTER__ != 8
|
||||
int link(const char *oldpath, const char *newpath) {
|
||||
static int (*real_link)(const char *, const char *);
|
||||
if (real_link == NULL) real_link = dlsym(RTLD_NEXT, "link");
|
||||
if (real_link == NULL) real_link = bl_next("link");
|
||||
return bl_link_lock(oldpath, newpath, real_link(oldpath, newpath));
|
||||
}
|
||||
#endif
|
||||
|
||||
int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, int flags) {
|
||||
static int (*real_linkat)(int, const char *, int, const char *, int);
|
||||
if (real_linkat == NULL) real_linkat = dlsym(RTLD_NEXT, "linkat");
|
||||
if (real_linkat == NULL) real_linkat = bl_next("linkat");
|
||||
int result = real_linkat(olddirfd, oldpath, newdirfd, newpath, flags);
|
||||
if (result != 0 && refused(errno) && flags == 0 && moves_instead())
|
||||
return renameat2(olddirfd, oldpath, newdirfd, newpath, RENAME_NOREPLACE);
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
* answer is "nobody", so it is reported unlocked rather than granted.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdarg.h>
|
||||
@@ -28,6 +27,8 @@
|
||||
#include <sys/file.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
/* The filesystem refuses the whole operation, rather than declining this particular lock. */
|
||||
static int unsupported(int error) {
|
||||
return error == ENOSYS || error == ENOTSUP || error == EOPNOTSUPP;
|
||||
@@ -37,7 +38,7 @@ int flock(int fd, int operation) {
|
||||
static int (*real_flock)(int, int);
|
||||
int result;
|
||||
|
||||
if (real_flock == NULL) real_flock = dlsym(RTLD_NEXT, "flock");
|
||||
if (real_flock == NULL) real_flock = bl_next("flock");
|
||||
if (real_flock == NULL) return 0;
|
||||
result = real_flock(fd, operation);
|
||||
if (result < 0 && unsupported(errno)) return 0;
|
||||
@@ -88,7 +89,7 @@ int fcntl(int fd, int cmd, ...) {
|
||||
arg = va_arg(ap, void *);
|
||||
va_end(ap);
|
||||
|
||||
if (real_fcntl == NULL) real_fcntl = dlsym(RTLD_NEXT, "fcntl");
|
||||
if (real_fcntl == NULL) real_fcntl = bl_next("fcntl");
|
||||
if (real_fcntl == NULL) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <arpa/inet.h>
|
||||
#include <spawn.h>
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/netlink.h>
|
||||
#include <netinet/in.h>
|
||||
@@ -24,6 +23,8 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
static const char *net_dir(void) {
|
||||
static char dir[256];
|
||||
if (!dir[0]) {
|
||||
@@ -124,7 +125,7 @@ __attribute__((visibility("hidden"))) int bl_udevmon_stand_in(int fd);
|
||||
|
||||
int bind(int fd, const struct sockaddr *addr, socklen_t len) {
|
||||
static bind_fn real;
|
||||
if (!real) real = (bind_fn)dlsym(RTLD_NEXT, "bind");
|
||||
if (!real) real = (bind_fn)bl_next("bind");
|
||||
if (bl_udevmon_stand_in(fd)) {
|
||||
if (addr == NULL || len < sizeof(struct sockaddr_nl) || addr->sa_family != AF_NETLINK) {
|
||||
errno = EINVAL;
|
||||
@@ -151,7 +152,7 @@ int bind(int fd, const struct sockaddr *addr, socklen_t len) {
|
||||
|
||||
int listen(int fd, int backlog) {
|
||||
static listen_fn real;
|
||||
if (!real) real = (listen_fn)dlsym(RTLD_NEXT, "listen");
|
||||
if (!real) real = (listen_fn)bl_next("listen");
|
||||
int ret = real(fd, backlog);
|
||||
if (ret == 0) record_port(fd, 0);
|
||||
return ret;
|
||||
@@ -233,7 +234,7 @@ static int guest_peer(int fd) {
|
||||
|
||||
int connect(int fd, const struct sockaddr *addr, socklen_t len) {
|
||||
static connect_fn real;
|
||||
if (!real) real = (connect_fn)dlsym(RTLD_NEXT, "connect");
|
||||
if (!real) real = (connect_fn)bl_next("connect");
|
||||
bind_before_loopback_connect(fd, addr, len);
|
||||
unsigned peer = loopback_family(addr, len) ? addr_port(addr, len) : 0;
|
||||
if (peer) record_port(fd, peer);
|
||||
@@ -249,7 +250,7 @@ int connect(int fd, const struct sockaddr *addr, socklen_t len) {
|
||||
|
||||
int accept4(int fd, struct sockaddr *addr, socklen_t *len, int flags) {
|
||||
static accept4_fn real;
|
||||
if (!real) real = (accept4_fn)dlsym(RTLD_NEXT, "accept4");
|
||||
if (!real) real = (accept4_fn)bl_next("accept4");
|
||||
for (;;) {
|
||||
int ret = real(fd, addr, len, flags);
|
||||
if (ret < 0) return ret;
|
||||
@@ -327,7 +328,7 @@ static int maybe_answer_lsof(const char *path, char *const argv[]) {
|
||||
|
||||
int execve(const char *path, char *const argv[], char *const envp[]) {
|
||||
static int (*real)(const char *, char *const[], char *const[]);
|
||||
if (!real) real = (int (*)(const char *, char *const[], char *const[]))dlsym(RTLD_NEXT, "execve");
|
||||
if (!real) real = (int (*)(const char *, char *const[], char *const[]))bl_next("execve");
|
||||
maybe_answer_lsof(path, argv);
|
||||
bl_binfmt_before(path, argv, envp);
|
||||
real(path, argv, envp);
|
||||
@@ -353,7 +354,7 @@ static int answer_lsof_via_spawn(const char *path, char *const argv[], pid_t *pi
|
||||
|
||||
int execvp(const char *file, char *const argv[]) {
|
||||
static int (*real)(const char *, char *const[]);
|
||||
if (!real) real = (int (*)(const char *, char *const[]))dlsym(RTLD_NEXT, "execvp");
|
||||
if (!real) real = (int (*)(const char *, char *const[]))bl_next("execvp");
|
||||
maybe_answer_lsof(file, argv);
|
||||
bl_binfmt_before_search(file, argv, environ);
|
||||
real(file, argv);
|
||||
@@ -362,7 +363,7 @@ int execvp(const char *file, char *const argv[]) {
|
||||
|
||||
int execv(const char *path, char *const argv[]) {
|
||||
static int (*real)(const char *, char *const[]);
|
||||
if (!real) real = (int (*)(const char *, char *const[]))dlsym(RTLD_NEXT, "execv");
|
||||
if (!real) real = (int (*)(const char *, char *const[]))bl_next("execv");
|
||||
maybe_answer_lsof(path, argv);
|
||||
bl_binfmt_before(path, argv, environ);
|
||||
real(path, argv);
|
||||
@@ -377,6 +378,6 @@ int posix_spawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t *
|
||||
return answer_lsof_via_spawn(path, argv, pid);
|
||||
}
|
||||
static spawn_fn real;
|
||||
if (!real) real = (spawn_fn)dlsym(RTLD_NEXT, "posix_spawn");
|
||||
if (!real) real = (spawn_fn)bl_next("posix_spawn");
|
||||
return bl_binfmt_spawn(pid, path, fa, attr, argv, envp, real);
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
*/
|
||||
#define _GNU_SOURCE 1
|
||||
#include <arpa/inet.h>
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <ifaddrs.h>
|
||||
@@ -32,6 +31,8 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
#define LINK_FILE "/etc/droiddeck-net"
|
||||
#define LO_NAME "lo"
|
||||
#define LO_INDEX 1
|
||||
@@ -68,7 +69,7 @@ typedef FILE *(*fopen_fn)(const char *, const char *);
|
||||
|
||||
static int read_link(struct link *link) {
|
||||
static fopen_fn real_fopen;
|
||||
if (real_fopen == NULL) real_fopen = (fopen_fn) dlsym(RTLD_NEXT, "fopen");
|
||||
if (real_fopen == NULL) real_fopen = (fopen_fn) bl_next("fopen");
|
||||
FILE *f = real_fopen ? real_fopen(LINK_FILE, "re") : NULL;
|
||||
if (f == NULL) return 0;
|
||||
|
||||
@@ -222,8 +223,8 @@ static freeifaddrs_fn real_freeifaddrs;
|
||||
int getifaddrs(struct ifaddrs **out) {
|
||||
typedef int (*getifaddrs_fn)(struct ifaddrs **);
|
||||
static getifaddrs_fn real;
|
||||
if (real == NULL) real = (getifaddrs_fn) dlsym(RTLD_NEXT, "getifaddrs");
|
||||
if (real_freeifaddrs == NULL) real_freeifaddrs = (freeifaddrs_fn) dlsym(RTLD_NEXT, "freeifaddrs");
|
||||
if (real == NULL) real = (getifaddrs_fn) bl_next("getifaddrs");
|
||||
if (real_freeifaddrs == NULL) real_freeifaddrs = (freeifaddrs_fn) bl_next("freeifaddrs");
|
||||
struct ifaddrs *list = NULL;
|
||||
int asked = !fresh(&ifaddrs_unusable_at);
|
||||
int rc = -1, saved = errno;
|
||||
@@ -273,7 +274,7 @@ void freeifaddrs(struct ifaddrs *list) {
|
||||
free((struct entry *) list - 1);
|
||||
return;
|
||||
}
|
||||
if (real_freeifaddrs == NULL) real_freeifaddrs = (freeifaddrs_fn) dlsym(RTLD_NEXT, "freeifaddrs");
|
||||
if (real_freeifaddrs == NULL) real_freeifaddrs = (freeifaddrs_fn) bl_next("freeifaddrs");
|
||||
real_freeifaddrs(list);
|
||||
}
|
||||
|
||||
@@ -286,7 +287,7 @@ struct name_block {
|
||||
struct if_nameindex *if_nameindex(void) {
|
||||
typedef struct if_nameindex *(*if_nameindex_fn)(void);
|
||||
static if_nameindex_fn real;
|
||||
if (real == NULL) real = (if_nameindex_fn) dlsym(RTLD_NEXT, "if_nameindex");
|
||||
if (real == NULL) real = (if_nameindex_fn) bl_next("if_nameindex");
|
||||
struct if_nameindex *list = NULL;
|
||||
int asked = !fresh(&nameindex_unusable_at);
|
||||
int saved = errno;
|
||||
@@ -323,12 +324,12 @@ void if_freenameindex(struct if_nameindex *list) {
|
||||
free(list);
|
||||
return;
|
||||
}
|
||||
((if_freenameindex_fn) dlsym(RTLD_NEXT, "if_freenameindex"))(list);
|
||||
((if_freenameindex_fn) bl_next("if_freenameindex"))(list);
|
||||
}
|
||||
|
||||
unsigned int if_nametoindex(const char *name) {
|
||||
typedef unsigned int (*if_nametoindex_fn)(const char *);
|
||||
unsigned int index = ((if_nametoindex_fn) dlsym(RTLD_NEXT, "if_nametoindex"))(name);
|
||||
unsigned int index = ((if_nametoindex_fn) bl_next("if_nametoindex"))(name);
|
||||
if (index != 0 || name == NULL) return index;
|
||||
int saved = errno;
|
||||
struct link link;
|
||||
@@ -340,7 +341,7 @@ unsigned int if_nametoindex(const char *name) {
|
||||
|
||||
char *if_indextoname(unsigned int index, char name[IF_NAMESIZE]) {
|
||||
typedef char *(*if_indextoname_fn)(unsigned int, char *);
|
||||
char *found = ((if_indextoname_fn) dlsym(RTLD_NEXT, "if_indextoname"))(index, name);
|
||||
char *found = ((if_indextoname_fn) bl_next("if_indextoname"))(index, name);
|
||||
if (found || name == NULL) return found;
|
||||
int saved = errno;
|
||||
struct link link;
|
||||
@@ -439,7 +440,7 @@ int ioctl(int fd, unsigned long request, ...) {
|
||||
int rc;
|
||||
if (bl_ntsync_ioctl(fd, request, arg, &rc)) return rc;
|
||||
if (bl_drm_gem_close(fd, request, arg, &rc)) return rc;
|
||||
if (real == NULL) real = (ioctl_fn) dlsym(RTLD_NEXT, "ioctl");
|
||||
if (real == NULL) real = (ioctl_fn) bl_next("ioctl");
|
||||
if (!bl_kgsl_poll(real, fd, request, arg, &rc)) rc = real(fd, request, arg);
|
||||
bl_kgsl_chip_id_fixup(request, arg, rc);
|
||||
if (arg == NULL || !is_link_query(request)) return rc;
|
||||
@@ -579,12 +580,12 @@ static FILE *open_stream(fopen_fn real, const char *path, const char *mode) {
|
||||
|
||||
FILE *fopen(const char *path, const char *mode) {
|
||||
static fopen_fn real;
|
||||
if (real == NULL) real = (fopen_fn) dlsym(RTLD_NEXT, "fopen");
|
||||
if (real == NULL) real = (fopen_fn) bl_next("fopen");
|
||||
return open_stream(real, path, mode);
|
||||
}
|
||||
|
||||
FILE *fopen64(const char *path, const char *mode) {
|
||||
static fopen_fn real;
|
||||
if (real == NULL) real = (fopen_fn) dlsym(RTLD_NEXT, "fopen64");
|
||||
if (real == NULL) real = (fopen_fn) bl_next("fopen64");
|
||||
return open_stream(real, path, mode);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* next.c: the definition a hook passes a call on to, as dlsym(RTLD_NEXT, name) finds it.
|
||||
*
|
||||
* Before glibc 2.34 a thread's first dlsym callocs dlerror's state. A hook first called from inside
|
||||
* an allocator that is still starting up - Chromium's PartitionAlloc calls syscall() from its first
|
||||
* malloc - re-enters the allocator there, and it waits for itself forever: every Electron and
|
||||
* Chromium program on Steam's x86 runtime (glibc 2.31) hung at start. There the libraries after this
|
||||
* one are searched by hand, with nothing allocated; an object or symbol that walk cannot handle is
|
||||
* left to dlsym.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <link.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
/* NEXT_TEST: tools/tests builds the walk on a newer glibc. */
|
||||
#if __GLIBC_PREREQ(2, 34) && !defined(NEXT_TEST)
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name) {
|
||||
return dlsym(RTLD_NEXT, name);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
struct search {
|
||||
const char *name;
|
||||
uint32_t hash;
|
||||
int passed;
|
||||
int unsure;
|
||||
void *found;
|
||||
};
|
||||
|
||||
static uint32_t gnu_hash(const char *name) {
|
||||
uint32_t h = 5381;
|
||||
for (; *name; name++) h = h * 33 + (unsigned char)*name;
|
||||
return h;
|
||||
}
|
||||
|
||||
/* glibc relocates these entries in place, but not every object's (the vDSO's). */
|
||||
static ElfW(Addr) address(const struct dl_phdr_info *info, ElfW(Addr) value) {
|
||||
return value < info->dlpi_addr ? info->dlpi_addr + value : value;
|
||||
}
|
||||
|
||||
/* The object's definition, as glibc 2.36 and later pick it: unversioned or the default version. */
|
||||
static void find(const struct dl_phdr_info *info, const ElfW(Dyn) *dyn, struct search *s) {
|
||||
const uint32_t *table = NULL;
|
||||
const ElfW(Sym) *symbols = NULL;
|
||||
const char *strings = NULL;
|
||||
const ElfW(Half) *versions = NULL;
|
||||
for (; dyn->d_tag != DT_NULL; dyn++) {
|
||||
if (dyn->d_tag == DT_GNU_HASH) table = (const uint32_t *)address(info, dyn->d_un.d_ptr);
|
||||
else if (dyn->d_tag == DT_SYMTAB) symbols = (const ElfW(Sym) *)address(info, dyn->d_un.d_ptr);
|
||||
else if (dyn->d_tag == DT_STRTAB) strings = (const char *)address(info, dyn->d_un.d_ptr);
|
||||
else if (dyn->d_tag == DT_VERSYM) versions = (const ElfW(Half) *)address(info, dyn->d_un.d_ptr);
|
||||
}
|
||||
if (!table || !symbols || !strings || !table[0]) {
|
||||
s->unsure = 1;
|
||||
return;
|
||||
}
|
||||
const uint32_t *buckets = (const uint32_t *)((const ElfW(Addr) *)(table + 4) + table[2]);
|
||||
const uint32_t *chain = buckets + table[0] - table[1];
|
||||
const ElfW(Sym) *only = NULL;
|
||||
int matches = 0, defaults = 0;
|
||||
for (uint32_t i = buckets[s->hash % table[0]]; i >= table[1] && i != 0; i++) {
|
||||
const ElfW(Sym) *sym = &symbols[i];
|
||||
if ((chain[i] | 1) == (s->hash | 1) && sym->st_shndx != SHN_UNDEF && sym->st_value != 0 &&
|
||||
ELF64_ST_BIND(sym->st_info) != STB_LOCAL && !strcmp(strings + sym->st_name, s->name)) {
|
||||
if (ELF64_ST_TYPE(sym->st_info) == STT_GNU_IFUNC || ELF64_ST_TYPE(sym->st_info) == STT_TLS) {
|
||||
s->unsure = 1;
|
||||
return;
|
||||
}
|
||||
if (!versions || (versions[i] & 0x7fff) < 2) {
|
||||
only = sym;
|
||||
defaults = 1;
|
||||
break;
|
||||
}
|
||||
matches++;
|
||||
if (!(versions[i] & 0x8000) && defaults++ == 0) only = sym;
|
||||
}
|
||||
if (chain[i] & 1) break;
|
||||
}
|
||||
/* Only hidden versions: dlsym's own rules decide. */
|
||||
if (defaults == 1) s->found = (void *)(info->dlpi_addr + only->st_value);
|
||||
else if (matches) s->unsure = 1;
|
||||
}
|
||||
|
||||
static int visit(struct dl_phdr_info *info, size_t size, void *data) {
|
||||
struct search *s = data;
|
||||
const ElfW(Dyn) *dyn = NULL;
|
||||
(void)size;
|
||||
for (int i = 0; i < info->dlpi_phnum; i++)
|
||||
if (info->dlpi_phdr[i].p_type == PT_DYNAMIC) dyn = (const ElfW(Dyn) *)(info->dlpi_addr + info->dlpi_phdr[i].p_vaddr);
|
||||
if (!dyn) return 0;
|
||||
if (!s->passed) {
|
||||
s->passed = dyn == _DYNAMIC;
|
||||
return 0;
|
||||
}
|
||||
find(info, dyn, s);
|
||||
return s->found || s->unsure;
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name) {
|
||||
struct search s = {name, gnu_hash(name), 0, 0, NULL};
|
||||
dl_iterate_phdr(visit, &s);
|
||||
if (s.found || (s.passed && !s.unsure)) return s.found;
|
||||
return dlsym(RTLD_NEXT, name);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <sys/statvfs.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
/* NOEXEC_TEST: tools/tests builds it for the host. */
|
||||
#if defined(__aarch64__) || defined(NOEXEC_TEST)
|
||||
|
||||
@@ -42,7 +44,7 @@ static int (*real_mprotect)(void *, size_t, int);
|
||||
|
||||
/* Looked up before anything runs: Wine calls mprotect from its signal handlers, where dlsym is unsafe. */
|
||||
__attribute__((constructor)) static void noexec_init(void) {
|
||||
real_mprotect = (int (*)(void *, size_t, int))dlsym(RTLD_NEXT, "mprotect");
|
||||
real_mprotect = (int (*)(void *, size_t, int))bl_next("mprotect");
|
||||
}
|
||||
|
||||
int mprotect(void *addr, size_t len, int prot) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* droiddeck-ntsync: a userspace implementation of the /dev/ntsync interface for kernels without the driver.
|
||||
* Credit: ntsync, the kernel driver and its Wine client, by Elizabeth Figura. */
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/futex.h>
|
||||
@@ -21,6 +20,8 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
struct ns_sem_args {
|
||||
uint32_t count;
|
||||
uint32_t max;
|
||||
@@ -194,7 +195,7 @@ static int ns_on(void) {
|
||||
}
|
||||
|
||||
static int ns_close_fd(int fd) {
|
||||
if (!ns_real_close) ns_real_close = (int (*)(int))dlsym(RTLD_NEXT, "close");
|
||||
if (!ns_real_close) ns_real_close = (int (*)(int))bl_next("close");
|
||||
return ns_real_close(fd);
|
||||
}
|
||||
|
||||
@@ -1242,7 +1243,7 @@ int bl_udevmon_recvmsg(int fd, struct msghdr *msg, int flags, ssize_t (*real_rec
|
||||
|
||||
ssize_t recvmsg(int fd, struct msghdr *msg, int flags) {
|
||||
static ssize_t (*real)(int, struct msghdr *, int);
|
||||
if (!real) real = (ssize_t (*)(int, struct msghdr *, int))dlsym(RTLD_NEXT, "recvmsg");
|
||||
if (!real) real = (ssize_t (*)(int, struct msghdr *, int))bl_next("recvmsg");
|
||||
/* udevmon.c's stand-in netlink sockets first: their messages are dressed as udevd's. */
|
||||
ssize_t answered;
|
||||
if (msg && bl_udevmon_recvmsg(fd, msg, flags, real, &answered)) return answered;
|
||||
@@ -1266,7 +1267,7 @@ int close(int fd) {
|
||||
|
||||
int dup2(int oldfd, int newfd) {
|
||||
static int (*real)(int, int);
|
||||
if (!real) real = (int (*)(int, int))dlsym(RTLD_NEXT, "dup2");
|
||||
if (!real) real = (int (*)(int, int))bl_next("dup2");
|
||||
if (oldfd != newfd && newfd >= 0) bl_fsync_fds_closed((unsigned int)newfd, (unsigned int)newfd);
|
||||
uint64_t *t = __atomic_load_n(&ns_fdt, __ATOMIC_ACQUIRE);
|
||||
if (t && oldfd != newfd && newfd >= 0 && newfd < NS_FDS && __atomic_load_n(&t[newfd], __ATOMIC_ACQUIRE)) ns_forget(newfd);
|
||||
@@ -1275,7 +1276,7 @@ int dup2(int oldfd, int newfd) {
|
||||
|
||||
int dup3(int oldfd, int newfd, int flags) {
|
||||
static int (*real)(int, int, int);
|
||||
if (!real) real = (int (*)(int, int, int))dlsym(RTLD_NEXT, "dup3");
|
||||
if (!real) real = (int (*)(int, int, int))bl_next("dup3");
|
||||
if (oldfd != newfd && newfd >= 0) bl_fsync_fds_closed((unsigned int)newfd, (unsigned int)newfd);
|
||||
uint64_t *t = __atomic_load_n(&ns_fdt, __ATOMIC_ACQUIRE);
|
||||
if (t && oldfd != newfd && newfd >= 0 && newfd < NS_FDS && __atomic_load_n(&t[newfd], __ATOMIC_ACQUIRE)) ns_forget(newfd);
|
||||
@@ -1291,7 +1292,7 @@ static void ns_forget_range(unsigned int first, unsigned int last) {
|
||||
|
||||
int close_range(unsigned int first, unsigned int last, int flags) {
|
||||
static int (*real)(unsigned int, unsigned int, int);
|
||||
if (!real) real = (int (*)(unsigned int, unsigned int, int))dlsym(RTLD_NEXT, "close_range");
|
||||
if (!real) real = (int (*)(unsigned int, unsigned int, int))bl_next("close_range");
|
||||
if (!(flags & CLOSE_RANGE_CLOEXEC)) {
|
||||
bl_fsync_fds_closed(first, last);
|
||||
ns_forget_range(first, last);
|
||||
@@ -1305,7 +1306,7 @@ int close_range(unsigned int first, unsigned int last, int flags) {
|
||||
|
||||
void closefrom(int lowfd) {
|
||||
static void (*real)(int);
|
||||
if (!real) real = (void (*)(int))dlsym(RTLD_NEXT, "closefrom");
|
||||
if (!real) real = (void (*)(int))bl_next("closefrom");
|
||||
if (lowfd >= 0) {
|
||||
bl_fsync_fds_closed((unsigned int)lowfd, ~0u);
|
||||
ns_forget_range((unsigned int)lowfd, ~0u);
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
* A process status is served without the container's tracer in it; see tracer.c.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdarg.h>
|
||||
@@ -33,6 +32,8 @@
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
int bl_status_without_tracer(const char *path, int flags) __attribute__((visibility("hidden")));
|
||||
int bl_writable_retry(int dirfd, const char *path, int flags) __attribute__((visibility("hidden")));
|
||||
int bl_ntsync_open(const char *path, int flags) __attribute__((visibility("hidden")));
|
||||
@@ -76,7 +77,7 @@ int open(const char *path, int flags, ...) {
|
||||
}
|
||||
if ((fd = bl_status_without_tracer(path, flags)) >= 0) return fd;
|
||||
if ((fd = bl_ntsync_open(path, flags)) != -2) return fd;
|
||||
if (real_open == NULL) real_open = dlsym(RTLD_NEXT, "open");
|
||||
if (real_open == NULL) real_open = bl_next("open");
|
||||
if (real_open == NULL) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
@@ -102,7 +103,7 @@ int openat(int dirfd, const char *path, int flags, ...) {
|
||||
}
|
||||
if ((fd = bl_status_without_tracer(path, flags)) >= 0) return fd;
|
||||
if ((fd = bl_ntsync_open(path, flags)) != -2) return fd;
|
||||
if (real_openat == NULL) real_openat = dlsym(RTLD_NEXT, "openat");
|
||||
if (real_openat == NULL) real_openat = bl_next("openat");
|
||||
if (real_openat == NULL) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* BL_NO_PATH_CACHE=1 turns it off.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
@@ -24,6 +23,8 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
#define PATH_CACHE_SLOTS 256
|
||||
#define PATH_CACHE_KEY_MAX 512
|
||||
#define PATH_CACHE_TTL_NS 2000000000LL
|
||||
@@ -148,7 +149,7 @@ static int cached_access(const char *path, int mode, int flags, int (*check)(con
|
||||
static int (*next_faccessat(void))(int, const char *, int, int) {
|
||||
static int (*real)(int, const char *, int, int);
|
||||
|
||||
if (real == NULL) real = (int (*)(int, const char *, int, int))dlsym(RTLD_NEXT, "faccessat");
|
||||
if (real == NULL) real = (int (*)(int, const char *, int, int))bl_next("faccessat");
|
||||
return real;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
* the link back finds the head without depending on libc's layout.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/futex.h>
|
||||
@@ -19,6 +18,8 @@
|
||||
#include <sys/uio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
#ifndef SYS_openat2
|
||||
#define SYS_openat2 437
|
||||
#endif
|
||||
@@ -123,7 +124,7 @@ long syscall(long number, ...) {
|
||||
return handled;
|
||||
}
|
||||
if (!real_syscall) {
|
||||
real_syscall = (long (*)(long, ...)) dlsym(RTLD_NEXT, "syscall");
|
||||
real_syscall = (long (*)(long, ...)) bl_next("syscall");
|
||||
}
|
||||
if (number == SYS_openat2) {
|
||||
if (!__atomic_load_n(&openat2_missing, __ATOMIC_RELAXED)) {
|
||||
|
||||
@@ -19,13 +19,14 @@
|
||||
* card. Every other path is passed through untouched.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/statfs.h>
|
||||
#include <sys/statvfs.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
/* A library root, or its steamapps, resolved to the directory that holds the games. Returns the
|
||||
* rewritten path in `buf`, or NULL when `path` is not one of ours and should be left alone. */
|
||||
static const char *content_of(const char *path, char *buf, size_t len) {
|
||||
@@ -64,7 +65,7 @@ static const char *content_of(const char *path, char *buf, size_t len) {
|
||||
char buf[4096]; \
|
||||
const char *content; \
|
||||
if (!real) \
|
||||
real = dlsym(RTLD_NEXT, #name); \
|
||||
real = bl_next(#name); \
|
||||
if (!real) \
|
||||
return -1; \
|
||||
content = content_of(path, buf, sizeof(buf)); \
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
* missing entry therefore does not prove the operation did not happen.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
@@ -22,6 +21,8 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
static int (*next_fstat)(int, struct stat *);
|
||||
static int (*next_fallocate)(int, int, off_t, off_t);
|
||||
static int (*next_posix_fallocate)(int, off_t, off_t);
|
||||
@@ -93,7 +94,7 @@ static int on_selected_device(int fd, const struct stat *known) {
|
||||
initialize();
|
||||
if (!enabled) return 0;
|
||||
if (known == NULL) {
|
||||
if (next_fstat == NULL) next_fstat = dlsym(RTLD_NEXT, "fstat");
|
||||
if (next_fstat == NULL) next_fstat = bl_next("fstat");
|
||||
if (next_fstat == NULL || next_fstat(fd, &local) != 0) return 0;
|
||||
known = &local;
|
||||
}
|
||||
@@ -181,7 +182,7 @@ static int cannot_reserve(long long device) {
|
||||
|
||||
static void note_cannot_reserve(int fd) {
|
||||
struct stat st;
|
||||
if (next_fstat == NULL) next_fstat = dlsym(RTLD_NEXT, "fstat");
|
||||
if (next_fstat == NULL) next_fstat = bl_next("fstat");
|
||||
if (next_fstat == NULL || next_fstat(fd, &st) != 0 || cannot_reserve((long long)st.st_dev)) return;
|
||||
unsigned int slot = __atomic_load_n(&unreserved_count, __ATOMIC_RELAXED);
|
||||
while (slot < 16 && !__atomic_compare_exchange_n(&unreserved_count, &slot, slot + 1, 0, __ATOMIC_ACQ_REL,
|
||||
@@ -192,7 +193,7 @@ static void note_cannot_reserve(int fd) {
|
||||
|
||||
int fallocate(int fd, int mode, off_t offset, off_t length) {
|
||||
int entry_errno = errno;
|
||||
if (next_fallocate == NULL) next_fallocate = dlsym(RTLD_NEXT, "fallocate");
|
||||
if (next_fallocate == NULL) next_fallocate = bl_next("fallocate");
|
||||
if (next_fallocate == NULL) { errno = ENOSYS; return -1; }
|
||||
int selected = on_selected_device(fd, NULL);
|
||||
errno = entry_errno;
|
||||
@@ -208,7 +209,7 @@ int fallocate(int fd, int mode, off_t offset, off_t length) {
|
||||
|
||||
int posix_fallocate(int fd, off_t offset, off_t length) {
|
||||
int entry_errno = errno;
|
||||
if (next_posix_fallocate == NULL) next_posix_fallocate = dlsym(RTLD_NEXT, "posix_fallocate");
|
||||
if (next_posix_fallocate == NULL) next_posix_fallocate = bl_next("posix_fallocate");
|
||||
if (next_posix_fallocate == NULL) { errno = entry_errno; return ENOSYS; }
|
||||
int selected = on_selected_device(fd, NULL);
|
||||
errno = entry_errno;
|
||||
@@ -229,7 +230,7 @@ int posix_fallocate(int fd, off_t offset, off_t length) {
|
||||
|
||||
ssize_t pwrite(int fd, const void *buffer, size_t count, off_t offset) {
|
||||
int entry_errno = errno;
|
||||
if (next_pwrite == NULL) next_pwrite = dlsym(RTLD_NEXT, "pwrite");
|
||||
if (next_pwrite == NULL) next_pwrite = bl_next("pwrite");
|
||||
if (next_pwrite == NULL) { errno = ENOSYS; return -1; }
|
||||
initialize();
|
||||
uint64_t sequence = enabled ? add(&pwrite_seen, 1) : 1;
|
||||
@@ -254,14 +255,14 @@ ssize_t pwrite(int fd, const void *buffer, size_t count, off_t offset) {
|
||||
|
||||
int ftruncate(int fd, off_t length) {
|
||||
int entry_errno = errno;
|
||||
if (next_ftruncate == NULL) next_ftruncate = dlsym(RTLD_NEXT, "ftruncate");
|
||||
if (next_ftruncate == NULL) next_ftruncate = bl_next("ftruncate");
|
||||
if (next_ftruncate == NULL) { errno = ENOSYS; return -1; }
|
||||
int selected = on_selected_device(fd, NULL);
|
||||
uint64_t start = selected ? now_ns() : 0;
|
||||
struct stat st;
|
||||
int known = 0;
|
||||
if (steam_client()) {
|
||||
if (next_fstat == NULL) next_fstat = dlsym(RTLD_NEXT, "fstat");
|
||||
if (next_fstat == NULL) next_fstat = bl_next("fstat");
|
||||
/* staging_file() reads /proc/self/fd, a round trip through proot: skip it for a shrink, which
|
||||
* is passed through untouched either way. */
|
||||
known = next_fstat != NULL && next_fstat(fd, &st) == 0 && S_ISREG(st.st_mode) &&
|
||||
@@ -293,7 +294,7 @@ int ftruncate(int fd, off_t length) {
|
||||
|
||||
int fstat(int fd, struct stat *out) {
|
||||
int entry_errno = errno;
|
||||
if (next_fstat == NULL) next_fstat = dlsym(RTLD_NEXT, "fstat");
|
||||
if (next_fstat == NULL) next_fstat = bl_next("fstat");
|
||||
if (next_fstat == NULL) { errno = ENOSYS; return -1; }
|
||||
initialize();
|
||||
errno = entry_errno;
|
||||
@@ -314,7 +315,7 @@ int fstat(int fd, struct stat *out) {
|
||||
|
||||
int fsync(int fd) {
|
||||
int entry_errno = errno;
|
||||
if (next_fsync == NULL) next_fsync = dlsym(RTLD_NEXT, "fsync");
|
||||
if (next_fsync == NULL) next_fsync = bl_next("fsync");
|
||||
if (next_fsync == NULL) { errno = ENOSYS; return -1; }
|
||||
int selected = on_selected_device(fd, NULL);
|
||||
errno = entry_errno;
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
#define STANDIN_DIR "/tmp/droiddeck-tracefs"
|
||||
#define KGSL_TOTAL "/sys/class/kgsl/kgsl/page_alloc"
|
||||
|
||||
@@ -54,10 +56,10 @@ static const char gpu_mem_total_format[] =
|
||||
static struct tracefs_instance *standin;
|
||||
|
||||
struct tracefs_instance *tracefs_instance_create(const char *name) {
|
||||
struct tracefs_instance *(*real)(const char *) = dlsym(RTLD_NEXT, "tracefs_instance_create");
|
||||
struct tracefs_instance *(*real)(const char *) = bl_next("tracefs_instance_create");
|
||||
struct tracefs_instance *instance = real ? real(name) : NULL;
|
||||
if (instance || !name) return instance;
|
||||
struct tracefs_instance *(*alloc)(const char *, const char *) = dlsym(RTLD_NEXT, "tracefs_instance_alloc");
|
||||
struct tracefs_instance *(*alloc)(const char *, const char *) = bl_next("tracefs_instance_alloc");
|
||||
if (!alloc) return NULL;
|
||||
char path[PATH_MAX];
|
||||
mkdir(STANDIN_DIR, 0700);
|
||||
@@ -69,7 +71,7 @@ struct tracefs_instance *tracefs_instance_create(const char *name) {
|
||||
|
||||
struct tep_handle *tracefs_local_events(const char *tracing_dir) {
|
||||
if (!tracing_dir || strncmp(tracing_dir, STANDIN_DIR "/", sizeof(STANDIN_DIR))) {
|
||||
struct tep_handle *(*real)(const char *) = dlsym(RTLD_NEXT, "tracefs_local_events");
|
||||
struct tep_handle *(*real)(const char *) = bl_next("tracefs_local_events");
|
||||
return real ? real(tracing_dir) : NULL;
|
||||
}
|
||||
struct tep_handle *(*tep_alloc)(void) = dlsym(RTLD_DEFAULT, "tep_alloc");
|
||||
@@ -84,7 +86,7 @@ int tracefs_iterate_raw_events(struct tep_handle *tep, struct tracefs_instance *
|
||||
if (!standin || instance != standin) {
|
||||
int (*real)(struct tep_handle *, struct tracefs_instance *, void *, int,
|
||||
int (*)(struct tep_event *, struct tep_record *, int, void *), void *) =
|
||||
dlsym(RTLD_NEXT, "tracefs_iterate_raw_events");
|
||||
bl_next("tracefs_iterate_raw_events");
|
||||
return real ? real(tep, instance, cpus, cpu_size, callback, context) : -1;
|
||||
}
|
||||
struct tep_event *(*find)(struct tep_handle *, int) = dlsym(RTLD_DEFAULT, "tep_find_event");
|
||||
|
||||
@@ -34,14 +34,26 @@ static const char *skip_digits(const char *s) {
|
||||
return end == s ? NULL : end;
|
||||
}
|
||||
|
||||
/* /proc/{self,thread-self,<pid>}[/task/<tid>]/status */
|
||||
static int is_own(const char *digits, const char *end) {
|
||||
long id = 0;
|
||||
for (; digits < end; digits++) {
|
||||
if (id > 99999999) return 0;
|
||||
id = id * 10 + (*digits - '0');
|
||||
}
|
||||
return id == (long)getpid() || id == syscall(SYS_gettid);
|
||||
}
|
||||
|
||||
/* /proc/{self,thread-self,<own pid or tid>}[/task/<tid>]/status. A program asks whether it is
|
||||
* traced about itself; another process's status is read as it is. Chromium opens each renderer's
|
||||
* and hands it over as a read-only file, which it checks: a memfd is open for writing, so Chromium
|
||||
* and every Electron program aborted as its first page connected. */
|
||||
static int is_status_path(const char *path) {
|
||||
const char *rest;
|
||||
if (path == NULL || strncmp(path, "/proc/", 6) != 0) return 0;
|
||||
path += 6;
|
||||
if (strncmp(path, "self/", 5) == 0) rest = path + 4;
|
||||
else if (strncmp(path, "thread-self/", 12) == 0) rest = path + 11;
|
||||
else if ((rest = skip_digits(path)) == NULL) return 0;
|
||||
else if ((rest = skip_digits(path)) == NULL || !is_own(path, rest)) return 0;
|
||||
if (strncmp(rest, "/task/", 6) == 0 && (rest = skip_digits(rest + 6)) == NULL) return 0;
|
||||
return strcmp(rest, "/status") == 0;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
struct stand_in {
|
||||
int fd; /* the descriptor handed out, -1 when the slot is free */
|
||||
int peer; /* the other end, kept open */
|
||||
@@ -131,7 +133,7 @@ int socket(int domain, int type, int protocol) {
|
||||
static int (*real_socket)(int, int, int);
|
||||
int fd;
|
||||
|
||||
if (real_socket == NULL) real_socket = dlsym(RTLD_NEXT, "socket");
|
||||
if (real_socket == NULL) real_socket = bl_next("socket");
|
||||
if (real_socket == NULL) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
@@ -168,7 +170,7 @@ int getsockname(int fd, struct sockaddr *addr, socklen_t *len) {
|
||||
*len = sizeof(nl);
|
||||
return 0;
|
||||
}
|
||||
if (real_getsockname == NULL) real_getsockname = dlsym(RTLD_NEXT, "getsockname");
|
||||
if (real_getsockname == NULL) real_getsockname = bl_next("getsockname");
|
||||
if (real_getsockname == NULL) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
@@ -187,7 +189,7 @@ int setsockopt(int fd, int level, int optname, const void *optval, socklen_t opt
|
||||
if (level == SOL_SOCKET && (optname == SO_ATTACH_FILTER || optname == SO_DETACH_FILTER))
|
||||
return 0;
|
||||
}
|
||||
if (real_setsockopt == NULL) real_setsockopt = dlsym(RTLD_NEXT, "setsockopt");
|
||||
if (real_setsockopt == NULL) real_setsockopt = bl_next("setsockopt");
|
||||
if (real_setsockopt == NULL) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
* session, and any other property, is passed straight on.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
|
||||
|
||||
/* Xlib's types, without needing its headers in the build image. */
|
||||
typedef struct _XDisplay Display;
|
||||
typedef unsigned long Atom;
|
||||
@@ -41,13 +42,13 @@ static int following(void) {
|
||||
int XChangeProperty(Display *dpy, Window w, Atom property, Atom type, int format, int mode,
|
||||
const unsigned char *data, int nelements) {
|
||||
static change_property_fn real;
|
||||
if (!real) real = (change_property_fn)dlsym(RTLD_NEXT, "XChangeProperty");
|
||||
if (!real) real = (change_property_fn)bl_next("XChangeProperty");
|
||||
if (!real) return 0;
|
||||
if (following() && format == 32 && nelements == 4 && data) {
|
||||
static intern_atom_fn intern;
|
||||
static Display *atom_dpy;
|
||||
static Atom mode_control;
|
||||
if (!intern) intern = (intern_atom_fn)dlsym(RTLD_NEXT, "XInternAtom");
|
||||
if (!intern) intern = (intern_atom_fn)bl_next("XInternAtom");
|
||||
/* Atoms belong to a connection; the client's is the one that writes this. */
|
||||
if (intern && atom_dpy != dpy) {
|
||||
mode_control = intern(dpy, "GAMESCOPE_XWAYLAND_MODE_CONTROL", 1);
|
||||
|
||||
@@ -61,7 +61,7 @@ class BinfmtPreloadTest(unittest.TestCase):
|
||||
cls.library = cls.tmp / "libbinfmt.so"
|
||||
subprocess.run(["gcc", "-shared", "-fPIC", "-O2", "-Wall", "-Wextra", "-Werror", "-DBINFMT_TEST",
|
||||
'-DBINFMT_HANDLER="%s"' % cls.handler, "-o", str(cls.library),
|
||||
"-pthread"] + [str(PRELOAD / name) for name in ("binfmt.c", "net.c", "udevmon.c", "pathcache.c")] + ["-ldl"],
|
||||
"-pthread"] + [str(PRELOAD / name) for name in ("binfmt.c", "net.c", "udevmon.c", "pathcache.c", "next.c")] + ["-ldl"],
|
||||
check=True)
|
||||
source = cls.tmp / "program.c"
|
||||
source.write_text(PROGRAM)
|
||||
|
||||
@@ -16,7 +16,7 @@ class CdpGuardTest(unittest.TestCase):
|
||||
def setUpClass(cls):
|
||||
cls.build = tempfile.TemporaryDirectory()
|
||||
cls.library = Path(cls.build.name) / "net.so"
|
||||
subprocess.run(["cc", "-shared", "-fPIC", "-O2", str(SOURCE), str(SOURCE.parent / "binfmt.c"), "-x", "c", "-", "-ldl",
|
||||
subprocess.run(["cc", "-shared", "-fPIC", "-O2", str(SOURCE), str(SOURCE.parent / "binfmt.c"), str(SOURCE.parent / "next.c"), "-x", "c", "-", "-ldl",
|
||||
"-o", str(cls.library)],
|
||||
input=b"int bl_udevmon_stand_in(int fd) { return 0; }", check=True, capture_output=True)
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ class LaunchTest(FexTestCase):
|
||||
self.assertNotIn("FEX_ROOTFS", guest)
|
||||
self.assertEqual(guest["FEX_TSOENABLED"], "1")
|
||||
self.assertTrue(guest["FEX_APP_CONFIG_LOCATION"].endswith("/config/"))
|
||||
self.assertEqual((guest["FEX_SILENTLOG"], guest["FEX_OUTPUTLOG"]), ("0", "stderr"))
|
||||
self.assertEqual((guest["FEX_SILENTLOG"], guest["FEX_OUTPUTLOG"]), ("0", ""))
|
||||
guest = FEX["guest_environment"]({"portable": False}, {"LD_PRELOAD": str(arm), "FEX_PORTABLE": "1", "FEX_OUTPUTLOG": "/tmp/fex.log"})
|
||||
self.assertEqual(guest["FEX_OUTPUTLOG"], "/tmp/fex.log")
|
||||
self.assertNotIn("LD_PRELOAD", guest)
|
||||
|
||||
@@ -319,7 +319,7 @@ class FsyncPreloadTest(unittest.TestCase):
|
||||
source = base / "fsync-test.c"
|
||||
source.write_text(PROGRAM)
|
||||
subprocess.run(["cc", "-O2", "-Wall", "-fPIC", "-shared", "-pthread", "-o", str(cls.lib),
|
||||
str(PRELOAD / "fsync.c"), str(PRELOAD / "robust.c"), str(PRELOAD / "noexec.c"), "-ldl"], check=True)
|
||||
str(PRELOAD / "fsync.c"), str(PRELOAD / "robust.c"), str(PRELOAD / "noexec.c"), str(PRELOAD / "next.c"), "-ldl"], check=True)
|
||||
subprocess.run(["cc", "-O2", "-Wall", "-pthread", "-o", str(cls.exe), str(source)], check=True)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload"
|
||||
|
||||
PROBE = r"""
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
|
||||
void *bl_next(const char *name) __attribute__((visibility("hidden")));
|
||||
void *__real_dlsym(void *handle, const char *name);
|
||||
|
||||
/* Lookups the walk left to dlsym. */
|
||||
static int walking;
|
||||
static long passed_on;
|
||||
|
||||
void *__wrap_dlsym(void *handle, const char *name) {
|
||||
passed_on += walking;
|
||||
return __real_dlsym(handle, name);
|
||||
}
|
||||
|
||||
void *probe_walk(const char *name) {
|
||||
walking = 1;
|
||||
void *found = bl_next(name);
|
||||
walking = 0;
|
||||
return found;
|
||||
}
|
||||
|
||||
void *probe_dlsym(const char *name) { return dlsym(RTLD_NEXT, name); }
|
||||
long probe_passed_on(void) { return passed_on; }
|
||||
"""
|
||||
|
||||
# Counts the allocations a thread makes, as an allocator that is still starting up would see them.
|
||||
COUNT = r"""
|
||||
#include <stddef.h>
|
||||
|
||||
extern void *__libc_malloc(size_t);
|
||||
extern void *__libc_calloc(size_t, size_t);
|
||||
extern void *__libc_realloc(void *, size_t);
|
||||
static __thread long made;
|
||||
|
||||
void *malloc(size_t size) { made++; return __libc_malloc(size); }
|
||||
void *calloc(size_t count, size_t size) { made++; return __libc_calloc(count, size); }
|
||||
void *realloc(void *old, size_t size) { made++; return __libc_realloc(old, size); }
|
||||
void count_reset(void) { made = 0; }
|
||||
long count_get(void) { return made; }
|
||||
"""
|
||||
|
||||
PROGRAM = r"""
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
|
||||
void *probe_walk(const char *name);
|
||||
void *probe_dlsym(const char *name);
|
||||
long probe_passed_on(void);
|
||||
void count_reset(void);
|
||||
long count_get(void);
|
||||
|
||||
static void *first_lookup(void *made) {
|
||||
long before = probe_passed_on();
|
||||
count_reset();
|
||||
void *found = probe_walk("openat");
|
||||
((long *)made)[0] = found ? count_get() : -1;
|
||||
((long *)made)[1] = probe_passed_on() - before;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int differ = 0;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
void *walked = probe_walk(argv[i]), *found = probe_dlsym(argv[i]);
|
||||
if (walked != found) {
|
||||
printf("%s: %p, dlsym %p\n", argv[i], walked, found);
|
||||
differ = 1;
|
||||
}
|
||||
}
|
||||
long made[2] = {0, 0};
|
||||
pthread_t thread;
|
||||
pthread_create(&thread, NULL, first_lookup, made);
|
||||
pthread_join(thread, NULL);
|
||||
printf("allocations %ld, left to dlsym %ld\n", made[0], made[1]);
|
||||
return differ;
|
||||
}
|
||||
"""
|
||||
|
||||
NAMES = ["open", "openat", "read", "recvmsg", "syscall", "close_range", "fstatat", "stat", "pthread_create",
|
||||
"dlopen", "malloc", "calloc", "memcpy", "strlen", "environ", "no_such_symbol"]
|
||||
|
||||
|
||||
@unittest.skipUnless(sys.platform.startswith("linux") and shutil.which("cc"), "needs Linux and a C compiler")
|
||||
class NextTest(unittest.TestCase):
|
||||
"""bl_next built as on glibc before 2.34, where the walk replaces dlsym(RTLD_NEXT)."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.build = tempfile.TemporaryDirectory()
|
||||
build = Path(cls.build.name)
|
||||
(build / "probe.c").write_text(PROBE)
|
||||
(build / "count.c").write_text(COUNT)
|
||||
(build / "program.c").write_text(PROGRAM)
|
||||
cls.count = build / "libcount.so"
|
||||
cls.program = build / "program"
|
||||
subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-DNEXT_TEST", "-Wl,--wrap=dlsym", "-o", str(build / "libprobe.so"),
|
||||
str(build / "probe.c"), str(PRELOAD / "next.c"), "-ldl"], check=True)
|
||||
subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-o", str(cls.count), str(build / "count.c")], check=True)
|
||||
subprocess.run(["cc", "-O2", "-pthread", "-o", str(cls.program), str(build / "program.c"),
|
||||
str(build / "libprobe.so"), str(cls.count), "-Wl,-rpath," + str(build)], check=True)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.build.cleanup()
|
||||
|
||||
def run_program(self):
|
||||
return subprocess.run([str(self.program)] + NAMES, capture_output=True, text=True)
|
||||
|
||||
def test_finds_what_dlsym_finds(self):
|
||||
result = self.run_program()
|
||||
self.assertEqual(0, result.returncode, result.stdout + result.stderr)
|
||||
|
||||
def test_a_threads_first_lookup_walks_and_allocates_nothing(self):
|
||||
self.assertIn("allocations 0, left to dlsym 0\n", self.run_program().stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -43,7 +43,7 @@ class NoexecPreloadTest(unittest.TestCase):
|
||||
cls.tmp = Path(tempfile.mkdtemp())
|
||||
cls.lib = cls.tmp / "libnoexec.so"
|
||||
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-fPIC", "-shared", "-pthread", "-DNOEXEC_TEST", "-o", str(cls.lib),
|
||||
str(PRELOAD / "noexec.c"), str(PRELOAD / "fsync.c"), str(PRELOAD / "robust.c"), "-ldl"], check=True)
|
||||
str(PRELOAD / "noexec.c"), str(PRELOAD / "fsync.c"), str(PRELOAD / "robust.c"), str(PRELOAD / "next.c"), "-ldl"], check=True)
|
||||
source = cls.tmp / "program.c"
|
||||
source.write_text(PROGRAM)
|
||||
for name in ("FEX", "game"):
|
||||
|
||||
@@ -45,7 +45,8 @@ cp "$TEST_CDN/${2##*/}" "$4"
|
||||
self.env = dict(os.environ, HOME=str(self.home), XDG_DATA_HOME=str(self.data),
|
||||
PATH=str(self.bin) + os.pathsep + os.environ['PATH'],
|
||||
TEST_CDN=str(self.cdn), TEST_REQUESTS=str(self.requests))
|
||||
for channel in ('steamdeck_publicbeta',):
|
||||
self.env.pop('BL_STEAM_CHANNEL', None)
|
||||
for channel in ('publicbeta', 'steamdeck_publicbeta'):
|
||||
archive = self.cdn / (channel + '.zip')
|
||||
with zipfile.ZipFile(archive, 'w') as package:
|
||||
package.writestr('steamrtarm64/steam', channel)
|
||||
@@ -57,11 +58,14 @@ cp "$TEST_CDN/${2##*/}" "$4"
|
||||
f'\t\t"file" "{archive.name}"\n\t\t"sha2" "{digest}"\n'
|
||||
'\t}\n}\n')
|
||||
|
||||
def install(self):
|
||||
return subprocess.run(['bash', str(SCRIPT)], env=self.env, text=True, capture_output=True)
|
||||
def install(self, channel=None):
|
||||
env = self.env.copy()
|
||||
if channel is not None:
|
||||
env['BL_STEAM_CHANNEL'] = channel
|
||||
return subprocess.run(['bash', str(SCRIPT)], env=env, text=True, capture_output=True)
|
||||
|
||||
def assert_installed(self, channel='steamdeck_publicbeta'):
|
||||
result = self.install()
|
||||
def assert_installed(self, channel):
|
||||
result = self.install(channel)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(channel + '\n', (self.steam / 'package/beta').read_text())
|
||||
self.assertEqual('123456\n', (self.steam / 'package/droiddeck-installed').read_text())
|
||||
@@ -74,31 +78,38 @@ cp "$TEST_CDN/${2##*/}" "$4"
|
||||
'https://client-update.fastly.steamstatic.com/' + channel + '.zip',
|
||||
], self.requests.read_text().splitlines())
|
||||
|
||||
def test_downloads_the_deck_beta_client(self):
|
||||
self.assert_installed()
|
||||
|
||||
def test_an_install_from_another_branch_is_left_for_steam_to_move(self):
|
||||
# An older app could install publicbeta; droiddeck-session moves the client at its start.
|
||||
self.assert_installed()
|
||||
requests = self.requests.read_text()
|
||||
(self.steam / 'package/beta').write_text('publicbeta\n')
|
||||
def test_unset_channel_downloads_deck_beta(self):
|
||||
result = self.install()
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(requests, self.requests.read_text())
|
||||
self.assertEqual('publicbeta\n', (self.steam / 'package/beta').read_text())
|
||||
self.assertEqual('steamdeck_publicbeta\n', (self.steam / 'package/beta').read_text())
|
||||
self.assertIn('/steam_client_steamdeck_publicbeta_linuxarm64\n', self.requests.read_text())
|
||||
|
||||
def test_failed_download_has_no_ready_marker_and_can_retry(self):
|
||||
archive = self.cdn / 'steamdeck_publicbeta.zip'
|
||||
original = archive.read_bytes()
|
||||
archive.write_bytes(b'corrupt download')
|
||||
self.assertNotEqual(0, self.install().returncode)
|
||||
self.assertFalse((self.steam / 'package/droiddeck-installed').exists())
|
||||
archive.write_bytes(original)
|
||||
self.assertEqual(0, self.install().returncode)
|
||||
def test_explicit_deck_beta_downloads_matching_client(self):
|
||||
self.assert_installed('steamdeck_publicbeta')
|
||||
|
||||
def test_explicit_public_beta_downloads_matching_client(self):
|
||||
self.assert_installed('publicbeta')
|
||||
|
||||
def test_existing_install_is_left_for_steam_to_update(self):
|
||||
self.assert_installed('steamdeck_publicbeta')
|
||||
requests = self.requests.read_text()
|
||||
result = self.install('publicbeta')
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertEqual(requests, self.requests.read_text())
|
||||
self.assertEqual('steamdeck_publicbeta\n', (self.steam / 'package/beta').read_text())
|
||||
|
||||
def test_failed_download_has_no_ready_marker_and_can_retry(self):
|
||||
archive = self.cdn / 'publicbeta.zip'
|
||||
original = archive.read_bytes()
|
||||
archive.write_bytes(b'corrupt download')
|
||||
self.assertNotEqual(0, self.install('publicbeta').returncode)
|
||||
self.assertFalse((self.steam / 'package/droiddeck-installed').exists())
|
||||
archive.write_bytes(original)
|
||||
self.assertEqual(0, self.install('publicbeta').returncode)
|
||||
self.assertEqual('publicbeta\n', (self.steam / 'package/beta').read_text())
|
||||
|
||||
def break_client(self, starts):
|
||||
self.assert_installed()
|
||||
self.assert_installed('steamdeck_publicbeta')
|
||||
self.requests.unlink()
|
||||
package = self.steam / 'package'
|
||||
(self.steam / 'steamrtarm64/steam').write_text('half updated')
|
||||
@@ -114,7 +125,7 @@ cp "$TEST_CDN/${2##*/}" "$4"
|
||||
|
||||
def test_two_starts_with_nothing_shown_fetch_the_client_again(self):
|
||||
package = self.break_client(2)
|
||||
self.assert_installed()
|
||||
self.assert_installed('steamdeck_publicbeta')
|
||||
self.assertEqual('repaired\n', (package / 'droiddeck-unconfirmed').read_text())
|
||||
self.assertFalse((self.steam / '.crash').exists())
|
||||
self.assertEqual([], sorted(p.name for p in package.glob('steam_client_*')))
|
||||
@@ -150,12 +161,18 @@ cp "$TEST_CDN/${2##*/}" "$4"
|
||||
self.assertFalse((package / 'droiddeck-installed').exists())
|
||||
archive.write_bytes(original)
|
||||
self.requests.unlink()
|
||||
self.assert_installed()
|
||||
self.assert_installed('steamdeck_publicbeta')
|
||||
self.assertFalse(request.exists())
|
||||
self.requests.unlink()
|
||||
self.assertEqual(0, self.install().returncode)
|
||||
self.assertFalse(self.requests.exists())
|
||||
|
||||
def test_invalid_channel_fails_before_any_download(self):
|
||||
result = self.install('../unknown')
|
||||
self.assertNotEqual(0, result.returncode)
|
||||
self.assertIn('unsupported Steam channel', result.stderr)
|
||||
self.assertFalse(self.requests.exists())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -81,7 +81,7 @@ class StoragePreloadTest(unittest.TestCase):
|
||||
(build / "program.c").write_text(PROGRAM)
|
||||
cls.storage = build / "storage.so"
|
||||
cls.fake = build / "fake.so"
|
||||
subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-o", str(cls.storage), str(PRELOAD / "storage.c"), "-ldl"], check=True)
|
||||
subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-o", str(cls.storage), str(PRELOAD / "storage.c"), str(PRELOAD / "next.c"), "-ldl"], check=True)
|
||||
subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-o", str(cls.fake), str(build / "fake.c")], check=True)
|
||||
cls.steam = cls.dir / "steamrtarm64/steam"
|
||||
cls.steam.parent.mkdir()
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload"
|
||||
|
||||
# Each path opened read-only through the preload, from a thread other than the main one: how it was
|
||||
# served, and its TracerPid line.
|
||||
PROGRAM = r"""
|
||||
import fcntl, json, os, sys, threading
|
||||
seen = {}
|
||||
def open_all():
|
||||
for path in sys.argv[1:]:
|
||||
path = path.replace("PID", str(os.getpid())).replace("TID", str(threading.get_native_id()))
|
||||
fd = os.open(path, os.O_RDONLY)
|
||||
try:
|
||||
text = os.read(fd, 65536).decode()
|
||||
seen[path] = {"read_only": fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE == os.O_RDONLY,
|
||||
"file": os.readlink("/proc/self/fd/%d" % fd),
|
||||
"tracer": next(line for line in text.splitlines() if line.startswith("TracerPid:"))}
|
||||
finally:
|
||||
os.close(fd)
|
||||
thread = threading.Thread(target=open_all)
|
||||
thread.start()
|
||||
thread.join()
|
||||
print(json.dumps(seen))
|
||||
"""
|
||||
|
||||
|
||||
@unittest.skipUnless(sys.platform.startswith("linux") and shutil.which("cc"), "needs Linux and a C compiler")
|
||||
class TracerTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.build = tempfile.TemporaryDirectory()
|
||||
cls.library = Path(cls.build.name) / "libblsession.so"
|
||||
subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-pthread", "-o", str(cls.library),
|
||||
*map(str, sorted(PRELOAD.glob("*.c"))), "-ldl"], check=True)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.build.cleanup()
|
||||
|
||||
def opened(self, *paths):
|
||||
result = subprocess.run([sys.executable, "-c", PROGRAM, *paths], env=dict(os.environ, LD_PRELOAD=str(self.library)),
|
||||
check=True, capture_output=True, text=True)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
def test_a_process_reads_its_own_status_without_the_tracer(self):
|
||||
for path, seen in self.opened("/proc/self/status", "/proc/thread-self/status", "/proc/PID/status",
|
||||
"/proc/TID/status", "/proc/PID/task/TID/status").items():
|
||||
self.assertEqual("TracerPid:\t0", seen["tracer"], path)
|
||||
self.assertTrue(seen["file"].startswith("/memfd:status"), path)
|
||||
|
||||
def test_another_process_status_is_the_real_read_only_file(self):
|
||||
# Chromium hands a renderer its status as a read-only file and checks that it is one.
|
||||
path = "/proc/%d/status" % os.getpid()
|
||||
seen = self.opened(path)[path]
|
||||
self.assertTrue(seen["read_only"])
|
||||
self.assertEqual(path, seen["file"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user