/* * Copyright (C) 2022 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. */ #include "host/commands/cvd/server.h" #include #include #include #include #include #include #include "cvd_server.pb.h" #include "common/libs/fs/shared_buf.h" #include "common/libs/fs/shared_fd.h" #include "common/libs/utils/environment.h" #include "common/libs/utils/files.h" #include "common/libs/utils/flag_parser.h" #include "common/libs/utils/result.h" #include "common/libs/utils/subprocess.h" #include "host/commands/cvd/instance_manager.h" #include "host/libs/config/cuttlefish_config.h" namespace cuttlefish { namespace { constexpr char kHostBugreportBin[] = "cvd_internal_host_bugreport"; constexpr char kStartBin[] = "cvd_internal_start"; constexpr char kFetchBin[] = "fetch_cvd"; constexpr char kMkdirBin[] = "/bin/mkdir"; constexpr char kClearBin[] = "clear_placeholder"; // Unused, runs CvdClear() constexpr char kFleetBin[] = "fleet_placeholder"; // Unused, runs CvdFleet() constexpr char kHelpBin[] = "help_placeholder"; // Unused, prints kHelpMessage. constexpr char kHelpMessage[] = R"(Cuttlefish Virtual Device (CVD) CLI. usage: cvd Commands: help Print this message. help Print help for a command. start Start a device. stop Stop a running device. clear Stop all running devices and delete all instance and assembly directories. fleet View the current fleet status. kill-server Kill the cvd_server background process. status Check and print the state of a running instance. host_bugreport Capture a host bugreport, including configs, logs, and tombstones. Args: Each command has its own set of args. See cvd help . --clean If provided, runs cvd kill-server before the requested command. )"; const std::map CommandToBinaryMap = { {"help", kHelpBin}, {"host_bugreport", kHostBugreportBin}, {"cvd_host_bugreport", kHostBugreportBin}, {"start", kStartBin}, {"launch_cvd", kStartBin}, {"status", kStatusBin}, {"cvd_status", kStatusBin}, {"stop", kStopBin}, {"stop_cvd", kStopBin}, {"clear", kClearBin}, {"fetch", kFetchBin}, {"fetch_cvd", kFetchBin}, {"mkdir", kMkdirBin}, {"fleet", kFleetBin}}; } // namespace CvdCommandHandler::CvdCommandHandler(InstanceManager& instance_manager) : instance_manager_(instance_manager) {} Result CvdCommandHandler::CanHandle( const RequestWithStdio& request) const { auto invocation = ParseInvocation(request.Message()); return CommandToBinaryMap.find(invocation.command) != CommandToBinaryMap.end(); } Result CvdCommandHandler::Handle( const RequestWithStdio& request) { std::unique_lock interrupt_lock(interruptible_); if (interrupted_) { return CF_ERR("Interrupted"); } CF_EXPECT(CanHandle(request)); cvd::Response response; response.mutable_command_response(); auto invocation = ParseInvocation(request.Message()); auto subcommand_bin = CommandToBinaryMap.find(invocation.command); CF_EXPECT(subcommand_bin != CommandToBinaryMap.end()); auto bin = subcommand_bin->second; // HOME is used to possibly set CuttlefishConfig path env variable later. This // env variable is used by subcommands when locating the config. auto request_home = request.Message().command_request().env().find("HOME"); std::string home = request_home != request.Message().command_request().env().end() ? request_home->second : StringFromEnv("HOME", "."); // Create a copy of args before parsing, to be passed to subcommands. auto args = invocation.arguments; auto args_copy = invocation.arguments; auto host_artifacts_path = request.Message().command_request().env().find("ANDROID_HOST_OUT"); if (host_artifacts_path == request.Message().command_request().env().end()) { response.mutable_status()->set_code(cvd::Status::FAILED_PRECONDITION); response.mutable_status()->set_message( "Missing ANDROID_HOST_OUT in client environment."); return response; } if (bin == kHelpBin) { // Handle `cvd help` if (args.empty()) { WriteAll(request.Out(), kHelpMessage); response.mutable_status()->set_code(cvd::Status::OK); return response; } // Certain commands have no detailed help text. std::set builtins = {"help", "clear", "kill-server"}; auto it = CommandToBinaryMap.find(args[0]); if (it == CommandToBinaryMap.end() || builtins.find(args[0]) != builtins.end()) { WriteAll(request.Out(), kHelpMessage); response.mutable_status()->set_code(cvd::Status::OK); return response; } // Handle `cvd help ` by calling the subcommand with --help. bin = it->second; args_copy.push_back("--help"); } else if (bin == kClearBin) { *response.mutable_status() = instance_manager_.CvdClear(request.Out(), request.Err()); return response; } else if (bin == kFleetBin) { auto env_config = request.Message().command_request().env().find( kCuttlefishConfigEnvVarName); std::string config_path; if (env_config != request.Message().command_request().env().end()) { config_path = env_config->second; } *response.mutable_status() = instance_manager_.CvdFleet(request.Out(), config_path); return response; } else if (bin == kStartBin) { auto first_instance = 1; auto instance_env = request.Message().command_request().env().find("CUTTLEFISH_INSTANCE"); if (instance_env != request.Message().command_request().env().end()) { first_instance = std::stoi(instance_env->second); } auto ins_flag = GflagsCompatFlag("base_instance_num", first_instance); auto num_instances = 1; auto num_instances_flag = GflagsCompatFlag("num_instances", num_instances); CF_EXPECT(ParseFlags({ins_flag, num_instances_flag}, args)); // Track this assembly_dir in the fleet. InstanceManager::InstanceGroupInfo info; info.host_binaries_dir = host_artifacts_path->second + "/bin/"; for (int i = first_instance; i < first_instance + num_instances; i++) { info.instances.insert(i); } instance_manager_.SetInstanceGroup(home, info); } Command command("(replaced)"); if (bin == kFetchBin) { command.SetExecutable(HostBinaryPath("fetch_cvd")); } else if (bin == kMkdirBin) { command.SetExecutable(kMkdirBin); } else { auto assembly_info = CF_EXPECT(instance_manager_.GetInstanceGroup(home)); command.SetExecutable(assembly_info.host_binaries_dir + bin); } for (const std::string& arg : args_copy) { command.AddParameter(arg); } // Set CuttlefishConfig path based on assembly dir, // used by subcommands when locating the CuttlefishConfig. if (request.Message().command_request().env().count( kCuttlefishConfigEnvVarName) == 0) { auto config_path = GetCuttlefishConfigPath(home); if (config_path) { command.AddEnvironmentVariable(kCuttlefishConfigEnvVarName, *config_path); } } for (auto& it : request.Message().command_request().env()) { command.UnsetFromEnvironment(it.first); command.AddEnvironmentVariable(it.first, it.second); } // Redirect stdin, stdout, stderr back to the cvd client command.RedirectStdIO(Subprocess::StdIOChannel::kStdIn, request.In()); command.RedirectStdIO(Subprocess::StdIOChannel::kStdOut, request.Out()); command.RedirectStdIO(Subprocess::StdIOChannel::kStdErr, request.Err()); SubprocessOptions options; if (request.Message().command_request().wait_behavior() == cvd::WAIT_BEHAVIOR_START) { options.ExitWithParent(false); } const auto& working_dir = request.Message().command_request().working_directory(); if (!working_dir.empty()) { auto fd = SharedFD::Open(working_dir, O_RDONLY | O_PATH | O_DIRECTORY); CF_EXPECT(fd->IsOpen(), "Couldn't open \"" << working_dir << "\": " << fd->StrError()); command.SetWorkingDirectory(fd); } subprocess_ = command.Start(options); if (request.Message().command_request().wait_behavior() == cvd::WAIT_BEHAVIOR_START) { response.mutable_status()->set_code(cvd::Status::OK); return response; } interrupt_lock.unlock(); siginfo_t infop{}; // This blocks until the process exits, but doesn't reap it. auto result = subprocess_->Wait(&infop, WEXITED | WNOWAIT); CF_EXPECT(result != -1, "Lost track of subprocess pid"); interrupt_lock.lock(); // Perform a reaping wait on the process (which should already have exited). result = subprocess_->Wait(&infop, WEXITED); CF_EXPECT(result != -1, "Lost track of subprocess pid"); // The double wait avoids a race around the kernel reusing pids. Waiting // with WNOWAIT won't cause the child process to be reaped, so the kernel // won't reuse the pid until the Wait call below, and any kill signals won't // reach unexpected processes. subprocess_ = {}; if (infop.si_code == CLD_EXITED && bin == kStopBin) { instance_manager_.RemoveInstanceGroup(home); } if (infop.si_code == CLD_EXITED && infop.si_status == 0) { response.mutable_status()->set_code(cvd::Status::OK); return response; } response.mutable_status()->set_code(cvd::Status::INTERNAL); if (infop.si_code == CLD_EXITED) { response.mutable_status()->set_message("Exited with code " + std::to_string(infop.si_status)); } else if (infop.si_code == CLD_KILLED) { response.mutable_status()->set_message("Exited with signal " + std::to_string(infop.si_status)); } else { response.mutable_status()->set_message("Quit with code " + std::to_string(infop.si_status)); } return response; } Result CvdCommandHandler::Interrupt() { std::scoped_lock interrupt_lock(interruptible_); if (subprocess_) { auto stop_result = subprocess_->Stop(); switch (stop_result) { case StopperResult::kStopFailure: return CF_ERR("Failed to stop subprocess"); case StopperResult::kStopCrash: return CF_ERR("Stopper caused process to crash"); case StopperResult::kStopSuccess: return {}; default: return CF_ERR("Unknown stop result: " << (uint64_t)stop_result); } } return {}; } CommandInvocation ParseInvocation(const cvd::Request& request) { CommandInvocation invocation; if (request.contents_case() != cvd::Request::ContentsCase::kCommandRequest) { return invocation; } if (request.command_request().args_size() == 0) { return invocation; } for (const std::string& arg : request.command_request().args()) { invocation.arguments.push_back(arg); } invocation.arguments[0] = cpp_basename(invocation.arguments[0]); if (invocation.arguments[0] == "cvd") { if (invocation.arguments.size() == 1) { // Show help if user invokes `cvd` alone. invocation.command = "help"; invocation.arguments = {}; } else { // More arguments invocation.command = invocation.arguments[1]; invocation.arguments.erase(invocation.arguments.begin()); invocation.arguments.erase(invocation.arguments.begin()); } } else { invocation.command = invocation.arguments[0]; invocation.arguments.erase(invocation.arguments.begin()); } return invocation; } fruit::Component> cvdCommandComponent() { return fruit::createComponent() .addMultibinding(); } } // namespace cuttlefish