// file : bbot/worker.cxx -*- C++ -*- // license : MIT; see accompanying LICENSE file #ifndef _WIN32 # include // signal() #else # include // SetErrorMode(), Sleep() #endif #include #include #include // strchr(), strncmp() #include #include #include // find(), find_if(), remove_if() #include #include #include #include // to_utf8(), eof() #include #include #include #include #include #include #include #include #include using namespace butl; using namespace bbot; using std::cout; using std::endl; namespace bbot { int main (int argc, char* argv[]); static int build (size_t argc, const char* argv[]); process_path argv0; worker_options ops; dir_path env_dir; // Note that upload can be quite large and take a while to upload under high // load. // const size_t tftp_blksize (1468); // Between 512 (default) and 65464. const size_t tftp_put_timeout (3600); // 1 hour (also the default). const size_t tftp_get_timeout (10); // 10 seconds. const size_t tftp_get_retries (3); // Task request retries (see startup()). } bool exists (const dir_path& d) try { return dir_exists (d); } catch (const system_error& e) { fail << "unable to stat path " << d << ": " << e << endf; } static dir_path current_directory () try { return dir_path::current_directory (); } catch (const system_error& e) { fail << "unable to obtain current directory: " << e << endf; } static dir_path change_wd (tracer& t, string* log, const dir_path& d, bool create = false) try { if (create) { if (verb >= 3) t << "mkdir -p " << d; if (log != nullptr) *log += "mkdir -p " + d.representation () + '\n'; try_mkdir_p (d); } dir_path r (current_directory ()); if (verb >= 3) t << "cd " << d; if (log != nullptr) *log += "cd " + d.representation () + '\n'; dir_path::current_directory (d); return r; } catch (const system_error& e) { fail << "unable to change current directory to " << d << ": " << e << endf; } static void mv (tracer& t, string* log, const dir_path& from, const dir_path& to) try { if (verb >= 3) t << "mv " << from << ' ' << to; if (log != nullptr) *log += "mv " + from.representation () + ' ' + to.representation () + "\n"; mvdir (from, to); } catch (const system_error& e) { fail << "unable to move directory '" << from << "' to '" << to << "': " << e << endf; } static void rm_r (tracer& t, string* log, const dir_path& d) try { if (verb >= 3) t << "rm -r " << d; if (log != nullptr) *log += "rm -r " + d.representation () + '\n'; rmdir_r (d); } catch (const system_error& e) { fail << "unable to remove directory " << d << ": " << e << endf; } // Step IDs. // enum class step_id { // Common fallbacks for bpkg_*_create/b_test_installed_create and // bpkg_*_configure_build/b_test_installed_configure, respectively. Note: // not breakpoints. // b_create, b_configure, // Note that bpkg_module_* options are only used if the main package is a // build system module (using just ~build2 otherwise). They also have no // fallback (build system modules are just too different to try to handle // them together with target and host; e.g., install root). However, // bpkg_module_create is complemented with arguments from un-prefixed step // ids, the same way as other *.create[_for_*] steps (note that un-prefixed // steps are not fallbacks, they are always added first). // bpkg_create, // Breakpoint and base. bpkg_target_create, //: b_create, bpkg_create bpkg_host_create, //: b_create, bpkg_create bpkg_module_create, //: no fallback bpkg_link, bpkg_configure_add, bpkg_configure_fetch, // Global (as opposed to package-specific) bpkg-pkg-build options (applies // to all *_configure_build* steps). Note: not a breakpoint. // bpkg_global_configure_build, // Note that bpkg_configure_build serves as a breakpoint for the // bpkg-pkg-build call that configures (at once) the main package and all // its external tests. // bpkg_configure_build, // Breakpoint and base. bpkg_target_configure_build, //: b_configure, bpkg_configure_build bpkg_host_configure_build, //: b_configure, bpkg_configure_build bpkg_module_configure_build, //: b_configure, bpkg_configure_build bpkg_update, bpkg_test, // Note that separate test packages are configured as part of the // bpkg_configure_build step above with options taken from // bpkg_{target,host}_configure_build, depending on tests package type. // bpkg_test_separate_update, //: bpkg_update bpkg_test_separate_test, //: bpkg_test // Note that we only perform the installation tests if this is a target // package or a self-hosted configuration. // bpkg_install, // Note: skipped for modules. // b_test_installed_create, //: b_create b_test_installed_configure, //: b_configure b_test_installed_test, // Note that for a host package this can involve both run-time and build- // time tests (which means we may also need a shared configuration for // modules). // // The *_for_{target,host,module} denote main package type, not // configuration being created, which will always be target (more precisely, // target or host, but host only in a self-hosted case, which means it's // the same as target). // // Note that if this is a non-self-hosted configuration, we can only end up // here if building target package and so can just use *_create and *_build // values in buildtabs. // bpkg_test_separate_installed_create, // Breakpoint and base. bpkg_test_separate_installed_create_for_target, //: bpkg_test_separate_installed_create bpkg_test_separate_installed_create_for_host, //: bpkg_test_separate_installed_create bpkg_test_separate_installed_create_for_module, //: no fallback bpkg_test_separate_installed_link, // breakpoint only bpkg_test_separate_installed_configure_add, //: bpkg_configure_add bpkg_test_separate_installed_configure_fetch, //: bpkg_configure_fetch bpkg_test_separate_installed_configure_build, // Breakpoint and base. bpkg_test_separate_installed_configure_build_for_target, //: bpkg_test_separate_installed_configure_build bpkg_test_separate_installed_configure_build_for_host, //: bpkg_test_separate_installed_configure_build bpkg_test_separate_installed_configure_build_for_module, //: bpkg_test_separate_installed_configure_build bpkg_test_separate_installed_update, //: bpkg_update bpkg_test_separate_installed_test, //: bpkg_test bpkg_uninstall, end }; static const strings step_id_str { "b.create", "b.configure", "bpkg.create", "bpkg.target.create", "bpkg.host.create", "bpkg.module.create", "bpkg.link", "bpkg.configure.add", "bpkg.configure.fetch", "bpkg.global.configure.build", "bpkg.configure.build", "bpkg.target.configure.build", "bpkg.host.configure.build", "bpkg.module.configure.build", "bpkg.update", "bpkg.test", "bpkg.test-separate.update", "bpkg.test-separate.test", "bpkg.install", "b.test-installed.create", "b.test-installed.configure", "b.test-installed.test", "bpkg.test-separate-installed.create", "bpkg.test-separate-installed.create_for_target", "bpkg.test-separate-installed.create_for_host", "bpkg.test-separate-installed.create_for_module", "bpkg.test-separate-installed.link", "bpkg.test-separate-installed.configure.add", "bpkg.test-separate-installed.configure.fetch", "bpkg.test-separate-installed.configure.build", "bpkg.test-separate-installed.configure.build_for_target", "bpkg.test-separate-installed.configure.build_for_host", "bpkg.test-separate-installed.configure.build_for_module", "bpkg.test-separate-installed.update", "bpkg.test-separate-installed.test", "bpkg.uninstall", "end"}; using std::regex; namespace regex_constants = std::regex_constants; using regexes = vector; // 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. // // Redirect stdout to stderr if the out argument is NULL. Otherwise, save the // process output into the referenced variable. Note: currently assumes that // the output will always fit into the pipe buffer. // // 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. // // For the special end step no command is executed. In this case only the user // is potentially prompted and the step is traced/logged. // template static result_status run_cmd (step_id step, tracer& t, string& log, optional* out, 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.env_script () << ' ' << ops.env_target (); 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 (); } }; auto prompt_step = [step, &t, &log, &bkp_step, &prompt] () { const string& sid (step_id_str[static_cast (step)]); // Prompt the user if the breakpoint is reached. // if (bkp_step && *bkp_step == step) prompt (sid + " step reached"); string ts (to_string (system_clock::now (), "%Y-%m-%d %H:%M:%S %Z", true /* special */, true /* local */)); // Log the step id and the command to be executed. // l3 ([&]{t << "step id: " << sid << ' ' << ts;}); #ifndef _WIN32 log += "# step id: "; #else log += "rem step id: "; #endif log += sid; log += ' '; log += ts; log += '\n'; }; try { // Trace, log, and save the command line. // auto cmdc = [&t, &log, &next_cmd, &prompt_step] (const char* c[], size_t n) { std::ostringstream os; process::print (os, c, n); next_cmd = os.str (); prompt_step (); t (c, n); log += next_cmd; log += '\n'; }; result_status r (result_status::success); if (step != step_id::end) { try { // Redirect stdout to stderr, if the caller is not interested in it. // // Text mode seems appropriate. // fdpipe out_pipe (out != nullptr ? fdopen_pipe () : fdpipe ()); fdpipe err_pipe (fdopen_pipe ()); process pr ( process_start_callback (cmdc, fdopen_null (), // Never reads from stdin. out != nullptr ? out_pipe.out.get () : 2, err_pipe, pe, forward (a)...)); out_pipe.out.close (); err_pipe.out.close (); { // Skip on exception. // ifdstream is (move (err_pipe.in), fdstream_mode::skip, ifdstream::badbit); for (string l; !eof (getline (is, l)); ) { // Match the log line with the warning-detecting regular // expressions until the first match. // if (r != result_status::warning) { for (const regex& 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; } } } add (move (l), false /* trace */); } } if (!pr.wait ()) { const process_exit& e (*pr.exit); add (name + ' ' + to_string (e)); r = e.normal () ? result_status::error : result_status::abnormal; } // Only read the buffered output if the process terminated normally. // if (out != nullptr && pr.exit->normal ()) { // Note: shouldn't throw since the output is buffered. // ifdstream is (move (out_pipe.in)); *out = is.read_text (); } last_cmd = move (next_cmd); if (bkp_status && r >= *bkp_status) { next_cmd.clear (); // Note: used by prompt(). prompt (!r ? "error occured" : "warning is issued"); } } catch (const process_error& e) { fail << "unable to execute " << name << ": " << e; } catch (const io_error& e) { fail << "unable to read " << name << " diagnostics: " << e; } } else { next_cmd.clear (); // Note: used by prompt_step(). prompt_step (); } return r; } catch (const abort&) { return result_status::abort; } } template static result_status run_bpkg (step_id step, const V& envvars, tracer& t, string& log, optional* out, 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 (step, t, log, out, warn_detect, "bpkg " + cmd, bkp_step, bkp_status, last_cmd, process_env ("bpkg", envvars), verbosity, cmd, forward (a)...); } template static result_status 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_bpkg (step, envvars, t, log, nullptr /* out */, warn_detect, bkp_step, bkp_status, last_cmd, verbosity, cmd, forward (a)...); } template static result_status run_bpkg (step_id step, tracer& t, string& log, optional* out, 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 (step, envvars, t, log, out, warn_detect, bkp_step, bkp_status, last_cmd, verbosity, cmd, forward (a)...); } template static result_status 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 (step, envvars, t, log, warn_detect, bkp_step, bkp_status, last_cmd, verbosity, cmd, forward (a)...); } template static result_status 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) { string name ("b"); for (const string& s: buildspecs) { if (!name.empty ()) name += ' '; name += s; } return run_cmd (step, t, log, nullptr /* out */, warn_detect, name, bkp_step, bkp_status, last_cmd, process_env ("b", envvars), verbosity, buildspecs, forward (a)...); } template static result_status 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 (step, t, log, nullptr /* out */, warn_detect, "b " + buildspec, bkp_step, bkp_status, last_cmd, process_env ("b", envvars), verbosity, buildspec, forward (a)...); } template static result_status 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 (step, envvars, t, log, warn_detect, bkp_step, bkp_status, last_cmd, verbosity, buildspec, forward (a)...); } // Upload compressed manifest to the specified TFTP URL with curl. Issue // diagnostics and throw failed on invalid manifest or process management // errors and throw io_error for input/output errors or non-zero curl exit. // template static void upload_manifest (tracer& trace, const string& url, const T& m, const string& what) { try { // Piping the data directly into curl's stdin sometimes results in the // broken pipe error on the client and partial/truncated upload on the // server. This happens quite regularly on older Linux distributions // (e.g., Debian 8, Ubuntu 16.04) but also sometimes on Windows. On the // other hand, uploading from a file appears to work reliably (we still // get an odd error on Windows from time to time with larger uploads). // // Let's not break lines in the manifest values not to further increase // the size of the manifest encoded representation. Also here we don't // care much about readability of the manifest since it will only be read // by the bbot agent anyway. // #if 0 // Note: need to add compression support if re-enable this. tftp_curl c (trace, path ("-"), nullfd, curl::put, url, "--tftp-blksize", tftp_blksize, "--max-time", tftp_put_timeout); manifest_serializer s (c.out, url, true /* long_lines */); m.serialize (s); c.out.close (); #else auto_rmfile tmp; try { tmp = auto_rmfile (path::temp_path (what + "-manifest.lz4")); ofdstream ofs (tmp.path, fdopen_mode::binary); olz4stream ozs (ofs, 9, 5 /* 256KB */, nullopt /* content_size */); manifest_serializer s (ozs, tmp.path.string (), true /* long_lines */); m.serialize (s); ozs.close (); ofs.close (); } catch (const io_error& e) // In case not derived from system_error. { fail << "unable to save " << what << " manifest: " << e; } catch (const system_error& e) { fail << "unable to save " << what << " manifest: " << e; } tftp_curl c (trace, tmp.path, nullfd, curl::put, url, "--tftp-blksize", tftp_blksize, "--max-time", tftp_put_timeout); #endif if (!c.wait ()) throw_generic_ios_failure (EIO, "non-zero curl exit code"); } catch (const manifest_serialization& e) { fail << "invalid " << what << " manifest: " << e.description; } catch (const process_error& e) { fail << "unable to execute curl: " << e; } catch (const system_error& e) { const auto& c (e.code ()); if (c.category () == generic_category ()) throw_generic_ios_failure (c.value (), e.what ()); else throw_system_ios_failure (c.value (), e.what ()); } } static const string worker_checksum ("4"); // Logic version. static int bbot:: build (size_t argc, const char* argv[]) { using namespace bpkg; using string_parser::unquote; 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 package/tests configurations, add the // repository to them, and configure, build, test, optionally install, // test installed and uninstall the package all while saving the logs in // the result manifest. // // 3. Upload the result manifest. // // NOTE: consider updating worker_checksum if making any logic changes. // // 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 ("task.manifest"), "task")); // Reset the dependency checksum if the task's worker checksum doesn't match // the current one. // if (!tm.worker_checksum || *tm.worker_checksum != worker_checksum) tm.dependency_checksum = nullopt; result_manifest rm { tm.name, tm.version, result_status::success, operation_results {}, worker_checksum, nullopt /* dependency_checksum */ }; 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. // 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. // optional bkp_step; optional bkp_status; string last_cmd; // Used in the user prompt. for (;;) // The "breakout" loop. { auto fail_operation = [&trace] (operation_result& r, const string& e, result_status s) { l3 ([&]{trace << e;}); r.log += "error: " + e + '\n'; r.status = s; }; // 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 string& re: tm.unquoted_warning_regex ()) wre.emplace_back (re, f); if (tm.interactive && *tm.interactive != "none") { 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; } } } if (!bkp_step && !bkp_status) { fail_operation (add_result ("configure"), "invalid interactive build breakpoint '" + b + '\'', 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 = [] (const string& a) -> optional> { size_t p (a.find_first_of (":=\"'")); if (p == string::npos || a[p] != ':') // No prefix. return make_pair (string (), unquote (a)); for (const string& id: step_id_str) { if (a.compare (0, p, id, 0, p) == 0 && (id.size () == p || (id.size () > p && id[p] == '.'))) return make_pair (a.substr (0, p), unquote (a.substr (p + 1))); } return nullopt; // Prefix is invalid. }; // Parse configuration arguments. Report failures to the bbot controller. // std::multimap tgt_args; for (const string& c: tm.target_config) { optional> v (parse_arg (c)); if (!v) { rm.status |= result_status::abort; l3 ([&]{trace << "invalid configuration argument prefix in " << "'" << c << "'";}); break; } if (v->second[0] != '-' && v->second.find ('=') == string::npos) { rm.status |= result_status::abort; l3 ([&]{trace << "invalid configuration argument '" << c << "'";}); break; } tgt_args.emplace (move (*v)); } if (!rm.status) break; // Parse environment arguments. // std::multimap modules; std::multimap env_args; for (size_t i (1); i != argc; ++i) { const char* a (argv[i]); optional> v (parse_arg (a)); if (!v) fail << "invalid environment argument prefix in '" << a << "'"; bool mod (v->second[0] != '-' && v->second.find ('=') == string::npos); if (mod && !v->first.empty () && v->first != "b.create" && v->first != "bpkg.create" && v->first != "bpkg.target.create" && v->first != "bpkg.host.create" && v->first != "bpkg.module.create" && v->first != "b.test-installed.create" && v->first != "bpkg.test-separate-installed.create" && v->first != "bpkg.test-separate-installed.create_for_target" && v->first != "bpkg.test-separate-installed.create_for_host" && v->first != "bpkg.test-separate-installed.create_for_module") fail << "invalid module prefix in '" << a << "'"; (mod ? modules : env_args).emplace (move (*v)); } // Return command arguments for the specified step id, complementing // *.create[_for_*] steps with un-prefixed arguments. If no arguments are // specified for the step then use the specified fallbacks, potentially // both. Arguments with more specific prefixes come last. // auto step_args = [] (const std::multimap& args, step_id step, optional fallback1 = nullopt, optional fallback2 = nullopt) -> cstrings { cstrings r; // Add arguments for a specified, potentially empty, prefix. // auto add_args = [&args, &r] (const string& prefix) { auto range (args.equal_range (prefix)); for (auto i (range.first); i != range.second; ++i) r.emplace_back (i->second.c_str ()); }; // Add un-prefixed arguments if this is one of the *.create[_for_*] // steps. // switch (step) { case step_id::b_create: case step_id::bpkg_create: case step_id::bpkg_target_create: case step_id::bpkg_host_create: case step_id::bpkg_module_create: case step_id::b_test_installed_create: case step_id::bpkg_test_separate_installed_create: case step_id::bpkg_test_separate_installed_create_for_target: case step_id::bpkg_test_separate_installed_create_for_host: case step_id::bpkg_test_separate_installed_create_for_module: { add_args (""); break; } default: break; } auto add_step_args = [&add_args] (step_id step) { const string& s (step_id_str[static_cast (step)]); for (size_t n (0);; ++n) { n = s.find ('.', n); add_args (n == string::npos ? s : string (s, 0, n)); if (n == string::npos) break; } }; // If no arguments found for the step id, then use the fallback step // ids, if specified. // if (args.find (step_id_str[static_cast (step)]) != args.end ()) { add_step_args (step); } else { // Note that if we ever need to specify fallback pairs with common // ancestors, we may want to suppress duplicate ancestor step ids. // if (fallback1) add_step_args (*fallback1); if (fallback2) add_step_args (*fallback2); } return r; }; // bpkg-rep-fetch trust options. // cstrings trust_ops; { const char* t ("--trust-no"); for (const string& fp: tm.trust) { if (fp == "yes") t = "--trust-yes"; else { trust_ops.push_back ("--trust"); trust_ops.push_back (fp.c_str ()); } } trust_ops.push_back (t); } const string& pkg (tm.name.string ()); const version& ver (tm.version); const string repo (tm.repository.string ()); const dir_path pkg_dir (pkg + '-' + ver.string ()); const string pkg_var (tm.name.variable ()); // Specify the revision explicitly for the bpkg-build command not to end // up with a race condition building the latest revision rather than the // zero revision. // const string pkg_rev (pkg + '/' + version (ver.epoch, ver.upstream, ver.release, ver.effective_revision (), ver.iteration).string ()); // Parse the build package configuration represented as a whitespace // separated list of the following potentially quoted bpkg-pkg-build // command arguments: // //