1 /*
<lambda>null2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.hardware
18 
19 import android.util.Log
20 import androidx.graphics.utils.HandlerThreadExecutor
21 import java.io.File
22 import java.lang.NumberFormatException
23 import java.util.concurrent.TimeUnit
24 import java.util.concurrent.atomic.AtomicBoolean
25 import kotlin.collections.removeLast as removeLastKt
26 
27 /**
28  * Class to monitor open file descriptors and clean up those with fences that have already signalled
29  */
30 internal class FileDescriptorMonitor(
31     executor: HandlerThreadExecutor? = null,
32     private val scheduleMillis: Long = MONITOR_DELAY,
33     manageExecutor: Boolean = executor != null
34 ) {
35 
36     private val mIsMonitoring = AtomicBoolean(false)
37     private val mProcFd = File("/proc/self/fd/")
38     private val mCleanupCompleteCallbacks = ArrayList<() -> Unit>()
39 
40     private val mIsManagingHandlerThread = AtomicBoolean(false)
41     private var mExecutor: HandlerThreadExecutor
42 
43     private data class FdSignalPair(val fd: Int, val signalTime: Long)
44 
45     private val pendingFileDescriptors = ArrayList<FdSignalPair>()
46 
47     init {
48         mExecutor = executor ?: HandlerThreadExecutor("fdcleanup")
49         mIsManagingHandlerThread.set(manageExecutor)
50     }
51 
52     private fun closePendingFileDescriptors() {
53         pendingFileDescriptors.sortByDescending { fdSignalTimePair -> fdSignalTimePair.signalTime }
54         while (pendingFileDescriptors.size > MAX_FD) {
55             val fdSignalPair = pendingFileDescriptors.removeLastKt()
56             try {
57                 val fd = fdSignalPair.fd
58                 // Re-query the signal time in case the fd was re-used
59                 val signalTime = SyncFenceBindings.nGetSignalTime(fd)
60                 val diff = signalTime.signalTimeDiffMillis()
61                 if (diff > SIGNAL_TIME_DELTA_MILLIS) {
62                     SyncFenceBindings.nForceClose(fd)
63                 }
64             } catch (_: Throwable) {
65                 // Just in case the owner actually does close the fd
66             }
67         }
68     }
69 
70     private fun Long.signalTimeDiffMillis(): Long {
71         val now = System.nanoTime()
72         val signalled =
73             this != SyncFenceCompat.SIGNAL_TIME_INVALID &&
74                 this != SyncFenceCompat.SIGNAL_TIME_PENDING
75         return if (signalled && now > this) {
76             TimeUnit.NANOSECONDS.toMillis(now - this)
77         } else {
78             -1
79         }
80     }
81 
82     private val mCleanupRunnable = Runnable {
83         mProcFd.listFiles()?.let { files ->
84             for (file in files) {
85                 try {
86                     val fd = Integer.parseInt(file.name)
87                     val signalTime = SyncFenceBindings.nGetSignalTime(fd)
88                     val diff = signalTime.signalTimeDiffMillis()
89                     if (diff > SIGNAL_TIME_DELTA_MILLIS) {
90                         // Store the signal time as it can potentially change in the middle of
91                         // executing the sorting algorithm and can throw exceptions
92                         pendingFileDescriptors.add(FdSignalPair(fd, signalTime))
93                         if (pendingFileDescriptors.size > MAX_FD) {
94                             closePendingFileDescriptors()
95                         }
96                     }
97                 } catch (formatException: NumberFormatException) {
98                     Log.w(TAG, "Unable to parse fd value from name ${file.name}")
99                 }
100             }
101         }
102 
103         if (mIsMonitoring.get()) {
104             scheduleCleanupTask()
105         } else {
106             teardownExecutorIfNecessary()
107         }
108         invokeCleanupCallbacks()
109     }
110 
111     private fun invokeCleanupCallbacks() {
112         synchronized(mCleanupCompleteCallbacks) {
113             for (callback in mCleanupCompleteCallbacks) {
114                 callback.invoke()
115             }
116             mCleanupCompleteCallbacks.clear()
117         }
118     }
119 
120     private fun scheduleCleanupTask() {
121         if (mExecutor.isRunning) {
122             mExecutor.postDelayed(mCleanupRunnable, scheduleMillis)
123         }
124     }
125 
126     fun addCleanupCallback(callback: () -> Unit) {
127         synchronized(mCleanupCompleteCallbacks) { mCleanupCompleteCallbacks.add(callback) }
128     }
129 
130     /**
131      * Starts periodic cleaning of file descriptors if it has not already been started previously
132      */
133     fun startMonitoring() {
134         if (!mIsMonitoring.get()) {
135             initExecutorIfNecessary()
136             scheduleCleanupTask()
137             mIsMonitoring.set(true)
138         }
139     }
140 
141     private fun initExecutorIfNecessary() {
142         if (mIsManagingHandlerThread.get() && !mExecutor.isRunning) {
143             mExecutor = HandlerThreadExecutor("fdcleanup")
144         }
145     }
146 
147     private fun teardownExecutorIfNecessary() {
148         if (mIsManagingHandlerThread.get() && mExecutor.isRunning) {
149             mExecutor.quit()
150         }
151     }
152 
153     /**
154      * Stop scheduling of the periodic clean up of file descriptors
155      *
156      * @param cancelPending Cancels any pending request to clean up contents. If false, the last
157      *   pending request to clean up content will still be scheduled but no more will be afterwards.
158      */
159     fun stopMonitoring(cancelPending: Boolean = false) {
160         if (mIsMonitoring.get()) {
161             if (cancelPending) {
162                 mExecutor.removeCallbacks(mCleanupRunnable)
163                 teardownExecutorIfNecessary()
164             }
165             mIsMonitoring.set(false)
166         }
167     }
168 
169     /**
170      * Returns true if [startMonitoring] has been invoked without a corresponding call to
171      * [stopMonitoring]
172      */
173     val isMonitoring: Boolean
174         get() = mIsMonitoring.get()
175 
176     companion object {
177         const val TAG = "FileDescriptorMonitor"
178 
179         /** Delta in which if a fence has signalled it should be removed */
180         const val SIGNAL_TIME_DELTA_MILLIS = 3000
181 
182         const val MONITOR_DELAY = 1000L
183 
184         const val MAX_FD = 100
185     }
186 }
187