From 2af2c4f092aa7efffe839ec615c06d22cf43cc3b Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Tue, 16 Mar 2021 20:21:59 +0300 Subject: Add support for interactive builds --- bbot/agent/agent.cli | 18 ++ bbot/agent/agent.cxx | 80 ++++++- bbot/agent/agent.hxx | 1 + bbot/agent/machine.cxx | 39 +++- bbot/agent/machine.hxx | 9 +- bbot/bbot-agent@.service | 6 + bbot/types-parsers.cxx | 21 ++ bbot/types-parsers.hxx | 9 + bbot/worker/worker.cli | 8 + bbot/worker/worker.cxx | 519 +++++++++++++++++++++++++++++-------------- doc/manual.cli | 60 ++++- tests/integration/testscript | 27 ++- 12 files changed, 609 insertions(+), 188 deletions(-) diff --git a/bbot/agent/agent.cli b/bbot/agent/agent.cli index 5d6cc9d..b50a43a 100644 --- a/bbot/agent/agent.cli +++ b/bbot/agent/agent.cli @@ -1,6 +1,8 @@ // file : bbot/agent.cli // license : TBC; see accompanying LICENSE file +include ; + include ; "\section=1" @@ -78,6 +80,15 @@ namespace bbot testing)." } + interactive_mode --interactive = interactive_mode::false_ + { + "", + "Interactive build support. Valid values for this option are \cb{false} + (only non-interactive), \cb{true} (only interactive), and \cb{both}. + If this option is not specified, then only non-interactive builds + are supported." + } + // We reserve 0 in case in the future we want to distinguish a single- // instance mode or some such. // @@ -182,6 +193,13 @@ namespace bbot default." } + size_t --intactive-timeout = 10800 + { + "", + "Maximum number of seconds to wait for interactive build completion, + 10800 (3 hours) by default." + } + size_t --connect-timeout = 60 { "", diff --git a/bbot/agent/agent.cxx b/bbot/agent/agent.cxx index 6c2e0d9..d85ecf5 100644 --- a/bbot/agent/agent.cxx +++ b/bbot/agent/agent.cxx @@ -8,6 +8,7 @@ #include // signal() #include // rand_r() #include // sleep(), getuid(), fsync(), [f]stat() +#include // getifaddrs(), freeifaddrs() #include // stat #include // [f]stat() #include // flock() @@ -62,6 +63,7 @@ namespace bbot uint16_t offset; string hname; + string hip; uid_t uid; string uname; } @@ -179,7 +181,8 @@ bootstrap_machine (const dir_path& md, mm, obmm ? obmm->machine.mac : nullopt, ops.bridge (), - tftpd.port ())); + tftpd.port (), + false /* pub_vnc */)); { // If we are terminating with an exception then force the machine down. @@ -888,7 +891,8 @@ try mm.machine, mm.machine.mac, ops.bridge (), - tftpd.port ())); + tftpd.port (), + tm.interactive.has_value ())); // Note: the machine handling logic is similar to bootstrap. // @@ -947,7 +951,9 @@ try // size_t to; const size_t startup_to (120); - const size_t build_to (ops.build_timeout ()); + const size_t build_to (tm.interactive + ? ops.intactive_timeout () + : ops.build_timeout ()); // Wait periodically making sure the machine is still alive. // @@ -1093,6 +1099,8 @@ try uid = getuid (); uname = getpwuid (uid)->pw_name; + // Obtain our hostname. + // { char buf[HOST_NAME_MAX + 1]; @@ -1103,6 +1111,44 @@ try hname = buf; } + // Obtain our IP address as a first discovered non-loopback IPv4 address. + // + // Note: Linux-specific implementation. + // + { + ifaddrs* i; + if (getifaddrs (&i) == -1) + fail << "unable to obtain IP addresses: " + << system_error (errno, std::generic_category ()); // Sanitize. + + unique_ptr deleter (i, freeifaddrs); + + for (; i != nullptr; i = i->ifa_next) + { + sockaddr* sa (i->ifa_addr); + + if (sa != nullptr && // Configured. + (i->ifa_flags & IFF_LOOPBACK) == 0 && // Not a loopback interface. + (i->ifa_flags & IFF_UP) != 0 && // Up. + sa->sa_family == AF_INET) // Ignore IPv6 for now. + { + char buf[INET_ADDRSTRLEN]; // IPv4 address. + if (inet_ntop (AF_INET, + &reinterpret_cast (sa)->sin_addr, + buf, + sizeof (buf)) == nullptr) + fail << "unable to obtain IPv4 address: " + << system_error (errno, std::generic_category ()); // Sanitize. + + hip = buf; + break; + } + } + + if (hip.empty ()) + fail << "no IPv4 address configured"; + } + // On POSIX ignore SIGPIPE which is signaled to a pipe-writing process if // the pipe reading end is closed. Note that by default this signal // terminates a process. Also note that there is no way to disable this @@ -1246,6 +1292,15 @@ try return std::uniform_int_distribution (50, 60) (g); }; + optional imode; + optional ilogin; + + if (ops.interactive () != interactive_mode::false_) + { + imode = ops.interactive (); + ilogin = machine_vnc (true /* public */); + } + for (unsigned int sleep (0);; ::sleep (sleep), sleep = 0) { bootstrapped_machines ms (enumerate_machines (ops.machines ())); @@ -1256,6 +1311,8 @@ try hname, tc_name, tc_ver, + imode, + ilogin, fingerprint, machine_header_manifests {} }; @@ -1379,6 +1436,19 @@ try continue; } + // Make sure that the task interactivity matches the requested mode. + // + if (( t.interactive && !imode) || + (!t.interactive && imode && *imode == interactive_mode::true_)) + { + if (t.interactive) + error << "interactive task from " << u << ": " << *t.interactive; + else + error << "non-interactive task from " << u; + + continue; + } + l2 ([&]{trace << "task for " << t.name << '/' << t.version << " " << "on " << t.machine << " " << "from " << u;}); @@ -1558,7 +1628,7 @@ namespace bbot iface_addr (const string& i) { if (i.size () >= IFNAMSIZ) - throw invalid_argument ("interface nama too long"); + throw invalid_argument ("interface name too long"); auto_fd fd (socket (AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0)); @@ -1572,7 +1642,7 @@ namespace bbot if (ioctl (fd.get (), SIOCGIFADDR, &ifr) == -1) throw_system_error (errno); - char buf[3 * 4 + 3 + 1]; // IPv4 address. + char buf[INET_ADDRSTRLEN]; // IPv4 address. if (inet_ntop (AF_INET, &reinterpret_cast (&ifr.ifr_addr)->sin_addr, buf, diff --git a/bbot/agent/agent.hxx b/bbot/agent/agent.hxx index 93c4b56..ba3719e 100644 --- a/bbot/agent/agent.hxx +++ b/bbot/agent/agent.hxx @@ -25,6 +25,7 @@ namespace bbot extern uint16_t inst; // Instance number. extern string hname; // Our host name. + extern string hip; // Our IP address. extern uid_t uid; // Our effective user id. extern string uname; // Our effective user name. diff --git a/bbot/agent/machine.cxx b/bbot/agent/machine.cxx index c884f8c..3768971 100644 --- a/bbot/agent/machine.cxx +++ b/bbot/agent/machine.cxx @@ -171,7 +171,8 @@ namespace bbot const machine_manifest&, const optional& mac, const string& br_iface, - uint16_t tftp_port); + uint16_t tftp_port, + bool pub_vnc); virtual bool shutdown (size_t& seconds) override; @@ -214,18 +215,14 @@ namespace bbot const machine_manifest& mm, const optional& omac, const string& br, - uint16_t port) + uint16_t port, + bool pub_vnc) : machine (mm.mac ? *mm.mac : // Fixed mac from machine manifest. omac ? *omac : // Generated mac from previous bootstrap. generate_mac ()), kvm ("kvm"), net (br, port), - // - // QEMU's -vnc option (see below) expects the port offset from 5900 - // rather than the absolute value. The low 5901+, 6001+, and 6101+ - // ports all look good collision-wise with anything useful. - // - vnc ("127.0.0.1:" + to_string (5900 + offset)), + vnc (machine_vnc (pub_vnc)), monitor ("/tmp/monitor-" + tc_name + '-' + to_string (inst)) { tracer trace ("kvm_machine", md.string ().c_str ()); @@ -402,7 +399,15 @@ namespace bbot // VNC. // - "-vnc", "127.0.0.1:" + to_string (offset), // 5900 + offset + // We listen on all IPs for a public VNC session and only on localhost + // for private. + // + // QEMU's -vnc option expects the port offset from 5900 rather than the + // absolute value. The low 5901+, 6001+, and 6101+ ports all look good + // collision-wise with anything useful. + // + "-vnc", + (pub_vnc ? ":" : "127.0.0.1:") + to_string (offset), // 5900 + offset // QMP. // @@ -636,16 +641,28 @@ namespace bbot const machine_manifest& mm, const optional& mac, const string& br_iface, - uint16_t tftp_port) + uint16_t tftp_port, + bool pub_vnc) { switch (mm.type) { case machine_type::kvm: - return make_unique (md, mm, mac, br_iface, tftp_port); + return make_unique ( + md, mm, mac, br_iface, tftp_port, pub_vnc); + case machine_type::nspawn: assert (false); //@@ TODO } return nullptr; } + + string + machine_vnc (bool pub) + { + string r (pub ? hip : "127.0.0.1"); + r += ':'; + r += to_string (5900 + offset); + return r; + } } diff --git a/bbot/agent/machine.hxx b/bbot/agent/machine.hxx index 44c7480..9a47d12 100644 --- a/bbot/agent/machine.hxx +++ b/bbot/agent/machine.hxx @@ -83,7 +83,14 @@ namespace bbot const machine_manifest&, const optional& mac, const string& br_iface, - uint16_t tftp_port); + uint16_t tftp_port, + bool pub_vnc); + + // Return the machine's public or private VNC session endpoint in the + // ':' form. + // + string + machine_vnc (bool pub_vnc); } #endif // BBOT_AGENT_MACHINE_HXX diff --git a/bbot/bbot-agent@.service b/bbot/bbot-agent@.service index f610af2..294fde7 100644 --- a/bbot/bbot-agent@.service +++ b/bbot/bbot-agent@.service @@ -19,12 +19,16 @@ Environment=BRIDGE=br1 Environment=AUTH_KEY= +Environment=INTERACTIVE=false + Environment=BOOTSTRAP_TIMEOUT=3600 Environment=BOOTSTRAP_RETRIES=2 Environment=BUILD_TIMEOUT=5400 Environment=BUILD_RETRIES=2 +Environment=INTERACTIVE_TIMEOUT=10800 + Environment=CONNECT_TIMEOUT=60 Environment=REQUEST_TIMEOUT=300 Environment=REQUEST_RETRIES=4 @@ -46,10 +50,12 @@ ExecStart=/build/bots/default/bin/bbot-agent \ --ram ${RAM} \ --bridge ${BRIDGE} \ --auth-key ${AUTH_KEY} \ + --interactive ${INTERACTIVE} \ --bootstrap-timeout ${BOOTSTRAP_TIMEOUT} \ --bootstrap-retries ${BOOTSTRAP_RETRIES} \ --build-timeout ${BUILD_TIMEOUT} \ --build-retries ${BUILD_RETRIES} \ + --intactive-timeout ${INTERACTIVE_TIMEOUT} \ --connect-timeout ${CONNECT_TIMEOUT} \ --request-timeout ${REQUEST_TIMEOUT} \ --request-retries ${REQUEST_RETRIES} \ diff --git a/bbot/types-parsers.cxx b/bbot/types-parsers.cxx index 7e82914..c4eff70 100644 --- a/bbot/types-parsers.cxx +++ b/bbot/types-parsers.cxx @@ -67,5 +67,26 @@ namespace bbot throw invalid_value (o, v, e.what ()); } } + + void parser:: + parse (interactive_mode& x, bool& xs, scanner& s) + { + xs = true; + const char* o (s.next ()); + + if (!s.more ()) + throw missing_value (o); + + const char* v (s.next ()); + + try + { + x = to_interactive_mode (v); + } + catch (const invalid_argument&) + { + throw invalid_value (o, v); + } + } } } diff --git a/bbot/types-parsers.hxx b/bbot/types-parsers.hxx index 1657086..23fc95b 100644 --- a/bbot/types-parsers.hxx +++ b/bbot/types-parsers.hxx @@ -7,6 +7,8 @@ #ifndef BBOT_TYPES_PARSERS_HXX #define BBOT_TYPES_PARSERS_HXX +#include + #include namespace bbot @@ -38,6 +40,13 @@ namespace bbot static void parse (standard_version&, bool&, scanner&); }; + + template <> + struct parser + { + static void + parse (interactive_mode&, bool&, scanner&); + }; } } diff --git a/bbot/worker/worker.cli b/bbot/worker/worker.cli index e84d147..e3e37ce 100644 --- a/bbot/worker/worker.cli +++ b/bbot/worker/worker.cli @@ -82,6 +82,14 @@ namespace bbot specified, then the user's home directory is used." } + path --environment + { + "", + "The environment setup executable path. This option is normally passed + by the worker running in the startup mode to the worker executed in the + build mode." + } + // Testing options. // string --tftp-host = "196.254.111.222" diff --git a/bbot/worker/worker.cxx b/bbot/worker/worker.cxx index bd880ae..3499ac4 100644 --- a/bbot/worker/worker.cxx +++ b/bbot/worker/worker.cxx @@ -16,6 +16,7 @@ #include #include +#include #include // to_utf8() #include #include @@ -147,33 +148,180 @@ catch (const system_error& e) fail << "unable to remove directory " << d << ": " << e << endf; } +// Step IDs. +// +enum class step_id +{ + bpkg_module_create, + bpkg_module_configure_add, + bpkg_module_configure_fetch, + bpkg_module_configure_build, + bpkg_module_update, + bpkg_module_test, + bpkg_create, + bpkg_configure_add, + bpkg_configure_fetch, + bpkg_configure_build, + bpkg_update, + bpkg_test, + bpkg_test_separate_configure_build, + bpkg_test_separate_update, + bpkg_test_separate_test, + bpkg_install, + b_test_installed_create, + b_test_installed_configure, + b_test_installed_test, + bpkg_test_installed_create, + bpkg_test_installed_configure_add, + bpkg_test_installed_configure_fetch, + bpkg_test_separate_installed_configure_build, + bpkg_test_separate_installed_update, + bpkg_test_separate_installed_test, + bpkg_uninstall +}; + +// @@ Feel a bit heavy-weight. Should we move to const char*[]? +// +static const strings step_id_str { + "bpkg.module.create", + "bpkg.module.configure.add", + "bpkg.module.configure.fetch", + "bpkg.module.configure.build", + "bpkg.module.update", + "bpkg.module.test", + "bpkg.create", + "bpkg.configure.add", + "bpkg.configure.fetch", + "bpkg.configure.build", + "bpkg.update", + "bpkg.test", + "bpkg.test-separate.configure.build", + "bpkg.test-separate.update", + "bpkg.test-separate.test", + "bpkg.install", + "b.test-installed.create", + "b.test-installed.configure", + "b.test-installed.test", + "bpkg.test-installed.create", + "bpkg.test-installed.configure.add", + "bpkg.test-installed.configure.fetch", + "bpkg.test-separate-installed.configure.build", + "bpkg.test-separate-installed.update", + "bpkg.test-separate-installed.test", + "bpkg.uninstall"}; + using std::regex; namespace regex_constants = std::regex_constants; using regexes = vector; -// Run a named command. Name is used for logging and diagnostics only. Match -// lines read from the command's stderr against the regular expressions and -// return the warning result status (instead of success) in case of a match. +// Run the worker script command. Name is used for logging and diagnostics +// only. Match lines read from the command's stderr against the regular +// expressions and return the warning result status (instead of success) in +// case of a match. Save the executed command into last_cmd. +// +// If bkp_step is present and is equal to the command step, then prior to +// running this command ask the user if to continue or abort the task +// execution. If bkp_status is present, then ask for that if the command +// execution results with the specified or more critical status. // template static result_status -run_cmd (tracer& t, +run_cmd (step_id step, + tracer& t, string& log, const regexes& warn_detect, const string& name, + const optional& bkp_step, + const optional& bkp_status, + string& last_cmd, const process_env& pe, A&&... a) { + // UTF-8-sanitize and log the diagnostics. Also print the raw diagnostics + // to stderr at verbosity level 3 or higher. + // + auto add = [&log, &t] (string&& s, bool trace = true) + { + if (verb >= 3) + { + if (trace) + t << s; + else + text << s; + } + + to_utf8 (s, '?', codepoint_types::graphic, U"\n\r\t"); + + log += s; + log += '\n'; + }; + + string next_cmd; + + // Prompt the user if to continue the task execution and, if they refuse, + // log this and throw abort. + // + struct abort {}; + + auto prompt = [&last_cmd, &next_cmd, &add] (const string& what) + { + diag_record dr (text); + + dr << '\n' + << what << '\n' + << " current dir: " << current_directory () << '\n' + << " environment: " << ops.environment (); + + if (!last_cmd.empty ()) + dr << '\n' + << " last command: " << last_cmd; + + if (!next_cmd.empty ()) + dr << '\n' + << " next command: " << next_cmd; + + dr.flush (); + + if (!yn_prompt ( + "continue execution (or you may shutdown the machine)? [y/n]")) + { + add ("execution aborted by interactive user"); + throw abort (); + } + }; + try { - // Trace and log the command line. + // Trace, log, and save the command line. // - auto cmdc = [&t, &log] (const char* c[], size_t n) + auto cmdc = [step, &t, &log, &bkp_step, &next_cmd, &prompt] + (const char* c[], size_t n) { - t (c, n); + const string& sid (step_id_str[static_cast (step)]); std::ostringstream os; process::print (os, c, n); - log += os.str (); + next_cmd = os.str (); + + // Prompt the user if the breakpoint is reached. + // + if (bkp_step && *bkp_step == step) + prompt (sid + " step is reached"); + + // Log the step id and the command to be executed. + // + l3 ([&]{t << "step id: " << sid;}); + +#ifndef _WIN32 + log += "# step id: "; +#else + log += "rem step id: "; +#endif + log += sid; + log += '\n'; + + t (c, n); + + log += next_cmd; log += '\n'; }; @@ -191,25 +339,6 @@ run_cmd (tracer& t, result_status r (result_status::success); - // UTF-8-sanitize and log the diagnostics. Also print the raw diagnostics - // to stderr at verbosity level 3 or higher. - // - auto add = [&log, &t] (string&& s, bool trace = true) - { - if (verb >= 3) - { - if (trace) - t << s; - else - text << s; - } - - to_utf8 (s, '?', codepoint_types::graphic, U"\n\r\t"); - - log += s; - log += '\n'; - }; - { ifdstream is (move (pipe.in), fdstream_mode::skip); // Skip on exception. @@ -242,14 +371,26 @@ run_cmd (tracer& t, } } - if (pr.wait ()) - return r; + if (!pr.wait ()) + { + const process_exit& e (*pr.exit); + add (name + " " + to_string (e)); + r = e.normal () ? result_status::error : result_status::abnormal; + } - const process_exit& e (*pr.exit); + last_cmd = move (next_cmd); - add (name + " " + to_string (e)); + if (bkp_status && r >= *bkp_status) + { + next_cmd.clear (); // Note: used by prompt(). + prompt (!r ? "error occured" : "warning is issued"); + } - return e.normal () ? result_status::error : result_status::abnormal; + return r; + } + catch (const abort&) + { + return result_status::abort; } catch (const process_error& e) { @@ -263,39 +404,55 @@ run_cmd (tracer& t, template static result_status -run_bpkg (const V& envvars, +run_bpkg (step_id step, + const V& envvars, tracer& t, string& log, const regexes& warn_detect, + const optional& bkp_step, + const optional& bkp_status, + string& last_cmd, const char* verbosity, const string& cmd, A&&... a) { - return run_cmd (t, + return run_cmd (step, + t, log, warn_detect, "bpkg " + cmd, + bkp_step, bkp_status, last_cmd, process_env ("bpkg", envvars), verbosity, cmd, forward (a)...); } template static result_status -run_bpkg (tracer& t, +run_bpkg (step_id step, + tracer& t, string& log, const regexes& warn_detect, + const optional& bkp_step, + const optional& bkp_status, + string& last_cmd, const char* verbosity, const string& cmd, A&&... a) { const char* const* envvars (nullptr); - return run_bpkg (envvars, + return run_bpkg (step, + envvars, t, log, warn_detect, + bkp_step, bkp_status, last_cmd, verbosity, cmd, forward (a)...); } template static result_status -run_b (const V& envvars, +run_b (step_id step, + const V& envvars, tracer& t, string& log, const regexes& warn_detect, + const optional& bkp_step, + const optional& bkp_status, + string& last_cmd, const char* verbosity, const strings& buildspecs, A&&... a) { @@ -308,39 +465,53 @@ run_b (const V& envvars, name += s; } - return run_cmd (t, + return run_cmd (step, + t, log, warn_detect, name, + bkp_step, bkp_status, last_cmd, process_env ("b", envvars), verbosity, buildspecs, forward (a)...); } template static result_status -run_b (const V& envvars, +run_b (step_id step, + const V& envvars, tracer& t, string& log, const regexes& warn_detect, + const optional& bkp_step, + const optional& bkp_status, + string& last_cmd, const char* verbosity, const string& buildspec, A&&... a) { - return run_cmd (t, + return run_cmd (step, + t, log, warn_detect, "b " + buildspec, + bkp_step, bkp_status, last_cmd, process_env ("b", envvars), verbosity, buildspec, forward (a)...); } template static result_status -run_b (tracer& t, +run_b (step_id step, + tracer& t, string& log, const regexes& warn_detect, + const optional& bkp_step, + const optional& bkp_status, + string& last_cmd, const char* verbosity, const string& buildspec, A&&... a) { const char* const* envvars (nullptr); - return run_b (envvars, + return run_b (step, + envvars, t, log, warn_detect, + bkp_step, bkp_status, last_cmd, verbosity, buildspec, forward (a)...); } @@ -507,71 +678,54 @@ build (size_t argc, const char* argv[]) for (const string& re: tm.unquoted_warning_regex ()) wre.emplace_back (re, f); - // Step IDs. + // Resolve the breakpoint specified by the interactive manifest value into + // the step id or the result status breakpoint. If the breakpoint is + // invalid, then log the error and abort the build. Note that we reuse the + // configure operation log here not to complicate things. // - enum class step_id + optional bkp_step; + optional bkp_status; + string last_cmd; // Used in the user prompt. + + if (tm.interactive) { - bpkg_module_create, - bpkg_module_configure_add, - bpkg_module_configure_fetch, - bpkg_module_configure_build, - bpkg_module_update, - bpkg_module_test, - bpkg_create, - bpkg_configure_add, - bpkg_configure_fetch, - bpkg_configure_build, - bpkg_update, - bpkg_test, - bpkg_test_separate_configure_build, - bpkg_test_separate_update, - bpkg_test_separate_test, - bpkg_install, - b_test_installed_create, - b_test_installed_configure, - b_test_installed_test, - bpkg_test_installed_create, - bpkg_test_installed_configure_add, - bpkg_test_installed_configure_fetch, - bpkg_test_separate_installed_configure_build, - bpkg_test_separate_installed_update, - bpkg_test_separate_installed_test, - bpkg_uninstall - }; + const string& b (*tm.interactive); + + if (b == "error") + bkp_status = result_status::error; + else if (b == "warning") + bkp_status = result_status::warning; + else + { + for (size_t i (0); i < step_id_str.size (); ++i) + { + if (b == step_id_str[i]) + { + bkp_step = static_cast (i); + break; + } + } + } - const strings step_id_str { - "bpkg.module.create", - "bpkg.module.configure.add", - "bpkg.module.configure.fetch", - "bpkg.module.configure.build", - "bpkg.module.update", - "bpkg.module.test", - "bpkg.create", - "bpkg.configure.add", - "bpkg.configure.fetch", - "bpkg.configure.build", - "bpkg.update", - "bpkg.test", - "bpkg.test-separate.configure.build", - "bpkg.test-separate.update", - "bpkg.test-separate.test", - "bpkg.install", - "b.test-installed.create", - "b.test-installed.configure", - "b.test-installed.test", - "bpkg.test-installed.create", - "bpkg.test-installed.configure.add", - "bpkg.test-installed.configure.fetch", - "bpkg.test-separate-installed.configure.build", - "bpkg.test-separate-installed.update", - "bpkg.test-separate-installed.test", - "bpkg.uninstall"}; + if (!bkp_step && !bkp_status) + { + string e ("invalid interactive build breakpoint '" + b + "'"); + + l3 ([&]{trace << e;}); + + operation_result& r (add_result ("configure")); + + r.log = "error: " + e + '\n'; + r.status = result_status::abort; + + break; + } + } // Split the argument into prefix (empty if not present) and unquoted // value. Return nullopt if the prefix is invalid. // - auto parse_arg = - [&step_id_str] (const string& a) -> optional> + auto parse_arg = [] (const string& a) -> optional> { size_t p (a.find_first_of (":=\"'")); @@ -660,10 +814,9 @@ build (size_t argc, const char* argv[]) // Return command arguments for the specified step id. Arguments with more // specific prefixes come last. // - auto step_args = [&step_id_str] (const std::multimap& args, - step_id step, - optional fallback = nullopt) - -> strings + auto step_args = [] (const std::multimap& args, + step_id step, + optional fallback = nullopt) -> strings { strings r; const string& sid (step_id_str[static_cast (step)]); @@ -807,11 +960,13 @@ build (size_t argc, const char* argv[]) // for the build2 process. Return true if the dist meta-operation // succeeds. // - auto redist = [&trace, &wre] (operation_result& r, - const dir_path& dist_root, - const dir_path& pkg_dir, // - - const char* import = nullptr, - const small_vector& envvars = {}) + auto redist = [&trace, &wre, &bkp_step, &bkp_status, &last_cmd] + (step_id step, + operation_result& r, + const dir_path& dist_root, + const dir_path& pkg_dir, // - + const char* import = nullptr, + const small_vector& envvars = {}) { // Temporarily change the current directory to the distribution root // parent directory from the configuration directory to shorten the @@ -832,8 +987,10 @@ build (size_t argc, const char* argv[]) dir_path redist_root ("re" + dn.string ()); r.status |= run_b ( + step, envvars, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "config.dist.root=" + redist_root.string (), import, @@ -907,7 +1064,9 @@ build (size_t argc, const char* argv[]) // to cc. // r.status |= run_b ( + step_id::bpkg_module_create, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-V", "create(" + module_dir.representation () + ",cc)", "config.config.load=~build2", @@ -921,7 +1080,9 @@ build (size_t argc, const char* argv[]) // bpkg create --existing // r.status |= run_bpkg ( + step_id::bpkg_module_create, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "create", "--existing"); @@ -934,7 +1095,9 @@ build (size_t argc, const char* argv[]) // bpkg.module.configure.add (bpkg.configure.add) // r.status |= run_bpkg ( + step_id::bpkg_module_configure_add, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "add", @@ -956,7 +1119,9 @@ build (size_t argc, const char* argv[]) // bpkg.module.configure.fetch (bpkg.configure.fetch) // r.status |= run_bpkg ( + step_id::bpkg_module_configure_fetch, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "fetch", @@ -978,7 +1143,9 @@ build (size_t argc, const char* argv[]) // [bpkg.module.configure.build] // r.status |= run_bpkg ( + step_id::bpkg_module_configure_build, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "build", "--configure-only", @@ -1012,7 +1179,10 @@ build (size_t argc, const char* argv[]) // Note that we reuse the configure operation log for the dist // meta-operation. // - if (!redist (r, dist_root, pkg_dir)) + if (!redist (step_id::bpkg_module_configure_build, + r, + dist_root, + pkg_dir)) break; rm.status |= r.status; @@ -1035,7 +1205,9 @@ build (size_t argc, const char* argv[]) // [bpkg.module.update] // r.status |= run_bpkg ( + step_id::bpkg_module_update, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "update", pkg); @@ -1069,7 +1241,9 @@ build (size_t argc, const char* argv[]) // [bpkg.module.test] // r.status |= run_bpkg ( + step_id::bpkg_module_test, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "test", "--package-cwd", @@ -1113,7 +1287,9 @@ build (size_t argc, const char* argv[]) // importable in this configuration (see above about bootstrap). // r.status |= run_bpkg ( + step_id::bpkg_create, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-V", "create", "-d", build_dir.string (), @@ -1134,7 +1310,9 @@ build (size_t argc, const char* argv[]) // bpkg.configure.add // r.status |= run_bpkg ( + step_id::bpkg_configure_add, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "add", step_args (env_args, step_id::bpkg_configure_add), @@ -1149,7 +1327,9 @@ build (size_t argc, const char* argv[]) // bpkg.configure.fetch // r.status |= run_bpkg ( + step_id::bpkg_configure_fetch, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "fetch", step_args (env_args, step_id::bpkg_configure_fetch), @@ -1167,7 +1347,9 @@ build (size_t argc, const char* argv[]) if (!module) // Note: the module is already built in the pre-step. { r.status |= run_bpkg ( + step_id::bpkg_configure_build, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "build", "--configure-only", @@ -1188,7 +1370,7 @@ build (size_t argc, const char* argv[]) if (dist) { - if (!redist (r, dist_root, pkg_dir)) + if (!redist (step_id::bpkg_configure_build, r, dist_root, pkg_dir)) break; rm.status |= r.status; @@ -1209,7 +1391,9 @@ build (size_t argc, const char* argv[]) // bpkg.update // r.status |= run_bpkg ( + step_id::bpkg_update, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "update", step_args (env_args, step_id::bpkg_update), @@ -1288,6 +1472,7 @@ build (size_t argc, const char* argv[]) // as a special dependency for the test package. // auto test = [&trace, &wre, + &bkp_step, &bkp_status, &last_cmd, &step_args, &config_args, &env_args, &pm, &redist] @@ -1298,18 +1483,6 @@ build (size_t argc, const char* argv[]) const char* import = nullptr, const small_vector& envvars = {}) { - auto args = [installed, &step_args] ( - const std::multimap& args, - step_id test_separate_installed_step, - step_id test_separate_step, - step_id main_step) - { - return installed - ? step_args (args, test_separate_installed_step, main_step) - : step_args (args, test_separate_step, main_step); - - }; - for (const test_dependency& td: pm.tests) { const string& pkg (td.name.string ()); @@ -1321,25 +1494,22 @@ build (size_t argc, const char* argv[]) // // bpkg.test-separate[-installed].configure.build (bpkg.configure.build) // + step_id s (installed + ? step_id::bpkg_test_separate_installed_configure_build + : step_id::bpkg_test_separate_configure_build); + r.status |= run_bpkg ( + s, envvars, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "build", "--configure-only", "--checkout-root", dist_root, "--yes", - - args (env_args, - step_id::bpkg_test_separate_installed_configure_build, - step_id::bpkg_test_separate_configure_build, - step_id::bpkg_configure_build), - - args (config_args, - step_id::bpkg_test_separate_installed_configure_build, - step_id::bpkg_test_separate_configure_build, - step_id::bpkg_configure_build), - + step_args (env_args, s, step_id::bpkg_configure_build), + step_args (config_args, s, step_id::bpkg_configure_build), import, "--", td.string (), @@ -1371,7 +1541,7 @@ build (size_t argc, const char* argv[]) dist_root); if (!pkg_dir.empty () && - !redist (r, dist_root, pkg_dir, import, envvars)) + !redist (s, r, dist_root, pkg_dir, import, envvars)) return false; } catch (const system_error& e) @@ -1385,22 +1555,19 @@ build (size_t argc, const char* argv[]) // // bpkg.test-separate[-installed].update (bpkg.update) // + s = installed + ? step_id::bpkg_test_separate_installed_update + : step_id::bpkg_test_separate_update; + r.status |= run_bpkg ( + s, envvars, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "update", - - args (env_args, - step_id::bpkg_test_separate_installed_update, - step_id::bpkg_test_separate_update, - step_id::bpkg_update), - - args (config_args, - step_id::bpkg_test_separate_installed_update, - step_id::bpkg_test_separate_update, - step_id::bpkg_update), - + step_args (env_args, s, step_id::bpkg_update), + step_args (config_args, s, step_id::bpkg_update), import, pkg); @@ -1416,23 +1583,20 @@ build (size_t argc, const char* argv[]) // // bpkg.test-separate[-installed].test (bpkg.test) // + s = installed + ? step_id::bpkg_test_separate_installed_test + : step_id::bpkg_test_separate_test; + r.status |= run_bpkg ( + s, envvars, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "test", "--package-cwd", // See above for details. - - args (env_args, - step_id::bpkg_test_separate_installed_test, - step_id::bpkg_test_separate_test, - step_id::bpkg_test), - - args (config_args, - step_id::bpkg_test_separate_installed_test, - step_id::bpkg_test_separate_test, - step_id::bpkg_test), - + step_args (env_args, s, step_id::bpkg_test), + step_args (config_args, s, step_id::bpkg_test), import, pkg); @@ -1465,7 +1629,9 @@ build (size_t argc, const char* argv[]) // bpkg.test // r.status |= run_bpkg ( + step_id::bpkg_test, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "test", "--package-cwd", // See above for details. @@ -1538,7 +1704,9 @@ build (size_t argc, const char* argv[]) // bpkg.install // r.status |= run_bpkg ( + step_id::bpkg_install, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "install", step_args (env_args, step_id::bpkg_install), @@ -1634,7 +1802,9 @@ build (size_t argc, const char* argv[]) dir_path out_dir ("build-installed"); r.status |= run_b ( + step_id::b_test_installed_create, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-V", "create('" + out_dir.representation () + "'" + mods + ")", step_args (env_args, step_id::b_test_installed_create), @@ -1660,8 +1830,10 @@ build (size_t argc, const char* argv[]) dir_path subprj_out_dir (out_dir / d); r.status |= run_b ( + step_id::b_test_installed_configure, envvars, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "configure('" + subprj_src_dir.representation () + "'@'" + @@ -1686,8 +1858,10 @@ build (size_t argc, const char* argv[]) // b.test-installed.test // r.status |= run_b ( + step_id::b_test_installed_test, envvars, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", test_specs, step_args (env_args, step_id::b_test_installed_test), @@ -1710,7 +1884,9 @@ build (size_t argc, const char* argv[]) dir_path config_dir ("build-installed-bpkg"); r.status |= run_bpkg ( + step_id::bpkg_test_installed_create, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-V", "create", "-d", config_dir.string (), @@ -1738,7 +1914,9 @@ build (size_t argc, const char* argv[]) // bpkg.test-installed.configure.add (bpkg.configure.add) // r.status |= run_bpkg ( + step_id::bpkg_test_installed_configure_add, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "add", @@ -1760,7 +1938,9 @@ build (size_t argc, const char* argv[]) // bpkg.test-installed.configure.fetch (bpkg.configure.fetch) // r.status |= run_bpkg ( + step_id::bpkg_test_installed_configure_fetch, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "fetch", @@ -1808,7 +1988,9 @@ build (size_t argc, const char* argv[]) // bpkg.uninstall // r.status |= run_bpkg ( + step_id::bpkg_uninstall, trace, r.log, wre, + bkp_step, bkp_status, last_cmd, "-v", "uninstall", step_args (env_args, step_id::bpkg_uninstall), @@ -1981,24 +2163,29 @@ startup () // strings os; + // Use the name=value notation for options to minimize the number of + // arguments passed to the environment setup executable. Note that the + // etc/environments/default-*.bat scripts can only handle the limited + // number of arguments. + // if (ops.systemd_daemon ()) os.push_back ("--systemd-daemon"); if (ops.verbose_specified ()) - { - os.push_back ("--verbose"); - os.push_back (to_string (ops.verbose ())); - } + os.push_back ("--verbose=" + to_string (ops.verbose ())); if (ops.tftp_host_specified ()) - { - os.push_back ("--tftp-host"); - os.push_back (ops.tftp_host ()); - } + os.push_back ("--tftp-host=" + ops.tftp_host ()); + + os.push_back (string ("--environment=") + pp.effect_string ()); // Note that we use the effective (absolute) path instead of recall since // we may have changed the CWD. // + // Also note that the worker can ask the user if to continue the task + // execution when the interactive build breakpoint is reached. Thus, we + // don't redirect stdin to /dev/null. + // // Exit code 2 signals abnormal termination but where the worker uploaded // the result itself. // @@ -2007,7 +2194,7 @@ startup () // nobody listening on the other end anymore). // string tg (tm.target.string ()); - switch (run_exit (trace, pp, tg, argv0.effect_string (), os)) + switch (run_io_exit (trace, 0, 2, 2, pp, tg, argv0.effect_string (), os)) { case 3: case 2: return 1; diff --git a/doc/manual.cli b/doc/manual.cli index 8285366..d08a6ff 100644 --- a/doc/manual.cli +++ b/doc/manual.cli @@ -63,6 +63,15 @@ machines and sends this information as part of the request. The controller responds with a build task manifest that identifies a specific build machine to use. +In the task request the agent specifies if only non-interactive, interactive, +or both build kinds are supported. If interactive builds are supported, it +additionally provides the login information for interactive build sessions. If +the controller responds with an interactive build task, then its manifest +specifies the breakpoint the worker must stop the task execution at and prompt +the user whether to continue or abort the execution. The user can log into the +build machine, potentially perform some troubleshooting, and, when done, +either answer the prompt or just shutdown the machine. + If the controller has higher-level controllers (for example, \c{brep}), then it aggregates the available build machines from its agents and polls these controllers (just as an agent would), forwarding build tasks to suitable @@ -328,6 +337,7 @@ target: [environment]: [config]: [warning-regex]: +[interactive]: \ @@ -495,6 +505,21 @@ Note that this built-in list also covers GCC and Clang warnings (for the English locale). +\h2#arch-task-interactive|\c{interactive}| + +\ +[interactive]: +\ + +The task execution step to stop at. Can only be present if the agent has +specified \c{interactive-mode} with either the \c{true} or \c{both} value in +the task request. + +The breakpoint can either be a primary step id of the worker script or the +special \c{error} or \c{warning} value. See \l{#arch-worker Worker Logic} for +details. + + \h#arch-result|Result Manifest| The result manifest describes a build result. The manifest synopsis is @@ -615,6 +640,8 @@ description of each value in subsequent sections. agent: toolchain-name: toolchain-version: +[interactive-mode]: false|true|both +[interactive-login]: [fingerprint]: \ @@ -647,6 +674,28 @@ toolchain-version: The \c{build2} toolchain version being used by the agent. +\h2#arch-task-req-interactive-mode|\c{interactive-mode}| + +\ +[interactive-mode]: false|true|both +\ + +The agent's capability to perform build tasks only non-interactively +(\c{false}), only interactively (\c{true}), or both (\c{both}). + +If it is not specified, then the \c{false} value is assumed. + + +\h2#arch-task-req-interactive-login|\c{interactive-login}| + +\ +[interactive-login]: +\ + +The login information for the interactive build session. Must be present only +if \c{interactive-mode} is specified with the \c{true} or \c{both} value. + + \h2#arch-task-req-fingerprint|\c{fingerprint}| \ @@ -742,8 +791,8 @@ The session id as returned by the controller in the task response. \ The answer to the private key challenge as posed by the controller in the task -response. It must be present only if the challenge value was present in the -task response. +response. It must be present only if the \c{challenge} value was present in +the task response. \h#arch-worker|Worker Logic| @@ -947,6 +996,13 @@ then its \c{dist} meta-operation is also tested as a part of the \c{bpkg[.*].configure.build} steps by re-distributing the source directory in the load distribution mode after configuration. +If the build is interactive, then the worker pauses its execution at the +specified breakpoint and prompts the user whether to continue or abort the +execution. If the breakpoint is a step id, then the worker pauses prior to +executing every command of the specified step. Otherwise, the breakpoint +denotes the result status and the worker pauses if the command results with +the specified or more critical status (see \l{#arch-result Result Manifest}). + As an example, the following POSIX shell script can be used to setup the environment for building C and C++ packages with GCC 9 on most Linux distributions. diff --git a/tests/integration/testscript b/tests/integration/testscript index e9b0db1..1408946 100644 --- a/tests/integration/testscript +++ b/tests/integration/testscript @@ -126,6 +126,22 @@ rep_type = git rfp = yes #\ +#\ +pkg = libcmark-gfm-extensions +ver = 0.29.0-a.1+7 +rep_url = https://pkg.cppget.org/1/alpha +rep_type = pkg +rfp = yes +#\ + +#\ +pkg = non-existing +ver = 0.1.0 +rep_url = https://pkg.cppget.org/1/alpha +rep_type = pkg +rfp = yes +#\ + # Note that we also need to make sure that the installed package libraries are # properly imported when configuring and running tests, and that the installed # executables are runnable. @@ -133,9 +149,13 @@ rfp = yes config = "\"config.install.root='$~/install'\" \ bpkg:--fetch-timeout=60 \ \"config.bin.rpath='$~/install/lib'\" \ +config.cc.coptions=-Wall \ b.test-installed.configure:\"config.cc.loptions=-L'$~/install/lib'\" \ bpkg.test-installed.create:\"config.cc.loptions=-L'$~/install/lib'\"" +#interactive="interactive: bpkg.configure.build" +#interactive="interactive: warning" + +cat <<"EOI" >=task : 1 name: $pkg @@ -146,6 +166,7 @@ bpkg.test-installed.create:\"config.cc.loptions=-L'$~/install/lib'\"" machine: $machine target: $target config: $config + $interactive EOI +if ("$environment" != "") @@ -188,9 +209,9 @@ a = $0 chmod ugo+x $env; sleep $wait; $w --verbose 3 --startup --tftp-host $tftp --environments $~ \ - &?build-module/*** &build/*** \ + &?build-module/*** &?build/*** \ &?build-installed/*** &?build-installed-bpkg/*** \ - &?dist/*** &?redist/*** \ + &?dist/*** &?redist/*** \ &?dist-installed/*** &?redist-installed/*** \ - &task.manifest 2>| + &task.manifest <| 2>| } -- cgit v1.1