Skip to content

Commit

Permalink
Merge #2535
Browse files Browse the repository at this point in the history
2535: Working directory mapping r=townsend2010 a=luis4a0

This PR adds the option `--working-directory` to the `exec` command (and, by extension, to the alias execution). It also adds the argument `--no-map-working-directory` to the alias definition, which causes the alias to change to the mounted directory if the current host directory is mounted on the executing instance.

The implementation consists in prepending `cd working-dir &&` to the command execution.

Fixes #2594

Co-authored-by: Luis Peñaranda <[email protected]>
  • Loading branch information
bors[bot] and luis4a0 authored Jun 21, 2022
2 parents da2d076 + 5b7ca57 commit 222a4c0
Show file tree
Hide file tree
Showing 26 changed files with 774 additions and 191 deletions.
6 changes: 6 additions & 0 deletions completions/bash/multipass
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,19 @@ _multipass_complete()
{
opts=""

local no_map_found=0
local help_found=0
local definition_found=0
local verbose_found=0

for ((i=2; i<COMP_CWORD; ++i)); do
if [[ "${COMP_WORDS[$i]}" == '--no-map-working-directory' ]]; then no_map_found=1; fi
if [[ "${COMP_WORDS[$i]}" == '--help' ]]; then help_found=1; fi
if [[ "${COMP_WORDS[$i]}" == '--verbose' ]]; then verbose_found=1; fi
if [[ "${COMP_WORDS[$i]}" =~ ':' ]]; then definition_found=1; fi
done

if [[ ${no_map_found} == 0 ]]; then opts="${opts} --no-map-working-directory"; fi
if [[ ${help_found} == 0 ]]; then opts="${opts} --help"; fi
if [[ ${verbose_found} == 0 ]]; then opts="${opts} --verbose"; fi

Expand Down Expand Up @@ -193,6 +196,9 @@ _multipass_complete()
fi

case "${cmd}" in
"exec")
opts="${opts} --working-directory --no-map-working-directory"
;;
"info")
opts="${opts} --all --format"
;;
Expand Down
1 change: 1 addition & 0 deletions include/multipass/cli/alias_definition.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ struct AliasDefinition
{
std::string instance;
std::string command;
std::string working_directory;
};

inline bool operator==(const AliasDefinition& a, const AliasDefinition& b)
Expand Down
2 changes: 2 additions & 0 deletions include/multipass/ssh/ssh_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ class SSHClient
SSHClient(SSHSessionUPtr ssh_session, ConsoleCreator console_creator);

int exec(const std::vector<std::string>& args);
int exec(const std::vector<std::vector<std::string>>& args_list);
void connect();

private:
void handle_ssh_events();
int exec_string(const std::string& cmd_line);

SSHSessionUPtr ssh_session;
ChannelUPtr channel;
Expand Down
15 changes: 14 additions & 1 deletion src/client/cli/cmd/alias.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
namespace mp = multipass;
namespace cmd = multipass::cmd;

namespace
{
const QString no_alias_dir_mapping_option{"no-map-working-directory"};
} // namespace

mp::ReturnCode cmd::Alias::run(mp::ArgParser* parser)
{
auto ret = parse_args(parser);
Expand Down Expand Up @@ -86,10 +91,16 @@ mp::ParseCode cmd::Alias::parse_args(mp::ArgParser* parser)
parser->addPositionalArgument("definition", "Alias definition in the form <instance>:<command>", "<definition>");
parser->addPositionalArgument("name", "Name given to the alias being defined, defaults to <command>", "[<name>]");

QCommandLineOption noAliasDirMappingOption({"n", no_alias_dir_mapping_option},
"Do not automatically map the host execution path to a mounted path");

parser->addOptions({noAliasDirMappingOption});

auto status = parser->commandParse(this);
if (status != ParseCode::Ok)
return status;

// The number of arguments
if (parser->positionalArguments().count() != 1 && parser->positionalArguments().count() != 2)
{
cerr << "Wrong number of arguments given\n";
Expand Down Expand Up @@ -130,9 +141,11 @@ mp::ParseCode cmd::Alias::parse_args(mp::ArgParser* parser)
}

auto instance = definition.left(colon_pos).toStdString();
auto working_directory = parser->isSet(no_alias_dir_mapping_option) ? "default" : "map";

info_request.mutable_instance_names()->add_instance_name(instance);
info_request.set_verbosity_level(0);
info_request.set_no_runtime_information(true);

auto on_success = [](InfoReply&) { return ReturnCode::Ok; };

Expand Down Expand Up @@ -167,7 +180,7 @@ mp::ParseCode cmd::Alias::parse_args(mp::ArgParser* parser)
return ParseCode::CommandLineError;
}

alias_definition = AliasDefinition{instance, command};
alias_definition = AliasDefinition{instance, command, working_directory};

return ParseCode::Ok;
}
5 changes: 4 additions & 1 deletion src/client/cli/cmd/aliases.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ QString cmd::Aliases::description() const
mp::ParseCode cmd::Aliases::parse_args(mp::ArgParser* parser)
{
QCommandLineOption formatOption(
"format", "Output list in the requested format.\nValid formats are: table (default), json, csv and yaml",
"format",
"Output list in the requested format. Valid formats are: table (default), json, csv and yaml. "
"The output working directory states whether the alias runs in the instance's default directory "
"or the alias running directory should try to be mapped to a mounted one.\n",
"format", "table");

parser->addOption(formatOption);
Expand Down
123 changes: 110 additions & 13 deletions src/client/cli/cmd/exec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@
namespace mp = multipass;
namespace cmd = multipass::cmd;

namespace
{
const QString work_dir_option_name{"working-directory"};
const QString no_dir_mapping_option{"no-map-working-directory"};

auto is_dir_mounted(const QStringList& split_current_dir, const QStringList& split_source_dir)
{
auto source_dir_size = split_source_dir.size();

if (split_current_dir.size() < source_dir_size)
return false;

for (int i = 0; i < source_dir_size; ++i)
if (split_current_dir[i] != split_source_dir[i])
return false;

return true;
}
} // namespace

mp::ReturnCode cmd::Exec::run(mp::ArgParser* parser)
{
auto ret = parse_args(parser);
Expand All @@ -32,26 +52,82 @@ mp::ReturnCode cmd::Exec::run(mp::ArgParser* parser)
return parser->returnCodeFrom(ret);
}

auto instance_name = ssh_info_request.instance_name(0);

std::vector<std::string> args;
for (int i = 1; i < parser->positionalArguments().size(); ++i)
args.push_back(parser->positionalArguments().at(i).toStdString());

auto on_success = [this, &args](mp::SSHInfoReply& reply) { return exec_success(reply, args, term); };
mp::optional<std::string> work_dir;
if (parser->isSet(work_dir_option_name))
{
// If the user asked for a working directory, prepend the appropriate `cd`.
work_dir = parser->value(work_dir_option_name).toStdString();
}
else
{
// If the current working directory is mounted in the instance, then prepend the appropriate `cd` to the
// command to be ran (unless the user specified the non-mapping option).
if (!parser->isSet(no_dir_mapping_option))
{
// The host directory on which the user is executing the command.
QString clean_exec_dir = QDir::cleanPath(QDir::current().canonicalPath());
QStringList split_exec_dir = clean_exec_dir.split('/');

auto on_info_success = [&work_dir, &split_exec_dir](mp::InfoReply& reply) {
for (const auto& mount : reply.info(0).mount_info().mount_paths())
{
auto source_dir = QDir(QString::fromStdString(mount.source_path()));
auto clean_source_dir = QDir::cleanPath(source_dir.absolutePath());
QStringList split_source_dir = clean_source_dir.split('/');

// If the directory is mounted, we need to `cd` to it in the instance before executing the command.
if (is_dir_mounted(split_exec_dir, split_source_dir))
{
for (int i = 0; i < split_source_dir.size(); ++i)
split_exec_dir.removeFirst();
work_dir = mount.target_path() + '/' + split_exec_dir.join('/').toStdString();
}
}

return ReturnCode::Ok;
};

auto on_info_failure = [this](grpc::Status& status) {
return standard_failure_handler_for(name(), cerr, status);
};

info_request.set_verbosity_level(parser->verbosityLevel());

InstanceNames instance_names;
auto info_instance_name = instance_names.add_instance_name();
info_instance_name->append(instance_name);
info_request.mutable_instance_names()->CopyFrom(instance_names);
info_request.set_no_runtime_information(true);

dispatch(&RpcMethod::info, info_request, on_info_success, on_info_failure);
// TODO: what to do with the returned value?
}
}

auto on_success = [this, &args, &work_dir](mp::SSHInfoReply& reply) {
return exec_success(reply, work_dir, args, term);
};

auto on_failure = [this, parser](grpc::Status& status) {
auto on_failure = [this, &instance_name, parser](grpc::Status& status) {
if (status.error_code() == grpc::StatusCode::ABORTED)
return run_cmd_and_retry({"multipass", "start", QString::fromStdString(request.instance_name(0))}, parser,
cout, cerr);
return run_cmd_and_retry({"multipass", "start", QString::fromStdString(instance_name)}, parser, cout, cerr);
else
return standard_failure_handler_for(name(), cerr, status);
};

request.set_verbosity_level(parser->verbosityLevel());
ReturnCode return_code;
while ((return_code = dispatch(&RpcMethod::ssh_info, request, on_success, on_failure)) == ReturnCode::Retry)
ssh_info_request.set_verbosity_level(parser->verbosityLevel());
ReturnCode ssh_return_code;
while ((ssh_return_code = dispatch(&RpcMethod::ssh_info, ssh_info_request, on_success, on_failure)) ==
ReturnCode::Retry)
;

return return_code;
return ssh_return_code;
}

std::string cmd::Exec::name() const
Expand All @@ -69,8 +145,8 @@ QString cmd::Exec::description() const
return QStringLiteral("Run a command on an instance");
}

mp::ReturnCode cmd::Exec::exec_success(const mp::SSHInfoReply& reply, const std::vector<std::string>& args,
mp::Terminal* term)
mp::ReturnCode cmd::Exec::exec_success(const mp::SSHInfoReply& reply, const mp::optional<std::string>& dir,
const std::vector<std::string>& args, mp::Terminal* term)
{
// TODO: mainly for testing - need a better way to test parsing
if (reply.ssh_info().empty())
Expand All @@ -86,7 +162,16 @@ mp::ReturnCode cmd::Exec::exec_success(const mp::SSHInfoReply& reply, const std:
{
auto console_creator = [&term](auto channel) { return Console::make_console(channel, term); };
mp::SSHClient ssh_client{host, port, username, priv_key_blob, console_creator};
return static_cast<mp::ReturnCode>(ssh_client.exec(args));

std::vector<std::vector<std::string>> all_args;
if (dir)
{
all_args = {{"cd", *dir}, {args}};
}
else
all_args = {{args}};

return static_cast<mp::ReturnCode>(ssh_client.exec(all_args));
}
catch (const std::exception& e)
{
Expand All @@ -100,6 +185,13 @@ mp::ParseCode cmd::Exec::parse_args(mp::ArgParser* parser)
parser->addPositionalArgument("name", "Name of instance to execute the command on", "<name>");
parser->addPositionalArgument("command", "Command to execute on the instance", "[--] <command>");

QCommandLineOption workDirOption({"d", work_dir_option_name}, "Change to <dir> before execution", "dir");
QCommandLineOption noDirMappingOption({"n", no_dir_mapping_option},
"Do not map the host execution path to a mounted path");

parser->addOptions({workDirOption});
parser->addOptions({noDirMappingOption});

auto status = parser->commandParse(this);

if (status != ParseCode::Ok)
Expand All @@ -114,14 +206,19 @@ mp::ParseCode cmd::Exec::parse_args(mp::ArgParser* parser)
return status;
}

if (parser->positionalArguments().count() < 2)
if (parser->isSet(work_dir_option_name) && parser->isSet(no_dir_mapping_option))
{
cerr << fmt::format("Options --{} and --{} clash\n", work_dir_option_name, no_dir_mapping_option);
status = ParseCode::CommandLineError;
}
else if (parser->positionalArguments().count() < 2)
{
cerr << "Wrong number of arguments\n";
status = ParseCode::CommandLineError;
}
else
{
auto entry = request.add_instance_name();
auto entry = ssh_info_request.add_instance_name();
entry->append(parser->positionalArguments().first().toStdString());
}

Expand Down
7 changes: 5 additions & 2 deletions src/client/cli/cmd/exec.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#include <multipass/cli/alias_dict.h>
#include <multipass/cli/command.h>
#include <multipass/optional.h>

namespace multipass
{
Expand All @@ -41,10 +42,12 @@ class Exec final : public Command
QString short_help() const override;
QString description() const override;

static ReturnCode exec_success(const SSHInfoReply& reply, const std::vector<std::string>& args, Terminal* term);
static ReturnCode exec_success(const SSHInfoReply& reply, const multipass::optional<std::string>& dir,
const std::vector<std::string>& args, Terminal* term);

private:
SSHInfoRequest request;
SSHInfoRequest ssh_info_request;
InfoRequest info_request;
AliasDict aliases;

ParseCode parse_args(ArgParser* parser);
Expand Down
7 changes: 7 additions & 0 deletions src/client/cli/cmd/info.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ mp::ParseCode cmd::Info::parse_args(mp::ArgParser* parser)
QCommandLineOption all_option(all_option_name, "Display info for all instances");
parser->addOption(all_option);

QCommandLineOption noRuntimeInfoOption(
"no-runtime-information",
"Retrieve from the daemon only the information obtained without running commands on the instance");
noRuntimeInfoOption.setFlags(QCommandLineOption::HiddenFromHelp);
parser->addOption(noRuntimeInfoOption);

QCommandLineOption formatOption(
"format", "Output info in the requested format.\nValid formats are: table (default), json, csv and yaml",
"format", "table");
Expand All @@ -80,6 +86,7 @@ mp::ParseCode cmd::Info::parse_args(mp::ArgParser* parser)
return parse_code;

request.mutable_instance_names()->CopyFrom(add_instance_names(parser));
request.set_no_runtime_information(parser->isSet(noRuntimeInfoOption));

status = handle_format_option(parser, &chosen_formatter, cerr);

Expand Down
4 changes: 2 additions & 2 deletions src/client/cli/formatter/csv_formatter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ std::string mp::CSVFormatter::format(const VersionReply& reply, const std::strin
std::string mp::CSVFormatter::format(const mp::AliasDict& aliases) const
{
fmt::memory_buffer buf;
fmt::format_to(buf, "Alias,Instance,Command\n");
fmt::format_to(buf, "Alias,Instance,Command,Working directory\n");

for (const auto& elt : sort_dict(aliases))
{
const auto& name = elt.first;
const auto& def = elt.second;

fmt::format_to(buf, "{},{},{}\n", name, def.instance, def.command);
fmt::format_to(buf, "{},{},{},{}\n", name, def.instance, def.command, def.working_directory);
}

return fmt::to_string(buf);
Expand Down
1 change: 1 addition & 0 deletions src/client/cli/formatter/json_formatter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ std::string mp::JsonFormatter::format(const mp::AliasDict& aliases) const
alias_obj.insert("alias", QString::fromStdString(alias));
alias_obj.insert("instance", QString::fromStdString(def.instance));
alias_obj.insert("command", QString::fromStdString(def.command));
alias_obj.insert("working-directory", QString::fromStdString(def.working_directory));

aliases_array.append(alias_obj);
}
Expand Down
10 changes: 7 additions & 3 deletions src/client/cli/formatter/table_formatter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,17 +256,21 @@ std::string mp::TableFormatter::format(const mp::AliasDict& aliases) const
aliases.cbegin(), aliases.cend(), [](const auto& alias) -> int { return alias.first.length(); }, 7);
const auto instance_width = mp::format::column_width(
aliases.cbegin(), aliases.cend(), [](const auto& alias) -> int { return alias.second.instance.length(); }, 10);
const auto command_width = mp::format::column_width(
aliases.cbegin(), aliases.cend(), [](const auto& alias) -> int { return alias.second.command.length(); }, 9);

const auto row_format = "{:<{}}{:<{}}{:<}\n";
const auto row_format = "{:<{}}{:<{}}{:<{}}{:<}\n";

fmt::format_to(buf, row_format, "Alias", alias_width, "Instance", instance_width, "Command");
fmt::format_to(buf, row_format, "Alias", alias_width, "Instance", instance_width, "Command", command_width,
"Working directory");

for (const auto& elt : sort_dict(aliases))
{
const auto& name = elt.first;
const auto& def = elt.second;

fmt::format_to(buf, row_format, name, alias_width, def.instance, instance_width, def.command);
fmt::format_to(buf, row_format, name, alias_width, def.instance, instance_width, def.command, command_width,
def.working_directory);
}

return fmt::to_string(buf);
Expand Down
1 change: 1 addition & 0 deletions src/client/cli/formatter/yaml_formatter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ std::string mp::YamlFormatter::format(const mp::AliasDict& aliases) const
alias_node["alias"] = alias;
alias_node["command"] = def.command;
alias_node["instance"] = def.instance;
alias_node["working-directory"] = def.working_directory;

aliases_node.push_back(alias_node);
}
Expand Down
Loading

0 comments on commit 222a4c0

Please sign in to comment.