From 72450e72e400eb03325ca182f0e118fde8cd1705 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Wed, 26 Jul 2017 13:44:24 +0200 Subject: Add support for printing progress --- libbutl/diagnostics.cxx | 93 +++++++++++++++++++++++++++++++++++++++++++---- libbutl/diagnostics.hxx | 57 ++++++++++++++++++++++++----- libbutl/fdstream.hxx | 2 + tests/buildfile | 2 +- tests/progress/buildfile | 7 ++++ tests/progress/driver.cxx | 62 +++++++++++++++++++++++++++++++ 6 files changed, 206 insertions(+), 17 deletions(-) create mode 100644 tests/progress/buildfile create mode 100644 tests/progress/driver.cxx diff --git a/libbutl/diagnostics.cxx b/libbutl/diagnostics.cxx index e73412b..05661ec 100644 --- a/libbutl/diagnostics.cxx +++ b/libbutl/diagnostics.cxx @@ -4,9 +4,23 @@ #include +#ifndef _WIN32 +# include // write() +#else +# include + +# include //_write() +#endif + +#include // ios::failure #include +#include +#include +#include // size_t #include // cerr +#include // stderr_fd() + using namespace std; namespace butl @@ -15,13 +29,82 @@ namespace butl static mutex diag_mutex; - diag_lock::diag_lock () + string diag_progress; + static string diag_progress_blank; // Being printed blanks out the line. + static size_t diag_progress_size; // Size of the last printed progress. + + // 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. + // + // Note that the output will not interleave with that of independent writers, + // given that the printed strings are not longer than PIPE_BUF for POSIX + // (which is at least 512 bytes on all platforms). + // + // @@ Is there any atomicity guarantees on Windows? + // + 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. + // + size_t n (s.size ()); + + if (n < diag_progress_size) + s.append (diag_progress_size - n, ' '); + + if (!s.empty ()) + { + s += '\r'; // Position the cursor at the beginning of the line. + + try + { +#ifndef _WIN32 + write (stderr_fd(), s.c_str (), s.size ()); +#else + _write (stderr_fd(), s.c_str (), static_cast (s.size ())); +#endif + } + catch (const ios::failure&) {} + + s.resize (n); // Restore the progress string. + diag_progress_size = n; // Save the size of the printed progress string. + } + } + + diag_stream_lock::diag_stream_lock () + { + diag_mutex.lock (); + + // If diagnostics shares the output stream with the progress bar, then + // blank out the line prior to printing the diagnostics, if required. + // + if (diag_stream == &cerr && diag_progress_size != 0) + progress_print (diag_progress_blank); + } + + diag_stream_lock::~diag_stream_lock () + { + // If diagnostics shares output stream with the progress bar, then reprint + // the current progress string, that was overwritten with the diagnostics. + // + if (diag_stream == &cerr && !diag_progress.empty ()) + progress_print (diag_progress); + + diag_mutex.unlock (); + } + + diag_progress_lock::diag_progress_lock () + { + assert (diag_stream == &cerr); diag_mutex.lock (); } - diag_lock::~diag_lock () + diag_progress_lock::~diag_progress_lock () { + progress_print (diag_progress); diag_mutex.unlock (); } @@ -33,11 +116,7 @@ namespace butl if (epilogue_ == nullptr) { os.put ('\n'); - - { - diag_lock l; - *diag_stream << os.str (); - } + diag_stream_lock () << os.str (); // We can endup flushing the result of several writes. The last one may // possibly be incomplete, but that's not a problem as it will also be diff --git a/libbutl/diagnostics.hxx b/libbutl/diagnostics.hxx index c195ba0..9544945 100644 --- a/libbutl/diagnostics.hxx +++ b/libbutl/diagnostics.hxx @@ -23,8 +23,8 @@ namespace butl // Diagnostics destination stream (std::cerr by default). Note that its // modification is not MT-safe. Also note that concurrent writing to the // stream from multiple threads can result in interleaved characters. To - // prevent this an object of diag_lock type (see below) must be created prior - // to write operation. + // prevent this an object of diag_stream_lock type (see below) must be + // created prior to write operation. // LIBBUTL_SYMEXPORT extern std::ostream* diag_stream; @@ -32,12 +32,51 @@ namespace butl // An object of the type must be created prior to writing to diag_stream (see // above). // - struct LIBBUTL_SYMEXPORT diag_lock + struct LIBBUTL_SYMEXPORT diag_stream_lock { - diag_lock (); - ~diag_lock (); + diag_stream_lock (); + ~diag_stream_lock (); + + // Support for one-liners, for example: + // + // diag_stream_lock () << "Hello, World!" << endl; + // + template + std::ostream& + operator<< (const T& x) const + { + return *diag_stream << x; + } + }; + + // 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. + // + // 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 + // line as short as possible. + // + // To restore the progress line being overwritten by an independent writer + // (such as a child process), create and destroy the diag_progress_lock. + // + LIBBUTL_SYMEXPORT extern std::string diag_progress; + + struct LIBBUTL_SYMEXPORT diag_progress_lock + { + diag_progress_lock (); + ~diag_progress_lock (); }; + // + // struct diag_record; template struct diag_prologue; template struct diag_mark; @@ -47,11 +86,11 @@ namespace butl struct LIBBUTL_SYMEXPORT diag_record { template - friend const diag_record& - operator<< (const diag_record& r, const T& x) + const diag_record& + operator<< (const T& x) const { - r.os << x; - return r; + os << x; + return *this; } diag_record () diff --git a/libbutl/fdstream.hxx b/libbutl/fdstream.hxx index 6673fed..fa62ff4 100644 --- a/libbutl/fdstream.hxx +++ b/libbutl/fdstream.hxx @@ -596,6 +596,8 @@ namespace butl fdmode (int, fdstream_mode); // Portable functions for obtaining file descriptors of standard streams. + // Throw ios::failure on the underlying OS error. + // // Note that you normally wouldn't want to close them using fddup() to // convert them to auto_fd, for example: // diff --git a/tests/buildfile b/tests/buildfile index 9a11c82..4c1708c 100644 --- a/tests/buildfile +++ b/tests/buildfile @@ -2,4 +2,4 @@ # copyright : Copyright (c) 2014-2017 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -./: {*/ -build/ -curl/ -openssl/ -sendmail/} +./: {*/ -build/ -curl/ -openssl/ -sendmail/ -progress/} diff --git a/tests/progress/buildfile b/tests/progress/buildfile new file mode 100644 index 0000000..19e85c0 --- /dev/null +++ b/tests/progress/buildfile @@ -0,0 +1,7 @@ +# file : tests/progress/buildfile +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +import libs = libbutl%lib{butl} + +exe{driver}: {hxx cxx}{*} $libs diff --git a/tests/progress/driver.cxx b/tests/progress/driver.cxx new file mode 100644 index 0000000..702319a --- /dev/null +++ b/tests/progress/driver.cxx @@ -0,0 +1,62 @@ +// file : tests/progress/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef _WIN32 +# include // this_thread::sleep_for() +#else +# include +#endif + +#include // size_t +#include + +#include + +using namespace std; +using namespace butl; + +int +main () +{ + auto sleep = [] (size_t ms = 100) + { + // MINGW GCC 4.9 doesn't implement this_thread so use Win32 Sleep(). + // +#ifndef _WIN32 + this_thread::sleep_for (chrono::milliseconds (ms)); +#else + Sleep (static_cast (ms)); +#endif + }; + + for (size_t i (100); i != 0; --i) + { + if (i % 10 == 0) + diag_stream_lock () << "Line " << i / 10 << endl; + + { + diag_progress_lock l; + diag_progress = " " + to_string (i) + "%"; + } + + sleep (); + } + + sleep (1000); + + // Test that the progress line is restored by the diag_stream_lock. + // + diag_stream_lock () << "Printed to diag_stream" << endl; + + sleep (1000); + + // Test that the progress line is restored after printing to cerr. + // + { + cerr << "Printed to std::cerr" << endl; + diag_progress_lock (); + } + + sleep (1000); +} -- cgit v1.1