From 561a189f49441a4d211c0217dce8127f2ce7c32e Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Sat, 5 Aug 2017 04:06:53 +0300 Subject: Fix printing progress to non-terminal STDERR --- libbutl/diagnostics.cxx | 28 +++++++++--- libbutl/diagnostics.hxx | 15 ++++--- libbutl/fdstream.cxx | 116 +++++++++++++++++++++++++++++++++++++++++++++++- libbutl/fdstream.hxx | 6 +++ 4 files changed, 151 insertions(+), 14 deletions(-) diff --git a/libbutl/diagnostics.cxx b/libbutl/diagnostics.cxx index 4cfc291..73eb155 100644 --- a/libbutl/diagnostics.cxx +++ b/libbutl/diagnostics.cxx @@ -19,7 +19,8 @@ #include // size_t #include // cerr -#include // stderr_fd() +#include +#include // stderr_fd(), fdterm() using namespace std; @@ -33,6 +34,8 @@ namespace butl static string diag_progress_blank; // Being printed blanks out the line. static size_t diag_progress_size; // Size of the last printed progress. + static optional diag_term; + // Print the progress string to STDERR. Ignore underlying OS errors (this is // a progress bar after all, and throwing from dtors wouldn't be nice). Must // be called with the diag_mutex being aquired. @@ -46,18 +49,31 @@ namespace butl static inline void progress_print (string& s) { - // If the new progress string is shorter than the printed one, then we will - // complement it with the required number of spaces (to overwrite the - // trailing junk) prior to printing, and restore it afterwards. + if (!diag_term) + try + { + diag_term = fdterm (stderr_fd()); + } + catch (const ios::failure&) + { + diag_term = false; + } + + // If we print to a terminal, and the new progress string is shorter than + // the printed one, then we will complement it with the required number of + // spaces (to overwrite the trailing junk) prior to printing, and restore + // it afterwards. // size_t n (s.size ()); - if (n < diag_progress_size) + if (*diag_term && n < diag_progress_size) s.append (diag_progress_size - n, ' '); if (!s.empty ()) { - s += '\r'; // Position the cursor at the beginning of the line. + s += *diag_term + ? '\r' // Position the cursor at the beginning of the line. + : '\n'; try { diff --git a/libbutl/diagnostics.hxx b/libbutl/diagnostics.hxx index 9544945..c1491d6 100644 --- a/libbutl/diagnostics.hxx +++ b/libbutl/diagnostics.hxx @@ -52,13 +52,14 @@ namespace butl // Progress line facility. // // The idea is to keep a progress line at the bottom of the terminal with - // other output scrolling above it. The printing of the progress line is - // integrated into diag_stream_lock and diag_progress_lock. To print or - // update the progress acquire diag_progress_lock and update the - // diag_progress string. To remove the progress line, set this string to - // empty. For better readability start the progress line with a space - // (which is where the cursor will be parked). Should only be used if - // diag_stream points to std::cerr. + // other output scrolling above it. For a non-terminal STDERR the progress + // line is printed as a regular one terminated with the newline character. + // The printing of the progress line is integrated into diag_stream_lock and + // diag_progress_lock. To print or update the progress acquire + // diag_progress_lock and update the diag_progress string. To remove the + // progress line, set this string to empty. For better readability start the + // progress line with a space (which is where the cursor will be parked). + // Should only be used if diag_stream points to std::cerr. // // Note that child processes writing to the same stream may not completely // overwrite the progress line so in this case it makes sense to keep the diff --git a/libbutl/fdstream.cxx b/libbutl/fdstream.cxx index 7681c88..ab78e6a 100644 --- a/libbutl/fdstream.cxx +++ b/libbutl/fdstream.cxx @@ -7,7 +7,7 @@ #ifndef _WIN32 # include // open(), O_*, fcntl() # include // close(), read(), write(), lseek(), dup(), pipe(), - // ssize_t, STD*_FILENO + // isatty(), ssize_t, STD*_FILENO # include // writev(), iovec # include // stat(), S_I* # include // stat, off_t @@ -20,6 +20,8 @@ # include // _fileno(), stdin, stdout, stderr # include // _O_* # include // S_I* + +# include // wcsncmp(), wcsstr() #endif #include // errno, E* @@ -928,6 +930,26 @@ namespace butl return r; } + bool + fdterm (int fd) + { + int r (isatty (fd)); + + if (r == 1) + return true; + + // POSIX specifies ENOTTY errno code for an indication that the descriptor + // doesn't refer to a terminal. However, both Linux and FreeBSD man pages + // also mention EINVAL for this case. + // + assert (r == 0); + + if (errno == ENOTTY || errno == EINVAL) + return false; + + throw_ios_failure (errno); + } + #else auto_fd @@ -1108,5 +1130,97 @@ namespace butl return {auto_fd (pd[0]), auto_fd (pd[1])}; } + + bool + fdterm (int fd) + { + // Resolve file descriptor to HANDLE. Note that the handle is closed either + // when CloseHandle() is called for it or when _close() is called for the + // associated file descriptor. So we don't need to close it. + // + HANDLE h (reinterpret_cast (_get_osfhandle (fd))); + if (h == INVALID_HANDLE_VALUE) + throw_ios_failure (errno); + + // Obtain the descriptor type. + // + DWORD t (GetFileType (h)); + DWORD e; + + if (t == FILE_TYPE_UNKNOWN && (e = GetLastError ()) != 0) + throw_ios_system_failure (e); + + if (t == FILE_TYPE_CHAR) // Terminal. + return true; + + if (t != FILE_TYPE_PIPE) // Pipe still can be a terminal (see below). + return false; + + // MSYS2 terminal file descriptor has the pipe type. To distinguish it + // from other pipes we will try to obtain the associated file name and + // heuristically decide if it is a terminal or not. If we fail to obtain + // the name (for any reason), then we consider the descriptor as not + // referring to a terminal. + // + // Note that the API we need to use is only available starting with + // Windows Vista/Server 2008. To allow programs linked to libbutl to run + // on earlier Windows versions we will link the API in run-time and will + // fallback to the non-terminal descriptor type on failure. We also need + // to partially reproduce the original API types. + // + HMODULE kh (GetModuleHandle ("kernel32.dll")); + if (kh == nullptr) + return false; + + // The original type is FILE_INFO_BY_HANDLE_CLASS enum. + // + enum file_info + { + file_name = 2 + }; + + using func = BOOL (*) (HANDLE, file_info, LPVOID, DWORD); + + func f (reinterpret_cast ( + GetProcAddress (kh, "GetFileInformationByHandleEx"))); + + if (f == nullptr) + return false; + + // The original type is FILE_NAME_INFO structure. + // + struct + { + DWORD length; + wchar_t name[_MAX_PATH + 1]; + } fn; + + // Reserve space for the trailing NULL character. + // + if (!f (h, file_name, &fn, sizeof (fn) - sizeof (wchar_t))) + return false; + + // Add the trailing NULL character. Sounds strange, but the name length is + // expressed in bytes. + // + fn.name[fn.length / sizeof (wchar_t)] = L'\0'; + + // The MSYS2 terminal descriptor file name looks like this: + // + // \msys-dd50a72ab4668b33-pty0-to-master + // + // We will recognize it by the '\msys-' prefix and the presence of the + // '-ptyN' entry. + // + if (wcsncmp (fn.name, L"\\msys-", 6) == 0) + { + const wchar_t* e (wcsstr (fn.name, L"-pty")); + + return e != nullptr && e[4] >= L'0' && e[4] <= L'9' && + (e[5] == L'-' || e[5] == L'\0'); + } + + return false; + } #endif } diff --git a/libbutl/fdstream.hxx b/libbutl/fdstream.hxx index fa62ff4..3bd8e8b 100644 --- a/libbutl/fdstream.hxx +++ b/libbutl/fdstream.hxx @@ -686,6 +686,12 @@ namespace butl // LIBBUTL_SYMEXPORT fdpipe fdopen_pipe (fdopen_mode = fdopen_mode::none); + + // Test whether a file descriptor refers to a terminal. Throw ios::failure on + // the underlying OS error. + // + LIBBUTL_SYMEXPORT bool + fdterm (int); } #include -- cgit v1.1