Compare commits

..
Author SHA1 Message Date
maxjivi05 8a6877423e AppImage apps: Electron programs such as Heroic open under FEX
Every Electron AppImage hung without a window or crashed at its first page.
Four causes, each enough on its own:

- The session preload's hooks found the real functions with
  dlsym(RTLD_NEXT). Before glibc 2.34 a thread's first dlsym callocs, and
  Chromium's PartitionAlloc calls syscall() from inside its first malloc:
  the allocator waited for itself. On older glibc (the x86 runtime's 2.31)
  next.c now walks the libraries after the preload with no allocation and
  picks each function's default version, as glibc 2.36 does; the old
  lookup handed realpath() and posix_spawn() their compat versions, and
  realpath(path, NULL) failed. arm64 keeps dlsym.
- libpci's sysfs backend exits when /sys/bus/pci/devices is listable but a
  device's files are not (a RedMagic's PCIe root port), taking every
  Chromium GPU process with it. An empty directory is bound over it on
  devices where that happens.
- FEX_OUTPUTLOG=stderr made FEX log to its own duplicate of descriptor 2.
  Chromium dup2()s its IPC socket onto that low number in each child it
  starts, then lists /proc/self/fd, which FEX logs into the socket. Empty,
  FEX writes to descriptor 2 itself.
- The tracer preload served every /proc/<pid>/status as a memfd, which is
  open for writing. Chromium hands each renderer its status as a read-only
  file and checks that it is one. Only a process's own status (pid, tid,
  self, thread-self) is rewritten now.
2026-10-11 00:17:55 -04:00
57 changed files with 787 additions and 1220 deletions

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)
+8 -3
View File
@@ -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>
+8 -3
View File
@@ -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>
+8 -3
View File
@@ -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>
+8 -3
View File
@@ -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>
+8 -3
View File
@@ -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>
+8 -3
View File
@@ -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>
+8 -3
View File
@@ -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>
+8 -3
View File
@@ -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>
+9 -4
View File
@@ -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
+6 -5
View File
@@ -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);
+3 -2
View File
@@ -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);
+3 -2
View File
@@ -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;
}
+14 -13
View File
@@ -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);
+16 -16
View File
@@ -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);
}
+5 -4
View File
@@ -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);
+4 -3
View File
@@ -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;
+10 -9
View File
@@ -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 -12
View File
@@ -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);
}
+110
View File
@@ -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
+3 -1
View File
@@ -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) {
+8 -7
View File
@@ -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);
+4 -3
View File
@@ -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;
+3 -2
View File
@@ -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;
}
+3 -2
View File
@@ -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)) {
+3 -2
View File
@@ -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)); \
+11 -10
View File
@@ -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;
+6 -4
View File
@@ -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");
+14 -2
View File
@@ -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;
}
+5 -3
View File
@@ -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;
+4 -3
View File
@@ -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);
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+129
View File
@@ -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()
+1 -1
View File
@@ -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"):
+43 -26
View File
@@ -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()
+1 -1
View File
@@ -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()
+68
View File
@@ -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()