mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Prune obsolete PowerVR runtimes after guest processes exit
This commit is contained in:
5 files changed
+180
No files matched your search
@@ -152,6 +152,28 @@ public final class SystemVulkanDriver {
|
||||
return new File(new File(context.getFilesDir(), "system_vulkan"), version);
|
||||
}
|
||||
|
||||
/** Collect abandoned stages, and obsolete versions once older guests can no longer load them. */
|
||||
public static synchronized void pruneUnused(Context context, boolean guestsStopped) throws IOException {
|
||||
pruneUnused(runtimeDirectory(context), guestsStopped);
|
||||
}
|
||||
|
||||
static synchronized void pruneUnused(File current, boolean guestsStopped) {
|
||||
File base = current.getParentFile();
|
||||
if (base == null || Files.isSymbolicLink(base.toPath()) || Files.isSymbolicLink(current.toPath())) return;
|
||||
File[] entries = base.listFiles();
|
||||
if (entries == null) return;
|
||||
// Retain the previous good version if publishing the replacement failed.
|
||||
boolean pruneVersions = guestsStopped && complete(current);
|
||||
for (File entry : entries) {
|
||||
if (entry.equals(current)) continue;
|
||||
String name = entry.getName();
|
||||
boolean oldVersion = pruneVersions && name.matches("[0-9a-f]{64}");
|
||||
boolean abandonedStage = name.matches("\\.stage-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}");
|
||||
// The prepare monitor excludes any extraction still writing a .stage directory.
|
||||
if (oldVersion || abandonedStage) FileUtils.delete(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean complete(File directory) {
|
||||
if (!new File(directory, "icd.json").isFile()) return false;
|
||||
for (String name : REQUIRED) if (!present(directory, name)) return false;
|
||||
|
||||
@@ -27,6 +27,23 @@ object OrphanReaper {
|
||||
|
||||
fun release(pid: Int) { kept.remove(pid) }
|
||||
|
||||
/** Signal 0 checks ownership/liveness without sending a signal, even when /proc is unreadable. */
|
||||
fun hasOtherProcesses(): Boolean = hasOtherProcesses(
|
||||
File("/proc").list()?.mapNotNull { it.toIntOrNull() }, android.os.Process.myPid(),
|
||||
) { pid ->
|
||||
try {
|
||||
android.system.Os.kill(pid, 0)
|
||||
true
|
||||
} catch (error: android.system.ErrnoException) {
|
||||
error.errno != android.system.OsConstants.ESRCH && error.errno != android.system.OsConstants.EPERM
|
||||
} catch (error: SecurityException) {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun hasOtherProcesses(pids: List<Int>?, ownPid: Int, signalable: (Int) -> Boolean): Boolean =
|
||||
pids == null || pids.any { it > 1 && it != ownPid && signalable(it) }
|
||||
|
||||
fun reap(reason: String): Int {
|
||||
val me = android.os.Process.myPid()
|
||||
val uid = android.os.Process.myUid()
|
||||
|
||||
@@ -336,6 +336,7 @@ class SessionService : Service() {
|
||||
try {
|
||||
if (SystemVulkanDriver.usesDefault(SessionPrefs.linuxDriver(this))) {
|
||||
SystemVulkanDriver.prepare(this)
|
||||
pruneSystemVulkanRuntime()
|
||||
}
|
||||
SessionFiles.stage(this, root)
|
||||
} catch (error: Exception) {
|
||||
@@ -1484,6 +1485,12 @@ class SessionService : Service() {
|
||||
}.onFailure { Log.w(TAG, "could not request Steam wake", it) }
|
||||
}
|
||||
|
||||
/** Background-only: preserve old runtimes until all earlier guest processes are gone. */
|
||||
private fun pruneSystemVulkanRuntime() {
|
||||
runCatching { SystemVulkanDriver.pruneUnused(this, !OrphanReaper.hasOtherProcesses()) }
|
||||
.onFailure { Log.w(TAG, "could not prune unused system Vulkan runtimes", it) }
|
||||
}
|
||||
|
||||
private fun finishSessionStop(status: Int, stoppedGen: Int) {
|
||||
mainHandler.post {
|
||||
if (stoppedGen != sessionGen || SessionState.running) {
|
||||
@@ -1599,9 +1606,11 @@ class SessionService : Service() {
|
||||
auxiliary.forEach { (pid, started) -> teardown(pid, started) }
|
||||
if (prootPid > 1) teardown(prootPid)
|
||||
killGuestLeftovers()
|
||||
pruneSystemVulkanRuntime()
|
||||
finishSessionStop(status, stoppedGen)
|
||||
}, "session-teardown").start()
|
||||
} else {
|
||||
Thread({ pruneSystemVulkanRuntime() }, "system-vulkan-prune").start()
|
||||
finishSessionStop(status, stoppedGen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.droiddeck.launcher.gpu
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
|
||||
class SystemVulkanPruningTest {
|
||||
@get:Rule val temporary = TemporaryFolder()
|
||||
|
||||
@Test fun completedReplacementCollectsOnlyManagedOldDirectories() {
|
||||
val base = temporary.newFolder("system_vulkan")
|
||||
val current = completeRuntime(base, "a".repeat(64))
|
||||
val old = completeRuntime(base, "b".repeat(64))
|
||||
val stage = abandonedStage(base)
|
||||
val unrelated = File(base, "notes").apply { mkdir(); File(this, "keep").writeText("keep") }
|
||||
|
||||
SystemVulkanDriver.pruneUnused(current, true)
|
||||
|
||||
assertEquals("current ICD", File(current, "icd.json").readText())
|
||||
assertTrue(File(current, "run-proton.py").canExecute())
|
||||
assertFalse(old.exists())
|
||||
assertFalse(stage.exists())
|
||||
assertEquals("keep", File(unrelated, "keep").readText())
|
||||
}
|
||||
|
||||
@Test fun aSurvivingGuestKeepsOldVersionsButNotAbandonedStages() {
|
||||
val base = temporary.newFolder("system_vulkan")
|
||||
val current = completeRuntime(base, "a".repeat(64))
|
||||
val old = completeRuntime(base, "b".repeat(64))
|
||||
val stage = abandonedStage(base)
|
||||
|
||||
SystemVulkanDriver.pruneUnused(current, false)
|
||||
|
||||
assertTrue(current.isDirectory)
|
||||
assertTrue(old.isDirectory)
|
||||
assertFalse(stage.exists())
|
||||
SystemVulkanDriver.pruneUnused(current, true)
|
||||
assertFalse("collect the old version once guests stop", old.exists())
|
||||
}
|
||||
|
||||
@Test fun failedReplacementPreservesThePreviousGoodRuntime() {
|
||||
val base = temporary.newFolder("system_vulkan")
|
||||
val current = File(base, "a".repeat(64))
|
||||
val old = completeRuntime(base, "b".repeat(64))
|
||||
val stage = abandonedStage(base)
|
||||
|
||||
SystemVulkanDriver.pruneUnused(current, true)
|
||||
assertTrue("missing replacement", old.isDirectory)
|
||||
assertFalse(stage.exists())
|
||||
|
||||
current.mkdir()
|
||||
File(current, "icd.json").writeText("incomplete")
|
||||
SystemVulkanDriver.pruneUnused(current, true)
|
||||
assertEquals("current ICD", File(old, "icd.json").readText())
|
||||
}
|
||||
|
||||
@Test fun deletingOldRuntimeDoesNotFollowItsSymlinks() {
|
||||
val base = temporary.newFolder("system_vulkan")
|
||||
val current = completeRuntime(base, "a".repeat(64))
|
||||
val old = File(base, "b".repeat(64)).apply { mkdir() }
|
||||
val outside = temporary.newFolder("outside")
|
||||
val sentinel = File(outside, "keep").apply { writeText("keep") }
|
||||
Files.createSymbolicLink(File(old, "outside").toPath(), outside.toPath())
|
||||
|
||||
SystemVulkanDriver.pruneUnused(current, true)
|
||||
|
||||
assertFalse(old.exists())
|
||||
assertEquals("keep", sentinel.readText())
|
||||
}
|
||||
|
||||
@Test fun linkedBaseOrCurrentRuntimePreventsCleanup() {
|
||||
val base = temporary.newFolder("system_vulkan")
|
||||
val old = completeRuntime(base, "b".repeat(64))
|
||||
val current = File(base, "a".repeat(64))
|
||||
Files.createSymbolicLink(current.toPath(), old.toPath())
|
||||
SystemVulkanDriver.pruneUnused(current, true)
|
||||
assertTrue(old.isDirectory)
|
||||
|
||||
Files.delete(current.toPath())
|
||||
completeRuntime(base, current.name)
|
||||
val alias = File(temporary.root, "alias")
|
||||
Files.createSymbolicLink(alias.toPath(), base.toPath())
|
||||
SystemVulkanDriver.pruneUnused(File(alias, current.name), true)
|
||||
assertTrue(old.isDirectory)
|
||||
}
|
||||
|
||||
private fun abandonedStage(base: File) =
|
||||
File(base, ".stage-12345678-1234-1234-1234-123456789abc").apply { mkdir(); File(this, "partial").writeText("partial") }
|
||||
|
||||
private fun completeRuntime(base: File, version: String): File {
|
||||
val directory = File(base, version).apply { mkdir() }
|
||||
val components = listOf(
|
||||
"lib/libhybris-vulkan-icd.so.0", "lib/libhybris-common.so.1", "lib/libhybris/linker/q.so",
|
||||
"lib/libVkLayer_hybris_compat.so", "run-proton.py",
|
||||
"wsi/libVkLayer_window_system_integration.so", "wsi/VkLayer_window_system_integration.json",
|
||||
) + listOf("x32", "x64").flatMap { arch ->
|
||||
listOf("d3d8", "d3d9", "d3d10core", "d3d11", "dxgi").map { "dxvk/$arch/$it.dll" }
|
||||
}
|
||||
components.forEach { path -> File(directory, path).apply { parentFile!!.mkdirs(); writeText("component") } }
|
||||
File(directory, "run-proton.py").setExecutable(true)
|
||||
File(directory, "icd.json").writeText("current ICD")
|
||||
return directory
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class OrphanReaperProcessTest {
|
||||
@Test fun unavailableProcessListDefersCleanup() {
|
||||
assertTrue(OrphanReaper.hasOtherProcesses(null, 100) { false })
|
||||
}
|
||||
|
||||
@Test fun theAppAndForeignProcessesDoNotKeepOldRuntimes() {
|
||||
assertFalse(OrphanReaper.hasOtherProcesses(listOf(0, 1, 100, 200), 100) { pid ->
|
||||
check(pid == 200) { "do not probe self or process groups" }
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
@Test fun anyRemainingAppProcessDefersCleanup() {
|
||||
assertTrue(OrphanReaper.hasOtherProcesses(listOf(100, 200, 300), 100) { it == 300 })
|
||||
assertFalse(OrphanReaper.hasOtherProcesses(emptyList(), 100) { true })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user