// file : mod/mod-build-log.cxx -*- C++ -*- // license : MIT; see accompanying LICENSE file #include <mod/mod-build-log.hxx> #include <odb/database.hxx> #include <odb/transaction.hxx> #include <libbutl/timestamp.hxx> // to_stream() #include <web/server/module.hxx> #include <libbrep/build.hxx> #include <libbrep/build-odb.hxx> #include <mod/module-options.hxx> using namespace std; using namespace brep::cli; using namespace odb::core; // While currently the user-defined copy constructor is not required (we don't // need to deep copy nullptr's), it is a good idea to keep the placeholder // ready for less trivial cases. // brep::build_log:: build_log (const build_log& r) : database_module (r), build_config_module (r), options_ (r.initialized_ ? r.options_ : nullptr) { } void brep::build_log:: init (scanner& s) { HANDLER_DIAG; options_ = make_shared<options::build_log> ( s, unknown_mode::fail, unknown_mode::fail); if (options_->build_config_specified ()) { database_module::init (*options_, options_->build_db_retry ()); build_config_module::init (*options_); } if (options_->root ().empty ()) options_->root (dir_path ("/")); } bool brep::build_log:: handle (request& rq, response& rs) { using brep::version; // Not to confuse with module::version. HANDLER_DIAG; if (build_db_ == nullptr) throw invalid_request (501, "not implemented"); // Parse the HTTP request URL path (without the root directory) to obtain // the build id and optional operation name. If the operation is not // specified then print logs for all the operations. // // Note that the URL path must be in the following form: // // <pkg-name>/<pkg-version>/log/<cfg-name>/<target>/<toolchain-name>/<toolchain-version>[/<operation>] // // Also note that the presence of the first 3 components is guaranteed by // the repository_root module. // build_id id; string op; path lpath (rq.path ().leaf (options_->root ())); // If the tenant is not empty then it is contained in the leftmost path // component (see repository_root for details). Strip it if that's the case. // if (!tenant.empty ()) { assert (!lpath.empty ()); lpath = path (++lpath.begin (), lpath.end ()); } assert (!lpath.empty ()); try { auto i (lpath.begin ()); package_name name; try { name = package_name (*i++); } catch (const invalid_argument& e) { throw invalid_argument (string ("invalid package name: ") + e.what ()); } assert (i != lpath.end ()); auto parse_version = [] (const string& v, const char* what) -> version { // Intercept exception handling to add the parsing error attribution. // try { return brep::version (v); } catch (const invalid_argument& e) { throw invalid_argument (string ("invalid ") + what + ": " + e.what ()); } }; version package_version (parse_version (*i++, "package version")); assert (i != lpath.end () && *i == "log"); if (++i == lpath.end ()) throw invalid_argument ("no target"); target_triplet target; try { target = target_triplet (*i++); } catch (const invalid_argument& e) { throw invalid_argument (string ("invalid target: ") + e.what ()); } if (i == lpath.end ()) throw invalid_argument ("no target configuration name"); string target_config (*i++); if (target_config.empty ()) throw invalid_argument ("empty target configuration name"); if (i == lpath.end ()) throw invalid_argument ("no package configuration name"); string package_config (*i++); if (package_config.empty ()) throw invalid_argument ("empty package configuration name"); if (i == lpath.end ()) throw invalid_argument ("no toolchain name"); string toolchain_name (*i++); if (toolchain_name.empty ()) throw invalid_argument ("empty toolchain name"); if (i == lpath.end ()) throw invalid_argument ("no toolchain version"); version toolchain_version (parse_version (*i++, "toolchain version")); id = build_id (package_id (tenant, move (name), package_version), move (target), move (target_config), move (package_config), move (toolchain_name), toolchain_version); if (i != lpath.end ()) op = *i++; if (i != lpath.end ()) throw invalid_argument ("unexpected path component"); } catch (const invalid_argument& e) { throw invalid_request (400, e.what ()); } // Make sure no parameters passed. // try { name_value_scanner s (rq.parameters (1024)); params::build_log (s, unknown_mode::fail, unknown_mode::fail); } catch (const cli::exception& e) { throw invalid_request (400, e.what ()); } // If the package build configuration expired (no such configuration, // package, etc), then we log this case with the trace severity and respond // with the 404 HTTP code (not found but may be available in the future). // The thinking is that this may be or may not be a problem with the // controller's setup (expires too fast or the link from some ancient email // is opened). // auto config_expired = [&trace, &lpath, this] (const string& d) { l2 ([&]{trace << "package build configuration for " << lpath << (!tenant.empty () ? '(' + tenant + ')' : "") << " expired: " << d;}); throw invalid_request (404, "package build configuration expired: " + d); }; // Make sure the build configuration still exists. // if (target_conf_map_->find ( build_target_config_id {id.target, id.target_config_name}) == target_conf_map_->end ()) config_expired ("no target configuration"); // Load the package build configuration (if present). // shared_ptr<build> b; { transaction t (build_db_->begin ()); package_build pb; if (!build_db_->query_one<package_build> ( query<package_build>::build::id == id, pb)) config_expired ("no package build"); b = pb.build; if (b->state != build_state::built) config_expired ("state is " + to_string (b->state)); else build_db_->load (*b, b->results_section); t.commit (); } // We have all the data so don't buffer the response content. // // Note that after we started to write the response content we need to be // accurate not throwing any exceptions, that would mess up the response. // ostream& os (rs.content (200, "text/plain;charset=utf-8", false)); auto print_header = [&os, &b, this] () { // Print the build tenant in the multi-tenant mode. // if (!b->tenant.empty ()) os << options_->tenant_name () << ": " << b->tenant << endl << endl; os << "package: " << b->package_name << endl << "version: " << b->package_version << endl << "toolchain: " << b->toolchain_name << '-' << b->toolchain_version << endl << "target: " << b->target << endl << "tgt config: " << b->target_config_name << endl << "pkg config: " << b->package_config_name << endl << "machine: " << b->machine << " (" << b->machine_summary << ")" << endl << "timestamp: "; butl::to_stream (os, b->timestamp, "%Y-%m-%d %H:%M:%S%[.N] %Z", true /* special */, true /* local */); os << endl << endl; }; if (op.empty ()) { print_header (); for (const auto& r: b->results) os << r.operation << ": " << r.status << endl; os << endl; for (const auto& r: b->results) os << r.log; } else { const operation_results& r (b->results); auto i ( find_if (r.begin (), r.end (), [&op] (const operation_result& v) {return v.operation == op;})); if (i == r.end ()) config_expired ("no operation"); print_header (); os << op << ": " << i->status << endl << endl << i->log; } return true; }