diff options
author | Karen Arutyunov <karen@codesynthesis.com> | 2017-07-13 22:50:15 +0300 |
---|---|---|
committer | Karen Arutyunov <karen@codesynthesis.com> | 2017-07-14 19:10:22 +0300 |
commit | c8ace1ee0a6cab5fd4ea2f084ea436cfa513637d (patch) | |
tree | a8db884a665fbf14797393a3b2ff95438c338bb9 /bbot/worker/worker.cxx | |
parent | 8e8d599b129d35f638f2c1957c869b054a38b021 (diff) |
Make use of wildcards in buildfiles
Diffstat (limited to 'bbot/worker/worker.cxx')
-rw-r--r-- | bbot/worker/worker.cxx | 656 |
1 files changed, 656 insertions, 0 deletions
diff --git a/bbot/worker/worker.cxx b/bbot/worker/worker.cxx new file mode 100644 index 0000000..2aad8aa --- /dev/null +++ b/bbot/worker/worker.cxx @@ -0,0 +1,656 @@ +// file : bbot/worker.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : TBC; see accompanying LICENSE file + +#ifndef _WIN32 +# include <signal.h> // signal() +#else +# include <stdlib.h> // getenv(), _putenv() +#endif + +#include <regex> +#include <iostream> + +#include <libbutl/pager.hxx> +#include <libbutl/filesystem.hxx> + +#include <libbbot/manifest.hxx> + +#include <bbot/types.hxx> +#include <bbot/utility.hxx> + +#include <bbot/diagnostics.hxx> +#include <bbot/bootstrap-manifest.hxx> + +#include <bbot/worker/worker-options.hxx> + +using namespace std; +using namespace butl; +using namespace bbot; + +namespace bbot +{ + process_path argv0; + worker_options ops; + + dir_path env_dir; + + const size_t tftp_timeout (10); // 10 seconds. + const size_t tftp_retries (3); // Task request retries (see startup()). +} + +static dir_path +change_wd (const dir_path& d, bool create = false) +try +{ + if (create) + try_mkdir_p (d); + + dir_path r (dir_path::current_directory ()); + + dir_path::current_directory (d); + + return r; +} +catch (const system_error& e) +{ + fail << "unable to change current directory to " << d << ": " << e << endf; +} + +using regexes = vector<regex>; + +// 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. +// +template <typename... A> +static result_status +run_bpkg (tracer& t, + string& log, const regexes& warn_detect, + const string& cmd, A&&... a) +{ + try + { + // Trace and log the command line. + // + auto cmdc = [&t, &log] (const char* c[], size_t n) + { + t (c, n); + + ostringstream os; + process::print (os, c, n); + log += os.str (); + log += '\n'; + }; + + fdpipe pipe (fdopen_pipe ()); // Text mode seems appropriate. + + process pr ( + process_start (cmdc, + fdnull (), // Never reads from stdout. + 2, // 1>&2 + pipe.out, + "bpkg", + "-v", + cmd, + forward<A> (a)...)); + + pipe.out.close (); + + result_status r (result_status::success); + + // Log the diagnostics. + // + { + ifdstream is (move (pipe.in), fdstream_mode::skip); // Skip on exception. + + for (string l; is.peek () != ifdstream::traits_type::eof (); ) + { + getline (is, l); + log += l; + log += '\n'; + + // Match the log line with the warning-detecting regular expressions + // until the first match. + // + if (r != result_status::warning) + { + for (const auto& re: warn_detect) + { + // Only examine the first 512 bytes. Long lines (e.g., linker + // command lines) could trigger implementation-specific limitations + // (like stack overflow). Plus, it is a performance concern. + // + if (regex_search (l.begin (), + l.size () < 512 ? l.end () : l.begin () + 512, + re)) + { + r = result_status::warning; + break; + } + } + } + } + } + + if (pr.wait ()) + return r; + + log += "bpkg " + cmd; + const process_exit& e (*pr.exit); + + if (e.normal ()) + { + log += " exited with code " + to_string (e.code ()) + "\n"; + return result_status::error; + } + else + { + log += " terminated abnormally: " + e.description () + + (e.core () ? " (core dumped)" : "") + "\n"; + return result_status::abnormal; + } + } + catch (const process_error& e) + { + fail << "unable to execute bpkg " << cmd << ": " << e << endf; + } + catch (const io_error& e) + { + fail << "unable to read bpkg " << cmd << " diagnostics: " << e << endf; + } +} + +static void +build (size_t argc, const char* argv[]) +{ + tracer trace ("build"); + + // Our overall plan is as follows: + // + // 1. Parse the task manifest (it should be in CWD). + // + // 2. Run bpkg to create the configuration, add the repository, and + // configure, build, and test the package all while saving the logs in + // the result manifest. + // + // 3. Upload the result manifest. + // + // Note also that we are being "watched" by the startup version of us which + // will upload an appropriate result in case we exit with an error. So here + // for abnormal situations (like a failure to parse the manifest), we just + // fail. + // + task_manifest tm (parse_manifest<task_manifest> (path ("manifest"), "task")); + + result_manifest rm { + tm.name, + tm.version, + result_status::success, + operation_results {} + }; + + auto add_result = [&rm] (string o) -> operation_result& + { + rm.results.push_back ( + operation_result {move (o), result_status::success, ""}); + + return rm.results.back (); + }; + + dir_path owd; + + for (;;) // The "breakout" loop. + { + // Regular expressions that detect different forms of build2 toolchain + // warnings. Accidently (or not), they also cover GCC and Clang warnings + // (for the English locale). + // + // The expressions will be matched multiple times, so let's make the + // matching faster, with the potential cost of making regular expressions + // creation slower. + // + regex::flag_type f (regex_constants::optimize); // ECMAScript is implied. + + regexes wre ({ + regex ("^warning: ", f), + regex ("^.+: warning: ", f)}); + + for (const auto& re: tm.unquoted_warning_regex ()) + wre.emplace_back (re, f); + + // Configure. + // + { + operation_result& r (add_result ("configure")); + + // bpkg create <config-vars> <env-module> <env-config-vars> + // + const vector_view<const char*> env (argv + 1, argc - 1); + + // Use target (if present) or machine as configuration directory name. + // + dir_path dir (tm.target ? tm.target->string () : tm.machine); + + r.status |= run_bpkg (trace, r.log, wre, + "create", + "-d", dir.string (), + "--wipe", + tm.unquoted_config (), + env); + + if (!r.status) + break; + + owd = change_wd (dir); + + // bpkg add <repository-url> + // + r.status |= run_bpkg (trace, r.log, wre, "add", tm.repository.string ()); + + if (!r.status) + break; + + // bpkg fetch + // + string t ("--trust-no"); + + cstrings ts; + for (const string& fp: tm.trust) + { + if (fp == "yes") + t = "--trust-yes"; + else + { + ts.push_back ("--trust"); + ts.push_back (fp.c_str ()); + } + } + + r.status |= run_bpkg (trace, r.log, wre, "fetch", ts, t); + + if (!r.status) + break; + + // bpkg build --configure-only <package-name>/<package-version> + // + r.status |= run_bpkg (trace, r.log, wre, + "build", + "--configure-only", + "--yes", + tm.name + '/' + tm.version.string ()); + + if (!r.status) + break; + + rm.status |= r.status; + } + + // Update. + // + { + operation_result& r (add_result ("update")); + + // bpkg update <package-name> + // + r.status |= run_bpkg (trace, r.log, wre, "update", tm.name); + + if (!r.status) + break; + + rm.status |= r.status; + } + + // Test. + // + { + operation_result& r (add_result ("test")); + + // bpkg test <package-name> + // + r.status |= run_bpkg (trace, r.log, wre, "test", tm.name); + + if (!r.status) + break; + + rm.status |= r.status; + } + + break; + } + + rm.status |= rm.results.back ().status; // Merge last in case of a break. + + if (!owd.empty ()) + change_wd (owd); + + // Upload the result. + // + const string url ("tftp://" + ops.tftp_host () + "/manifest"); + + try + { + tftp_curl c (trace, + path ("-"), + nullfd, + curl::put, + url, + "--max-time", tftp_timeout); + + serialize_manifest (rm, c.out, url, "result"); + c.out.close (); + + if (!c.wait ()) + throw_generic_error (EIO); + } + catch (const system_error& e) + { + fail << "unable to upload result manifest to " << url << ": " << e; + } +} + +static void +startup () +{ + tracer trace ("startup"); + + // Our overall plan is as follows: + // + // 1. Download the task manifest into the build directory (CWD). + // + // 2. Parse it and get the target. + // + // 3. Find the environment setup executable for this target. + // + // 4. Execute the environment setup executable. + // + // 5. If the environment setup executable fails, then upload the (failed) + // result ourselves. + // + const string url ("tftp://" + ops.tftp_host () + "/manifest"); + const path mf ("manifest"); + + // If we fail, try to upload the result manifest (abnormal termination). The + // idea is that the machine gets suspended and we can investigate what's + // going on by logging in and examining the diagnostics (e.g., via + // journalctl, etc). + // + task_manifest tm; + + try + { + // Download the task. + // + // We are downloading from our host so there shouldn't normally be any + // connectivity issues. Unless, of course, we are on Windows where all + // kinds of flakiness is business as usual. Note that having a long enough + // timeout is not enough: if we try to connect before the network is up, + // we will keep waiting forever, even after it is up. So we have to + // timeout and try again. This is also pretty bad (unlike, say during + // bootstrap which doesn't happen very often) since we are wasting the + // machine time. So we are going to log it as a warning and not merely a + // trace since if this is a common occurrence, then something has to be + // done about it. + // + for (size_t retry (1);; ++retry) + { + try + { + tftp_curl c (trace, + nullfd, + mf, + curl::get, + url, + "--max-time", tftp_timeout); + + if (!c.wait ()) + throw_generic_error (EIO); + + break; + } + catch (const system_error& e) + { + bool bail (retry > tftp_retries); + diag_record dr (bail ? error : warn); + + dr << "unable to download task manifest from " << url << " on " + << retry << " try: " << e; + + if (bail) + throw failed (); + } + } + + // Parse it. + // + tm = parse_manifest<task_manifest> (mf, "task"); + + // Find the environment setup executable. + // + string tg; + process_path pp; + + if (tm.target) + { + tg = tm.target->string (); + + // While the executable path contains a directory (so the PATH search + // does not apply) we still use process::path_search() to automatically + // handle appending platform-specific executable extensions (.exe/.bat, + // etc). + // + pp = process::try_path_search (env_dir / tg, false); + } + + if (pp.empty ()) + pp = process::try_path_search (env_dir / "default", false); + + if (pp.empty ()) + fail << "no environment setup executable in " << env_dir << " " + << "for target '" << tg << "'"; + + // Run it. + // + strings os; + + if (ops.systemd_daemon ()) + os.push_back ("--systemd-daemon"); + + if (ops.verbose_specified ()) + { + os.push_back ("--verbose"); + os.push_back (to_string (ops.verbose ())); + } + + if (ops.tftp_host_specified ()) + { + os.push_back ("--tftp-host"); + os.push_back (ops.tftp_host ()); + } + + // Note that we use the effective (absolute) path instead of recall since + // we may have changed the CWD. + // + run (trace, pp, tg, argv0.effect_string (), os); + } + catch (const failed&) + { + // If we failed before being able to parse the task manifest, use the + // "unknown" values for the package name and version. + // + result_manifest rm { + tm.name.empty () ? "unknown" : tm.name, + tm.version.empty () ? bpkg::version ("0") : tm.version, + result_status::abnormal, + operation_results {} + }; + + try + { + tftp_curl c (trace, + path ("-"), + nullfd, + curl::put, + url, + "--max-time", tftp_timeout); + + serialize_manifest (rm, c.out, url, "result"); + c.out.close (); + + if (!c.wait ()) + throw_generic_error (EIO); + } + catch (const system_error& e) + { + error << "unable to upload result manifest to " << url << ": " << e; + } + + throw; + } +} + +static void +bootstrap () +{ + bootstrap_manifest bm { + bootstrap_manifest::versions_type { + {"bbot", standard_version (BBOT_VERSION_STR)}, + {"libbbot", standard_version (LIBBBOT_VERSION_STR)}, + {"libbpkg", standard_version (LIBBPKG_VERSION_STR)}, + {"libbutl", standard_version (LIBBUTL_VERSION_STR)} + } + }; + + serialize_manifest (bm, cout, "stdout", "bootstrap"); +} + +int +main (int argc, char* argv[]) +try +{ + // This is a little hack to make out baseutils for Windows work when called + // with absolute path. In a nutshell, MSYS2's exec*p() doesn't search in the + // parent's executable directory, only in PATH. And since we are running + // without a shell (that would read /etc/profile which sets PATH to some + // sensible values), we are only getting Win32 PATH values. And MSYS2 /bin + // is not one of them. So what we are going to do is add /bin at the end of + // PATH (which will be passed as is by the MSYS2 machinery). This will make + // MSYS2 search in /bin (where our baseutils live). And for everyone else + // this should be harmless since it is not a valid Win32 path. + // +#ifdef _WIN32 + { + string mp ("PATH="); + if (const char* p = getenv ("PATH")) + { + mp += p; + mp += ';'; + } + mp += "/bin"; + + _putenv (mp.c_str ()); + } +#endif + + // 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 + // behavior on a file descriptor basis or for the write() function call. + // +#ifndef _WIN32 + if (signal (SIGPIPE, SIG_IGN) == SIG_ERR) + fail << "unable to ignore broken pipe (SIGPIPE) signal: " + << system_error (errno, generic_category ()); // Sanitize. +#endif + + cli::argv_scanner scan (argc, argv, true); + ops.parse (scan); + + verb = ops.verbose (); + + if (ops.systemd_daemon ()) + systemd_diagnostics (false); + + // Version. + // + if (ops.version ()) + { + cout << "bbot-worker " << BBOT_VERSION_ID << endl + << "libbbot " << LIBBBOT_VERSION_ID << endl + << "libbpkg " << LIBBBOT_VERSION_ID << endl + << "libbutl " << LIBBUTL_VERSION_ID << endl + << "Copyright (c) 2014-2017 Code Synthesis Ltd" << endl + << "TBC; All rights reserved" << endl; + + return 0; + } + + // Help. + // + if (ops.help ()) + { + pager p ("bbot-worker help", false); + print_bbot_worker_usage (p.stream ()); + + // If the pager failed, assume it has issued some diagnostics. + // + return p.wait () ? 0 : 1; + } + + // Figure out our mode. + // + if (ops.bootstrap () && ops.startup ()) + fail << "--bootstrap and --startup are mutually exclusive"; + + enum class mode {boot, start, build} m (mode::build); + + if (ops.bootstrap ()) m = mode::boot; + if (ops.startup ()) m = mode::start; + + if (ops.systemd_daemon ()) + { + info << "bbot worker " << BBOT_VERSION_ID; + } + + // Figure out our path (used for re-exec). + // + argv0 = process::path_search (argv[0], true); + + // Sort out the build directory. + // + if (ops.build_specified ()) + change_wd (ops.build (), true); // Create if does not exist. + + // Sort out the environment directory. + // + try + { + env_dir = ops.environments_specified () + ? ops.environments () + : dir_path::home_directory (); + + if (!dir_exists (env_dir)) + throw_generic_error (ENOENT); + } + catch (const system_error& e) + { + fail << "invalid environment directory: " << e; + } + + switch (m) + { + case mode::boot: bootstrap (); break; + case mode::start: startup (); break; + case mode::build: build (static_cast<size_t> (argc), + const_cast<const char**> (argv)); break; + } +} +catch (const failed&) +{ + return 1; // Diagnostics has already been issued. +} +catch (const cli::exception& e) +{ + error << e; + return 1; +} |