mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fccaf96751 | ||
|
|
b98d5a5e85 |
No files matched your search
@@ -8,6 +8,7 @@ import org.json.JSONObject
|
||||
/** The in-session switches: the HUD and how the on-screen controls decide to appear. */
|
||||
object SessionPrefs {
|
||||
const val SUSPEND_AUTO = "auto"
|
||||
const val SUSPEND_NATIVE = "native"
|
||||
const val SUSPEND_MANUAL = "manual"
|
||||
const val SUSPEND_NEVER = "never"
|
||||
|
||||
@@ -760,11 +761,14 @@ object SessionPrefs {
|
||||
|
||||
fun suspendPolicy(context: Context, mode: String): String =
|
||||
prefs(context).getString("suspendPolicy.${prefMode(mode)}", SUSPEND_MANUAL)
|
||||
?.takeIf { it == SUSPEND_AUTO || it == SUSPEND_MANUAL || it == SUSPEND_NEVER }
|
||||
?.takeIf { it in setOf(SUSPEND_AUTO, SUSPEND_NATIVE, SUSPEND_MANUAL, SUSPEND_NEVER) }
|
||||
// Direct games share Steam's settings but have no Steam client to prepare.
|
||||
?.let { if (it == SUSPEND_NATIVE && mode != SessionService.MODE_STEAM) SUSPEND_AUTO else it }
|
||||
?: SUSPEND_MANUAL
|
||||
|
||||
fun setSuspendPolicy(context: Context, mode: String, policy: String) {
|
||||
val normalized = policy.takeIf { it == SUSPEND_AUTO || it == SUSPEND_MANUAL || it == SUSPEND_NEVER }
|
||||
val normalized = policy.takeIf { it in setOf(SUSPEND_AUTO, SUSPEND_MANUAL, SUSPEND_NEVER) ||
|
||||
(it == SUSPEND_NATIVE && mode == SessionService.MODE_STEAM) }
|
||||
?: SUSPEND_MANUAL
|
||||
prefs(context).edit().putString("suspendPolicy.${prefMode(mode)}", normalized).apply()
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Owns the running session: the proot tree, the audio daemon, the network link and the locks that
|
||||
@@ -77,6 +78,10 @@ class SessionService : Service() {
|
||||
private var manualPauseRequested = false
|
||||
/** An explicit Steam sleep request is independent of the background policy. */
|
||||
private var steamSleepToken: String? = null
|
||||
private var nativeSleepToken: String? = null
|
||||
private var nativeSleepReady = false
|
||||
private var nativeSleepFallback = false
|
||||
private var nativeSleepTimeout: Runnable? = null
|
||||
private var suspendOperationPending = false
|
||||
private var pipTask = false
|
||||
private var suspendAttemptFailed = false
|
||||
@@ -195,6 +200,7 @@ class SessionService : Service() {
|
||||
screenOn = (getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isInteractive ?: true
|
||||
manualPauseRequested = false
|
||||
steamSleepToken = null
|
||||
completeNativeSleep()
|
||||
suspendOperationPending = false
|
||||
suspendAttemptFailed = false
|
||||
suspendController = null
|
||||
@@ -992,7 +998,8 @@ class SessionService : Service() {
|
||||
activityVisible = true
|
||||
screenOn = (getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isInteractive ?: screenOn
|
||||
manualPauseRequested = false
|
||||
if (steamSleepToken == null) requestSteamWake()
|
||||
if (steamSleepToken == null && (nativeSleepToken == null || nativeSleepFallback)) requestSteamWake()
|
||||
completeNativeSleep()
|
||||
completeSteamSleep()
|
||||
suspendAttemptFailed = false
|
||||
updateSuspendPolicy()
|
||||
@@ -1071,11 +1078,28 @@ class SessionService : Service() {
|
||||
|
||||
private fun watchLaunchRequests(dir: File) {
|
||||
launchWatcher?.stopWatching()
|
||||
listOf("steam-sleep", "steam-sleep-state", "steam-sleep-ready", "steam-wake", "steam-wake.tmp").forEach { File(dir, it).delete() }
|
||||
listOf("steam-sleep", "steam-sleep-state", "steam-sleep-ready", "steam-wake", "steam-wake.tmp",
|
||||
"steam-native-sleep", "steam-native-sleep.tmp", "steam-native-ready", "steam-native-ready.tmp"
|
||||
).forEach { File(dir, it).delete() }
|
||||
val gen = sessionGen
|
||||
@Suppress("DEPRECATION")
|
||||
val watcher = object : android.os.FileObserver(dir.path, CLOSE_WRITE or MOVED_TO) {
|
||||
override fun onEvent(event: Int, path: String?) {
|
||||
if (path == "steam-native-ready") {
|
||||
val request = File(dir, path)
|
||||
val token = runCatching { request.readText().trim() }.getOrNull() ?: return
|
||||
request.delete()
|
||||
mainHandler.post {
|
||||
if (gen != sessionGen || !SessionState.running || token != nativeSleepToken) return@post
|
||||
nativeSleepReady = true
|
||||
nativeSleepTimeout?.let { mainHandler.removeCallbacks(it) }
|
||||
nativeSleepTimeout = null
|
||||
writeSteamSleepState(token, "paused")
|
||||
SessionEvents.record("steam.native_sleep_prepared")
|
||||
updateSuspendPolicy()
|
||||
}
|
||||
return
|
||||
}
|
||||
if (path == "steam-sleep") {
|
||||
val request = File(dir, path)
|
||||
val token = runCatching { request.readText().trim() }.getOrNull() ?: return
|
||||
@@ -1143,18 +1167,27 @@ class SessionService : Service() {
|
||||
if (suspendPolicy == SessionPrefs.SUSPEND_MANUAL && (!activityVisible || !screenOn)) {
|
||||
manualPauseRequested = true
|
||||
}
|
||||
val controller = suspendController ?: return
|
||||
val hidden = !activityVisible || !screenOn
|
||||
if (suspendPolicy != SessionPrefs.SUSPEND_NATIVE || !hidden) {
|
||||
completeNativeSleep()
|
||||
} else if (nativeSleepToken == null && steamSleepToken == null &&
|
||||
!SessionState.suspended && !suspendAttemptFailed) {
|
||||
prepareNativeSleep()
|
||||
}
|
||||
val shouldSuspend = steamSleepToken != null || when (suspendPolicy) {
|
||||
SessionPrefs.SUSPEND_AUTO -> !activityVisible || !screenOn
|
||||
SessionPrefs.SUSPEND_NATIVE -> hidden && nativeSleepReady
|
||||
SessionPrefs.SUSPEND_MANUAL -> manualPauseRequested
|
||||
else -> false
|
||||
}
|
||||
val controller = suspendController ?: return
|
||||
if (suspendOperationPending || suspendAttemptFailed || shouldSuspend == SessionState.suspended) return
|
||||
val gen = sessionGen
|
||||
suspendOperationPending = true
|
||||
SessionEvents.record(if (shouldSuspend) "session.suspend_requested" else "session.resume_requested", mapOf(
|
||||
"policy" to suspendPolicy, "activityVisible" to activityVisible,
|
||||
"screenOn" to screenOn, "steamSleep" to (steamSleepToken != null),
|
||||
"nativeSleep" to (nativeSleepToken != null),
|
||||
))
|
||||
if (shouldSuspend) {
|
||||
controller.freeze { success ->
|
||||
@@ -1167,6 +1200,7 @@ class SessionService : Service() {
|
||||
releaseLocks()
|
||||
refreshNotification()
|
||||
} else {
|
||||
completeNativeSleep()
|
||||
completeSteamSleep()
|
||||
suspendAttemptFailed = true
|
||||
Log.w(TAG, "could not confirm that the session stopped")
|
||||
@@ -1212,12 +1246,54 @@ class SessionService : Service() {
|
||||
}
|
||||
|
||||
private fun resumeSteamSleepOnReturn() {
|
||||
if (suspendPolicy == SessionPrefs.SUSPEND_AUTO && activityVisible && screenOn) {
|
||||
if (steamSleepToken == null) requestSteamWake()
|
||||
if (suspendPolicy in setOf(SessionPrefs.SUSPEND_AUTO, SessionPrefs.SUSPEND_NATIVE) && activityVisible && screenOn) {
|
||||
if (steamSleepToken == null && (nativeSleepToken == null || nativeSleepFallback)) requestSteamWake()
|
||||
completeNativeSleep()
|
||||
completeSteamSleep()
|
||||
}
|
||||
}
|
||||
|
||||
/** Let Steam prepare before freezing; retain the wake lock until that freeze completes. */
|
||||
private fun prepareNativeSleep() {
|
||||
val token = UUID.randomUUID().toString().replace("-", "")
|
||||
nativeSleepToken = token
|
||||
nativeSleepReady = false
|
||||
nativeSleepFallback = false
|
||||
val dir = LinuxRuntime.sessionRoot(this)
|
||||
runCatching {
|
||||
val staged = File(dir, "steam-native-sleep.tmp")
|
||||
staged.writeText("$token\n")
|
||||
check(staged.renameTo(File(dir, "steam-native-sleep")))
|
||||
}.onFailure { Log.w(TAG, "could not request Native sleep; using timed pause", it) }
|
||||
SessionEvents.record("steam.native_sleep_requested")
|
||||
val gen = sessionGen
|
||||
val timeout = Runnable {
|
||||
if (gen != sessionGen || !SessionState.running || nativeSleepToken != token) return@Runnable
|
||||
nativeSleepTimeout = null
|
||||
nativeSleepReady = true
|
||||
nativeSleepFallback = true
|
||||
writeSteamSleepState(token, "paused")
|
||||
SessionEvents.record("steam.native_sleep_timeout")
|
||||
updateSuspendPolicy()
|
||||
}
|
||||
nativeSleepTimeout = timeout
|
||||
// A missing or stalled guest helper must not keep the device awake indefinitely.
|
||||
mainHandler.postDelayed(timeout, 10000L)
|
||||
}
|
||||
|
||||
private fun completeNativeSleep() {
|
||||
nativeSleepTimeout?.let { mainHandler.removeCallbacks(it) }
|
||||
nativeSleepTimeout = null
|
||||
val token = nativeSleepToken ?: return
|
||||
writeSteamSleepState(token, "awake")
|
||||
val dir = LinuxRuntime.sessionRoot(this)
|
||||
File(dir, "steam-native-sleep").delete()
|
||||
File(dir, "steam-native-ready").delete()
|
||||
nativeSleepToken = null
|
||||
nativeSleepReady = false
|
||||
nativeSleepFallback = false
|
||||
}
|
||||
|
||||
/** Resume must also recover a Steam sleep whose request never reached Android. */
|
||||
private fun requestSteamWake() {
|
||||
if (!SessionState.running || SessionState.mode != MODE_STEAM) return
|
||||
@@ -1281,6 +1357,7 @@ class SessionService : Service() {
|
||||
|
||||
private fun stopSession(status: Int) {
|
||||
cancelQueuedSteamGame()
|
||||
completeNativeSleep()
|
||||
synchronized(stopLock) {
|
||||
if (!SessionState.running) return
|
||||
SessionState.running = false
|
||||
|
||||
@@ -288,9 +288,9 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
SessionPrefs.SUSPEND_AUTO to stringResource(R.string.common_auto),
|
||||
SessionPrefs.SUSPEND_MANUAL to stringResource(R.string.mode_suspend_manual),
|
||||
SessionPrefs.SUSPEND_NEVER to stringResource(R.string.common_never),
|
||||
),
|
||||
) + if (steam) listOf(SessionPrefs.SUSPEND_NATIVE to stringResource(R.string.mode_suspend_native)) else emptyList(),
|
||||
s.suspendPolicy,
|
||||
note = stringResource(R.string.mode_suspend_note),
|
||||
note = stringResource(if (steam) R.string.mode_suspend_steam_note else R.string.mode_suspend_note),
|
||||
onPick = a.onSuspendPolicy,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -507,9 +507,9 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
|
||||
SessionPrefs.SUSPEND_AUTO to stringResource(R.string.common_auto),
|
||||
SessionPrefs.SUSPEND_MANUAL to stringResource(R.string.mode_suspend_manual),
|
||||
SessionPrefs.SUSPEND_NEVER to stringResource(R.string.common_never),
|
||||
),
|
||||
) + if (a.steam) listOf(SessionPrefs.SUSPEND_NATIVE to stringResource(R.string.mode_suspend_native)) else emptyList(),
|
||||
a.suspendPolicy,
|
||||
note = stringResource(R.string.mode_suspend_note),
|
||||
note = stringResource(if (a.steam) R.string.mode_suspend_steam_note else R.string.mode_suspend_note),
|
||||
chipModifier = focus.track(page, "suspend"),
|
||||
onPick = a.onSuspendPolicy,
|
||||
)
|
||||
|
||||
@@ -263,6 +263,8 @@
|
||||
<string name="mode_suspend">Background behavior</string>
|
||||
<string name="mode_suspend_hint">When the app is hidden or the screen turns off.</string>
|
||||
<string name="mode_suspend_manual">Manual</string>
|
||||
<string name="mode_suspend_native">Native</string>
|
||||
<string name="mode_suspend_steam_note">Auto pauses and resumes when shown. Native prepares Steam for sleep first. Manual waits for Resume. Never keeps running.</string>
|
||||
<string name="mode_suspend_note">Auto pauses in the background and resumes when shown. Manual waits for Resume. Never keeps running.</string>
|
||||
<string name="mode_startup">Startup</string>
|
||||
<string name="mode_steam_startup">Start Steam with DroidDeck</string>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], manifest = Config.NONE)
|
||||
class SessionBackgroundPrefsTest {
|
||||
private val context get() = RuntimeEnvironment.getApplication()
|
||||
|
||||
@Before fun clearSettings() {
|
||||
context.getSharedPreferences("session", Context.MODE_PRIVATE).edit().clear().commit()
|
||||
}
|
||||
|
||||
@Test fun nativeIsOptInAndDoesNotChangeDesktopOrExistingPolicies() {
|
||||
assertEquals(SessionPrefs.SUSPEND_MANUAL, SessionPrefs.suspendPolicy(context, "steam"))
|
||||
for (policy in listOf(SessionPrefs.SUSPEND_AUTO, SessionPrefs.SUSPEND_MANUAL, SessionPrefs.SUSPEND_NEVER,
|
||||
SessionPrefs.SUSPEND_NATIVE)) {
|
||||
SessionPrefs.setSuspendPolicy(context, "steam", policy)
|
||||
assertEquals(policy, SessionPrefs.suspendPolicy(context, "steam"))
|
||||
assertEquals(SessionPrefs.SUSPEND_MANUAL, SessionPrefs.suspendPolicy(context, "desktop"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun directGamesUseAutoWithoutOverwritingSteamsNativeChoice() {
|
||||
SessionPrefs.setSuspendPolicy(context, SessionService.MODE_STEAM, SessionPrefs.SUSPEND_NATIVE)
|
||||
assertEquals(SessionPrefs.SUSPEND_AUTO, SessionPrefs.suspendPolicy(context, SessionService.MODE_RUN))
|
||||
assertEquals(SessionPrefs.SUSPEND_NATIVE, SessionPrefs.suspendPolicy(context, SessionService.MODE_STEAM))
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ must see this service before startup or it can stay black after its idle timer.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
@@ -20,12 +21,22 @@ ROOT = "/org/freedesktop/login1"
|
||||
INTERFACE = NAME + ".Manager"
|
||||
UPOWER = "org.freedesktop.UPower"
|
||||
UPOWER_ROOT = "/org/freedesktop/UPower"
|
||||
NATIVE_HOST_ACK_MS = 12000 # Android falls back after ten seconds; allow its freeze to finish.
|
||||
INTROSPECTION = """
|
||||
<node>
|
||||
<interface name="org.freedesktop.login1.Manager">
|
||||
<method name="Suspend"><arg type="b" direction="in"/></method>
|
||||
<method name="SuspendWithFlags"><arg type="t" direction="in"/></method>
|
||||
<method name="CanSuspend"><arg type="s" direction="out"/></method>
|
||||
<method name="Inhibit">
|
||||
<arg type="s" direction="in"/><arg type="s" direction="in"/>
|
||||
<arg type="s" direction="in"/><arg type="s" direction="in"/>
|
||||
<arg type="h" direction="out"/>
|
||||
</method>
|
||||
<method name="ListInhibitors"><arg type="a(ssssuu)" direction="out"/></method>
|
||||
<property name="BlockInhibited" type="s" access="read"/>
|
||||
<property name="DelayInhibited" type="s" access="read"/>
|
||||
<property name="InhibitDelayMaxUSec" type="t" access="read"/>
|
||||
<property name="PreparingForSleep" type="b" access="read"/>
|
||||
<signal name="PrepareForSleep"><arg type="b"/></signal>
|
||||
</interface>
|
||||
@@ -47,8 +58,17 @@ class LoginManager:
|
||||
self.request = self.directory / "steam-sleep"
|
||||
self.state = self.directory / "steam-sleep-state"
|
||||
self.wake = self.directory / "steam-wake"
|
||||
self.native_request = self.directory / "steam-native-sleep"
|
||||
self.native_ready = self.directory / "steam-native-ready"
|
||||
self.pending = None
|
||||
self.timeout = None
|
||||
self.inhibitors = {}
|
||||
self.next_inhibitor = 0
|
||||
self.native = False
|
||||
self.waiting = set()
|
||||
self.steam_participated = False
|
||||
self.preparation_failed = False
|
||||
self.preparation_flushed = False
|
||||
info = Gio.DBusNodeInfo.new_for_xml(INTROSPECTION).interfaces[0]
|
||||
self.registration = bus.register_object(ROOT, info, self.method, self.property, None)
|
||||
# Steam can issue its suspend command through the legacy UPower service even
|
||||
@@ -61,6 +81,13 @@ class LoginManager:
|
||||
def property(self, connection, sender, path, interface, name):
|
||||
if name == "PreparingForSleep":
|
||||
return GLib.Variant("b", self.pending is not None)
|
||||
if name == "InhibitDelayMaxUSec":
|
||||
return GLib.Variant("t", 10000000)
|
||||
if name in ("BlockInhibited", "DelayInhibited"):
|
||||
mode = "block" if name == "BlockInhibited" else "delay"
|
||||
kinds = {kind for item in self.inhibitors.values() if item[3] == mode
|
||||
for kind in item[0].split(":")}
|
||||
return GLib.Variant("s", ":".join(sorted(kinds)))
|
||||
return None
|
||||
|
||||
def notify(self, sleeping):
|
||||
@@ -74,6 +101,10 @@ class LoginManager:
|
||||
def method(self, connection, sender, path, interface, name, args, reply):
|
||||
if name == "CanSuspend":
|
||||
reply.return_value(GLib.Variant("(s)", ("yes",)))
|
||||
elif name == "Inhibit":
|
||||
self.inhibit(sender, args.unpack(), reply)
|
||||
elif name == "ListInhibitors":
|
||||
reply.return_value(GLib.Variant("(a(ssssuu))", ([item[:6] for item in self.inhibitors.values()],)))
|
||||
elif name in ("Suspend", "SuspendWithFlags"):
|
||||
if self.pending:
|
||||
reply.return_dbus_error(NAME + ".OperationInProgress", "The session is already pausing")
|
||||
@@ -83,29 +114,131 @@ class LoginManager:
|
||||
reply.return_value(None)
|
||||
# Flush the signal and method reply without blocking this D-Bus callback. Once both are
|
||||
# on the wire, publishing the request lets the host safely freeze this service.
|
||||
self.bus.flush(None, self.publish_after_flush, None)
|
||||
self.bus.flush(None, self.publish_after_flush, self.pending)
|
||||
|
||||
def publish_after_flush(self, connection, result, _data):
|
||||
def peer(self, sender, method, signature):
|
||||
return self.bus.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus",
|
||||
"org.freedesktop.DBus", method, GLib.Variant("(s)", (sender,)),
|
||||
GLib.VariantType.new(signature), Gio.DBusCallFlags.NONE, 2000, None).unpack()[0]
|
||||
|
||||
def inhibit(self, sender, args, reply):
|
||||
what, who, why, mode = args
|
||||
kinds = set(what.split(":"))
|
||||
valid = {"sleep", "shutdown", "idle", "handle-power-key", "handle-suspend-key",
|
||||
"handle-hibernate-key", "handle-lid-switch"}
|
||||
if mode not in ("block", "delay") or not kinds <= valid or (mode == "delay" and not kinds <= {"sleep", "shutdown"}):
|
||||
reply.return_dbus_error("org.freedesktop.DBus.Error.InvalidArgs", "Invalid inhibitor type or mode")
|
||||
return
|
||||
try:
|
||||
uid = self.peer(sender, "GetConnectionUnixUser", "(u)")
|
||||
pid = self.peer(sender, "GetConnectionUnixProcessID", "(u)")
|
||||
read_fd, write_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC)
|
||||
descriptors = Gio.UnixFDList.new()
|
||||
try:
|
||||
index = descriptors.append(write_fd)
|
||||
finally:
|
||||
os.close(write_fd)
|
||||
self.next_inhibitor += 1
|
||||
key = self.next_inhibitor
|
||||
self.inhibitors[key] = (what, who, why, mode, uid, pid, sender, read_fd)
|
||||
GLib.io_add_watch(read_fd, GLib.IOCondition.HUP | GLib.IOCondition.ERR,
|
||||
self.release_inhibitor, key)
|
||||
if self.native and mode == "delay" and "sleep" in kinds:
|
||||
self.waiting.add(key)
|
||||
self.steam_participated |= who.lower() == "steam"
|
||||
reply.return_value_with_unix_fd_list(GLib.Variant("(h)", (index,)), descriptors)
|
||||
print("== login1: inhibitor acquired: " + who + " " + what + " " + mode, flush=True)
|
||||
except (GLib.Error, OSError) as error:
|
||||
reply.return_dbus_error(NAME + ".Failed", str(error))
|
||||
|
||||
def release_inhibitor(self, fd, condition, key):
|
||||
item = self.inhibitors.pop(key)
|
||||
os.close(fd)
|
||||
print("== login1: inhibitor released: " + item[1], flush=True)
|
||||
if key in self.waiting:
|
||||
self.waiting.remove(key)
|
||||
# A disconnected/crashed client is not a preparation acknowledgement.
|
||||
try:
|
||||
alive = self.peer(item[6], "NameHasOwner", "(b)")
|
||||
os.kill(item[5], 0)
|
||||
except (GLib.Error, OSError):
|
||||
alive = False
|
||||
self.preparation_failed |= not alive
|
||||
self.finish_preparation()
|
||||
return False
|
||||
|
||||
def publish_after_flush(self, connection, result, token):
|
||||
try:
|
||||
connection.flush_finish(result)
|
||||
except GLib.Error as error:
|
||||
print("== login1: cannot complete suspend handshake: " + str(error), file=sys.stderr)
|
||||
self.resume()
|
||||
return
|
||||
self.publish()
|
||||
if self.pending == token:
|
||||
self.publish(self.request)
|
||||
|
||||
def publish(self):
|
||||
def publish(self, request):
|
||||
self.check_state()
|
||||
if self.pending is None:
|
||||
return False
|
||||
try:
|
||||
staged = self.request.with_suffix(".tmp")
|
||||
staged = request.with_suffix(".tmp")
|
||||
staged.write_text(self.pending + "\n")
|
||||
staged.replace(self.request)
|
||||
staged.replace(request)
|
||||
if self.timeout is not None:
|
||||
GLib.source_remove(self.timeout)
|
||||
self.timeout = GLib.timeout_add_seconds(5, self.check_ack)
|
||||
except OSError as error:
|
||||
print("== login1: cannot request pause: " + str(error), file=sys.stderr)
|
||||
self.resume()
|
||||
return False
|
||||
|
||||
def prepare_native(self):
|
||||
try:
|
||||
token = self.native_request.read_text().strip()
|
||||
self.native_request.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
return
|
||||
if not re.fullmatch(r"[a-f0-9]{32}", token) or self.pending is not None:
|
||||
return
|
||||
# Android may have returned before this helper read its request.
|
||||
if self.read_state() == token + ":awake":
|
||||
return
|
||||
self.pending = token
|
||||
self.native = True
|
||||
self.waiting = {key for key, item in self.inhibitors.items()
|
||||
if item[3] == "delay" and "sleep" in item[0].split(":")}
|
||||
self.steam_participated = any(self.inhibitors[key][1].lower() == "steam" for key in self.waiting)
|
||||
self.preparation_failed = False
|
||||
self.preparation_flushed = False
|
||||
self.timeout = GLib.timeout_add(NATIVE_HOST_ACK_MS, self.check_ack)
|
||||
self.notify(True)
|
||||
self.bus.flush(None, self.prepare_after_flush, token)
|
||||
|
||||
def prepare_after_flush(self, connection, result, token):
|
||||
try:
|
||||
connection.flush_finish(result)
|
||||
except GLib.Error as error:
|
||||
print("== login1: cannot prepare native sleep: " + str(error), file=sys.stderr)
|
||||
self.resume()
|
||||
return
|
||||
self.check_state()
|
||||
if self.pending == token:
|
||||
self.preparation_flushed = True
|
||||
self.finish_preparation()
|
||||
|
||||
def finish_preparation(self):
|
||||
self.check_state()
|
||||
if (self.native and self.pending and self.preparation_flushed and self.steam_participated
|
||||
and not self.waiting and not self.preparation_failed):
|
||||
self.native = False
|
||||
print("== login1: Steam acknowledged sleep preparation", flush=True)
|
||||
self.publish(self.native_ready)
|
||||
return False
|
||||
|
||||
def changed(self, monitor, file, other, event):
|
||||
if file.get_basename() == self.native_request.name or (other and other.get_basename() == self.native_request.name):
|
||||
self.prepare_native()
|
||||
if file.get_basename() == self.wake.name or (other and other.get_basename() == self.wake.name):
|
||||
if self.wake.exists():
|
||||
self.wake.unlink(missing_ok=True)
|
||||
@@ -128,6 +261,7 @@ class LoginManager:
|
||||
if self.pending and self.read_state() != self.pending + ":paused":
|
||||
# A missing host or a failed freeze must not strand Steam in black.
|
||||
self.request.unlink(missing_ok=True)
|
||||
self.native_ready.unlink(missing_ok=True)
|
||||
self.resume()
|
||||
return False
|
||||
|
||||
@@ -140,6 +274,8 @@ class LoginManager:
|
||||
self.bus.flush(None, self.finish_recovery, None)
|
||||
return
|
||||
self.pending = None
|
||||
self.native = False
|
||||
self.waiting.clear()
|
||||
if self.timeout is not None:
|
||||
GLib.source_remove(self.timeout)
|
||||
self.timeout = None
|
||||
|
||||
@@ -134,6 +134,134 @@ class SleepHandshakeTest(unittest.TestCase):
|
||||
staged.write_text("wake\n")
|
||||
staged.replace(self.dir / "steam-wake")
|
||||
|
||||
def request_native(self, token="1" * 32):
|
||||
staged = self.dir / "steam-native-sleep.tmp"
|
||||
staged.write_text(token + "\n")
|
||||
staged.replace(self.dir / "steam-native-sleep")
|
||||
return token
|
||||
|
||||
def inhibitor(self, who="Steam", mode="delay", what="sleep"):
|
||||
result, descriptors = self.bus.call_with_unix_fd_list_sync(
|
||||
NAME, ROOT, IFACE, "Inhibit", GLib.Variant("(ssss)", (what, who, "Preparing sleep", mode)),
|
||||
GLib.VariantType.new("(h)"), Gio.DBusCallFlags.NONE, 2000, None, None)
|
||||
return descriptors.get(result.unpack()[0])
|
||||
|
||||
def test_native_sleep_prepares_before_host_can_freeze(self):
|
||||
descriptor = self.inhibitor()
|
||||
token = self.request_native()
|
||||
self.wait_for(lambda: self.signals == [True])
|
||||
self.assertTrue(self.preparing())
|
||||
self.assertFalse((self.dir / "steam-native-ready").exists())
|
||||
self.assertFalse((self.dir / "steam-sleep").exists())
|
||||
time.sleep(1.7)
|
||||
self.assertFalse((self.dir / "steam-native-ready").exists())
|
||||
os.close(descriptor)
|
||||
self.wait_for(lambda: (self.dir / "steam-native-ready").exists())
|
||||
self.assertEqual((self.dir / "steam-native-ready").read_text().strip(), token)
|
||||
self.state(token, "paused")
|
||||
self.state(token, "awake")
|
||||
self.wait_for(lambda: self.signals == [True, False])
|
||||
self.assertFalse(self.preparing())
|
||||
|
||||
def test_return_during_native_preparation_cancels_late_pause(self):
|
||||
descriptor = self.inhibitor()
|
||||
token = self.request_native()
|
||||
self.wait_for(lambda: self.signals == [True])
|
||||
self.state(token, "awake")
|
||||
self.wait_for(lambda: self.signals == [True, False])
|
||||
os.close(descriptor)
|
||||
time.sleep(1.7)
|
||||
self.assertFalse((self.dir / "steam-native-ready").exists())
|
||||
self.assertFalse(self.preparing())
|
||||
|
||||
def test_native_request_cancelled_before_helper_reads_it(self):
|
||||
os.kill(self.process.pid, signal.SIGSTOP)
|
||||
token = self.request_native()
|
||||
self.state(token, "awake")
|
||||
os.kill(self.process.pid, signal.SIGCONT)
|
||||
self.wait_for(lambda: not (self.dir / "steam-native-sleep").exists())
|
||||
time.sleep(1.7)
|
||||
self.assertFalse((self.dir / "steam-native-ready").exists())
|
||||
self.assertFalse(self.preparing())
|
||||
self.assertEqual(self.signals, [])
|
||||
|
||||
def test_second_native_sleep_cannot_receive_first_preparation_callback(self):
|
||||
stale = self.inhibitor()
|
||||
first = self.request_native()
|
||||
self.wait_for(lambda: self.signals == [True])
|
||||
self.state(first, "awake")
|
||||
self.wait_for(lambda: self.signals == [True, False])
|
||||
current = self.inhibitor()
|
||||
self.signals.clear()
|
||||
second = self.request_native("2" * 32)
|
||||
self.wait_for(lambda: self.signals == [True])
|
||||
os.close(stale)
|
||||
time.sleep(0.1)
|
||||
self.assertFalse((self.dir / "steam-native-ready").exists())
|
||||
os.close(current)
|
||||
self.wait_for(lambda: (self.dir / "steam-native-ready").exists())
|
||||
self.assertEqual((self.dir / "steam-native-ready").read_text().strip(), second)
|
||||
self.state(second, "awake")
|
||||
self.wait_for(lambda: self.signals == [True, False])
|
||||
|
||||
def test_native_sleep_with_missing_host_recovers(self):
|
||||
self.request_native()
|
||||
self.wait_for(lambda: self.signals == [True, False], timeout=14)
|
||||
self.assertFalse(self.preparing())
|
||||
self.assertFalse((self.dir / "steam-native-ready").exists())
|
||||
|
||||
def test_no_steam_acknowledgement_does_not_publish_ready(self):
|
||||
descriptor = self.inhibitor(who="Other client")
|
||||
token = self.request_native()
|
||||
self.wait_for(lambda: self.signals == [True])
|
||||
os.close(descriptor)
|
||||
time.sleep(1.7)
|
||||
self.assertFalse((self.dir / "steam-native-ready").exists())
|
||||
self.state(token, "paused") # host's ten-second fallback
|
||||
self.state(token, "awake")
|
||||
self.wait_for(lambda: self.signals == [True, False])
|
||||
|
||||
def test_block_inhibitor_is_not_a_steam_acknowledgement(self):
|
||||
descriptor = self.inhibitor(mode="block")
|
||||
token = self.request_native()
|
||||
self.wait_for(lambda: self.signals == [True])
|
||||
os.close(descriptor)
|
||||
time.sleep(0.1)
|
||||
self.assertFalse((self.dir / "steam-native-ready").exists())
|
||||
self.state(token, "awake")
|
||||
self.wait_for(lambda: self.signals == [True, False])
|
||||
|
||||
def test_delay_fd_duplicates_must_all_be_closed(self):
|
||||
descriptor = self.inhibitor()
|
||||
duplicate = os.dup(descriptor)
|
||||
token = self.request_native()
|
||||
self.wait_for(lambda: self.signals == [True])
|
||||
os.close(descriptor)
|
||||
time.sleep(0.1)
|
||||
self.assertFalse((self.dir / "steam-native-ready").exists())
|
||||
os.close(duplicate)
|
||||
self.wait_for(lambda: (self.dir / "steam-native-ready").exists())
|
||||
self.state(token, "awake")
|
||||
self.wait_for(lambda: self.signals == [True, False])
|
||||
|
||||
def test_disconnected_steam_client_does_not_confirm_preparation(self):
|
||||
original = self.bus
|
||||
client = Gio.DBusConnection.new_for_address_sync(
|
||||
os.environ["DBUS_SESSION_BUS_ADDRESS"],
|
||||
Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT | Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION,
|
||||
None, None)
|
||||
self.bus = client
|
||||
descriptor = self.inhibitor()
|
||||
self.bus = original
|
||||
token = self.request_native()
|
||||
self.wait_for(lambda: self.signals == [True])
|
||||
client.close_sync(None)
|
||||
os.close(descriptor)
|
||||
time.sleep(0.2)
|
||||
self.assertFalse((self.dir / "steam-native-ready").exists())
|
||||
self.state(token, "awake")
|
||||
self.wait_for(lambda: self.signals == [True, False])
|
||||
|
||||
def test_host_resume_recovers_without_a_sleep_request(self):
|
||||
self.assertFalse(self.preparing())
|
||||
self.request_wake()
|
||||
|
||||
Reference in new issue
Block a user