Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ cf_cc_library(
"//cuttlefish/host/commands/cvd/cli:utils",
"//cuttlefish/host/commands/cvd/instances",
"//cuttlefish/host/commands/cvd/instances:instance_manager",
"//cuttlefish/host/libs/config:config_constants",
"//cuttlefish/result",
"//libbase",
"@abseil-cpp//absl/strings",
Expand Down
225 changes: 121 additions & 104 deletions base/cvd/cuttlefish/host/commands/cvd/cli/selector/selector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include <iostream>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <utility>
Expand All @@ -29,24 +30,18 @@
#include "cuttlefish/host/commands/cvd/cli/interruptible_terminal.h"
#include "cuttlefish/host/commands/cvd/cli/utils.h"
#include "cuttlefish/host/commands/cvd/instances/local_instance_group.h"
#include "cuttlefish/host/libs/config/config_constants.h"

namespace cuttlefish {
namespace selector {
namespace {

Result<LocalInstanceGroup> GetDefaultGroup(
const InstanceManager& instance_manager) {
const std::vector<LocalInstanceGroup> all_groups =
CF_EXPECT(instance_manager.FindGroups({}));
CF_EXPECTF(all_groups.size() == 1,
"There are {} instance groups, unable to pick one",
all_groups.size());
return all_groups.front();
}
enum class DisplayBehavior {
LabelGroup,
LabelInstance,
};

Result<InstanceDatabase::Filter> BuildFilterFromSelectors(
const SelectorOptions& selectors, const cvd_common::Envs& env) {
const SelectorOptions& selectors) {
InstanceDatabase::Filter filter;
filter.group_name = selectors.group_name;
if (selectors.instance_names) {
Expand All @@ -56,131 +51,153 @@ Result<InstanceDatabase::Filter> BuildFilterFromSelectors(
filter.instance_names.insert(per_instance_name);
}
}
auto it = env.find(kCuttlefishInstanceEnvVarName);
if (it != env.end()) {
unsigned id;
std::string cuttlefish_instance = it->second;
CF_EXPECT(absl::SimpleAtoi(cuttlefish_instance, &id));
}

return filter;
}

std::string SelectionMenu(const std::vector<LocalInstanceGroup>& groups) {
// Multiple instance groups found, please choose one:
// [i] : group_name (created: TIME)
// <a> instance0.device_name() (id: instance_id)
// <b> instance1.device_name() (id: instance_id)
std::stringstream ss;
ss << "Multiple instance groups found, please choose one:" << std::endl;
int group_idx = 0;
for (const auto& group : groups) {
fmt::print(ss, " [{}] : {} (created: {})\n", group_idx, group.GroupName(),
Format(group.StartTime()));
char instance_idx = 'a';
for (const auto& instance : group.Instances()) {
fmt::print(ss, " <{}> {}-{} (id : {})\n", instance_idx++,
group.GroupName(), instance.name(), instance.id());
std::string GroupDisplay(const LocalInstanceGroup& group,
const std::optional<int> group_index,
const bool label_instances) {
std::stringstream result;
if (group_index) {
fmt::print(result, "[{}] - ", *group_index);
}
fmt::print(result, "{} (created: {})\n", group.GroupName(),
Format(group.StartTime()));

int instance_index = 0;
for (const LocalInstance& instance : group.Instances()) {
result << "\t";
if (label_instances) {
fmt::print(result, "[{}] - ", instance_index);
}
group_idx++;
fmt::print(result, "{}-{} (id : {})\n", group.GroupName(), instance.name(),
instance.id());
instance_index++;
}
return ss.str();

return result.str();
}

Result<LocalInstanceGroup> PromptUserForGroup(
const InstanceManager& instance_manager, const CommandRequest& request,
const cvd_common::Envs& envs, InstanceDatabase::Filter filter) {
// show the menu and let the user choose
std::vector<LocalInstanceGroup> groups =
CF_EXPECT(instance_manager.FindGroups({}));
std::string menu = SelectionMenu(groups);
std::string FleetDisplay(const std::vector<LocalInstanceGroup>& groups,
const DisplayBehavior behavior) {
std::stringstream result;
int group_index = 0;
for (const LocalInstanceGroup& group : groups) {
result << GroupDisplay(group,
behavior == DisplayBehavior::LabelGroup
? std::optional(group_index)
: std::nullopt,
behavior == DisplayBehavior::LabelInstance);
group_index++;
}
return result.str();
}

std::cout << menu << "\n";
std::unique_ptr<InterruptibleTerminal> terminal_ =
Result<int> PromptForSelection(const int max_selection) {
std::unique_ptr<InterruptibleTerminal> terminal =
std::make_unique<InterruptibleTerminal>();

TerminalColors colors(isatty(2));
while (true) {
std::string input_line = CF_EXPECT(terminal_->ReadLine());
int selection = -1;
std::string chosen_group_name;
if (absl::SimpleAtoi(input_line, &selection)) {
const int n_groups = groups.size();
if (n_groups <= selection || selection < 0) {
fmt::print(std::cerr,
"\n Selection {}{}{} is beyond the range {}[0, {}]{}\n\n",
colors.BoldRed(), selection, colors.Reset(), colors.Cyan(),
n_groups - 1, colors.Reset());
continue;
}
chosen_group_name = groups[selection].GroupName();
} else {
chosen_group_name = std::string(absl::StripAsciiWhitespace(input_line));
}

filter.group_name = chosen_group_name;
Result<LocalInstanceGroup> instance_group_result =
instance_manager.FindGroup(filter);
if (instance_group_result.ok()) {
return instance_group_result;
int selection = -1;
while (selection < 0 || selection > max_selection) {
std::string input_line = CF_EXPECT(terminal->ReadLine());
if (!absl::SimpleAtoi(input_line, &selection)) {
selection = -1;
fmt::print(std::cerr,
"\nSelection \"{}{}{}\" is not a valid label. Choose between "
"{}[0, {}]{}.\n",
colors.BoldRed(), input_line, colors.Reset(), colors.Cyan(),
max_selection, colors.Reset());
continue;
}
if (selection > max_selection) {
fmt::print(std::cerr,
"\n Selection \"{}{}{}\" is beyond the range {}[0, {}]{}.\n",
colors.BoldRed(), selection, colors.Reset(), colors.Cyan(),
max_selection, colors.Reset());
continue;
}
fmt::print(std::cerr,
"\n Failed to find a group whose name is {}\"{}\"{}\n\n",
colors.BoldRed(), chosen_group_name, colors.Reset());
}
return selection;
}

Result<LocalInstanceGroup> FindGroupOrDefault(
const InstanceDatabase::Filter& filter,
const InstanceManager& instance_manager) {
if (filter.Empty()) {
return CF_EXPECT(GetDefaultGroup(instance_manager));
}
std::vector<LocalInstanceGroup> groups =
CF_EXPECT(instance_manager.FindGroups(filter));
CF_EXPECT_EQ(groups.size(), 1u, "groups.size() = " << groups.size());
return groups.front();
Result<LocalInstanceGroup> PromptUserForGroup(
const InstanceManager& instance_manager, const CommandRequest& request) {
// show the menu and let the user choose
const std::vector<LocalInstanceGroup> groups =
CF_EXPECT(instance_manager.FindGroups({}));
std::string menu = FleetDisplay(groups, DisplayBehavior::LabelGroup);
std::cout << menu << "\n";

const int selection = CF_EXPECT(PromptForSelection(groups.size() - 1));
auto group_filter = InstanceDatabase::Filter{
.group_name = groups[selection].GroupName(),
};

return CF_EXPECT(instance_manager.FindGroup(group_filter));
}

Result<std::pair<LocalInstance, LocalInstanceGroup>> FindDefaultInstance(
const InstanceManager& instance_manager) {
const LocalInstanceGroup group = CF_EXPECT(GetDefaultGroup(instance_manager));
const std::vector<LocalInstance> instances = group.Instances();
CF_EXPECT_EQ(instances.size(), 1u,
"Default instance is the single instance in the default group.");
return std::make_pair(instances.front(), group);
Result<std::pair<LocalInstance, LocalInstanceGroup>> PromptUserForInstance(
const InstanceManager& instance_manager, const CommandRequest& request) {
const LocalInstanceGroup group =
CF_EXPECT(PromptUserForGroup(instance_manager, request));
const std::string menu =
FleetDisplay({group}, DisplayBehavior::LabelInstance);
std::cout << menu << "\n";

const int selection =
CF_EXPECT(PromptForSelection(group.Instances().size() - 1));
auto instance_filter = InstanceDatabase::Filter{
.group_name = group.GroupName(),
.instance_names = {group.Instances()[selection].name()},
};
std::vector<std::pair<LocalInstance, LocalInstanceGroup>> result =
CF_EXPECT(instance_manager.FindInstances(instance_filter));
CF_EXPECT_EQ(result.size(), 1);
return result.front();
}

} // namespace

Result<LocalInstanceGroup> SelectGroup(const InstanceManager& instance_manager,
const CommandRequest& request) {
const bool has_groups = CF_EXPECT(instance_manager.HasInstanceGroups());
CF_EXPECT(std::move(has_groups), "No instance groups available");
const cvd_common::Envs& env = request.Env();
const SelectorOptions& selector_options = request.Selectors();
InstanceDatabase::Filter filter =
CF_EXPECT(BuildFilterFromSelectors(selector_options, request.Env()));
Result<LocalInstanceGroup> group_selection_result =
FindGroupOrDefault(filter, instance_manager);
if (group_selection_result.ok()) {
return CF_EXPECT(std::move(group_selection_result));
const InstanceDatabase::Filter filter =
CF_EXPECT(BuildFilterFromSelectors(request.Selectors()));
std::vector<LocalInstanceGroup> groups;
if (filter.Empty()) { // try to default
groups = CF_EXPECT(instance_manager.FindGroups({}));
} else {
groups = CF_EXPECT(instance_manager.FindGroups(filter));
}
CF_EXPECT(!groups.empty(), "No instance groups available");
if (groups.size() == 1) {
return groups.front();
}
CF_EXPECT(isatty(0),
"Multiple groups found. Narrow the selection with selector "
"arguments or run in an interactive terminal.");
return CF_EXPECT(
PromptUserForGroup(instance_manager, request, env, std::move(filter)));
return CF_EXPECT(PromptUserForGroup(instance_manager, request));
}

Result<std::pair<LocalInstance, LocalInstanceGroup>> SelectInstance(
const InstanceManager& instance_manager, const CommandRequest& request) {
InstanceDatabase::Filter filter =
CF_EXPECT(BuildFilterFromSelectors(request.Selectors(), request.Env()));

return filter.Empty()
? CF_EXPECT(FindDefaultInstance(instance_manager))
: CF_EXPECT(instance_manager.FindInstanceWithGroup(filter));
const InstanceDatabase::Filter filter =
CF_EXPECT(BuildFilterFromSelectors(request.Selectors()));
std::vector<std::pair<LocalInstance, LocalInstanceGroup>> instances;
if (filter.Empty()) { // try to default
instances = CF_EXPECT(instance_manager.FindInstances({}));
} else {
instances = CF_EXPECT(instance_manager.FindInstances(filter));
}
CF_EXPECT(!instances.empty(), "No instances available");
if (instances.size() == 1) {
return instances.front();
}
CF_EXPECT(isatty(0),
"Multiple instances found. Narrow the selection with selector "
"arguments or run in an interactive terminal");
return CF_EXPECT(PromptUserForInstance(instance_manager, request));
}

} // namespace selector
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,35 @@ InstanceDatabase::FindInstanceWithGroup(const Filter& filter) const {
});
}

Result<std::vector<std::pair<LocalInstance, LocalInstanceGroup>>>
InstanceDatabase::FindInstances(const Filter& filter) const {
CF_EXPECT_LE(filter.instance_names.size(), 1u,
"Can't find single instance when multiple names specified: "
<< filter.instance_names.size());
return viewer_.WithSharedLock<
std::vector<std::pair<LocalInstance, LocalInstanceGroup>>>(
[&filter](const auto& data)
-> Result<std::vector<std::pair<LocalInstance, LocalInstanceGroup>>> {
std::vector<std::pair<LocalInstance, LocalInstanceGroup>> result;
for (const auto& group : data.instance_groups()) {
if (!GroupMatches(group, filter)) {
continue;
}
for (int i = 0; i < group.instances_size(); ++i) {
const auto& instance = group.instances(i);
if (!InstanceMatches(instance, filter)) {
continue;
}
LocalInstanceGroup local_group =
CF_EXPECT(LocalInstanceGroup::Create(group));
result.push_back(
std::make_pair(local_group.Instances()[i], local_group));
}
}
return result;
});
}

Result<std::vector<LocalInstanceGroup>> InstanceDatabase::InstanceGroups()
const {
return viewer_.WithSharedLock<std::vector<LocalInstanceGroup>>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ class InstanceDatabase {
Result<std::pair<LocalInstance, LocalInstanceGroup>> FindInstanceWithGroup(
const Filter& filter) const;

Result<std::vector<std::pair<LocalInstance, LocalInstanceGroup>>>
FindInstances(const Filter& filter) const;

private:
template <typename T>
Result<T> ExactlyOne(Result<std::vector<T>>&& container_result) const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ InstanceManager::FindInstanceWithGroup(
return CF_EXPECT(instance_db_.FindInstanceWithGroup(filter));
}

Result<std::vector<std::pair<LocalInstance, LocalInstanceGroup>>>
InstanceManager::FindInstances(const InstanceDatabase::Filter& filter) const {
return CF_EXPECT(instance_db_.FindInstances(filter));
}

Result<bool> InstanceManager::HasInstanceGroups() const {
return !CF_EXPECT(instance_db_.IsEmpty());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ class InstanceManager {
Result<std::pair<LocalInstance, LocalInstanceGroup>> FindInstanceWithGroup(
const InstanceDatabase::Filter& filter) const;

Result<std::vector<std::pair<LocalInstance, LocalInstanceGroup>>>
FindInstances(const InstanceDatabase::Filter& filter) const;

// Stops the device by asking it over the control socket. If launcher_timeout
// has a value, it will wait for at most that time before returning an error.
Result<void> StopInstanceGroup(
Expand Down
Loading