aboutsummaryrefslogtreecommitdiff
path: root/build2/test/script/builtin.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'build2/test/script/builtin.cxx')
-rw-r--r--build2/test/script/builtin.cxx293
1 files changed, 271 insertions, 22 deletions
diff --git a/build2/test/script/builtin.cxx b/build2/test/script/builtin.cxx
index 9241447..2dcfde6 100644
--- a/build2/test/script/builtin.cxx
+++ b/build2/test/script/builtin.cxx
@@ -10,12 +10,22 @@
# include <sys/utime.h>
#endif
+#include <thread>
+
#include <butl/path-io> // use default operator<< implementation
-#include <butl/fdstream> // fdopen_mode
+#include <butl/fdstream> // fdopen_mode, fdstream_mode
#include <butl/filesystem> // mkdir_status
#include <build2/test/script/script>
+// Strictly speaking a builtin which reads/writes from/to standard streams
+// must be asynchronous so that the caller can communicate with it through
+// pipes without being blocked on I/O operations. However, as an optimization,
+// we allow builtins that only print diagnostics to STDERR to be synchronous
+// assuming that their output will always fit the pipe buffer. Synchronous
+// builtins must not read from STDIN and write to STDOUT. Later we may relax
+// this rule to allow a "short" output for such builtins.
+//
using namespace std;
using namespace butl;
@@ -25,6 +35,18 @@ namespace build2
{
namespace script
{
+ using builtin_impl = uint8_t (scope&,
+ const strings& args,
+ auto_fd in, auto_fd out, auto_fd err);
+ static future<uint8_t>
+ to_future (uint8_t status)
+ {
+ promise<uint8_t> p;
+ future<uint8_t> f (p.get_future ());
+ p.set_value (status);
+ return f;
+ }
+
// Operation failed, diagnostics has already been issued.
//
struct failed {};
@@ -48,13 +70,141 @@ namespace build2
return p;
}
+ // Builtin commands functions.
+ //
+
+ // cat <file>...
+ //
+ // Read files in sequence and write their contents to STDOUT in the same
+ // sequence. Read from STDIN if no argumements provided or '-' is
+ // specified as a file path. STDIN, STDOUT and file streams are set to
+ // binary mode prior to I/O operations.
+ //
+ // Note that POSIX doesn't specify if after I/O operation failure the
+ // command should proceed with the rest of the arguments. The current
+ // implementation exits immediatelly in such a case.
+ //
+ // @@ Shouldn't we check that we don't print a nonempty regular file to
+ // itself, as that would merely exhaust the output device? POSIX
+ // allows (but not requires) such a check and some implementations do
+ // this. That would require to fstat() file descriptors and complicate
+ // the code a bit. Was able to reproduce on a big file (should be
+ // bigger than the stream buffer size) with the test
+ // 'cat file >>>&file'.
+ //
+ // Note: must be executed asynchronously.
+ //
+ static uint8_t
+ cat (scope& sp,
+ const strings& args,
+ auto_fd in, auto_fd out, auto_fd err) noexcept
+ try
+ {
+ uint8_t r (1);
+ ofdstream cerr (move (err));
+
+ try
+ {
+ ifdstream cin (move (in), fdstream_mode::binary);
+ ofdstream cout (move (out), fdstream_mode::binary);
+
+ // Copy input stream to STDOUT.
+ //
+ auto copy = [&cout] (istream& is)
+ {
+ if (is.peek () != ifdstream::traits_type::eof ())
+ cout << is.rdbuf ();
+
+ is.clear (istream::eofbit); // Sets eofbit.
+ };
+
+ // Path of a file being printed to STDOUT. An empty path represents
+ // STDIN. Used in diagnostics.
+ //
+ path p;
+
+ try
+ {
+ // Print STDIN.
+ //
+ if (args.empty ())
+ copy (cin);
+
+ // Print files.
+ //
+ for (auto i (args.begin ()); i != args.end (); ++i)
+ {
+ if (*i == "-")
+ {
+ if (!cin.eof ())
+ {
+ p.clear ();
+ copy (cin);
+ }
+
+ continue;
+ }
+
+ p = parse_path (*i, sp.wd_path);
+
+ ifdstream is (p, ifdstream::binary);
+ copy (is);
+ is.close ();
+ }
+ }
+ catch (const io_error& e)
+ {
+ cerr << "cat: unable to print ";
+
+ if (p.empty ())
+ cerr << "stdin";
+ else
+ cerr << "'" << p << "'";
+
+ cerr << ": " << e.what () << endl;
+ throw failed ();
+ }
+
+ cin.close ();
+ cout.close ();
+ r = 0;
+ }
+ catch (const invalid_path& e)
+ {
+ cerr << "cat: invalid path '" << e.path << "'" << endl;
+ }
+ // Can be thrown while closing cin, cout or writing to cerr (that's
+ // why need to check its state before writing).
+ //
+ catch (const io_error& e)
+ {
+ if (cerr.good ())
+ cerr << "cat: " << e.what () << endl;
+ }
+ catch (const failed&)
+ {
+ // Diagnostics has already been issued.
+ }
+
+ cerr.close ();
+ return r;
+ }
+ catch (const std::exception&)
+ {
+ return 1;
+ }
+
// echo <string>...
//
- static int
- echo (scope&, const strings& args, auto_fd in, auto_fd out, auto_fd err)
+ // Note: must be executed asynchronously.
+ //
+ static uint8_t
+ echo (scope&,
+ const strings& args,
+ auto_fd in, auto_fd out, auto_fd err) noexcept
try
{
- int r (1);
+ uint8_t r (1);
ofdstream cerr (move (err));
try
@@ -83,6 +233,26 @@ namespace build2
return 1;
}
+ // false
+ //
+ // Return 1. Failure to close the file descriptors is silently ignored.
+ //
+ static future<uint8_t>
+ false_ (scope&, const strings&, auto_fd, auto_fd, auto_fd)
+ {
+ return to_future (1);
+ }
+
+ // true
+ //
+ // Return 0. Failure to close the file descriptors is silently ignored.
+ //
+ static future<uint8_t>
+ true_ (scope&, const strings&, auto_fd, auto_fd, auto_fd)
+ {
+ return to_future (0);
+ }
+
// Create a directory if not exist and its parent directories if
// necessary. Throw system_error on failure. Register created
// directories for cleanup. The directory path must be absolute.
@@ -106,20 +276,19 @@ namespace build2
// Create any missing intermediate pathname components. Each argument
// that names an existing directory must be ignored without error.
//
- static int
+ // Note that POSIX doesn't specify if after a directory creation failure
+ // the command should proceed with the rest of the arguments. The current
+ // implementation exits immediatelly in such a case.
+ //
+ // Note: can be executed synchronously.
+ //
+ static uint8_t
mkdir (scope& sp,
const strings& args,
- auto_fd in, auto_fd out, auto_fd err)
+ auto_fd in, auto_fd out, auto_fd err) noexcept
try
{
- // @@ Should we set a proper verbosity so paths get printed as
- // relative? Can be inconvenient for end-user when build2 runs from
- // a testscript.
- //
- // No, don't think so. If this were an external program, there
- // won't be such functionality.
- //
- int r (1);
+ uint8_t r (1);
ofdstream cerr (move (err));
try
@@ -170,7 +339,6 @@ namespace build2
{
cerr << "mkdir: unable to create directory '" << p << "': "
<< e.what () << endl;
-
throw failed ();
}
}
@@ -181,6 +349,14 @@ namespace build2
{
cerr << "mkdir: invalid path '" << e.path << "'" << endl;
}
+ // Can be thrown while closing in, out or writing to cerr (that's why
+ // need to check its state before writing).
+ //
+ catch (const io_error& e)
+ {
+ if (cerr.good ())
+ cerr << "mkdir: " << e.what () << endl;
+ }
catch (const failed&)
{
// Diagnostics has already been issued.
@@ -194,7 +370,7 @@ namespace build2
return 1;
}
- // touch <path>...
+ // touch <file>...
//
// Change file access and modification times to the current time. Create
// a file if doesn't exist. Fail if a file system entry other than file
@@ -203,13 +379,19 @@ namespace build2
// Note that POSIX doesn't specify the behavior for touching an entry
// other than file.
//
- static int
+ // Also note that POSIX doesn't specify if after a file touch failure the
+ // command should proceed with the rest of the arguments. The current
+ // implementation exits immediatelly in such a case.
+ //
+ // Note: can be executed synchronously.
+ //
+ static uint8_t
touch (scope& sp,
const strings& args,
- auto_fd in, auto_fd out, auto_fd err)
+ auto_fd in, auto_fd out, auto_fd err) noexcept
try
{
- int r (1);
+ uint8_t r (1);
ofdstream cerr (move (err));
try
@@ -283,6 +465,14 @@ namespace build2
{
cerr << "touch: invalid path '" << e.path << "'" << endl;
}
+ // Can be thrown while closing in, out or writing to cerr (that's why
+ // need to check its state before writing).
+ //
+ catch (const io_error& e)
+ {
+ if (cerr.good ())
+ cerr << "touch: " << e.what () << endl;
+ }
catch (const failed&)
{
// Diagnostics has already been issued.
@@ -296,11 +486,70 @@ namespace build2
return 1;
}
+ static void
+ thread_thunk (builtin_impl* fn,
+ scope& sp,
+ const strings& args,
+ auto_fd in, auto_fd out, auto_fd err,
+ promise<uint8_t> p)
+ {
+ // The use of set_value_at_thread_exit() would be more appropriate but
+ // the function is not supported by old versions of g++ (e.g., not in
+ // 4.9). There could also be overhead associated with it.
+ //
+ p.set_value (fn (sp, args, move (in), move (out), move (err)));
+ }
+
+ // Run builtin implementation asynchronously.
+ //
+ static future<uint8_t>
+ async_impl (builtin_impl fn,
+ scope& sp,
+ const strings& args,
+ auto_fd in, auto_fd out, auto_fd err)
+ {
+ promise<uint8_t> p;
+ future<uint8_t> f (p.get_future ());
+
+ thread t (thread_thunk,
+ fn,
+ ref (sp),
+ cref (args),
+ move (in), move (out), move (err),
+ move (p));
+
+ t.detach ();
+ return f;
+ }
+
+ template <builtin_impl fn>
+ static future<uint8_t>
+ async_impl (scope& sp,
+ const strings& args,
+ auto_fd in, auto_fd out, auto_fd err)
+ {
+ return async_impl (fn, sp, args, move (in), move (out), move (err));
+ }
+
+ // Run builtin implementation synchronously.
+ //
+ template <builtin_impl fn>
+ static future<uint8_t>
+ sync_impl (scope& sp,
+ const strings& args,
+ auto_fd in, auto_fd out, auto_fd err)
+ {
+ return to_future (fn (sp, args, move (in), move (out), move (err)));
+ }
+
const builtin_map builtins
{
- {"echo", &echo},
- {"mkdir", &mkdir},
- {"touch", &touch}
+ {"cat", &async_impl<&cat>},
+ {"echo", &async_impl<&echo>},
+ {"false", &false_},
+ {"mkdir", &sync_impl<&mkdir>},
+ {"touch", &sync_impl<&touch>},
+ {"true", &true_}
};
}
}