mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 03:21:08 +00:00
fix: repair Android 36 runtime edge cases
This commit is contained in:
commit
42239e0097
25 files changed
+539
-29
No files matched your search
@@ -207,6 +207,8 @@ android {
|
||||
pickFirsts += ['**/libtermux.so']
|
||||
// Shipped in the proot and tracking packs (tools/packs) and run or loaded from there.
|
||||
excludes += ['**/libproot.so', '**/libproot-loader.so', '**/libopencv_java4.so', '**/libmediapipe_tasks_jni.so']
|
||||
// Shipped in the proot and tracking packs (tools/packs) and run or loaded from there.
|
||||
excludes += ['**/libproot.so', '**/libproot-loader.so', '**/libopencv_java4.so', '**/libmediapipe_tasks_jni.so']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
@@ -19,7 +21,8 @@
|
||||
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
|
||||
@@ -40,6 +43,7 @@
|
||||
android:hardwareAccelerated="true"
|
||||
android:largeHeap="true"
|
||||
android:extractNativeLibs="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:appCategory="game"
|
||||
android:isGame="true">
|
||||
|
||||
@@ -190,13 +194,24 @@
|
||||
<service
|
||||
android:name=".session.SessionService"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false" />
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:stopWithTask="false">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Runs the user's Linux game session (compositor, Steam client, audio) while the app is not on screen" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".runtime.RuntimeInstallService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name=".core.WirelessAdbPairingService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Waits for the wireless debugging pairing code the user enters from the notification" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".stores.download.StoreDownloadService"
|
||||
android:exported="false"
|
||||
|
||||
@@ -412,6 +412,8 @@ class MainActivity : ComponentActivity() {
|
||||
private var logsEnabled by mutableStateOf(true)
|
||||
private var vacDiagnostics by mutableStateOf(false)
|
||||
private var agentCommands by mutableStateOf(false)
|
||||
private var bluetoothAllowed by mutableStateOf<Boolean?>(null)
|
||||
private var allFilesAllowed by mutableStateOf<Boolean?>(null)
|
||||
private var showRoms by mutableStateOf(false)
|
||||
private var homeAppSelected by mutableStateOf(false)
|
||||
private var homeScreenEnabled by mutableStateOf(false)
|
||||
@@ -790,6 +792,7 @@ class MainActivity : ComponentActivity() {
|
||||
offlineAccount = offlineAccount, offline = offline,
|
||||
frameGenLabel = frameGenLabel, romsDir = romsDir, logsEnabled = logsEnabled,
|
||||
vacDiagnostics = vacDiagnostics, agentCommands = agentCommands,
|
||||
bluetoothAllowed = bluetoothAllowed, allFilesAllowed = allFilesAllowed,
|
||||
steamGames = steamGames, steamGamesX64 = steamGamesX64, emulators = emulatorList, running = runningLabel,
|
||||
frameGen = FrameGen.mode(this),
|
||||
lossless = lossless,
|
||||
@@ -950,6 +953,8 @@ class MainActivity : ComponentActivity() {
|
||||
com.droiddeck.launcher.agent.AgentAccess.setCommandsEnabled(this, !com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(this))
|
||||
agentCommands = com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(this)
|
||||
},
|
||||
onBluetoothAllowed = { askBluetooth() },
|
||||
onAllFilesAllowed = { com.droiddeck.launcher.core.AllFilesAccess.openSettings(this) },
|
||||
onShareLogs = {
|
||||
SessionLogShare.prepare(this, { SessionLogShare.latest(this) }) { zip ->
|
||||
if (zip == null) android.widget.Toast.makeText(this, R.string.main_no_logs_yet, android.widget.Toast.LENGTH_LONG).show()
|
||||
@@ -1183,15 +1188,28 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
val wanted = ArrayList<String>()
|
||||
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
|
||||
&& checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
wanted.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !SessionPrefs.bluetoothAsked(this)
|
||||
&& checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
|
||||
wanted.add(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
SessionPrefs.setBluetoothAsked(this)
|
||||
}
|
||||
if (SessionPrefs.micEnabled(this) && !SessionPrefs.micAsked(this)
|
||||
&& checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
wanted.add(Manifest.permission.RECORD_AUDIO)
|
||||
SessionPrefs.setMicAsked(this)
|
||||
}
|
||||
if (wanted.isNotEmpty()) requestPermissions(wanted.toTypedArray(), 1)
|
||||
else if (!com.droiddeck.launcher.core.AllFilesAccess.granted() && !SessionPrefs.allFilesAsked(this)) {
|
||||
Thread({
|
||||
if (com.droiddeck.launcher.core.AllFilesAccess.usesSharedStorage(this)) runOnUiThread {
|
||||
if (com.droiddeck.launcher.core.AllFilesAccess.ask(this)) SessionPrefs.setAllFilesAsked(this)
|
||||
}
|
||||
}, "all-files-access").start()
|
||||
}
|
||||
Thread({ com.droiddeck.launcher.stores.StoreAccounts.encryptAll(this) }, "store-credentials").start()
|
||||
if (!SessionState.running) Thread({
|
||||
com.droiddeck.launcher.session.LogMigration.run(this)
|
||||
@@ -1280,11 +1298,32 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshPermissionRows() {
|
||||
bluetoothAllowed = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED else null
|
||||
allFilesAllowed = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) com.droiddeck.launcher.core.AllFilesAccess.granted() else null
|
||||
}
|
||||
|
||||
private fun askBluetooth() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
|
||||
val permission = Manifest.permission.BLUETOOTH_CONNECT
|
||||
val granted = checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
|
||||
if (granted || (SessionPrefs.bluetoothAsked(this) && !shouldShowRequestPermissionRationale(permission))) {
|
||||
runCatching {
|
||||
startActivity(Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, android.net.Uri.parse("package:$packageName")))
|
||||
}
|
||||
} else {
|
||||
SessionPrefs.setBluetoothAsked(this)
|
||||
requestPermissions(arrayOf(permission), 3)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
orientation = SessionPrefs.orientation(this)
|
||||
requestedOrientation = orientation
|
||||
refreshWifiDiscovery()
|
||||
refreshPermissionRows()
|
||||
com.droiddeck.launcher.stores.StoresState.refresh(this)
|
||||
com.droiddeck.launcher.stores.StoresState.libraryListener = { refreshAddedGames(); refresh() }
|
||||
com.droiddeck.launcher.ui.Motion.refresh(this)
|
||||
|
||||
@@ -109,7 +109,7 @@ public class PulseAudioComponent extends SessionPart {
|
||||
env.add("HOME=" + workingDir);
|
||||
env.add("TMPDIR=" + workingDir);
|
||||
|
||||
String command = workingDir.getAbsolutePath() + "/libpulseaudio.so"
|
||||
String command = HostProcess.LINKER + " " + workingDir.getAbsolutePath() + "/libpulseaudio.so"
|
||||
+ " --system=false --disable-shm=true --fail=false"
|
||||
+ " -n --file=default.pa --daemonize=false --use-pid-file=false --exit-idle-time=-1"
|
||||
+ " --log-level=info";
|
||||
@@ -158,6 +158,7 @@ public class PulseAudioComponent extends SessionPart {
|
||||
try {
|
||||
File modules = new File(workingDir, "modules/arm64");
|
||||
ArrayList<String> cmd = new ArrayList<>();
|
||||
cmd.add(HostProcess.LINKER);
|
||||
cmd.add(pactl.getAbsolutePath());
|
||||
java.util.Collections.addAll(cmd, args);
|
||||
ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||
@@ -211,7 +212,7 @@ public class PulseAudioComponent extends SessionPart {
|
||||
try {
|
||||
File modules = new File(workingDir, "modules/arm64");
|
||||
ProcessBuilder builder = new ProcessBuilder(
|
||||
pactl.getAbsolutePath(), "suspend-sink", "@DEFAULT_SINK@", Boolean.toString(suspended));
|
||||
HostProcess.LINKER, pactl.getAbsolutePath(), "suspend-sink", "@DEFAULT_SINK@", Boolean.toString(suspended));
|
||||
builder.directory(workingDir);
|
||||
builder.redirectErrorStream(true);
|
||||
builder.redirectOutput(new File("/dev/null"));
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.frontend.AddedExes
|
||||
import com.droiddeck.launcher.stores.StoreInstallRoot
|
||||
|
||||
object AllFilesAccess {
|
||||
fun granted(): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || Environment.isExternalStorageManager()
|
||||
|
||||
fun usesSharedStorage(context: Context): Boolean {
|
||||
val own = (listOf(context.filesDir.parentFile) + context.getExternalFilesDirs(null).map { it?.parentFile })
|
||||
.filterNotNull().map { it.absolutePath + "/" }
|
||||
fun shared(path: String) = own.none { (path + "/").startsWith(it) }
|
||||
return AddedExes.list(context).any { shared(it.folder) } ||
|
||||
StoreInstallRoot.roots(context).any { shared(it.absolutePath) }
|
||||
}
|
||||
|
||||
fun ask(activity: Activity): Boolean {
|
||||
if (granted() || activity.isFinishing) return false
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.all_files_title)
|
||||
.setMessage(R.string.all_files_message)
|
||||
.setPositiveButton(R.string.all_files_allow) { _, _ -> openSettings(activity) }
|
||||
.setNegativeButton(R.string.all_files_later, null)
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
|
||||
fun openSettings(activity: Activity) {
|
||||
val forApp = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:" + activity.packageName))
|
||||
try {
|
||||
activity.startActivity(forApp)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
runCatching { activity.startActivity(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,9 +67,11 @@ public final class Downloader {
|
||||
String expectedSha256, java.util.function.Consumer<Float> progress) {
|
||||
Identity identity = loadIdentity(destination, resume, expectedSha256);
|
||||
for (int i = 0; i < sources.size(); i++) {
|
||||
if (Thread.currentThread().isInterrupted()) return false;
|
||||
String source = sources.get(i);
|
||||
if (i > 0) Log.i(TAG, "retrying " + original + " via " + source);
|
||||
Outcome outcome = attempt(source, destination, resume, identity, expectedSha256, progress);
|
||||
if (Thread.currentThread().isInterrupted()) return false;
|
||||
if (outcome == Outcome.DONE) {
|
||||
if (expectedSha256 != null && !expectedSha256.equalsIgnoreCase(Hashes.sha256(destination))) {
|
||||
Log.w(TAG, "checksum mismatch from " + source + "; trying another source");
|
||||
@@ -212,6 +214,7 @@ public final class Downloader {
|
||||
byte[] buffer = new byte[1 << 16];
|
||||
long lastReport = 0;
|
||||
for (int read = in.read(buffer); read > 0; read = in.read(buffer)) {
|
||||
if (Thread.currentThread().isInterrupted()) return Outcome.RETRY;
|
||||
out.write(buffer, 0, read);
|
||||
written += read;
|
||||
if (progress != null && written - lastReport > (1 << 20)) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import java.io.File
|
||||
import java.util.function.Consumer
|
||||
|
||||
object HostProcess {
|
||||
const val LINKER = "/system/bin/linker64"
|
||||
|
||||
private const val TAG = "HostProcess"
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -10,9 +10,10 @@ import com.droiddeck.launcher.session.SessionPrefs
|
||||
object WifiDiscovery {
|
||||
val permissions = arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
|
||||
fun permissionGranted(context: Context): Boolean = permissions.any {
|
||||
context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
fun permissionGranted(context: Context): Boolean =
|
||||
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) else permissions).any {
|
||||
context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
fun locationEnabled(context: Context): Boolean = Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
|
||||
(context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager)?.isLocationEnabled == true
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.droiddeck.launcher.files
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
@@ -22,7 +23,9 @@ class FileManagerActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
requestedOrientation = com.droiddeck.launcher.session.SessionPrefs.orientation(this)
|
||||
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
com.droiddeck.launcher.core.AllFilesAccess.ask(this)
|
||||
} else if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 1)
|
||||
}
|
||||
val initialDir = intent.getStringExtra(EXTRA_START_DIR)?.let(::File)
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
@@ -26,7 +27,9 @@ class FilePickerActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
requestedOrientation = com.droiddeck.launcher.session.SessionPrefs.orientation(this)
|
||||
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
com.droiddeck.launcher.core.AllFilesAccess.ask(this)
|
||||
} else if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -442,9 +442,12 @@ object AddedGames {
|
||||
}
|
||||
}
|
||||
|
||||
fun listingFile(context: Context): File = File(context.filesDir, "session/added-games.json")
|
||||
|
||||
@Synchronized
|
||||
fun writeListing(context: Context, games: List<Game>,
|
||||
extra: List<JSONObject> = listOfNotNull(com.droiddeck.launcher.stores.epic.EpicClient.steamListing(context))): File {
|
||||
val file = File(context.filesDir, "session/added-games.json").apply { parentFile?.mkdirs() }
|
||||
val file = listingFile(context).apply { parentFile?.mkdirs() }
|
||||
val json = StringBuilder("[")
|
||||
games.forEachIndexed { i, g ->
|
||||
if (i > 0) json.append(',')
|
||||
@@ -464,7 +467,13 @@ object AddedGames {
|
||||
if (games.isNotEmpty() || i > 0) json.append(',')
|
||||
json.append(entry.toString())
|
||||
}
|
||||
file.writeText(json.append(']').toString())
|
||||
val staged = File(file.path + ".tmp")
|
||||
staged.writeText(json.append(']').toString())
|
||||
java.nio.file.Files.move(
|
||||
staged.toPath(), file.toPath(),
|
||||
java.nio.file.StandardCopyOption.ATOMIC_MOVE,
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING,
|
||||
)
|
||||
return file
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.droiddeck.launcher.frontend
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import org.json.JSONArray
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object AddedGamesPublisher {
|
||||
private const val TAG = "AddedGamesPublisher"
|
||||
@@ -13,6 +15,7 @@ object AddedGamesPublisher {
|
||||
internal var now = false
|
||||
|
||||
private val worker = Executors.newSingleThreadExecutor { r -> Thread(r, "added-games-publish") }
|
||||
private val retries = Executors.newSingleThreadScheduledExecutor { r -> Thread(r, "added-games-publish-retry") }
|
||||
|
||||
fun later(context: Context, live: () -> Unit = {}) {
|
||||
val app = context.applicationContext
|
||||
@@ -23,6 +26,42 @@ object AddedGamesPublisher {
|
||||
if (now) work.run() else worker.execute(work)
|
||||
}
|
||||
|
||||
fun refreshDuringSession(context: Context) {
|
||||
val app = context.applicationContext
|
||||
worker.execute {
|
||||
try {
|
||||
val before = AddedGames.listingFile(app).takeIf { it.isFile }?.readText().orEmpty()
|
||||
val known = runCatching {
|
||||
val listing = JSONArray(before)
|
||||
List(listing.length()) { listing.getJSONObject(it).optString("exe") }.toSet()
|
||||
}.getOrDefault(emptySet())
|
||||
val games = AddedGames.scan(app)
|
||||
AddedGames.writeListing(app, games)
|
||||
val arch = com.droiddeck.launcher.session.SessionPrefs.activeSteamClientArch(app)
|
||||
LibraryCache.save(app, Library.launchableGames(app, added = games, arch = arch), arch == "x86_64")
|
||||
val fresh = games.filter { it.guestExe !in known }
|
||||
if (fresh.isEmpty()) return@execute
|
||||
Log.i(TAG, "session refresh: ${fresh.size} game(s) not in the listing the session started with")
|
||||
val attempts = java.util.concurrent.atomic.AtomicInteger()
|
||||
val retry = retries.scheduleWithFixedDelay({
|
||||
try {
|
||||
if (com.droiddeck.launcher.stores.SteamLiveShortcuts.sync(app, fresh) { true } ||
|
||||
!com.droiddeck.launcher.session.SessionState.running || attempts.incrementAndGet() >= 36) {
|
||||
throw java.util.concurrent.CancellationException()
|
||||
}
|
||||
} catch (e: java.util.concurrent.CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "session shortcut retry: ${e.message}")
|
||||
}
|
||||
}, 5, 5, TimeUnit.SECONDS)
|
||||
retries.schedule({ retry.cancel(false) }, 180, TimeUnit.SECONDS)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "session refresh: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun republish(context: Context) {
|
||||
val games = AddedGames.scan(context)
|
||||
AddedGames.writeListing(context, games)
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -69,6 +70,7 @@ public final class LinuxRuntimeInstaller {
|
||||
volatile Step step = Step.OTHER;
|
||||
volatile int percent = -1;
|
||||
volatile boolean ok;
|
||||
volatile Thread worker;
|
||||
final boolean removal;
|
||||
boolean maintenance;
|
||||
Job(boolean removal, String stage) { this.removal = removal; this.stage = stage; }
|
||||
@@ -113,6 +115,7 @@ public final class LinuxRuntimeInstaller {
|
||||
owner = running == null;
|
||||
if (owner) { running = new Job(false, context.getString(R.string.user_apps_starting)); removalError = null; }
|
||||
job = running;
|
||||
if (owner) job.worker = Thread.currentThread();
|
||||
if (listener != null) job.listeners.add(listener);
|
||||
}
|
||||
if (!owner) return join(job, listener);
|
||||
@@ -148,6 +151,12 @@ public final class LinuxRuntimeInstaller {
|
||||
synchronized (JOB_LOCK) { return running != null; }
|
||||
}
|
||||
|
||||
public static void cancelActiveOperation() {
|
||||
synchronized (JOB_LOCK) {
|
||||
if (running != null && running.worker != null) running.worker.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isRemoving() {
|
||||
synchronized (JOB_LOCK) { return running != null && running.removal; }
|
||||
}
|
||||
@@ -275,6 +284,7 @@ public final class LinuxRuntimeInstaller {
|
||||
keepPartial = true;
|
||||
return false;
|
||||
}
|
||||
requireNotInterrupted();
|
||||
|
||||
if (listener != null) listener.onProgress(Step.VERIFYING, context.getString(R.string.rtinst_verifying), -1);
|
||||
String actual = Hashes.sha256(archive);
|
||||
@@ -293,6 +303,7 @@ public final class LinuxRuntimeInstaller {
|
||||
RuntimeFileTree.delete(staging, null);
|
||||
return false;
|
||||
}
|
||||
requireNotInterrupted();
|
||||
FileUtils.writeString(new File(staging, VERSION_FILE), release.version);
|
||||
|
||||
if (!discard(old, null)) {
|
||||
@@ -303,6 +314,11 @@ public final class LinuxRuntimeInstaller {
|
||||
RuntimeFileTree.delete(staging, null);
|
||||
return false;
|
||||
}
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
if (old.isDirectory()) old.renameTo(root);
|
||||
RuntimeFileTree.delete(staging, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
RuntimeFileTree.carryHome(old, staging);
|
||||
@@ -314,7 +330,7 @@ public final class LinuxRuntimeInstaller {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!staging.renameTo(root)) {
|
||||
if (Thread.currentThread().isInterrupted() || !staging.renameTo(root)) {
|
||||
File keptTo = new File(staging, USER_DATA);
|
||||
if (keptTo.isDirectory() && old.isDirectory()) keptTo.renameTo(new File(old, USER_DATA));
|
||||
if (old.isDirectory()) old.renameTo(root);
|
||||
@@ -470,6 +486,7 @@ public final class LinuxRuntimeInstaller {
|
||||
TarArchiveInputStream tar = new TarArchiveInputStream(in)) {
|
||||
TarArchiveEntry entry;
|
||||
while ((entry = tar.getNextTarEntry()) != null) {
|
||||
requireNotInterrupted();
|
||||
File file = ArchivePaths.inside(destination, entry.getName());
|
||||
if (file == null) {
|
||||
Log.w(TAG, "skipping entry outside the rootfs: " + entry.getName());
|
||||
@@ -508,7 +525,10 @@ public final class LinuxRuntimeInstaller {
|
||||
try (OutputStream out = new FileOutputStream(part)) {
|
||||
byte[] buffer = new byte[1 << 16];
|
||||
int read;
|
||||
while ((read = tar.read(buffer)) != -1) out.write(buffer, 0, read);
|
||||
while ((read = tar.read(buffer)) != -1) {
|
||||
requireNotInterrupted();
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
if ((entry.getMode() & 0111) != 0) part.setExecutable(true, false);
|
||||
place(part, file);
|
||||
@@ -538,6 +558,10 @@ public final class LinuxRuntimeInstaller {
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireNotInterrupted() throws InterruptedIOException {
|
||||
if (Thread.currentThread().isInterrupted()) throw new InterruptedIOException("Runtime installation was interrupted");
|
||||
}
|
||||
|
||||
static File partFile(File file) {
|
||||
return new File(file.getParentFile(), ".dd-part-" + Integer.toHexString(file.getName().hashCode()));
|
||||
}
|
||||
|
||||
@@ -13,11 +13,21 @@ import com.droiddeck.launcher.MainActivity
|
||||
import com.droiddeck.launcher.R
|
||||
|
||||
class RuntimeInstallService : Service() {
|
||||
@Volatile private var activeWork: Thread? = null
|
||||
@Volatile private var timedOut = false
|
||||
override fun attachBaseContext(newBase: android.content.Context) =
|
||||
super.attachBaseContext(com.droiddeck.launcher.core.AppLanguage.wrap(newBase))
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onTimeout(startId: Int, fgsType: Int) {
|
||||
timedOut = true
|
||||
LinuxRuntimeInstaller.cancelActiveOperation()
|
||||
activeWork?.interrupt()
|
||||
stopForeground(STOP_FOREGROUND_DETACH)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent?.action == ACTION_PACKAGE) return holdPackage(startId)
|
||||
val removal = intent?.action == ACTION_REMOVE
|
||||
@@ -27,7 +37,7 @@ class RuntimeInstallService : Service() {
|
||||
stopSelf(startId)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
Thread({
|
||||
activeWork = Thread({
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
var shown = ""
|
||||
val progress = LinuxRuntimeInstaller.ProgressListener { stage, percent ->
|
||||
@@ -38,10 +48,12 @@ class RuntimeInstallService : Service() {
|
||||
}
|
||||
if (removal) LinuxRuntimeInstaller.attach(progress)
|
||||
else LinuxRuntimeInstaller.install(applicationContext, release!!, progress)
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(true)
|
||||
stopSelf(startId)
|
||||
}, "runtime-install").start()
|
||||
if (!timedOut) {
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(true)
|
||||
stopSelf(startId)
|
||||
}
|
||||
}, "runtime-install").also { it.start() }
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
|
||||
@@ -326,6 +326,18 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("micAsked", true).apply()
|
||||
}
|
||||
|
||||
fun allFilesAsked(context: Context): Boolean = prefs(context).getBoolean("allFilesAsked", false)
|
||||
|
||||
fun setAllFilesAsked(context: Context) {
|
||||
prefs(context).edit().putBoolean("allFilesAsked", true).apply()
|
||||
}
|
||||
|
||||
fun bluetoothAsked(context: Context): Boolean = prefs(context).getBoolean("bluetoothAsked", false)
|
||||
|
||||
fun setBluetoothAsked(context: Context) {
|
||||
prefs(context).edit().putBoolean("bluetoothAsked", true).apply()
|
||||
}
|
||||
|
||||
fun setMicEnabled(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("mic", on).apply()
|
||||
}
|
||||
|
||||
@@ -437,12 +437,19 @@ class SessionService : Service() {
|
||||
.onFailure { Log.w(TAG, "Epic install directory setup failed", it) }
|
||||
}
|
||||
if (steamHere || noSteamGame) {
|
||||
val added = com.droiddeck.launcher.frontend.AddedGames.scan(this)
|
||||
val listing = com.droiddeck.launcher.frontend.AddedGames.writeListing(this, added)
|
||||
val cached = com.droiddeck.launcher.frontend.AddedGames.listingFile(this)
|
||||
val listing = if (steamHere && cached.isFile) {
|
||||
com.droiddeck.launcher.frontend.AddedGamesPublisher.refreshDuringSession(this)
|
||||
Log.i(TAG, "added games: the last listing, refreshed in the background")
|
||||
cached
|
||||
} else {
|
||||
val added = com.droiddeck.launcher.frontend.AddedGames.scan(this)
|
||||
if (added.isNotEmpty()) Log.i(TAG, "added games: " + added.joinToString { "${it.name} (${it.exe.name})" })
|
||||
com.droiddeck.launcher.frontend.AddedGames.writeListing(this, added)
|
||||
}
|
||||
guest.add("BL_ADDED_GAMES=" + listing.path)
|
||||
if (OfflineMode.enabled(this)) guest.add("BL_STEAM_OFFLINE=1")
|
||||
if (com.droiddeck.launcher.gpu.Lossless.owned(this)) guest.add("BL_LOSSLESS_OWNED=1")
|
||||
if (added.isNotEmpty()) Log.i(TAG, "added games: " + added.joinToString { "${it.name} (${it.exe.name})" })
|
||||
}
|
||||
guest.add("BL_LAUNCH_DIR=" + sessionRoot.path)
|
||||
GameStorage.effective(this)?.let { guest.add("BL_LIBRARY_LABEL=" + it.label.replace('"', ' ')) }
|
||||
|
||||
@@ -19,9 +19,10 @@ object SteamLiveShortcuts {
|
||||
private const val EVAL_TIMEOUT_MS = 20_000L
|
||||
private const val THAW_WAIT_MS = 15_000L
|
||||
|
||||
fun sync(context: Context, games: List<AddedGames.Game>, include: (AddedGames.Game) -> Boolean = { it.source != Library.ADDED }) {
|
||||
fun sync(context: Context, games: List<AddedGames.Game>, include: (AddedGames.Game) -> Boolean = { it.source != Library.ADDED }): Boolean {
|
||||
val wanted = games.filter(include)
|
||||
if (wanted.isEmpty() || !clientRunning()) return
|
||||
if (wanted.isEmpty()) return true
|
||||
if (!clientRunning()) return false
|
||||
val list = JSONArray()
|
||||
for (g in wanted) list.put(JSONObject().put("name", g.name).put("exe", g.guestExe).put("dir", g.guestDir).put("appid", g.appId and 0xFFFFFFFFL))
|
||||
val js = """
|
||||
@@ -35,9 +36,10 @@ object SteamLiveShortcuts {
|
||||
return added;
|
||||
})()
|
||||
""".trimIndent()
|
||||
val result = evaluate(context, js) ?: return
|
||||
val result = evaluate(context, js) ?: return false
|
||||
Log.i(TAG, "live shortcuts: $result added")
|
||||
if (result != "0") StoresState.logLine("Steam: $result shortcut(s) added to the running client")
|
||||
return true
|
||||
}
|
||||
|
||||
fun ensure(context: Context, name: String, exe: String, dir: String, compat: Boolean, appId: Long? = null): Long? {
|
||||
|
||||
@@ -70,6 +70,15 @@ class StoreDownloadService : Service() {
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onTimeout(startId: Int, fgsType: Int) {
|
||||
Log.w(TAG, "data sync time used up; pausing downloads")
|
||||
StoresState.latestDownloads.filter { it.state == DownloadState.RUNNING }.forEach {
|
||||
DownloadQueue.pause(it.key)
|
||||
pausedNote(this, it.key)
|
||||
}
|
||||
stopNow()
|
||||
}
|
||||
|
||||
private fun pending(action: String, key: String, code: Int): PendingIntent {
|
||||
val i = Intent(this, StoreDownloadService::class.java).setAction(action).putExtra(EXTRA_KEY, key)
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) PendingIntent.getForegroundService(this, code, i, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
@@ -100,6 +100,8 @@ class FrontEndState(
|
||||
val logsEnabled: Boolean,
|
||||
val vacDiagnostics: Boolean = false,
|
||||
val agentCommands: Boolean = false,
|
||||
val bluetoothAllowed: Boolean? = null,
|
||||
val allFilesAllowed: Boolean? = null,
|
||||
val steamGames: List<Library.SteamGame>,
|
||||
val steamGamesX64: Boolean = false,
|
||||
val emulators: List<Library.Emulator>,
|
||||
@@ -200,6 +202,8 @@ class FrontEndActions(
|
||||
val onLogs: () -> Unit,
|
||||
val onVacDiagnostics: () -> Unit = {},
|
||||
val onAgentCommands: () -> Unit = {},
|
||||
val onBluetoothAllowed: () -> Unit = {},
|
||||
val onAllFilesAllowed: () -> Unit = {},
|
||||
val onShareLogs: () -> Unit = {},
|
||||
val onClearLogs: () -> Unit = {},
|
||||
val onResetSettings: () -> Unit = {},
|
||||
|
||||
@@ -451,6 +451,12 @@ internal fun SetupPanel(
|
||||
ToggleRow(host, "logs", stringResource(R.string.setup_logs), stringResource(R.string.setup_logs_hint), s.logsEnabled) { a.onLogs() }
|
||||
ToggleRow(host, "vacdiag", stringResource(R.string.setup_vac_diag), stringResource(R.string.setup_vac_diag_hint), s.vacDiagnostics) { a.onVacDiagnostics() }
|
||||
ToggleRow(host, "agent-commands", stringResource(R.string.setup_agent_commands), null, s.agentCommands) { a.onAgentCommands() }
|
||||
s.allFilesAllowed?.let { on ->
|
||||
ToggleRow(host, "all-files", stringResource(R.string.setup_all_files), stringResource(R.string.setup_all_files_hint), on) { a.onAllFilesAllowed() }
|
||||
}
|
||||
s.bluetoothAllowed?.let { on ->
|
||||
ToggleRow(host, "bluetooth", stringResource(R.string.setup_bluetooth), stringResource(R.string.setup_bluetooth_hint), on) { a.onBluetoothAllowed() }
|
||||
}
|
||||
ActionRow(stringResource(R.string.setup_latest_logs), stringResource(R.string.drawer_logs_hint), stringResource(R.string.drawer_share_logs), a.onShareLogs,
|
||||
progress = com.droiddeck.launcher.session.SessionLogShare.progress)
|
||||
ActionRow(stringResource(R.string.setup_saved_logs), stringResource(R.string.setup_saved_logs_hint, com.droiddeck.launcher.session.SessionPaths.KEEP_SESSIONS), stringResource(R.string.setup_clear_logs), a.onClearLogs)
|
||||
|
||||
@@ -1792,6 +1792,14 @@
|
||||
<string name="adbpair_waiting_long">Turn on Wireless debugging, then tap “Pair device with pairing code”. Stay in Settings: the code goes in this notification.</string>
|
||||
<string name="adbpair_code_label">6-digit pairing code</string>
|
||||
<string name="adbpair_code_hint">Tap Enter code and type the 6 digits shown in Settings.</string>
|
||||
<string name="setup_all_files">All files access</string>
|
||||
<string name="setup_all_files_hint">Lets DroidDeck see games and programs on your storage and SD card</string>
|
||||
<string name="setup_bluetooth">Bluetooth Steam Controller</string>
|
||||
<string name="setup_bluetooth_hint">Lets DroidDeck find a paired Steam Controller (Nearby devices)</string>
|
||||
<string name="all_files_title">Allow access to your game folders</string>
|
||||
<string name="all_files_message">DroidDeck runs games and programs from your phone\'s storage and SD card. Android only lets it see those files with All files access, which you can turn on for DroidDeck on the next screen.</string>
|
||||
<string name="all_files_allow">Open settings</string>
|
||||
<string name="all_files_later">Not now</string>
|
||||
<string name="adbpair_title_code">Enter the Wi-Fi pairing code</string>
|
||||
<string name="adbpair_title_retry">Try the code again</string>
|
||||
<string name="adbpair_enter_code">Enter code</string>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
sdk=35
|
||||
@@ -119,6 +119,7 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then
|
||||
unset bl_icd_lib
|
||||
fi
|
||||
export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0}
|
||||
rm -rf /tmp/.droiddeck-rx
|
||||
|
||||
start_agent() {
|
||||
[ -n "${BL_LAUNCH_DIR:-}" ] && [ -x /usr/local/bin/droiddeck-agent ] || return 0
|
||||
|
||||
@@ -3,8 +3,15 @@
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/auxv.h>
|
||||
#include <sys/stat.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
|
||||
#if defined(__aarch64__) || defined(NOEXEC_TEST)
|
||||
|
||||
@@ -36,11 +43,145 @@ __attribute__((constructor)) static void noexec_init(void) {
|
||||
real_mprotect = (int (*)(void *, size_t, int))dlsym(RTLD_NEXT, "mprotect");
|
||||
}
|
||||
|
||||
static int private_file_mapping(uintptr_t start, uintptr_t end) {
|
||||
int fd = (int)syscall(SYS_openat, AT_FDCWD, "/proc/self/maps", O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) return 0;
|
||||
char buf[4096], line[1024];
|
||||
size_t used = 0;
|
||||
int found = 0, done = 0;
|
||||
ssize_t n;
|
||||
while (!done && (n = syscall(SYS_read, fd, buf, sizeof buf)) > 0) {
|
||||
for (ssize_t i = 0; i < n && !done; i++) {
|
||||
if (buf[i] != '\n') {
|
||||
if (used < sizeof line - 1) line[used++] = buf[i];
|
||||
continue;
|
||||
}
|
||||
line[used] = 0;
|
||||
used = 0;
|
||||
char *p = line;
|
||||
uintptr_t lo = strtoull(p, &p, 16), hi = strtoull(p + 1, &p, 16);
|
||||
if (start < lo || start >= hi) continue;
|
||||
done = 1;
|
||||
char *perms = p + 1;
|
||||
if (end > hi || perms[3] != 'p') break;
|
||||
char *path = strchr(perms, '/');
|
||||
struct statvfs vfs;
|
||||
found = path && !(statvfs(path, &vfs) == 0 && (vfs.f_flag & ST_NOEXEC));
|
||||
}
|
||||
}
|
||||
syscall(SYS_close, fd);
|
||||
return found;
|
||||
}
|
||||
|
||||
static const char default_rx_dir[] = "/tmp/.droiddeck-rx";
|
||||
|
||||
static const char *rx_directory(void) {
|
||||
#ifdef NOEXEC_TEST
|
||||
const char *directory = getenv("DROIDDECK_NOEXEC_CACHE_DIR");
|
||||
if (directory && *directory) return directory;
|
||||
#endif
|
||||
return default_rx_dir;
|
||||
}
|
||||
|
||||
static uint64_t fnv1a(const unsigned char *p, size_t n) {
|
||||
uint64_t h = 1469598103934665603ULL;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
h ^= p[i];
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static char *put_hex(char *out, uint64_t v) {
|
||||
for (int shift = 60; shift >= 0; shift -= 4) *out++ = "0123456789abcdef"[(v >> shift) & 15];
|
||||
return out;
|
||||
}
|
||||
|
||||
static int same_content(int fd, const unsigned char *p, size_t len) {
|
||||
char buf[16384];
|
||||
for (size_t off = 0; off < len;) {
|
||||
size_t want = len - off < sizeof buf ? len - off : sizeof buf;
|
||||
if (syscall(SYS_pread64, fd, buf, want, off) != (long)want || memcmp(buf, p + off, want)) return 0;
|
||||
off += want;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int write_all(int fd, const unsigned char *p, size_t len) {
|
||||
for (size_t off = 0; off < len;) {
|
||||
long n = syscall(SYS_write, fd, p + off, len - off);
|
||||
if (n <= 0) return -1;
|
||||
off += (size_t)n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int rebake_shared(void *addr, size_t len, int prot) {
|
||||
const char *directory = rx_directory();
|
||||
size_t directory_len = strlen(directory);
|
||||
if (!directory_len || directory_len + 40 >= PATH_MAX) return -1;
|
||||
char path[PATH_MAX], staged[PATH_MAX];
|
||||
char *end = path + directory_len;
|
||||
memcpy(path, directory, directory_len);
|
||||
if (end[-1] != '/') *end++ = '/';
|
||||
*end++ = '/';
|
||||
end = put_hex(end, fnv1a(addr, len));
|
||||
*end++ = '-';
|
||||
end = put_hex(end, len);
|
||||
*end = 0;
|
||||
int fd = (int)syscall(SYS_openat, AT_FDCWD, path, O_RDONLY | O_CLOEXEC);
|
||||
if (fd >= 0 && !same_content(fd, addr, len)) {
|
||||
syscall(SYS_close, fd);
|
||||
return -1;
|
||||
}
|
||||
if (fd < 0) {
|
||||
syscall(SYS_mkdirat, AT_FDCWD, directory, 0700);
|
||||
memcpy(staged, path, (size_t)(end - path));
|
||||
char *tail = put_hex(staged + (end - path), (uint64_t)syscall(SYS_gettid));
|
||||
*tail = 0;
|
||||
int out = (int)syscall(SYS_openat, AT_FDCWD, staged, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600);
|
||||
if (out < 0) return -1;
|
||||
int bad = write_all(out, addr, len);
|
||||
syscall(SYS_close, out);
|
||||
if (bad || syscall(SYS_renameat, AT_FDCWD, staged, AT_FDCWD, path)) {
|
||||
syscall(SYS_unlinkat, AT_FDCWD, staged, 0);
|
||||
return -1;
|
||||
}
|
||||
fd = (int)syscall(SYS_openat, AT_FDCWD, path, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) return -1;
|
||||
}
|
||||
long mapped = syscall(SYS_mmap, addr, len, prot, MAP_PRIVATE | MAP_FIXED, fd, 0);
|
||||
syscall(SYS_close, fd);
|
||||
return mapped == (long)addr ? 0 : -1;
|
||||
}
|
||||
|
||||
static int rebake(void *addr, size_t len, int prot) {
|
||||
size_t page = getauxval(AT_PAGESZ);
|
||||
if (page) len = (len + page - 1) & ~(page - 1);
|
||||
if (real_mprotect(addr, len, PROT_READ)) return -1;
|
||||
if (!rebake_shared(addr, len, prot)) return 0;
|
||||
void *copy = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (copy == MAP_FAILED) return -1;
|
||||
memcpy(copy, addr, len);
|
||||
if (mmap(addr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == MAP_FAILED) {
|
||||
munmap(copy, len);
|
||||
return -1;
|
||||
}
|
||||
memcpy(addr, copy, len);
|
||||
munmap(copy, len);
|
||||
__builtin___clear_cache((char *)addr, (char *)addr + len);
|
||||
return real_mprotect(addr, len, prot);
|
||||
}
|
||||
|
||||
int mprotect(void *addr, size_t len, int prot) {
|
||||
if (!real_mprotect) noexec_init();
|
||||
int result = real_mprotect(addr, len, prot);
|
||||
if (result && errno == EACCES && (prot & PROT_EXEC) && in_fex()) {
|
||||
result = real_mprotect(addr, len, prot & ~PROT_EXEC);
|
||||
if (result && errno == EACCES && (prot & PROT_EXEC)) {
|
||||
if (in_fex()) {
|
||||
result = real_mprotect(addr, len, prot & ~PROT_EXEC);
|
||||
} else if (len && private_file_mapping((uintptr_t)addr, (uintptr_t)addr + len)) {
|
||||
result = rebake(addr, len, prot);
|
||||
}
|
||||
if (result) errno = EACCES;
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -70,5 +70,125 @@ class NoexecPreloadTest(unittest.TestCase):
|
||||
self.assertEqual(refused[1:], ["protect 13", "read -"])
|
||||
|
||||
|
||||
EXECMOD = r"""
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int private_file(void *addr) {
|
||||
FILE *maps = fopen("/proc/self/maps", "r");
|
||||
char line[512];
|
||||
int found = 0;
|
||||
while (maps && fgets(line, sizeof line, maps)) {
|
||||
unsigned long lo, hi;
|
||||
char perms[5];
|
||||
if (sscanf(line, "%lx-%lx %4s", &lo, &hi, perms) != 3) continue;
|
||||
if ((unsigned long)addr >= lo && (unsigned long)addr < hi) {
|
||||
found = perms[3] == 'p' && strchr(line, '/') != NULL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (maps) fclose(maps);
|
||||
return found;
|
||||
}
|
||||
|
||||
int mprotect(void *addr, size_t len, int prot) {
|
||||
if ((prot & PROT_EXEC) && private_file(addr)) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
return (int)syscall(SYS_mprotect, addr, len, prot);
|
||||
}
|
||||
"""
|
||||
|
||||
CALLER = r"""
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int fd = open(argv[1], O_RDWR);
|
||||
unsigned char *code = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
|
||||
#if defined(__aarch64__)
|
||||
static const unsigned int body[] = {0x52800540, 0xd65f03c0};
|
||||
#else
|
||||
static const unsigned char body[] = {0xb8, 0x2a, 0x00, 0x00, 0x00, 0xc3};
|
||||
#endif
|
||||
memcpy(code + 64, body, sizeof body);
|
||||
int result = mprotect(code, 4096, PROT_READ | PROT_EXEC);
|
||||
printf("protect %d\n", result ? errno : 0);
|
||||
__builtin___clear_cache((char *)code, (char *)code + 4096);
|
||||
if (!result) printf("call %d\n", ((int (*)(void))(code + 64))());
|
||||
printf("data %d\n", code[0]);
|
||||
char line[512];
|
||||
FILE *maps = fopen("/proc/self/maps", "r");
|
||||
while (maps && fgets(line, sizeof line, maps)) {
|
||||
unsigned long lo, hi;
|
||||
if (sscanf(line, "%lx-%lx", &lo, &hi) == 2 && (unsigned long)code >= lo && (unsigned long)code < hi)
|
||||
printf("shared %d\n", strstr(line, ".droiddeck-rx") != NULL);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which("cc") and os.uname().sysname == "Linux", "needs cc on Linux")
|
||||
class ExecmodRebakeTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.tmp = Path(tempfile.mkdtemp())
|
||||
cls.lib = cls.tmp / "libnoexec.so"
|
||||
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-fPIC", "-shared", "-pthread", "-DNOEXEC_TEST", "-o", str(cls.lib),
|
||||
str(PRELOAD / "noexec.c"), str(PRELOAD / "fsync.c"), str(PRELOAD / "robust.c"), "-ldl"], check=True)
|
||||
(cls.tmp / "execmod.c").write_text(EXECMOD)
|
||||
cls.execmod = cls.tmp / "libexecmod.so"
|
||||
subprocess.run(["cc", "-O2", "-fPIC", "-shared", "-o", str(cls.execmod), str(cls.tmp / "execmod.c")], check=True)
|
||||
(cls.tmp / "caller.c").write_text(CALLER)
|
||||
cls.caller = cls.tmp / "wine"
|
||||
subprocess.run(["cc", "-O2", "-o", str(cls.caller), str(cls.tmp / "caller.c")], check=True)
|
||||
cls.image = cls.tmp / "ntdll.dll"
|
||||
cls.image.write_bytes(bytes([7]) + bytes(4095))
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
shutil.rmtree(cls.tmp, ignore_errors=True)
|
||||
|
||||
def setUp(self):
|
||||
self.cache = self.tmp / self._testMethodName / "rx"
|
||||
self.cache.mkdir(parents=True)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp / self._testMethodName, ignore_errors=True)
|
||||
|
||||
def run_with(self, *libs):
|
||||
env = dict(os.environ, LD_PRELOAD=":".join(str(lib) for lib in libs),
|
||||
DROIDDECK_NOEXEC_CACHE_DIR=str(self.cache))
|
||||
result = subprocess.run([str(self.caller), str(self.image)], env=env, capture_output=True, text=True, check=True)
|
||||
return result.stdout.split()
|
||||
|
||||
def test_a_refused_relocated_image_runs_from_anonymous_memory(self):
|
||||
self.assertEqual(self.run_with(self.lib, self.execmod), ["protect", "0", "call", "42", "data", "7", "shared", "1"])
|
||||
|
||||
def test_processes_share_one_copy_of_the_code(self):
|
||||
self.run_with(self.lib, self.execmod)
|
||||
self.run_with(self.lib, self.execmod)
|
||||
self.assertEqual(len(os.listdir(self.cache)), 1)
|
||||
|
||||
def test_without_the_preload_the_refusal_stands(self):
|
||||
self.assertEqual(self.run_with(self.execmod), ["protect", "13", "data", "7", "shared", "0"])
|
||||
|
||||
def test_the_file_itself_is_not_changed(self):
|
||||
self.run_with(self.lib, self.execmod)
|
||||
self.assertEqual(self.image.read_bytes()[:65], bytes([7]) + bytes(64))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user