SourceXtractorPlusPlus  0.8
Please provide a description of the project.
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
ProgressNCurses.cpp
Go to the documentation of this file.
1 
17 #include "SEMain/ProgressNCurses.h"
18 #include "ModelFitting/utils.h"
19 
20 #include <poll.h>
21 #include <semaphore.h>
22 #include <ncurses.h>
23 #include <fcntl.h>
24 #include <readline/readline.h>
25 #include <csignal>
26 #include <chrono>
27 #include <iostream>
28 #include <iomanip>
29 #include <mutex>
30 #include <boost/algorithm/string/trim.hpp>
31 #include <boost/thread.hpp>
32 
33 
34 namespace SourceXtractor {
35 
36 // Signal handlers
37 static struct sigaction sigterm_action, sigstop_action, sigcont_action, sigwich_action;
39 
40 // Used for sending signals to the UI thread
41 static int signal_fds[2];
42 
43 // Used by the UI thread to notify that is is done
44 static struct ncurses_done {
45  sem_t m_semaphore;
47  sem_init(&m_semaphore, 0, 1);
48  }
49 } ncurses_done;
50 
51 // Forward declaration of signal handlers
52 static void handleTerminatingSignal(int s);
53 static void handleStopSignal(int s);
54 static void handleContinuationSignal(int s);
55 static void handleResizeSignal(int);
56 
57 
67 static int interceptFileDescriptor(int old_fd, int *backup_fd) {
68  int pipe_fds[2];
69 
70  *backup_fd = dup(old_fd);
71  if (*backup_fd < 0) {
73  }
74 
75  if (pipe(pipe_fds) < 0) {
77  }
78 
79  int flags = fcntl(pipe_fds[0], F_GETFL, 0);
80  if (fcntl(pipe_fds[0], F_SETFL, flags | O_NONBLOCK) < 0) {
82  }
83 
84  if (dup2(pipe_fds[1], old_fd) < 0) {
86  }
87  close(pipe_fds[1]);
88 
89  return pipe_fds[0];
90 }
91 
100 static void override_rl_display(void) {
101 }
102 
106 class Screen : public boost::noncopyable {
107 public:
108 
116  Screen(FILE *outfd, FILE *infd) {
117  if (pipe(signal_fds) < 0) {
119  }
120 
121  m_old_redisplay = rl_redisplay_function;
122  rl_redisplay_function = override_rl_display;
123 
124  // Tell readline to leave the terminal be
125  rl_catch_signals = 0;
126  rl_deprep_term_function = NULL;
127  rl_prep_term_function = NULL;
128 
129  // It seems like the readline in MacOSX is not the "real" readline, but a compatibility
130  // layer which misses some things, like the following:
131 #ifndef __APPLE__
132  rl_catch_sigwinch = 0;
133 #endif
134 
135  // Setup signal handlers
136  ::memset(&sigterm_action, 0, sizeof(sigterm_action));
137  ::memset(&sigstop_action, 0, sizeof(sigstop_action));
138  ::memset(&sigcont_action, 0, sizeof(sigcont_action));
139  ::memset(&sigwich_action, 0, sizeof(sigwich_action));
140 
141  // Termination
142  sigterm_action.sa_handler = &handleTerminatingSignal;
143  ::sigaction(SIGINT, &sigterm_action, &prev_signal[SIGINT]);
144  ::sigaction(SIGTERM, &sigterm_action, &prev_signal[SIGTERM]);
145  ::sigaction(SIGABRT, &sigterm_action, &prev_signal[SIGABRT]);
146  ::sigaction(SIGSEGV, &sigterm_action, &prev_signal[SIGSEGV]);
147  ::sigaction(SIGHUP, &sigterm_action, &prev_signal[SIGHUP]);
148 
149  // bg
150  sigstop_action.sa_handler = &handleStopSignal;
151  ::sigaction(SIGTSTP, &sigstop_action, &prev_signal[SIGTSTP]);
152 
153  // fg
154  sigcont_action.sa_handler = &handleContinuationSignal;
155  ::sigaction(SIGCONT, &sigcont_action, &prev_signal[SIGCONT]);
156 
157  // Resizing
158  // Some versions of ncurses handle this by themselves, some other do not, so
159  // we do it ourselves in anycase
160  sigwich_action.sa_handler = &handleResizeSignal;
161  ::sigaction(SIGWINCH, &sigwich_action, &prev_signal[SIGWINCH]);
162 
163  // Enter ncurses
164  initscr();
165  m_screen = newterm(nullptr, outfd, infd);
166  set_term(m_screen);
167 
168  // Hide cursor
169  curs_set(0);
170 
171  // Get input without echo, but leave Ctrl+<Key> to the terminal
172  cbreak();
173  keypad(stdscr, TRUE);
174  noecho();
175 
176  // Setup colors
177  use_default_colors();
178  start_color();
179  }
180 
184  virtual ~Screen() {
185  // Exit ncurses
186  endwin();
187  delscreen(m_screen);
188  rl_redisplay_function = m_old_redisplay;
189  // Restore signal handlers
190  ::sigaction(SIGINT, &prev_signal[SIGINT], nullptr);
191  ::sigaction(SIGTERM, &prev_signal[SIGTERM], nullptr);
192  ::sigaction(SIGABRT, &prev_signal[SIGABRT], nullptr);
193  ::sigaction(SIGSEGV, &prev_signal[SIGSEGV], nullptr);
194  ::sigaction(SIGHUP, &prev_signal[SIGHUP], nullptr);
195  ::sigaction(SIGCONT, &prev_signal[SIGCONT], nullptr);
196  ::sigaction(SIGWINCH, &prev_signal[SIGWINCH], nullptr);
197  close(signal_fds[0]);
198  close(signal_fds[1]);
199  }
200 
204  short initColor(short fg, short bg) {
205  init_pair(m_color_idx, fg, bg);
206  return m_color_idx++;
207  }
208 
209 private:
210  short m_color_idx = 1;
211  SCREEN *m_screen;
212  rl_voidfunc_t* m_old_redisplay;
213 };
214 
231 static void handleTerminatingSignal(int s) {
232  // Restore handler (so if we get stuck somewhere, and second
233  // signal occurs, like a SIGTERM, the process is killed for good)
234  sigaction(s, &prev_signal[s], nullptr);
235 
236  // Notify
237  if (write(signal_fds[1], &s, sizeof(s)) == sizeof(s)) {
238  close(signal_fds[1]);
239  // Wait for UI thread to be done
240 #if _POSIX_C_SOURCE >= 200112L
241  timespec timeout;
242  clock_gettime(CLOCK_REALTIME, &timeout);
243  timeout.tv_sec += 5;
244  sem_timedwait(&ncurses_done.m_semaphore, &timeout);
245 #else
246  // MacOSX does not have timedwait
247  int timeout = 5;
248  while(timeout > 0 && sem_trywait(&ncurses_done.m_semaphore) < 0) {
249  sleep(1);
250  --timeout;
251  }
252 #endif
253  }
254 
255  // Call the previous handler
256  raise(s);
257 }
258 
262 static void handleStopSignal(int s) {
263  // Restore handler
264  sigaction(s, &prev_signal[s], nullptr);
265 
266  // Exit ncurses
267  endwin();
268 
269  // Trigger the previous handler
270  raise(s);
271 }
272 
276 static void handleContinuationSignal(int) {
277  // Restore handlers
278  sigaction(SIGCONT, &sigcont_action, nullptr);
279  sigaction(SIGTSTP, &sigstop_action, nullptr);
280 }
281 
285 static void handleResizeSignal(int s) {
286  if (write(signal_fds[1], &s, sizeof(s)) < 0) {
287  // Just ignore
288  }
289 }
290 
294 class LogWidget {
295 private:
296  WINDOW *m_pad, *m_scroll;
297  // Screen coordinates!
300  // Number of total lines being written so far
302  // Last line being *displayed*
304  // Colors
306 
307  static const int BUFFER_INCREASE_STEP_SIZE = 10, BUFFER_MAX_SIZE = 16384;
308 
309 public:
310 
326  LogWidget(int display_height, int display_width, int display_y, int display_x, short bar_color, short ind_color)
327  : m_pad(newpad(BUFFER_INCREASE_STEP_SIZE, display_width)),
328  m_scroll(newpad(display_height, 1)),
329  m_display_height(display_height), m_display_width(display_width), m_display_y(display_y), m_display_x(display_x),
330  m_written_lines(0), m_active_line(0), m_scroll_bar_color(bar_color), m_scroll_ind_color(ind_color) {
331  scrollok(m_pad, TRUE);
332  }
333 
337  virtual ~LogWidget() {
338  delwin(m_pad);
339  delwin(m_scroll);
340  }
341 
345  void write(const char *data, ssize_t nchars) {
346  while (nchars > 0) {
347  if (*data == '\n') {
348  // If the current line is the last one, follow the log
350  ++m_active_line;
351  }
353  // Increase buffer if we ran out of lines on the pad
354  if (getmaxy(m_pad) <= m_written_lines) {
356  }
357  }
358  waddch(m_pad, *data);
359  ++data, --nchars;
360  }
361  drawLog();
362  drawScroll();
363  }
364 
368  void resize(int display_height, int display_width) {
369  m_display_height = display_height;
370  m_display_width = display_width;
371 
372  // Resize to make place for the new width only if it is bigger.
373  // Note that the pad height depends on the number of written lines, not displayed size!
374  if (display_width > getmaxx(m_pad)) {
375  wresize(m_pad, getmaxy(m_pad), display_width);
376  }
377  wresize(m_scroll, display_height, 1);
378  drawLog();
379  drawScroll();
380  }
381 
386  void scrollText(int d) {
387  m_active_line += d;
388  if (m_active_line > getcury(m_pad) + 1) {
389  m_active_line = getcury(m_pad) + 1;
390  }
393  }
396  }
397  drawLog();
398  drawScroll();
399  }
400 
404  void handleKeyPress(int key) {
405  switch (key) {
406  case KEY_DOWN:
407  scrollText(1);
408  break;
409  case KEY_UP:
410  scrollText(-1);
411  break;
412  case KEY_NPAGE:
413  scrollText(LINES);
414  break;
415  case KEY_PPAGE:
416  scrollText(-LINES);
417  break;
418  }
419  }
420 
425  // Scan line by line
426  std::vector<std::string> term_lines;
427  for (int i = 0; i < m_written_lines; ++i) {
428  // Note: We do not want the '\0' to be part of the final string, so we use the string constructor to prune those
429  std::vector<char> buffer(m_display_width + 1, '\0');
430  mvwinnstr(m_pad, i, 0, buffer.data(), m_display_width - 2);
431  term_lines.emplace_back(buffer.data());
432  boost::algorithm::trim(term_lines.back());
433  }
434  // Prune trailing empty lines
435  while (!term_lines.empty() && term_lines.back().empty()) {
436  term_lines.pop_back();
437  }
438  return term_lines;
439  }
440 
441 private:
445  void drawScroll() const {
446  werase(m_scroll);
447 
448  int max_selectable_line = m_written_lines;
449  int min_selectable_line = std::min(m_written_lines, m_display_height);
450  int displayed_line_offset = m_active_line - min_selectable_line;
451  float p = std::max(0.f, std::min(1.f, displayed_line_offset / float(max_selectable_line - min_selectable_line)));
452 
453  int scroll_marker_pos = p * (m_display_height - 1);
454  for (int i = 0; i < m_display_height; ++i) {
455  if (i == scroll_marker_pos)
456  waddch(m_scroll, ACS_CKBOARD | COLOR_PAIR(m_scroll_ind_color));
457  else
458  waddch(m_scroll, '|' | COLOR_PAIR(m_scroll_bar_color));
459  }
460  pnoutrefresh(m_scroll,
461  0, 0,
463  m_display_y + m_display_height - 1, m_display_x + m_display_width - 1
464  );
465  }
466 
470  void drawLog() const {
471  int pad_y = std::max(m_active_line - m_display_height, 0);
472  pnoutrefresh(m_pad,
473  pad_y, 0, // Pad coordinates
474  m_display_y, m_display_x, // Start screen coordinates
475  m_display_y + m_display_height - 1, m_display_x + m_display_width - 2 // End screen coordinates
476  );
477  }
478 };
479 
483 class ProgressWidget : public boost::noncopyable {
484 public:
500  ProgressWidget(int height, int width, int y, int x, short done_color, short progress_color)
501  : m_window(newwin(height, width, y, x)), m_started(std::chrono::steady_clock::now()),
502  m_done_color(done_color), m_progress_color(progress_color) {
503  }
504 
509  delwin(m_window);
510  }
511 
519  void move(int y, int x) {
520  mvwin(m_window, y, x);
521  wnoutrefresh(m_window);
522  }
523 
531  void resize(int height, int width) {
532  wresize(m_window, height, width);
533  wnoutrefresh(m_window);
534  }
535 
539  unsigned getHeight() const {
540  return getmaxy(m_window);
541  }
542 
546  void update(const std::list<ProgressInfo>& info) {
547  // Precalculate layout, so labels are aligned
548  size_t value_position = sizeof("Elapsed");
549 
550  for (auto& entry: info) {
551  if (entry.m_label.size() > value_position) {
552  value_position = entry.m_label.size();
553  }
554  }
555  value_position++; // Plus space
556 
557  // Width of the bar is the with of the windows - a space - two brackets []
558  size_t bar_width = getmaxx(m_window) - 2 - value_position;
559 
560  // Elapsed
561  auto now = std::chrono::steady_clock::now();
562  auto elapsed = now - m_started;
563 
564  // Restore position to the beginning
565  werase(m_window);
566 
567  // Now, print the actual progress
568  int line = 0;
569  for (auto& entry : info) {
570  drawProgressLine(value_position, bar_width, line, entry.m_label, entry.m_total, entry.m_done);
571  ++line;
572  }
573 
574  // Elapsed time
575  drawElapsed(value_position, elapsed, line);
576 
577  // Flush
578  wnoutrefresh(m_window);
579  }
580 
581 private:
585  void drawElapsed(size_t value_position, const std::chrono::steady_clock::duration& elapsed, int line) const {
588  auto s = std::chrono::duration_cast<std::chrono::seconds>(elapsed - h - m);
589  std::ostringstream elapsed_str;
590  elapsed_str.fill('0');
591  elapsed_str << std::setw(2) << h.count() << ':' << std::setw(2) << m.count() << ':' << std::setw(2) << s.count();
592 
593  wattron(m_window, A_BOLD);
594  mvwaddstr(m_window, line, 0, "Elapsed");
595  wattroff(m_window, A_BOLD);
596  mvwaddstr(
597  m_window,
598  line, value_position + 1,
599  elapsed_str.str().c_str()
600  );
601  }
602 
606  void drawProgressLine(int value_position, int bar_width, int line, const std::string& label,
607  int total, int done) const {
608  // Label
609  wattron(m_window, A_BOLD);
610  mvwaddstr(m_window, line, 0, label.c_str());
611  wattroff(m_window, A_BOLD);
612 
613  // Total number is unknown
614  if (total <= 0) {
615  mvwprintw(m_window, line, value_position + 1, "%d", done);
616  return;
617  }
618 
619  // Otherwise, report progress as a bar
620  float ratio = done / static_cast<float>(total);
621  // This can happens sometimes, as a measurement could be notified before the deblending, for instance
622  if (ratio > 1)
623  ratio = 1.;
624 
625  // Build the report string
627  bar << done << " / " << total << " (" << std::fixed << std::setprecision(2) << ratio * 100. << "%)";
628 
629  // Attach as many spaces as needed to fill the screen width, minus brackets
630  bar << std::string(bar_width - bar.str().size(), ' ');
631 
632  // Print label
633  wattron(m_window, A_BOLD);
634  mvwaddstr(m_window, line, 0, label.c_str());
635  wattroff(m_window, A_BOLD);
636  mvwaddch(m_window, line, value_position, '[');
637 
638  // Completed
639  auto bar_content = bar.str();
640  int completed = bar_content.size() * ratio;
641 
642  wattron(m_window, COLOR_PAIR(m_done_color));
643  waddstr(m_window, bar_content.substr(0, completed).c_str());
644  wattroff(m_window, COLOR_PAIR(m_done_color));
645 
646  // Rest
647  wattron(m_window, COLOR_PAIR(m_progress_color));
648  waddstr(m_window, bar_content.substr(completed).c_str());
649  wattroff(m_window, COLOR_PAIR(2));
650 
651  // Closing bracket
652  waddch(m_window, ']');
653  }
654 
655  WINDOW *m_window;
656  std::chrono::steady_clock::time_point m_started;
658 };
659 
671 private:
675 
676  // stderr intercept
679  // stdout intercept
681 
682  // Used to recover log into the standard output
684 
685  std::atomic_bool m_trigger_resize;
686 
690  void uiThread() {
691  sem_wait(&ncurses_done.m_semaphore);
692  // SIGTERM, SIGINT and SIGHUP should not be handled by this thread, or we will not be able to properly
693  // exit ncurses.
694  // Hopefully there should be no SIGABRT or SIGSEGV here. If there were, we will exit but we will not be able
695  // to restore the terminal state. Having an abort or a segmentation fault is a bug anyway.
696  sigset_t set;
697  sigaddset(&set, SIGTERM);
698  sigaddset(&set, SIGINT);
699  sigaddset(&set, SIGHUP);
700  pthread_sigmask(SIG_BLOCK, &set, nullptr);
701  // Enter ncurses
702  ncursesMode();
703  // Recover file descriptors
704  dup2(m_stderr_original, STDERR_FILENO);
705  dup2(m_stdout_original, STDOUT_FILENO);
706  // Dump recovered text
707  for (const auto& line : m_log_text) {
708  std::cerr << line << std::endl;
709  }
710  sem_post(&ncurses_done.m_semaphore);
711  }
712 
716  void ncursesMode() {
717  Screen screen(m_stderr, stdin);
718 
719  // Log area
720  LogWidget logWidget(
721  LINES - 1, COLS, 0, 0,
722  screen.initColor(COLOR_WHITE, COLOR_BLACK), screen.initColor(COLOR_WHITE, COLOR_WHITE)
723  );
724 
725  // Progress widget
726  ProgressWidget progressWidget(
727  1, COLS, LINES - 1, 0,
728  screen.initColor(COLOR_WHITE, COLOR_GREEN), screen.initColor(COLOR_WHITE, COLOR_BLACK)
729  );
730 
731  // File descriptors to watch for
732  struct pollfd poll_fds[] = {
733  {m_stderr_pipe, POLLIN, 0},
734  {m_stdout_pipe, POLLIN, 0},
735  {STDIN_FILENO, POLLIN, 0},
736  {signal_fds[0], POLLIN, 0}
737  };
738 
739  // Event loop
740  char buf[64];
741  ssize_t nbytes;
742  bool exit_loop = false;
743 
744  do {
745  // There has been a signal
746  if (poll_fds[3].revents & POLLIN) {
747  int signal_no;
748  if (read(signal_fds[0], &signal_no, sizeof(signal_no)) > 0 && signal_no == SIGWINCH) {
749  m_trigger_resize = true;
750  endwin();
751  refresh();
752  clear();
753  }
754  else {
755  logWidget.write(buf, snprintf(buf, sizeof(buf), "Caught signal %s\n", strsignal(signal_no)));
756  exit_loop = true;
757  }
758  }
759 
760  // Resize widgets if needed
761  if (m_trigger_resize) {
763  progressWidget.move(LINES - m_progress_info.size() - 1, 0);
764  progressWidget.resize(m_progress_info.size() + 1, COLS);
765  logWidget.resize(LINES - progressWidget.getHeight(), COLS);
766  m_trigger_resize = false;
767  }
768 
769  // There is output/error to redirect
770  if (poll_fds[0].revents & POLLIN) {
771  while ((nbytes = read(m_stderr_pipe, &buf, sizeof(buf))) > 0) {
772  logWidget.write(buf, nbytes);
773  }
774  }
775  if (poll_fds[1].revents & POLLIN) {
776  while ((nbytes = read(m_stdout_pipe, &buf, sizeof(buf))) > 0) {
777  logWidget.write(buf, nbytes);
778  }
779  }
780 
781  // There is a key to read
782  if (poll_fds[2].revents & POLLIN) {
783  int key = wgetch(stdscr);
784  if (key != KEY_RESIZE) {
785  logWidget.handleKeyPress(key);
786  }
787  }
788 
789  {
791  progressWidget.update(m_progress_info);
792  }
793 
794  // Update screen
795  doupdate();
796 
797  // Wait for events
798  if (poll(poll_fds, 4, 1000) < 0) {
799  // poll may return with EINTR if a signal happened halfway
800  exit_loop = (errno != EINTR);
801  }
802  } while (!exit_loop && !boost::this_thread::interruption_requested());
803  m_log_text = logWidget.getText();
804  }
805 
806 public:
815  int new_stderr_fd = dup(m_stderr_original);
816  if (new_stderr_fd < 0) {
818  }
819  m_stderr = fdopen(new_stderr_fd, "w");
820  m_ui_thread = make_unique<boost::thread>(std::bind(&Dashboard::uiThread, this));
821  }
822 
828  if (m_ui_thread) {
829  try {
830  m_ui_thread->interrupt();
831  if (m_ui_thread->joinable()) {
832  m_ui_thread->join();
833  }
834  }
835  catch (...) {
836  // Ignore
837  }
838  }
839  fclose(m_stderr);
840  // Unneeded duplicates now
841  close(m_stderr_original);
842  close(m_stdout_original);
843  close(m_stderr_pipe);
844  close(m_stdout_pipe);
845  }
846 
850  void update(const std::list<ProgressInfo>& info) {
852  m_trigger_resize = (m_progress_info.size() != info.size()) | m_trigger_resize;
853  m_progress_info = info;
854  }
855 };
856 
858  m_dashboard = make_unique<Dashboard>();
859 }
860 
862 }
863 
865  return isatty(STDERR_FILENO);
866 }
867 
869  if (m_dashboard)
870  m_dashboard->update(info);
871 }
872 
873 void ProgressNCurses::handleMessage(const bool& done) {
874  if (done && m_dashboard)
875  m_dashboard.reset(nullptr);
876 }
877 
878 } // end SourceXtractor
static std::map< int, struct sigaction > prev_signal
rl_voidfunc_t * m_old_redisplay
T empty(T...args)
ProgressWidget(int height, int width, int y, int x, short done_color, short progress_color)
void drawProgressLine(int value_position, int bar_width, int line, const std::string &label, int total, int done) const
static const int BUFFER_MAX_SIZE
std::shared_ptr< DependentParameter< std::shared_ptr< EngineParameter > > > x
T generic_category(T...args)
constexpr double s
T endl(T...args)
T duration_cast(T...args)
static const int BUFFER_INCREASE_STEP_SIZE
constexpr double bar
STL class.
static void override_rl_display(void)
std::shared_ptr< DependentParameter< std::shared_ptr< EngineParameter > > > y
T setw(T...args)
void drawElapsed(size_t value_position, const std::chrono::steady_clock::duration &elapsed, int line) const
T fclose(T...args)
static void handleTerminatingSignal(int s)
STL class.
T min(T...args)
static void handleStopSignal(int s)
static struct sigaction sigterm_action sigstop_action sigcont_action sigwich_action
T data(T...args)
constexpr double m
void write(const char *data, ssize_t nchars)
std::chrono::steady_clock::time_point m_started
void resize(int display_height, int display_width)
T pop_back(T...args)
T bind(T...args)
static int interceptFileDescriptor(int old_fd, int *backup_fd)
STL class.
T max(T...args)
T fixed(T...args)
void handleMessage(const std::list< ProgressInfo > &info) override
static struct SourceXtractor::ncurses_done ncurses_done
static void handleResizeSignal(int)
Wrap the terminal into a singleton.
T size(T...args)
STL class.
LogWidget(int display_height, int display_width, int display_y, int display_x, short bar_color, short ind_color)
STL class.
static int signal_fds[2]
T c_str(T...args)
Set of progress bars/information entries.
T back(T...args)
void update(const std::list< ProgressInfo > &info)
short initColor(short fg, short bg)
static void handleContinuationSignal(int s)
T fill(T...args)
std::unique_ptr< Dashboard > m_dashboard
T setprecision(T...args)
std::vector< std::string > getText()
T snprintf(T...args)
void update(const std::list< ProgressInfo > &info)
void resize(int height, int width)
std::unique_ptr< boost::thread > m_ui_thread
Screen(FILE *outfd, FILE *infd)
T emplace_back(T...args)