Skip to content

Commit 77a478f

Browse files
committed
feat: Linux播放时的休眠抑制
1 parent aa2becb commit 77a478f

File tree

1 file changed

+220
-13
lines changed

1 file changed

+220
-13
lines changed
Lines changed: 220 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,235 @@
1-
/*
2-
* Copyright (C) 2024-2025 OpenAni and contributors.
3-
*
4-
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
5-
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
6-
*
7-
* https://github.com/open-ani/ani/blob/main/LICENSE
8-
*/
9-
101
package me.him188.ani.app.platform.window
112

123
import androidx.compose.ui.window.WindowPlacement
134
import androidx.compose.ui.window.WindowState
5+
import kotlinx.atomicfu.locks.ReentrantLock
6+
import kotlinx.atomicfu.locks.withLock
147
import me.him188.ani.app.platform.PlatformWindow
8+
import me.him188.ani.utils.logging.logger
9+
import java.io.File
10+
import java.lang.ref.WeakReference
11+
import java.util.concurrent.TimeUnit
1512

1613
class LinuxWindowUtils : AwtWindowUtils() {
14+
private val lock = ReentrantLock()
15+
16+
@Volatile private var dbusCookie: UInt? = null
17+
@Volatile private var systemdProcess: Process? = null
18+
@Volatile private var closed = false
19+
20+
companion object {
21+
private val logger = logger<LinuxWindowUtils>()
22+
private val instances = mutableListOf<WeakReference<LinuxWindowUtils>>()
23+
24+
init {
25+
Runtime.getRuntime().addShutdownHook(Thread {
26+
synchronized(instances) {
27+
instances.removeAll { it.get() == null }
28+
instances.mapNotNull { it.get() }
29+
}.forEach { inst ->
30+
try {
31+
if (inst.lock.tryLock(2, TimeUnit.SECONDS)) {
32+
try { if (!inst.closed) inst.cleanupLocked() }
33+
finally { inst.lock.unlock() }
34+
} else {
35+
logger.warn("[ScreenSaver] Shutdown: lock timeout")
36+
}
37+
} catch (e: InterruptedException) {
38+
Thread.currentThread().interrupt()
39+
logger.warn("[ScreenSaver] Shutdown interrupted", e)
40+
} catch (e: Exception) {
41+
logger.warn("[ScreenSaver] Shutdown error", e)
42+
}
43+
}
44+
})
45+
}
46+
}
47+
48+
init {
49+
synchronized(instances) { instances.add(WeakReference(this)) }
50+
}
51+
1752
override suspend fun setUndecoratedFullscreen(
1853
window: PlatformWindow,
1954
windowState: WindowState,
2055
undecorated: Boolean
2156
) {
22-
if (undecorated) {
23-
windowState.apply { placement = WindowPlacement.Fullscreen }
57+
windowState.placement = if (undecorated) WindowPlacement.Fullscreen else WindowPlacement.Floating
58+
}
59+
60+
override fun setPreventScreenSaver(prevent: Boolean) = lock.withLock {
61+
if (closed) return@withLock
62+
63+
if (prevent) {
64+
val systemdOk = systemdProcess?.isAlive ?: false || trySystemdInhibitLocked()
65+
val dbusOk = dbusCookie != null || tryDbusInhibitLocked()
66+
67+
when {
68+
systemdOk && dbusOk -> logger.info("[ScreenSaver] Inhibited (systemd + dbus)")
69+
systemdOk || dbusOk -> logger.info("[ScreenSaver] Partial (systemd=$systemdOk, dbus=$dbusOk)")
70+
else -> logger.warn("[ScreenSaver] All methods failed")
71+
}
2472
} else {
25-
windowState.apply { placement = WindowPlacement.Floating }
73+
cleanupLocked()
74+
}
75+
}
76+
77+
private fun trySystemdInhibitLocked(): Boolean {
78+
if (!cmdExists("systemd-inhibit") || !cmdExists("tail")) return false
79+
80+
systemdProcess?.destroy()
81+
systemdProcess = null
82+
83+
return runCatching {
84+
val p = ProcessBuilder(
85+
"systemd-inhibit", "--what=sleep:idle", "--who=AniVideoPlayer",
86+
"--why=Playing video", "--mode=block", "tail", "-f", "/dev/null"
87+
).redirectErrorStream(true).start()
88+
89+
// Drain output async
90+
Thread {
91+
try {
92+
try {
93+
p.inputStream.copyTo(java.io.OutputStream.nullOutputStream())
94+
} catch (_: Exception) {
95+
// ignore copy errors
96+
}
97+
} finally {
98+
try { p.inputStream.close() } catch (_: Exception) {}
99+
}
100+
}.apply { isDaemon = true; name = "ScreenSaver-drain"; start() }
101+
102+
// Check if exits immediately
103+
val exitedImmediately = try {
104+
p.waitFor(500, TimeUnit.MILLISECONDS)
105+
} catch (ie: InterruptedException) {
106+
Thread.currentThread().interrupt()
107+
logger.warn("[ScreenSaver] Interrupted while starting systemd-inhibit", ie)
108+
try {
109+
p.destroyForcibly()
110+
p.waitFor(2, TimeUnit.SECONDS)
111+
} catch (_: Exception) {}
112+
return false
113+
}
114+
115+
if (exitedImmediately) {
116+
logger.warn("[ScreenSaver] systemd-inhibit exited: ${p.exitValue()}")
117+
try {
118+
p.destroyForcibly()
119+
p.waitFor(2, TimeUnit.SECONDS)
120+
} catch (_: Exception) {}
121+
false
122+
} else {
123+
systemdProcess = p
124+
true
125+
}
126+
}.onFailure { logger.debug("[ScreenSaver] systemd failed", it) }.getOrDefault(false)
127+
}
128+
129+
private fun tryDbusInhibitLocked(): Boolean {
130+
if (!cmdExists("dbus-send")) return false
131+
132+
return runCatching {
133+
val p = ProcessBuilder(
134+
"dbus-send", "--session", "--print-reply",
135+
"--dest=org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver",
136+
"org.freedesktop.ScreenSaver.Inhibit", "string:AniVideoPlayer", "string:Playing video"
137+
).redirectErrorStream(true).start()
138+
139+
val finished = try {
140+
p.waitFor(5, TimeUnit.SECONDS)
141+
} catch (ie: InterruptedException) {
142+
Thread.currentThread().interrupt()
143+
logger.warn("[ScreenSaver] Interrupted while waiting for dbus-send", ie)
144+
try { p.destroyForcibly(); p.waitFor(2, TimeUnit.SECONDS) } catch (_: Exception) {}
145+
return false
146+
}
147+
148+
if (!finished) {
149+
try { p.destroyForcibly(); p.waitFor(2, TimeUnit.SECONDS) } catch (_: Exception) {}
150+
logger.warn("[ScreenSaver] dbus-send timed out")
151+
return false
152+
}
153+
154+
val out = try { p.inputStream.bufferedReader().use { it.readText() } } catch (_: Exception) { "" }
155+
val cookie = Regex("""\buint32\s+(\d+)""").find(out)?.groups?.get(1)?.value?.toUIntOrNull()
156+
157+
if (cookie != null) {
158+
dbusCookie = cookie
159+
true
160+
} else {
161+
logger.debug("[ScreenSaver] No cookie in: $out")
162+
false
163+
}
164+
}.onFailure { logger.debug("[ScreenSaver] dbus failed", it) }.getOrDefault(false)
165+
}
166+
167+
private fun cleanupLocked() {
168+
systemdProcess?.let { p ->
169+
try {
170+
p.destroy()
171+
val exited = try {
172+
p.waitFor(300, TimeUnit.MILLISECONDS)
173+
} catch (ie: InterruptedException) {
174+
Thread.currentThread().interrupt()
175+
logger.warn("[ScreenSaver] Interrupted while waiting for systemd-inhibit to stop", ie)
176+
false
177+
}
178+
179+
if (!exited) {
180+
try {
181+
p.destroyForcibly()
182+
val forcibleExited = try {
183+
p.waitFor(3, TimeUnit.SECONDS)
184+
} catch (ie: InterruptedException) {
185+
Thread.currentThread().interrupt()
186+
logger.warn("[ScreenSaver] Interrupted while waiting for forcible stop", ie)
187+
false
188+
}
189+
if (!forcibleExited) {
190+
logger.warn("[ScreenSaver] systemd-inhibit won't die")
191+
}
192+
} catch (e: Exception) {
193+
logger.warn("[ScreenSaver] Error forcing systemd-inhibit stop", e)
194+
}
195+
}
196+
} finally {
197+
systemdProcess = null
198+
}
199+
}
200+
201+
dbusCookie?.let { cookie ->
202+
runCatching {
203+
ProcessBuilder(
204+
"dbus-send", "--session", "--dest=org.freedesktop.ScreenSaver",
205+
"/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver.UnInhibit", "uint32:$cookie"
206+
).redirectErrorStream(true).start().waitFor(5, TimeUnit.SECONDS)
207+
}.onFailure { logger.debug("[ScreenSaver] dbus uninhibit failed", it) }
208+
dbusCookie = null
26209
}
27210
}
28-
}
211+
212+
fun close() = lock.withLock {
213+
if (closed) return@withLock
214+
closed = true
215+
synchronized(instances) {
216+
instances.removeAll { ref ->
217+
val instance = ref.get()
218+
instance == null || instance === this
219+
}
220+
}
221+
cleanupLocked()
222+
}
223+
224+
private fun cmdExists(cmd: String): Boolean = runCatching {
225+
if (cmd.contains(File.separatorChar)) {
226+
if (cmd.contains("..")) return false
227+
val f = File(cmd)
228+
// Simpler check: allow symlinks; just require file exists and is executable.
229+
f.exists() && f.canExecute()
230+
} else {
231+
System.getenv("PATH")?.split(File.pathSeparator)
232+
?.any { File(it, cmd).canExecute() } ?: false
233+
}
234+
}.getOrDefault(false)
235+
}

0 commit comments

Comments
 (0)