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