mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-09 17:11:32 -10:00
Compare commits
5
Commits
feat/agent-host
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae3e9509f4 | ||
|
|
6bcd431ca6 | ||
|
|
836340f3a2 | ||
|
|
1b19837fc6 | ||
|
|
c85b84a964 |
No files matched your search
@@ -30,6 +30,7 @@
|
||||
#include <linux/uinput.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <signal.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
@@ -208,23 +209,33 @@ struct FakeConfig {
|
||||
bool uinput_enabled;
|
||||
};
|
||||
|
||||
// The first allocation here can start the host program's own allocator, which may call a hooked
|
||||
// function (mimalloc probes NUMA nodes with access()) and land back in config(). A guarded static
|
||||
// would throw a recursive-init error there, so the fields are filled without allocating, and a
|
||||
// reentry on the initializing thread gets them while other threads wait for the fork handlers.
|
||||
static FakeConfig &config() {
|
||||
static FakeConfig *cfg = [] {
|
||||
auto *c = new FakeConfig();
|
||||
const char *dir = getenv("FAKE_EVDEV_DIR");
|
||||
c->hook_dir = dir ? dir : "/data/data/com.termux/files/home/fake-input";
|
||||
c->udev_data_dir = getenv("FAKE_UDEV_DATA_DIR");
|
||||
c->vibration_enabled =
|
||||
getenv("FAKE_EVDEV_VIBRATION") && atoi(getenv("FAKE_EVDEV_VIBRATION"));
|
||||
const char *identity = getenv("FAKE_EVDEV_IDENTITY");
|
||||
c->xbox360_identity = identity && !strcmp(identity, "xbox360");
|
||||
c->uinput_enabled = getenv("FAKE_EVDEV_UINPUT") && atoi(getenv("FAKE_EVDEV_UINPUT"));
|
||||
pthread_atfork(controller_fork_prepare, controller_fork_parent,
|
||||
controller_fork_child);
|
||||
Logger::init();
|
||||
return c;
|
||||
}();
|
||||
return *cfg;
|
||||
static FakeConfig cfg;
|
||||
static std::atomic<int> state{0};
|
||||
static __thread bool initializing __attribute__((tls_model("initial-exec")));
|
||||
if (state.load(std::memory_order_acquire) == 2 || initializing) return cfg;
|
||||
int expected = 0;
|
||||
if (!state.compare_exchange_strong(expected, 1, std::memory_order_acq_rel)) {
|
||||
while (state.load(std::memory_order_acquire) != 2) sched_yield();
|
||||
return cfg;
|
||||
}
|
||||
initializing = true;
|
||||
const char *dir = getenv("FAKE_EVDEV_DIR");
|
||||
cfg.hook_dir = dir ? dir : "/data/data/com.termux/files/home/fake-input";
|
||||
cfg.udev_data_dir = getenv("FAKE_UDEV_DATA_DIR");
|
||||
cfg.vibration_enabled = getenv("FAKE_EVDEV_VIBRATION") && atoi(getenv("FAKE_EVDEV_VIBRATION"));
|
||||
const char *identity = getenv("FAKE_EVDEV_IDENTITY");
|
||||
cfg.xbox360_identity = identity && !strcmp(identity, "xbox360");
|
||||
cfg.uinput_enabled = getenv("FAKE_EVDEV_UINPUT") && atoi(getenv("FAKE_EVDEV_UINPUT"));
|
||||
pthread_atfork(controller_fork_prepare, controller_fork_parent, controller_fork_child);
|
||||
Logger::init();
|
||||
state.store(2, std::memory_order_release);
|
||||
initializing = false;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static std::recursive_mutex &controller_mutex() {
|
||||
|
||||
@@ -121,6 +121,7 @@ import com.droiddeck.launcher.session.SessionArtifacts
|
||||
import com.droiddeck.launcher.session.SessionLogShare
|
||||
import com.droiddeck.launcher.session.SessionPhase
|
||||
import com.droiddeck.launcher.session.SessionPaths
|
||||
import com.droiddeck.launcher.session.SessionLoadingDecision
|
||||
import com.droiddeck.launcher.wayland.HdrSupport
|
||||
import com.droiddeck.launcher.session.SessionService
|
||||
import com.droiddeck.launcher.session.SessionState
|
||||
@@ -2435,6 +2436,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private fun watchSession() {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
var ticks = 0
|
||||
val loadingStartedAt = SystemClock.elapsedRealtime()
|
||||
val poll = object : Runnable {
|
||||
override fun run() {
|
||||
if (!watching) return
|
||||
@@ -2447,6 +2449,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
if (loading.visible && !loading.ended && !SessionState.running && !installingRuntime &&
|
||||
SessionLoadingDecision.shouldReturnToLauncher(
|
||||
loadingStartedAt, SystemClock.elapsedRealtime(), SessionState.running, SessionState.phase)) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
if (loading.visible && !loading.ended) {
|
||||
if (!installingRuntime) loading.update(this@SessionActivity, SessionState.logFile)
|
||||
if (ticks++ % 2 == 0) loading.tick()
|
||||
@@ -2471,8 +2479,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val hint = sessionExitHint(status)
|
||||
runOnUiThread {
|
||||
if (isFinishing || isDestroyed) return@runOnUiThread
|
||||
if (status == 0) SessionState.relaunch?.let { next ->
|
||||
SessionState.relaunch = null
|
||||
if (status == 0) SessionState.takeRelaunch()?.let { next ->
|
||||
next.putExtra(EXTRA_RETURN_HOME, intent.getBooleanExtra(EXTRA_RETURN_HOME, false))
|
||||
next.putExtra(EXTRA_LINK_TASK, intent.getIntExtra(EXTRA_LINK_TASK, -1))
|
||||
setIntent(next)
|
||||
|
||||
@@ -14,6 +14,7 @@ object SessionEvents {
|
||||
SessionPaths.take()
|
||||
val dir = SessionPaths.beginOrCurrent(context, label ?: SessionPaths.label(mode, null))
|
||||
synchronized(SessionState) {
|
||||
SessionState.clearRelaunch()
|
||||
SessionState.phase = SessionPhase.PREPARING
|
||||
SessionState.mode = mode
|
||||
SessionState.sessionId = dir.name
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
internal object SessionLoadingDecision {
|
||||
const val TIMEOUT_MS = 10_000L
|
||||
|
||||
fun shouldReturnToLauncher(startedAt: Long, now: Long, running: Boolean, phase: SessionPhase): Boolean =
|
||||
!running && phase in setOf(SessionPhase.IDLE, SessionPhase.FAILED) && now - startedAt >= TIMEOUT_MS
|
||||
}
|
||||
@@ -1697,8 +1697,8 @@ class SessionService : Service() {
|
||||
.putExtra(EXTRA_STEAM_UI, ui)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
if (url.isNotEmpty()) next.putExtra(EXTRA_STEAM_URL, url)
|
||||
SessionState.relaunch = next
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).post { stopSession(0) }
|
||||
SessionState.queueRelaunch(next)
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).post { stopSession(0, relaunch = true) }
|
||||
}
|
||||
}
|
||||
watcher.startWatching()
|
||||
@@ -2013,7 +2013,7 @@ class SessionService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopSession(status: Int, gen: Int? = null) {
|
||||
private fun stopSession(status: Int, gen: Int? = null, relaunch: Boolean = false) {
|
||||
if (gen != null && gen != sessionGen) {
|
||||
Log.i(TAG, "ignoring a stop from session $gen; session $sessionGen is current")
|
||||
return
|
||||
@@ -2024,6 +2024,7 @@ class SessionService : Service() {
|
||||
cancelQueuedSteamGame()
|
||||
gameExitWatchToken++
|
||||
if (!SessionState.running) return
|
||||
if (relaunch) SessionState.confirmRelaunchStop()
|
||||
SessionEvents.record("guest.exited", mapOf("status" to status))
|
||||
SessionEvents.transition(SessionPhase.STOPPING, "session.stopping", mapOf("status" to status))
|
||||
SessionState.running = false
|
||||
|
||||
@@ -73,7 +73,44 @@ object SessionState {
|
||||
@Volatile
|
||||
var steamClientArch: String? = null
|
||||
var steamVr by mutableStateOf(false)
|
||||
@Volatile
|
||||
var relaunch: android.content.Intent? = null
|
||||
@Volatile
|
||||
private var relaunchSessionId: String? = null
|
||||
@Volatile
|
||||
private var relaunchStopRequested = false
|
||||
|
||||
fun queueRelaunch(intent: android.content.Intent) {
|
||||
synchronized(this) {
|
||||
relaunch = intent
|
||||
relaunchSessionId = sessionId
|
||||
relaunchStopRequested = false
|
||||
}
|
||||
}
|
||||
|
||||
fun confirmRelaunchStop() {
|
||||
synchronized(this) {
|
||||
relaunchStopRequested = relaunch != null && relaunchSessionId == sessionId
|
||||
}
|
||||
}
|
||||
|
||||
fun takeRelaunch(): android.content.Intent? = synchronized(this) {
|
||||
val next = relaunch
|
||||
val valid = relaunchStopRequested && relaunchSessionId == sessionId
|
||||
relaunch = null
|
||||
relaunchSessionId = null
|
||||
relaunchStopRequested = false
|
||||
next.takeIf { valid }
|
||||
}
|
||||
|
||||
fun clearRelaunch() {
|
||||
synchronized(this) {
|
||||
relaunch = null
|
||||
relaunchSessionId = null
|
||||
relaunchStopRequested = false
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic var hdr = false
|
||||
|
||||
@Volatile private var deckPadValue = false
|
||||
|
||||
@@ -58,6 +58,7 @@ object Themes {
|
||||
const val RGB_SPEEDS = 10
|
||||
const val DEFAULT_RGB_SPEED = 3
|
||||
const val RGB_HUE_STEP = 3f
|
||||
const val WHITE_RED = "white_red"
|
||||
|
||||
var rgbSpeed by mutableStateOf(DEFAULT_RGB_SPEED)
|
||||
private set
|
||||
@@ -127,6 +128,13 @@ object Themes {
|
||||
primary = Color(0xFF2E86FF), primary2 = Color(0xFF2684FF), onPrimary = Color(0xFF03111F), signal = Color(0xFF2E86FF),
|
||||
good = Color(0xFF4CD37F),
|
||||
),
|
||||
Palette(
|
||||
WHITE_RED, R.string.theme_white_red, R.string.theme_white_red_detail,
|
||||
background = Color(0xFFF8F8F8), surface = Color(0xFFFFFFFF), surfaceVariant = Color(0xFFF0F0F0), line = Color(0xFFDDDDDD), line2 = Color(0xFFC8C8C8),
|
||||
onBackground = Color(0xFF191919), onSurfaceVariant = Color(0xFF555555),
|
||||
primary = Color(0xFFD62828), primary2 = Color(0xFFB71C1C), onPrimary = Color(0xFFFFFFFF), signal = Color(0xFFD62828),
|
||||
good = Color(0xFF248548),
|
||||
),
|
||||
innerGamer(0f),
|
||||
ThemeColors.palette(DEFAULT_PRIMARY, DEFAULT_SECONDARY, R.string.theme_gradient, R.string.theme_gradient_detail),
|
||||
)
|
||||
|
||||
@@ -3809,4 +3809,7 @@
|
||||
<string name="input_reset_positions">إعادة تعيين المواضع</string>
|
||||
<string name="input_show_label">إظهار التسمية</string>
|
||||
<string name="input_layout_import_failed">تعذّر استيراد هذا التخطيط</string>
|
||||
|
||||
<string name="theme_white_red">ثلج قرمزي</string>
|
||||
|
||||
</resources>
|
||||
@@ -3665,4 +3665,6 @@
|
||||
<string name="input_reset_positions">Restablecer posiciones</string>
|
||||
<string name="input_show_label">Mostrar etiqueta</string>
|
||||
<string name="input_layout_import_failed">No se pudo importar esta disposición</string>
|
||||
<string name="theme_white_red">Nieve escarlata</string>
|
||||
<string name="theme_white_red_detail">Superficies blancas con acentos rojos.</string>
|
||||
</resources>
|
||||
@@ -3653,4 +3653,7 @@
|
||||
<string name="input_reset_positions">Нулиране на позициите</string>
|
||||
<string name="input_show_label">Показване на етикета</string>
|
||||
<string name="input_layout_import_failed">Това разположение не можа да бъде импортирано</string>
|
||||
|
||||
<string name="theme_white_red">Ален сняг</string>
|
||||
|
||||
</resources>
|
||||
@@ -3731,4 +3731,7 @@
|
||||
<string name="input_reset_positions">Obnovit pozice</string>
|
||||
<string name="input_show_label">Zobrazit popisek</string>
|
||||
<string name="input_layout_import_failed">Toto rozložení nelze importovat</string>
|
||||
|
||||
<string name="theme_white_red">Šarlatový sníh</string>
|
||||
|
||||
</resources>
|
||||
@@ -3653,4 +3653,7 @@
|
||||
<string name="input_reset_positions">Nulstil placeringer</string>
|
||||
<string name="input_show_label">Vis etiket</string>
|
||||
<string name="input_layout_import_failed">Dette layout kunne ikke importeres</string>
|
||||
|
||||
<string name="theme_white_red">Skarlagensne</string>
|
||||
|
||||
</resources>
|
||||
@@ -3653,4 +3653,7 @@
|
||||
<string name="input_reset_positions">Positionen zurücksetzen</string>
|
||||
<string name="input_show_label">Beschriftung anzeigen</string>
|
||||
<string name="input_layout_import_failed">Dieses Layout konnte nicht importiert werden</string>
|
||||
|
||||
<string name="theme_white_red">Scharlachroter Schnee</string>
|
||||
|
||||
</resources>
|
||||
@@ -3653,4 +3653,7 @@
|
||||
<string name="input_reset_positions">Επαναφορά θέσεων</string>
|
||||
<string name="input_show_label">Εμφάνιση ετικέτας</string>
|
||||
<string name="input_layout_import_failed">Δεν ήταν δυνατή η εισαγωγή αυτής της διάταξης</string>
|
||||
|
||||
<string name="theme_white_red">Κατακόκκινο χιόνι</string>
|
||||
|
||||
</resources>
|
||||
@@ -3692,4 +3692,7 @@
|
||||
<string name="input_reset_positions">Restablecer posiciones</string>
|
||||
<string name="input_show_label">Mostrar etiqueta</string>
|
||||
<string name="input_layout_import_failed">No se ha podido importar esta disposición</string>
|
||||
|
||||
<string name="theme_white_red">Nieve escarlata</string>
|
||||
|
||||
</resources>
|
||||
@@ -3653,4 +3653,7 @@
|
||||
<string name="input_reset_positions">Palauta sijainnit</string>
|
||||
<string name="input_show_label">Näytä nimi</string>
|
||||
<string name="input_layout_import_failed">Tätä asettelua ei voitu tuoda</string>
|
||||
|
||||
<string name="theme_white_red">Helakanpunainen lumi</string>
|
||||
|
||||
</resources>
|
||||
@@ -3692,4 +3692,7 @@
|
||||
<string name="input_reset_positions">Réinitialiser les positions</string>
|
||||
<string name="input_show_label">Afficher le libellé</string>
|
||||
<string name="input_layout_import_failed">Impossible d’importer cette disposition</string>
|
||||
|
||||
<string name="theme_white_red">Neige écarlate</string>
|
||||
|
||||
</resources>
|
||||
@@ -3653,4 +3653,7 @@
|
||||
<string name="input_reset_positions">स्थितियाँ रीसेट करें</string>
|
||||
<string name="input_show_label">लेबल दिखाएँ</string>
|
||||
<string name="input_layout_import_failed">यह लेआउट इम्पोर्ट नहीं किया जा सका</string>
|
||||
|
||||
<string name="theme_white_red">किरमिज़ी बर्फ</string>
|
||||
|
||||
</resources>
|
||||
@@ -3653,4 +3653,7 @@
|
||||
<string name="input_reset_positions">Pozíciók visszaállítása</string>
|
||||
<string name="input_show_label">Címke megjelenítése</string>
|
||||
<string name="input_layout_import_failed">Ez az elrendezés nem importálható</string>
|
||||
|
||||
<string name="theme_white_red">Skarlátvörös hó</string>
|
||||
|
||||
</resources>
|
||||
@@ -3623,4 +3623,7 @@
|
||||
<string name="input_reset_positions">Atur ulang posisi</string>
|
||||
<string name="input_show_label">Tampilkan label</string>
|
||||
<string name="input_layout_import_failed">Tata letak ini tidak dapat diimpor</string>
|
||||
|
||||
<string name="theme_white_red">Salju Kirmizi</string>
|
||||
|
||||
</resources>
|
||||
@@ -3692,4 +3692,7 @@
|
||||
<string name="input_reset_positions">Ripristina posizioni</string>
|
||||
<string name="input_show_label">Mostra etichetta</string>
|
||||
<string name="input_layout_import_failed">Impossibile importare questo layout</string>
|
||||
|
||||
<string name="theme_white_red">Neve scarlatta</string>
|
||||
|
||||
</resources>
|
||||
@@ -3623,4 +3623,7 @@
|
||||
<string name="input_reset_positions">位置をリセット</string>
|
||||
<string name="input_show_label">ラベルを表示</string>
|
||||
<string name="input_layout_import_failed">このレイアウトをインポートできませんでした</string>
|
||||
|
||||
<string name="theme_white_red">緋色の雪</string>
|
||||
|
||||
</resources>
|
||||
@@ -3623,4 +3623,7 @@
|
||||
<string name="input_reset_positions">위치 초기화</string>
|
||||
<string name="input_show_label">라벨 표시</string>
|
||||
<string name="input_layout_import_failed">이 레이아웃을 가져올 수 없습니다</string>
|
||||
|
||||
<string name="theme_white_red">진홍빛 눈</string>
|
||||
|
||||
</resources>
|
||||
@@ -3623,4 +3623,7 @@
|
||||
<string name="input_reset_positions">Tetapkan semula kedudukan</string>
|
||||
<string name="input_show_label">Tunjukkan label</string>
|
||||
<string name="input_layout_import_failed">Susun atur ini tidak dapat diimport</string>
|
||||
|
||||
<string name="theme_white_red">Salji Merah Skarlet</string>
|
||||
|
||||
</resources>
|
||||
@@ -3653,4 +3653,7 @@
|
||||
<string name="input_reset_positions">Tilbakestill plasseringer</string>
|
||||
<string name="input_show_label">Vis etikett</string>
|
||||
<string name="input_layout_import_failed">Kunne ikke importere dette oppsettet</string>
|
||||
|
||||
<string name="theme_white_red">Skarlagenssnø</string>
|
||||
|
||||
</resources>
|
||||
@@ -3653,4 +3653,7 @@
|
||||
<string name="input_reset_positions">Posities herstellen</string>
|
||||
<string name="input_show_label">Label tonen</string>
|
||||
<string name="input_layout_import_failed">Deze indeling kan niet worden geïmporteerd</string>
|
||||
|
||||
<string name="theme_white_red">Scharlakenrode sneeuw</string>
|
||||
|
||||
</resources>
|
||||
@@ -3731,4 +3731,7 @@
|
||||
<string name="input_reset_positions">Resetuj położenie</string>
|
||||
<string name="input_show_label">Pokaż etykietę</string>
|
||||
<string name="input_layout_import_failed">Nie można zaimportować tego układu</string>
|
||||
|
||||
<string name="theme_white_red">Szkarłatny śnieg</string>
|
||||
|
||||
</resources>
|
||||
@@ -3692,4 +3692,7 @@
|
||||
<string name="input_reset_positions">Repor posições</string>
|
||||
<string name="input_show_label">Mostrar etiqueta</string>
|
||||
<string name="input_layout_import_failed">Não foi possível importar este esquema</string>
|
||||
|
||||
<string name="theme_white_red">Neve Escarlate</string>
|
||||
|
||||
</resources>
|
||||
@@ -3692,4 +3692,7 @@
|
||||
<string name="input_reset_positions">Redefinir posições</string>
|
||||
<string name="input_show_label">Mostrar rótulo</string>
|
||||
<string name="input_layout_import_failed">Não foi possível importar este layout</string>
|
||||
|
||||
<string name="theme_white_red">Neve Escarlate</string>
|
||||
|
||||
</resources>
|
||||
@@ -3692,4 +3692,7 @@
|
||||
<string name="input_reset_positions">Resetează pozițiile</string>
|
||||
<string name="input_show_label">Afișează eticheta</string>
|
||||
<string name="input_layout_import_failed">Acest aranjament nu a putut fi importat</string>
|
||||
|
||||
<string name="theme_white_red">Zăpadă stacojie</string>
|
||||
|
||||
</resources>
|
||||
@@ -3814,4 +3814,7 @@
|
||||
<string name="input_reset_positions">Сбросить положение</string>
|
||||
<string name="input_show_label">Показывать подпись</string>
|
||||
<string name="input_layout_import_failed">Не удалось импортировать эту раскладку</string>
|
||||
|
||||
<string name="theme_white_red">Алый снег</string>
|
||||
|
||||
</resources>
|
||||
@@ -3653,4 +3653,7 @@
|
||||
<string name="input_reset_positions">Återställ positioner</string>
|
||||
<string name="input_show_label">Visa etikett</string>
|
||||
<string name="input_layout_import_failed">Det gick inte att importera layouten</string>
|
||||
|
||||
<string name="theme_white_red">Scharlakansröd snö</string>
|
||||
|
||||
</resources>
|
||||
@@ -3623,4 +3623,7 @@
|
||||
<string name="input_reset_positions">รีเซ็ตตำแหน่ง</string>
|
||||
<string name="input_show_label">แสดงป้ายกำกับ</string>
|
||||
<string name="input_layout_import_failed">ไม่สามารถนำเข้าการจัดวางนี้ได้</string>
|
||||
|
||||
<string name="theme_white_red">หิมะสีแดงชาด</string>
|
||||
|
||||
</resources>
|
||||
@@ -3653,4 +3653,7 @@
|
||||
<string name="input_reset_positions">Konumları sıfırla</string>
|
||||
<string name="input_show_label">Etiketi göster</string>
|
||||
<string name="input_layout_import_failed">Bu düzen içe aktarılamadı</string>
|
||||
|
||||
<string name="theme_white_red">Kızıl Kar</string>
|
||||
|
||||
</resources>
|
||||
@@ -3731,4 +3731,7 @@
|
||||
<string name="input_reset_positions">Скинути розташування</string>
|
||||
<string name="input_show_label">Показувати підпис</string>
|
||||
<string name="input_layout_import_failed">Не вдалося імпортувати цю розкладку</string>
|
||||
|
||||
<string name="theme_white_red">Багряний сніг</string>
|
||||
|
||||
</resources>
|
||||
@@ -3623,4 +3623,7 @@
|
||||
<string name="input_reset_positions">Đặt lại vị trí</string>
|
||||
<string name="input_show_label">Hiện nhãn</string>
|
||||
<string name="input_layout_import_failed">Không thể nhập bố cục này</string>
|
||||
|
||||
<string name="theme_white_red">Tuyết đỏ thẫm</string>
|
||||
|
||||
</resources>
|
||||
@@ -3627,4 +3627,7 @@
|
||||
<string name="input_reset_positions">重置位置</string>
|
||||
<string name="input_show_label">显示标签</string>
|
||||
<string name="input_layout_import_failed">无法导入此布局</string>
|
||||
|
||||
<string name="theme_white_red">猩红之雪</string>
|
||||
|
||||
</resources>
|
||||
@@ -3626,4 +3626,7 @@
|
||||
<string name="input_reset_positions">重設位置</string>
|
||||
<string name="input_show_label">顯示標籤</string>
|
||||
<string name="input_layout_import_failed">無法匯入此佈局</string>
|
||||
|
||||
<string name="theme_white_red">緋紅之雪</string>
|
||||
|
||||
</resources>
|
||||
@@ -3626,4 +3626,7 @@
|
||||
<string name="input_reset_positions">重設位置</string>
|
||||
<string name="input_show_label">顯示標籤</string>
|
||||
<string name="input_layout_import_failed">無法匯入此版面配置</string>
|
||||
|
||||
<string name="theme_white_red">緋紅之雪</string>
|
||||
|
||||
</resources>
|
||||
@@ -3814,4 +3814,7 @@
|
||||
<string name="input_reset_positions">Reset positions</string>
|
||||
<string name="input_show_label">Show label</string>
|
||||
<string name="input_layout_import_failed">Could not import this layout</string>
|
||||
|
||||
<string name="theme_white_red">Scarlet Snow</string>
|
||||
<string name="theme_white_red_detail">White surfaces with red accents.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Intent
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class SessionLoadingDecisionTest {
|
||||
private var previousSessionId: String? = null
|
||||
private var previousRelaunch: Intent? = null
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
previousSessionId = SessionState.sessionId
|
||||
previousRelaunch = SessionState.relaunch
|
||||
SessionState.clearRelaunch()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
SessionState.sessionId = previousSessionId
|
||||
SessionState.relaunch = previousRelaunch
|
||||
SessionState.clearRelaunch()
|
||||
if (previousRelaunch != null) SessionState.queueRelaunch(previousRelaunch!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadingTimesOutOnlyWhenNoSessionStarted() {
|
||||
assertEquals(false, SessionLoadingDecision.shouldReturnToLauncher(100L, 10_099L, false, SessionPhase.IDLE))
|
||||
assertEquals(true, SessionLoadingDecision.shouldReturnToLauncher(100L, 10_100L, false, SessionPhase.IDLE))
|
||||
assertEquals(false, SessionLoadingDecision.shouldReturnToLauncher(100L, 10_100L, true, SessionPhase.IDLE))
|
||||
assertEquals(false, SessionLoadingDecision.shouldReturnToLauncher(100L, 10_100L, false, SessionPhase.PREPARING))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relaunchRequiresTheCurrentSessionToHaveRequestedTheStop() {
|
||||
SessionState.sessionId = "session-a"
|
||||
val next = Intent().apply { action = "next" }
|
||||
SessionState.queueRelaunch(next)
|
||||
assertNull(SessionState.takeRelaunch())
|
||||
SessionState.queueRelaunch(next)
|
||||
SessionState.confirmRelaunchStop()
|
||||
assertEquals(next, SessionState.takeRelaunch())
|
||||
assertNull(SessionState.takeRelaunch())
|
||||
|
||||
SessionState.queueRelaunch(next)
|
||||
SessionState.confirmRelaunchStop()
|
||||
SessionState.sessionId = "session-b"
|
||||
assertNull(SessionState.takeRelaunch())
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -263,10 +263,15 @@ def displays(adb: str, serial: str) -> list[dict]:
|
||||
device = re.search(r"mPrimaryDisplayDevice=(.+)", block)
|
||||
size = re.search(r"real (\d+) x (\d+)", block)
|
||||
name = device.group(1).strip() if device else None
|
||||
physical = devices.get(name) if name else None
|
||||
if name and not physical:
|
||||
# Some ROMs (Retroid Pocket Duo) print the device as "Built-in Screen(local:<id>)"
|
||||
inline = re.search(r"\(local:(\d+)\)$", name)
|
||||
physical = inline.group(1) if inline else None
|
||||
out.append({
|
||||
"display": logical,
|
||||
"name": name,
|
||||
"physicalId": devices.get(name) if name else None,
|
||||
"physicalId": physical,
|
||||
"size": [int(size.group(1)), int(size.group(2))] if size else None,
|
||||
})
|
||||
return out
|
||||
|
||||
@@ -927,6 +927,33 @@ def steam_shortcut_state(agent_dir, game_id, timeout=6):
|
||||
time.sleep(min(0.5, max(0, deadline - time.monotonic())))
|
||||
|
||||
|
||||
def steam_ready(agent_dir):
|
||||
expression = """(() => {
|
||||
const store = globalThis.appStore;
|
||||
return !!store?.m_bIsInitialized && typeof globalThis.SteamClient?.Apps?.ContinueGameAction === 'function';
|
||||
})()"""
|
||||
return cdp_eval(agent_dir, expression, await_promise=False, timeout=3).get("value") is True
|
||||
|
||||
|
||||
def steam_clear_pending_interstitial(agent_dir, app_id):
|
||||
if not isinstance(app_id, str) or not app_id.isascii() or not app_id.isdigit() or int(app_id) <= 0:
|
||||
raise AgentError("INVALID_APP_ID", "appId must be a positive decimal Steam app id")
|
||||
expression = """(async () => {
|
||||
const apps = globalThis.SteamClient?.Apps;
|
||||
if (typeof apps?.GetActiveGameActions !== 'function' || typeof apps.ContinueGameAction !== 'function') return false;
|
||||
const active = await apps.GetActiveGameActions() || [];
|
||||
for (const action of active) {
|
||||
const appid = String(action?.strAppID ?? action?.gameid ?? action?.nAppID ?? '');
|
||||
if (appid === %s && action?.strTaskName === 'ShowInterstitials') {
|
||||
apps.ContinueGameAction(action.nGameActionID, appid, 'ShowInterstitials');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})()""" % json.dumps(app_id)
|
||||
return cdp_eval(agent_dir, expression, timeout=3).get("value") is True
|
||||
|
||||
|
||||
def game_pids(app_id):
|
||||
wanted = [("SteamAppId=%s" % app_id).encode(), ("SteamGameId=%s" % app_id).encode()]
|
||||
me = os.getpid()
|
||||
@@ -1261,6 +1288,19 @@ def main(argv):
|
||||
return 1
|
||||
print(state)
|
||||
return {"ready": 0, "missing": 4, "not-shortcut": 3}.get(state, 1)
|
||||
if argv and argv[0] == "--steam-ready":
|
||||
try:
|
||||
return 0 if steam_ready(Path(base) / "agent") else 1
|
||||
except (AgentError, OSError, ValueError):
|
||||
return 1
|
||||
if argv and argv[0] == "--steam-clear-pending-interstitial":
|
||||
if len(argv) != 2:
|
||||
print("usage: droiddeck-agent --steam-clear-pending-interstitial <appid>", file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
return 0 if steam_clear_pending_interstitial(Path(base) / "agent", argv[1]) else 1
|
||||
except (AgentError, OSError, ValueError):
|
||||
return 1
|
||||
serve(Path(base) / "agent", focus="--no-focus" not in argv)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -1244,7 +1244,14 @@ case $mode in
|
||||
sleep 1
|
||||
done
|
||||
[ "${#game}" -gt 10 ] && shortcut=1
|
||||
[ -n "$shortcut" ] || sleep 4
|
||||
_bl_ready_end=$((SECONDS + 30))
|
||||
while [ "$waited" -lt 30 ] && [ "$((_bl_ready_end - SECONDS))" -gt 0 ]; do
|
||||
timeout "$((_bl_ready_end - SECONDS))" /usr/local/bin/droiddeck-agent --steam-ready >/dev/null 2>&1 && break
|
||||
[ "$((_bl_ready_end - SECONDS))" -gt 1 ] || break
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
waited=0
|
||||
echo "== steam: interface up; launching the requested game $game"
|
||||
_bl_hand_off
|
||||
[ -n "$shortcut" ] || return 0
|
||||
@@ -1264,7 +1271,9 @@ case $mode in
|
||||
elif _bl_starting; then
|
||||
echo "== steam: the requested game started while waiting; not launching again"
|
||||
elif [ "$(_bl_focused)" = 769 ]; then
|
||||
echo "== steam: launching the requested game again $game (overview status $status)"
|
||||
echo "== steam: clearing the pending launch interstitial for $game (overview status $status)"
|
||||
/usr/local/bin/droiddeck-agent --steam-clear-pending-interstitial "$(((game >> 32) & 0xffffffff))" >/dev/null 2>&1 && return 0
|
||||
echo "== steam: launching the requested game again $game"
|
||||
_bl_hand_off
|
||||
else
|
||||
echo "== steam: the game took focus while waiting; not launching again"
|
||||
|
||||
@@ -152,6 +152,16 @@ Prototype (inert unless `PROOT_FASTPATH` is set; see `docs/development/proot-per
|
||||
natively reserve 256 GiB above 64 GiB for guest memory: under PRoot the reservation failed and
|
||||
the first guest instruction faulted forever. 48-bit address spaces and 32-bit programs are
|
||||
unchanged in practice.
|
||||
- `0027-guest-seccomp-sigsys.patch` - PRoot treated every seccomp `SIGSYS` as Android's app policy
|
||||
refusing a call: it emulated the few it knows and answered the rest with `ENOSYS`, so a filter
|
||||
the guest installs itself never reached its own handler. Firefox's content sandbox traps
|
||||
`openat` and opens files through a broker in the parent; every font open came back `ENOSYS`,
|
||||
and each tab crashed in `gfxFontGroup::GetDefaultFont`. Android's policy traps with
|
||||
`SECCOMP_RET_DATA` 0, while Firefox's and Chromium's filters tag their traps, so a tagged
|
||||
`SIGSYS` is now delivered to the guest. A trap from the fast path's trampoline still gets
|
||||
`ENOSYS`, because its arguments are host paths; the fast path then stops for that process and
|
||||
repeats the call through a plain `svc`, which the guest's filter traps with the guest path.
|
||||
`bench/guest-trap.c` installs a tagged `openat` trap and checks its handler answers the call.
|
||||
|
||||
Old kernel fixes:
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Run inside a session, with and without the fast path: a seccomp filter the guest installs
|
||||
// itself must reach its own SIGSYS handler, which answers the trapped openat.
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/filter.h>
|
||||
#include <linux/seccomp.h>
|
||||
#include <signal.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <ucontext.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define ANSWER 4242
|
||||
static volatile int traps, data;
|
||||
|
||||
static void on_sigsys(int sig, siginfo_t *info, void *context) {
|
||||
(void)sig;
|
||||
ucontext_t *uc = context;
|
||||
traps++;
|
||||
data = info->si_errno;
|
||||
uc->uc_mcontext.regs[0] = ANSWER;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof sa);
|
||||
sa.sa_sigaction = on_sigsys;
|
||||
sa.sa_flags = SA_SIGINFO;
|
||||
sigaction(SIGSYS, &sa, NULL);
|
||||
struct sock_filter code[] = {
|
||||
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
|
||||
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SYS_openat, 0, 1),
|
||||
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRAP | 7),
|
||||
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
|
||||
};
|
||||
struct sock_fprog filter = {.len = sizeof code / sizeof *code, .filter = code};
|
||||
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) || prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &filter, 0, 0)) {
|
||||
perror("seccomp");
|
||||
return 77;
|
||||
}
|
||||
int fd = open("/etc/hostname", O_RDONLY);
|
||||
int err = errno;
|
||||
printf("open=%d errno=%d traps=%d data=%d\n", fd, fd < 0 ? err : 0, traps, data);
|
||||
return fd == ANSWER && traps == 1 && data == 7 ? 0 : 1;
|
||||
}
|
||||
@@ -49,7 +49,30 @@ static size_t rootlen;
|
||||
static long long ttl_ns = 2000000000LL;
|
||||
static unsigned long hits, misses;
|
||||
|
||||
static long sc(long nr, long a, long b, long c, long d, long e) { return fp_sys(nr, a, b, c, d, e, 0); }
|
||||
// A seccomp filter the guest installs itself (Firefox's content sandbox) can trap a trampoline
|
||||
// syscall that the host allows; proot answers those with ENOSYS. The call is then repeated
|
||||
// through libc, where the guest's own SIGSYS handler sees the guest path, and the process stops
|
||||
// using the fast path.
|
||||
static __thread int fp_refused;
|
||||
static int optional_syscall(long nr) {
|
||||
return nr == SYS_statx || nr == SYS_memfd_create || nr == SYS_faccessat2 || nr == SYS_close_range ||
|
||||
nr == SYS_openat2 || nr == SYS_renameat2;
|
||||
}
|
||||
static long sc(long nr, long a, long b, long c, long d, long e) {
|
||||
if (fp_refused) return -ENOSYS;
|
||||
long r = fp_sys(nr, a, b, c, d, e, 0);
|
||||
if (r == -ENOSYS && !optional_syscall(nr)) {
|
||||
__atomic_store_n(&fp_on, 0, __ATOMIC_RELAXED);
|
||||
fp_refused = 1;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
static inline int refused(void) {
|
||||
if (!fp_refused) return 0;
|
||||
fp_refused = 0;
|
||||
return 1;
|
||||
}
|
||||
static inline int answered(long r) { return !refused() && r != FP_SLOW; }
|
||||
static void fp_init(void);
|
||||
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
|
||||
static inline int fp_ready(void) {
|
||||
@@ -788,7 +811,7 @@ static int ret(long r) {
|
||||
if (!real_##name) real_##name = (type)dlsym(RTLD_NEXT, #name)
|
||||
|
||||
#define ANSWER(r, dirfd, path, real_call) \
|
||||
do { long r_ = (r); if (r_ != FP_SLOW) { hits++; return ret(r_); } missed(__func__, dirfd, path); return real_call; } while (0)
|
||||
do { long r_ = (r); if (answered(r_)) { hits++; return ret(r_); } missed(__func__, dirfd, path); return real_call; } while (0)
|
||||
|
||||
|
||||
static long fp_open(int dirfd, const char *path, int flags, mode_t mode) {
|
||||
@@ -805,7 +828,7 @@ static long fp_open(int dirfd, const char *path, int flags, mode_t mode) {
|
||||
|
||||
static int do_openat(int dirfd, const char *path, int flags, mode_t mode, int (*real)(int, const char *, int, ...)) {
|
||||
long r = fp_open(dirfd, path, flags, mode);
|
||||
if (r != FP_SLOW) { hits++; return ret(r); }
|
||||
if (answered(r)) { hits++; return ret(r); }
|
||||
missed(__func__, dirfd, path);
|
||||
return real(dirfd, path, flags, mode);
|
||||
}
|
||||
@@ -870,8 +893,10 @@ int lstat64(const char *p, struct stat64 *st) { return lstat(p, (struct stat *)s
|
||||
int fstat(int fd, struct stat *st) {
|
||||
REAL(fstat, int (*)(int, struct stat *));
|
||||
if (!fp_ready()) return real_fstat(fd, st);
|
||||
long r = sc(SYS_fstat, fd, (long)st, 0, 0, 0);
|
||||
if (!answered(r)) return real_fstat(fd, st);
|
||||
hits++;
|
||||
return ret(sc(SYS_fstat, fd, (long)st, 0, 0, 0));
|
||||
return ret(r);
|
||||
}
|
||||
int fstat64(int fd, struct stat64 *st) { return fstat(fd, (struct stat *)st); }
|
||||
|
||||
@@ -989,7 +1014,7 @@ static long fp_access(int dirfd, const char *path, int mode, int flags) {
|
||||
static int access_common(int dirfd, const char *path, int mode, int flags) {
|
||||
REAL(faccessat, int (*)(int, const char *, int, int));
|
||||
long rs = fp_access(dirfd, path, mode, flags);
|
||||
if (rs != FP_SLOW) { hits++; return ret(rs); }
|
||||
if (answered(rs)) { hits++; return ret(rs); }
|
||||
missed(__func__, dirfd, path);
|
||||
if (flags == 0) return (int)syscall(SYS_faccessat, dirfd, path, mode);
|
||||
return real_faccessat(dirfd, path, mode, flags);
|
||||
@@ -1087,7 +1112,7 @@ char *canonicalize_file_name(const char *path) { return realpath(path, NULL); }
|
||||
char *getcwd(char *buf, size_t size) {
|
||||
REAL(getcwd, char *(*)(char *, size_t));
|
||||
char g[PATH_MAX];
|
||||
if (!fp_ready() || own_cwd(g) == FP_SLOW) { missed(__func__, AT_FDCWD, "."); return real_getcwd(buf, size); }
|
||||
if (!fp_ready() || own_cwd(g) == FP_SLOW || refused()) { missed(__func__, AT_FDCWD, "."); return real_getcwd(buf, size); }
|
||||
hits++;
|
||||
size_t need = strlen(g) + 1;
|
||||
if (buf && size == 0) { errno = EINVAL; return NULL; }
|
||||
@@ -1181,7 +1206,7 @@ static long fp_rename(int olddirfd, const char *old, int newdirfd, const char *n
|
||||
static int rename_common(int olddirfd, const char *old, int newdirfd, const char *new, unsigned flags) {
|
||||
REAL(renameat2, int (*)(int, const char *, int, const char *, unsigned));
|
||||
long rs = fp_rename(olddirfd, old, newdirfd, new, flags);
|
||||
if (rs != FP_SLOW) { hits++; return ret(rs); }
|
||||
if (answered(rs)) { hits++; return ret(rs); }
|
||||
missed(__func__, olddirfd, old);
|
||||
if (!flags) return (int)syscall(SYS_renameat, olddirfd, old, newdirfd, new);
|
||||
return real_renameat2(olddirfd, old, newdirfd, new, flags);
|
||||
@@ -1355,7 +1380,7 @@ static long fp_getxattr(const char *path, const char *name, void *value, size_t
|
||||
static ssize_t getxattr_common(const char *path, const char *name, void *value, size_t size, int follow,
|
||||
ssize_t (*real)(const char *, const char *, void *, size_t)) {
|
||||
long rs = fp_getxattr(path, name, value, size, follow);
|
||||
if (rs != FP_SLOW) { hits++; return rs < 0 ? ret(rs) : rs; }
|
||||
if (answered(rs)) { hits++; return rs < 0 ? ret(rs) : rs; }
|
||||
missed(__func__, AT_FDCWD, path);
|
||||
return real(path, name, value, size);
|
||||
}
|
||||
@@ -1378,7 +1403,7 @@ static long fp_listxattr(const char *path, char *list, size_t size, int follow)
|
||||
static ssize_t listxattr_common(const char *path, char *list, size_t size, int follow,
|
||||
ssize_t (*real)(const char *, char *, size_t)) {
|
||||
long rs = fp_listxattr(path, list, size, follow);
|
||||
if (rs != FP_SLOW) { hits++; return rs < 0 ? ret(rs) : rs; }
|
||||
if (answered(rs)) { hits++; return rs < 0 ? ret(rs) : rs; }
|
||||
missed(__func__, AT_FDCWD, path);
|
||||
return real(path, list, size);
|
||||
}
|
||||
@@ -1581,6 +1606,7 @@ static int accept_common(int fd, struct sockaddr *addr, socklen_t *len, int flag
|
||||
struct sockaddr_storage got;
|
||||
socklen_t got_len = sizeof got;
|
||||
long r = sc(SYS_accept4, fd, addr ? (long)&got : 0, addr ? (long)&got_len : 0, flags, 0);
|
||||
if (refused()) return real_accept4(fd, addr, len, flags);
|
||||
if (r >= 0) inherit_long_name(fd, (int)r);
|
||||
if (!addr) return ret(r);
|
||||
if (r >= 0) report_address((int)r, LONG_PEER, &got, got_len, addr, len);
|
||||
@@ -1595,6 +1621,7 @@ static int name_common(long nr, int fd, struct sockaddr *addr, socklen_t *len, i
|
||||
struct sockaddr_storage got;
|
||||
socklen_t got_len = sizeof got;
|
||||
long r = sc(nr, fd, (long)&got, (long)&got_len, 0, 0);
|
||||
if (refused()) return real(fd, addr, len);
|
||||
if (r == 0) report_address(fd, nr == SYS_getsockname ? LONG_SELF : LONG_PEER, &got, got_len, addr, len);
|
||||
return ret(r);
|
||||
}
|
||||
@@ -1687,7 +1714,7 @@ static int online_cpus(void) {
|
||||
int get_nprocs(void) {
|
||||
REAL(get_nprocs, int (*)(void));
|
||||
int n = fp_ready() ? online_cpus() : -1;
|
||||
if (n > 0) { hits++; return n; }
|
||||
if (n > 0 && !refused()) { hits++; return n; }
|
||||
missed(__func__, AT_FDCWD, "(nprocs)");
|
||||
return real_get_nprocs();
|
||||
}
|
||||
@@ -1743,6 +1770,7 @@ int isatty(int fd) {
|
||||
if (!fp_ready()) return real_isatty(fd);
|
||||
hits++;
|
||||
long r = sc(SYS_ioctl, fd, TCGETS, (long)&k, 0, 0);
|
||||
if (refused()) return real_isatty(fd);
|
||||
if (r < 0) { errno = (int)-r; return 0; }
|
||||
return r == 0;
|
||||
}
|
||||
|
||||
@@ -74,11 +74,14 @@ long fp_svc_dispatch(const long *r) {
|
||||
int saved_errno = errno;
|
||||
long result = svc_blocked(r);
|
||||
uintptr_t frame = (uintptr_t)r;
|
||||
if (result == FP_SLOW && (!svc_busy || frame >= svc_busy)) {
|
||||
if (result == FP_SLOW && __atomic_load_n(&fp_on, __ATOMIC_RELAXED) && (!svc_busy || frame >= svc_busy)) {
|
||||
uintptr_t previous = svc_busy;
|
||||
svc_busy = frame;
|
||||
if (svc_stack_room(frame)) { result = svc_path_call(r); svc_busy = 0; }
|
||||
else svc_busy = previous;
|
||||
if (svc_stack_room(frame)) {
|
||||
result = svc_path_call(r);
|
||||
if (refused()) result = FP_SLOW;
|
||||
svc_busy = 0;
|
||||
} else svc_busy = previous;
|
||||
}
|
||||
if (result == FP_SLOW) {
|
||||
__atomic_fetch_add(&svc_slow, 1, __ATOMIC_RELAXED);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Kurt Himebauch
|
||||
Date: Mon, 9 Nov 2026 15:30:00 -0500
|
||||
Subject: [PATCH] seccomp: hand a guest filter's own SIGSYS back to the guest
|
||||
|
||||
---
|
||||
--- a/src/tracee/seccomp.h
|
||||
+++ b/src/tracee/seccomp.h
|
||||
@@ -1,3 +1,4 @@
|
||||
+#include <signal.h>
|
||||
#include "tracee/tracee.h"
|
||||
#include "sys/vfs.h"
|
||||
|
||||
@@ -18,5 +19,5 @@
|
||||
|
||||
void restart_syscall_after_seccomp(Tracee* tracee);
|
||||
void set_result_after_seccomp(Tracee *tracee, word_t result);
|
||||
-int handle_seccomp_event(Tracee* tracee);
|
||||
+int handle_seccomp_event(Tracee* tracee, const siginfo_t *siginfo);
|
||||
void fix_and_restart_enosys_syscall(Tracee* tracee);
|
||||
--- a/src/tracee/seccomp.c
|
||||
+++ b/src/tracee/seccomp.c
|
||||
@@ -68,7 +68,7 @@
|
||||
*
|
||||
* Return 0 to swallow signal or SIGSYS to deliver it to process.
|
||||
*/
|
||||
-int handle_seccomp_event(Tracee* tracee)
|
||||
+int handle_seccomp_event(Tracee* tracee, const siginfo_t *siginfo)
|
||||
{
|
||||
int ret;
|
||||
|
||||
@@ -100,6 +100,16 @@
|
||||
|
||||
print_current_regs(tracee, 3, "seccomp SIGSYS");
|
||||
|
||||
+ /* Android's app policy traps with SECCOMP_RET_DATA 0. A filter the guest
|
||||
+ * installed itself (Firefox's and Chromium's sandboxes) tags its traps, and
|
||||
+ * its own SIGSYS handler answers them, usually through a broker process.
|
||||
+ * A trap from the fast path's trampoline still gets ENOSYS: its arguments
|
||||
+ * are host paths, and the fast path repeats the call through libc. */
|
||||
+ if (siginfo->si_errno != 0 && !from_fastpath_stub(tracee)) {
|
||||
+ VERBOSE(tracee, 4, "SIGSYS from the guest's own seccomp filter (data %d)", siginfo->si_errno);
|
||||
+ return SIGSYS;
|
||||
+ }
|
||||
+
|
||||
return handle_seccomp_event_common(tracee);
|
||||
}
|
||||
|
||||
--- a/src/tracee/event.c
|
||||
+++ b/src/tracee/event.c
|
||||
@@ -676,7 +676,7 @@
|
||||
tracee->skip_next_seccomp_signal = false;
|
||||
signal = 0;
|
||||
} else {
|
||||
- signal = handle_seccomp_event(tracee);
|
||||
+ signal = handle_seccomp_event(tracee, &siginfo);
|
||||
}
|
||||
} else {
|
||||
VERBOSE(tracee, 1, "non-seccomp SIGSYS");
|
||||
@@ -457,6 +457,26 @@ class AgentTest(unittest.TestCase):
|
||||
self.assertEqual(1, agent.main(["--steam-shortcut-state", game_id]))
|
||||
self.assertEqual(2, agent.main(["--steam-shortcut-state"]))
|
||||
|
||||
def test_steam_ready_uses_steams_initialized_store(self):
|
||||
with patch.object(agent, "cdp_eval", return_value={"value": True}) as evaluate:
|
||||
self.assertTrue(agent.steam_ready(self.dir))
|
||||
self.assertIn("m_bIsInitialized", evaluate.call_args.args[1])
|
||||
with patch.object(agent, "cdp_eval", return_value={"value": False}):
|
||||
self.assertFalse(agent.steam_ready(self.dir))
|
||||
|
||||
def test_pending_interstitial_is_continued_by_steam(self):
|
||||
with patch.object(agent, "cdp_eval", return_value={"value": True}) as evaluate:
|
||||
self.assertTrue(agent.steam_clear_pending_interstitial(self.dir, "3551182975"))
|
||||
expression = evaluate.call_args.args[1]
|
||||
self.assertIn("ContinueGameAction", expression)
|
||||
self.assertIn("ShowInterstitials", expression)
|
||||
self.assertIn('"3551182975"', expression)
|
||||
self.assertIn("await apps.GetActiveGameActions()", expression)
|
||||
with patch.object(agent, "cdp_eval", return_value={"value": False}):
|
||||
self.assertFalse(agent.steam_clear_pending_interstitial(self.dir, "3551182975"))
|
||||
with self.assertRaises(agent.AgentError):
|
||||
agent.steam_clear_pending_interstitial(self.dir, "0")
|
||||
|
||||
def test_asleep_command_reports_steams_own_sleep_state(self):
|
||||
with patch.dict(os.environ, {"BL_LAUNCH_DIR": str(self.dir.parent)}):
|
||||
for value, code in (("yes", 0), ("no", 5), ("unknown", 1), (None, 1)):
|
||||
|
||||
@@ -328,6 +328,15 @@ class DroiddeckctlTest(FakeAdbTest):
|
||||
self.assertEqual("222", out["displays"][1]["physicalId"])
|
||||
self.assertEqual([1080, 1240], out["displays"][1]["size"])
|
||||
|
||||
def test_displays_named_with_their_unique_id(self):
|
||||
text = DUMPSYS_DISPLAY.replace("mPrimaryDisplayDevice=Screen-2",
|
||||
"mPrimaryDisplayDevice=Screen-2(local:222)")
|
||||
self.configure({}, dumpsys_display=text)
|
||||
code, out = self.run_ctl("screenshot", str(self.dir / "shot.png"), "--display", "4")
|
||||
self.assertEqual(0, code)
|
||||
shot = [c for c in self.calls() if c["args"][:2] == ["exec-out", "screencap"]][0]
|
||||
self.assertEqual(["exec-out", "screencap", "-p", "-d", "222"], shot["args"])
|
||||
|
||||
def test_unknown_display_is_an_error(self):
|
||||
self.configure({}, dumpsys_display=DUMPSYS_DISPLAY)
|
||||
code, out = self.run_ctl("screenshot", str(self.dir / "x.png"), "--display", "9")
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
SOURCE = Path(__file__).resolve().parents[2] / "app/src/main/cpp/fakeinput_steam.cpp"
|
||||
|
||||
# Stands in for an allocator like mimalloc that calls access() while it starts up (Claude Code
|
||||
# probes /sys/devices/system/node/node1 on its first allocation, which comes from fakeinput's
|
||||
# constructor when it is preloaded).
|
||||
PROGRAM = r"""
|
||||
#define _GNU_SOURCE
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern void *__libc_malloc(size_t);
|
||||
static int started;
|
||||
|
||||
void *malloc(size_t size) {
|
||||
if (!started) {
|
||||
started = 1;
|
||||
access("/sys/devices/system/node/node1", R_OK);
|
||||
}
|
||||
return __libc_malloc(size);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
puts("ok");
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
@unittest.skipUnless(sys.platform == "linux" and shutil.which("gcc") and shutil.which("g++"),
|
||||
"needs Linux, gcc and g++")
|
||||
class FakeinputReentryTest(unittest.TestCase):
|
||||
def test_allocator_calling_hooks_during_first_allocation_survives_preload(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
(path / "program.c").write_text(PROGRAM)
|
||||
subprocess.run(["gcc", "-rdynamic", "-o", str(path / "program"), str(path / "program.c")], check=True)
|
||||
subprocess.run(["g++", "-shared", "-fPIC", "-O2", "-Wno-attributes", "-pthread",
|
||||
"-std=c++17", "-static-libstdc++", "-static-libgcc",
|
||||
"-Wl,--exclude-libs,ALL", "-o", str(path / "libfakeinput.so"),
|
||||
str(SOURCE), "-ldl"], check=True)
|
||||
result = subprocess.run([str(path / "program")],
|
||||
env=dict(os.environ, LD_PRELOAD=str(path / "libfakeinput.so")),
|
||||
capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout, "ok\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -41,6 +41,7 @@ class SteamColdLaunchTest(unittest.TestCase):
|
||||
"#!/bin/sh\n"
|
||||
"printf '%s\n' \"$@\" > \"$AGENT_ARGS\"\n"
|
||||
"n=$(cat \"$AGENT_COUNT\" 2>/dev/null || echo 0); n=$((n + 1)); echo $n > \"$AGENT_COUNT\"\n"
|
||||
"[ \"$1\" = --steam-ready ] && exit \"${READY_STATUS:-0}\"\n"
|
||||
"exit \"$AGENT_STATUS\"\n")
|
||||
agent.chmod(0o755)
|
||||
sleep = self.bin / "sleep"
|
||||
@@ -55,12 +56,13 @@ class SteamColdLaunchTest(unittest.TestCase):
|
||||
pgrep.chmod(0o755)
|
||||
self.block = LAUNCH_BLOCK.replace("/usr/local/bin/droiddeck-agent", str(agent))
|
||||
|
||||
def run_launch(self, target, focus="769", helper_status=0, running_after=999):
|
||||
def run_launch(self, target, focus="769", helper_status=0, running_after=999, ready_status=0):
|
||||
env = dict(os.environ, HOME=str(self.root), BL_LAUNCH_DIR=str(self.launch_dir),
|
||||
PATH=str(self.bin) + os.pathsep + os.environ["PATH"],
|
||||
AGENT_ARGS=str(self.agent_args), SLEEP_ARGS=str(self.sleep_args),
|
||||
AGENT_COUNT=str(self.agent_count), AGENT_STATUS=str(helper_status),
|
||||
FOCUS=focus, FOCUS_COUNT=str(self.focus_count), RUNNING_AFTER=str(running_after))
|
||||
FOCUS=focus, FOCUS_COUNT=str(self.focus_count), RUNNING_AFTER=str(running_after),
|
||||
READY_STATUS=str(ready_status))
|
||||
script = self.block + "\nprintf 'ARG:%s\n' \"$@\"\nwait\n"
|
||||
url = target if target.startswith("steam://") else "steam://rungameid/" + target
|
||||
result = subprocess.run(["bash", "-c", script, "cold-launch", url],
|
||||
@@ -72,28 +74,40 @@ class SteamColdLaunchTest(unittest.TestCase):
|
||||
return [l for l in result.stdout.splitlines() if "launching the requested game" in l]
|
||||
|
||||
def test_a_shortcut_that_takes_focus_is_handed_over_once_without_waiting_for_the_overview(self):
|
||||
result = self.run_launch(SHORTCUT, focus="769 769 4242")
|
||||
result = self.run_launch(SHORTCUT, focus="769 4242")
|
||||
self.assertEqual(SHORTCUT, (self.launch_dir / "steam-game").read_text().strip())
|
||||
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
|
||||
self.assertFalse(self.agent_args.exists(), result.stdout)
|
||||
self.assertEqual(["1"], self.sleep_args.read_text().split())
|
||||
self.assertEqual(["--steam-ready"], self.agent_args.read_text().splitlines())
|
||||
self.assertFalse(self.sleep_args.exists())
|
||||
|
||||
def test_a_shortcut_that_never_takes_focus_is_handed_over_again_after_the_overview_wait(self):
|
||||
result = self.run_launch(SHORTCUT, focus="769", helper_status=0)
|
||||
self.assertEqual(2, len(self.hand_offs(result)), result.stdout)
|
||||
self.assertIn("launching the requested game again", result.stdout)
|
||||
self.assertEqual(["--wait-steam-shortcut-overview", SHORTCUT, "45"], self.agent_args.read_text().splitlines())
|
||||
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
|
||||
self.assertIn("clearing the pending launch interstitial", result.stdout)
|
||||
self.assertEqual(["--steam-clear-pending-interstitial", "3551182975"], self.agent_args.read_text().splitlines())
|
||||
self.assertEqual(["1"] * 20, self.sleep_args.read_text().split())
|
||||
|
||||
def test_a_timed_out_overview_still_gets_one_more_hand_off(self):
|
||||
result = self.run_launch(SHORTCUT, focus="769", helper_status=1)
|
||||
self.assertEqual(2, len(self.hand_offs(result)), result.stdout)
|
||||
self.assertIn("overview status 1", result.stdout)
|
||||
self.assertIn("launching the requested game again", result.stdout)
|
||||
|
||||
def test_a_steam_that_never_reports_ready_still_gets_the_launch(self):
|
||||
result = self.run_launch("413150", ready_status=1)
|
||||
self.assertEqual("413150", (self.launch_dir / "steam-game").read_text().strip())
|
||||
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
|
||||
self.assertEqual(["1"] * 30, self.sleep_args.read_text().split())
|
||||
|
||||
def test_a_focus_change_does_not_end_the_readiness_wait(self):
|
||||
result = self.run_launch("413150", focus="769 4242", ready_status=1)
|
||||
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
|
||||
self.assertEqual(["1"] * 30, self.sleep_args.read_text().split())
|
||||
|
||||
def test_a_game_still_starting_without_a_window_is_not_launched_twice(self):
|
||||
result = self.run_launch(SHORTCUT, running_after=1)
|
||||
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
|
||||
self.assertFalse(self.agent_args.exists())
|
||||
self.assertIn("--steam-ready", self.agent_args.read_text())
|
||||
self.assertEqual(["-f", "reaper SteamLaunch AppId=3551182975( |$)"],
|
||||
(self.root / "pgrep-args").read_text().splitlines())
|
||||
self.assertIn("already starting", result.stdout)
|
||||
@@ -108,15 +122,15 @@ class SteamColdLaunchTest(unittest.TestCase):
|
||||
focus = " ".join(["769"] * 23) + " 4242"
|
||||
result = self.run_launch(SHORTCUT, focus=focus, helper_status=0)
|
||||
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
|
||||
self.assertIn("took focus while waiting", result.stdout)
|
||||
self.assertNotIn("clearing the pending launch interstitial", result.stdout)
|
||||
self.assertNotIn("launching the requested game again", result.stdout)
|
||||
|
||||
def test_native_app_id_keeps_the_short_delay_and_is_handed_over_once(self):
|
||||
result = self.run_launch("413150")
|
||||
self.assertEqual("413150", (self.launch_dir / "steam-game").read_text().strip())
|
||||
self.assertEqual(1, len(self.hand_offs(result)), result.stdout)
|
||||
self.assertEqual("4", self.sleep_args.read_text().strip())
|
||||
self.assertFalse(self.agent_args.exists())
|
||||
self.assertFalse(self.sleep_args.exists())
|
||||
self.assertIn("--steam-ready", self.agent_args.read_text())
|
||||
|
||||
def test_a_restart_for_a_game_uses_the_same_hand_off(self):
|
||||
loop = SOURCE[SOURCE.index(' if [ -s "$HOME/.bl-restart-game" ]; then'):SOURCE.index(' if [ -f "$HOME/.bl-restart-once" ]; then')]
|
||||
|
||||
Reference in new issue
Block a user