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-
101package me.him188.ani.app.platform.window
112
123import androidx.compose.ui.window.WindowPlacement
134import androidx.compose.ui.window.WindowState
5+ import kotlinx.atomicfu.locks.ReentrantLock
6+ import kotlinx.atomicfu.locks.withLock
147import 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
1613class 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