Merge "Disable LLMNR in the guest VM" into main
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index 2711af0..733a72b 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -9,7 +9,6 @@
"java/**/*.kt",
],
resource_dirs: ["res"],
- asset_dirs: ["assets"],
static_libs: [
"androidx-constraintlayout_constraintlayout",
"androidx.window_window",
diff --git a/android/TerminalApp/assets/.gitkeep b/android/TerminalApp/assets/.gitkeep
deleted file mode 100644
index e69de29..0000000
--- a/android/TerminalApp/assets/.gitkeep
+++ /dev/null
diff --git a/android/TerminalApp/assets/client.p12 b/android/TerminalApp/assets/client.p12
deleted file mode 100644
index f1f5820..0000000
--- a/android/TerminalApp/assets/client.p12
+++ /dev/null
Binary files differ
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
index e1342e9..cec1b7a 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ConfigJson.java
@@ -35,8 +35,16 @@
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
+import java.io.BufferedReader;
import java.io.FileReader;
+import java.io.IOException;
+import java.io.PipedReader;
+import java.io.PipedWriter;
+import java.io.Reader;
+import java.nio.file.Path;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Objects;
/** This class and its inner classes model vm_config.json. */
@@ -69,14 +77,54 @@
private GpuJson gpu;
/** Parses JSON file at jsonPath */
- static ConfigJson from(String jsonPath) {
- try (FileReader r = new FileReader(jsonPath)) {
+ static ConfigJson from(Context context, Path jsonPath) {
+ try (FileReader fileReader = new FileReader(jsonPath.toFile());
+ Reader r = replaceKeywords(fileReader, context)) {
return new Gson().fromJson(r, ConfigJson.class);
} catch (Exception e) {
throw new RuntimeException("Failed to parse " + jsonPath, e);
}
}
+ private static Reader replaceKeywords(Reader r, Context context) throws IOException {
+ PipedWriter pipeIn = new PipedWriter();
+ PipedReader pipeOut = new PipedReader();
+ pipeOut.connect(pipeIn);
+
+ Map<String, String> rules = new HashMap<>();
+ rules.put("\\$PAYLOAD_DIR", InstallUtils.getInternalStorageDir(context).toString());
+ rules.put("\\$USER_ID", String.valueOf(context.getUserId()));
+ rules.put("\\$PACKAGE_NAME", context.getPackageName());
+ String appDataDir = context.getDataDir().toString();
+ // TODO: remove this hack
+ if (context.getUserId() == 0) {
+ appDataDir = "/data/data/" + context.getPackageName();
+ }
+ rules.put("\\$APP_DATA_DIR", appDataDir);
+
+ try (BufferedReader br = new BufferedReader(r)) {
+ br.lines()
+ .map(
+ line -> {
+ for (Map.Entry<String, String> rule : rules.entrySet()) {
+ line = line.replaceAll(rule.getKey(), rule.getValue());
+ }
+ return line;
+ })
+ .forEach(
+ line -> {
+ try {
+ pipeIn.write(line);
+ pipeIn.write('\n');
+ } catch (IOException e) {
+ // this cannot happen as it is connected to a pipe.
+ throw new RuntimeException(e);
+ }
+ });
+ }
+ return pipeOut;
+ }
+
private int getCpuTopology() {
switch (cpu_topology) {
case "one_cpu":
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
new file mode 100644
index 0000000..12f485a
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/ImageArchive.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.virtualization.terminal;
+
+import android.os.Build;
+import android.os.Environment;
+
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Arrays;
+import java.util.function.Function;
+
+/**
+ * ImageArchive models the archive file (images.tar.gz) where VM payload files are in. This class
+ * provides methods for handling the archive file, most importantly installing it.
+ */
+class ImageArchive {
+ private static final String DIR_IN_SDCARD = "linux";
+ private static final String ARCHIVE_NAME = "images.tar.gz";
+ private static final String HOST_URL = "https://dl.google.com/android/ferrochrome/latest";
+
+ // Only one can be non-null
+ private final URL mUrl;
+ private final Path mPath;
+
+ private ImageArchive(URL url) {
+ mUrl = url;
+ mPath = null;
+ }
+
+ private ImageArchive(Path path) {
+ mUrl = null;
+ mPath = path;
+ }
+
+ /** Creates ImageArchive which is located in the sdcard. This archive is for testing only. */
+ public static ImageArchive fromSdCard() {
+ Path dir = Environment.getExternalStoragePublicDirectory(DIR_IN_SDCARD).toPath();
+ Path file = dir.resolve(ARCHIVE_NAME);
+ return new ImageArchive(file);
+ }
+
+ /** Creates ImageArchive which is hosted in the Google server. This is the official archive. */
+ public static ImageArchive fromInternet() {
+ String arch = Arrays.asList(Build.SUPPORTED_ABIS).contains("x86_64") ? "x86_64" : "aarch64";
+ try {
+ URL url = new URL(HOST_URL + "/" + arch + "/" + ARCHIVE_NAME);
+ return new ImageArchive(url);
+ } catch (MalformedURLException e) {
+ // cannot happen
+ throw new RuntimeException(e);
+ }
+ }
+
+ private InputStream getInputStream(Function<InputStream, InputStream> filter)
+ throws IOException {
+ InputStream is = mPath != null ? new FileInputStream(mPath.toFile()) : mUrl.openStream();
+ BufferedInputStream bufStream = new BufferedInputStream(is);
+ return filter == null ? bufStream : filter.apply(bufStream);
+ }
+
+ /**
+ * Installs this ImageArchive to a directory pointed by path. filter can be supplied to provide
+ * an additional input stream which will be used during the installation.
+ */
+ public void installTo(Path path, Function<InputStream, InputStream> filter) throws IOException {
+ try (InputStream stream = getInputStream(filter);
+ GzipCompressorInputStream gzStream = new GzipCompressorInputStream(stream);
+ TarArchiveInputStream tarStream = new TarArchiveInputStream(gzStream)) {
+
+ Files.createDirectories(path);
+ ArchiveEntry entry;
+ while ((entry = tarStream.getNextEntry()) != null) {
+ Path to = path.resolve(entry.getName());
+ if (Files.isDirectory(to)) {
+ Files.createDirectories(to);
+ } else {
+ Files.copy(tarStream, to, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+ }
+ postInstall();
+ }
+
+ // To save storage, delete the source archive on the disk.
+ private void postInstall() throws IOException {
+ if (mPath != null) {
+ Files.deleteIfExists(mPath);
+ }
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java
index 71f2a2d..1b6da6c 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java
@@ -35,9 +35,6 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.Function;
public class InstallUtils {
private static final String VM_CONFIG_FILENAME = "vm_config.json";
@@ -47,8 +44,8 @@
private static final String INSTALLATION_COMPLETED_FILENAME = "completed";
private static final String PAYLOAD_DIR = "linux";
- public static String getVmConfigPath(Context context) {
- return getInternalStorageDir(context).toPath().resolve(VM_CONFIG_FILENAME).toString();
+ public static Path getVmConfigPath(Context context) {
+ return getInternalStorageDir(context).resolve(VM_CONFIG_FILENAME);
}
public static boolean isImageInstalled(Context context) {
@@ -57,15 +54,16 @@
public static void backupRootFs(Context context) throws IOException {
Files.move(
- getRootfsFile(context).toPath(),
- getBackupFile(context).toPath(),
+ getRootfsFile(context),
+ getBackupFile(context),
StandardCopyOption.REPLACE_EXISTING);
}
public static boolean createInstalledMarker(Context context) {
try {
- File file = new File(getInstallationCompletedPath(context).toString());
- return file.createNewFile();
+ Path path = getInstallationCompletedPath(context);
+ Files.createFile(path);
+ return true;
} catch (IOException e) {
Log.e(TAG, "Failed to mark install completed", e);
return false;
@@ -74,7 +72,7 @@
@VisibleForTesting
public static void deleteInstallation(Context context) {
- FileUtils.deleteContentsAndDir(getInternalStorageDir(context));
+ FileUtils.deleteContentsAndDir(getInternalStorageDir(context).toFile());
}
private static Path getPayloadPath() {
@@ -91,16 +89,16 @@
return Files.exists(getPayloadPath());
}
- public static File getInternalStorageDir(Context context) {
- return new File(context.getFilesDir(), PAYLOAD_DIR);
+ public static Path getInternalStorageDir(Context context) {
+ return context.getFilesDir().toPath().resolve(PAYLOAD_DIR);
}
- public static File getBackupFile(Context context) {
- return new File(context.getFilesDir(), BACKUP_FILENAME);
+ public static Path getBackupFile(Context context) {
+ return context.getFilesDir().toPath().resolve(BACKUP_FILENAME);
}
private static Path getInstallationCompletedPath(Context context) {
- return getInternalStorageDir(context).toPath().resolve(INSTALLATION_COMPLETED_FILENAME);
+ return getInternalStorageDir(context).resolve(INSTALLATION_COMPLETED_FILENAME);
}
public static boolean installImageFromExternalStorage(Context context) {
@@ -128,15 +126,6 @@
Log.e(TAG, "installation failed", e);
return false;
}
- if (!resolvePathInVmConfig(context)) {
- Log.d(TAG, "resolving path failed");
- try {
- Files.deleteIfExists(Path.of(getVmConfigPath(context)));
- } catch (IOException e) {
- return false;
- }
- return false;
- }
// remove payload if installation is done.
try {
@@ -149,46 +138,12 @@
return createInstalledMarker(context);
}
- private static Function<String, String> getReplacer(Context context) {
- Map<String, String> rules = new HashMap<>();
- rules.put("\\$PAYLOAD_DIR", new File(context.getFilesDir(), PAYLOAD_DIR).toString());
- rules.put("\\$USER_ID", String.valueOf(context.getUserId()));
- rules.put("\\$PACKAGE_NAME", context.getPackageName());
- String appDataDir = context.getDataDir().toString();
- // TODO: remove this hack
- if (context.getUserId() == 0) {
- appDataDir = "/data/data/" + context.getPackageName();
- }
- rules.put("\\$APP_DATA_DIR", appDataDir);
- return (s) -> {
- for (Map.Entry<String, String> rule : rules.entrySet()) {
- s = s.replaceAll(rule.getKey(), rule.getValue());
- }
- return s;
- };
- }
-
- public static boolean resolvePathInVmConfig(Context context) {
- try {
- String replacedVmConfig =
- String.join(
- "\n",
- Files.readAllLines(Path.of(getVmConfigPath(context))).stream()
- .map(getReplacer(context))
- .toList());
- Files.write(Path.of(getVmConfigPath(context)), replacedVmConfig.getBytes());
- return true;
- } catch (IOException e) {
- return false;
- }
- }
-
- public static File getRootfsFile(Context context) throws FileNotFoundException {
- File file = new File(getInternalStorageDir(context), ROOTFS_FILENAME);
- if (!file.exists()) {
- Log.d(TAG, file.getAbsolutePath() + " - file not found");
+ public static Path getRootfsFile(Context context) throws FileNotFoundException {
+ Path path = getInternalStorageDir(context).resolve(ROOTFS_FILENAME);
+ if (!Files.exists(path)) {
+ Log.d(TAG, path.toString() + " - file not found");
throw new FileNotFoundException("File not found: " + ROOTFS_FILENAME);
}
- return file;
+ return path;
}
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
index 6fd3b5c..4b2e640 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
@@ -177,7 +177,7 @@
}
private void reLabelImagesSELinuxContext() {
- File payloadFolder = InstallUtils.getInternalStorageDir(this);
+ File payloadFolder = InstallUtils.getInternalStorageDir(this).toFile();
// The context should be u:object_r:privapp_data_file:s0:c35,c257,c512,c768
// and we want to get s0:c35,c257,c512,c768 part
@@ -236,7 +236,7 @@
TarArchiveInputStream tar =
new TarArchiveInputStream(new GzipCompressorInputStream(wifiInputStream))) {
ArchiveEntry entry;
- Path baseDir = InstallUtils.getInternalStorageDir(this).toPath();
+ Path baseDir = InstallUtils.getInternalStorageDir(this);
Files.createDirectories(baseDir);
while ((entry = tar.getNextEntry()) != null) {
Path extractTo = baseDir.resolve(entry.getName());
@@ -261,10 +261,6 @@
return false;
}
- if (!InstallUtils.resolvePathInVmConfig(this)) {
- notifyError(getString(R.string.installer_error_unknown));
- return false;
- }
return InstallUtils.createInstalledMarker(this);
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index bc5d037..a3b0577 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -61,7 +61,6 @@
import com.google.android.material.appbar.MaterialToolbar;
-import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -70,6 +69,8 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
@@ -334,33 +335,29 @@
.start();
}
- private void diskResize(File file, long sizeInBytes) throws IOException {
+ private void diskResize(Path path, long sizeInBytes) throws IOException {
try {
if (sizeInBytes == 0) {
return;
}
- String filePath = file.getAbsolutePath();
- Log.d(TAG, "Disk-resize in progress for partition: " + filePath);
+ Log.d(TAG, "Disk-resize in progress for partition: " + path);
- long currentSize = Os.stat(filePath).st_size;
- runE2fsck(filePath);
+ long currentSize = Files.size(path);
+ runE2fsck(path);
if (sizeInBytes > currentSize) {
- allocateSpace(file, sizeInBytes);
+ allocateSpace(path, sizeInBytes);
}
- resizeFilesystem(filePath, sizeInBytes);
- } catch (ErrnoException e) {
- Log.e(TAG, "ErrnoException during disk resize", e);
- throw new IOException("ErrnoException during disk resize", e);
+ resizeFilesystem(path, sizeInBytes);
} catch (IOException e) {
Log.e(TAG, "Failed to resize disk", e);
throw e;
}
}
- private static void allocateSpace(File file, long sizeInBytes) throws IOException {
+ private static void allocateSpace(Path path, long sizeInBytes) throws IOException {
try {
- RandomAccessFile raf = new RandomAccessFile(file, "rw");
+ RandomAccessFile raf = new RandomAccessFile(path.toFile(), "rw");
FileDescriptor fd = raf.getFD();
Os.posix_fallocate(fd, 0, sizeInBytes);
raf.close();
@@ -371,17 +368,18 @@
}
}
- private static void runE2fsck(String filePath) throws IOException {
+ private static void runE2fsck(Path path) throws IOException {
try {
- runCommand("/system/bin/e2fsck", "-y", "-f", filePath);
- Log.d(TAG, "e2fsck completed: " + filePath);
+ String p = path.toAbsolutePath().toString();
+ runCommand("/system/bin/e2fsck", "-y", "-f", p);
+ Log.d(TAG, "e2fsck completed: " + path);
} catch (IOException e) {
Log.e(TAG, "Failed to run e2fsck", e);
throw e;
}
}
- private static void resizeFilesystem(String filePath, long sizeInBytes) throws IOException {
+ private static void resizeFilesystem(Path path, long sizeInBytes) throws IOException {
long sizeInMB = sizeInBytes / (1024 * 1024);
if (sizeInMB == 0) {
Log.e(TAG, "Invalid size: " + sizeInBytes + " bytes");
@@ -389,8 +387,9 @@
}
String sizeArg = sizeInMB + "M";
try {
- runCommand("/system/bin/resize2fs", filePath, sizeArg);
- Log.d(TAG, "resize2fs completed: " + filePath + ", size: " + sizeArg);
+ String p = path.toAbsolutePath().toString();
+ runCommand("/system/bin/resize2fs", p, sizeArg);
+ Log.d(TAG, "resize2fs completed: " + path + ", size: " + sizeArg);
} catch (IOException e) {
Log.e(TAG, "Failed to run resize2fs", e);
throw e;
@@ -591,10 +590,11 @@
return (long) Math.ceil(((double) diskSize) / diskSizeStep) * diskSizeStep;
}
- public static long getMinFilesystemSize(File file) throws IOException, NumberFormatException {
+ public static long getMinFilesystemSize(Path path) throws IOException, NumberFormatException {
try {
- runE2fsck(file.getAbsolutePath());
- String result = runCommand("/system/bin/resize2fs", "-P", file.getAbsolutePath());
+ runE2fsck(path);
+ String p = path.toAbsolutePath().toString();
+ String result = runCommand("/system/bin/resize2fs", "-P", p);
// The return value is the number of 4k block
long minSize = Long.parseLong(
result.lines().toArray(String[]::new)[1].substring(42)) * 4 * 1024;
@@ -605,13 +605,13 @@
}
}
- private static long getFilesystemSize(File file) throws ErrnoException {
- return Os.stat(file.getAbsolutePath()).st_size;
+ private static long getFilesystemSize(Path fsPath) throws ErrnoException {
+ return Os.stat(fsPath.toAbsolutePath().toString()).st_size;
}
private void resizeDiskIfNecessary() {
try {
- File file = InstallUtils.getRootfsFile(this);
+ Path file = InstallUtils.getRootfsFile(this);
SharedPreferences sharedPref = this.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
index e291b57..4b6bf96 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
@@ -27,6 +27,7 @@
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import java.io.IOException
+import java.nio.file.Files
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -55,7 +56,7 @@
dialog.show()
}
val resetBackupCard = findViewById<View>(R.id.settings_recovery_reset_backup_card)
- resetBackupCard.isVisible = InstallUtils.getBackupFile(this).exists()
+ resetBackupCard.isVisible = Files.exists(InstallUtils.getBackupFile(this))
resetBackupCard.setOnClickListener {
val dialog = MaterialAlertDialogBuilder(this)
@@ -73,7 +74,10 @@
}
private fun removeBackup(): Unit {
- if (!InstallUtils.getBackupFile(this@SettingsRecoveryActivity).delete()) {
+ val file = InstallUtils.getBackupFile(this@SettingsRecoveryActivity)
+ try {
+ Files.deleteIfExists(file)
+ } catch (e: IOException) {
Snackbar.make(
findViewById(android.R.id.content),
R.string.settings_recovery_error_during_removing_backup,
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
index 652af8e..879b5e5 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
@@ -50,6 +50,7 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Set;
@@ -151,13 +152,13 @@
}
mExecutorService = Executors.newCachedThreadPool();
- ConfigJson json = ConfigJson.from(InstallUtils.getVmConfigPath(this));
+ ConfigJson json = ConfigJson.from(this, InstallUtils.getVmConfigPath(this));
VirtualMachineConfig.Builder configBuilder = json.toConfigBuilder(this);
VirtualMachineCustomImageConfig.Builder customImageConfigBuilder =
json.toCustomImageConfigBuilder(this);
- File backupFile = InstallUtils.getBackupFile(this);
- if (backupFile.exists()) {
- customImageConfigBuilder.addDisk(Disk.RWDisk(backupFile.getPath()));
+ Path backupFile = InstallUtils.getBackupFile(this);
+ if (Files.exists(backupFile)) {
+ customImageConfigBuilder.addDisk(Disk.RWDisk(backupFile.toString()));
configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
}
VirtualMachineConfig config = configBuilder.build();