// 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 // getenv(), _putenv() #endif #include #include #include #include #include #include #include #include #include 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. } 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; } template static result_status run_bpkg (tracer& t, string& log, const string& cmd, A&&... a) { try { // Trace and log the command line. // auto cmdc = [&t, &log] (const char* c[], size_t n) { run_trace (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, dir_path (), "bpkg", "-v", cmd, forward (a)...)); pipe.out.close (); // 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'; } } if (pr.wait ()) return result_status::success; 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 (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. { // Configure. // { operation_result& r (add_result ("configure")); // bpkg create // const vector_view env (argv + 1, argc - 1); vector cfg; for (const variable& v: tm.config) cfg.push_back (v.unquoted ()); dir_path dir ("build"); r.status |= run_bpkg (trace, r.log, "create", "-d", dir.string (), "--wipe", cfg, env); if (!r.status) break; owd = change_wd (dir); // bpkg add // r.status |= run_bpkg (trace, r.log, "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, "fetch", ts, t); if (!r.status) break; // bpkg build --configure-only / // r.status |= run_bpkg (trace, r.log, "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, "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, "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. // try { tftp_curl c (trace, nullfd, mf, curl::get, url, "--max-time", tftp_timeout); if (!c.wait ()) throw_generic_error (EIO); } catch (const system_error& e) { fail << "unable to download task manifest from " << url << ": " << e; } // Parse it. // tm = parse_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. // // 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 ()); } 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", BBOT_VERSION}, {"libbbot", LIBBBOT_VERSION}, {"libbpkg", LIBBPKG_VERSION}, {"libbutl", LIBBUTL_VERSION} } }; 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 (); // Version. // if (ops.version ()) { cout << "bbot-worker " << BBOT_VERSION_STR << endl << "libbbot " << LIBBBOT_VERSION_STR << endl << "libbutl " << LIBBUTL_VERSION_STR << 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; // 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.environment_specified () ? ops.environment () : 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; }