Performance: idle front end, proot stops, launch-time passes and log collection (#529)

* Front end: draw the capsule wall without clipping it every frame

The tilted wall clipped each capsule to a rounded rect and recomposed its columns on every drift
step, and a rotated rounded clip is rasterised in software on the render threads: the idle main
page held about 1.7 cores. The art is now loaded at the capsule's size with its corners already
rounded, each column draws its capsules itself in one layer moved by the drift, and the drift
advances at most every 15 ms. The idle page takes about a third of a core.

* libfakeinput: let calls on descriptors it does not serve skip its lock

Every read, write, ioctl, close and poll of every process in a Proton session went through the
controller lock and the three map lookups, Wine's wineserver pipes and the game's files alike.
A bitmap of the descriptors the maps hold, set before one is handed out and cleared when it is
closed, lets everything else go straight to the kernel.

* Pad motion: ask the display for its rotation four times a second

The rotation was looked up for every sensor reading, every 2 ms, and cost more than the rest of
the reading; it is now refreshed every 250 ms, and the readings no longer allocate pairs.

* Session: keep the library passes out of a game's launch

The redist seeder stamped each prefix with the checksum of the whole set of names, which differs
between the session's pass and the game launch's (the game's own names are added), so every
launch re-appended the markers to every prefix in the library while the game loaded. Each
prefix's marker now lists the names it was given and only the missing ones are added.

The beat's seed and compatibility passes, and the Lossless Scaling ownership probe (which loads
steamclient.so), also wait while a wineserver runs: a launch rewrites the manifest and made them
due at exactly that moment. The game's own launch seeds its prefix regardless.

* Agent: keep the focus watcher's X connection open while nothing changes

The connect timeout stayed on the socket, so with no property change the read timed out, and the
watcher dropped the connection, reconnected and took a fresh snapshot every few seconds.

* Game environment: start Cyberpunk 2077 without REDlauncher

The prelauncher opens REDlauncher, a Qt WebEngine window that hands over to the game half a
minute later and then stays up beside it, well over a core busy for as long as the game runs; its
own --launcher-skip started the game twice. The game's executable is started directly instead.

* netif: answer Wine's network driver from a reading of the link

Mortal Kombat X has Wine's network driver enumerate every interface, address and route table
hundreds of times a second. Each enumeration read /etc/droiddeck-net several times, asked the
kernel for its interface dump again (a netlink socket bound under proot, refused each time) and
looked the real functions up with dlsym. The link file is now read at most once a second, a
refused dump is not asked for again within that second, and the real functions are looked up
once.

* Fast path: create memfds without a stop in proot

proot looks at a memfd's name only to refuse the few that are then executed or locked; every other
memfd_create is now issued from the trampoline. The session's /proc/net and status copies made
one per read, a stop each. Without memfd in the kernel the call still goes to proot's ashmem.

* Logs: run each redaction rule only on a line that could match it

A session's end copies some 70 MB of the Steam client's logs and puts every line through all
twenty-odd rules, most of a minute of a core on the phone after every session. Each rule now runs
only on a line holding what it cannot match without: its keyword, the five separators of a MAC,
the four dashes of a GUID, a long enough hex or token run, the learned account or address text.
Lines with non-ASCII text still meet every case-insensitive rule. Over 895,000 lines of real
client logs, with the device's accounts and addresses learned, the output is identical and the
pass takes a twentieth of the time.
This commit is contained in:
MaxsTechReview authored and GitHub committed 2026-10-10 19:00:33 -04:00
1 parent 96d7c66535
commit eadebc46d5
11 files changed
+416 -125

No files matched your search

+46 -3
View File
@@ -266,6 +266,31 @@ static std::unordered_map<int, struct ff_effect> &ff_effects() {
return *map;
}
// Every fd the maps hold, readable without controller_mutex(): the hooks see each read, write,
// ioctl and poll a process makes, and almost none are on these. Set before an fd is handed out,
// cleared once no map holds it; an fd past the bitmap takes the lock once any such fd exists.
static constexpr int kWatchedFds = 1 << 16;
static constexpr int kWatchedBits = sizeof(unsigned long) * 8;
static std::atomic<unsigned long> watched_fds[kWatchedFds / kWatchedBits];
static std::atomic<bool> watched_beyond{false};
static void watch_fd(int fd) {
if (fd < 0) return;
if (fd >= kWatchedFds) watched_beyond.store(true, std::memory_order_release);
else watched_fds[fd / kWatchedBits].fetch_or(1UL << (fd % kWatchedBits), std::memory_order_release);
}
static void unwatch_fd(int fd) {
if (fd >= 0 && fd < kWatchedFds)
watched_fds[fd / kWatchedBits].fetch_and(~(1UL << (fd % kWatchedBits)), std::memory_order_release);
}
static bool watched(int fd) {
if (fd < 0) return false;
if (fd >= kWatchedFds) return watched_beyond.load(std::memory_order_acquire);
return (watched_fds[fd / kWatchedBits].load(std::memory_order_acquire) >> (fd % kWatchedBits)) & 1;
}
static const char *fake_hook_dir() { return config().hook_dir; }
static const char *fake_udev_data_dir() { return config().udev_data_dir; }
static bool fake_vibration_enabled() { return config().vibration_enabled; }
@@ -692,6 +717,7 @@ open_fake_input_ring(const char *event, int flags) {
{
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
controller_map()[fd] = controller;
watch_fd(fd);
}
Logger::log("Adding ring-backed controller, fd %d event %s slot %d\n", fd,
@@ -774,11 +800,13 @@ copy_slot_ioctl_string(int op, void *argp, const char *format, int event_number)
}
__attribute__((visibility("hidden"))) static bool is_fake_input_fd(int fd) {
if (!watched(fd)) return false;
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
return controller_map().find(fd) != controller_map().end();
}
__attribute__((visibility("hidden"))) static bool fake_fd_is_stale(int fd) {
if (!watched(fd)) return false;
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
auto controller = controller_map().find(fd);
return controller != controller_map().end() &&
@@ -892,6 +920,7 @@ __attribute__((visibility("hidden"))) static int open_uinput(int flags) {
writer->owner = getpid();
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
uinput_map()[fd] = writer;
watch_fd(fd);
Logger::log("uinput: opened as fd %d\n", fd);
return fd;
}
@@ -1556,6 +1585,7 @@ __attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags) {
{
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
deck_map()[pair[0]] = deck;
watch_fd(pair[0]);
}
pthread_t thread;
auto *arg = new std::shared_ptr<DeckHidraw>(deck);
@@ -1563,6 +1593,7 @@ __attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags) {
delete arg;
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
deck_map().erase(pair[0]);
unwatch_fd(pair[0]);
syscall(SYS_close, pair[0]);
syscall(SYS_close, pair[1]);
munmap(mapping, mapping_size);
@@ -1898,6 +1929,7 @@ __attribute__((visibility("hidden"))) static int open_touch_hidraw(int flags) {
{
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
deck_map()[pair[0]] = touch;
watch_fd(pair[0]);
}
touch_state([](TouchRingFile &r) { r.opened++; });
pthread_t thread;
@@ -1906,6 +1938,7 @@ __attribute__((visibility("hidden"))) static int open_touch_hidraw(int flags) {
delete arg;
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
deck_map().erase(pair[0]);
unwatch_fd(pair[0]);
syscall(SYS_close, pair[0]);
syscall(SYS_close, pair[1]);
touch_state([](TouchRingFile &r) { if (r.opened) r.opened--; });
@@ -2689,6 +2722,7 @@ static int fake_stat_path(const char *pathname, S *statbuf, Real real) {
template <typename S, typename Real>
static int fake_stat_fd(int fd, S *buf, Real real) {
int ret = real(fd, buf);
if (ret != 0 || !watched(fd)) return ret;
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
if (ret == 0 && deck_map().count(fd)) {
@@ -2898,6 +2932,7 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) {
// The lock must not be held across the passthrough: binder's transport is a
// blocking ioctl(BINDER_WRITE_READ), so a parked binder pool thread would own
// controller_mutex() for as long as it waits and deadlock every other ioctl.
if (!watched(fd)) return syscall(SYS_ioctl, fd, op, argp);
std::unique_lock<std::recursive_mutex> guard(controller_mutex());
auto maker = uinput_map().find(fd);
if (maker != uinput_map().end()) return ioctl_uinput(*maker->second, op, argp);
@@ -3117,6 +3152,7 @@ EXPORT int ioctl(int fd, ioctl_request_t op, ...) {
EXPORT int close(int fd) {
static auto my_close = reinterpret_cast<decltype(&::close)>(dlsym(RTLD_NEXT, "close"));
if (!watched(fd)) return my_close(fd);
close_uinput(fd);
std::unique_lock<std::recursive_mutex> guard(controller_mutex());
deck_map().erase(fd);
@@ -3127,6 +3163,7 @@ EXPORT int close(int fd) {
controller->second->closed = true;
controller_map().erase(fd);
}
unwatch_fd(fd);
guard.unlock();
return my_close(fd);
@@ -3135,6 +3172,7 @@ EXPORT int close(int fd) {
static constexpr int kMaxRingWaitMs = 2;
EXPORT ssize_t read(int fd, void *buf, size_t count) {
if (!watched(fd)) return syscall(SYS_read, fd, buf, count);
std::unique_lock<std::recursive_mutex> guard(controller_mutex());
auto controller = controller_map().find(fd);
if (controller == controller_map().end()) {
@@ -3233,6 +3271,7 @@ EXPORT ssize_t read(int fd, void *buf, size_t count) {
EXPORT ssize_t write(int fd, const void *buf, size_t count) {
static auto my_write = reinterpret_cast<decltype(&::write)>(dlsym(RTLD_NEXT, "write"));
if (!watched(fd)) return my_write(fd, buf, count);
std::unique_lock<std::recursive_mutex> guard(controller_mutex());
auto made = uinput_map().find(fd);
if (made != uinput_map().end()) return write_uinput(*made->second, buf, count);
@@ -3262,6 +3301,7 @@ EXPORT ssize_t write(int fd, const void *buf, size_t count) {
}
EXPORT ssize_t writev(int fd, const struct iovec *iov, int iovcnt) {
if (!watched(fd)) return syscall(SYS_writev, fd, iov, iovcnt);
std::unique_lock<std::recursive_mutex> guard(controller_mutex());
if (deck_map().count(fd)) {
ssize_t total = 0;
@@ -3312,11 +3352,14 @@ static int poll_fake(struct pollfd *fds, nfds_t nfds, int timeout,
static auto my_poll = reinterpret_cast<decltype(&::poll)>(dlsym(RTLD_NEXT, "poll"));
static auto my_ppoll = reinterpret_cast<decltype(&::ppoll)>(dlsym(RTLD_NEXT, "ppoll"));
nfds_t first = 0;
while (first < nfds && !watched(fds[first].fd)) first++;
bool has_fake_fds = false;
std::vector<struct pollfd> real_fds;
real_fds.reserve(nfds);
std::vector<std::shared_ptr<FakeController>> fake_fds(nfds);
{
std::vector<std::shared_ptr<FakeController>> fake_fds;
if (first < nfds) {
real_fds.reserve(nfds);
fake_fds.resize(nfds);
std::lock_guard<std::recursive_mutex> guard(controller_mutex());
for (nfds_t i = 0; i < nfds; i++) {
real_fds.push_back(fds[i]);
@@ -93,9 +93,12 @@ object LogRedactor {
"(?i)(?<![A-Za-z0-9_])(login)(\\s*=\\s*|[\"']\\s*[=:]\\s*|[\"']\\s+)" + ACCOUNT_VALUE
)
/** A learned pattern and the text (lowercase) a line must hold for it to match; null when none can be said. */
private class Learned(val regex: Regex, val needs: String?)
/** The device's Steam accounts and persona names as patterns (see [learnAccounts]). */
@Volatile
private var accounts: List<Regex> = emptyList()
private var accounts: List<Learned> = emptyList()
/**
* Learns every Steam account on the device from the client's loginusers.vdf - its AccountName
@@ -103,12 +106,13 @@ object LogRedactor {
* Names under three characters are skipped: blanking every "a" would ruin a log.
*/
fun learnAccounts(loginUsers: java.io.File) {
val found = ArrayList<Regex>()
val found = ArrayList<Learned>()
try {
if (loginUsers.isFile) {
val kv = Regex("\"(AccountName|PersonaName)\"\\s+\"([^\"]*)\"")
kv.findAll(loginUsers.readText()).map { it.groupValues[2].trim() }.filter { it.length >= 3 }.distinct().forEach { name ->
found += Regex("(?i)(?<![A-Za-z0-9_])" + Regex.escape(name) + "(?![A-Za-z0-9_])")
val needs = name.lowercase().takeIf { name.all { it.code < 0x80 } }
found += Learned(Regex("(?i)(?<![A-Za-z0-9_])" + Regex.escape(name) + "(?![A-Za-z0-9_])"), needs)
}
}
} catch (e: Exception) {
@@ -125,7 +129,7 @@ object LogRedactor {
/** This device's public addresses as patterns (see [learnOwnAddresses]); empty until learned. */
@Volatile
private var own: List<Regex> = emptyList()
private var own: List<Learned> = emptyList()
/**
* Learns the device's addresses from the link file the app writes for the session
@@ -133,7 +137,7 @@ object LogRedactor {
* private, link-local and loopback addresses identify nobody and are left out.
*/
fun learnOwnAddresses(linkFile: java.io.File) {
val found = ArrayList<Regex>()
val found = ArrayList<Learned>()
try {
if (linkFile.isFile) linkFile.forEachLine { line ->
val addr = line.trim().takeIf { it.startsWith("addr ") }?.split(Regex("\\s+"))?.getOrNull(1) ?: return@forEachLine
@@ -142,9 +146,12 @@ object LogRedactor {
val groups = expand6(addr)?.take(4) ?: return@forEachLine
// Any spelling of an address in this /64: leading zeros dropped, :: anywhere after.
val prefix = groups.joinToString(":") { g -> "0{0,3}" + Regex.escape(g.trimStart('0').ifEmpty { "0" }) }
found += Regex("(?i)(?<![0-9A-Fa-f:])$prefix(?::[0-9A-Fa-f]{0,4}){1,4}(?:%[A-Za-z0-9_.]+)?")
found += Learned(
Regex("(?i)(?<![0-9A-Fa-f:])$prefix(?::[0-9A-Fa-f]{0,4}){1,4}(?:%[A-Za-z0-9_.]+)?"),
groups[0].trimStart('0').ifEmpty { "0" },
)
}
"public IPv4" -> found += Regex("(?<![0-9.])" + Regex.escape(addr) + "(?![0-9.])")
"public IPv4" -> found += Learned(Regex("(?<![0-9.])" + Regex.escape(addr) + "(?![0-9.])"), addr)
}
}
} catch (e: Exception) {
@@ -263,41 +270,87 @@ object LogRedactor {
return "$q<redacted:account>$q"
}
/**
* A line on its way through [redact]. Each rule runs only on a line holding what it cannot match
* without - a word, enough separators, a long enough run: most of a Steam log is lines no rule
* touches, and a session's logs went through every rule at about a megabyte a second. A line with anything but ASCII in it goes through
* every case-insensitive rule, since Android's regex folds case more widely than lowercase().
*/
private class Line(var text: String) {
private val unfolded = text.any { it.code >= 0x80 }
private var lower: String? = null
fun has(vararg words: String): Boolean {
if (unfolded) return true
val folded = lower ?: text.lowercase().also { lower = it }
return words.any { folded.contains(it) }
}
fun set(next: String) {
if (next != text) {
text = next
lower = null
}
}
fun holds(c: Char, times: Int): Boolean {
var n = 0
for (ch in text) if (ch == c && ++n >= times) return true
return false
}
inline fun hasRun(length: Int, inRun: (Char) -> Boolean): Boolean {
var n = 0
for (ch in text) if (!inRun(ch)) n = 0 else if (++n >= length) return true
return false
}
}
private fun isHex(c: Char) = c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F'
/** [line] with every credential shape replaced. Null- and exception-safe by construction. */
fun redact(line: String): String {
if (line.isEmpty()) return line
return try {
var out = line
out = MAC_FIELD.replace(out) { "${it.groupValues[1]}<redacted:mac>" }
out = MAC.replace(out, "<redacted:mac>")
out = DEVICE_IDENTIFIER.replace(out) {
val out = Line(line)
if (out.has("mac")) out.set(MAC_FIELD.replace(out.text) { "${it.groupValues[1]}<redacted:mac>" })
if (out.holds(':', 5) || out.holds('-', 5)) out.set(MAC.replace(out.text, "<redacted:mac>"))
if (out.has("serial", "android")) out.set(DEVICE_IDENTIFIER.replace(out.text) {
val value = it.groupValues[2]
val quote = value.first().takeIf { char -> char == '\"' || char == '\'' }?.toString().orEmpty()
"${it.groupValues[1]}${quote}<redacted:serial>${quote}"
})
if (out.holds('-', 4)) out.set(GUID.replace(out.text, "<redacted:guid>"))
if (out.has("jwt")) out.set(JWT_LABELLED.replace(out.text) { "${it.groupValues[1]}<redacted:jwt>" })
if ("ey" in out.text) out.set(JWT_BASE64.replace(out.text, "<redacted:jwt>"))
if (out.has("token", "auth", "ticket", "sessionid", "steamloginsecure", "api", "machine", "pass", "pwd", "secret")) {
out.set(SECRET_KV.replace(out.text) { "${it.groupValues[1]}${it.groupValues[2]}<redacted:token>" })
}
out = GUID.replace(out, "<redacted:guid>")
out = JWT_LABELLED.replace(out) { "${it.groupValues[1]}<redacted:jwt>" }
out = JWT_BASE64.replace(out, "<redacted:jwt>")
out = SECRET_KV.replace(out) { "${it.groupValues[1]}${it.groupValues[2]}<redacted:token>" }
out = GUARD_CODE.replace(out) { "${it.groupValues[1]}<redacted:code>" }
out = WEBAPI_KEY.replace(out, "<redacted:key>")
if (out.has("guard", "factor", "2fa")) out.set(GUARD_CODE.replace(out.text) { "${it.groupValues[1]}<redacted:code>" })
if (out.hasRun(32, ::isHex)) out.set(WEBAPI_KEY.replace(out.text, "<redacted:key>"))
// Mask, not delete: the last four digits let a reader correlate lines to one account.
out = STEAMID64.replace(out) { "${it.groupValues[1]}********${it.groupValues[3]}" }
out = STEAMID3.replace(out) { m ->
if ("76561" in out.text) out.set(STEAMID64.replace(out.text) { "${it.groupValues[1]}********${it.groupValues[3]}" })
if ("[U:1:" in out.text) out.set(STEAMID3.replace(out.text) { m ->
val id = m.groupValues[1]
"[U:1:${if (id.length > 4) "*".repeat(id.length - 4) + id.takeLast(4) else id}]"
})
if (out.has("external")) out.set(EXTERNAL_ADDR.replace(out.text) { "${it.groupValues[1]}<redacted:ip>" })
for (r in own) if (r.needs == null || out.has(r.needs)) out.set(r.regex.replace(out.text, "<redacted:ip>"))
if (out.has("onloginstatechange")) out.set(LOGIN_STATE.replace(out.text) { "${it.groupValues[1]}<redacted:account>" })
if ("OnLoginUsersChanged " in out.text) out.set(LOGIN_USERS.replace(out.text) { "${it.groupValues[1]}<redacted:account>" })
if (out.has("account", "user")) {
out.set(ACCOUNT_KV.replace(out.text) { "${it.groupValues[1]}${it.groupValues[2]}${quoted(it.groupValues[3])}" })
}
out = EXTERNAL_ADDR.replace(out) { "${it.groupValues[1]}<redacted:ip>" }
for (r in own) out = r.replace(out, "<redacted:ip>")
out = LOGIN_STATE.replace(out) { "${it.groupValues[1]}<redacted:account>" }
out = LOGIN_USERS.replace(out) { "${it.groupValues[1]}<redacted:account>" }
out = ACCOUNT_KV.replace(out) { "${it.groupValues[1]}${it.groupValues[2]}${quoted(it.groupValues[3])}" }
out = LOGIN_KV.replace(out) { "${it.groupValues[1]}${it.groupValues[2]}${quoted(it.groupValues[3])}" }
for (r in accounts) out = r.replace(out, "<redacted:account>")
out = EMAIL.replace(out, "<redacted:email>")
out = RESIDUAL.replace(out) { "${it.groupValues[1]}=<redacted:token>" }
out = LONG_TOKEN.replace(out, "<redacted:token>")
out
if (out.has("login")) out.set(LOGIN_KV.replace(out.text) { "${it.groupValues[1]}${it.groupValues[2]}${quoted(it.groupValues[3])}" })
for (r in accounts) if (r.needs == null || out.has(r.needs)) out.set(r.regex.replace(out.text, "<redacted:account>"))
if ('@' in out.text) out.set(EMAIL.replace(out.text, "<redacted:email>"))
if (out.has("jwt", "token", "ticket", "sessionid", "steamloginsecure", "machineauth")) {
out.set(RESIDUAL.replace(out.text) { "${it.groupValues[1]}=<redacted:token>" })
}
if (out.hasRun(88) { it in 'a'..'z' || it in 'A'..'Z' || it in '0'..'9' || it == '_' || it == '-' }) {
out.set(LONG_TOKEN.replace(out.text, "<redacted:token>"))
}
out.text
} catch (t: Throwable) {
// A log line is never worth crashing a session over, but an unscrubbed one must not
// reach the file either.
@@ -35,6 +35,9 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) :
private val gyro = ShortArray(3)
private var reading = false
private var idlePolls = 0
// Asking the display costs more than the rest of a reading, which arrives every 2 ms.
private var turn = Surface.ROTATION_0
private var turnAt = 0L
fun start() {
if (thread != null || sensors == null) return
@@ -86,6 +89,7 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) :
idlePolls = if (wanted) 0 else idlePolls + 1
if (wanted && !reading) {
reading = true
turnAt = 0L
gyroscope?.let { sensors?.registerListener(this, it, SAMPLING_US, handler) }
accelerometer?.let { sensors?.registerListener(this, it, SAMPLING_US, handler) }
Log.i(TAG, "pad motion: reading")
@@ -107,17 +111,25 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) :
override fun onSensorChanged(event: SensorEvent) {
if (!active) return
val values = event.values
if (event.timestamp - turnAt > ROTATION_REFRESH_NS) {
turn = rotation()
turnAt = event.timestamp
}
// Natural orientation to the screen's: Android's display-rotation transform.
val (x, y) = when (rotation()) {
Surface.ROTATION_90 -> -values[1] to values[0]
Surface.ROTATION_180 -> -values[0] to -values[1]
Surface.ROTATION_270 -> values[1] to -values[0]
else -> values[0] to values[1]
val x: Float
val y: Float
when (turn) {
Surface.ROTATION_90 -> { x = -values[1]; y = values[0] }
Surface.ROTATION_180 -> { x = -values[0]; y = -values[1] }
Surface.ROTATION_270 -> { x = values[1]; y = -values[0] }
else -> { x = values[0]; y = values[1] }
}
val z = values[2]
val (target, scale) = when (event.sensor.type) {
Sensor.TYPE_GYROSCOPE -> gyro to GYRO_COUNTS_PER_RAD_S
Sensor.TYPE_ACCELEROMETER -> accel to ACCEL_COUNTS_PER_M_S2
val target: ShortArray
val scale: Float
when (event.sensor.type) {
Sensor.TYPE_GYROSCOPE -> { target = gyro; scale = GYRO_COUNTS_PER_RAD_S }
Sensor.TYPE_ACCELEROMETER -> { target = accel; scale = ACCEL_COUNTS_PER_M_S2 }
else -> return
}
// Screen frame (right, up, towards the player) to the Deck's (right, away, up).
@@ -140,6 +152,7 @@ class PadMotion(private val context: Context, private val rotation: () -> Int) :
private const val SAMPLING_US = 4000
/** How often [follow] checks whether the client wants motion. */
private const val POLL_MS = 500L
private const val ROTATION_REFRESH_NS = 250_000_000L
private const val ACCEL_COUNTS_PER_M_S2 = 16384f / SensorManager.GRAVITY_EARTH
private const val GYRO_COUNTS_PER_RAD_S = (32768f / 2000f) * (180f / Math.PI.toFloat())
}
@@ -4,7 +4,6 @@ import androidx.compose.ui.platform.testTag
import com.droiddeck.launcher.R
import androidx.compose.ui.res.stringResource
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.runtime.State
import androidx.compose.animation.core.animateFloat
@@ -12,7 +11,6 @@ import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
@@ -41,21 +39,38 @@ import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.droiddeck.launcher.frontend.Library
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.inset
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import coil.size.Scale
import coil.transform.RoundedCornersTransformation
import kotlin.math.roundToInt
/** From this many games the Steam tab is a wall; fewer would only repeat the same covers. */
private const val WALL_MIN_GAMES = 4
private const val DRIFT_STEP_NS = 15_000_000L
/**
* The Steam tab: the library as a slowly drifting, tilted wall of capsules (a few games instead
* lean on their own; none, the DroidDeck mark wandering a blank wall), and over it the wordmark and the one thing to do here -
@@ -126,32 +141,49 @@ internal fun CapsuleWall(
games: List<Library.SteamGame>,
driftMs: Int = if (games.isEmpty()) 75_000 else 40_000,
modifier: Modifier = Modifier,
) = TiltedWall(games.size, driftMs, modifier) { i, m ->
Capsule(if (games.isEmpty()) null else games[i], RoundedCornerShape(10.dp), m)
) {
val pal = LocalPalette.current
val blank = remember(pal) { WallCapsule(Brush.linearGradient(listOf(pal.surface, pal.background)), edge = pal.line) }
TiltedWall(games.size, driftMs, modifier) { i -> if (games.isEmpty()) blank else WallCapsule(artBrush(hueOf(games[i].name)), games[i].art) }
}
/** One capsule of a [TiltedWall]: its ground, the art laid over it once loaded, and an outline. */
internal class WallCapsule(val ground: Brush, val art: Any? = null, val edge: Color? = null)
/**
* [CapsuleWall]'s tilted, drifting columns for any [count] of capsules; [capsule] draws number i
* (0 until [count], or 0 for each blank one when [count] is 0) at the modifier it is given.
* [CapsuleWall]'s tilted, drifting columns for any [count] of capsules; [capsule] describes number i
* (0 until [count], or 0 for each blank one when [count] is 0). Each column is one drawing with the
* covers' corners cut into their bitmaps: a rounded clip under the tilt is rasterised on the CPU,
* for every capsule on every frame.
*/
@Composable
internal fun TiltedWall(
count: Int,
driftMs: Int,
modifier: Modifier = Modifier,
capsule: @Composable (index: Int, modifier: Modifier) -> Unit,
capsule: (index: Int) -> WallCapsule,
) {
val capW = 112.dp
val capH = 168.dp
val gap = 14.dp
val transition = rememberInfiniteTransition(label = "wall")
val drift = transition.animateFloat(
0f, 1f,
infiniteRepeatable(tween(driftMs, easing = LinearEasing)),
label = "drift",
)
// Animations off in the system settings: a still wall.
val still = Motion.scale == 0f
// The wall moves a few pixels a step. Stepped on frames at most every 15 ms it looks the same as
// stepped on every frame, which on a 120 Hz screen drew it twice as often.
val drift = remember { mutableFloatStateOf(0f) }
if (!still) LaunchedEffect(driftMs) {
val period = driftMs * 1_000_000.0 * Motion.scale
var start = 0L
var shown = 0L
while (true) withFrameNanos { now ->
if (start == 0L) start = now
if (shown == 0L || now - shown >= DRIFT_STEP_NS) {
shown = now
drift.floatValue = ((now - start) % period / period).toFloat()
}
}
}
val density = LocalDensity.current
BoxWithConstraints(modifier.fillMaxSize()) {
// Centred on the page and big enough that, once tilted, it still covers every corner.
val tilt = Math.toRadians(13.0)
@@ -162,6 +194,17 @@ internal fun TiltedWall(
val columns = ((wallW + gap) / (capW + gap)).toInt() + 1
val perRun = ((wallH + gap) / (capH + gap)).toInt() + 1
val run = (capH + gap) * perRun
val cap = with(density) { Size(capW.toPx(), capH.toPx()) }
val step = with(density) { (capH + gap).toPx() }
val radius = with(density) { 10.dp.toPx() }
val line = with(density) { 1.dp.toPx() }
val indexOf = { c: Int, i: Int -> if (count == 0) 0 else (c * 5 + (i % perRun) * 3) % count }
val shown = remember(count, columns, perRun) { (0 until columns).flatMap { c -> (0 until perRun).map { indexOf(c, it) } }.distinct() }
val looks = HashMap<Int, Pair<WallCapsule, Painter?>>(shown.size * 2)
for (index in shown) key(index) {
val look = capsule(index)
looks[index] = look to look.art?.let { rememberCapsuleArt(it, cap, radius) }
}
Row(
horizontalArrangement = Arrangement.spacedBy(gap),
modifier = Modifier
@@ -171,26 +214,52 @@ internal fun TiltedWall(
.graphicsLayer { rotationZ = -13f },
) {
repeat(columns) { c ->
Column(
verticalArrangement = Arrangement.spacedBy(gap),
modifier = Modifier.wrapContentHeight(Alignment.Top, unbounded = true).graphicsLayer {
val shift = if (still) 0f else run.toPx() * drift.value
translationY = if (c % 2 == 0) -shift else shift - run.toPx()
},
) {
repeat(perRun * 2) { i ->
capsule(if (count == 0) 0 else (c * 5 + (i % perRun) * 3) % count, Modifier.size(capW, capH))
}
}
Spacer(
Modifier.width(capW).wrapContentHeight(Alignment.Top, unbounded = true).height(run * 2 - gap)
.graphicsLayer {
val shift = if (still) 0f else run.toPx() * drift.floatValue
translationY = if (c % 2 == 0) -shift else shift - run.toPx()
}
.drawBehind {
for (i in 0 until perRun * 2) {
val (look, art) = looks[indexOf(c, i)] ?: continue
val top = i * step
inset(0f, top, 0f, size.height - top - cap.height) { drawCapsule(look, art, radius, line) }
}
},
)
}
}
}
}
/** A cover sized for a capsule, its corners already cut, so it draws without a clip. */
@Composable
private fun rememberCapsuleArt(model: Any, size: Size, radius: Float): Painter {
val context = LocalContext.current
val w = size.width.roundToInt()
val h = size.height.roundToInt()
return rememberAsyncImagePainter(
remember(model, w, h, radius) {
ImageRequest.Builder(context).data(model).size(w, h).scale(Scale.FILL)
.transformations(RoundedCornersTransformation(radius)).build()
},
)
}
private fun DrawScope.drawCapsule(look: WallCapsule, art: Painter?, radius: Float, line: Float) {
drawRoundRect(look.ground, size = size, cornerRadius = CornerRadius(radius))
if (art != null) with(art) { draw(size) }
look.edge?.let {
drawRoundRect(it, Offset(line / 2, line / 2), Size(size.width - line, size.height - line), CornerRadius(radius - line / 2), style = Stroke(line))
}
}
/** One to three games, each a large capsule leaning on the others and floating a little. */
@Composable
private fun CapsuleFan(games: List<Library.SteamGame>) {
val pal = LocalPalette.current
val density = LocalDensity.current
val transition = rememberInfiniteTransition(label = "fan")
val floats = listOf(9_000, 11_000, 13_000).map { ms ->
transition.animateFloat(0f, 1f, infiniteRepeatable(tween(ms, easing = FastOutSlowInEasing), RepeatMode.Reverse), label = "float")
@@ -212,12 +281,14 @@ private fun CapsuleFan(games: List<Library.SteamGame>) {
)
},
) {
val radius = with(density) { 14.dp.toPx() }
for (i in games.indices.reversed()) {
val slot = slots[i]
val h = maxHeight * slot.height
val v: State<Float> = floats[i]
Capsule(
games[i], RoundedCornerShape(14.dp),
val look = WallCapsule(artBrush(hueOf(games[i].name)), games[i].art)
val art = look.art?.let { rememberCapsuleArt(it, with(density) { Size((h * 2f / 3f).toPx(), h.toPx()) }, radius) }
Spacer(
Modifier
.offset(x = maxWidth * slot.x, y = maxHeight * slot.y)
.size(h * 2f / 3f, h)
@@ -228,23 +299,11 @@ private fun CapsuleFan(games: List<Library.SteamGame>) {
shadowElevation = 24.dp.toPx()
shape = RoundedCornerShape(14.dp)
clip = false
},
}
.drawBehind { drawCapsule(look, art, radius, 0f) },
)
}
}
}
private class FanSlot(val x: Float, val y: Float, val height: Float, val lean: Float)
/** A game's portrait capsule, or a blank one standing in for a game yet to be installed. */
@Composable
private fun Capsule(g: Library.SteamGame?, shape: RoundedCornerShape, modifier: Modifier) {
val pal = LocalPalette.current
if (g == null) {
Box(modifier.clip(shape).background(Brush.linearGradient(listOf(pal.surface, pal.background))).border(1.dp, pal.line, shape))
return
}
Box(modifier.clip(shape).background(artBrush(hueOf(g.name)))) {
if (g.art != null) AsyncImage(model = g.art, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize())
}
}
@@ -36,6 +36,7 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.draw.blur
import androidx.compose.foundation.layout.wrapContentHeight
@@ -774,14 +775,12 @@ private fun SignInCard(store: Store, height: Dp) {
.takeIf { it.size >= 8 }.orEmpty()
}
val shape = RoundedCornerShape(20.dp)
val capsule = RoundedCornerShape(10.dp)
val blank = WallCapsule(Brush.linearGradient(listOf(c.fill, colors.background)), edge = c.dot.copy(alpha = 0.22f))
val ground = SolidColor(c.fill)
Box(Modifier.fillMaxWidth().height(height.coerceAtLeast(320.dp)).clip(shape).background(colors.background).border(1.dp, pal.line, shape)) {
// The storefront arrives a moment after the page: its wall fades in over the blank one.
androidx.compose.animation.Crossfade(art, animationSpec = Motion.tw(600), label = "signInWall") { tiles ->
TiltedWall(tiles.size, if (tiles.isEmpty()) 75_000 else 50_000) { i, m ->
if (tiles.isEmpty()) Box(m.clip(capsule).background(Brush.linearGradient(listOf(c.fill, colors.background))).border(1.dp, c.dot.copy(alpha = 0.22f), capsule))
else Box(m.clip(capsule).background(c.fill)) { AsyncImage(model = tiles[i], contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize()) }
}
TiltedWall(tiles.size, if (tiles.isEmpty()) 75_000 else 50_000) { i -> if (tiles.isEmpty()) blank else WallCapsule(ground, tiles[i]) }
}
// The wall gives way to the words: dark at the left and the foot, and the store's glow under them.
Box(
@@ -287,6 +287,9 @@ class FocusWatcher(threading.Thread):
x = X(os.environ["DISPLAY"])
wanted = {x.atom(name) for name in FOCUS_PROPS}
x.select(x.root, 0x400000) # PropertyChangeMask
# Waits for events as long as there are none: the connect timeout left in place
# here dropped and remade the connection, and took a snapshot, every few seconds.
x.s.settimeout(None)
self.changed.set()
while True:
event = x.next_event()
@@ -106,6 +106,19 @@ def godot_sentry(game):
return {"WINEDLLOVERRIDES": ";".join(n + "=d" for n in sentry)}, [], "Godot: Sentry crash handler off"
def skip_red_launcher(command):
"""Cyberpunk 2077's prelauncher opens REDlauncher, a Qt WebEngine window that hands over to the
game half a minute later and then stays up beside it, well over a core busy for as long as the
game runs. The game is started itself instead; the prelauncher's own --launcher-skip started it
twice."""
if len(command) > 2 and os.path.basename(command[2]).lower() == "redprelauncher.exe":
game = os.path.join(os.path.dirname(command[2]), "bin", "x64", "Cyberpunk2077.exe")
if os.path.isfile(game):
print("droiddeck-game-env: REDlauncher skipped", file=sys.stderr)
return command[:2] + [game] + command[3:]
return command
def engine_fixes(exe, args):
"""Environment and extra arguments a game needs because of what it is built with."""
game = Game(exe, args)
@@ -269,6 +282,7 @@ def main():
waiting = len(command) > 1 and command[1] == "waitforexitandrun"
fixes = {}
if waiting:
command = skip_red_launcher(command)
fixes, extra, notes = engine_fixes(command[2], command[3:]) if len(command) > 2 else ({}, [], [])
for note in notes:
print("droiddeck-game-env: " + note, file=sys.stderr)
@@ -108,12 +108,18 @@ vcrt() {
done
}
# The stamp is the set of names, taken before the block below dates its keys: stamped from the
# dated block it never matched, and every prefix had the whole block appended again on every beat.
stamp=$(printf '%s vcrt %s.%s.%s' "$pairs" "$VCRT_MAJOR" "$VCRT_MINOR" "$VCRT_BUILD" | cksum)
# Each prefix's marker lists the names it was given, so it is handed only what it lacks. One stamp
# for the whole set flipped between the session's set and the evaluator's (which adds the game's
# own names) on every launch, and every prefix was appended to again, inside the launch.
want=()
declare -A wanted=()
while IFS= read -r line; do
[[ $line == *$'\t'* ]] && [ -z "${wanted[$line]:-}" ] && wanted[$line]=1 && want+=("$line")
done <<<"$pairs"
want+=("vcrt"$'\t'"$VCRT_MAJOR.$VCRT_MINOR.$VCRT_BUILD")
printf -v now '%(%s)T' -1
block=$(printf '%s\n' "$pairs" | awk -F'\t' -v now="$now" '
block_for() { printf '%s\n' "$@" | awk -F'\t' -v now="$now" '
{
path = $1
sub(/^HKEY_LOCAL_MACHINE\\\\/, "", path)
@@ -127,9 +133,10 @@ block=$(printf '%s\n' "$pairs" | awk -F'\t' -v now="$now" '
next
printf "\n[%s] %d\n\"%s\"=dword:00000001\n", "Software\\\\Wow6432Node\\\\" substr(path, 11), now, $2
}
')
block="$block
$(vcrt "$now")"
'
local line
for line; do [[ $line == vcrt$'\t'* ]] && vcrt "$now"; done
}
# Proton builds a new prefix from its template, and the client runs the install scripts in that
# prefix moments later, so the template carries the markers too. Its date is kept: Proton reads a
@@ -147,13 +154,17 @@ for reg in "$steam_root"/steamapps/common/Proton*/files/share/default_pfx*/syste
if [ -f "$legacy" ] && [ ! -f "$seeded" ]; then
mv -f "$legacy" "$seeded"
fi
have=
[ -f "$seeded" ] && read -r have < "$seeded"
[ "$have" = "$stamp" ] && continue
declare -A had=()
if [ -f "$seeded" ]; then
while IFS= read -r line; do [[ $line == *$'\t'* ]] && had[$line]=1; done <"$seeded"
fi
missing=()
for line in "${want[@]}"; do [ -n "${had[$line]:-}" ] || missing+=("$line"); done
[ ${#missing[@]} -eq 0 ] && continue
dated=$(mktemp) || continue
touch -r "$reg" "$dated"
if printf '%s\n' "$block" >>"$reg"; then
printf '%s\n' "$stamp" >"$seeded"
if printf '%s\n' "$(block_for "${missing[@]}")" >>"$reg"; then
printf '%s\n' "${!had[@]}" "${missing[@]}" >"$seeded"
echo "== seeded redist markers into $reg"
fi
touch -r "$dated" "$reg"
@@ -642,11 +642,18 @@ case $mode in
beat=$((beat + 1))
[ "$((beat % 3))" -eq 0 ] || continue
libraries=("$steam_root/steamapps" /mnt/droiddeck/steamapps /mnt/droiddeck-sd/steamapps /root/Games/*/steamapps)
if seed_due; then
seed=; compat=
seed_due && seed=1
compat_due && compat=1
[ -n "$seed$compat" ] || continue
# Not beside a running game: the client rewrites its manifest at each launch, which made both
# due while it loaded. Its own launch seeds its prefix (droiddeck-game-env) regardless.
pgrep -x wineserver >/dev/null 2>&1 && continue
if [ -n "$seed" ]; then
: > "$seed_stamp"
droiddeck-seed-redists "$steam_root" >/dev/null 2>&1 || true
fi
if compat_due; then
if [ -n "$compat" ]; then
: > "$compat_stamp"
steam-compatibility "$steam_root" >/dev/null 2>&1 || true
fi
@@ -869,6 +876,8 @@ BUSCONF
for ((_i = 0; _i < 60; _i++)); do
nap 30
[ -e "$owned" ] && exit 0
# Loading steamclient.so under proot takes seconds of CPU: not while a game loads or runs.
pgrep -x wineserver >/dev/null 2>&1 && continue
answer=$(droiddeck-steam-games probe "$steam_root" 993090 2>/dev/null) || continue
case $answer in
*'"993090": true'*) mkdir -p "${owned%/*}" && : > "$owned" && echo "== Lossless Scaling: owned" ;;
+96 -22
View File
@@ -21,6 +21,7 @@
#include <net/if_arp.h>
#include <netpacket/packet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
@@ -28,6 +29,7 @@
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#define LINK_FILE "/etc/droiddeck-net"
@@ -35,6 +37,7 @@
#define LO_INDEX 1
#define LO_MTU 65536
#define MAX_ADDRS 16
#define FRESH_NS 1000000000LL
struct link_addr {
int family;
@@ -61,10 +64,11 @@ static int parse_addr(const char *text, struct link_addr *out) {
return 1;
}
/* Read on every call: the app rewrites the file when the device changes network. */
static int load_link(struct link *link) {
typedef FILE *(*fopen_fn)(const char *, const char *);
fopen_fn real_fopen = (fopen_fn) dlsym(RTLD_NEXT, "fopen");
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");
FILE *f = real_fopen ? real_fopen(LINK_FILE, "re") : NULL;
if (f == NULL) return 0;
@@ -93,6 +97,50 @@ static int load_link(struct link *link) {
return link->name[0] != '\0' && link->index > LO_INDEX && link->addr_count > 0;
}
static long long now_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC_COARSE, &ts);
return ts.tv_sec * 1000000000LL + ts.tv_nsec;
}
static int fresh(long long *at) {
long long then = __atomic_load_n(at, __ATOMIC_RELAXED);
return then != 0 && now_ns() - then < FRESH_NS;
}
/* Wine's network driver reads the link several times for each table it builds, and some games
* have it build them all hundreds of times a second. The app rewrites the file when the device
* changes network, so a reading stands for a second. */
static pthread_mutex_t link_lock = PTHREAD_MUTEX_INITIALIZER;
static struct link link_read;
static int link_ok;
static long long link_at;
static int load_link(struct link *link) {
pthread_mutex_lock(&link_lock);
if (!fresh(&link_at)) {
link_ok = read_link(&link_read);
__atomic_store_n(&link_at, now_ns(), __ATOMIC_RELAXED);
}
int ok = link_ok;
if (ok) *link = link_read;
pthread_mutex_unlock(&link_lock);
return ok;
}
/* See drm.c: a fork must not leave the lock held by a thread the child does not have. */
static void link_lock_before_fork(void) { pthread_mutex_lock(&link_lock); }
static void link_unlock_after_fork(void) { pthread_mutex_unlock(&link_lock); }
static void link_reset_after_fork(void) { pthread_mutex_init(&link_lock, NULL); }
__attribute__((constructor)) static void install_link_fork_handlers(void) {
pthread_atfork(link_lock_before_fork, link_unlock_after_fork, link_reset_after_fork);
}
/* When the kernel's own list was of no use, it is not asked again for a second: each asking binds
* a netlink socket, a stop in the tracer, only to be refused again. */
static long long ifaddrs_unusable_at, nameindex_unusable_at;
static void prefix_mask(int family, unsigned prefix, unsigned char *out) {
int len = family == AF_INET ? 4 : 16;
for (int i = 0; i < len; i++, prefix -= prefix < 8 ? prefix : 8) {
@@ -168,25 +216,35 @@ static int real_list_usable(const struct ifaddrs *list) {
return 0;
}
typedef void (*freeifaddrs_fn)(struct ifaddrs *);
static freeifaddrs_fn real_freeifaddrs;
int getifaddrs(struct ifaddrs **out) {
typedef int (*getifaddrs_fn)(struct ifaddrs **);
typedef void (*freeifaddrs_fn)(struct ifaddrs *);
getifaddrs_fn real = (getifaddrs_fn) dlsym(RTLD_NEXT, "getifaddrs");
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");
struct ifaddrs *list = NULL;
int rc = real(&list);
int saved = errno;
if (rc == 0 && real_list_usable(list)) {
*out = list;
return 0;
int asked = !fresh(&ifaddrs_unusable_at);
int rc = -1, saved = errno;
if (asked) {
rc = real(&list);
saved = errno;
if (rc == 0 && real_list_usable(list)) {
*out = list;
return 0;
}
}
struct link link;
if (!load_link(&link)) {
if (!asked) return real(out);
if (rc == 0) *out = list;
errno = saved;
return rc;
}
if (rc == 0 && list) ((freeifaddrs_fn) dlsym(RTLD_NEXT, "freeifaddrs"))(list);
if (asked) __atomic_store_n(&ifaddrs_unusable_at, now_ns(), __ATOMIC_RELAXED);
if (rc == 0 && list) real_freeifaddrs(list);
/* A leading slot keeps fill_entry()'s back link in bounds; the list starts after it. */
struct entry *block = calloc((size_t) link.addr_count + 5, sizeof(*block));
@@ -210,13 +268,13 @@ int getifaddrs(struct ifaddrs **out) {
}
void freeifaddrs(struct ifaddrs *list) {
typedef void (*freeifaddrs_fn)(struct ifaddrs *);
if (list == NULL) return;
if (list->ifa_data == list) {
free((struct entry *) list - 1);
return;
}
((freeifaddrs_fn) dlsym(RTLD_NEXT, "freeifaddrs"))(list);
if (real_freeifaddrs == NULL) real_freeifaddrs = (freeifaddrs_fn) dlsym(RTLD_NEXT, "freeifaddrs");
real_freeifaddrs(list);
}
/* Names live in the same allocation as the array, right behind it; glibc strdup()s each one. */
@@ -227,16 +285,29 @@ struct name_block {
struct if_nameindex *if_nameindex(void) {
typedef struct if_nameindex *(*if_nameindex_fn)(void);
struct if_nameindex *list = ((if_nameindex_fn) dlsym(RTLD_NEXT, "if_nameindex"))();
static if_nameindex_fn real;
if (real == NULL) real = (if_nameindex_fn) dlsym(RTLD_NEXT, "if_nameindex");
struct if_nameindex *list = NULL;
int asked = !fresh(&nameindex_unusable_at);
int saved = errno;
if (list && list[0].if_index != 0) return list;
if (asked) {
list = real();
saved = errno;
if (list && list[0].if_index != 0) return list;
}
struct link link;
struct name_block *block;
if (!load_link(&link) || (block = calloc(1, sizeof(*block))) == NULL) {
if (!load_link(&link)) {
if (!asked) return real();
errno = saved;
return list;
}
if (asked) __atomic_store_n(&nameindex_unusable_at, now_ns(), __ATOMIC_RELAXED);
if ((block = calloc(1, sizeof(*block))) == NULL) {
if (asked) errno = saved;
return list;
}
if (list) if_freenameindex(list);
snprintf(block->names[0], IFNAMSIZ, "%s", LO_NAME);
snprintf(block->names[1], IFNAMSIZ, "%s", link.name);
@@ -489,8 +560,7 @@ static FILE *synthesize(const char *table) {
/* tracer.c */
int bl_status_without_tracer(const char *path, int flags) __attribute__((visibility("hidden")));
static FILE *open_stream(const char *symbol, const char *path, const char *mode) {
typedef FILE *(*fopen_fn)(const char *, const char *);
static FILE *open_stream(fopen_fn real, const char *path, const char *mode) {
int status = mode && mode[0] == 'r' && !strchr(mode, '+')
? bl_status_without_tracer(path, O_RDONLY | O_CLOEXEC) : -1;
if (status >= 0) {
@@ -498,7 +568,7 @@ static FILE *open_stream(const char *symbol, const char *path, const char *mode)
if (copy == NULL) close(status);
return copy;
}
FILE *f = ((fopen_fn) dlsym(RTLD_NEXT, symbol))(path, mode);
FILE *f = real(path, mode);
if (f || path == NULL || mode == NULL || mode[0] != 'r') return f;
int saved = errno;
const char *table = denied_table(path);
@@ -508,9 +578,13 @@ static FILE *open_stream(const char *symbol, const char *path, const char *mode)
}
FILE *fopen(const char *path, const char *mode) {
return open_stream("fopen", path, mode);
static fopen_fn real;
if (real == NULL) real = (fopen_fn) dlsym(RTLD_NEXT, "fopen");
return open_stream(real, path, mode);
}
FILE *fopen64(const char *path, const char *mode) {
return open_stream("fopen64", path, mode);
static fopen_fn real;
if (real == NULL) real = (fopen_fn) dlsym(RTLD_NEXT, "fopen64");
return open_stream(real, path, mode);
}
+13
View File
@@ -1651,6 +1651,19 @@ int isatty(int fd) {
return r == 0;
}
/* proot reads a memfd's name only to refuse the few that are then executed or locked; the rest are
* made here. The session's /proc/net and status copies, wineserver and Xwayland make them often.
* Without memfd in the kernel, proot's ashmem stands in. */
int memfd_create(const char *name, unsigned flags) {
REAL(memfd_create, int (*)(const char *, unsigned));
if (!name || !strncmp(name, "JITCode:", 8) || !strcmp(name, "opcache_lock") || !strncmp(name, "lib/apk/exec/", 13)
|| !fp_ready()) return real_memfd_create(name, flags);
long r = sc(SYS_memfd_create, (long)name, flags, 0, 0, 0);
if (r == -ENOSYS) return real_memfd_create(name, flags);
hits++;
return ret(r);
}
/* ---------------------------------------------------------------- passwd */
/*