From f41599c8e9435f3dfec60b872c2b4ae31177efdd Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Sat, 10 Oct 2020 17:22:46 +0300 Subject: Add support for test timeouts --- libbuild2/script/builtin-options.cxx | 271 +++++++++++++++ libbuild2/script/builtin-options.hxx | 58 ++++ libbuild2/script/builtin-options.ixx | 9 + libbuild2/script/builtin.cli | 5 + libbuild2/script/parser.cxx | 95 ++++-- libbuild2/script/parser.hxx | 16 +- libbuild2/script/run.cxx | 639 +++++++++++++++++++++++++++++------ libbuild2/script/script.cxx | 26 +- libbuild2/script/script.hxx | 53 ++- libbuild2/script/script.ixx | 35 +- libbuild2/script/timeout.cxx | 26 ++ libbuild2/script/timeout.hxx | 52 +++ libbuild2/script/timeout.ixx | 44 +++ 13 files changed, 1181 insertions(+), 148 deletions(-) create mode 100644 libbuild2/script/timeout.cxx create mode 100644 libbuild2/script/timeout.hxx create mode 100644 libbuild2/script/timeout.ixx (limited to 'libbuild2/script') diff --git a/libbuild2/script/builtin-options.cxx b/libbuild2/script/builtin-options.cxx index abf325f..9b91bd2 100644 --- a/libbuild2/script/builtin-options.cxx +++ b/libbuild2/script/builtin-options.cxx @@ -652,6 +652,277 @@ namespace build2 return r; } + + // timeout_options + // + + timeout_options:: + timeout_options () + : success_ () + { + } + + timeout_options:: + timeout_options (int& argc, + char** argv, + bool erase, + ::build2::script::cli::unknown_mode opt, + ::build2::script::cli::unknown_mode arg) + : success_ () + { + ::build2::script::cli::argv_scanner s (argc, argv, erase); + _parse (s, opt, arg); + } + + timeout_options:: + timeout_options (int start, + int& argc, + char** argv, + bool erase, + ::build2::script::cli::unknown_mode opt, + ::build2::script::cli::unknown_mode arg) + : success_ () + { + ::build2::script::cli::argv_scanner s (start, argc, argv, erase); + _parse (s, opt, arg); + } + + timeout_options:: + timeout_options (int& argc, + char** argv, + int& end, + bool erase, + ::build2::script::cli::unknown_mode opt, + ::build2::script::cli::unknown_mode arg) + : success_ () + { + ::build2::script::cli::argv_scanner s (argc, argv, erase); + _parse (s, opt, arg); + end = s.end (); + } + + timeout_options:: + timeout_options (int start, + int& argc, + char** argv, + int& end, + bool erase, + ::build2::script::cli::unknown_mode opt, + ::build2::script::cli::unknown_mode arg) + : success_ () + { + ::build2::script::cli::argv_scanner s (start, argc, argv, erase); + _parse (s, opt, arg); + end = s.end (); + } + + timeout_options:: + timeout_options (::build2::script::cli::scanner& s, + ::build2::script::cli::unknown_mode opt, + ::build2::script::cli::unknown_mode arg) + : success_ () + { + _parse (s, opt, arg); + } + + typedef + std::map + _cli_timeout_options_map; + + static _cli_timeout_options_map _cli_timeout_options_map_; + + struct _cli_timeout_options_map_init + { + _cli_timeout_options_map_init () + { + _cli_timeout_options_map_["--success"] = + &::build2::script::cli::thunk< timeout_options, bool, &timeout_options::success_ >; + _cli_timeout_options_map_["-s"] = + &::build2::script::cli::thunk< timeout_options, bool, &timeout_options::success_ >; + } + }; + + static _cli_timeout_options_map_init _cli_timeout_options_map_init_; + + bool timeout_options:: + _parse (const char* o, ::build2::script::cli::scanner& s) + { + _cli_timeout_options_map::const_iterator i (_cli_timeout_options_map_.find (o)); + + if (i != _cli_timeout_options_map_.end ()) + { + (*(i->second)) (*this, s); + return true; + } + + return false; + } + + bool timeout_options:: + _parse (::build2::script::cli::scanner& s, + ::build2::script::cli::unknown_mode opt_mode, + ::build2::script::cli::unknown_mode arg_mode) + { + // Can't skip combined flags (--no-combined-flags). + // + assert (opt_mode != ::build2::script::cli::unknown_mode::skip); + + bool r = false; + bool opt = true; + + while (s.more ()) + { + const char* o = s.peek (); + + if (std::strcmp (o, "--") == 0) + { + opt = false; + s.skip (); + r = true; + continue; + } + + if (opt) + { + if (_parse (o, s)) + { + r = true; + continue; + } + + if (std::strncmp (o, "-", 1) == 0 && o[1] != '\0') + { + // Handle combined option values. + // + std::string co; + if (const char* v = std::strchr (o, '=')) + { + co.assign (o, 0, v - o); + ++v; + + int ac (2); + char* av[] = + { + const_cast (co.c_str ()), + const_cast (v) + }; + + ::build2::script::cli::argv_scanner ns (0, ac, av); + + if (_parse (co.c_str (), ns)) + { + // Parsed the option but not its value? + // + if (ns.end () != 2) + throw ::build2::script::cli::invalid_value (co, v); + + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = co.c_str (); + } + } + + // Handle combined flags. + // + char cf[3]; + { + const char* p = o + 1; + for (; *p != '\0'; ++p) + { + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + break; + } + + if (*p == '\0') + { + for (p = o + 1; *p != '\0'; ++p) + { + std::strcpy (cf, "-"); + cf[1] = *p; + cf[2] = '\0'; + + int ac (1); + char* av[] = + { + cf + }; + + ::build2::script::cli::argv_scanner ns (0, ac, av); + + if (!_parse (cf, ns)) + break; + } + + if (*p == '\0') + { + // All handled. + // + s.next (); + r = true; + continue; + } + else + { + // Set the unknown option and fall through. + // + o = cf; + } + } + } + + switch (opt_mode) + { + case ::build2::script::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::build2::script::cli::unknown_mode::stop: + { + break; + } + case ::build2::script::cli::unknown_mode::fail: + { + throw ::build2::script::cli::unknown_option (o); + } + } + + break; + } + } + + switch (arg_mode) + { + case ::build2::script::cli::unknown_mode::skip: + { + s.skip (); + r = true; + continue; + } + case ::build2::script::cli::unknown_mode::stop: + { + break; + } + case ::build2::script::cli::unknown_mode::fail: + { + throw ::build2::script::cli::unknown_argument (o); + } + } + + break; + } + + return r; + } } } diff --git a/libbuild2/script/builtin-options.hxx b/libbuild2/script/builtin-options.hxx index 5a3f153..d0d3c31 100644 --- a/libbuild2/script/builtin-options.hxx +++ b/libbuild2/script/builtin-options.hxx @@ -326,6 +326,64 @@ namespace build2 bool newline_; bool whitespace_; }; + + class timeout_options + { + public: + timeout_options (); + + timeout_options (int& argc, + char** argv, + bool erase = false, + ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail, + ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop); + + timeout_options (int start, + int& argc, + char** argv, + bool erase = false, + ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail, + ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop); + + timeout_options (int& argc, + char** argv, + int& end, + bool erase = false, + ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail, + ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop); + + timeout_options (int start, + int& argc, + char** argv, + int& end, + bool erase = false, + ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail, + ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop); + + timeout_options (::build2::script::cli::scanner&, + ::build2::script::cli::unknown_mode option = ::build2::script::cli::unknown_mode::fail, + ::build2::script::cli::unknown_mode argument = ::build2::script::cli::unknown_mode::stop); + + // Option accessors. + // + const bool& + success () const; + + // Implementation details. + // + protected: + bool + _parse (const char*, ::build2::script::cli::scanner&); + + private: + bool + _parse (::build2::script::cli::scanner&, + ::build2::script::cli::unknown_mode option, + ::build2::script::cli::unknown_mode argument); + + public: + bool success_; + }; } } diff --git a/libbuild2/script/builtin-options.ixx b/libbuild2/script/builtin-options.ixx index dc59f98..5edf31a 100644 --- a/libbuild2/script/builtin-options.ixx +++ b/libbuild2/script/builtin-options.ixx @@ -173,6 +173,15 @@ namespace build2 { return this->whitespace_; } + + // timeout_options + // + + inline const bool& timeout_options:: + success () const + { + return this->success_; + } } } diff --git a/libbuild2/script/builtin.cli b/libbuild2/script/builtin.cli index 68db23e..1a6f523 100644 --- a/libbuild2/script/builtin.cli +++ b/libbuild2/script/builtin.cli @@ -17,5 +17,10 @@ namespace build2 bool --newline|-n; bool --whitespace|-w; }; + + class timeout_options + { + bool --success|-s; + }; } } diff --git a/libbuild2/script/parser.cxx b/libbuild2/script/parser.cxx index d5cff1a..b4184ea 100644 --- a/libbuild2/script/parser.cxx +++ b/libbuild2/script/parser.cxx @@ -1027,7 +1027,9 @@ namespace build2 bool env (false); if (prog && tt == type::word && t.value == "env") { - c.variables = parse_env_builtin (t, tt); + parsed_env r (parse_env_builtin (t, tt)); + c.variables = move (r.variables); + c.timeout = r.timeout; env = true; } @@ -1287,7 +1289,7 @@ namespace build2 return make_pair (move (expr), move (hd)); } - environment_vars parser:: + parser::parsed_env parser:: parse_env_builtin (token& t, token_type& tt) { // enter: 'env' word token @@ -1295,11 +1297,10 @@ namespace build2 next (t, tt); // Skip 'env'. - // Note that the -u option and its value can belong to the different - // name chunks. That's why we parse the env builtin arguments in the - // chunking mode into the argument/location pair list up to the '--' - // separator and parse this list into the variable sets/unsets - // afterwords. + // Note that an option name and value can belong to different name + // chunks. That's why we parse the env builtin arguments in the chunking + // mode into the argument/location pair list up to the '--' separator + // and parse this list into the variable sets/unsets afterwords. // // Align the size with environment_vars (double because of -u // which is two arguments). @@ -1352,13 +1353,13 @@ namespace build2 // Parse the env builtin options and arguments. // - environment_vars r; + parsed_env r; // Note: args is empty in the pre-parse mode. // auto i (as.begin ()), e (as.end ()); - // Parse the variable unsets (from options). + // Parse options (the timeout and variable unsets). // for (; i != e; ++i) { @@ -1372,32 +1373,76 @@ namespace build2 break; } - // Unset the variable, adding its name to the resulting variable list. + // We should probably switch to CLI if we need anything more + // elaborate. Note however, that we will have no precise error + // location then. // - auto unset = [&r, &i, this] (string&& v, const char* o) + + // If this is an option represented with its long or short name, then + // return its value as string and nullopt otherwise. In the former + // case strip the value assignment from the option, if it is in the + // = form, and fail if the option value is empty. + // + auto str = [&i, &e, &o, &l, this] (const char* lo, const char* so) { - if (v.empty ()) + optional r; + + if (o == lo || o == so) + { + if (++i == e) + fail (l) << "env: missing value for option '" << o << "'"; + + r = move (i->first); + } + else + { + size_t n (string::traits_type::length (lo)); + + if (o.compare (0, n, lo) == 0 && o[n] == '=') + { + r = string (o, n + 1); + o.resize (n); + } + } + + if (r && r->empty ()) fail (i->second) << "env: empty value for option '" << o << "'"; - if (v.find ('=') != string::npos) - fail (i->second) << "env: invalid value '" << v << "' for " - << "option '" << o << "': contains '='"; + return r; + }; + + // As above but convert the option value to a number and fail on + // error. + // + auto num = [&i, &o, &str, this] (const char* ln, const char* sn) + { + optional r; + if (optional s = str (ln, sn)) + { + r = parse_number (*s); + + if (!r) + fail (i->second) << "env: invalid value '" << *s + << "' for option '" << o << "'"; + } - r.push_back (move (v)); + return r; }; - // If this is the --unset|-u option then add the variable unset and - // bail out to parsing the variable sets otherwise. + // Parse a known option or bail out to parsing the variable sets. // - if (o == "--unset" || o == "-u") + if (optional v = num ("--timeout", "-t")) { - if (++i == e) - fail (l) << "env: missing value for option '" << o << "'"; + r.timeout = chrono::seconds (*v); + } + else if (optional v = str ("--unset", "-u")) + { + if (v->find ('=') != string::npos) + fail (i->second) << "env: invalid value '" << *v << "' for " + << "option '" << o << "': contains '='"; - unset (move (i->first), o.c_str ()); + r.variables.push_back (move (*v)); } - else if (o.compare (0, 8, "--unset=") == 0) - unset (string (o, 8), "--unset"); else break; } @@ -1421,7 +1466,7 @@ namespace build2 // Add the variable set to the resulting list. // - r.push_back (move (a)); + r.variables.push_back (move (a)); } return r; diff --git a/libbuild2/script/parser.hxx b/libbuild2/script/parser.hxx index da69591..9098b3c 100644 --- a/libbuild2/script/parser.hxx +++ b/libbuild2/script/parser.hxx @@ -130,12 +130,18 @@ namespace build2 pre_parse_line_start (token&, token_type&, lexer_mode); // Parse the env pseudo-builtin arguments up to the program name. Return - // the list of the variables that should be unset ("name") and/or set - // ("name=value") in the command environment and the token/type that - // starts the program name. Note that the variable unsets come first, if - // present. + // the program execution timeout, the list of the variables that should + // be unset ("name") and/or set ("name=value") in the command + // environment, and the token/type that starts the program name. Note + // that the variable unsets come first, if present. // - environment_vars + struct parsed_env + { + optional timeout; + environment_vars variables; + }; + + parsed_env parse_env_builtin (token&, token_type&); // Execute. diff --git a/libbuild2/script/run.cxx b/libbuild2/script/run.cxx index 8c71b32..f13359f 100644 --- a/libbuild2/script/run.cxx +++ b/libbuild2/script/run.cxx @@ -3,6 +3,12 @@ #include +#ifndef _WIN32 +# include // SIG* +#else +# include // DBG_TERMINATE_PROCESS +#endif + #include // streamsize #include @@ -15,6 +21,7 @@ #include #include +#include #include using namespace std; @@ -757,6 +764,39 @@ namespace build2 return false; } + // The timeout pseudo-builtin: set the script timeout. See the script- + // specific set_timeout() implementations for the exact semantics. + // + // timeout [--success|-s] + // + static void + timeout_builtin (environment& env, + const strings& args, + const location& ll) + { + try + { + // Parse arguments. + // + cli::vector_scanner scan (args); + timeout_options ops (scan); + + if (!scan.more ()) + fail (ll) << "missing timeout"; + + string a (scan.next ()); + + if (scan.more ()) + fail (ll) << "unexpected argument '" << scan.next () << "'"; + + env.set_timeout (a, ops.success (), ll); + } + catch (const cli::exception& e) + { + fail (ll) << "timeout: " << e; + } + } + // The exit pseudo-builtin: exit the script successfully, or print the // diagnostics and exit the script unsuccessfully. Always throw exit // exception. @@ -786,6 +826,16 @@ namespace build2 throw exit (false); } + // Return the command program path for diagnostics. + // + static inline path + cmd_path (const command& c) + { + return c.program.initial == nullptr // Not pre-searched? + ? c.program.recall + : path (c.program.recall_string ()); + } + // The set pseudo-builtin: set variable from the stdin input. // // set [-e|--exact] [(-n|--newline)|(-w|--whitespace)] [] @@ -794,16 +844,12 @@ namespace build2 set_builtin (environment& env, const strings& args, auto_fd in, + const optional& dl, + const command& deadline_cmd, const location& ll) { try { - // Do not throw when eofbit is set (end of stream reached), and - // when failbit is set (read operation failed to extract any - // character). - // - ifdstream cin (move (in), ifdstream::badbit); - // Parse arguments. // cli::vector_scanner scan (args); @@ -828,62 +874,136 @@ namespace build2 if (vname.empty ()) fail (ll) << "empty variable name"; - // Read the input. + // Read out the stream content into a string while keeping an eye on + // the deadline. Then parse it according to the split mode. // - cin.peek (); // Sets eofbit for an empty stream. - - names ns; - while (!cin.eof ()) + string s; { - // Read next element that depends on the whitespace mode being - // enabled or not. For the later case it also make sense to strip - // the trailing CRs that can appear while, for example, - // cross-testing Windows target or as a part of msvcrt junk - // production (see above). + ifdstream cin; + + // If the execution deadline is specified, then turn the stream into + // the non-blocking mode reading its content in chunks and with a + // single operation otherwise. If the specified deadline is reached + // while reading the stream, then bail out for the successful + // deadline and fail otherwise. Note that in the former case the + // variable value will be incomplete, but we leave it to the caller + // to handle that. // - string s; - if (ops.whitespace ()) - cin >> s; - else + if (dl) { - getline (cin, s); + fdselect_set fds {in.get ()}; + cin.open (move (in), fdstream_mode::non_blocking); - while (!s.empty () && s.back () == '\r') - s.pop_back (); - } + const timestamp& dlt (dl->value); - // If failbit is set then we read nothing into the string as eof is - // reached. That in particular means that the stream has trailing - // whitespaces (possibly including newlines) if the whitespace mode - // is enabled, or the trailing newline otherwise. If so then - // we append the "blank" to the variable value in the exact mode - // prior to bailing out. - // - if (cin.fail ()) - { - if (ops.exact ()) + for (char buf[4096];; ) { - if (ops.whitespace () || ops.newline ()) - ns.emplace_back (move (s)); // Reuse empty string. - else if (ns.empty ()) - ns.emplace_back ("\n"); - else - ns[0].value += '\n'; - } + timestamp now (system_clock::now ()); - break; - } + if (dlt <= now || ifdselect (fds, dlt - now) == 0) + { + if (!dl->success) + fail (ll) << cmd_path (deadline_cmd) + << " terminated: execution timeout expired"; + else + break; + } + + streamsize n (cin.readsome (buf, sizeof (buf))); + + // Bail out if eos is reached. + // + if (n == 0) + break; - if (ops.whitespace () || ops.newline () || ns.empty ()) - ns.emplace_back (move (s)); + s.append (buf, n); + } + } else { - ns[0].value += '\n'; - ns[0].value += s; + cin.open (move (in)); + s = cin.read_text (); } + + cin.close (); } - cin.close (); + // Parse the stream content into the variable value. + // + names ns; + + if (!s.empty ()) + { + if (ops.whitespace ()) // The whitespace mode. + { + // Note that we collapse multiple consecutive whitespaces. + // + for (size_t p (0); p != string::npos; ) + { + // Skip the whitespaces. + // + const char* sep (" \n\r\t"); + size_t b (s.find_first_not_of (sep, p)); + + if (b != string::npos) // Word beginning. + { + size_t e (s.find_first_of (sep, b)); // Find the word end. + ns.emplace_back (string (s, b, e != string::npos ? e - b : e)); + + p = e; + } + else // Trailings whitespaces. + { + // Append the trailing "blank" after the trailing whitespaces + // in the exact mode. + // + if (ops.exact ()) + ns.emplace_back (empty_string); + + // Bail out since the end of the string is reached. + // + break; + } + } + } + else // The newline or no-split mode. + { + // Note that we don't collapse multiple consecutive newlines. + // + // Note also that we always sanitize CRs so this loop is always + // needed. + // + for (size_t p (0); p != string::npos; ) + { + size_t e (s.find ('\n', p)); + string l (s, p, e != string::npos ? e - p : e); + + // Strip the trailing CRs that can appear while, for example, + // cross-testing Windows target or as a part of msvcrt junk + // production (see above). + // + while (!l.empty () && l.back () == '\r') + l.pop_back (); + + // Append the line. + // + if (!l.empty () || // Non-empty. + e != string::npos || // Empty, non-trailing. + ops.exact ()) // Empty, trailing, in the exact mode. + { + if (ops.newline () || ns.empty ()) + ns.emplace_back (move (l)); + else + { + ns[0].value += '\n'; + ns[0].value += l; + } + } + + p = e != string::npos ? e + 1 : e; + } + } + } env.set_variable (move (vname), move (ns), @@ -915,24 +1035,70 @@ namespace build2 name); } + // Stack-allocated linked list of information about the running pipeline + // processes and builtins. + // + struct pipe_command + { + // We could probably use a union here, but let's keep it simple for now + // (one is NULL). + // + process* proc; + builtin* bltn; + + // True if this command has been terminated. + // + bool terminated = false; + + // Only for diagnostics. + // + const command& cmd; + const location& loc; + + pipe_command* prev; // NULL for the left-most command. + + pipe_command (process& p, + const command& c, + const location& l, + pipe_command* v) + : proc (&p), bltn (nullptr), cmd (c), loc (l), prev (v) {} + + pipe_command (builtin& b, + const command& c, + const location& l, + pipe_command* v) + : proc (nullptr), bltn (&b), cmd (c), loc (l), prev (v) {} + }; + static bool run_pipe (environment& env, command_pipe::const_iterator bc, command_pipe::const_iterator ec, auto_fd ifd, size_t ci, size_t li, const location& ll, - bool diag) + bool diag, + optional dl = nullopt, + const command* dl_cmd = nullptr, // env -t + pipe_command* prev_cmd = nullptr) { + tracer trace ("script::run_pipe"); + if (bc == ec) // End of the pipeline. return true; - // The overall plan is to run the first command in the pipe, reading - // its input from the file descriptor passed (or, for the first - // command, according to stdin redirect specification) and redirecting - // its output to the right-hand part of the pipe recursively. Fail if - // the right-hand part fails. Otherwise check the process exit code, - // match stderr (and stdout for the last command in the pipe) according - // to redirect specification(s) and fail if any of the above fails. + // The overall plan is to run the first command in the pipe, reading its + // input from the file descriptor passed (or, for the first command, + // according to stdin redirect specification) and redirecting its output + // to the right-hand part of the pipe recursively. Fail if the + // right-hand part fails. Otherwise check the process exit code, match + // stderr (and stdout for the last command in the pipe) according to + // redirect specification(s) and fail if any of the above fails. + // + // If the command has a deadline, then terminate the whole pipeline when + // the deadline is reached. This way the pipeline processes get a chance + // to terminate gracefully, which in particular may require to interrupt + // their IO operations, closing their standard streams readers and + // writers. // const command& c (*bc); @@ -996,48 +1162,69 @@ namespace build2 return args; }; - // Prior to opening file descriptors for command input/output - // redirects let's check if the command is the exit builtin. Being a - // builtin syntactically it differs from the regular ones in a number - // of ways. It doesn't communicate with standard streams, so - // redirecting them is meaningless. It may appear only as a single - // command in a pipeline. It doesn't return any value and stops the - // script execution, so checking its exit status is meaningless as - // well. That all means we can short-circuit here calling the builtin - // and bailing out right after that. Checking that the user didn't - // specify any redirects or exit code check sounds like a right thing - // to do. + // Prior to opening file descriptors for command input/output redirects + // let's check if the command is the timeout or exit builtin. Being a + // builtin syntactically they differ from the regular ones in a number + // of ways. They don't communicate with standard streams, so redirecting + // them is meaningless. They may appear only as a single command in a + // pipeline. They don't return any value, so checking their exit status + // is meaningless as well. That all means we can short-circuit here + // calling the builtin and bailing out right after that. Checking that + // the user didn't specify any variables, timeout, redirects, or exit + // code check sounds like a right thing to do. // - if (resolve && program == "exit") + if (resolve && (program == "timeout" || program == "exit")) { // In case the builtin is erroneously pipelined from the other // command, we will close stdin gracefully (reading out the stream - // content), to make sure that the command doesn't print any - // unwanted diagnostics about IO operation failure. + // content), to make sure that the command doesn't print any unwanted + // diagnostics about IO operation failure. // - // Note that dtor will ignore any errors (which is what we want). + // Note though, that doing so would be a bad idea if the deadline is + // specified, since we can block on read and miss the deadline. // - ifdstream is (move (ifd), fdstream_mode::skip); + if (!dl) + { + // Note that dtor will ignore any errors (which is what we want). + // + ifdstream (move (ifd), fdstream_mode::skip); + } if (!first || !last) - fail (ll) << "exit builtin must be the only pipe command"; + fail (ll) << program << " builtin must be the only pipe command"; + + if (!c.variables.empty ()) + fail (ll) << "environment variables cannot be (un)set for " + << program << " builtin"; + + if (c.timeout) + fail (ll) << "timeout cannot be specified for " << program + << " builtin"; if (c.in) - fail (ll) << "exit builtin stdin cannot be redirected"; + fail (ll) << program << " builtin stdin cannot be redirected"; if (c.out) - fail (ll) << "exit builtin stdout cannot be redirected"; + fail (ll) << program << " builtin stdout cannot be redirected"; if (c.err) - fail (ll) << "exit builtin stderr cannot be redirected"; + fail (ll) << program << " builtin stderr cannot be redirected"; if (c.exit) - fail (ll) << "exit builtin exit code cannot be checked"; + fail (ll) << program << " builtin exit code cannot be checked"; if (verb >= 2) print_process (process_args ()); - exit_builtin (c.arguments, ll); // Throws exit exception. + if (program == "timeout") + { + timeout_builtin (env, c.arguments, ll); + return true; + } + else if (program == "exit") + exit_builtin (c.arguments, ll); // Throws exit exception. + else + assert (false); } // Create a unique path for a command standard stream cache file. @@ -1121,6 +1308,9 @@ namespace build2 // process to hang which can be interpreted as a command failure. // @@ Both ways are quite ugly. Is there some better way to do // this? + // @@ Maybe we can create a pipe, write a byte into it, close the + // writing end, and after the process terminates make sure we can + // still read this byte out? // // Fall through. // @@ -1163,6 +1353,24 @@ namespace build2 assert (ifd.get () != -1); + // Calculate the process/builtin execution deadline. Note that we should + // also consider the left-hand side processes deadlines, not to keep + // them waiting for us and allow them to terminate not later than their + // deadlines. Thus, let's also track which command has introduced the + // deadline, so we can report it if the deadline is missed. + // + dl = earlier (dl, env.effective_deadline ()); + + if (c.timeout) + { + deadline d (system_clock::now () + *c.timeout, false /* success */); + if (!dl || d < *dl) + { + dl = d; + dl_cmd = &c; + } + } + // Prior to opening file descriptors for command outputs redirects // let's check if the command is the set builtin. Being a builtin // syntactically it differs from the regular ones in a number of ways. @@ -1190,7 +1398,10 @@ namespace build2 if (verb >= 2) print_process (process_args ()); - set_builtin (env, c.arguments, move (ifd), ll); + set_builtin (env, c.arguments, move (ifd), + dl, dl_cmd != nullptr ? *dl_cmd : c, + ll); + return true; } @@ -1344,10 +1555,119 @@ namespace build2 // assert (ofd.out.get () != -1 && efd.get () != -1); + // Wait for a process/builtin to complete until the deadline is reached + // and return the underlying wait function result (optional). + // + auto timed_wait = [] (auto& p, const timestamp& deadline) + { + timestamp now (system_clock::now ()); + return deadline > now ? p.timed_wait (deadline - now) : p.try_wait (); + }; + + // Terminate the pipeline processes starting from the specified one and + // up to the leftmost one and then kill those which didn't terminate + // after 1 second. + // + // After that wait for the pipeline builtins completion. Since their + // standard streams should no longer be written to or read from by any + // process, that shouldn't take long. If, however, they won't be able to + // complete in 1 second, then some of them have probably stuck while + // communicating with a slow filesystem device or similar, and since we + // currently have no way to terminate asynchronous builtins, we have no + // choice but to abort. + // + // Issue diagnostics and fail if something goes wrong, but still try to + // terminate/kill all the pipe processes. + // + auto term_pipe = [&timed_wait, &trace] (pipe_command* pc) + { + diag_record dr; + + auto prog = [] (pipe_command* c) {return cmd_path (c->cmd);}; + + // Terminate processes gracefully and set the terminate flag for the + // pipe commands. + // + for (pipe_command* c (pc); c != nullptr; c = c->prev) + { + if (process* p = c->proc) + try + { + l5 ([&]{trace (c->loc) << "terminating: " << c->cmd;}); + + p->term (); + } + catch (const process_error& e) + { + // If unable to terminate the process for any reason (the process + // is exiting on Windows, etc) then just ignore this, postponing + // the potential failure till the kill() call. + // + l5 ([&]{trace (c->loc) <<"unable to terminate " << prog (c) + << ": " << e;}); + } + + c->terminated = true; + } + + // Wait a bit for the processes to terminate and kill the remaining + // ones. + // + timestamp dl (system_clock::now () + chrono::seconds (1)); + + for (pipe_command* c (pc); c != nullptr; c = c->prev) + { + if (process* p = c->proc) + try + { + l5 ([&]{trace (c->loc) << "waiting: " << c->cmd;}); + + if (!timed_wait (*p, dl)) + { + l5 ([&]{trace (c->loc) << "killing: " << c->cmd;}); + + p->kill (); + p->wait (); + } + } + catch (const process_error& e) + { + dr << fail (c->loc) << "unable to wait/kill " << prog (c) << ": " + << e; + } + } + + // Wait a bit for the builtins to complete and abort if any remain + // running. + // + dl = system_clock::now () + chrono::seconds (1); + + for (pipe_command* c (pc); c != nullptr; c = c->prev) + { + if (builtin* b = c->bltn) + try + { + l5 ([&]{trace (c->loc) << "waiting: " << c->cmd;}); + + if (!timed_wait (*b, dl)) + { + error (c->loc) << prog (c) << " builtin hanged, aborting"; + terminate (false /* trace */); + } + } + catch (const system_error& e) + { + dr << fail (c->loc) << "unable to wait for " << prog (c) << ": " + << e; + } + } + }; + + // Absent if the process/builtin misses the "unsuccessful" deadline. + // optional exit; - const builtin_info* bi (resolve - ? builtins.find (program) - : nullptr); + + const builtin_info* bi (resolve ? builtins.find (program) : nullptr); bool success; @@ -1394,6 +1714,21 @@ namespace build2 if (cleanup_builtin (program)) cln = cleanup (); + // We also extend the sleep builtin, deactivating the thread before + // going to sleep and waking up before the deadline is reached. + // + // Let's "wrap up" the sleep-related values into the single object to + // rely on "small function object" optimization. + // + struct sleep + { + optional deadline; + bool terminated = false; + + sleep (const optional& d): deadline (d) {} + }; + sleep slp (dl ? dl->value : optional ()); + builtin_callbacks bcs { // create @@ -1555,14 +1890,29 @@ namespace build2 // sleep // - // Deactivate the thread before going to sleep. - // - [&env] (const duration& d) + [&env, &slp] (const duration& d) { + duration t (d); + const optional& dl (slp.deadline); + + if (dl) + { + timestamp now (system_clock::now ()); + + slp.terminated = now + t > *dl; + + if (*dl <= now) + return; + + duration d (*dl - now); + if (t > d) + t = d; + } + // If/when required we could probably support the precise sleep // mode (e.g., via an option). // - env.context.sched.sleep (d); + env.context.sched.sleep (t); } }; @@ -1575,13 +1925,49 @@ namespace build2 *env.work_dir.path, bcs)); + pipe_command pc (b, c, ll, prev_cmd); + + // If the deadline is specified, then make sure we don't miss it + // waiting indefinitely in the builtin destructor on the right-hand + // side of the pipe failure. + // + auto g (make_exception_guard ([&dl, &pc, &term_pipe] () + { + if (dl) + try + { + term_pipe (&pc); + } + catch (const failed&) + { + // We can't do much here. + } + })); + success = run_pipe (env, - nc, - ec, + nc, ec, move (ofd.in), - ci + 1, li, ll, diag); + ci + 1, li, ll, diag, + dl, dl_cmd, + &pc); + + if (!dl) + b.wait (); + else if (!timed_wait (b, dl->value)) + term_pipe (&pc); + + // Note that this also handles ad hoc termination (without the call + // to term_pipe()) by the sleep builtin (see above). + // + if (pc.terminated || slp.terminated) + { + assert (dl); - exit = process_exit (b.wait ()); + if (dl->success) + exit = process_exit (0); + } + else + exit = process_exit (r); } catch (const system_error& e) { @@ -1654,19 +2040,60 @@ namespace build2 env.work_dir.path->string ().c_str (), pe.vars); + // Can't throw. + // ifd.reset (); ofd.out.reset (); efd.reset (); + pipe_command pc (pr, c, ll, prev_cmd); + + // If the deadline is specified, then make sure we don't miss it + // waiting indefinitely in the process destructor on the right-hand + // part of the pipe failure. + // + auto g (make_exception_guard ([&dl, &pc, &term_pipe] () + { + if (dl) + try + { + term_pipe (&pc); + } + catch (const failed&) + { + // We can't do much here. + } + })); + success = run_pipe (env, - nc, - ec, + nc, ec, move (ofd.in), - ci + 1, li, ll, diag); - - pr.wait (); + ci + 1, li, ll, diag, + dl, dl_cmd, + &pc); + + if (!dl) + pr.wait (); + else if (!timed_wait (pr, dl->value)) + term_pipe (&pc); + +#ifndef _WIN32 + if (pc.terminated && + !pr.exit->normal () && + pr.exit->signal () == SIGTERM) +#else + if (pc.terminated && + !pr.exit->normal () && + pr.exit->status == DBG_TERMINATE_PROCESS) +#endif + { + assert (dl); - exit = move (pr.exit); + if (dl->success) + exit = process_exit (0); + } + else + exit = pr.exit; } catch (const process_error& e) { @@ -1679,19 +2106,19 @@ namespace build2 } } - assert (exit); - // If the righ-hand side pipeline failed than the whole pipeline fails, // and no further checks are required. // if (!success) return false; - // Use the program path for diagnostics (print relative, etc). + // Fail if the process is terminated due to reaching the deadline. // - const path& pr (resolve - ? c.program.recall - : path (c.program.recall_string ())); // Can't throw. + if (!exit) + fail (ll) << cmd_path (dl_cmd != nullptr ? *dl_cmd : c) + << " terminated: execution timeout expired"; + + path pr (cmd_path (c)); // If there is no valid exit code available by whatever reason then we // print the proper diagnostics, dump stderr (if cached and not too @@ -1794,7 +2221,7 @@ namespace build2 // pipe that "switches on" the diagnostics potential printing. // command_expr::const_iterator trailing_ands; // Undefined if diag is - // disallowed. + // disallowed. if (diag) { auto i (expr.crbegin ()); @@ -1817,8 +2244,10 @@ namespace build2 // with false. // if (!((or_op && r) || (!or_op && !r))) - r = run_pipe ( - env, p.begin (), p.end (), auto_fd (), ci, li, ll, print); + r = run_pipe (env, + p.begin (), p.end (), + auto_fd (), + ci, li, ll, print); ci += p.size (); } diff --git a/libbuild2/script/script.cxx b/libbuild2/script/script.cxx index 6ee702e..f540687 100644 --- a/libbuild2/script/script.cxx +++ b/libbuild2/script/script.cxx @@ -3,6 +3,7 @@ #include +#include #include #include // strchr() @@ -408,13 +409,20 @@ namespace build2 if ((m & command_to_stream::header) == command_to_stream::header) { - // Print the env builtin arguments, if any environment variable - // (un)sets are present. + // Print the env builtin if any of its options/arguments are present. // - if (!c.variables.empty ()) + if (!c.variables.empty () || c.timeout) { o << "env"; + // Timeout. + // + if (c.timeout) + o << " -t " + << chrono::duration_cast (*c.timeout).count (); + + // Variable unsets/sets. + // auto b (c.variables.begin ()), i (b), e (c.variables.end ()); // Print a variable name or assignment to the stream, quoting it if @@ -456,8 +464,7 @@ namespace build2 // // Print the variable unsets as the -u options until a variable set // is encountered (contains '=') or the end of the variable list is - // reached. In the former case, to avoid a potential ambiguity add - // the '-' separator, if there are any options. + // reached. // // Note that we rely on the fact that unsets come first, which is // guaranteed by parser::parse_env_builtin(). @@ -471,16 +478,15 @@ namespace build2 o << " -u "; print (v, true /* name*/); } else // Variable set. - { - if (i != b) - o << " -"; - break; - } } // Variable sets. // + // Note that we don't add the '-' separator since we always use the + // `-* ` option notation and so there can't be any ambiguity + // with a variable set. + // for (; i != e; ++i) { o << ' '; print (*i, false /* name */); diff --git a/libbuild2/script/script.hxx b/libbuild2/script/script.hxx index 4a62c77..ecd2c2b 100644 --- a/libbuild2/script/script.hxx +++ b/libbuild2/script/script.hxx @@ -308,8 +308,9 @@ namespace build2 // process_path program; - strings arguments; - environment_vars variables; + strings arguments; + environment_vars variables; // From env builtin. + optional timeout; // From env builtin. optional in; optional out; @@ -363,6 +364,37 @@ namespace build2 ostream& operator<< (ostream&, const command_expr&); + struct timeout + { + duration value; + bool success; + + timeout (duration d, bool s): value (d), success (s) {} + }; + + struct deadline + { + timestamp value; + bool success; + + deadline (timestamp t, bool s): value (t), success (s) {} + }; + + // If timestamps/durations are equal, the failure is less than the + // success. + // + bool + operator< (const deadline&, const deadline&); + + bool + operator< (const timeout&, const timeout&); + + optional + to_deadline (const optional&, bool success); + + optional + to_timeout (const optional&, bool success); + // Script execution environment. // class environment @@ -469,6 +501,23 @@ namespace build2 const string& attrs, const location&) = 0; + // Set the script execution timeout from the timeout builtin call. + // + // The builtin argument semantics is script implementation-dependent. If + // success is true then a process missing this deadline should not be + // considered as failed unless it didn't terminate gracefully and had to + // be killed. + // + virtual void + set_timeout (const string& arg, bool success, const location&) = 0; + + // Return the script execution deadline which can potentially rely on + // factors besides the latest timeout builtin call (variables, scoping, + // etc). + // + virtual optional + effective_deadline () = 0; + // Create the temporary directory and set the temp_dir reference target // to its path. Must only be called if temp_dir is empty. // diff --git a/libbuild2/script/script.ixx b/libbuild2/script/script.ixx index 56043b2..37c77ec 100644 --- a/libbuild2/script/script.ixx +++ b/libbuild2/script/script.ixx @@ -25,7 +25,6 @@ namespace build2 inline command_to_stream operator| (command_to_stream x, command_to_stream y) {return x |= y;} - // command // inline ostream& @@ -52,5 +51,39 @@ namespace build2 to_stream (o, e, command_to_stream::all); return o; } + + // deadline + // + inline bool + operator< (const deadline& x, const deadline& y) + { + if (x.value != y.value) + return x.value < y.value; + + return x.success < y.success; + } + + inline optional + to_deadline (const optional& ts, bool success) + { + return ts ? deadline (*ts, success) : optional (); + } + + // timeout + // + inline bool + operator< (const timeout& x, const timeout& y) + { + if (x.value != y.value) + return x.value < y.value; + + return x.success < y.success; + } + + inline optional + to_timeout (const optional& d, bool success) + { + return d ? timeout (*d, success) : optional (); + } } } diff --git a/libbuild2/script/timeout.cxx b/libbuild2/script/timeout.cxx new file mode 100644 index 0000000..a44e1bb --- /dev/null +++ b/libbuild2/script/timeout.cxx @@ -0,0 +1,26 @@ +// file : libbuild2/script/timeout.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include + +#include + +#include + +using namespace std; + +namespace build2 +{ + optional + parse_timeout (const string& s, const char* what, const location& l) + { + if (optional n = parse_number (s)) + { + return *n != 0 + ? chrono::duration_cast (chrono::seconds (*n)) + : optional (); + } + else + fail (l) << "invalid " << what << " '" << s << "'" << endf; + } +} diff --git a/libbuild2/script/timeout.hxx b/libbuild2/script/timeout.hxx new file mode 100644 index 0000000..9991ad6 --- /dev/null +++ b/libbuild2/script/timeout.hxx @@ -0,0 +1,52 @@ +// file : libbuild2/script/timeout.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_SCRIPT_TIMEOUT_HXX +#define LIBBUILD2_SCRIPT_TIMEOUT_HXX + +#include +#include + +namespace build2 +{ + // Parse the specified in seconds timeout returning it if the value is not + // zero and nullopt otherwise. Issue diagnostics and fail if the argument is + // not a valid timeout. + // + optional + parse_timeout (const string&, + const char* what, + const location& = location ()); + + // As above, but return the timepoint which is away from now by the + // specified timeout. + // + optional + parse_deadline (const string&, + const char* what, + const location& = location ()); + + // Return the earlier timeout/deadline of two values, if any is present. + // + // Note that earlier(nullopt, v) and earlier(v, nullopt) return v. + // + template + T + earlier (const T&, const T&); + + template + T + earlier (const optional&, const T&); + + template + T + earlier (const T&, const optional&); + + template + optional + earlier (const optional&, const optional&); +} + +#include + +#endif // LIBBUILD2_SCRIPT_TIMEOUT_HXX diff --git a/libbuild2/script/timeout.ixx b/libbuild2/script/timeout.ixx new file mode 100644 index 0000000..755af17 --- /dev/null +++ b/libbuild2/script/timeout.ixx @@ -0,0 +1,44 @@ +// file : libbuild2/script/timeout.ixx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +namespace build2 +{ + inline optional + parse_deadline (const string& s, const char* what, const location& l) + { + if (optional t = parse_timeout (s, what, l)) + return system_clock::now () + *t; + else + return nullopt; + } + + template + inline T + earlier (const T& x, const T& y) + { + return x < y ? x : y; + } + + template + inline T + earlier (const T& x, const optional& y) + { + return y ? earlier (x, *y) : x; + } + + template + inline T + earlier (const optional& x, const T& y) + { + return earlier (y, x); + } + + template + inline optional + earlier (const optional& x, const optional& y) + { + return x ? earlier (*x, y) : + y ? earlier (*y, x) : + optional (); + } +} -- cgit v1.1