diff options
Diffstat (limited to 'libbuild2/utility.cxx')
-rw-r--r-- | libbuild2/utility.cxx | 497 |
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> |