1 /* <lambda>null2 * Copyright (C) 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 package com.android.virtualization.terminal 17 18 import android.content.Context 19 import android.os.FileUtils 20 import android.system.ErrnoException 21 import android.system.Os 22 import android.system.OsConstants 23 import android.util.Log 24 import com.android.virtualization.terminal.MainActivity.Companion.TAG 25 import java.io.BufferedReader 26 import java.io.FileReader 27 import java.io.IOException 28 import java.io.RandomAccessFile 29 import java.nio.file.Files 30 import java.nio.file.Path 31 import java.nio.file.StandardCopyOption 32 import kotlin.math.ceil 33 34 /** Collection of files that consist of a VM image. */ 35 public class InstalledImage private constructor(val installDir: Path) { 36 private val rootPartition: Path = installDir.resolve(ROOTFS_FILENAME) 37 val backupFile: Path = installDir.resolve(BACKUP_FILENAME) 38 39 /** The path to the VM config file. */ 40 val configPath: Path = installDir.resolve(CONFIG_FILENAME) 41 private val marker: Path = installDir.resolve(MARKER_FILENAME) 42 /** The build ID of the installed image */ 43 val buildId: String by lazy { readBuildId() } 44 45 /** Tests if this InstalledImage is actually installed. */ 46 fun isInstalled(): Boolean { 47 return Files.exists(marker) 48 } 49 50 /** Fully uninstall this InstalledImage by deleting everything. */ 51 @Throws(IOException::class) 52 fun uninstallFully() { 53 FileUtils.deleteContentsAndDir(installDir.toFile()) 54 } 55 56 private fun readBuildId(): String { 57 val file = installDir.resolve(BUILD_ID_FILENAME) 58 if (!Files.exists(file)) { 59 return "<no build id>" 60 } 61 try { 62 BufferedReader(FileReader(file.toFile())).use { r -> 63 return r.readLine() 64 } 65 } catch (e: IOException) { 66 throw RuntimeException("Failed to read build ID", e) 67 } 68 } 69 70 fun isOlderThanCurrentVersion(): Boolean { 71 val year = 72 try { 73 buildId.split(" ").last().toInt() 74 } catch (_: Exception) { 75 0 76 } 77 return year < RELEASE_YEAR 78 } 79 80 @Throws(IOException::class) 81 fun uninstallAndBackup(): Path { 82 Files.delete(marker) 83 Files.move(rootPartition, backupFile, StandardCopyOption.REPLACE_EXISTING) 84 return backupFile 85 } 86 87 fun hasBackup(): Boolean { 88 return Files.exists(backupFile) 89 } 90 91 @Throws(IOException::class) 92 fun deleteBackup() { 93 Files.deleteIfExists(backupFile) 94 } 95 96 @Throws(IOException::class) 97 fun getApparentSize(): Long { 98 return Files.size(rootPartition) 99 } 100 101 @Throws(IOException::class) 102 fun getPhysicalSize(): Long { 103 val stat = RandomAccessFile(rootPartition.toFile(), "rw").use { raf -> Os.fstat(raf.fd) } 104 // The unit of st_blocks is 512 byte in Android. 105 return 512L * stat.st_blocks 106 } 107 108 @Throws(IOException::class) 109 fun getSmallestSizePossible(): Long { 110 runE2fsck(rootPartition) 111 val p: String = rootPartition.toAbsolutePath().toString() 112 val result = runCommand("/system/bin/resize2fs", "-P", p) 113 val regex = "Estimated minimum size of the filesystem: ([0-9]+)".toRegex() 114 val matchResult = result.lines().firstNotNullOfOrNull { regex.find(it) } 115 if (matchResult != null) { 116 try { 117 val size = matchResult.groupValues[1].toLong() 118 // The return value is the number of 4k block 119 return roundUp(size * 4 * 1024) 120 } catch (e: NumberFormatException) { 121 // cannot happen 122 } 123 } 124 val msg = "Failed to get min size, p=$p, result=$result" 125 Log.e(TAG, msg) 126 throw RuntimeException(msg) 127 } 128 129 @Throws(IOException::class) 130 fun resize(desiredSize: Long): Long { 131 val roundedUpDesiredSize = roundUp(desiredSize) 132 val curSize = getApparentSize() 133 134 runE2fsck(rootPartition) 135 136 if (roundedUpDesiredSize == curSize) { 137 return roundedUpDesiredSize 138 } 139 140 if (roundedUpDesiredSize > curSize) { 141 if (!allocateSpace(roundedUpDesiredSize)) { 142 return curSize 143 } 144 } 145 resizeFilesystem(rootPartition, roundedUpDesiredSize) 146 return getApparentSize() 147 } 148 149 @Throws(IOException::class) 150 private fun allocateSpace(sizeInBytes: Long): Boolean { 151 val curSizeInBytes = getApparentSize() 152 try { 153 RandomAccessFile(rootPartition.toFile(), "rw").use { raf -> 154 Os.posix_fallocate(raf.fd, 0, sizeInBytes) 155 } 156 Log.d(TAG, "Allocated space to: $sizeInBytes bytes") 157 return true 158 } catch (e: ErrnoException) { 159 Log.e(TAG, "Failed to allocate space", e) 160 if (e.errno == OsConstants.ENOSPC) { 161 Log.d(TAG, "Trying to truncate disk into the original size") 162 truncate(curSizeInBytes) 163 return false 164 } else { 165 throw IOException("Failed to allocate space", e) 166 } 167 } 168 } 169 170 @Throws(IOException::class) 171 fun shrinkToMinimumSize(): Long { 172 // Fix filesystem before resizing. 173 runE2fsck(rootPartition) 174 175 val p: String = rootPartition.toAbsolutePath().toString() 176 runCommand("/system/bin/resize2fs", "-M", p) 177 Log.d(TAG, "resize2fs -M completed: $rootPartition") 178 179 // resize2fs may result in an inconsistent filesystem state. Fix with e2fsck. 180 runE2fsck(rootPartition) 181 return getApparentSize() 182 } 183 184 @Throws(IOException::class) 185 fun truncate(size: Long) { 186 try { 187 RandomAccessFile(rootPartition.toFile(), "rw").use { raf -> Os.ftruncate(raf.fd, size) } 188 Log.d(TAG, "Truncated space to: $size bytes") 189 } catch (e: ErrnoException) { 190 Log.e(TAG, "Failed to truncate space", e) 191 throw IOException("Failed to truncate space", e) 192 } 193 } 194 195 companion object { 196 private const val INSTALL_DIRNAME = "linux" 197 private const val ROOTFS_FILENAME = "root_part" 198 private const val BACKUP_FILENAME = "root_part_backup" 199 private const val CONFIG_FILENAME = "vm_config.json" 200 private const val BUILD_ID_FILENAME = "build_id" 201 const val MARKER_FILENAME: String = "completed" 202 203 const val RESIZE_STEP_BYTES: Long = 4 shl 20 // 4 MiB 204 const val RELEASE_YEAR: Int = 2025 205 206 /** Returns InstalledImage for a given app context */ 207 fun getDefault(context: Context): InstalledImage { 208 val installDir = context.getFilesDir().toPath().resolve(INSTALL_DIRNAME) 209 return InstalledImage(installDir) 210 } 211 212 @Throws(IOException::class) 213 private fun runE2fsck(path: Path) { 214 val p: String = path.toAbsolutePath().toString() 215 runCommand("/system/bin/e2fsck", "-y", "-f", p) 216 Log.d(TAG, "e2fsck completed: $path") 217 } 218 219 @Throws(IOException::class) 220 private fun resizeFilesystem(path: Path, sizeInBytes: Long) { 221 val sizeInMB = sizeInBytes / (1024 * 1024) 222 if (sizeInMB == 0L) { 223 Log.e(TAG, "Invalid size: $sizeInBytes bytes") 224 throw IllegalArgumentException("Size cannot be zero MB") 225 } 226 val sizeArg = sizeInMB.toString() + "M" 227 val p: String = path.toAbsolutePath().toString() 228 runCommand("/system/bin/resize2fs", p, sizeArg) 229 Log.d(TAG, "resize2fs completed: $path, size: $sizeArg") 230 } 231 232 @Throws(IOException::class) 233 private fun runCommand(vararg command: String): String { 234 try { 235 val process = ProcessBuilder(*command).redirectErrorStream(true).start() 236 process.waitFor() 237 val result = String(process.inputStream.readAllBytes()) 238 if (process.exitValue() != 0) { 239 Log.w( 240 TAG, 241 "Process returned with error, command=${listOf(*command).joinToString(" ")}," + 242 "exitValue=${process.exitValue()}, result=$result", 243 ) 244 } 245 return result 246 } catch (e: InterruptedException) { 247 Thread.currentThread().interrupt() 248 throw IOException("Command interrupted", e) 249 } 250 } 251 252 internal fun roundUp(bytes: Long): Long { 253 // Round up every diskSizeStep MB 254 return ceil((bytes.toDouble()) / RESIZE_STEP_BYTES).toLong() * RESIZE_STEP_BYTES 255 } 256 } 257 } 258