• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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