diff options
Diffstat (limited to 'msvc-filter.cxx')
-rw-r--r-- | msvc-filter.cxx | 416 |
1 files changed, 416 insertions, 0 deletions
diff --git a/msvc-filter.cxx b/msvc-filter.cxx new file mode 100644 index 0000000..0a06202 --- /dev/null +++ b/msvc-filter.cxx @@ -0,0 +1,416 @@ +// file : msvc-filter/msvc-filter.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <sys/time.h> // timeval +#include <sys/select.h> + +#include <ios> // ios_base::failure +#include <string> +#include <cstring> // strchr() +#include <ostream> +#include <iostream> +#include <algorithm> // max() +#include <system_error> +#include <unordered_map> + +#include <butl/path> // path::traits::realize() +#include <butl/utility> // alpha() +#include <butl/process> +#include <butl/fdstream> + +using namespace std; +using namespace butl; + +// Cached mapping of Windows paths to the corresponding POSIX paths. +// +static unordered_map<string, string> path_cache; + +inline static bool +path_separator (char c) +{ + return c == '\\' || c == '/'; +} + +// Replace absolute Windows paths encountered in the line with their POSIX +// representation. Write the result followed by a newline to the stream. Throw +// ostream::failure on IO failures, system_error on others. +// +static void +filter (const char* s, size_t n, ostream& os) +{ + // Strip the terminating 0xOD character if present. + // + if (n > 0 && s[n - 1] == '\r') + --n; + + // Translate absolute Windows paths back to POSIX. The hard part here is to + // determing the end of the path. For example, the error location has the + // 'X:\...\foo(10):' form. However, we cannot assume that '(' ends the path; + // remember 'Program Files (x86)'. + // + // To sidestep this whole mess we are going to use this trick: instead of + // translating the whole path we will only translate its directory part, + // that is, the longest part that still ends with the directory + // separator. We will also still recognize ':' and '\'' as path terminators + // as well as space if it is the first character in the component. + // + // We also pass the path through realpath in order to get the actual path + // rather than Wine's dosdevices links. + // + const char* b (s); + const char* e (s + n); + + for (;;) + { + const char* p (b); + + // Line tail should be at least 3 characters long to contain an absolute + // Windows path. + // + bool no_path (e - b < 3); + + if (!no_path) + { + // An absolute path must begin with [A-Za-z]:[\/] (like C:\). + // + const char* pe (e - 3); + + for (; p != pe; ++p) + { + if (alpha (p[0]) && p[1] == ':' && path_separator (p[2])) + break; + } + + no_path = p == pe; + } + + // Bail out if we reached the end of the line with no path found. + // + if (no_path) + { + os.write (b, e - b); + os.put ('\n'); + break; + } + + os.write (b, p - b); // Print characters that preceed the path. + + b = p; // Beginnig of the path. + const char* pe (p + 3); // End of the last path component. + + for (p = pe; p != e; ++p) + { + char c (*p); + if (c == ':' || c == '\'' || (p == pe && c == ' ')) + break; + + if (path_separator (c)) + pe = p + 1; + } + + // Convert the Windows directory path to POSIX. First check if the mapping + // is already cached. + // + string d (b, pe - b); + auto i (path_cache.find (d)); + + b = pe; + + if (i == path_cache.end ()) + { + const char* args[] = {"winepath", "-u", d.c_str (), nullptr}; + + // Redirect stderr to /dev/null not to mess the output (read more in + // main()). + // + process pr (args, 0, -1, -2); + ifdstream is (pr.in_ofd); + + string pd; + getline (is, pd); + is.close (); + + // It's unknown what can cause winepath to fail. At least not a + // non-existent path. Anyway will consider it fatal. + // + if (!pr.wait ()) + throw system_error (ECHILD, system_category ()); + + try + { + path::traits::realize (pd); + + assert (!pd.empty ()); + + // Restore the trailing slash. + // + if (pd.back () != '/') + pd += '/'; + + } + catch (const invalid_path&) + { + // The path doesn't exist. Let's keep it as provided by winepath. + } + + i = path_cache.emplace (move (d), move (pd)).first; + } + + os.write (i->second.c_str (), i->second.size ()); + } +} + +int +main (int argc, char* argv[]) +try +{ + auto print_usage = [argv]() + { + cerr << "usage: " << argv[0] + << " <diag> <wine-path> <program-path> [arguments]" << endl; + }; + + if (argc < 2) + { + cerr << "error: diag stream file descriptor expected" << endl; + print_usage (); + return 1; + } + + string d (argv[1]); + if (d != "1" && d != "2") + { + cerr << "error: invalid diag stream file descriptor" << endl; + print_usage (); + return 1; + } + + size_t diag (stoi (d)); + + if (argc < 3) + { + cerr << "error: wine path expected" << endl; + print_usage (); + return 1; + } + + if (argc < 4) + { + cerr << "error: program path expected" << endl; + print_usage (); + return 1; + } + + // After arguments are validated we will not be printing error messages on + // failures not to mess the filtered output of the child Windows process. + // Note that in the case of a failure the text from STDERR can be treated by + // the calling process as a build tool diagnostics so our message most + // likelly would be misinterpreted. + // + // The reason we still print error message on the arguments parsing failure + // is that it is likely to be a program misuse rather than runtime error. + // + + // If diag is 1 then both stdout and stderr of the child process are read and + // filtered (achieved by redirecting stdout to stderr). Otherwise the data + // read from child stdout is proxied to own stdout (sounds redundant but + // prevents Windows process from writing to /dev/null directly which is known + // to be super slow). The filtered data is written to the diag file + // descriptor. + // + process pr (const_cast <const char**> (&argv[2]), 0, diag == 1 ? 2 : -1, -1); + + // Stream to filter from. + // + ifdstream isf (pr.in_efd, fdstream_mode::non_blocking); + + // Stream to proxy from. + // + ifdstream isp (diag == 1 ? -1 : pr.in_ofd, fdstream_mode::non_blocking); + + ostream& osf (diag == 1 ? cout : cerr); // Stream to filter to. + ostream* osp (diag == 1 ? nullptr : &cout); // Stream to proxy to. + + // The presense of proxy input and output streams must be correlated. + // + assert (isp.is_open () == (osp != nullptr)); + + // Will be using ostream::write() solely, so badbit is the only one which + // needs to be set. + // + osf.exceptions (ostream::badbit); + + if (osp != nullptr) + osp->exceptions (ostream::badbit); + + const size_t nbuf (8192); + char buf[nbuf + 1]; // Reserve one extra for terminating '\0'. + + bool terminated (false); // Required for Wine bug workaround (see below). + string line; // Incomplete line. + + while (isf.is_open () || isp.is_open ()) + { + // Use timeout to workaround the wineserver bug: sometimes the file + // descriptor that corresponds to the redirected STDERR of a Windows + // process is not closed when that process is terminated. So if STDERR is + // redirected to a pipe (as in our case) the reading peer does not receive + // EOF and hangs forever. We will consider the no-data 100 ms period for an + // exited process to represent such a situation. + // + // Note that it is wineserver who owns the corresponding file descriptor + // not a wine process which runs the Windows program. + // + // Note also that some implementations of select() can modify the timeout + // value so it is essential to reinitialize it prior to every select() + // call. + // + timeval timeout {0, 100000}; + + fd_set rd; + FD_ZERO (&rd); + + if (isf.is_open ()) + FD_SET (isf.fd (), &rd); + + if (isp.is_open ()) + FD_SET (isp.fd (), &rd); + + int r (select (max (isf.fd (), isp.fd ()) + 1, + &rd, + nullptr, + nullptr, + &timeout)); + + if (r == -1) + { + if (errno == EINTR) + continue; + + throw system_error (errno, system_category ()); + } + + // Timeout occured. Apply wineserver bug workaround if required. + // + bool status; + if (r == 0 && pr.try_wait (status)) + { + if (!status) + // Handle the child failure outside the loop. + // + break; + + // Presumably end of the data reached. + // + if (!terminated) + { + // We don't know when the process exited. It possibly wasn't writing + // to the output for quite a long time before terminating a nanosecond + // ago. But let's wait for another timeout to be sure that the process + // has terminated a long (enough) time ago. + // + terminated = true; + continue; + } + + break; + } + + // Proxy the data if requested. + // + if (FD_ISSET (isp.fd (), &rd)) + { + for (;;) + { + // The only leagal way to read from non-blocking ifdstream. + // + streamsize n (isp.readsome (buf, nbuf)); + + if (isp.eof ()) + { + // End of the data to be proxied reached. + // + isp.close (); + break; + } + + if (n == 0) + break; // No data available, try later. + + assert (osp != nullptr); + osp->write (buf, n); + } + } + + // Read & filter. + // + if (FD_ISSET (isf.fd (), &rd)) + { + for (;;) + { + // The only leagal way to read from non-blocking ifdstream. + // + streamsize n (isf.readsome (buf, nbuf)); + + if (isf.eof ()) + { + // End of the data to be filtered reached. + // + isf.close (); + break; + } + + if (n == 0) + break; // No data available, try later. + + // Filter buffer content line by line. Concatenate the line with an + // incomplete one if produced on the previous iteration. Save the last + // line if incomplete (not terminated with '\n'). + // + buf[n] = '\0'; + const char* b (buf); + + for (;;) + { + const char* le (strchr (b, '\n')); + + if (le == nullptr) + { + line += b; + break; + } + + if (!line.empty ()) + { + line.append (b, le - b); + filter (line.c_str (), line.size (), osf); + line.clear (); + } + else + filter (b, le - b, osf); + + b = le + 1; // Skip the newline character. + } + } + } + } + + if (!line.empty ()) + filter (line.c_str (), line.size (), osf); + + isf.close (); + isp.close (); + + return pr.wait () ? 0 : pr.status; +} +catch (const ios_base::failure&) +{ + return 1; +} +// Also handles process_error exception (derived from system_error). +// +catch (const system_error&) +{ + return 1; +} |