SourceXtractorPlusPlus 0.18
SourceXtractor++, the next generation SExtractor
ProgressNCurses.cpp
Go to the documentation of this file.
1
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
35
36
37namespace SourceXtractor {
38
39// Signal handlers
40static struct sigaction sigterm_action, sigstop_action, sigcont_action, sigwich_action;
42
43// Used for sending signals to the UI thread
44static int signal_fds[2];
45
46// Used by the UI thread to notify that is is done
47static struct ncurses_done {
50 sem_init(&m_semaphore, 0, 1);
51 }
53
54// Forward declaration of signal handlers
55static void handleTerminatingSignal(int s);
56static void handleStopSignal(int s);
57static void handleContinuationSignal(int s);
58static void handleResizeSignal(int);
59
60
70static int interceptFileDescriptor(int old_fd, int *backup_fd) {
71 int pipe_fds[2];
72
73 *backup_fd = dup(old_fd);
74 if (*backup_fd < 0) {
76 }
77
78 if (pipe(pipe_fds) < 0) {
80 }
81
82 int flags = fcntl(pipe_fds[0], F_GETFL, 0);
83 if (fcntl(pipe_fds[0], F_SETFL, flags | O_NONBLOCK) < 0) {
85 }
86
87 if (dup2(pipe_fds[1], old_fd) < 0) {
89 }
90 close(pipe_fds[1]);
91
92 return pipe_fds[0];
93}
94
103static void override_rl_display(void) {
104}
105
109class Screen : public boost::noncopyable {
110public:
111
119 Screen(FILE *outfd, FILE *infd) {
120 if (pipe(signal_fds) < 0) {
122 }
123
124 m_old_redisplay = rl_redisplay_function;
125 rl_redisplay_function = override_rl_display;
126
127 // Tell readline to leave the terminal be
128 rl_catch_signals = 0;
129 rl_deprep_term_function = NULL;
130 rl_prep_term_function = NULL;
131
132 // It seems like the readline in MacOSX is not the "real" readline, but a compatibility
133 // layer which misses some things, like the following:
134#ifndef __APPLE__
135 rl_catch_sigwinch = 0;
136#endif
137
138 // Setup signal handlers
139 ::memset(&sigterm_action, 0, sizeof(sigterm_action));
140 ::memset(&sigstop_action, 0, sizeof(sigstop_action));
141 ::memset(&sigcont_action, 0, sizeof(sigcont_action));
142 ::memset(&sigwich_action, 0, sizeof(sigwich_action));
143
144 // Termination
145 sigterm_action.sa_handler = &handleTerminatingSignal;
146 ::sigaction(SIGINT, &sigterm_action, &prev_signal[SIGINT]);
147 ::sigaction(SIGTERM, &sigterm_action, &prev_signal[SIGTERM]);
148 ::sigaction(SIGABRT, &sigterm_action, &prev_signal[SIGABRT]);
149 ::sigaction(SIGSEGV, &sigterm_action, &prev_signal[SIGSEGV]);
150 ::sigaction(SIGHUP, &sigterm_action, &prev_signal[SIGHUP]);
151
152 // bg
153 sigstop_action.sa_handler = &handleStopSignal;
154 ::sigaction(SIGTSTP, &sigstop_action, &prev_signal[SIGTSTP]);
155
156 // fg
157 sigcont_action.sa_handler = &handleContinuationSignal;
158 ::sigaction(SIGCONT, &sigcont_action, &prev_signal[SIGCONT]);
159
160 // Resizing
161 // Some versions of ncurses handle this by themselves, some other do not, so
162 // we do it ourselves in anycase
164 ::sigaction(SIGWINCH, &sigwich_action, &prev_signal[SIGWINCH]);
165
166 // Enter ncurses
167 initscr();
168 m_screen = newterm(nullptr, outfd, infd);
169 set_term(m_screen);
170
171 // Hide cursor
172 curs_set(0);
173
174 // Get input without echo, but leave Ctrl+<Key> to the terminal
175 cbreak();
176 keypad(stdscr, TRUE);
177 noecho();
178
179 // Setup colors
180 use_default_colors();
181 start_color();
182 }
183
187 virtual ~Screen() {
188 // Exit ncurses
189 endwin();
190 delscreen(m_screen);
191 rl_redisplay_function = m_old_redisplay;
192 // Restore signal handlers
193 ::sigaction(SIGINT, &prev_signal[SIGINT], nullptr);
194 ::sigaction(SIGTERM, &prev_signal[SIGTERM], nullptr);
195 ::sigaction(SIGABRT, &prev_signal[SIGABRT], nullptr);
196 ::sigaction(SIGSEGV, &prev_signal[SIGSEGV], nullptr);
197 ::sigaction(SIGHUP, &prev_signal[SIGHUP], nullptr);
198 ::sigaction(SIGCONT, &prev_signal[SIGCONT], nullptr);
199 ::sigaction(SIGWINCH, &prev_signal[SIGWINCH], nullptr);
200 close(signal_fds[0]);
201 close(signal_fds[1]);
202 }
203
207 short initColor(short fg, short bg) {
208 init_pair(m_color_idx, fg, bg);
209 return m_color_idx++;
210 }
211
212private:
213 short m_color_idx = 1;
214 SCREEN *m_screen;
215 rl_voidfunc_t* m_old_redisplay;
216};
217
234static void handleTerminatingSignal(int s) {
235 // Restore handler (so if we get stuck somewhere, and second
236 // signal occurs, like a SIGTERM, the process is killed for good)
237 sigaction(s, &prev_signal[s], nullptr);
238
239 // Notify
240 if (write(signal_fds[1], &s, sizeof(s)) == sizeof(s)) {
241 close(signal_fds[1]);
242 // Wait for UI thread to be done
243#if _POSIX_C_SOURCE >= 200112L
244 timespec timeout;
245 clock_gettime(CLOCK_REALTIME, &timeout);
246 timeout.tv_sec += 5;
247 sem_timedwait(&ncurses_done.m_semaphore, &timeout);
248#else
249 // MacOSX does not have timedwait
250 int timeout = 5;
251 while(timeout > 0 && sem_trywait(&ncurses_done.m_semaphore) < 0) {
252 sleep(1);
253 --timeout;
254 }
255#endif
256 }
257
258 // Call the previous handler
259 raise(s);
260}
261
265static void handleStopSignal(int s) {
266 // Restore handler
267 sigaction(s, &prev_signal[s], nullptr);
268
269 // Exit ncurses
270 endwin();
271
272 // Trigger the previous handler
273 raise(s);
274}
275
279static void handleContinuationSignal(int) {
280 // Restore handlers
281 sigaction(SIGCONT, &sigcont_action, nullptr);
282 sigaction(SIGTSTP, &sigstop_action, nullptr);
283}
284
288static void handleResizeSignal(int s) {
289 if (write(signal_fds[1], &s, sizeof(s)) < 0) {
290 // Just ignore
291 }
292}
293
298private:
299 WINDOW *m_pad, *m_scroll;
300 // Screen coordinates!
303 // Number of total lines being written so far
305 // Last line being *displayed*
307 // Colors
309
310 static const int BUFFER_INCREASE_STEP_SIZE = 10, BUFFER_MAX_SIZE = 16384;
311
312public:
313
329 LogWidget(int display_height, int display_width, int display_y, int display_x, short bar_color, short ind_color)
330 : m_pad(newpad(BUFFER_INCREASE_STEP_SIZE, display_width)),
331 m_scroll(newpad(display_height, 1)),
332 m_display_height(display_height), m_display_width(display_width), m_display_y(display_y), m_display_x(display_x),
334 scrollok(m_pad, TRUE);
335 }
336
340 virtual ~LogWidget() {
341 delwin(m_pad);
342 delwin(m_scroll);
343 }
344
348 void write(const char *data, ssize_t nchars) {
349 while (nchars > 0) {
350 if (*data == '\n') {
351 // If the current line is the last one, follow the log
354 }
356 // Increase buffer if we ran out of lines on the pad
357 if (getmaxy(m_pad) <= m_written_lines) {
359 }
360 }
361 waddch(m_pad, *data);
362 ++data, --nchars;
363 }
364 drawLog();
365 drawScroll();
366 }
367
371 void resize(int display_height, int display_width) {
372 m_display_height = display_height;
373 m_display_width = display_width;
374
375 // Resize to make place for the new width only if it is bigger.
376 // Note that the pad height depends on the number of written lines, not displayed size!
377 if (display_width > getmaxx(m_pad)) {
378 wresize(m_pad, getmaxy(m_pad), display_width);
379 }
380 wresize(m_scroll, display_height, 1);
381 drawLog();
382 drawScroll();
383 }
384
389 void scrollText(int d) {
390 m_active_line += d;
391 if (m_active_line > getcury(m_pad) + 1) {
392 m_active_line = getcury(m_pad) + 1;
393 }
396 }
399 }
400 drawLog();
401 drawScroll();
402 }
403
407 void handleKeyPress(int key) {
408 switch (key) {
409 case KEY_DOWN:
410 scrollText(1);
411 break;
412 case KEY_UP:
413 scrollText(-1);
414 break;
415 case KEY_NPAGE:
416 scrollText(LINES);
417 break;
418 case KEY_PPAGE:
419 scrollText(-LINES);
420 break;
421 }
422 }
423
428 // Scan line by line
429 std::vector<std::string> term_lines;
430 for (int i = 0; i < m_written_lines; ++i) {
431 // Note: We do not want the '\0' to be part of the final string, so we use the string constructor to prune those
432 std::vector<char> buffer(m_display_width + 1, '\0');
433 mvwinnstr(m_pad, i, 0, buffer.data(), m_display_width - 2);
434 term_lines.emplace_back(buffer.data());
435 boost::algorithm::trim(term_lines.back());
436 }
437 // Prune trailing empty lines
438 while (!term_lines.empty() && term_lines.back().empty()) {
439 term_lines.pop_back();
440 }
441 return term_lines;
442 }
443
444private:
448 void drawScroll() const {
449 werase(m_scroll);
450
451 int max_selectable_line = m_written_lines;
452 int min_selectable_line = std::min(m_written_lines, m_display_height);
453 int displayed_line_offset = m_active_line - min_selectable_line;
454 float p = std::max(0.f, std::min(1.f, displayed_line_offset / float(max_selectable_line - min_selectable_line)));
455
456 int scroll_marker_pos = p * (m_display_height - 1);
457 for (int i = 0; i < m_display_height; ++i) {
458 if (i == scroll_marker_pos)
459 waddch(m_scroll, ACS_CKBOARD | COLOR_PAIR(m_scroll_ind_color));
460 else
461 waddch(m_scroll, '|' | COLOR_PAIR(m_scroll_bar_color));
462 }
463 pnoutrefresh(m_scroll,
464 0, 0,
467 );
468 }
469
473 void drawLog() const {
474 int pad_y = std::max(m_active_line - m_display_height, 0);
475 pnoutrefresh(m_pad,
476 pad_y, 0, // Pad coordinates
477 m_display_y, m_display_x, // Start screen coordinates
478 m_display_y + m_display_height - 1, m_display_x + m_display_width - 2 // End screen coordinates
479 );
480 }
481};
482
486class ProgressWidget : public boost::noncopyable {
487public:
503 ProgressWidget(int height, int width, int y, int x, short done_color, short progress_color)
504 : m_window(newwin(height, width, y, x)), m_started(std::chrono::steady_clock::now()),
505 m_done_color(done_color), m_progress_color(progress_color) {
506 }
507
512 delwin(m_window);
513 }
514
522 void move(int y, int x) {
523 mvwin(m_window, y, x);
524 wnoutrefresh(m_window);
525 }
526
534 void resize(int height, int width) {
535 wresize(m_window, height, width);
536 wnoutrefresh(m_window);
537 }
538
542 unsigned getHeight() const {
543 return getmaxy(m_window);
544 }
545
549 void update(const std::list<ProgressInfo>& info) {
550 // Precalculate layout, so labels are aligned
551 size_t value_position = sizeof("Elapsed");
552
553 for (auto& entry: info) {
554 if (entry.m_label.size() > value_position) {
555 value_position = entry.m_label.size();
556 }
557 }
558 value_position++; // Plus space
559
560 // Width of the bar is the with of the windows - a space - two brackets []
561 size_t bar_width = getmaxx(m_window) - 2 - value_position;
562
563 // Elapsed
565 auto elapsed = now - m_started;
566
567 // Restore position to the beginning
568 werase(m_window);
569
570 // Now, print the actual progress
571 int line = 0;
572 for (auto& entry : info) {
573 drawProgressLine(value_position, bar_width, line, entry.m_label, entry.m_total, entry.m_done);
574 ++line;
575 }
576
577 // Elapsed time
578 drawElapsed(value_position, elapsed, line);
579
580 // Flush
581 wnoutrefresh(m_window);
582 }
583
584private:
588 void drawElapsed(size_t value_position, const std::chrono::steady_clock::duration& elapsed, int line) const {
589 auto h = std::chrono::duration_cast<std::chrono::hours>(elapsed);
590 auto m = std::chrono::duration_cast<std::chrono::minutes>(elapsed - h);
591 auto s = std::chrono::duration_cast<std::chrono::seconds>(elapsed - h - m);
592 std::ostringstream elapsed_str;
593 elapsed_str.fill('0');
594 elapsed_str << std::setw(2) << h.count() << ':' << std::setw(2) << m.count() << ':' << std::setw(2) << s.count();
595
596 wattron(m_window, A_BOLD);
597 mvwaddstr(m_window, line, 0, "Elapsed");
598 wattroff(m_window, A_BOLD);
599 mvwaddstr(
600 m_window,
601 line, value_position + 1,
602 elapsed_str.str().c_str()
603 );
604 }
605
609 void drawProgressLine(int value_position, int bar_width, int line, const std::string& label,
610 int total, int done) const {
611 // Label
612 wattron(m_window, A_BOLD);
613 mvwaddstr(m_window, line, 0, label.c_str());
614 wattroff(m_window, A_BOLD);
615
616 // Total number is unknown
617 if (total <= 0) {
618 mvwprintw(m_window, line, value_position + 1, "%d", done);
619 return;
620 }
621
622 // Otherwise, report progress as a bar
623 float ratio = done / static_cast<float>(total);
624 // This can happens sometimes, as a measurement could be notified before the deblending, for instance
625 if (ratio > 1)
626 ratio = 1.;
627
628 // Build the report string
630 bar << done << " / " << total << " (" << std::fixed << std::setprecision(2) << ratio * 100. << "%)";
631
632 // Attach as many spaces as needed to fill the screen width, minus brackets
633 bar << std::string(bar_width - bar.str().size(), ' ');
634
635 // Print label
636 wattron(m_window, A_BOLD);
637 mvwaddstr(m_window, line, 0, label.c_str());
638 wattroff(m_window, A_BOLD);
639 mvwaddch(m_window, line, value_position, '[');
640
641 // Completed
642 auto bar_content = bar.str();
643 int completed = bar_content.size() * ratio;
644
645 wattron(m_window, COLOR_PAIR(m_done_color));
646 waddstr(m_window, bar_content.substr(0, completed).c_str());
647 wattroff(m_window, COLOR_PAIR(m_done_color));
648
649 // Rest
650 wattron(m_window, COLOR_PAIR(m_progress_color));
651 waddstr(m_window, bar_content.substr(completed).c_str());
652 wattroff(m_window, COLOR_PAIR(2));
653
654 // Closing bracket
655 waddch(m_window, ']');
656 }
657
658 WINDOW *m_window;
659 std::chrono::steady_clock::time_point m_started;
661};
662
674private:
678
679 // stderr intercept
682 // stdout intercept
684
685 // Used to recover log into the standard output
687
688 std::atomic_bool m_trigger_resize, m_exit_loop;
689
693 void uiThread() {
694 sem_wait(&ncurses_done.m_semaphore);
695 // SIGTERM, SIGINT and SIGHUP should not be handled by this thread, or we will not be able to properly
696 // exit ncurses.
697 // Hopefully there should be no SIGABRT or SIGSEGV here. If there were, we will exit but we will not be able
698 // to restore the terminal state. Having an abort or a segmentation fault is a bug anyway.
699 sigset_t set;
700 sigaddset(&set, SIGTERM);
701 sigaddset(&set, SIGINT);
702 sigaddset(&set, SIGHUP);
703 pthread_sigmask(SIG_BLOCK, &set, nullptr);
704 // Enter ncurses
705 ncursesMode();
706 // Recover file descriptors
707 dup2(m_stderr_original, STDERR_FILENO);
708 dup2(m_stdout_original, STDOUT_FILENO);
709 // Dump recovered text
710 for (const auto& line : m_log_text) {
711 std::cerr << line << std::endl;
712 }
713 sem_post(&ncurses_done.m_semaphore);
714 }
715
716 void handleSignal(const struct pollfd& poll_fd, LogWidget& logWidget) {
717 if (poll_fd.revents & POLLIN) {
718 int signal_no;
719 if (read(signal_fds[0], &signal_no, sizeof(signal_no)) > 0 && signal_no == SIGWINCH) {
720 m_trigger_resize = true;
721 endwin();
722 refresh();
723 clear();
724 }
725 else {
726 char buf[64];
727 logWidget.write(buf, snprintf(buf, sizeof(buf), "Caught signal %s\n", strsignal(signal_no)));
728 m_exit_loop = true;
729 }
730 }
731 }
732
733 void pipeToLog(const struct pollfd& poll_fd, int pipe, LogWidget& out) {
734 if (poll_fd.revents & POLLIN) {
735 ssize_t nbytes;
736 char buf[64];
737 while ((nbytes = read(pipe, &buf, sizeof(buf))) > 0) {
738 out.write(buf, nbytes);
739 }
740 }
741 }
742
743 void handleKeyPress(const struct pollfd& poll_fd, LogWidget& logWidget) const {
744 if (poll_fd.revents & POLLIN) {
745 int key = wgetch(stdscr);
746 if (key != KEY_RESIZE) {
747 logWidget.handleKeyPress(key);
748 }
749 }
750 }
751
755 void ncursesMode() {
756 Screen screen(m_stderr, stdin);
757
758 // Log area
759 LogWidget logWidget(
760 LINES - 1, COLS, 0, 0,
761 screen.initColor(COLOR_WHITE, COLOR_BLACK), screen.initColor(COLOR_WHITE, COLOR_WHITE)
762 );
763
764 // Progress widget
765 ProgressWidget progressWidget(
766 1, COLS, LINES - 1, 0,
767 screen.initColor(COLOR_WHITE, COLOR_GREEN), screen.initColor(COLOR_WHITE, COLOR_BLACK)
768 );
769
770 // File descriptors to watch for
771 struct pollfd poll_fds[] = {
772 {m_stderr_pipe, POLLIN, 0},
773 {m_stdout_pipe, POLLIN, 0},
774 {STDIN_FILENO, POLLIN, 0},
775 {signal_fds[0], POLLIN, 0}
776 };
777
778 // Event loop
779 m_exit_loop = false;
780
781 do {
782 // There has been a signal
783 handleSignal(poll_fds[3], logWidget);
784
785 // Resize widgets if needed
786 if (m_trigger_resize) {
788 progressWidget.move(LINES - m_progress_info.size() - 1, 0);
789 progressWidget.resize(m_progress_info.size() + 1, COLS);
790 logWidget.resize(LINES - progressWidget.getHeight(), COLS);
791 m_trigger_resize = false;
792 }
793
794 // There is output/error to redirect
795 pipeToLog(poll_fds[0], m_stderr_pipe, logWidget);
796 pipeToLog(poll_fds[1], m_stdout_pipe, logWidget);
797
798 // There is a key to read
799 handleKeyPress(poll_fds[2], logWidget);
800
801 {
803 progressWidget.update(m_progress_info);
804 }
805
806 // Update screen
807 doupdate();
808
809 // Wait for events
810 if (poll(poll_fds, 4, 1000) < 0) {
811 // poll may return with EINTR if a signal happened halfway
812 m_exit_loop = (errno != EINTR);
813 }
814 } while (!m_exit_loop && !boost::this_thread::interruption_requested());
815 m_log_text = logWidget.getText();
816 }
817
818public:
827 int new_stderr_fd = dup(m_stderr_original);
828 if (new_stderr_fd < 0) {
830 }
831 m_stderr = fdopen(new_stderr_fd, "w");
832 m_ui_thread = Euclid::make_unique<boost::thread>(std::bind(&Dashboard::uiThread, this));
833 }
834
840 if (m_ui_thread) {
841 try {
842 m_ui_thread->interrupt();
843 if (m_ui_thread->joinable()) {
844 m_ui_thread->join();
845 }
846 }
847 catch (...) {
848 // Ignore
849 }
850 }
852 // Unneeded duplicates now
853 close(m_stderr_original);
854 close(m_stdout_original);
855 close(m_stderr_pipe);
856 close(m_stdout_pipe);
857 }
858
862 void update(const std::list<ProgressInfo>& info) {
865 m_progress_info = info;
866 }
867};
868
870 m_dashboard = make_unique<Dashboard>();
871}
872
874}
875
877 return isatty(STDERR_FILENO);
878}
879
881 if (m_dashboard)
882 m_dashboard->update(info);
883}
884
885void ProgressNCurses::handleMessage(const bool& done) {
886 if (done && m_dashboard)
887 m_dashboard.reset(nullptr);
888}
889
890} // end SourceXtractor
std::shared_ptr< DependentParameter< std::shared_ptr< EngineParameter > > > x
std::shared_ptr< DependentParameter< std::shared_ptr< EngineParameter > > > y
T back(T... args)
T bind(T... args)
T c_str(T... args)
LogWidget(int display_height, int display_width, int display_y, int display_x, short bar_color, short ind_color)
void write(const char *data, ssize_t nchars)
static const int BUFFER_INCREASE_STEP_SIZE
std::vector< std::string > getText()
void resize(int display_height, int display_width)
static const int BUFFER_MAX_SIZE
void handleKeyPress(const struct pollfd &poll_fd, LogWidget &logWidget) const
void pipeToLog(const struct pollfd &poll_fd, int pipe, LogWidget &out)
std::unique_ptr< boost::thread > m_ui_thread
void update(const std::list< ProgressInfo > &info)
void handleSignal(const struct pollfd &poll_fd, LogWidget &logWidget)
std::unique_ptr< Dashboard > m_dashboard
void handleMessage(const std::list< ProgressInfo > &info) override
Set of progress bars/information entries.
void drawElapsed(size_t value_position, const std::chrono::steady_clock::duration &elapsed, int line) const
ProgressWidget(int height, int width, int y, int x, short done_color, short progress_color)
std::chrono::steady_clock::time_point m_started
void resize(int height, int width)
void update(const std::list< ProgressInfo > &info)
void drawProgressLine(int value_position, int bar_width, int line, const std::string &label, int total, int done) const
Wrap the terminal into a singleton.
rl_voidfunc_t * m_old_redisplay
short initColor(short fg, short bg)
Screen(FILE *outfd, FILE *infd)
T data(T... args)
T emplace_back(T... args)
T empty(T... args)
T endl(T... args)
T fclose(T... args)
T fill(T... args)
T fixed(T... args)
T snprintf(T... args)
T generic_category(T... args)
T max(T... args)
T min(T... args)
constexpr double s
constexpr double bar
constexpr double m
std::unique_ptr< T > make_unique(Args &&... args)
static void handleResizeSignal(int)
static struct sigaction sigterm_action sigstop_action sigcont_action sigwich_action
static void override_rl_display(void)
static std::map< int, struct sigaction > prev_signal
static void handleTerminatingSignal(int s)
static void handleStopSignal(int s)
static int signal_fds[2]
static struct SourceXtractor::ncurses_done ncurses_done
static void handleContinuationSignal(int s)
static int interceptFileDescriptor(int old_fd, int *backup_fd)
STL namespace.
T pop_back(T... args)
T raise(T... args)
T setprecision(T... args)
T setw(T... args)
T size(T... args)
T str(T... args)