| /* |
| * 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 <set> |
| #include <string> |
| #include <vector> |
| |
| #include <android-base/file.h> |
| #include <android-base/logging.h> |
| #include <fruit/fruit.h> |
| |
| #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 <command> <args> |
| |
| Commands: |
| help Print this message. |
| help <command> 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: |
| <command args> Each command has its own set of args. See cvd help <command>. |
| --clean If provided, runs cvd kill-server before the requested command. |
| )"; |
| |
| const std::map<std::string, std::string> 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<bool> CvdCommandHandler::CanHandle( |
| const RequestWithStdio& request) const { |
| auto invocation = ParseInvocation(request.Message()); |
| return CommandToBinaryMap.find(invocation.command) != |
| CommandToBinaryMap.end(); |
| } |
| |
| Result<cvd::Response> 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<std::string> 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 <subcommand>` 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<void> 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<fruit::Required<InstanceManager>> cvdCommandComponent() { |
| return fruit::createComponent() |
| .addMultibinding<CvdServerHandler, CvdCommandHandler>(); |
| } |
| |
| } // namespace cuttlefish |