Install vm payload from /sdcard to /data/data/app
Bug: 373248801
Bug: 374244795
Test: build and run
Change-Id: I96124eb14bacc03365999f8d7ee31a8ff17ceb99
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index e5e8b0a..932ca76 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -26,15 +26,3 @@
"com.android.virt",
],
}
-
-filegroup {
- name: "linux_vm_setup.rc",
- srcs: ["linux_vm_setup.rc"],
-}
-
-sh_binary {
- name: "linux_vm_setup",
- src: "linux_vm_setup.sh",
- init_rc: [":linux_vm_setup.rc"],
- host_supported: false,
-}
diff --git a/android/TerminalApp/generate_assets.sh b/android/TerminalApp/generate_assets.sh
deleted file mode 100755
index 4001bfd..0000000
--- a/android/TerminalApp/generate_assets.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/bin/bash
-set -e
-
-if [ "$#" -ne 1 ]; then
- echo "$0 <image.raw path>"
- echo "image.raw can be built with packages/modules/Virtualization/build/debian/build.sh"
- exit 1
-fi
-image_raw_path=$(realpath $1)
-pushd $(dirname $0) > /dev/null
-tempdir=$(mktemp -d)
-asset_dir=./assets/linux
-mkdir -p ${asset_dir}
-echo Copy files...
-pushd ${tempdir} > /dev/null
-cp "${image_raw_path}" ${tempdir}
-tar czvS -f images.tar.gz $(basename ${image_raw_path})
-popd > /dev/null
-cp vm_config.json ${asset_dir}
-mv ${tempdir}/images.tar.gz ${asset_dir}
-echo Calculating hash...
-hash=$(cat ${asset_dir}/images.tar.gz ${asset_dir}/vm_config.json | sha1sum | cut -d' ' -f 1)
-echo ${hash} > ${asset_dir}/hash
-popd > /dev/null
-echo Cleaning up...
-rm -rf ${tempdir}
-
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
index 1c739e2..a49ea72 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -16,37 +16,20 @@
package com.android.virtualization.terminal;
-import android.annotation.WorkerThread;
import android.app.Activity;
+import android.os.Build;
import android.os.Bundle;
-import android.os.Environment;
-import android.os.SystemProperties;
-import android.text.TextUtils;
import android.util.Log;
import android.widget.TextView;
-import libcore.io.Streams;
+import com.android.virtualization.vmlauncher.InstallUtils;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class InstallerActivity extends Activity {
private static final String TAG = "LinuxInstaller";
- private static final Path DEST_DIR =
- Path.of(Environment.getExternalStorageDirectory().getPath(), "linux");
-
- private static final String ASSET_DIR = "linux";
- private static final String HASH_FILE_NAME = "hash";
- private static final Path HASH_FILE = Path.of(DEST_DIR.toString(), HASH_FILE_NAME);
-
ExecutorService executorService = Executors.newSingleThreadExecutor();
@Override
@@ -60,115 +43,24 @@
}
private void installLinuxImage() {
- if (!hasLocalAssets()) {
- updateStatus("No local assets");
- setResult(RESULT_CANCELED, null);
- return;
+ Log.d(TAG, "installLinuxImage");
+ // Installing from sdcard is supported only in debuggable build.
+ if (Build.isDebuggable()) {
+ updateStatus("try /sdcard/linux/images.tar.gz");
+ if (InstallUtils.installImageFromExternalStorage(this)) {
+ Log.d(TAG, "success / sdcard");
+ updateStatus("image is installed from /sdcard/linux/images.tar.gz");
+ setResult(RESULT_OK);
+ finish();
+ return;
+ }
+ Log.d(TAG, "fail / sdcard");
+ updateStatus("There is no /sdcard/linux/images.tar.gz");
}
- try {
- updateImageIfNeeded();
- } catch (IOException e) {
- Log.e(TAG, "failed to update image", e);
- return;
- }
- updateStatus("Done.");
- setResult(RESULT_OK);
+ setResult(RESULT_CANCELED, null);
finish();
}
- @WorkerThread
- private boolean hasLocalAssets() {
- try {
- String[] files = getAssets().list(ASSET_DIR);
- return files != null && files.length > 0;
- } catch (IOException e) {
- Log.e(TAG, "there is an error during listing up assets", e);
- return false;
- }
- }
-
- @WorkerThread
- private void updateImageIfNeeded() throws IOException {
- if (!isUpdateNeeded()) {
- Log.d(TAG, "No update needed.");
- return;
- }
-
- try {
- if (Files.notExists(DEST_DIR)) {
- Files.createDirectory(DEST_DIR);
- }
-
- updateStatus("Copying images...");
- String[] files = getAssets().list(ASSET_DIR);
- for (String file : files) {
- updateStatus(file);
- Path dst = Path.of(DEST_DIR.toString(), file);
- updateFile(getAssets().open(ASSET_DIR + "/" + file), dst);
- }
- } catch (IOException e) {
- Log.e(TAG, "Error while updating image: " + e);
- updateStatus("Failed to update image.");
- throw e;
- }
- extractImages(DEST_DIR.toAbsolutePath().toString());
- }
-
- @WorkerThread
- private void extractImages(String destDir) throws IOException {
- updateStatus("Extracting images...");
-
- if (TextUtils.isEmpty(destDir)) {
- throw new RuntimeException("Internal error: destDir shouldn't be null");
- }
-
- SystemProperties.set("debug.linux_vm_setup.path", destDir);
- SystemProperties.set("debug.linux_vm_setup.done", "false");
- SystemProperties.set("debug.linux_vm_setup.start", "true");
- while (!SystemProperties.getBoolean("debug.linux_vm_setup.done", false)) {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- Log.e(TAG, "Error while extracting image: " + e);
- updateStatus("Failed to extract image.");
- throw new IOException("extracting image is interrupted", e);
- }
- }
- }
-
- @WorkerThread
- private boolean isUpdateNeeded() {
- Path[] pathsToCheck = {DEST_DIR, HASH_FILE};
- for (Path p : pathsToCheck) {
- if (Files.notExists(p)) {
- Log.d(TAG, p.toString() + " does not exist.");
- return true;
- }
- }
-
- try {
- String installedHash = readAll(new FileInputStream(HASH_FILE.toFile()));
- String updatedHash = readAll(getAssets().open(ASSET_DIR + "/" + HASH_FILE_NAME));
- if (installedHash.equals(updatedHash)) {
- return false;
- }
- Log.d(TAG, "Hash mismatch. Installed: " + installedHash + " Updated: " + updatedHash);
- } catch (IOException e) {
- Log.e(TAG, "Error while checking hash: " + e);
- }
- return true;
- }
-
- private static String readAll(InputStream input) throws IOException {
- return Streams.readFully(new InputStreamReader(input)).strip();
- }
-
- private static void updateFile(InputStream input, Path path) throws IOException {
- try (input) {
- Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING);
- }
- }
-
private void updateStatus(String line) {
runOnUiThread(
() -> {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index 612da12..1659931 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.content.Intent;
import android.net.http.SslError;
+import android.os.Build;
import android.os.Bundle;
import android.system.ErrnoException;
import android.system.Os;
@@ -39,6 +40,7 @@
import androidx.appcompat.app.AppCompatActivity;
+import com.android.virtualization.vmlauncher.InstallUtils;
import com.android.virtualization.vmlauncher.VmLauncherServices;
import com.google.android.material.appbar.MaterialToolbar;
@@ -77,7 +79,7 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- checkForUpdate();
+ boolean launchInstaller = installIfNecessary();
try {
// No resize for now.
long newSizeInBytes = 0;
@@ -103,6 +105,11 @@
connectToTerminalService();
readClientCertificate();
+
+ // if installer is launched, it will be handled in onActivityResult
+ if (!launchInstaller) {
+ startVm();
+ }
}
private URL getTerminalServiceUrl() {
@@ -382,12 +389,22 @@
}
}
- private void checkForUpdate() {
- Intent intent = new Intent(this, InstallerActivity.class);
- startActivityForResult(intent, REQUEST_CODE_INSTALLER);
+ private boolean installIfNecessary() {
+ // If payload from external storage exists(only for debuggable build) or there is no
+ // installed image, launch installer activity.
+ if ((Build.isDebuggable() && InstallUtils.payloadFromExternalStorageExists())
+ || !InstallUtils.isImageInstalled(this)) {
+ Intent intent = new Intent(this, InstallerActivity.class);
+ startActivityForResult(intent, REQUEST_CODE_INSTALLER);
+ return true;
+ }
+ return false;
}
private void startVm() {
+ if (!InstallUtils.isImageInstalled(this)) {
+ return;
+ }
android.os.Trace.beginAsyncSection("executeTerminal", 0);
VmLauncherServices.startVmLauncherService(this, this);
}
diff --git a/android/TerminalApp/linux_vm_setup.rc b/android/TerminalApp/linux_vm_setup.rc
deleted file mode 100644
index ac91532..0000000
--- a/android/TerminalApp/linux_vm_setup.rc
+++ /dev/null
@@ -1,23 +0,0 @@
-# Copyright 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.
-
-service linux_vm_setup /system/bin/linux_vm_setup
- user shell
- group shell media_rw
- disabled
- oneshot
- seclabel u:r:shell:s0
-
-on property:debug.linux_vm_setup.start=true
- start linux_vm_setup
diff --git a/android/TerminalApp/linux_vm_setup.sh b/android/TerminalApp/linux_vm_setup.sh
deleted file mode 100644
index 6a93f6f..0000000
--- a/android/TerminalApp/linux_vm_setup.sh
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/system/bin/sh
-
-function round_up() {
- num=$1
- div=$2
- echo $((( (( ${num} / ${div} ) + 1) * ${div} )))
-}
-
-function install() {
- src_dir=$(getprop debug.linux_vm_setup.path)
- src_dir=${src_dir/#\/storage\/emulated\//\/data\/media\/}
- dst_dir=/data/local/tmp/
-
- cat $(find ${src_dir} -name "images.tar.gz*" | sort) | tar xz -C ${dst_dir}
- cp -u ${src_dir}/vm_config.json ${dst_dir}
- chmod 666 ${dst_dir}/*
-
- if [ -f ${dst_dir}state.img ]; then
- # increase the size of state.img to the multiple of 4096
- num_blocks=$(du -b -K ${dst_dir}state.img | cut -f 1)
- required_num_blocks=$(round_up ${num_blocks} 4)
- additional_blocks=$((( ${required_num_blocks} - ${num_blocks} )))
- dd if=/dev/zero bs=512 count=${additional_blocks} >> ${dst_dir}state.img
- fi
- rm ${src_dir}/images.tar.gz*
- rm ${src_dir}/vm_config.json
-}
-
-setprop debug.linux_vm_setup.done false
-install
-setprop debug.linux_vm_setup.start false
-setprop debug.linux_vm_setup.done true
diff --git a/android/TerminalApp/vm_config.json b/android/TerminalApp/vm_config.json
deleted file mode 100644
index 474e9c3..0000000
--- a/android/TerminalApp/vm_config.json
+++ /dev/null
@@ -1,21 +0,0 @@
-
-{
- "name": "debian",
- "disks": [
- {
- "image": "/data/local/tmp/image.raw",
- "partitions": [],
- "writable": true
- }
- ],
- "protected": false,
- "cpu_topology": "match_host",
- "platform_version": "~1.0",
- "memory_mib": 4096,
- "debuggable": true,
- "console_out": true,
- "connect_console": true,
- "console_input_device": "ttyS0",
- "network": true
-}
-
diff --git a/build/apex/Android.bp b/build/apex/Android.bp
index 4b69660..f794239 100644
--- a/build/apex/Android.bp
+++ b/build/apex/Android.bp
@@ -175,10 +175,6 @@
"true": ["virtualizationservice.xml"],
default: unset,
}),
- required: select(release_flag("RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES"), {
- true: ["linux_vm_setup"],
- default: [],
- }),
}
apex_defaults {
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh
index 308661b..257b7da 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh
+++ b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh
@@ -6,7 +6,8 @@
sudo losetup -D
grep vmx /proc/cpuinfo || true
sudo ./build.sh
-tar czvS -f ${KOKORO_ARTIFACTS_DIR}/images.tar.gz image.raw vm_config.json.aarch64 --transform s/vm_config.json.aarch64/vm_config.json/
+# --sparse option isn't supported in apache-commons-compress
+tar czv -f ${KOKORO_ARTIFACTS_DIR}/images.tar.gz image.raw vm_config.json.aarch64 --transform s/vm_config.json.aarch64/vm_config.json/
mkdir -p ${KOKORO_ARTIFACTS_DIR}/logs
sudo cp -r /var/log/fai/* ${KOKORO_ARTIFACTS_DIR}/logs || true
diff --git a/build/debian/vm_config.json.aarch64 b/build/debian/vm_config.json.aarch64
index 2841e6d..c9a16bf 100644
--- a/build/debian/vm_config.json.aarch64
+++ b/build/debian/vm_config.json.aarch64
@@ -2,7 +2,7 @@
"name": "debian",
"disks": [
{
- "image": "$DATA_DIR/image.raw",
+ "image": "$PAYLOAD_DIR/image.raw",
"partitions": [],
"writable": true
}
diff --git a/libs/vm_launcher_lib/Android.bp b/libs/vm_launcher_lib/Android.bp
index 5267508..f47f6b6 100644
--- a/libs/vm_launcher_lib/Android.bp
+++ b/libs/vm_launcher_lib/Android.bp
@@ -13,6 +13,7 @@
static_libs: [
"gson",
"debian-service-grpclib-lite",
+ "apache-commons-compress",
],
libs: [
"framework-virtualization.impl",
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
new file mode 100644
index 0000000..eb6dd77
--- /dev/null
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
@@ -0,0 +1,141 @@
+/*
+ * 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.vmlauncher;
+
+import android.content.Context;
+import android.os.Environment;
+import android.util.Log;
+
+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.File;
+import java.io.IOException;
+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 TAG = InstallUtils.class.getSimpleName();
+
+ private static final String VM_CONFIG_FILENAME = "vm_config.json";
+ private static final String COMPRESSED_PAYLOAD_FILENAME = "images.tar.gz";
+ private static final String PAYLOAD_DIR = "linux";
+
+ public static String getVmConfigPath(Context context) {
+ return new File(context.getFilesDir(), PAYLOAD_DIR)
+ .toPath()
+ .resolve(VM_CONFIG_FILENAME)
+ .toString();
+ }
+
+ public static boolean isImageInstalled(Context context) {
+ return Files.exists(Path.of(getVmConfigPath(context)));
+ }
+
+ private static Path getPayloadPath() {
+ File payloadDir = Environment.getExternalStoragePublicDirectory(PAYLOAD_DIR);
+ if (payloadDir == null) {
+ Log.d(TAG, "no payload dir: " + payloadDir);
+ return null;
+ }
+ Path payloadPath = payloadDir.toPath().resolve(COMPRESSED_PAYLOAD_FILENAME);
+ return payloadPath;
+ }
+
+ public static boolean payloadFromExternalStorageExists() {
+ return Files.exists(getPayloadPath());
+ }
+
+ public static boolean installImageFromExternalStorage(Context context) {
+ if (!payloadFromExternalStorageExists()) {
+ Log.d(TAG, "no artifact file from external storage");
+ }
+ Path payloadPath = getPayloadPath();
+ try (BufferedInputStream inputStream =
+ new BufferedInputStream(Files.newInputStream(payloadPath));
+ TarArchiveInputStream tar =
+ new TarArchiveInputStream(new GzipCompressorInputStream(inputStream))) {
+ ArchiveEntry entry;
+ Path baseDir = new File(context.getFilesDir(), PAYLOAD_DIR).toPath();
+ Files.createDirectories(baseDir);
+ while ((entry = tar.getNextEntry()) != null) {
+ Path extractTo = baseDir.resolve(entry.getName());
+ if (entry.isDirectory()) {
+ Files.createDirectories(extractTo);
+ } else {
+ Files.copy(tar, extractTo, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "installation failed", e);
+ return false;
+ }
+ if (!isImageInstalled(context)) {
+ 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 {
+ Files.deleteIfExists(payloadPath);
+ } catch (IOException e) {
+ Log.d(TAG, "failed to remove installed payload", e);
+ }
+
+ return true;
+ }
+
+ 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());
+ return (s) -> {
+ for (Map.Entry<String, String> rule : rules.entrySet()) {
+ s = s.replaceAll(rule.getKey(), rule.getValue());
+ }
+ return s;
+ };
+ }
+
+ private 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;
+ }
+ }
+}
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
index 3d5c345..a59cc3d 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
@@ -40,8 +40,6 @@
public class VmLauncherService extends Service implements DebianServiceImpl.DebianServiceCallback {
private static final String TAG = "VmLauncherService";
- // TODO: this path should be from outside of this service
- private static final String VM_CONFIG_PATH = "/data/local/tmp/vm_config.json";
private static final int RESULT_START = 0;
private static final int RESULT_STOP = 1;
@@ -81,7 +79,7 @@
}
mExecutorService = Executors.newCachedThreadPool();
- ConfigJson json = ConfigJson.from(VM_CONFIG_PATH);
+ ConfigJson json = ConfigJson.from(InstallUtils.getVmConfigPath(this));
VirtualMachineConfig config = json.toConfig(this);
Runner runner;