Support preserving assembly/runtime files

When using overlays, they are placed in cuttlefish_runtime which causes
them to be deleted on every invocation of launch_cvd. This change allows
preserving some files in cuttlefish_assembly and cuttlefish_runtime
between invocations.

Test: launch_cvd --num_instances=2, modify /data, re-launch
Bug: 148242646
Change-Id: Ibd5ba2969da4f2653e9122a91259e332ade9f342
diff --git a/host/commands/assemble_cvd/flags.cc b/host/commands/assemble_cvd/flags.cc
index 6d8e170..5e9cb57 100644
--- a/host/commands/assemble_cvd/flags.cc
+++ b/host/commands/assemble_cvd/flags.cc
@@ -1,6 +1,10 @@
 #include "host/commands/assemble_cvd/flags.h"
 
+#include <dirent.h>
+#include <sys/types.h>
+#include <sys/stat.h>
 #include <sys/statvfs.h>
+#include <unistd.h>
 
 #include <algorithm>
 #include <iostream>
@@ -161,6 +165,11 @@
              "bootloader. It will default to 'a' if empty and not using a "
              "bootloader.");
 DEFINE_int32(num_instances, 1, "Number of Android guests to launch");
+DEFINE_bool(resume, true, "Resume using the disk from the last session, if "
+                          "possible. i.e., if --noresume is passed, the disk "
+                          "will be reset to the state it was initially launched "
+                          "in. This flag is ignored if the underlying partition "
+                          "images have been updated since the first launch.");
 
 namespace {
 
@@ -459,8 +468,77 @@
   return ResolveInstanceFiles();
 }
 
-bool CleanPriorFiles(const std::vector<std::string>& paths) {
-  std::string prior_files = android::base::Join(paths, " ");
+std::string cpp_basename(const std::string& str) {
+  char* copy = strdup(str.c_str()); // basename may modify its argument
+  std::string ret(basename(copy));
+  free(copy);
+  return ret;
+}
+
+bool CleanPriorFiles(const std::string& path, const std::set<std::string>& preserving) {
+  if (preserving.count(cpp_basename(path))) {
+    LOG(INFO) << "Preserving: " << path;
+    return true;
+  }
+  struct stat statbuf;
+  if (lstat(path.c_str(), &statbuf) < 0) {
+    int error_num = errno;
+    if (error_num == ENOENT) {
+      return true;
+    } else {
+      LOG(ERROR) << "Could not stat \"" << path << "\": " << strerror(error_num);
+      return false;
+    }
+  }
+  if ((statbuf.st_mode & S_IFMT) != S_IFDIR) {
+    LOG(INFO) << "Deleting: " << path;
+    if (unlink(path.c_str()) < 0) {
+      int error_num = errno;
+      LOG(ERROR) << "Could not unlink \"" << path << "\", error was " << strerror(error_num);
+      return false;
+    }
+    return true;
+  }
+  std::unique_ptr<DIR, int(*)(DIR*)> dir(opendir(path.c_str()), closedir);
+  if (!dir) {
+    int error_num = errno;
+    LOG(ERROR) << "Could not clean \"" << path << "\": error was " << strerror(error_num);
+    return false;
+  }
+  for (auto entity = readdir(dir.get()); entity != nullptr; entity = readdir(dir.get())) {
+    std::string entity_name(entity->d_name);
+    if (entity_name == "." || entity_name == "..") {
+      continue;
+    }
+    std::string entity_path = path + "/" + entity_name;
+    if (!CleanPriorFiles(entity_path.c_str(), preserving)) {
+      return false;
+    }
+  }
+  if (rmdir(path.c_str()) < 0) {
+    if (!(errno == EEXIST || errno == ENOTEMPTY)) {
+      // If EEXIST or ENOTEMPTY, probably because a file was preserved
+      int error_num = errno;
+      LOG(ERROR) << "Could not rmdir \"" << path << "\", error was " << strerror(error_num);
+      return false;
+    }
+  }
+  return true;
+}
+
+bool CleanPriorFiles(const std::vector<std::string>& paths, const std::set<std::string>& preserving) {
+  std::string prior_files;
+  for (auto path : paths) {
+    struct stat statbuf;
+    if (stat(path.c_str(), &statbuf) < 0 && errno != ENOENT) {
+      // If ENOENT, it doesn't exist yet, so there is no work to do'
+      int error_num = errno;
+      LOG(ERROR) << "Could not stat \"" << path << "\": " << strerror(error_num);
+      return false;
+    }
+    bool is_directory = (statbuf.st_mode & S_IFMT) == S_IFDIR;
+    prior_files += (is_directory ? (path + "/*") : path) + " ";
+  }
   LOG(INFO) << "Assuming prior files of " << prior_files;
   std::string lsof_cmd = "lsof -t " + prior_files + " >/dev/null 2>&1";
   int rval = std::system(lsof_cmd.c_str());
@@ -469,19 +547,19 @@
     LOG(ERROR) << "Clean aborted: files are in use";
     return false;
   }
-  std::string clean_command = "rm -rf " + prior_files;
-  rval = std::system(clean_command.c_str());
-  if (WEXITSTATUS(rval) != 0) {
-    LOG(ERROR) << "Remove of files failed";
-    return false;
+  for (const auto& path : paths) {
+    if (!CleanPriorFiles(path, preserving)) {
+      LOG(ERROR) << "Remove of file under \"" << path << "\" failed";
+      return false;
+    }
   }
   return true;
 }
 
-bool CleanPriorFiles(const vsoc::CuttlefishConfig& config) {
+bool CleanPriorFiles(const vsoc::CuttlefishConfig& config, const std::set<std::string>& preserving) {
   std::vector<std::string> paths = {
     // Everything in the assembly directory
-    FLAGS_assembly_dir + "/*",
+    FLAGS_assembly_dir,
     // The environment file
     GetCuttlefishEnvPath(),
     // The global link to the config file
@@ -490,7 +568,7 @@
   for (const auto& instance : config.Instances()) {
     paths.push_back(instance.instance_dir());
   }
-  return CleanPriorFiles(paths);
+  return CleanPriorFiles(paths, preserving);
 }
 
 bool DecompressKernel(const std::string& src, const std::string& dst) {
@@ -548,23 +626,20 @@
   return partitions;
 }
 
-bool ShouldCreateCompositeDisk() {
-  if (FLAGS_vm_manager == vm_manager::CrosvmManager::name()) {
-    // The crosvm implementation is very fast to rebuild but also more brittle due to being split
-    // into multiple files. The QEMU implementation is slow to build, but completely self-contained
-    // at that point. Therefore, always rebuild on crosvm but check if it is necessary for QEMU.
-    return true;
-  }
-  auto composite_age = cvd::FileModificationTime(FLAGS_composite_disk);
+std::chrono::system_clock::time_point LastUpdatedInputDisk() {
+  std::chrono::system_clock::time_point ret;
   for (auto& partition : disk_config()) {
-    auto partition_age = cvd::FileModificationTime(partition.image_file_path);
-    if (partition_age >= composite_age) {
-      LOG(INFO) << "composite disk age was \"" << std::chrono::system_clock::to_time_t(composite_age) << "\", "
-                << "partition age was \"" << std::chrono::system_clock::to_time_t(partition_age) << "\"";
-      return true;
+    auto partition_mod_time = cvd::FileModificationTime(partition.image_file_path);
+    if (partition_mod_time > ret) {
+      ret = partition_mod_time;
     }
   }
-  return false;
+  return ret;
+}
+
+bool ShouldCreateCompositeDisk() {
+  auto composite_age = cvd::FileModificationTime(FLAGS_composite_disk);
+  return composite_age < LastUpdatedInputDisk();
 }
 
 bool ConcatRamdisks(const std::string& new_ramdisk_path, const std::string& ramdisk_a_path,
@@ -618,10 +693,6 @@
     std::string header_path = config.AssemblyPath("gpt_header.img");
     std::string footer_path = config.AssemblyPath("gpt_footer.img");
     CreateCompositeDisk(disk_config(), header_path, footer_path, FLAGS_composite_disk);
-    for (auto instance : config.Instances()) {
-      auto overlay_path = instance.PerInstancePath("overlay.img");
-      CreateQcowOverlay(config.crosvm_binary(), FLAGS_composite_disk, overlay_path);
-    }
   } else {
     auto existing_size = cvd::FileSize(FLAGS_composite_disk);
     auto available_space = AvailableSpaceAtPath(FLAGS_composite_disk);
@@ -654,14 +725,27 @@
     // two operations, as those will assume they can read the config object from
     // disk.
     auto config = InitializeCuttlefishConfiguration(*boot_img_unpacker, fetcher_config);
-    if (!CleanPriorFiles(config)) {
+    std::set<std::string> preserving;
+    if (FLAGS_resume && ShouldCreateCompositeDisk()) {
+      LOG(WARNING) << "Requested resuming a previous session (the default behavior) "
+                   << "but the base images have changed under the overlay, making the "
+                   << "overlay incompatible. Wiping the overlay files.";
+    } else if (FLAGS_resume && !ShouldCreateCompositeDisk()) {
+      preserving.insert("overlay.img");
+      preserving.insert("gpt_header.img");
+      preserving.insert("gpt_footer.img");
+      preserving.insert("composite.img");
+      preserving.insert("access-kregistry");
+    }
+    if (!CleanPriorFiles(config, preserving)) {
       LOG(ERROR) << "Failed to clean prior files";
       exit(AssemblerExitCodes::kPrioFilesCleanupError);
     }
     // Create assembly directory if it doesn't exist.
     if (!cvd::DirectoryExists(FLAGS_assembly_dir.c_str())) {
       LOG(INFO) << "Setting up " << FLAGS_assembly_dir;
-      if (mkdir(FLAGS_assembly_dir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) < 0) {
+      if (mkdir(FLAGS_assembly_dir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) < 0
+          && errno != EEXIST) {
         LOG(ERROR) << "Failed to create assembly directory: "
                   << FLAGS_assembly_dir << ". Error: " << errno;
         exit(AssemblerExitCodes::kAssemblyDirCreationError);
@@ -671,7 +755,8 @@
       // Create instance directory if it doesn't exist.
       if (!cvd::DirectoryExists(instance.instance_dir().c_str())) {
         LOG(INFO) << "Setting up " << FLAGS_instance_dir << ".N";
-        if (mkdir(instance.instance_dir().c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) < 0) {
+        if (mkdir(instance.instance_dir().c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) < 0
+            && errno != EEXIST) {
           LOG(ERROR) << "Failed to create instance directory: "
                     << FLAGS_instance_dir << ". Error: " << errno;
           exit(AssemblerExitCodes::kInstanceDirCreationError);
@@ -679,8 +764,8 @@
       }
       auto internal_dir = instance.instance_dir() + "/" + vsoc::kInternalDirName;
       if (!cvd::DirectoryExists(internal_dir)) {
-        if (mkdir(internal_dir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) <
-            0) {
+        if (mkdir(internal_dir.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) < 0
+           && errno != EEXIST) {
           LOG(ERROR) << "Failed to create internal instance directory: "
                     << internal_dir << ". Error: " << errno;
           exit(AssemblerExitCodes::kInstanceDirCreationError);
@@ -791,6 +876,19 @@
   }
 
   for (auto instance : config->Instances()) {
+    auto overlay_path = instance.PerInstancePath("overlay.img");
+    if (!cvd::FileExists(overlay_path) || ShouldCreateCompositeDisk() || !FLAGS_resume
+        || cvd::FileModificationTime(overlay_path) < cvd::FileModificationTime(FLAGS_composite_disk)) {
+      if (FLAGS_resume) {
+        LOG(WARNING) << "Requested to continue an existing session, but the overlay was "
+                     << "newer than its underlying composite disk. Wiping the overlay.";
+      }
+      CreateQcowOverlay(config->crosvm_binary(), FLAGS_composite_disk, overlay_path);
+      CreateBlankImage(instance.access_kregistry_path(), 1, "none", "64K");
+    }
+  }
+
+  for (auto instance : config->Instances()) {
     // Check that the files exist
     for (const auto& file : instance.virtual_disk_paths()) {
       if (!file.empty() && !cvd::FileHasContent(file.c_str())) {