From 3ee9761b73aff34c7f30ee44b8aac276d413cc21 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Wed, 14 Oct 2020 16:15:41 +0300 Subject: Add builtin::timed_wait(), builtin::try_wait(), and pseudo_builtin() --- libbutl/builtin.cxx | 61 ++++++++++++---- libbutl/builtin.ixx | 78 ++++++++++++++++++++ libbutl/builtin.mxx | 81 ++++++++++++++++----- tests/builtin/buildfile | 3 + tests/builtin/driver.cxx | 151 ++++++++++++++++++++++++++++++++++----- tests/builtin/timeout.testscript | 30 ++++++++ 6 files changed, 357 insertions(+), 47 deletions(-) create mode 100644 libbutl/builtin.ixx create mode 100644 tests/builtin/timeout.testscript diff --git a/libbutl/builtin.cxx b/libbutl/builtin.cxx index c6083b6..7a2c024 100644 --- a/libbutl/builtin.cxx +++ b/libbutl/builtin.cxx @@ -2164,17 +2164,22 @@ namespace butl const dir_path& cwd, const builtin_callbacks& cbs) { - return builtin ( - r, - thread ([fn, &r, &args, - in = move (in), - out = move (out), - err = move (err), - &cwd, - &cbs] () mutable noexcept - { - r = fn (args, move (in), move (out), move (err), cwd, cbs); - })); + unique_ptr s ( + new builtin::async_state ( + [fn, + &r, + &args, + in = move (in), out = move (out), err = move (err), + &cwd, + &cbs] () mutable noexcept + { + r = fn (args, + move (in), move (out), move (err), + cwd, + cbs); + })); + + return builtin (r, move (s)); } template @@ -2200,7 +2205,7 @@ namespace butl const builtin_callbacks& cbs) { r = fn (args, move (in), move (out), move (err), cwd, cbs); - return builtin (r, thread ()); + return builtin (r); } const builtin_map builtins @@ -2222,4 +2227,36 @@ namespace butl {"touch", {&sync_impl<&touch>, 2}}, {"true", {&true_, 0}} }; + + // builtin + // + uint8_t builtin:: + wait () + { + if (state_ != nullptr) + { + unique_lock l (state_->mutex); + + if (!state_->finished) + state_->condv.wait (l, [this] {return state_->finished;}); + } + + return result_; + } + + template <> + optional builtin:: + timed_wait (const chrono::milliseconds& tm) + { + if (state_ != nullptr) + { + unique_lock l (state_->mutex); + + if (!state_->finished && + !state_->condv.wait_for (l, tm, [this] {return state_->finished;})) + return nullopt; + } + + return result_; + } } diff --git a/libbutl/builtin.ixx b/libbutl/builtin.ixx new file mode 100644 index 0000000..0356f8b --- /dev/null +++ b/libbutl/builtin.ixx @@ -0,0 +1,78 @@ +// file : libbutl/builtin.ixx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +namespace butl +{ + // builtin + // + // Implement timed_wait() function templates in terms of their milliseconds + // specialization. + // + template <> + LIBBUTL_SYMEXPORT optional builtin:: + timed_wait (const std::chrono::milliseconds&); + + template + inline optional builtin:: + timed_wait (const std::chrono::duration& d) + { + using namespace std::chrono; + return timed_wait (duration_cast (d)); + } + + inline optional builtin:: + try_wait () + { + if (state_ != nullptr) + { + std::unique_lock l (state_->mutex); + + if (!state_->finished) + return nullopt; + } + + return result_; + } + + // builtin_map + // + inline const builtin_info* builtin_map:: + find (const std::string& n) const + { + auto i (base::find (n)); + return i != end () ? &i->second : nullptr; + } + + // builtin::async_state + // + template + inline builtin::async_state:: + async_state (F f) + : thread ([f = std::move (f), this] () mutable noexcept + { + f (); + + { + std::unique_lock l (this->mutex); + finished = true; + } + + condv.notify_all (); + }) + { + } + + template + inline builtin + pseudo_builtin (std::uint8_t& r, F f) + { + std::unique_ptr s ( + new builtin::async_state ( + [f = std::move (f), &r] () mutable noexcept + { + r = f (); + })); + + return builtin (r, move (s)); + } +} diff --git a/libbutl/builtin.mxx b/libbutl/builtin.mxx index e4dd4f8..a99d6f4 100644 --- a/libbutl/builtin.mxx +++ b/libbutl/builtin.mxx @@ -9,13 +9,17 @@ #ifndef __cpp_lib_modules_ts #include +#include #include #include #include -#include // size_t -#include // move() -#include // uint8_t +#include +#include // unique_ptr +#include // size_t +#include // move() +#include // uint8_t #include +#include #endif // Other includes. @@ -41,26 +45,61 @@ LIBBUTL_MODEXPORT namespace butl { // A process/thread-like object representing a running builtin. // - // For now, instead of allocating the result storage dynamically, we - // expect it to be provided by the caller. + // For now, instead of allocating the result storage dynamically, we expect + // it to be provided by the caller (allocating it dynamically would be + // wasteful for synchronous builtins). // - class builtin + class LIBBUTL_SYMEXPORT builtin { public: + // Wait for the builtin to complete and return its exit code. This + // function can be called multiple times. + // std::uint8_t - wait () {if (t_.joinable ()) t_.join (); return r_;} + wait (); + + // Return the same result as wait() if the builtin has already completed + // and nullopt otherwise. + // + optional + try_wait (); - ~builtin () {wait ();} + // Wait for the builtin to complete for up to the specified time duration. + // Return the same result as wait() if the builtin has completed in this + // timeframe and nullopt otherwise. + // + template + optional + timed_wait (const std::chrono::duration&); + + ~builtin () {if (state_ != nullptr) state_->thread.join ();} public: - builtin (std::uint8_t& r, std::thread&& t = std::thread ()) - : r_ (r), t_ (move (t)) {} + struct async_state + { + bool finished = false; + std::mutex mutex; + std::condition_variable condv; + std::thread thread; + + // Note that we can't use std::function as an argument type to get rid + // of the template since std::function can only be instantiated with a + // copy-constructible function and that's too restrictive for us (won't + // be able to capture auto_fd by value in a lambda, etc). + // + template + explicit + async_state (F); + }; + + builtin (std::uint8_t& r, std::unique_ptr&& s = nullptr) + : result_ (r), state_ (move (s)) {} builtin (builtin&&) = default; private: - std::uint8_t& r_; - std::thread t_; + std::uint8_t& result_; + std::unique_ptr state_; }; // Builtin execution callbacks that can be used for checking/handling the @@ -181,12 +220,20 @@ LIBBUTL_MODEXPORT namespace butl // Return NULL if not a builtin. // const builtin_info* - find (const std::string& n) const - { - auto i (base::find (n)); - return i != end () ? &i->second : nullptr; - } + find (const std::string&) const; }; + // Asynchronously run a function as if it was a builtin. The function must + // have the std::uint8_t() signature and not throw exceptions. + // + // Note that using std::function as an argument type would be too + // restrictive (see above). + // + template + builtin + pseudo_builtin (std::uint8_t&, F); + LIBBUTL_SYMEXPORT extern const builtin_map builtins; } + +#include diff --git a/tests/builtin/buildfile b/tests/builtin/buildfile index 8341847..8d22fe4 100644 --- a/tests/builtin/buildfile +++ b/tests/builtin/buildfile @@ -6,3 +6,6 @@ import libs = libbutl%lib{butl} ./: exe{driver} file{cp-dir/cp-file} exe{driver}: {hxx cxx}{*} $libs testscript{*} + +if ($cxx.target.class != 'windows') + cxx.libs += -lpthread diff --git a/tests/builtin/driver.cxx b/tests/builtin/driver.cxx index 9fb6d6f..843631a 100644 --- a/tests/builtin/driver.cxx +++ b/tests/builtin/driver.cxx @@ -1,14 +1,23 @@ // file : tests/builtin/driver.cxx -*- C++ -*- // license : MIT; see accompanying LICENSE file +#ifdef _WIN32 +# include +#endif + #include #ifndef __cpp_lib_modules_ts #include #include +#include #include // move() +#include // uint8_t #include #include +#ifndef _WIN32 +# include // this_thread::sleep_for() +#endif #endif // Other includes. @@ -40,14 +49,20 @@ operator<< (ostream& os, const path& p) return os << p.representation (); } -// Usage: argv[0] [-d ] [-o ] [-c] [-i] +// Usage: argv[0] [-d ] [-o ] [-c] [-i] [-t ] [-s ] +// // // Execute the builtin and exit with its exit status. // -// -d use as a current working directory -// -c use callbacks that, in particular, trace calls to stdout -// -o additional builtin option recognized by the callback -// -i read lines from stdin and append them to the builtin arguments +// -d use as a current working directory +// -c use callbacks that, in particular, trace calls to stdout +// -o additional builtin option recognized by the callback +// -i read lines from stdin and append them to the builtin arguments +// -t print diag if the builtin didn't complete in milliseconds +// -s sleep seconds prior to running the builtin +// +// Note that the 'roundtrip' builtin name is also recognized and results in +// running the pseudo-builtin that just roundtrips stdin to stdout. // int main (int argc, char* argv[]) @@ -62,12 +77,24 @@ main (int argc, char* argv[]) dir_path cwd; string option; builtin_callbacks callbacks; + optional timeout; + optional sec; string name; vector args; auto flag = [] (bool v) {return v ? "true" : "false";}; + auto num = [] (const string& s) + { + assert (!s.empty ()); + + char* e (nullptr); + uint64_t r (strtoull (s.c_str (), &e, 10)); // Can't throw. + assert (errno != ERANGE && e == s.c_str () + s.size ()); + return r; + }; + // Parse the driver options and arguments. // int i (1); @@ -120,7 +147,23 @@ main (int argc, char* argv[]) ); } else if (a == "-i") + { in = true; + } + else if (a == "-t") + { + ++i; + + assert (i != argc); + timeout = chrono::milliseconds (num (argv[i])); + } + else if (a == "-s") + { + ++i; + + assert (i != argc); + sec = chrono::seconds (num (argv[i])); + } else break; } @@ -142,23 +185,95 @@ main (int argc, char* argv[]) args.push_back (move (s)); } - // Execute the builtin. - // - const builtin_info* bi (builtins.find (name)); + auto sleep = [&sec] () + { + if (sec) + { + // MINGW GCC 4.9 doesn't implement this_thread so use Win32 Sleep(). + // +#ifndef _WIN32 + this_thread::sleep_for (*sec); +#else + Sleep (static_cast (sec->count () * 1000)); +#endif + } + }; - if (bi == nullptr) + auto wait = [&timeout] (builtin& b) { - cerr << "unknown builtin '" << name << "'" << endl; - return 1; - } + optional r; - if (bi->function == nullptr) + if (timeout) + { + r = b.timed_wait (*timeout); + + if (!r) + { + cerr << "timeout expired" << endl; + + b.wait (); + r = 1; + } + } + else + r = b.wait (); + + assert (b.try_wait ()); // While at it, test try_wait(). + + return *r; + }; + + // Execute the builtin. + // + if (name != "roundtrip") { - cerr << "external builtin '" << name << "'" << endl; - return 1; + const builtin_info* bi (builtins.find (name)); + + if (bi == nullptr) + { + cerr << "unknown builtin '" << name << "'" << endl; + return 1; + } + + if (bi->function == nullptr) + { + cerr << "external builtin '" << name << "'" << endl; + return 1; + } + + sleep (); + + uint8_t r; // Storage. + builtin b (bi->function (r, args, nullfd, nullfd, nullfd, cwd, callbacks)); + return wait (b); } + else + { + uint8_t r; // Storage. + + auto run = [&r, &sleep] () + { + // While at it, test that a non-copyable lambda can be used as a + // builtin. + // + auto_fd fd; - uint8_t r; // Storage. - builtin b (bi->function (r, args, nullfd, nullfd, nullfd, cwd, callbacks)); - return b.wait (); + return pseudo_builtin ( + r, + [&sleep, fd = move (fd)] () mutable noexcept + { + fd.reset (); + + sleep (); + + if (cin.peek () != istream::traits_type::eof ()) + cout << cin.rdbuf (); + + return 0; + }); + }; + + builtin b (run ()); + return wait (b); + } } diff --git a/tests/builtin/timeout.testscript b/tests/builtin/timeout.testscript new file mode 100644 index 0000000..de81c6c --- /dev/null +++ b/tests/builtin/timeout.testscript @@ -0,0 +1,30 @@ +# file : tests/builtin/timeout.testscript +# license : MIT; see accompanying LICENSE file + +: async-builtin +: +{ + : expired + : + $* -s 3 'cat' <'test' | $* -t 1 'cat' >=f 2>'timeout expired' != 0 + + : not-expired + : + echo 'test' | $* -t 10000 'cat' >! +} + +: pseudo-builtin +: +{ + : expired + : + $* -s 3 'cat' <'test' | $* -t 1 'roundtrip' >=f 2>'timeout expired' != 0 + + : not-expired + : + echo 'test' | $* -t 10000 'roundtrip' >! +} + +: sync-builtin +: +$* -t 1 'mkdir' d &d/ -- cgit v1.1