SourceXtractorPlusPlus 1.0.3
SourceXtractor++, the next generation SExtractor
Loading...
Searching...
No Matches
ProgressNCurses.cpp
Go to the documentation of this file.
1
17#include "AlexandriaKernel/memory_tools.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
35
36
37namespace SourceXtractor {
38
39// Signal handlers
40static struct sigaction sigterm_action;
41static struct sigaction sigstop_action;
42static struct sigaction sigcont_action;
43static struct sigaction sigwich_action;
45
46// Used for sending signals to the UI thread
47static int signal_fds[2];
48
49// Used by the UI thread to notify that is is done
53 sem_init(&m_semaphore, 0, 1);
54 }
55};
57
58// Forward declaration of signal handlers
59static void handleTerminatingSignal(int s);
60static void handleStopSignal(int s);
61static void handleContinuationSignal(int s);
62static void handleResizeSignal(int);
63
64
74static int interceptFileDescriptor(int old_fd, int *backup_fd) {
75 int pipe_fds[2];
76
77 *backup_fd = dup(old_fd);
78 if (*backup_fd < 0) {
80 }
81
82 if (pipe(pipe_fds) < 0) {
84 }
85
86 int flags = fcntl(pipe_fds[0], F_GETFL, 0);
87 if (fcntl(pipe_fds[0], F_SETFL, flags | O_NONBLOCK) < 0) {
89 }
90
91 if (dup2(pipe_fds[1], old_fd) < 0) {
93 }
94 close(pipe_fds[1]);
95
96 return pipe_fds[0];
97}
98
107static void override_rl_display(void) {
108}
109
113class Screen : public boost::noncopyable {
114public:
115
123 Screen(FILE *outfd, FILE *infd) {
124 if (pipe(signal_fds) < 0) {
126 }
127
128 m_old_redisplay = rl_redisplay_function;
129 rl_redisplay_function = override_rl_display;
130
131 // Tell readline to leave the terminal be
132 rl_catch_signals = 0;
133 rl_deprep_term_function = nullptr;
134 rl_prep_term_function = nullptr;
135
136 // It seems like the readline in MacOSX is not the "real" readline, but a compatibility
137 // layer which misses some things, like the following:
138#ifndef __APPLE__
139 rl_catch_sigwinch = 0;
140#endif
141
142 // Setup signal handlers
147
148 // Termination
150 ::sigaction(SIGINT, &sigterm_action, &prev_signal[SIGINT]);
151 ::sigaction(SIGTERM, &sigterm_action, &prev_signal[SIGTERM]);
152 ::sigaction(SIGABRT, &sigterm_action, &prev_signal[SIGABRT]);
153 ::sigaction(SIGSEGV, &sigterm_action, &prev_signal[SIGSEGV]);
154 ::sigaction(SIGHUP, &sigterm_action, &prev_signal[SIGHUP]);
155
156 // bg
157 sigstop_action.sa_handler = &handleStopSignal;
158 ::sigaction(SIGTSTP, &sigstop_action, &prev_signal[SIGTSTP]);
159
160 // fg
162 ::sigaction(SIGCONT, &sigcont_action, &prev_signal[SIGCONT]);
163
164 // Resizing
165 // Some versions of ncurses handle this by themselves, some other do not, so
166 // we do it ourselves in anycase
168 ::sigaction(SIGWINCH, &sigwich_action, &prev_signal[SIGWINCH]);
169
170 // Enter ncurses
171 initscr();
172 m_screen = newterm(nullptr, outfd, infd);
173 set_term(m_screen);
174
175 // Hide cursor
176 curs_set(0);
177
178 // Get input without echo, but leave Ctrl+<Key> to the terminal
179 cbreak();
180 keypad(stdscr, TRUE);
181 noecho();
182
183 // Setup colors
184 use_default_colors();
185 start_color();
186 }
187
191 virtual ~Screen() {
192 // Exit ncurses
193 endwin();
194 delscreen(m_screen);
195 rl_redisplay_function = m_old_redisplay;
196 // Restore signal handlers
197 ::sigaction(SIGINT, &prev_signal[SIGINT], nullptr);
198 ::sigaction(SIGTERM, &prev_signal[SIGTERM], nullptr);
199 ::sigaction(SIGABRT, &prev_signal[SIGABRT], nullptr);
200 ::sigaction(SIGSEGV, &prev_signal[SIGSEGV], nullptr);
201 ::sigaction(SIGHUP, &prev_signal[SIGHUP], nullptr);
202 ::sigaction(SIGCONT, &prev_signal[SIGCONT], nullptr);
203 ::sigaction(SIGWINCH, &prev_signal[SIGWINCH], nullptr);
204 close(signal_fds[0]);
205 close(signal_fds[1]);
206 }
207
211 short initColor(short fg, short bg) {
212 init_pair(m_color_idx, fg, bg);
213 return m_color_idx++;
214 }
215
216private:
217 short m_color_idx = 1;
218 SCREEN *m_screen;
219 rl_voidfunc_t* m_old_redisplay;
220};
221
238static void handleTerminatingSignal(int s) {
239 // Restore handler (so if we get stuck somewhere, and second
240 // signal occurs, like a SIGTERM, the process is killed for good)
241 sigaction(s, &prev_signal[s], nullptr);
242
243 // Notify
244 if (write(signal_fds[1], &s, sizeof(s)) == sizeof(s)) {
245 close(signal_fds[1]);
246 // Wait for UI thread to be done
247#if _POSIX_C_SOURCE >= 200112L
248 timespec timeout;
249 clock_gettime(CLOCK_REALTIME, &timeout);
250 timeout.tv_sec += 5;
251 sem_timedwait(&ncurses_done.m_semaphore, &timeout);
252#else
253 // MacOSX does not have timedwait
254 int timeout = 5;
255 while(timeout > 0 && sem_trywait(&ncurses_done.m_semaphore) < 0) {
256 sleep(1);
257 --timeout;
258 }
259#endif
260 }
261
262 // Call the previous handler
263 raise(s);
264}
265
269static void handleStopSignal(int s) {
270 // Restore handler
271 sigaction(s, &prev_signal[s], nullptr);
272
273 // Exit ncurses
274 endwin();
275
276 // Trigger the previous handler
277 raise(s);
278}
279
283static void handleContinuationSignal(int) {
284 // Restore handlers
285 sigaction(SIGCONT, &sigcont_action, nullptr);
286 sigaction(SIGTSTP, &sigstop_action, nullptr);
287}
288
292static void handleResizeSignal(int s) {
293 if (write(signal_fds[1], &s, sizeof(s)) < 0) {
294 // Just ignore
295 }
296}
297
302private:
303 WINDOW* m_pad;
304 WINDOW* m_scroll;
305 // Screen coordinates!
310 // Number of total lines being written so far
312 // Last line being *displayed*
314 // Colors
317
318 static const int BUFFER_INCREASE_STEP_SIZE = 10;
319 static const int BUFFER_MAX_SIZE = 16384;
320
321public:
322
338 LogWidget(int display_height, int display_width, int display_y, int display_x, short bar_color, short ind_color)
339 : m_pad(newpad(BUFFER_INCREASE_STEP_SIZE, display_width)),
340 m_scroll(newpad(display_height, 1)),
341 m_display_height(display_height), m_display_width(display_width), m_display_y(display_y), m_display_x(display_x),
342 m_scroll_bar_color(bar_color), m_scroll_ind_color(ind_color) {
343 scrollok(m_pad, TRUE);
344 }
345
349 virtual ~LogWidget() {
350 delwin(m_pad);
351 delwin(m_scroll);
352 }
353
354 LogWidget(const LogWidget&) = delete;
355 const LogWidget& operator=(const LogWidget&) = delete;
356
360 void write(const char *data, ssize_t nchars) {
361 while (nchars > 0) {
362 if (*data == '\n') {
363 // If the current line is the last one, follow the log
366 }
368 // Increase buffer if we ran out of lines on the pad
369 if (getmaxy(m_pad) <= m_written_lines) {
371 }
372 }
373 waddch(m_pad, *data);
374 ++data;
375 --nchars;
376 }
377 drawLog();
378 drawScroll();
379 }
380
384 void resize(int display_height, int display_width) {
385 m_display_height = display_height;
386 m_display_width = display_width;
387
388 // Resize to make place for the new width only if it is bigger.
389 // Note that the pad height depends on the number of written lines, not displayed size!
390 if (display_width > getmaxx(m_pad)) {
391 wresize(m_pad, getmaxy(m_pad), display_width);
392 }
393 wresize(m_scroll, display_height, 1);
394 drawLog();
395 drawScroll();
396 }
397
402 void scrollText(int d) {
403 m_active_line += d;
404 if (m_active_line > getcury(m_pad) + 1) {
405 m_active_line = getcury(m_pad) + 1;
406 }
409 }
412 }
413 drawLog();
414 drawScroll();
415 }
416
420 void handleKeyPress(int key) {
421 switch (key) {
422 case KEY_DOWN:
423 scrollText(1);
424 break;
425 case KEY_UP:
426 scrollText(-1);
427 break;
428 case KEY_NPAGE:
429 scrollText(LINES);
430 break;
431 case KEY_PPAGE:
432 scrollText(-LINES);
433 break;
434 default:
435 break;
436 }
437 }
438
443 // Scan line by line
444 std::vector<std::string> term_lines;
445 for (int i = 0; i < m_written_lines; ++i) {
446 // Note: We do not want the '\0' to be part of the final string, so we use the string constructor to prune those
447 std::vector<char> buffer(m_display_width + 1, '\0');
448 mvwinnstr(m_pad, i, 0, buffer.data(), m_display_width - 2);
449 term_lines.emplace_back(buffer.data());
450 boost::algorithm::trim(term_lines.back());
451 }
452 // Prune trailing empty lines
453 while (!term_lines.empty() && term_lines.back().empty()) {
454 term_lines.pop_back();
455 }
456 return term_lines;
457 }
458
459private:
463 void drawScroll() const {
464 werase(m_scroll);
465
466 int max_selectable_line = m_written_lines;
467 int min_selectable_line = std::min(m_written_lines, m_display_height);
468 int displayed_line_offset = m_active_line - min_selectable_line;
469 float p = std::max(0.f, std::min(1.f, static_cast<float>(displayed_line_offset) /
470 static_cast<float>(max_selectable_line - min_selectable_line)));
471
472 auto scroll_marker_pos = static_cast<int>(p * static_cast<float>(m_display_height - 1));
473 for (int i = 0; i < m_display_height; ++i) {
474 if (i == scroll_marker_pos)
475 waddch(m_scroll, ACS_CKBOARD | COLOR_PAIR(m_scroll_ind_color));
476 else
477 waddch(m_scroll, '|' | COLOR_PAIR(m_scroll_bar_color));
478 }
479 pnoutrefresh(m_scroll,
480 0, 0,
483 );
484 }
485
489 void drawLog() const {
490 int pad_y = std::max(m_active_line - m_display_height, 0);
491 pnoutrefresh(m_pad,
492 pad_y, 0, // Pad coordinates
493 m_display_y, m_display_x, // Start screen coordinates
494 m_display_y + m_display_height - 1, m_display_x + m_display_width - 2 // End screen coordinates
495 );
496 }
497};
498
502class ProgressWidget : public boost::noncopyable {
503public:
519 ProgressWidget(int height, int width, int y, int x, short done_color, short progress_color)
520 : m_window(newwin(height, width, y, x)), m_done_color(done_color), m_progress_color(progress_color) {}
521
526 delwin(m_window);
527 }
528
536 void move(int y, int x) {
537 mvwin(m_window, y, x);
538 wnoutrefresh(m_window);
539 }
540
548 void resize(int height, int width) {
549 wresize(m_window, height, width);
550 wnoutrefresh(m_window);
551 }
552
556 unsigned getHeight() const {
557 return getmaxy(m_window);
558 }
559
563 void update(const std::list<ProgressInfo>& info) {
564 // Precalculate layout, so labels are aligned
565 size_t value_position = sizeof("Elapsed");
566
567 for (auto& entry: info) {
568 if (entry.m_label.size() > value_position) {
569 value_position = entry.m_label.size();
570 }
571 }
572 value_position++; // Plus space
573
574 // Width of the bar is the with of the windows - a space - two brackets []
575 int bar_width = getmaxx(m_window) - 2 - static_cast<int>(value_position);
576
577 // Elapsed
579 auto elapsed = now - m_started;
580
581 // Restore position to the beginning
582 werase(m_window);
583
584 // Now, print the actual progress
585 int line = 0;
586 for (auto& entry : info) {
587 drawProgressLine(static_cast<int>(value_position), bar_width, line, entry.m_label, entry.m_total, entry.m_done);
588 ++line;
589 }
590
591 // Elapsed time
592 drawElapsed(static_cast<int>(value_position), elapsed, line);
593
594 // Flush
595 wnoutrefresh(m_window);
596 }
597
598private:
602 void drawElapsed(size_t value_position, const std::chrono::steady_clock::duration& elapsed, int line) const {
605 auto s = std::chrono::duration_cast<std::chrono::seconds>(elapsed - h - m);
606 std::ostringstream elapsed_str;
607 elapsed_str.fill('0');
608 elapsed_str << std::setw(2) << h.count() << ':' << std::setw(2) << m.count() << ':' << std::setw(2) << s.count();
609
610 wattron(m_window, A_BOLD);
611 mvwaddstr(m_window, line, 0, "Elapsed");
612 wattroff(m_window, A_BOLD);
613 mvwaddstr(
614 m_window,
615 line, value_position + 1,
616 elapsed_str.str().c_str()
617 );
618 }
619
623 void drawProgressLine(int value_position, int bar_width, int line, const std::string& label,
624 int total, int done) const {
625 // Label
626 wattron(m_window, A_BOLD);
627 mvwaddstr(m_window, line, 0, label.c_str());
628 wattroff(m_window, A_BOLD);
629
630 // Total number is unknown
631 if (total <= 0) {
632 mvwprintw(m_window, line, value_position + 1, "%d", done);
633 return;
634 }
635
636 // Otherwise, report progress as a bar
637 float ratio = static_cast<float>(done) / static_cast<float>(total);
638 // This can happens sometimes, as a measurement could be notified before the deblending, for instance
639 if (ratio > 1)
640 ratio = 1.;
641
642 // Build the report string
644 bar << done << " / " << total << " (" << std::fixed << std::setprecision(2) << ratio * 100. << "%)";
645
646 // Attach as many spaces as needed to fill the screen width, minus brackets
647 bar << std::string(bar_width - bar.str().size(), ' ');
648
649 // Print label
650 wattron(m_window, A_BOLD);
651 mvwaddstr(m_window, line, 0, label.c_str());
652 wattroff(m_window, A_BOLD);
653 mvwaddch(m_window, line, value_position, '[');
654
655 // Completed
656 auto bar_content = bar.str();
657 auto completed = static_cast<int>(static_cast<float>(bar_content.size()) * ratio);
658
659 wattron(m_window, COLOR_PAIR(m_done_color));
660 waddstr(m_window, bar_content.substr(0, completed).c_str());
661 wattroff(m_window, COLOR_PAIR(m_done_color));
662
663 // Rest
664 wattron(m_window, COLOR_PAIR(m_progress_color));
665 waddstr(m_window, bar_content.substr(completed).c_str());
666 wattroff(m_window, COLOR_PAIR(2));
667
668 // Closing bracket
669 waddch(m_window, ']');
670 }
671
672 WINDOW *m_window;
673 std::chrono::steady_clock::time_point m_started = std::chrono::steady_clock::now();
676};
677
689private:
693
694 // stderr intercept
698 // stdout intercept
701
702 // Used to recover log into the standard output
704
705 std::atomic_bool m_trigger_resize{false};
706 std::atomic_bool m_exit_loop{false};
707
711 void uiThread() {
712 sem_wait(&ncurses_done.m_semaphore);
713 // SIGTERM, SIGINT and SIGHUP should not be handled by this thread, or we will not be able to properly
714 // exit ncurses.
715 // Hopefully there should be no SIGABRT or SIGSEGV here. If there were, we will exit but we will not be able
716 // to restore the terminal state. Having an abort or a segmentation fault is a bug anyway.
717 sigset_t set;
718 sigaddset(&set, SIGTERM);
719 sigaddset(&set, SIGINT);
720 sigaddset(&set, SIGHUP);
721 pthread_sigmask(SIG_BLOCK, &set, nullptr);
722 // Enter ncurses
723 ncursesMode();
724 // Recover file descriptors
725 dup2(m_stderr_original, STDERR_FILENO);
726 dup2(m_stdout_original, STDOUT_FILENO);
727 // Dump recovered text
728 for (const auto& line : m_log_text) {
729 std::cerr << line << std::endl;
730 }
731 sem_post(&ncurses_done.m_semaphore);
732 }
733
734 void handleSignal(const struct pollfd& poll_fd, LogWidget& logWidget) {
735 if (poll_fd.revents & POLLIN) {
736 int signal_no;
737 if (read(signal_fds[0], &signal_no, sizeof(signal_no)) == sizeof(signal_no) && signal_no == SIGWINCH) {
738 m_trigger_resize = true;
739 endwin();
740 refresh();
741 clear();
742 }
743 else {
744 char buf[64];
745 logWidget.write(buf, snprintf(buf, sizeof(buf), "Caught signal %s\n", strsignal(signal_no)));
746 m_exit_loop = true;
747 }
748 }
749 }
750
751 void pipeToLog(const struct pollfd& poll_fd, int pipe, LogWidget& out) const {
752 if (poll_fd.revents & POLLIN) {
753 ssize_t nbytes;
754 char buf[64];
755 while ((nbytes = read(pipe, &buf, sizeof(buf))) > 0) {
756 out.write(buf, nbytes);
757 }
758 }
759 }
760
761 void handleKeyPress(const struct pollfd& poll_fd, LogWidget& logWidget) const {
762 if (poll_fd.revents & POLLIN) {
763 int key = wgetch(stdscr);
764 if (key != KEY_RESIZE) {
765 logWidget.handleKeyPress(key);
766 }
767 }
768 }
769
773 void ncursesMode() {
774 Screen screen(m_stderr, stdin);
775
776 // Log area
777 LogWidget logWidget(
778 LINES - 1, COLS, 0, 0,
779 screen.initColor(COLOR_WHITE, COLOR_BLACK), screen.initColor(COLOR_WHITE, COLOR_WHITE)
780 );
781
782 // Progress widget
783 ProgressWidget progressWidget(
784 1, COLS, LINES - 1, 0,
785 screen.initColor(COLOR_WHITE, COLOR_GREEN), screen.initColor(COLOR_WHITE, COLOR_BLACK)
786 );
787
788 // File descriptors to watch for
789 struct pollfd poll_fds[] = {
790 {m_stderr_pipe, POLLIN, 0},
791 {m_stdout_pipe, POLLIN, 0},
792 {STDIN_FILENO, POLLIN, 0},
793 {signal_fds[0], POLLIN, 0}
794 };
795
796 // Event loop
797 m_exit_loop = false;
798
799 do {
800 // There has been a signal
801 handleSignal(poll_fds[3], logWidget);
802
803 // Resize widgets if needed
804 if (m_trigger_resize) {
806 progressWidget.move(static_cast<int>(LINES - m_progress_info.size() - 1), 0);
807 progressWidget.resize(static_cast<int>(m_progress_info.size() + 1), COLS);
808 logWidget.resize(LINES - progressWidget.getHeight(), COLS);
809 m_trigger_resize = false;
810 }
811
812 // There is output/error to redirect
813 pipeToLog(poll_fds[0], m_stderr_pipe, logWidget);
814 pipeToLog(poll_fds[1], m_stdout_pipe, logWidget);
815
816 // There is a key to read
817 handleKeyPress(poll_fds[2], logWidget);
818
819 {
821 progressWidget.update(m_progress_info);
822 }
823
824 // Update screen
825 doupdate();
826
827 // Wait for events
828 if (poll(poll_fds, 4, 1000) < 0) {
829 // poll may return with EINTR if a signal happened halfway
830 m_exit_loop = (errno != EINTR);
831 }
832 } while (!m_exit_loop && !boost::this_thread::interruption_requested());
833 m_log_text = logWidget.getText();
834 }
835
836public:
845 int new_stderr_fd = dup(m_stderr_original);
846 if (new_stderr_fd < 0) {
848 }
849 m_stderr = fdopen(new_stderr_fd, "w");
851 }
852
858 if (m_ui_thread) {
859 try {
860 m_ui_thread->interrupt();
861 if (m_ui_thread->joinable()) {
862 m_ui_thread->join();
863 }
864 }
865 catch (...) {
866 // Ignore
867 }
868 }
870 // Unneeded duplicates now
871 close(m_stderr_original);
872 close(m_stdout_original);
873 close(m_stderr_pipe);
874 close(m_stdout_pipe);
875 }
876
885};
886
890
892
894 return isatty(STDERR_FILENO);
895}
896
898 if (m_dashboard)
899 m_dashboard->update(info);
900}
901
902void ProgressNCurses::handleMessage(const bool& done) {
903 if (done && m_dashboard)
904 m_dashboard.reset(nullptr);
905}
906
907} // end SourceXtractor
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)
const LogWidget & operator=(const LogWidget &)=delete
LogWidget(const LogWidget &)=delete
void handleKeyPress(const struct pollfd &poll_fd, LogWidget &logWidget) const
std::unique_ptr< boost::thread > m_ui_thread
void update(const std::list< ProgressInfo > &info)
void pipeToLog(const struct pollfd &poll_fd, int pipe, LogWidget &out) const
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 duration_cast(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 memset(T... args)
T min(T... args)
std::unique_ptr< T > make_unique(Args &&... args)
static void handleResizeSignal(int)
static struct sigaction sigterm_action
static struct sigaction sigcont_action
static void override_rl_display(void)
static std::map< int, struct sigaction > prev_signal
static void handleTerminatingSignal(int s)
static struct sigaction sigwich_action
static void handleStopSignal(int s)
static int signal_fds[2]
static void handleContinuationSignal(int s)
static int interceptFileDescriptor(int old_fd, int *backup_fd)
static struct sigaction sigstop_action
static ncurses_done_t ncurses_done
std::unique_ptr< T > make_unique(Args &&... args)
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)