1 /*
<lambda>null2  * Copyright 2024 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.benchmark
18 
19 import android.app.UiAutomation
20 import android.os.Build
21 import android.os.ParcelFileDescriptor
22 import android.util.Log
23 import androidx.annotation.RequiresApi
24 import androidx.annotation.RestrictTo
25 import androidx.benchmark.ShellFile.Companion.rootState
26 import androidx.test.platform.app.InstrumentationRegistry
27 import androidx.tracing.trace
28 import java.io.DataInputStream
29 import java.io.File
30 import java.io.InputStream
31 import java.io.OutputStream
32 
33 /**
34  * Virtual file is a wrapper class around the concept of file. Depending on where the file is stored
35  * it can be accessed either by the shell user or the app user. When the file is managed by the app
36  * user this is managed by the subclass [UserFile], when the file is managed by shell user this is
37  * managed by the subclass [ShellFile]. Note that shell user runs as user 0.
38  *
39  * When the selected android user is the primary user, that is also 0. In this case shell can access
40  * both user storage and shell storage. When running in multiuser mode or on headless devices (where
41  * the primary user is by default 10), shell cannot access the user storage and this class offers a
42  * layer of abstraction to primarily copy and move files across the 2 different storages.
43  *
44  * Note that [Shell] is not used in this implementation for multiple reasons. [ShellFile] uses
45  * [UiAutomation.executeShellCommandRw] that is only available from api 31. Additionally, since it
46  * needs to interact with the instrumentation process to copy any amount of data, it uses streaming
47  * api to read and write from files and allow large file size IO.
48  *
49  * Internally, [Shell] utilizes [ShellFile] to create scripts, including the ones for
50  * [Shell.executeScriptSilent] for android multiuser.
51  */
52 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
53 sealed class VirtualFile {
54 
55     companion object {
56         private const val USER_SPACE_PATH_PREFIX = "/storage/emulated/"
57 
58         fun fromPath(path: String): VirtualFile =
59             if (path.startsWith(USER_SPACE_PATH_PREFIX)) {
60                 UserFile(path)
61             } else {
62                 ShellFile(path)
63             }
64     }
65 
66     abstract val absolutePath: String
67     abstract val fileType: String
68 
69     fun writeText(content: String) = copyFrom(content.byteInputStream())
70 
71     fun writeBytes(bytes: ByteArray) = copyFrom(bytes.inputStream())
72 
73     fun readText(): String = useInputStream { it.bufferedReader().readText() }
74 
75     fun readBytes(): ByteArray = useInputStream { it.readBytes() }
76 
77     abstract fun delete(): Boolean
78 
79     protected abstract fun <T> useInputStream(block: (InputStream) -> (T)): T
80 
81     protected abstract fun useOutputStream(block: (OutputStream) -> (Unit))
82 
83     fun copyFrom(otherInputStream: InputStream) = useOutputStream { o ->
84         otherInputStream.copyTo(o)
85     }
86 
87     fun copyTo(otherOutputStream: OutputStream) = useInputStream { i ->
88         i.copyTo(otherOutputStream)
89     }
90 
91     fun copyFrom(otherVirtualFile: VirtualFile) {
92         if (this is ShellFile && otherVirtualFile is ShellFile) {
93             // Optimization: reading and writing a shell file require 2 processes to run.
94             // We don't need to do that if the file is copied in shell storage.
95             executeCommand { "cp ${otherVirtualFile.absolutePath} $absolutePath" }
96             return
97         }
98         otherVirtualFile.useInputStream { i -> useOutputStream { o -> i.copyTo(o) } }
99     }
100 
101     fun copyTo(otherVirtualFile: VirtualFile) {
102         if (this is ShellFile && otherVirtualFile is ShellFile) {
103             // Optimization: reading and writing a shell file require 2 processes to run.
104             // We don't need to do that if the file is copied in shell storage.
105             executeCommand { "cp $absolutePath ${otherVirtualFile.absolutePath}" }
106             return
107         }
108         useInputStream { i -> otherVirtualFile.useOutputStream { o -> i.copyTo(o) } }
109     }
110 
111     fun moveTo(otherVirtualFile: VirtualFile) {
112         if (this is ShellFile && otherVirtualFile is ShellFile) {
113             // Optimization: reading and writing a shell file require 2 processes to run.
114             // We don't need to do that if the file is moved in shell storage.
115             executeCommand { "mv $absolutePath ${otherVirtualFile.absolutePath}" }
116             return
117         }
118         copyTo(otherVirtualFile).also { this.delete() }
119     }
120 
121     protected abstract fun executeCommand(block: (String) -> String): String
122 
123     fun md5sum(): String = executeCommand { "md5sum $it" }.substringBefore(" ")
124 
125     fun chmod(args: String) = executeCommand { "chmod $args $it" }
126 
127     fun ls(): List<String> = executeCommand { "ls -1 $it" }.lines().filter { it.isNotBlank() }
128 
129     abstract fun mkdir()
130 
131     abstract fun exists(): Boolean
132 }
133 
134 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
135 class UserFile(private val file: File) : VirtualFile() {
136 
137     companion object {
inOutputsDirnull138         fun inOutputsDir(name: String): UserFile {
139             val file = File(Outputs.dirUsableByAppAndShell, name)
140             if (Outputs.forceFilesForShellAccessible) {
141                 // script content must be readable by shell, and for some reason
142                 // doesn't inherit shell readability from dirUsableByAppAndShell
143                 file.setReadable(true, false)
144             }
145             return UserFile(file)
146         }
147     }
148 
149     constructor(path: String) : this(File(path))
150 
151     override val absolutePath: String
152         get() = file.absolutePath
153 
154     override val fileType: String
155         get() = "UserFile"
156 
useInputStreamnull157     override fun <T> useInputStream(block: (InputStream) -> T): T =
158         file.inputStream().use { block(it) }
159 
useOutputStreamnull160     override fun useOutputStream(block: (OutputStream) -> Unit) =
161         file.outputStream().use { block(it) }
162 
deletenull163     override fun delete() = file.deleteRecursively()
164 
165     override fun executeCommand(block: (String) -> String): String {
166         val cmd = block(absolutePath)
167         return trace("UserFile#executeCommand $cmd".take(127)) {
168             DataInputStream(Runtime.getRuntime().exec(cmd).inputStream)
169                 .bufferedReader()
170                 .use { it.readText() }
171                 .trim()
172         }
173     }
174 
mkdirnull175     override fun mkdir() {
176         file.mkdirs()
177     }
178 
existsnull179     override fun exists() = file.exists()
180 }
181 
182 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
183 class ShellFile(override val absolutePath: String) : VirtualFile() {
184 
185     override val fileType: String
186         get() = "ShellFile"
187 
188     companion object {
189         private const val TAG = "ShellFile"
190         private val uiAutomation: UiAutomation =
191             InstrumentationRegistry.getInstrumentation().uiAutomation
192         private val rootState by lazy { RootState.check() }
193 
194         fun inTempDir(name: String) = ShellFile("/data/local/tmp/", name)
195     }
196 
197     constructor(
198         directory: String,
199         filename: String
200     ) : this("${if (directory.endsWith("/")) directory else "$directory/"}$filename")
201 
202     override fun <T> useInputStream(block: (InputStream) -> T): T {
203         // The following command prints the size of the file in bytes followed by the name.
204         // If the file is a folder or the user doesn't have permission it throws an error.
205         // We use the following command to ensure this is a valid read operation because
206         // `UiAutomation#executeShellCommand` does not give any information if the cat process
207         // fails.
208         size()
209 
210         val cmd = rootState.maybeRootify("cat $absolutePath")
211         val value =
212             trace("ShellFile#useInputStream $cmd".take(127)) {
213                 val outDescriptor = uiAutomation.executeShellCommand(cmd)
214                 ParcelFileDescriptor.AutoCloseInputStream(outDescriptor).use(block)
215             }
216         return value
217     }
218 
219     @RequiresApi(Build.VERSION_CODES.S)
220     override fun useOutputStream(block: (OutputStream) -> Unit) {
221         val cmd = rootState.maybeRootify("cp /dev/stdin $absolutePath")
222         var counterOs: CounterOutputStream? = null
223 
224         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
225             trace("ShellFile#useOutputStream $cmd".take(127)) {
226                 val (_, inDescriptor, errDescriptor) = uiAutomation.executeShellCommandRwe(cmd)
227                 ParcelFileDescriptor.AutoCloseOutputStream(inDescriptor).use {
228                     counterOs = CounterOutputStream(it)
229                     block(counterOs!!)
230                 }
231                 checkErr(errDescriptor)
232             }
233         } else {
234             trace("ShellFile#useOutputStream $cmd".take(127)) {
235                 val (_, inDescriptor) = uiAutomation.executeShellCommandRw(cmd)
236                 ParcelFileDescriptor.AutoCloseOutputStream(inDescriptor).use {
237                     counterOs = CounterOutputStream(it)
238                     block(counterOs!!)
239                 }
240             }
241         }
242 
243         // The file might still be in cache. Sync will force the writing.
244         // Without this, trying to read the file just written may result in file not found.
245         sync()
246 
247         // Verify that the actual file size is the same that has been written
248         val actualSize = size()
249         val expectedSize = counterOs!!.writtenBytes
250         if (actualSize != expectedSize) {
251             throw IllegalStateException(
252                 "Expected $absolutePath size to be $actualSize but was $expectedSize"
253             )
254         }
255     }
256 
257     override fun delete(): Boolean {
258         val output = executeCommand { "rm -Rf $it" }
259         sync()
260         if (output.isBlank()) {
261             return true
262         } else {
263             Log.d(TAG, "Error deleting `$absolutePath`: $output")
264             return false
265         }
266     }
267 
268     override fun executeCommand(block: (String) -> String): String {
269         val cmd = rootState.maybeRootify(block(absolutePath))
270         val output =
271             trace("ShellFile#executeCommand $cmd".take(127)) {
272                 uiAutomation.executeShellCommand(cmd).fullyReadInputStream()
273             }
274         return output.trim()
275     }
276 
277     override fun mkdir() {
278         executeCommand { "mkdir -p $it" }
279     }
280 
281     override fun exists(): Boolean {
282         return ls().isNotEmpty()
283     }
284 
285     private fun sync() {
286         executeCommand { "sync" }
287     }
288 
289     private fun size() =
290         try {
291             executeCommand { "wc -c $it" }.split(" ").first().toLong()
292         } catch (e: Throwable) {
293             throw IllegalStateException("Cannot check size of $absolutePath.", e)
294         }
295 
296     private fun checkErr(errDescriptor: ParcelFileDescriptor) {
297         val err =
298             ParcelFileDescriptor.AutoCloseInputStream(errDescriptor)
299                 .bufferedReader()
300                 .readText()
301                 .trim()
302         if (err.isNotBlank()) {
303             throw IllegalStateException("Error writing in $absolutePath: `$err`")
304         }
305     }
306 }
307 
308 private class CounterOutputStream(private val ostream: OutputStream) : OutputStream() {
309 
310     private var _writtenBytes = 0L
311     val writtenBytes: Long
312         get() = _writtenBytes
313 
writenull314     override fun write(b: Int) {
315         _writtenBytes++
316         ostream.write(b)
317     }
318 
closenull319     override fun close() {
320         ostream.close()
321     }
322 
flushnull323     override fun flush() {
324         ostream.flush()
325     }
326 }
327 
328 private class RootState(val sessionRooted: Boolean, val suAvailable: Boolean) {
329     companion object {
330         private val uiAutomation: UiAutomation =
331             InstrumentationRegistry.getInstrumentation().uiAutomation
332 
checknull333         fun check() =
334             RootState(
335                 sessionRooted =
336                     DeviceInfo.isRooted &&
337                         uiAutomation
338                             .executeShellCommand("id")
339                             .fullyReadInputStream()
340                             .contains("uid=0(root)"),
341                 suAvailable =
342                     DeviceInfo.isRooted &&
343                         uiAutomation
344                             .executeShellCommand("su root id")
345                             .fullyReadInputStream()
346                             .contains("uid=0(root)")
347             )
348     }
349 
350     fun maybeRootify(cmd: String) =
351         trace("buildCommand $cmd".take(127)) {
352             if (!sessionRooted && suAvailable) {
353                 "su root $cmd"
354             } else {
355                 cmd
356             }
357         }
358 }
359