diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt index 584c2c380..86d41b2a0 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -992,6 +992,7 @@ class SessionService : Service() { activityVisible = true screenOn = (getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isInteractive ?: screenOn manualPauseRequested = false + if (steamSleepToken == null) requestSteamWake() completeSteamSleep() suspendAttemptFailed = false updateSuspendPolicy() @@ -1070,7 +1071,7 @@ class SessionService : Service() { private fun watchLaunchRequests(dir: File) { launchWatcher?.stopWatching() - listOf("steam-sleep", "steam-sleep-state", "steam-sleep-ready").forEach { File(dir, it).delete() } + listOf("steam-sleep", "steam-sleep-state", "steam-sleep-ready", "steam-wake", "steam-wake.tmp").forEach { File(dir, it).delete() } val gen = sessionGen @Suppress("DEPRECATION") val watcher = object : android.os.FileObserver(dir.path, CLOSE_WRITE or MOVED_TO) { @@ -1211,7 +1212,21 @@ class SessionService : Service() { } private fun resumeSteamSleepOnReturn() { - if (suspendPolicy == SessionPrefs.SUSPEND_AUTO && activityVisible && screenOn) completeSteamSleep() + if (suspendPolicy == SessionPrefs.SUSPEND_AUTO && activityVisible && screenOn) { + if (steamSleepToken == null) requestSteamWake() + completeSteamSleep() + } + } + + /** Resume must also recover a Steam sleep whose request never reached Android. */ + private fun requestSteamWake() { + if (!SessionState.running || SessionState.mode != MODE_STEAM) return + val dir = LinuxRuntime.sessionRoot(this) + runCatching { + val staged = File(dir, "steam-wake.tmp") + staged.writeText("wake\n") + check(staged.renameTo(File(dir, "steam-wake"))) + }.onFailure { Log.w(TAG, "could not request Steam wake", it) } } private fun finishSessionStop(status: Int, stoppedGen: Int) { diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 b/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 index 3e2f24b77..990dd9378 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-login1 @@ -18,6 +18,8 @@ from gi.repository import Gio, GLib # noqa: E402 NAME = "org.freedesktop.login1" ROOT = "/org/freedesktop/login1" INTERFACE = NAME + ".Manager" +UPOWER = "org.freedesktop.UPower" +UPOWER_ROOT = "/org/freedesktop/UPower" INTROSPECTION = """ @@ -29,6 +31,13 @@ INTROSPECTION = """ """ +UPOWER_INTROSPECTION = """ + + + + + +""" class LoginManager: @@ -37,10 +46,15 @@ class LoginManager: self.directory = Path(directory) self.request = self.directory / "steam-sleep" self.state = self.directory / "steam-sleep-state" + self.wake = self.directory / "steam-wake" self.pending = None self.timeout = None 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 + # while listening to login1's sleep/resume notifications. Use one handshake. + upower_info = Gio.DBusNodeInfo.new_for_xml(UPOWER_INTROSPECTION).interfaces[0] + self.upower_registration = bus.register_object(UPOWER_ROOT, upower_info, self.method, None, None) self.monitor = Gio.File.new_for_path(str(self.directory)).monitor_directory(Gio.FileMonitorFlags.NONE, None) self.monitor.connect("changed", self.changed) @@ -69,7 +83,7 @@ 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_async(None, self.publish_after_flush, None) + self.bus.flush(None, self.publish_after_flush, None) def publish_after_flush(self, connection, result, _data): try: @@ -92,6 +106,10 @@ class LoginManager: return False def changed(self, monitor, file, other, event): + 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) + self.resume(force=True) if file.get_basename() == self.state.name or (other and other.get_basename() == self.state.name): self.check_state() @@ -113,8 +131,13 @@ class LoginManager: self.resume() return False - def resume(self): + def resume(self, force=False): if self.pending is None: + if force: + # A missed suspend command leaves Steam without the first half of + # the handshake. Replay both notifications, without pausing again. + self.notify(True) + self.bus.flush(None, self.finish_recovery, None) return self.pending = None if self.timeout is not None: @@ -122,6 +145,14 @@ class LoginManager: self.timeout = None self.notify(False) + def finish_recovery(self, connection, result, _data): + try: + connection.flush_finish(result) + except GLib.Error as error: + print("== login1: cannot flush wake recovery: " + str(error), file=sys.stderr) + if self.pending is None: + self.notify(False) + def main(): directory = os.environ.get("BL_LAUNCH_DIR") @@ -131,23 +162,29 @@ def main(): ready.unlink(missing_ok=True) loop = GLib.MainLoop() manager = [] + acquired_names = set() def connected(bus, name): - manager.append(LoginManager(bus, directory)) + if not manager: + manager.append(LoginManager(bus, directory)) def acquired(bus, name): - ready.write_text("ready\n") + acquired_names.add(name) + if acquired_names == {NAME, UPOWER}: + ready.write_text("ready\n") def lost(bus, name): ready.unlink(missing_ok=True) loop.quit() - owner = Gio.bus_own_name(Gio.BusType.SYSTEM, NAME, Gio.BusNameOwnerFlags.NONE, connected, acquired, lost) + owners = [Gio.bus_own_name(Gio.BusType.SYSTEM, name, Gio.BusNameOwnerFlags.NONE, connected, acquired, lost) + for name in (NAME, UPOWER)] try: loop.run() finally: ready.unlink(missing_ok=True) - Gio.bus_unown_name(owner) + for owner in owners: + Gio.bus_unown_name(owner) if __name__ == "__main__": diff --git a/tools/tests/test_login1.py b/tools/tests/test_login1.py index 184458f69..4cbdc1c36 100644 --- a/tools/tests/test_login1.py +++ b/tools/tests/test_login1.py @@ -117,6 +117,38 @@ class SleepHandshakeTest(unittest.TestCase): self.wait_for(lambda: self.signals == [True, False]) self.assertFalse(self.preparing()) + def test_legacy_upower_suspend_uses_the_same_wake_handshake(self): + self.bus.call_sync("org.freedesktop.UPower", "/org/freedesktop/UPower", + "org.freedesktop.UPower", "Suspend", None, None, + Gio.DBusCallFlags.NONE, 2000, None) + self.wait_for(lambda: (self.dir / "steam-sleep").exists()) + self.wait_for(lambda: self.signals == [True]) + token = (self.dir / "steam-sleep").read_text().strip() + self.assertTrue(self.preparing()) + self.state(token, "awake") + self.wait_for(lambda: self.signals == [True, False]) + self.assertFalse(self.preparing()) + + def request_wake(self): + staged = self.dir / "steam-wake.tmp" + staged.write_text("wake\n") + staged.replace(self.dir / "steam-wake") + + def test_host_resume_recovers_without_a_sleep_request(self): + self.assertFalse(self.preparing()) + self.request_wake() + self.wait_for(lambda: self.signals == [True, False]) + self.assertFalse((self.dir / "steam-wake").exists()) + + def test_host_wake_survives_frozen_service(self): + token = self.suspend() + self.state(token, "paused") + os.kill(self.process.pid, signal.SIGSTOP) + self.request_wake() + os.kill(self.process.pid, signal.SIGCONT) + self.wait_for(lambda: self.signals == [True, False]) + self.assertFalse(self.preparing()) + def test_failed_pause_recovers_and_duplicate_is_rejected(self): token = self.suspend() with self.assertRaises(GLib.Error):