Merge pull request #50 from Droid-Deck/fix/release-blockers

Fix/release blockers
This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-09-28 10:32:44 -04:00
commit da0775913f
10 files changed
+94 -30

No files matched your search

+3 -2
View File
@@ -79,8 +79,9 @@ jobs:
}
stable=$(package_line "$RUNNER_TEMP/stable/DroidDeck-$stable_tag.apk")
candidate=$(package_line "${apks[0]}")
[[ "$stable" =~ ^com\.droiddeck\.launcher\ [0-9]+$ && "$candidate" == "$stable" ]] || {
echo "::error::PR package/versionCode '$candidate' differs from stable '$stable'"; exit 1;
[[ "$stable" =~ ^com\.droiddeck\.launcher\ [0-9]+$ && "$candidate" =~ ^com\.droiddeck\.launcher\ [0-9]+$ &&
${candidate##* } -ge ${stable##* } ]] || {
echo "::error::PR package/versionCode '$candidate' cannot install over stable '$stable'"; exit 1;
}
current=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")
+4
View File
@@ -35,6 +35,10 @@ jobs:
run: |
set -euo pipefail
[[ $VERSION =~ ^[0-9A-Za-z._-]+$ ]] || { echo "::error::version '$VERSION': letters, digits, . _ - only"; exit 1; }
name=$(sed -n 's/^ *versionName "\(.*\)"$/\1/p' app/build.gradle)
[[ $VERSION == "$name" || $VERSION == "$name"-* ]] || {
echo "::error::version '$VERSION' does not match versionName '$name' in app/build.gradle: bump versionName and versionCode first"; exit 1;
}
runs=$(gh run list -R "$GITHUB_REPOSITORY" --workflow build.yml --branch main --commit "$GITHUB_SHA" \
--status success --json databaseId,event,headSha -L 20)
id=$(jq -r --arg sha "$GITHUB_SHA" \
+2 -2
View File
@@ -63,8 +63,8 @@ android {
// file out of its own data directory, and this app's entire job is to exec a rootfs:
// proot, gamescope, the Steam client and everything it launches all live under files/.
targetSdkVersion 28
versionCode 8
versionName "0.1.7"
versionCode 9
versionName "0.2.0"
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
ndk {
@@ -97,13 +97,11 @@ object HomeApp {
val intent = Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setComponent(ComponentName(app.packageName, app.className))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (displayId == null) {
context.startActivity(intent)
return
}
// Keep the target app's task separate from DroidDeck. Without this, Android can put the
// app on top of the caller's task and move the entire task to the chosen display.
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId)
context.startActivity(intent, options.toBundle())
}
@@ -264,13 +264,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
handleHomeGuideIntent(intent)
updateOnScreenControls()
WaylandCompositor.setFirstFrameListener {
SessionEvents.firstFrame()
runOnUiThread {
loading.visible = false
hud.start()
}
}
WaylandCompositor.setFirstFrameListener(firstFrameListener)
SessionEvents.markReadyIfPossible()
SessionState.endListener = endListener
// A single Back opens the session menu; two quick presses/swipes send the Steam QAM chord.
@@ -1537,6 +1531,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
/** Ours, so a finishing activity never unhooks the one that replaced it. */
private val endListener: (Int) -> Unit = { status -> onSessionEnded(status) }
private val firstFrameListener = Runnable {
SessionEvents.firstFrame()
runOnUiThread {
loading.visible = false
hud.start()
}
}
override fun onDestroy() {
// Deliberately does NOT end the session: this activity can be destroyed while the user is
@@ -1549,7 +1550,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
if (::hud.isInitialized) hud.stop()
padBridge?.stop()
if (SessionState.endListener === endListener) SessionState.endListener = null
WaylandCompositor.setFirstFrameListener(null)
WaylandCompositor.clearFirstFrameListener(firstFrameListener)
super.onDestroy()
}
@@ -210,6 +210,8 @@ public final class LinuxRuntimeInstaller {
// runtime that isInstalled() would happily launch.
File root = LinuxRuntime.rootDir(context);
File staging = new File(root.getParentFile(), LinuxRuntime.DIR + ".new");
File old = new File(root.getParentFile(), LinuxRuntime.DIR + ".old");
recoverInterruptedSwap(root, staging, old);
FileUtils.delete(staging);
if (!staging.mkdirs()) return false;
if (listener != null) listener.onProgress("Extracting", -1);
@@ -219,7 +221,6 @@ public final class LinuxRuntimeInstaller {
}
FileUtils.writeString(new File(staging, VERSION_FILE), release.version);
File old = new File(root.getParentFile(), LinuxRuntime.DIR + ".old");
FileUtils.delete(old);
if (root.isDirectory() && !root.renameTo(old)) {
FileUtils.delete(staging);
@@ -244,6 +245,8 @@ public final class LinuxRuntimeInstaller {
}
if (!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);
return false;
}
@@ -257,6 +260,24 @@ public final class LinuxRuntimeInstaller {
}
}
private static void recoverInterruptedSwap(File root, File staging, File old) {
File target = root.isDirectory() ? root : old;
File stagedHome = new File(staging, USER_DATA);
File home = new File(target, USER_DATA);
if (target.isDirectory() && stagedHome.isDirectory() && (!home.exists() || isEmptyDir(home))) {
FileUtils.delete(home);
if (stagedHome.renameTo(home)) Log.w(TAG, "recovered " + USER_DATA + " from an interrupted update");
}
if (!root.isDirectory() && old.isDirectory() && old.renameTo(root)) {
Log.w(TAG, "restored the previous runtime after an interrupted update");
}
}
private static boolean isEmptyDir(File dir) {
String[] names = dir.list();
return dir.isDirectory() && names != null && names.length == 0;
}
/** Removes the runtime; returns false without touching it while an install is running. */
public static boolean uninstall(Context context) {
synchronized (JOB_LOCK) {
@@ -81,7 +81,7 @@ class PerfHud(context: Context) {
fun stop() {
running = false
WaylandCompositor.setGameListener(null)
WaylandCompositor.clearGameListener(listener)
text = ""
}
@@ -57,6 +57,7 @@ import java.util.Locale
*/
class SessionService : Service() {
private val components = java.util.concurrent.CopyOnWriteArrayList<SessionPart>()
private val stopLock = Any()
private var wakeLock: PowerManager.WakeLock? = null
private var wifiLock: WifiManager.WifiLock? = null
private var sessionPid = -1
@@ -84,7 +85,7 @@ class SessionService : Service() {
}
}
/** Counts sessions this service has started; a process exit from an earlier one is ignored. */
private var sessionGen = 0
@Volatile private var sessionGen = 0
override fun onCreate() {
super.onCreate()
@@ -392,6 +393,11 @@ class SessionService : Service() {
networkLink.attach(this)
networkLink.publish()
components.add(networkLink)
if (gen != sessionGen || !SessionState.running) {
Log.i(TAG, "session stopped while it was starting; not launching it")
if (gen == sessionGen) components.clear()
return
}
components.forEach { it.start() }
val line = command.joinToString(" ") {
@@ -402,7 +408,7 @@ class SessionService : Service() {
// by the teardown a second after the new one has started, and its exit used to arrive
// here as "session ended: 137" and end the NEW session. An exit belongs to the session
// that started it.
sessionPid = HostProcess.start(line, hostEnv.asArray(), root, { status ->
val pid = HostProcess.start(line, hostEnv.asArray(), root, { status ->
if (gen != sessionGen) {
Log.i(TAG, "an earlier session's process ended ($status); the current one carries on")
return@start
@@ -410,12 +416,24 @@ class SessionService : Service() {
Log.i(TAG, "session ended: $status")
stopSession(status ?: -1)
}, null)
Log.i(TAG, "session pid $sessionPid, log ${sessionLog.path}")
if (gen == sessionGen && SessionState.running && sessionPid > 1) {
SessionEvents.guestStarted(sessionPid)
} else if (gen == sessionGen && SessionState.running) {
SessionEvents.record("guest.start_failed", mapOf("pid" to sessionPid))
SessionEvents.fail("GUEST_START_FAILED", "The guest process could not be started", sessionPid)
Log.i(TAG, "session pid $pid, log ${sessionLog.path}")
if (gen != sessionGen || !SessionState.running) {
Log.i(TAG, "session stopped while its guest was starting; taking it down")
if (gen == sessionGen) {
launchWatcher?.stopWatching()
launchWatcher = null
components.reversed().forEach { runCatching { it.stop() } }
components.clear()
}
if (pid > 1) Thread({ teardown(pid) }, "session-teardown").start()
return
}
sessionPid = pid
if (pid > 1) {
SessionEvents.guestStarted(pid)
} else {
SessionEvents.record("guest.start_failed", mapOf("pid" to pid))
SessionEvents.fail("GUEST_START_FAILED", "The guest process could not be started", pid)
stopSession(-1)
return
}
@@ -877,8 +895,12 @@ class SessionService : Service() {
}
}
private fun finishSessionStop(status: Int) {
private fun finishSessionStop(status: Int, stoppedGen: Int) {
mainHandler.post {
if (stoppedGen != sessionGen || SessionState.running) {
Log.i(TAG, "session $stoppedGen finished stopping after a new one started")
return@post
}
SessionState.suspended = false
SessionState.guestPid = -1
releaseLocks()
@@ -897,8 +919,11 @@ class SessionService : Service() {
}
private fun stopSession(status: Int) {
if (!SessionState.running) return
SessionState.running = false
synchronized(stopLock) {
if (!SessionState.running) return
SessionState.running = false
}
val stoppedGen = sessionGen
SessionState.stopRequested = false
SessionEvents.record("guest.exited", mapOf("status" to status))
SessionEvents.transition(SessionPhase.STOPPING, "session.stopping", mapOf("status" to status))
@@ -942,10 +967,10 @@ class SessionService : Service() {
auxiliary.forEach { (pid, started) -> teardown(pid, started) }
if (prootPid > 1) teardown(prootPid)
killGuestLeftovers()
finishSessionStop(status)
finishSessionStop(status, stoppedGen)
}, "session-teardown").start()
} else {
finishSessionStop(status)
finishSessionStop(status, stoppedGen)
}
}
if (prootPid > 1) acquireLocks()
@@ -1127,6 +1152,10 @@ class SessionService : Service() {
}
fun stop(context: Context) {
if (!SessionState.running) {
SessionState.stopRequested = true
return
}
context.startService(Intent(context, SessionService::class.java).setAction(ACTION_STOP))
}
@@ -26,6 +26,10 @@ public final class WaylandCompositor {
* listener must marshal to the UI thread itself. */
public static void setFirstFrameListener(Runnable r) { firstFrameListener = r; }
public static synchronized void clearFirstFrameListener(Runnable r) {
if (firstFrameListener == r) firstFrameListener = null;
}
/** Invoked from native (banner_on_first_frame) on the first present. */
@SuppressWarnings("unused")
static void onFirstFramePresented() {
@@ -52,6 +56,10 @@ public final class WaylandCompositor {
public static void setGameListener(GameListener l) { gameListener = l; }
public static synchronized void clearGameListener(GameListener l) {
if (gameListener == l) gameListener = null;
}
/** Invoked from native (banner_on_game_surface). */
@SuppressWarnings("unused")
static void onGameSurface(String window, String gpuName) {
@@ -591,6 +591,8 @@ BUSCONF
wait "$decky_supervisor" 2>/dev/null || true
echo "== Decky: session supervisor stopped"
fi
_bl_jobs=$(jobs -p)
[ -n "$_bl_jobs" ] && kill $_bl_jobs 2>/dev/null
reap_orphans
fi
exit "$rc"