kyua: Add "debug -x|--execute cmd" option

With execenv=jail specified, the "cmd" runs inside the test's jail.

Reviewed by:	ngie, markj
Differential Revision:	https://reviews.freebsd.org/D52642
This commit is contained in:
Igor Ostapenko 2026-01-11 13:43:33 +00:00
parent 7afa03963c
commit 8a21c17ccf
9 changed files with 178 additions and 11 deletions

View file

@ -28,6 +28,10 @@
#include "cli/cmd_debug.hpp"
extern "C" {
#include <unistd.h>
}
#include <cstdlib>
#include <iostream>
@ -39,13 +43,20 @@
#include "utils/cmdline/parser.ipp"
#include "utils/cmdline/ui.hpp"
#include "utils/format/macros.hpp"
#include "utils/fs/path.hpp"
#include "utils/process/child.ipp"
#include "utils/process/executor.hpp"
#include "utils/process/operations.hpp"
#include "utils/process/status.hpp"
namespace cmdline = utils::cmdline;
namespace config = utils::config;
namespace executor = utils::process::executor;
namespace process = utils::process;
using cli::cmd_debug;
using utils::process::args_vector;
using utils::process::child;
namespace {
@ -62,6 +73,57 @@ const cmdline::bool_option pause_before_cleanup_option(
"Pauses right before the test cleanup");
static const char* DEFAULT_CMD = "$SHELL";
const cmdline::string_option execute_option(
'x', "execute",
"A command to run within the given execenv upon test failure",
"cmd", DEFAULT_CMD, true);
/// Functor to execute a program.
class execute {
const std::string& _cmd;
executor::exit_handle& _eh;
public:
/// Constructor.
///
/// \param program Program binary absolute path.
/// \param args Program arguments.
execute(
const std::string& cmd_,
executor::exit_handle& eh_) :
_cmd(cmd_),
_eh(eh_)
{
}
/// Body of the subprocess.
void
operator()(void)
{
if (::chdir(_eh.work_directory().c_str()) == -1) {
std::cerr << "execute: chdir() errors: "
<< strerror(errno) << ".\n";
std::exit(EXIT_FAILURE);
}
std::string program_path = "/bin/sh";
const char* shell = std::getenv("SHELL");
if (shell)
program_path = shell;
args_vector av;
if (!(_cmd.empty() || _cmd == DEFAULT_CMD)) {
av.push_back("-c");
av.push_back(_cmd);
}
process::exec(utils::fs::path(program_path), av);
}
};
/// The debugger interface implementation.
class dbg : public engine::debugger {
/// Object to interact with the I/O of the program.
@ -103,6 +165,21 @@ public:
}
};
void upon_test_failure(
const model::test_program_ptr&,
const model::test_case&,
optional< model::test_result >&,
executor::exit_handle& eh) const
{
if (!_cmdline.has_option(execute_option.long_name()))
return;
const std::string& cmd = _cmdline.get_option<cmdline::string_option>(
execute_option.long_name());
std::unique_ptr< process::child > child = child::fork_interactive(
execute(cmd, eh));
(void) child->wait();
};
};
@ -127,6 +204,8 @@ cmd_debug::cmd_debug(void) : cli_command(
add_option(cmdline::path_option(
"stderr", "Where to direct the standard error of the test case",
"path", "/dev/stderr"));
add_option(execute_option);
}
@ -151,7 +230,8 @@ cmd_debug::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline,
engine::debugger_ptr debugger = nullptr;
if (cmdline.has_option(pause_before_cleanup_upon_fail_option.long_name())
|| cmdline.has_option(pause_before_cleanup_option.long_name())) {
|| cmdline.has_option(pause_before_cleanup_option.long_name())
|| cmdline.has_option(execute_option.long_name())) {
debugger = std::shared_ptr< engine::debugger >(new dbg(ui, cmdline));
}

View file

@ -58,6 +58,13 @@ public:
const model::test_case&,
optional< model::test_result >&,
executor::exit_handle&) const = 0;
/// Called upon test failure.
virtual void upon_test_failure(
const model::test_program_ptr&,
const model::test_case&,
optional< model::test_result >&,
executor::exit_handle&) const = 0;
};

View file

@ -1403,6 +1403,9 @@ scheduler::scheduler_handle::wait_any(void)
if (debugger) {
debugger->before_cleanup(test_data->test_program, test_case,
result, handle);
if (!result.get().good())
debugger->upon_test_failure(test_data->test_program, test_case,
result, handle);
}
if (test_data->needs_cleanup) {

View file

@ -53,15 +53,18 @@ namespace text = utils::text;
/// purposes.
/// \param default_value_ If not NULL, specifies that the option has a default
/// value for the mandatory argument.
/// \param arg_is_optional_ Specifies if a value must be provided or not.
cmdline::base_option::base_option(const char short_name_,
const char* long_name_,
const char* description_,
const char* arg_name_,
const char* default_value_) :
const char* default_value_,
bool arg_is_optional_) :
_short_name(short_name_),
_long_name(long_name_),
_description(description_),
_arg_name(arg_name_ == NULL ? "" : arg_name_),
_arg_is_optional(arg_is_optional_),
_has_default_value(default_value_ != NULL),
_default_value(default_value_ == NULL ? "" : default_value_)
{
@ -164,6 +167,16 @@ cmdline::base_option::arg_name(void) const
}
/// Returns optionality of the argument.
///
/// \return The optionality.
bool
cmdline::base_option::arg_is_optional(void) const
{
return _arg_is_optional;
}
/// Checks whether the option has a default value for its argument.
///
/// \pre needs_arg() must be true.
@ -558,9 +571,10 @@ cmdline::string_option::string_option(const char short_name_,
const char* long_name_,
const char* description_,
const char* arg_name_,
const char* default_value_) :
const char* default_value_,
bool arg_is_optional_) :
base_option(short_name_, long_name_, description_, arg_name_,
default_value_)
default_value_, arg_is_optional_)
{
}

View file

@ -91,6 +91,9 @@ class base_option {
/// Descriptive name of the required argument; empty if not allowed.
std::string _arg_name;
/// If the option can be used without an explicit argument provided.
bool _arg_is_optional = false;
/// Whether the option has a default value or not.
///
/// \todo We should probably be using the optional class here.
@ -101,7 +104,7 @@ class base_option {
public:
base_option(const char, const char*, const char*, const char* = NULL,
const char* = NULL);
const char* = NULL, bool = false);
base_option(const char*, const char*, const char* = NULL,
const char* = NULL);
virtual ~base_option(void);
@ -113,6 +116,7 @@ public:
bool needs_arg(void) const;
const std::string& arg_name(void) const;
bool arg_is_optional(void) const;
bool has_default_value(void) const;
const std::string& default_value(void) const;
@ -219,7 +223,7 @@ public:
class string_option : public base_option {
public:
string_option(const char, const char*, const char*, const char*,
const char* = NULL);
const char* = NULL, bool = false);
string_option(const char*, const char*, const char*, const char* = NULL);
virtual ~string_option(void) {}

View file

@ -88,7 +88,10 @@ options_to_getopt_data(const cmdline::options_vector& options,
long_option.name = option->long_name().c_str();
if (option->needs_arg())
long_option.has_arg = required_argument;
if (option->arg_is_optional())
long_option.has_arg = optional_argument;
else
long_option.has_arg = required_argument;
else
long_option.has_arg = no_argument;
@ -96,7 +99,7 @@ options_to_getopt_data(const cmdline::options_vector& options,
if (option->has_short_name()) {
data.short_options += option->short_name();
if (option->needs_arg())
data.short_options += ':';
data.short_options += option->arg_is_optional() ? "::" : ":";
id = option->short_name();
} else {
id = cur_id++;
@ -320,9 +323,11 @@ cmdline::parse(const int argc, const char* const* argv,
for (cmdline::options_vector::const_iterator iter = options.begin();
iter != options.end(); iter++) {
const cmdline::base_option* option = *iter;
if (option->needs_arg() && option->has_default_value())
if (option->needs_arg() && option->has_default_value() &&
!option->arg_is_optional()) {
option_values[option->long_name()].push_back(
option->default_value());
}
}
args_vector args;
@ -357,8 +362,13 @@ cmdline::parse(const int argc, const char* const* argv,
if (::optarg != NULL) {
option->validate(::optarg);
option_values[option->long_name()].push_back(::optarg);
} else
INV(option->has_default_value());
} else {
if (option->arg_is_optional())
option_values[option->long_name()].push_back(
option->default_value());
else
INV(option->has_default_value());
}
} else {
option_values[option->long_name()].push_back("");
}

View file

@ -235,6 +235,30 @@ process::child::fork_capture_aux(void)
}
std::unique_ptr< process::child >
process::child::fork_interactive(void)
{
std::cout.flush();
std::cerr.flush();
std::unique_ptr< signals::interrupts_inhibiter > inhibiter(
new signals::interrupts_inhibiter);
pid_t pid = detail::syscall_fork();
if (pid == -1) {
inhibiter.reset(); // Unblock signals.
throw process::system_error("fork(2) failed", errno);
} else if (pid == 0) {
inhibiter.reset(); // Unblock signals.
return {};
} else {
signals::add_pid_to_kill(pid);
inhibiter.reset(NULL); // Unblock signals.
return std::unique_ptr< process::child >(
new process::child(new impl(pid, NULL)));
}
}
/// Helper function for fork().
///
/// Please note: if you update this function to change the return type or to

View file

@ -80,6 +80,8 @@ class child : noncopyable {
static std::unique_ptr< child > fork_capture_aux(void);
static std::unique_ptr< child > fork_interactive(void);
static std::unique_ptr< child > fork_files_aux(const fs::path&,
const fs::path&);
@ -92,6 +94,9 @@ public:
static std::unique_ptr< child > fork_capture(Hook);
std::istream& output(void);
template< typename Hook >
static std::unique_ptr< child > fork_interactive(Hook);
template< typename Hook >
static std::unique_ptr< child > fork_files(Hook, const fs::path&,
const fs::path&);

View file

@ -104,6 +104,26 @@ child::fork_capture(Hook hook)
}
template< typename Hook >
std::unique_ptr< child >
child::fork_interactive(Hook hook)
{
std::unique_ptr< child > child = fork_interactive();
if (child.get() == NULL) {
try {
hook();
std::abort();
} catch (const std::runtime_error& e) {
detail::report_error_and_abort(e);
} catch (...) {
detail::report_error_and_abort();
}
}
return child;
}
} // namespace process
} // namespace utils