Skip to content

Commit 0497dc3

Browse files
authored
feat: Linux播放时的休眠抑制 (#2773)
* feat: Linux播放时的休眠抑制 * fix * fix: 移除了错误的dbus实现 * fix: Change WHO to a more fitting name * fix: Apply suggestions from code review
1 parent 6f34e7a commit 0497dc3

1 file changed

Lines changed: 181 additions & 4 deletions

File tree

app/shared/ui-foundation/src/desktopMain/kotlin/platform/window/LinuxWindowUtils.kt

Lines changed: 181 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,195 @@ package me.him188.ani.app.platform.window
1111

1212
import androidx.compose.ui.window.WindowPlacement
1313
import androidx.compose.ui.window.WindowState
14+
import kotlinx.atomicfu.locks.ReentrantLock
15+
import kotlinx.atomicfu.locks.withLock
1416
import me.him188.ani.app.platform.PlatformWindow
17+
import me.him188.ani.utils.logging.logger
18+
import java.io.File
19+
import java.lang.ref.WeakReference
20+
import java.util.concurrent.TimeUnit
1521

1622
class LinuxWindowUtils : AwtWindowUtils() {
23+
private val lock = ReentrantLock()
24+
25+
@Volatile private var systemdProcess: Process? = null
26+
@Volatile private var closed = false
27+
28+
companion object {
29+
private val logger = logger<LinuxWindowUtils>()
30+
private val instances = mutableListOf<WeakReference<LinuxWindowUtils>>()
31+
32+
private val hasSystemdInhibit by lazy { cmdExists("systemd-inhibit") }
33+
private val hasSleep by lazy { cmdExists("sleep") }
34+
35+
private fun cmdExists(cmd: String): Boolean = runCatching {
36+
if (cmd.contains(File.separatorChar)) {
37+
if (cmd.contains("..")) return false
38+
val f = File(cmd)
39+
f.isFile && f.canExecute()
40+
} else {
41+
System.getenv("PATH")?.split(File.pathSeparator)
42+
?.any { dir -> File(dir, cmd).let { it.isFile && it.canExecute() } } ?: false
43+
}
44+
}.getOrDefault(false)
45+
46+
init {
47+
Runtime.getRuntime().addShutdownHook(Thread {
48+
synchronized(instances) {
49+
instances.removeAll { it.get() == null }
50+
instances.mapNotNull { it.get() }
51+
}.forEach { inst ->
52+
try {
53+
if (inst.lock.tryLock()) {
54+
try { if (!inst.closed) inst.cleanupLocked() }
55+
finally { inst.lock.unlock() }
56+
} else {
57+
logger.warn("[ScreenSaver] Shutdown: lock unavailable; skipping cleanup")
58+
}
59+
} catch (e: Exception) {
60+
logger.warn("[ScreenSaver] Shutdown error", e)
61+
}
62+
}
63+
})
64+
}
65+
}
66+
67+
init {
68+
synchronized(instances) {
69+
instances.removeAll { it.get() == null }
70+
instances.add(WeakReference(this))
71+
}
72+
}
73+
1774
override suspend fun setUndecoratedFullscreen(
1875
window: PlatformWindow,
1976
windowState: WindowState,
2077
undecorated: Boolean
2178
) {
22-
if (undecorated) {
23-
windowState.apply { placement = WindowPlacement.Fullscreen }
79+
windowState.placement = if (undecorated) WindowPlacement.Fullscreen else WindowPlacement.Floating
80+
}
81+
82+
override fun setPreventScreenSaver(prevent: Boolean) = lock.withLock {
83+
if (closed) return@withLock
84+
85+
if (prevent) {
86+
val alreadyInhibited = systemdProcess?.isAlive == true
87+
val systemdOk = alreadyInhibited || trySystemdInhibitLocked()
88+
89+
if (systemdOk) {
90+
if (alreadyInhibited) {
91+
logger.info("[ScreenSaver] Already inhibited via systemd-inhibit")
92+
} else {
93+
logger.info("[ScreenSaver] Inhibited via systemd-inhibit")
94+
}
95+
} else {
96+
logger.warn("[ScreenSaver] systemd-inhibit failed")
97+
}
2498
} else {
25-
windowState.apply { placement = WindowPlacement.Floating }
99+
cleanupLocked()
100+
}
101+
}
102+
103+
private fun trySystemdInhibitLocked(): Boolean {
104+
if (!hasSystemdInhibit || !hasSleep) return false
105+
106+
systemdProcess?.let { p ->
107+
try { p.outputStream.close() } catch (_: Exception) {}
108+
p.destroy()
109+
try {
110+
if (!p.waitFor(200, TimeUnit.MILLISECONDS)) {
111+
p.destroyForcibly()
112+
}
113+
} catch (_: Exception) {}
114+
}
115+
systemdProcess = null
116+
117+
return runCatching {
118+
val p = ProcessBuilder(
119+
"systemd-inhibit", "--what=sleep:idle", "--who=Animeko",
120+
"--why=Playing video", "--mode=block", "sleep", "infinity"
121+
).redirectErrorStream(true).start()
122+
123+
// Drain output async
124+
Thread {
125+
try {
126+
try {
127+
p.inputStream.copyTo(java.io.OutputStream.nullOutputStream())
128+
} catch (_: Exception) {
129+
// ignore copy errors
130+
}
131+
} finally {
132+
try { p.inputStream.close() } catch (_: Exception) {}
133+
}
134+
}.apply { isDaemon = true; name = "ScreenSaver-drain"; start() }
135+
136+
// Check if exits immediately
137+
val exitedImmediately = try {
138+
p.waitFor(500, TimeUnit.MILLISECONDS)
139+
} catch (ie: InterruptedException) {
140+
Thread.currentThread().interrupt()
141+
logger.warn("[ScreenSaver] Interrupted while starting systemd-inhibit", ie)
142+
try {
143+
p.destroyForcibly()
144+
p.waitFor(2, TimeUnit.SECONDS)
145+
} catch (_: Exception) {}
146+
return false
147+
}
148+
149+
if (exitedImmediately) {
150+
logger.warn("[ScreenSaver] systemd-inhibit exited: ${p.exitValue()}")
151+
try {
152+
p.destroyForcibly()
153+
p.waitFor(2, TimeUnit.SECONDS)
154+
} catch (_: Exception) {}
155+
false
156+
} else {
157+
systemdProcess = p
158+
true
159+
}
160+
}.onFailure { logger.debug("[ScreenSaver] systemd failed", it) }.getOrDefault(false)
161+
}
162+
163+
private fun cleanupLocked() {
164+
systemdProcess?.let { p ->
165+
try {
166+
try { p.outputStream.close() } catch (_: Exception) {}
167+
168+
p.destroy()
169+
val exited = try {
170+
p.waitFor(300, TimeUnit.MILLISECONDS)
171+
} catch (ie: InterruptedException) {
172+
Thread.currentThread().interrupt()
173+
logger.warn("[ScreenSaver] Interrupted while waiting for systemd-inhibit to stop", ie)
174+
false
175+
}
176+
177+
if (!exited) {
178+
p.destroyForcibly()
179+
try {
180+
if (!p.waitFor(3, TimeUnit.SECONDS)) {
181+
logger.warn("[ScreenSaver] systemd-inhibit won't die")
182+
}
183+
} catch (ie: InterruptedException) {
184+
Thread.currentThread().interrupt()
185+
logger.warn("[ScreenSaver] Interrupted while waiting for forcible stop", ie)
186+
}
187+
}
188+
} finally {
189+
systemdProcess = null
190+
}
191+
}
192+
}
193+
194+
fun close() = lock.withLock {
195+
if (closed) return@withLock
196+
closed = true
197+
synchronized(instances) {
198+
instances.removeAll { ref ->
199+
val instance = ref.get()
200+
instance == null || instance === this
201+
}
26202
}
203+
cleanupLocked()
27204
}
28-
}
205+
}

0 commit comments

Comments
 (0)