aboutsummaryrefslogtreecommitdiff
path: root/libbuild2/utility.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'libbuild2/utility.cxx')
-rw-r--r--libbuild2/utility.cxx497
1 files changed, 448 insertions, 49 deletions
diff --git a/libbuild2/utility.cxx b/libbuild2/utility.cxx
index f7f3d41..1135851 100644
--- a/libbuild2/utility.cxx
+++ b/libbuild2/utility.cxx
@@ -3,8 +3,18 @@
#include <libbuild2/utility.hxx>
+#ifndef _WIN32
+# include <signal.h> // signal()
+#else
+# include <libbutl/win32-utility.hxx>
+#endif
+
#include <time.h> // tzset() (POSIX), _tzset() (Windows)
+#ifdef __GLIBCXX__
+# include <locale>
+#endif
+
#include <cerrno> // ENOENT
#include <cstring> // strlen(), str[n]cmp()
#include <iostream> // cerr
@@ -19,7 +29,6 @@
using namespace std;
using namespace butl;
-//
// <libbuild2/types.hxx>
//
namespace build2
@@ -31,12 +40,9 @@ namespace build2
{
return os << run_phase_[static_cast<uint8_t> (p)];
}
-}
-namespace std
-{
ostream&
- operator<< (ostream& os, const ::butl::path& p)
+ operator<< (ostream& os, const path& p)
{
using namespace build2;
@@ -47,7 +53,7 @@ namespace std
}
ostream&
- operator<< (ostream& os, const ::butl::path_name_view& v)
+ operator<< (ostream& os, const path_name_view& v)
{
assert (!v.empty ());
@@ -55,7 +61,7 @@ namespace std
}
ostream&
- operator<< (ostream& os, const ::butl::process_path& p)
+ operator<< (ostream& os, const process_path& p)
{
using namespace build2;
@@ -76,11 +82,55 @@ namespace std
}
}
+// <libbuild2/utility.hxx>
+//
namespace build2
{
- //
- // <libbuild2/utility.hxx>
- //
+ static const char hex_digits[] = "0123456789abcdef";
+
+ string
+ to_string (uint64_t i, int b, size_t w)
+ {
+ // One day we can switch to C++17 std::to_chars().
+ //
+ string r;
+ switch (b)
+ {
+ case 10:
+ {
+ r = to_string (i);
+ if (w > r.size ())
+ r.insert (0, w - r.size (), '0');
+ break;
+ }
+ case 16:
+ {
+ r.reserve (18);
+ r += "0x";
+
+ for (size_t j (64); j != 0; )
+ {
+ j -= 4;
+ size_t d ((i >> j) & 0x0f);
+
+ // Omit leading zeros but watch out for the i==0 corner case.
+ //
+ if (d != 0 || r.size () != 2 || j == 0)
+ r += hex_digits[d];
+ }
+
+ if (w > r.size () - 2)
+ r.insert (2, w - (r.size () - 2), '0');
+
+ break;
+ }
+ default:
+ throw invalid_argument ("unsupported base");
+ }
+
+ return r;
+ }
+
void (*terminate) (bool);
process_path argv0;
@@ -126,13 +176,13 @@ namespace build2
if (p.absolute ())
{
if (p == b)
- return cur ? "." + p.separator_string () : string ();
+ return cur ? '.' + p.separator_string () : string ();
#ifndef _WIN32
if (!home.empty ())
{
if (p == home)
- return "~" + p.separator_string ();
+ return '~' + p.separator_string ();
}
#endif
@@ -210,11 +260,10 @@ namespace build2
process
run_start (uint16_t verbosity,
const process_env& pe,
- const char* args[],
+ const char* const* args,
int in,
int out,
- bool err,
- const dir_path& cwd,
+ int err,
const location& l)
try
{
@@ -228,17 +277,15 @@ namespace build2
args,
in,
out,
- (err ? 2 : 1),
- (!cwd.empty ()
- ? cwd.string ().c_str ()
- : pe.cwd != nullptr ? pe.cwd->string ().c_str () : nullptr),
+ err,
+ pe.cwd != nullptr ? pe.cwd->string ().c_str () : nullptr,
pe.vars);
}
catch (const process_error& e)
{
if (e.child)
{
- // Note: run_finish() expects this exact message.
+ // Note: run_finish_impl() below expects this exact message.
//
cerr << "unable to execute " << args[0] << ": " << e << endl;
@@ -253,7 +300,7 @@ namespace build2
}
bool
- run_wait (const char* args[], process& pr, const location& loc)
+ run_wait (const char* const* args, process& pr, const location& loc)
try
{
return pr.wait ();
@@ -264,55 +311,330 @@ namespace build2
}
bool
- run_finish_impl (const char* args[],
+ run_finish_impl (const char* const* args,
process& pr,
- bool err,
+ bool f,
const string& l,
+ uint16_t v,
+ bool omit_normal,
const location& loc)
- try
{
tracer trace ("run_finish");
- if (pr.wait ())
- return true;
-
- const process_exit& e (*pr.exit);
-
- if (!e.normal ())
- fail (loc) << "process " << args[0] << " " << e;
+ try
+ {
+ if (pr.wait ())
+ return true;
+ }
+ catch (const process_error& e)
+ {
+ fail (loc) << "unable to execute " << args[0] << ": " << e << endf;
+ }
- // Normall but non-zero exit status.
+ // Note: see similar code in diag_buffer::close().
+ //
+ const process_exit& pe (*pr.exit);
+ bool ne (pe.normal ());
+
+ // Even if the user redirected the diagnostics, one error that we want to
+ // let through is the inability to execute the program itself. We cannot
+ // reserve a special exit status to signal this so we will just have to
+ // compare the output. In a sense, we treat this as a special case of
+ // abnormal termination. This particular situation will result in a single
+ // error line printed by run_start() above.
//
- if (err)
+ if (ne && l.compare (0, 18, "unable to execute ") == 0)
+ fail (loc) << l;
+
+ if (omit_normal && ne)
+ {
+ // While we assume diagnostics has already been issued (to stderr), if
+ // that's not the case, it's a real pain to debug. So trace it. (And
+ // if you think that doesn't happen in sensible programs, check GCC
+ // bug #107448).
+ //
+ l4 ([&]{trace << "process " << args[0] << " " << pe;});
+ }
+ else
{
- // While we assuming diagnostics has already been issued (to STDERR), if
- // that's not the case, it's a real pain to debug. So trace it.
+ // It's unclear whether we should print this only if printing the
+ // command line (we could also do things differently for normal/abnormal
+ // exit). Let's print this always and see how it wears. Note that we now
+ // rely on this in, for example, process_finish(), extract_metadata().
+ //
+ // Note: make sure keep the above trace if decide not to print.
//
- l4 ([&]{trace << "process " << args[0] << " " << e;});
+ diag_record dr;
+ dr << error (loc) << "process " << args[0] << " " << pe;
- throw failed ();
+ if (verb >= 1 && verb <= v)
+ {
+ dr << info << "command line: ";
+ print_process (dr, args);
+ }
}
- // Even if the user asked to suppress diagnostiscs, one error that we
- // want to let through is the inability to execute the program itself.
- // We cannot reserve a special exit status to signal this so we will
- // just have to compare the output. This particular situation will
- // result in a single error line printed by run_start() above.
- //
- if (l.compare (0, 18, "unable to execute ") == 0)
- fail (loc) << l;
+ if (f || !ne)
+ throw failed ();
return false;
}
- catch (const process_error& e)
+
+ bool
+ run_finish_impl (diag_buffer& dbuf,
+ const char* const* args,
+ process& pr,
+ bool f,
+ uint16_t v,
+ bool on,
+ const location& loc)
{
- fail (loc) << "unable to execute " << args[0] << ": " << e << endf;
+ try
+ {
+ pr.wait ();
+ }
+ catch (const process_error& e)
+ {
+ fail (loc) << "unable to execute " << args[0] << ": " << e << endf;
+ }
+
+ const process_exit& pe (*pr.exit);
+
+ dbuf.close (args, pe, v, on, loc);
+
+ if (pe)
+ return true;
+
+ if (f || !pe.normal ())
+ throw failed ();
+
+ return false;
}
void
- run_io_error (const char* args[], const io_error& e)
+ run (context& ctx,
+ const process_env& pe,
+ const char* const* args,
+ uint16_t v)
{
- fail << "io error reading " << args[0] << " output: " << e << endf;
+ if (ctx.phase == run_phase::load)
+ {
+ process pr (run_start (pe, args));
+ run_finish (args, pr, v);
+ }
+ else
+ {
+ process pr (run_start (pe,
+ args,
+ 0 /* stdin */,
+ 1 /* stdout */,
+ diag_buffer::pipe (ctx) /* stderr */));
+ diag_buffer dbuf (ctx, args[0], pr);
+ dbuf.read ();
+ run_finish (dbuf, args, pr, v);
+ }
+ }
+
+ bool
+ run (context& ctx,
+ uint16_t verbosity,
+ const process_env& pe,
+ const char* const* args,
+ uint16_t finish_verbosity,
+ const function<bool (string&, bool)>& f,
+ bool tr,
+ bool err,
+ bool ignore_exit,
+ sha256* checksum)
+ {
+ assert (!err || !ignore_exit);
+
+ if (!err || ctx.phase == run_phase::load)
+ {
+ process pr (run_start (verbosity,
+ pe,
+ args,
+ 0 /* stdin */,
+ -1 /* stdout */,
+ err ? 2 : 1 /* stderr */));
+
+ string l; // Last line of output.
+ try
+ {
+ ifdstream is (move (pr.in_ofd), fdstream_mode::skip);
+
+ bool empty (true);
+
+ // Make sure we keep the last line.
+ //
+ for (bool last (is.peek () == ifdstream::traits_type::eof ());
+ !last && getline (is, l); )
+ {
+ last = (is.peek () == ifdstream::traits_type::eof ());
+
+ if (tr)
+ trim (l);
+
+ if (checksum != nullptr)
+ checksum->append (l);
+
+ if (empty)
+ {
+ empty = f (l, last);
+
+ if (!empty && checksum == nullptr)
+ break;
+ }
+ }
+
+ is.close ();
+ }
+ catch (const io_error& e)
+ {
+ if (run_wait (args, pr))
+ fail << "io error reading " << args[0] << " output: " << e << endf;
+
+ // If the child process has failed then assume the io error was
+ // caused by that and let run_finish() deal with it.
+ }
+
+ // Omit normal exit code diagnostics if err is false.
+ //
+ if (!(run_finish_impl (args, pr, err, l, finish_verbosity, !err) ||
+ ignore_exit))
+ return false;
+ }
+ else
+ {
+ // We have to use the non-blocking setup since we have to read from stdout
+ // and stderr simultaneously.
+ //
+ process pr (run_start (verbosity,
+ pe,
+ args,
+ 0 /* stdin */,
+ -1 /* stdout */,
+ diag_buffer::pipe (ctx) /* stderr */));
+
+ // Note that while we read both streams until eof in the normal
+ // circumstances, we cannot use fdstream_mode::skip for the exception
+ // case on both of them: we may end up being blocked trying to read one
+ // stream while the process may be blocked writing to the other. So in
+ // case of an exception we only skip the diagnostics and close stdout
+ // hard. The latter should happen first so the order of the dbuf/is
+ // variables is important.
+ //
+ diag_buffer dbuf (ctx, args[0], pr, (fdstream_mode::non_blocking |
+ fdstream_mode::skip));
+ try
+ {
+ ifdstream is (move (pr.in_ofd),
+ fdstream_mode::non_blocking,
+ ifdstream::badbit);
+
+ bool empty (true);
+
+ // Read until we reach EOF on all streams.
+ //
+ // Note that if dbuf is not opened, then we automatically get an
+ // inactive nullfd entry.
+ //
+ fdselect_set fds {is.fd (), dbuf.is.fd ()};
+ fdselect_state& ist (fds[0]);
+ fdselect_state& dst (fds[1]);
+
+ // To detect the last line we are going keep the previous line and
+ // only call the function once we've read the next.
+ //
+ optional<string> pl;
+
+ for (string l; ist.fd != nullfd || dst.fd != nullfd; )
+ {
+ if (ist.fd != nullfd && getline_non_blocking (is, l))
+ {
+ if (eof (is))
+ {
+ if (pl && empty)
+ f (*pl, true /* last */);
+
+ ist.fd = nullfd;
+ }
+ else
+ {
+ if (checksum != nullptr || empty)
+ {
+ if (tr)
+ trim (l);
+
+ if (checksum != nullptr)
+ checksum->append (l);
+
+ if (empty)
+ {
+ if (pl)
+ {
+ if ((empty = f (*pl, false /* last */)))
+ swap (l, *pl);
+
+ // Note that we cannot bail out like in the other version
+ // since we don't have the skip mode on is. Plus, we might
+ // still have the diagnostics.
+ }
+ else
+ pl = move (l);
+ }
+ }
+
+ l.clear ();
+ }
+
+ continue;
+ }
+
+ ifdselect (fds);
+
+ if (dst.ready)
+ {
+ if (!dbuf.read ())
+ dst.fd = nullfd;
+ }
+ }
+
+ is.close ();
+ }
+ catch (const io_error& e)
+ {
+ if (run_wait (args, pr))
+ {
+ // Note that we will drop the diagnostics in this case since reading
+ // it could have been the cause of this error.
+ //
+ fail << "io error reading " << args[0] << " output: " << e << endf;
+ }
+
+ // If the child process has failed then assume the io error was caused
+ // by that and let run_finish() deal with it.
+ }
+
+ run_finish_impl (dbuf, args, pr, true /* fail */, finish_verbosity);
+ }
+
+ return true;
+ }
+
+ cstrings
+ process_args (const char* program, const strings& args)
+ {
+ cstrings r;
+ r.reserve (args.size () + 2);
+
+ r.push_back (program);
+
+ for (const string& a: args)
+ r.push_back (a.c_str ());
+
+ r.push_back (nullptr);
+ return r;
}
fdpipe
@@ -556,8 +878,73 @@ namespace build2
}
void
+ init_process ()
+ {
+ // This is a little hack to make out baseutils for Windows work when
+ // called with absolute path. In a nutshell, MSYS2's exec*p() doesn't
+ // search in the parent's executable directory, only in PATH. And since we
+ // are running without a shell (that would read /etc/profile which sets
+ // PATH to some sensible values), we are only getting Win32 PATH values.
+ // And MSYS2 /bin is not one of them. So what we are going to do is add
+ // /bin at the end of PATH (which will be passed as is by the MSYS2
+ // machinery). This will make MSYS2 search in /bin (where our baseutils
+ // live). And for everyone else this should be harmless since it is not a
+ // valid Win32 path.
+ //
+#ifdef _WIN32
+ {
+ string mp;
+ if (optional<string> p = getenv ("PATH"))
+ {
+ mp = move (*p);
+ mp += ';';
+ }
+ mp += "/bin";
+
+ setenv ("PATH", mp);
+ }
+#endif
+
+ // On POSIX ignore SIGPIPE which is signaled to a pipe-writing process if
+ // the pipe reading end is closed. Note that by default this signal
+ // terminates a process. Also note that there is no way to disable this
+ // behavior on a file descriptor basis or for the write() function call.
+ //
+#ifndef _WIN32
+ if (signal (SIGPIPE, SIG_IGN) == SIG_ERR)
+ fail << "unable to ignore broken pipe (SIGPIPE) signal: "
+ << system_error (errno, generic_category ()); // Sanitize.
+#endif
+
+ // Initialize time conversion data that is used by localtime_r().
+ //
+#ifndef _WIN32
+ tzset ();
+#else
+ _tzset ();
+#endif
+
+ // A data race happens in the libstdc++ (as of GCC 7.2) implementation of
+ // the ctype<char>::narrow() function (bug #77704). The issue is easily
+ // triggered by the testscript runner that indirectly (via regex) uses
+ // ctype<char> facet of the global locale (and can potentially be
+ // triggered by other locale-aware code). We work around this by
+ // pre-initializing the global locale facet internal cache.
+ //
+#ifdef __GLIBCXX__
+ {
+ const ctype<char>& ct (use_facet<ctype<char>> (locale ()));
+
+ for (size_t i (0); i != 256; ++i)
+ ct.narrow (static_cast<char> (i), '\0');
+ }
+#endif
+ }
+
+ void
init (void (*t) (bool),
const char* a0,
+ bool ss,
optional<bool> mc,
optional<path> cs,
optional<path> cg)
@@ -592,6 +979,18 @@ namespace build2
}
script::regex::init ();
+
+ if (!ss)
+ {
+#ifdef _WIN32
+ // On Windows disable displaying error reporting dialog box for the
+ // current and child processes unless we are in the stop mode. Failed
+ // that we may have multiple dialog boxes popping up.
+ //
+ SetErrorMode (SetErrorMode (0) | // Returns the current mode.
+ SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
+#endif
+ }
}
optional<uint64_t>