/// Executes an external binary and replaces the current process. /// /// This differs from process::exec() in that this function reports errors /// caused by the exec(2) system call to let the caller decide how to handle /// them. /// /// This function must not use any of the logging features so that the output /// of the subprocess is not "polluted" by our own messages. /// /// This function must also not affect the global state of the current process /// as otherwise we would not be able to use vfork(). Only state stored in the /// stack can be touched. /// /// \param program The binary to execute. /// \param args The arguments to pass to the binary, without the program name. /// /// \throw system_error If the exec(2) call fails. void process::exec_unsafe(const fs::path& program, const args_vector& args) { PRE(args.size() < MAX_ARGS); int original_errno = 0; try { const char* argv[MAX_ARGS + 1]; argv[0] = program.c_str(); for (args_vector::size_type i = 0; i < args.size(); i++) argv[1 + i] = args[i].c_str(); argv[1 + args.size()] = NULL; const int ret = ::execv(program.c_str(), (char* const*)(unsigned long)(const void*)argv); original_errno = errno; INV(ret == -1); std::cerr << "Failed to execute " << program << ": " << std::strerror(original_errno) << "\n"; } catch (const std::runtime_error& error) { std::cerr << "Failed to execute " << program << ": " << error.what() << "\n"; std::abort(); } catch (...) { std::cerr << "Failed to execute " << program << "; got unexpected " "exception during exec\n"; std::abort(); } // We must do this here to prevent our exception from being caught by the // generic handlers above. INV(original_errno != 0); throw system_error("Failed to execute " + program.str(), original_errno); }
/// Initializes an empty database. /// /// \param db The database to initialize. /// /// \return The metadata record written into the new database. /// /// \throw store::error If there is a problem initializing the database. store::metadata store::detail::initialize(sqlite::database& db) { PRE(empty_database(db)); const fs::path schema = schema_file(); std::ifstream input(schema.c_str()); if (!input) throw error(F("Cannot open database schema '%s'") % schema); LI(F("Populating new database with schema from %s") % schema); const std::string schema_string = utils::read_stream(input); try { db.exec(schema_string); const metadata metadata = metadata::fetch_latest(db); LI(F("New metadata entry %s") % metadata.timestamp()); if (metadata.schema_version() != detail::current_schema_version) { UNREACHABLE_MSG(F("current_schema_version is out of sync with " "%s") % schema); } return metadata; } catch (const store::integrity_error& e) { // Could be raised by metadata::fetch_latest. UNREACHABLE_MSG("Inconsistent code while creating a database"); } catch (const sqlite::error& e) { throw error(F("Failed to initialize database: %s") % e.what()); } }
/// Creates a directory. /// /// \param dir The path to the directory to create. /// \param mode The permissions for the new directory. /// /// \throw system_error If the call to mkdir(2) fails. void fs::mkdir(const fs::path& dir, const int mode) { if (::mkdir(dir.c_str(), static_cast< mode_t >(mode)) == -1) { const int original_errno = errno; throw fs::system_error(F("Failed to create directory %s") % dir, original_errno); } }
/// Backs up a database for schema migration purposes. /// /// \todo We should probably use the SQLite backup API instead of doing a raw /// file copy. We issue our backup call with the database already open, but /// because it is quiescent, it's OK to do so. /// /// \param source Location of the database to be backed up. /// \param old_version Version of the database's CURRENT schema, used to /// determine the name of the backup file. /// /// \throw error If there is a problem during the backup. void store::detail::backup_database(const fs::path& source, const int old_version) { const fs::path target(F("%s.v%s.backup") % source.str() % old_version); LI(F("Backing up database %s to %s") % source % target); std::ifstream input(source.c_str()); if (!input) throw error(F("Cannot open database file %s") % source); std::ofstream output(target.c_str()); if (!output) throw error(F("Cannot create database backup file %s") % target); char buffer[1024]; while (input.good()) { input.read(buffer, sizeof(buffer)); if (input.good() || input.eof()) output.write(buffer, input.gcount()); } if (!input.good() && !input.eof()) throw error(F("Error while reading input file %s") % source); }
/// Creates a temporary file. /// /// The temporary file is created using mkstemp(3) using the provided template. /// This should be most likely used in conjunction with fs::auto_file. /// /// \param path_template The template for the temporary path, which is a /// basename that is created within the TMPDIR. Must contain the XXXXXX /// pattern, which is atomically replaced by a random unique string. /// /// \return The generated path for the temporary directory. /// /// \throw fs::system_error If the call to mkstemp(3) fails. fs::path fs::mkstemp(const std::string& path_template) { PRE(path_template.find("XXXXXX") != std::string::npos); const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp")); const fs::path full_template = tmpdir / path_template; utils::auto_array< char > buf(new char[full_template.str().length() + 1]); std::strcpy(buf.get(), full_template.c_str()); if (::mkstemp(buf.get()) == -1) { const int original_errno = errno; throw fs::system_error(F("Cannot create temporary file using template " "%s") % full_template, original_errno); } return fs::path(buf.get()); }
/// Computes the test cases list of a test program. /// /// \param status The termination status of the subprocess used to execute /// the exec_test() method or none if the test timed out. /// \param stdout_path Path to the file containing the stdout of the test. /// \param stderr_path Path to the file containing the stderr of the test. /// /// \return A list of test cases. /// /// \throw error If there is a problem parsing the test case list. model::test_cases_map engine::atf_interface::parse_list(const optional< process::status >& status, const fs::path& stdout_path, const fs::path& stderr_path) const { const std::string stderr_contents = utils::read_file(stderr_path); if (!stderr_contents.empty()) LW("Test case list wrote to stderr: " + stderr_contents); if (!status) throw engine::error("Test case list timed out"); if (status.get().exited()) { const int exitstatus = status.get().exitstatus(); if (exitstatus == EXIT_SUCCESS) { // Nothing to do; fall through. } else if (exitstatus == exit_eacces) { throw engine::error("Permission denied to run test program"); } else if (exitstatus == exit_enoent) { throw engine::error("Cannot find test program"); } else if (exitstatus == exit_enoexec) { throw engine::error("Invalid test program format"); } else { throw engine::error("Test program did not exit cleanly"); } } else { throw engine::error("Test program received signal"); } std::ifstream input(stdout_path.c_str()); if (!input) throw engine::load_error(stdout_path, "Cannot open file for read"); const model::test_cases_map test_cases = parse_atf_list(input); if (!stderr_contents.empty()) throw engine::error("Test case list wrote to stderr"); return test_cases; }
/// Recursively removes a directory. /// /// This operation simulates a "rm -r". No effort is made to forcibly delete /// files and no attention is paid to mount points. /// /// \param directory The directory to remove. /// /// \throw fs::error If there is a problem removing any directory or file. void fs::rm_r(const fs::path& directory) { DIR* dirp = ::opendir(directory.c_str()); if (dirp == NULL) { const int original_errno = errno; throw fs::system_error(F("Failed to open directory %s") % directory.str(), original_errno); } try { ::dirent* dp; while ((dp = ::readdir(dirp)) != NULL) { const std::string name = dp->d_name; if (name == "." || name == "..") continue; const fs::path entry = directory / dp->d_name; const struct ::stat sb = safe_stat(entry); if (S_ISDIR(sb.st_mode)) { LD(F("Descending into %s") % entry); fs::rm_r(entry); } else { LD(F("Removing file %s") % entry); fs::unlink(entry); } } } catch (...) { ::closedir(dirp); throw; } ::closedir(dirp); LD(F("Removing empty directory %s") % directory); fs::rmdir(directory); }
/// Checks if a file exists. /// /// Be aware that this is racy in the same way as access(2) is. /// /// \param path The file to check the existance of. /// /// \return True if the file exists; false otherwise. bool fs::exists(const fs::path& path) { return ::access(path.c_str(), F_OK) == 0; }
/// Waits for completion of any forked test case. /// /// Note that if the terminated test case has a cleanup routine, this function /// is the one in charge of spawning the cleanup routine asynchronously. /// /// \return The result of the execution of a subprocess. This is a dynamically /// allocated object because the scheduler can spawn subprocesses of various /// types and, at wait time, we don't know upfront what we are going to get. scheduler::result_handle_ptr scheduler::scheduler_handle::wait_any(void) { _pimpl->generic.check_interrupt(); executor::exit_handle handle = _pimpl->generic.wait_any(); const exec_data_map::iterator iter = _pimpl->all_exec_data.find( handle.original_pid()); exec_data_ptr& data = (*iter).second; utils::dump_stacktrace_if_available(data->test_program->absolute_path(), _pimpl->generic, handle); optional< model::test_result > result; try { test_exec_data* test_data = &dynamic_cast< test_exec_data& >( *data.get()); test_data->exit_handle = handle; const model::test_case& test_case = test_data->test_program->find( test_data->test_case_name); result = test_case.fake_result(); if (!result && handle.status() && handle.status().get().exited() && handle.status().get().exitstatus() == exit_skipped) { // If the test's process terminated with our magic "exit_skipped" // status, there are two cases to handle. The first is the case // where the "skipped cookie" exists, in which case we never got to // actually invoke the test program; if that's the case, handle it // here. The second case is where the test case actually decided to // exit with the "exit_skipped" status; in that case, just fall back // to the regular status handling. const fs::path skipped_cookie_path = handle.control_directory() / skipped_cookie; std::ifstream input(skipped_cookie_path.c_str()); if (input) { result = model::test_result(model::test_result_skipped, utils::read_stream(input)); input.close(); // If we determined that the test needs to be skipped, we do not // want to run the cleanup routine because doing so could result // in errors. However, we still want to run the cleanup routine // if the test's body reports a skip (because actions could have // already been taken). test_data->needs_cleanup = false; } } if (!result) { result = test_data->interface->compute_result( handle.status(), handle.control_directory(), handle.stdout_file(), handle.stderr_file()); } INV(result); if (!result.get().good()) { append_files_listing(handle.work_directory(), handle.stderr_file()); } if (test_data->needs_cleanup) { INV(test_case.get_metadata().has_cleanup()); // The test body has completed and we have processed it. If there // is a cleanup routine, trigger it now and wait for any other test // completion. The caller never knows about cleanup routines. _pimpl->spawn_cleanup(test_data->test_program, test_data->test_case_name, test_data->user_config, handle, result.get()); test_data->needs_cleanup = false; // TODO(jmmv): Chaining this call is ugly. We'd be better off by // looping over terminated processes until we got a result suitable // for user consumption. For the time being this is good enough and // not a problem because the call chain won't get big: the majority // of test cases do not have cleanup routines. return wait_any(); } } catch (const std::bad_cast& e) { const cleanup_exec_data* cleanup_data = &dynamic_cast< const cleanup_exec_data& >(*data.get()); // Handle the completion of cleanup subprocesses internally: the caller // is not aware that these exist so, when we return, we must return the // data for the original test that triggered this routine. For example, // because the caller wants to see the exact same exec_handle that was // returned by spawn_test. const model::test_result& body_result = cleanup_data->body_result; if (body_result.good()) { if (!handle.status()) { result = model::test_result(model::test_result_broken, "Test case cleanup timed out"); } else { if (!handle.status().get().exited() || handle.status().get().exitstatus() != EXIT_SUCCESS) { result = model::test_result( model::test_result_broken, "Test case cleanup did not terminate successfully"); } else { result = body_result; } } } else { result = body_result; } handle = cleanup_data->body_exit_handle; } INV(result); std::shared_ptr< result_handle::bimpl > result_handle_bimpl( new result_handle::bimpl(handle, _pimpl->all_exec_data)); std::shared_ptr< test_result_handle::impl > test_result_handle_impl( new test_result_handle::impl( data->test_program, data->test_case_name, result.get())); return result_handle_ptr(new test_result_handle(result_handle_bimpl, test_result_handle_impl)); }