@@ -11,18 +11,195 @@ package me.him188.ani.app.platform.window
1111
1212import androidx.compose.ui.window.WindowPlacement
1313import androidx.compose.ui.window.WindowState
14+ import kotlinx.atomicfu.locks.ReentrantLock
15+ import kotlinx.atomicfu.locks.withLock
1416import 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
1622class 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