aboutsummaryrefslogtreecommitdiff
path: root/libbuild2/script
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2020-10-10 17:22:46 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2020-11-06 19:32:09 +0300
commitf41599c8e9435f3dfec60b872c2b4ae31177efdd (patch)
tree088f8d9bf906e4a2ed734e034699163c9ccc7306 /libbuild2/script
parentac76a4fd2afff48a0d5db84592babe5cabef3a2c (diff)
Add support for test timeouts
Diffstat (limited to 'libbuild2/script')
-rw-r--r--libbuild2/script/builtin-options.cxx271
-rw-r--r--libbuild2/script/builtin-options.hxx58
-rw-r--r--libbuild2/script/builtin-options.ixx9
-rw-r--r--libbuild2/script/builtin.cli5
-rw-r--r--libbuild2/script/parser.cxx95
-rw-r--r--libbuild2/script/parser.hxx16
-rw-r--r--libbuild2/script/run.cxx639
-rw-r--r--libbuild2/script/script.cxx26
-rw-r--r--libbuild2/script/script.hxx53
-rw-r--r--libbuild2/script/script.ixx35
-rw-r--r--libbuild2/script/timeout.cxx26
-rw-r--r--libbuild2/script/timeout.hxx52
-rw-r--r--libbuild2/script/timeout.ixx44
13 files changed, 1181 insertions, 148 deletions
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<std::string, void (*) (timeout_options&, ::build2::script::cli::scanner&)>
+ _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<char*> (co.c_str ()),
+ const_cast<char*> (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 <var>
// 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
+ // <name>=<value> 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<string> 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<uint64_t> r;
+ if (optional<string> 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<uint64_t> v = num ("--timeout", "-t"))
{
- if (++i == e)
- fail (l) << "env: missing value for option '" << o << "'";
+ r.timeout = chrono::seconds (*v);
+ }
+ else if (optional<string> 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<duration> 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 <libbuild2/script/run.hxx>
+#ifndef _WIN32
+# include <signal.h> // SIG*
+#else
+# include <libbutl/win32-utility.hxx> // DBG_TERMINATE_PROCESS
+#endif
+
#include <ios> // streamsize
#include <libbutl/regex.mxx>
@@ -15,6 +21,7 @@
#include <libbuild2/diagnostics.hxx>
#include <libbuild2/script/regex.hxx>
+#include <libbuild2/script/timeout.hxx>
#include <libbuild2/script/builtin-options.hxx>
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] <timeout>
+ //
+ 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)] [<attr>] <var>
@@ -794,16 +844,12 @@ namespace build2
set_builtin (environment& env,
const strings& args,
auto_fd in,
+ const optional<deadline>& 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<deadline> dl = nullopt,
+ const command* dl_cmd = nullptr, // env -t <cmd>
+ 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<something>).
+ //
+ 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<process_exit> 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<timestamp> deadline;
+ bool terminated = false;
+
+ sleep (const optional<timestamp>& d): deadline (d) {}
+ };
+ sleep slp (dl ? dl->value : optional<timestamp> ());
+
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<timestamp>& 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 <libbuild2/script/script.hxx>
+#include <chrono>
#include <sstream>
#include <cstring> // 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<chrono::seconds> (*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
+ // `-* <value>` 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<duration> timeout; // From env builtin.
optional<redirect> in;
optional<redirect> 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<deadline>
+ to_deadline (const optional<timestamp>&, bool success);
+
+ optional<timeout>
+ to_timeout (const optional<duration>&, 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<deadline>
+ 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<deadline>
+ to_deadline (const optional<timestamp>& ts, bool success)
+ {
+ return ts ? deadline (*ts, success) : optional<deadline> ();
+ }
+
+ // 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<timeout>
+ to_timeout (const optional<duration>& d, bool success)
+ {
+ return d ? timeout (*d, success) : optional<timeout> ();
+ }
}
}
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 <libbuild2/script/timeout.hxx>
+
+#include <chrono>
+
+#include <libbuild2/diagnostics.hxx>
+
+using namespace std;
+
+namespace build2
+{
+ optional<duration>
+ parse_timeout (const string& s, const char* what, const location& l)
+ {
+ if (optional<uint64_t> n = parse_number (s))
+ {
+ return *n != 0
+ ? chrono::duration_cast<duration> (chrono::seconds (*n))
+ : optional<duration> ();
+ }
+ 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 <libbuild2/types.hxx>
+#include <libbuild2/utility.hxx>
+
+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<duration>
+ 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<timestamp>
+ 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 <typename T>
+ T
+ earlier (const T&, const T&);
+
+ template <typename T>
+ T
+ earlier (const optional<T>&, const T&);
+
+ template <typename T>
+ T
+ earlier (const T&, const optional<T>&);
+
+ template <typename T>
+ optional<T>
+ earlier (const optional<T>&, const optional<T>&);
+}
+
+#include <libbuild2/script/timeout.ixx>
+
+#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<timestamp>
+ parse_deadline (const string& s, const char* what, const location& l)
+ {
+ if (optional<duration> t = parse_timeout (s, what, l))
+ return system_clock::now () + *t;
+ else
+ return nullopt;
+ }
+
+ template <typename T>
+ inline T
+ earlier (const T& x, const T& y)
+ {
+ return x < y ? x : y;
+ }
+
+ template <typename T>
+ inline T
+ earlier (const T& x, const optional<T>& y)
+ {
+ return y ? earlier (x, *y) : x;
+ }
+
+ template <typename T>
+ inline T
+ earlier (const optional<T>& x, const T& y)
+ {
+ return earlier (y, x);
+ }
+
+ template <typename T>
+ inline optional<T>
+ earlier (const optional<T>& x, const optional<T>& y)
+ {
+ return x ? earlier (*x, y) :
+ y ? earlier (*y, x) :
+ optional<T> ();
+ }
+}