aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2020-10-14 16:15:41 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2020-11-06 10:47:57 +0300
commit3ee9761b73aff34c7f30ee44b8aac276d413cc21 (patch)
tree8720a9282eb9cb9ea211c8fa0487724615b513c2
parente197ae6fae73719266fd4747f499cd6106fbff4e (diff)
Add builtin::timed_wait(), builtin::try_wait(), and pseudo_builtin()
-rw-r--r--libbutl/builtin.cxx61
-rw-r--r--libbutl/builtin.ixx78
-rw-r--r--libbutl/builtin.mxx81
-rw-r--r--tests/builtin/buildfile3
-rw-r--r--tests/builtin/driver.cxx151
-rw-r--r--tests/builtin/timeout.testscript30
6 files changed, 357 insertions, 47 deletions
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<builtin::async_state> 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 <builtin_impl fn>
@@ -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<mutex> l (state_->mutex);
+
+ if (!state_->finished)
+ state_->condv.wait (l, [this] {return state_->finished;});
+ }
+
+ return result_;
+ }
+
+ template <>
+ optional<uint8_t> builtin::
+ timed_wait (const chrono::milliseconds& tm)
+ {
+ if (state_ != nullptr)
+ {
+ unique_lock<mutex> 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<std::uint8_t> builtin::
+ timed_wait (const std::chrono::milliseconds&);
+
+ template <typename R, typename P>
+ inline optional<std::uint8_t> builtin::
+ timed_wait (const std::chrono::duration<R, P>& d)
+ {
+ using namespace std::chrono;
+ return timed_wait (duration_cast<milliseconds> (d));
+ }
+
+ inline optional<std::uint8_t> builtin::
+ try_wait ()
+ {
+ if (state_ != nullptr)
+ {
+ std::unique_lock<std::mutex> 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 <typename F>
+ inline builtin::async_state::
+ async_state (F f)
+ : thread ([f = std::move (f), this] () mutable noexcept
+ {
+ f ();
+
+ {
+ std::unique_lock<std::mutex> l (this->mutex);
+ finished = true;
+ }
+
+ condv.notify_all ();
+ })
+ {
+ }
+
+ template <typename F>
+ inline builtin
+ pseudo_builtin (std::uint8_t& r, F f)
+ {
+ std::unique_ptr<builtin::async_state> 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 <map>
+#include <mutex>
#include <string>
#include <vector>
#include <thread>
-#include <cstddef> // size_t
-#include <utility> // move()
-#include <cstdint> // uint8_t
+#include <chrono>
+#include <memory> // unique_ptr
+#include <cstddef> // size_t
+#include <utility> // move()
+#include <cstdint> // uint8_t
#include <functional>
+#include <condition_variable>
#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<std::uint8_t>
+ 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 <typename R, typename P>
+ optional<std::uint8_t>
+ timed_wait (const std::chrono::duration<R, P>&);
+
+ ~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 <typename F>
+ explicit
+ async_state (F);
+ };
+
+ builtin (std::uint8_t& r, std::unique_ptr<async_state>&& 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<async_state> 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 <typename F>
+ builtin
+ pseudo_builtin (std::uint8_t&, F);
+
LIBBUTL_SYMEXPORT extern const builtin_map builtins;
}
+
+#include <libbutl/builtin.ixx>
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 <libbutl/win32-utility.hxx>
+#endif
+
#include <cassert>
#ifndef __cpp_lib_modules_ts
#include <string>
#include <vector>
+#include <chrono>
#include <utility> // move()
+#include <cstdint> // uint8_t
#include <ostream>
#include <iostream>
+#ifndef _WIN32
+# include <thread> // 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 <dir>] [-o <opt>] [-c] [-i] <builtin> <builtin-args>
+// Usage: argv[0] [-d <dir>] [-o <opt>] [-c] [-i] [-t <msec>] [-s <sec>]
+// <builtin> <builtin-args>
//
// Execute the builtin and exit with its exit status.
//
-// -d <dir> use as a current working directory
-// -c use callbacks that, in particular, trace calls to stdout
-// -o <opt> additional builtin option recognized by the callback
-// -i read lines from stdin and append them to the builtin arguments
+// -d <dir> use as a current working directory
+// -c use callbacks that, in particular, trace calls to stdout
+// -o <opt> additional builtin option recognized by the callback
+// -i read lines from stdin and append them to the builtin arguments
+// -t <msec> print diag if the builtin didn't complete in <msec> milliseconds
+// -s <sec> sleep <sec> 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<duration> timeout;
+ optional<chrono::seconds> sec;
string name;
vector<string> 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<DWORD> (sec->count () * 1000));
+#endif
+ }
+ };
- if (bi == nullptr)
+ auto wait = [&timeout] (builtin& b)
{
- cerr << "unknown builtin '" << name << "'" << endl;
- return 1;
- }
+ optional<uint8_t> 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/