// ------------------------------------------------------------------------------------------- // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // This file is part of the AWS CDI-SDK, licensed under the BSD 2-Clause "Simplified" License. // License details at: https://github.com/aws/aws-cdi-sdk/blob/mainline/LICENSE // ------------------------------------------------------------------------------------------- /** * @file * @brief * This file contains utility functions for outputting data on the console. In addition to standard console output, it * supports a multi-window console using the ncurses libary on linux and the PDCurses libary on windows. */ // Include headers in the following order: Related header, C system headers, other libraries' headers, your project's // headers. #include "test_console.h" #ifdef _WIN32 #include <fcntl.h> // For O_TEXT #endif #include <stdio.h> #include <stdlib.h> #include "cdi_logger_api.h" #include "cdi_test.h" #include "curses.h" #include "utilities_api.h" //********************************************************************************************************************* //***************************************** START OF DEFINITIONS AND TYPES ******************************************** //********************************************************************************************************************* /// Maximum length of a single line from stderr output. The text wraps if the value is exceeded. #define MAX_MESSAGE_SIZE (1024) //********************************************************************************************************************* //*********************************************** START OF VARIABLES ************************************************** //********************************************************************************************************************* /// Multi-window screen has been successfully initialized. static bool screen_initialized = false; static int console_height = 0; ///< Height of console. static int console_width = 0; ///< Width of console. /// If multi-window mode is enabled, this pointer is used by the stats window. static WINDOW* stats_window_ptr = NULL; static int stats_window_height = 0; ///< Height of stats window. static chtype* stats_window_buffer_ptr = NULL; ///< Pointer to buffer used to hold a copy of the stats window. /// If multi-window mode is enabled, this pointer is used by the scrolling log message window. static WINDOW* log_window_ptr = NULL; static int log_window_height = 0; ///< Height of log window. static chtype* log_window_buffer_ptr = NULL; ///< Pointer to buffer used to hold a copy of the log window. /// Critical section used to protect access to the log window. static CdiCsID log_window_lock = NULL; /// File descriptor for pipe. [0]= read fd, [1]= write fd. static int pipe_fd_array[2] = { CDI_INVALID_HANDLE_VALUE, CDI_INVALID_HANDLE_VALUE }; static int original_stdout_fd = CDI_INVALID_HANDLE_VALUE; ///< File descriptor for stdout. CdiThreadID console_thread_id; ///< Thread ID for this connection. /// When true, the console is being destroyed. This is used to prevent threads from using the console output functions /// while TestConsoleDestroy() is executing. static bool abnormal_termination_enabled = false; //********************************************************************************************************************* //******************************************* START OF STATIC FUNCTIONS *********************************************** //********************************************************************************************************************* /** * Allocate a buffer and save the contents of a window to it. * * @param window_ptr Pointer to window handle. * @param buffer_ptr Pointer to where to store the window data. * @param height Height of window. * @param width Width of window. */ static void SaveWindowToBuffer(WINDOW* window_ptr, chtype* buffer_ptr, int height, int width) { for (int i = 0; i < height; i++) { mvwinchnstr(window_ptr, i, 0, buffer_ptr + i*width, width); } } /** * Dump the text in a window to stdout. * * @param buffer_ptr Pointer to window buffer. * @param height Height of window. * @param width Width of window. */ static void DumpSavedWindowToStdout(chtype* buffer_ptr, int height, int width) { for (int r = 0; r < height; r++) { // Ignore blank lines. bool is_blank = true; for (int i = 0; i < width; i++) { char c = buffer_ptr[i + r*width] & A_CHARTEXT; if ('\0' == c) { break; } else if (' ' != c) { is_blank = false; break; } } if (!is_blank) { for (int i = 0; i < width; i++) { char c = buffer_ptr[i + r*width] & A_CHARTEXT; if ('\0' == c) { break; } putchar(c); } #ifndef _WIN32 // Force a carriage return on stdout. Some linux consoles only move down to the next line and not back to // the start of it. This is not needed in windows. puts("\r"); #endif } } } /** * Behaves like vprintf but adds a prefix and a newline to the format string. * * @param prefix_str Prefix string. * @param format_str Format string. * @param vars Argument list (see vprintf). * * @return */ static int vprintf_line(const char* prefix_str, const char* format_str, va_list vars) { int ret = 0; const size_t prefix_size = strlen(prefix_str); const size_t format_size = strlen(format_str); char format_buffer[MAX_MESSAGE_SIZE]; if (prefix_size + format_size < sizeof(format_buffer) - 2) { char* p = format_buffer; memcpy(p, prefix_str, prefix_size); p += prefix_size; memcpy(p, format_str, format_size); p += format_size; *p++ = '\r'; *p++ = '\n'; *p = 0; ret = vprintf(format_buffer, vars); } else { // If the buffer is too small we fall back on the non thread-safe way. if (prefix_size) { printf("%s", prefix_str); } ret = vprintf(format_str, vars); // send to stdout if (ret >= 0) { ret += printf("\r\n"); } } return ret; } /** * This function monitors the stderr pipe, and sends any data to the console log window. * * @param arg_ptr Pointer to parameter for use by the thread (not used here). * * @return Always returns 0 (not used). */ static CDI_THREAD TestConsoleThread(void* arg_ptr) { (void)arg_ptr; // Not used char msg_buf_ptr[MAX_MESSAGE_SIZE]; if (CDI_INVALID_HANDLE_VALUE != pipe_fd_array[1]) { int index = 0; // Read characters until the pipe has been closed and is empty. while (read(pipe_fd_array[0], &msg_buf_ptr[index], 1) > 0) { // Process a line of characters delimited by '\n' or max size. if ('\n' == msg_buf_ptr[index] || index >= MAX_MESSAGE_SIZE - 1) { // Don't want to include the line ending character, since TestConsoleLog() will add one. if ('\n' != msg_buf_ptr[index]) { index++; } msg_buf_ptr[index] = '\0'; // Put end of line marker in the string. // Write the line to the scrolling log window. TestConsoleLog(kLogInfo, msg_buf_ptr); index = 0; // Reset pointer to start of buffer for next meesage. } else { index++; } } // Pipe has been closed and emptied. Log any remaining characters that are in our line buffer. if (index) { msg_buf_ptr[index] = '\0'; // Put end of line marker in the string. TestConsoleLog(kLogInfo, msg_buf_ptr); } close(pipe_fd_array[0]); // Close the read end of the pipe. // Invalidate the FDs. pipe_fd_array[0] = CDI_INVALID_HANDLE_VALUE; pipe_fd_array[1] = CDI_INVALID_HANDLE_VALUE; } return 0; // This is not used. } //********************************************************************************************************************* //******************************************* START OF PUBLIC FUNCTIONS *********************************************** //********************************************************************************************************************* bool TestConsoleCreate(bool multi_window_mode, int num_stats_lines) { bool ret = true; stats_window_height = num_stats_lines; if (multi_window_mode) { ret = CdiOsCritSectionCreate(&log_window_lock); if (ret) { // We are going to redirect stderr to a pipe and control when it text goes to the log window. Otherwise it will // be written directly to the console and appear at random locations. original_stdout_fd = dup(CDI_STDERR_FILENO); // Save a copy of the original stderr. // Create a read/write pipe to use for stderr. #ifdef _WIN32 ret = _pipe(pipe_fd_array, 1024, O_TEXT) == 0; #else ret = pipe(pipe_fd_array) == 0; #endif } if (ret) { ret = dup2(pipe_fd_array[1], CDI_STDERR_FILENO) >= 0; // Send output on stderr to the pipe's writer. } if (ret) { ret = CdiOsThreadCreate(TestConsoleThread, &console_thread_id, "Console", NULL, NULL); } // Initialize the curses console. if (ret && NULL == initscr()) { ret = false; } else { screen_initialized = true; } if (ret) { // Get console size. getmaxyx(stdscr, console_height, console_width); // Create the stats window. Stats window contains some static text lines + one line for each connection. stats_window_ptr = newwin(stats_window_height, console_width, 0, 0); // lines, columns, y, x scrollok(stats_window_ptr, false); // Don't want it to scroll. // Create the scrolling log window. log_window_height = console_height - stats_window_height; log_window_ptr = newwin(log_window_height, console_width, stats_window_height, 0); scrollok(log_window_ptr, true); // Want it to scroll automatically. // Allocate buffers to hold the windows. Use +1 to allow for NULL termination for each line. int buf_size = stats_window_height * console_width * (sizeof(chtype) + 1); stats_window_buffer_ptr = CdiOsMemAlloc(buf_size); buf_size = log_window_height * console_width * (sizeof(chtype) + 1); log_window_buffer_ptr = CdiOsMemAlloc(buf_size); } } if (!ret) { TestConsoleDestroy(false); // false= Normal termination } return ret; } void TestConsoleDestroy(bool abnormal_termination) { if (CDI_INVALID_HANDLE_VALUE != pipe_fd_array[1]) { close(pipe_fd_array[1]); // Close the write end of the pipe, to prevent read() from continuing to block. pipe_fd_array[1] = CDI_INVALID_HANDLE_VALUE; close(CDI_STDERR_FILENO); // Done with stderr for now. Will set back to the original value below. } if (console_thread_id) { // Wait for the thread to complete. CdiOsThreadJoin(console_thread_id, CDI_INFINITE, NULL); console_thread_id = NULL; } // Now that the thread has stopped, it is safe to clean up the remaining resources. if (abnormal_termination) { // Set flag to disable all console log API functions to prevent other threads from generating erroneous error // messages. abnormal_termination_enabled = true; // Wait a bit to ensure blocked threads are not waiting in any of the console log API functions. CdiOsSleep(100); } if (stats_window_ptr) { if (stats_window_buffer_ptr) { // Save away the stats window buffer, so we can later send it to stdout. SaveWindowToBuffer(stats_window_ptr, stats_window_buffer_ptr, stats_window_height, console_width); } delwin(stats_window_ptr); stats_window_ptr = NULL; } if (log_window_ptr) { if (log_window_buffer_ptr) { // Save away the log window buffer, so we can later send it to stdout. SaveWindowToBuffer(log_window_ptr, log_window_buffer_ptr, log_window_height, console_width); } delwin(log_window_ptr); log_window_ptr = NULL; } if (screen_initialized) { // Screen was initialized for multi-window mode, so switch it back to the standard console. endwin(); screen_initialized = false; } if (stats_window_buffer_ptr) { // Dump the saved stats window to the standard console so we can still see the info. DumpSavedWindowToStdout(stats_window_buffer_ptr, stats_window_height, console_width); CdiOsMemFree(stats_window_buffer_ptr); stats_window_buffer_ptr = NULL; } if (log_window_buffer_ptr) { // Dump the saved log window to the standard console so we can still see the info. DumpSavedWindowToStdout(log_window_buffer_ptr, log_window_height, console_width); CdiOsMemFree(log_window_buffer_ptr); log_window_buffer_ptr = NULL; } if (CDI_INVALID_HANDLE_VALUE != original_stdout_fd) { #ifndef _WIN32 // Restore the original stderr handler. Only need to do for linux. Causes a segfault in windows. Windows code // for reference: dup2(_fileno(stderr), original_stdout_fd); dup2(CDI_STDERR_FILENO, original_stdout_fd); #endif close(original_stdout_fd); original_stdout_fd = CDI_INVALID_HANDLE_VALUE; } if (log_window_lock) { CdiOsCritSectionDelete(log_window_lock); log_window_lock = NULL; } } void TestConsoleLogMessageCallback(const CdiLogMessageCbData* cb_data_ptr) { if (abnormal_termination_enabled) { return; } if (CdiLoggerIsEnabled(NULL, cb_data_ptr->component, cb_data_ptr->log_level)) { // We need to generate a single log message that contains an optional function name and line number for the // first line. Multiline messages need to have each line separated with a line ending character. This is all // handled by the Multiline API functions, so we will just use them. CdiLogMultilineState m_state; CdiLoggerMultilineBegin(NULL, cb_data_ptr->component, cb_data_ptr->log_level, cb_data_ptr->source_code_function_name_ptr, cb_data_ptr->source_code_line_number, &m_state); // Walk through each line and write to the new single log message buffer. const char* line_str = cb_data_ptr->message_str; for (int i = 0; i < cb_data_ptr->line_count; i++) { CdiLoggerMultiline(&m_state, line_str); line_str += strlen(line_str) + 1; // Advance pointer to byte just past end of the current string. } char* log_str = CdiLoggerMultilineGetBuffer(&m_state); if (log_window_ptr) { CdiOsCritSectionReserve(log_window_lock); // NOTE: Caller must use TestConsoleStatsRefresh() to see the updated text in the window. waddstr(log_window_ptr, log_str); waddstr(log_window_ptr, "\r"); wrefresh(log_window_ptr); CdiOsCritSectionRelease(log_window_lock); } else { printf("%s\r\n", log_str); // send to stdout } CdiLoggerMultilineEnd(&m_state); } } void TestConsoleStats(int x, int y, int attribute, const char* format_str, ...) { if (abnormal_termination_enabled) { return; } va_list vars; va_start(vars, format_str); if (stats_window_ptr) { CdiOsCritSectionReserve(log_window_lock); wmove(stats_window_ptr, y, x); if (A_NORMAL != attribute) { wattron(stats_window_ptr, attribute); } // NOTE: Caller must use TestConsoleStatsRefresh() to see the updated text in the window. vw_printw(stats_window_ptr, format_str, vars); if (A_NORMAL != attribute) { wattroff(stats_window_ptr, attribute); } CdiOsCritSectionRelease(log_window_lock); } else { vprintf_line("", format_str, vars); // send to stdout fflush(stdout); } va_end(vars); } void TestConsoleStatsHorzLine(int x, int y, int width) { if (abnormal_termination_enabled) { return; } if (0 == width) { width = console_width - x; } if (stats_window_ptr) { // NOTE: Caller must use TestConsoleStatsRefresh() to see the updated text in the window. mvwhline(stats_window_ptr, y, x, '-', width); } } void TestConsoleStatsRefresh(void) { if (abnormal_termination_enabled) { return; } if (stats_window_ptr) { CdiOsCritSectionReserve(log_window_lock); wrefresh(stats_window_ptr); CdiOsCritSectionRelease(log_window_lock); } } void TestConsoleLog(CdiLogLevel log_level, const char* format_str, ...) { if (abnormal_termination_enabled) { return; } if (CdiLoggerIsEnabled(NULL, kLogComponentGeneric, log_level)) { va_list vars; va_start(vars, format_str); if (log_window_ptr) { CdiOsCritSectionReserve(log_window_lock); if (kLogError == log_level) { waddstr(log_window_ptr, "ERROR: "); } vw_printw(log_window_ptr, format_str, vars); waddstr(log_window_ptr, "\n\r"); wrefresh(log_window_ptr); // Update the text in the window. CdiOsCritSectionRelease(log_window_lock); } else { const char* prefix_str = (kLogError == log_level) ? "ERROR: " : ""; vprintf_line(prefix_str, format_str, vars); // Send to stdout. fflush(stdout); } va_end(vars); } }