// file : bbot/worker.cxx -*- C++ -*- // copyright : Copyright (c) 2014-2017 Code Synthesis Ltd // license : TBC; see accompanying LICENSE file #ifndef _WIN32 # include // signal() #else # include # include // getenv(), _putenv() #endif #include #include // strchr() #include #include #include #include #include #include #include #include #include #include using namespace butl; using namespace bbot; using std::cout; using std::endl; 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 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. // template static result_status run_cmd (tracer& t, string& log, const regexes& warn_detect, const string& name, const process_env& pe, A&&... a) { try { // Trace and log the command line. // auto cmdc = [&t, &log] (const char* c[], size_t n) { t (c, n); std::ostringstream os; process::print (os, c, n); log += os.str (); log += '\n'; }; fdpipe pipe (fdopen_pipe ()); // Text mode seems appropriate. process pr ( process_start_callback (cmdc, fdnull (), // Never reads from stdout. 2, // 1>&2 pipe.out, pe, forward (a)...)); pipe.out.close (); result_status r (result_status::success); // Log the diagnostics. Also print it to STDERR at verbosity level 3 or // higher. // auto add = [&log, &t] (const string& s, bool trace = true) { log += s; log += '\n'; if (verb >= 3) { if (trace) t << s; else text << s; } }; { ifdstream is (move (pipe.in), fdstream_mode::skip); // Skip on exception. for (string l; is.peek () != ifdstream::traits_type::eof (); ) { getline (is, l); add (l, false); // 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; const process_exit& e (*pr.exit); if (e.normal ()) { add (name + " exited with code " + to_string (e.code ())); return result_status::error; } else { add (name + " terminated abnormally: " + e.description () + (e.core () ? " (core dumped)" : "")); return result_status::abnormal; } } catch (const process_error& e) { fail << "unable to execute " << name << ": " << e << endf; } catch (const io_error& e) { fail << "unable to read " << name << " diagnostics: " << e << endf; } } template static result_status run_bpkg (tracer& t, string& log, const regexes& warn_detect, const string& cmd, A&&... a) { return run_cmd (t, log, warn_detect, "bpkg " + cmd, "bpkg", "-v", cmd, forward (a)...); } template static result_status run_b (tracer& t, string& log, const regexes& warn_detect, const V& envvars, const string& buildspec, A&&... a) { return run_cmd (t, log, warn_detect, "b " + buildspec, process_env ("b", envvars), "-v", forward (a)..., buildspec); } template static result_status run_b (tracer& t, string& log, const regexes& warn_detect, const string& buildspec, A&&... a) { const char* const* envvars (nullptr); return run_b (t, log, warn_detect, envvars, buildspec, forward (a)...); } 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, test, install and uninstall 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 (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 rwd; // Root working directory. 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); strings config (tm.unquoted_config ()); const vector_view env (argv + 1, argc - 1); // Configure. // // Configuration directory name. // dir_path build_dir ("build"); { operation_result& r (add_result ("configure")); // bpkg create // r.status |= run_bpkg (trace, r.log, wre, "create", "-d", build_dir.string (), "--wipe", config, env); if (!r.status) break; rwd = change_wd (build_dir); // bpkg add // 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 / // 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 // 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 // r.status |= run_bpkg (trace, r.log, wre, "test", tm.name); if (!r.status) break; rm.status |= r.status; } // Install the package, optionally test the installation and uninstall // afterwards. // // These operations are triggered by presence of config.install.root // configuration variable having a non-empty value. Passing [null] value // would be meaningless, so we don't recognize it as a special one. // dir_path install_root; { size_t n (19); auto space = [] (char c) {return c == ' ' || c == '\t';}; for (const auto& s: reverse_iterate (config)) { if (s.compare (0, n, "config.install.root") == 0 && (s[n] == '=' || space (s[n]))) { while (space (s[n])) ++n; // Skip spaces. if (s[n] == '=') ++n; // Skip the equal sign. while (space (s[n])) ++n; // Skip spaces. install_root = dir_path (s, n, s.size () - n); break; } } if (install_root.empty ()) break; } // Now the overall plan is as follows: // // 1. Install the package. // // 2. If the package project has the 'tests' subdirectory that is a // subproject, then configure, build and test it out of the source tree // against the installed package. // // 3. Uninstall the package. // // Install. // { operation_result& r (add_result ("install")); // bpkg install // r.status |= run_bpkg (trace, r.log, wre, "install", tm.name); if (!r.status) break; rm.status |= r.status; } // Test installed. // // The package tests subdirectory path (may not exist). // dir_path tests_dir (tm.name + "-" + tm.version.string ()); tests_dir /= "tests"; // We will consider the tests subdirectory to be a subproject if it // contains a build2 bootstrap file. // if (file_exists (tests_dir / path ("build/bootstrap.build"))) { operation_result& r (add_result ("test-installed")); change_wd (rwd); // Sort environment arguments into modules and configuration variables. // string mods; // build2 create meta-operation parameters. cstrings vars; for (const auto& a: env) { // Note that we don't check for the argument emptiness, as this is // already done by 'bpkg create' (see above). // if (strchr (a, '=') != nullptr) { vars.push_back (a); } else { mods += mods.empty () ? ", " : " "; mods += a; } } // b create(, ) // // Amalgamation directory that will contain configuration subdirectory // for package tests out of source tree build. // dir_path out_dir ("build-installed"); r.status |= run_b ( trace, r.log, wre, "create(" + out_dir.representation () + mods + ")", config, vars); if (!r.status) break; rm.status |= r.status; // Make sure that the installed package executables are properly imported // when configuring and running tests. // // Note that we add the $config.install.root/bin directory at the // beginning of the PATH environment variable value, so the installed // executables are found first. // string paths ("PATH=" + (install_root / "bin").string ()); if (char const* s = getenv ("PATH")) { paths += path::traits::path_separator; paths += s; } small_vector envvars ({move (paths)}); // b configure(@) // dir_path tests_out_dir (out_dir / dir_path ("tests")); r.status |= run_b (trace, r.log, wre, envvars, "configure(" + (build_dir / tests_dir).representation () + '@' + tests_out_dir.representation () + ")"); if (!r.status) break; rm.status |= r.status; // b test() // r.status |= run_b (trace, r.log, wre, envvars, "test(" + tests_out_dir.representation () + ')'); if (!r.status) break; rm.status |= r.status; change_wd (build_dir); } // Uninstall. // { operation_result& r (add_result ("uninstall")); // bpkg uninstall // r.status |= run_bpkg (trace, r.log, wre, "uninstall", 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 (!rwd.empty ()) change_wd (rwd); // 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 (mf, "task"); // Find the environment setup executable. // // 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). // string tg (tm.target.string ()); process_path 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. // // On Windows disable displaying error reporting dialog box. Note that the // error mode is inherited by child processes. // #ifndef _WIN32 if (signal (SIGPIPE, SIG_IGN) == SIG_ERR) fail << "unable to ignore broken pipe (SIGPIPE) signal: " << system_error (errno, std::generic_category ()); // Sanitize. #else SetErrorMode (SetErrorMode (0) | // Returns the current mode. SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); #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 " << LIBBPKG_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 (argc), const_cast (argv)); break; } } catch (const failed&) { return 1; // Diagnostics has already been issued. } catch (const cli::exception& e) { error << e; return 1; }