From dff990081eaf05d9e299c546d9cff9073f4c9623 Mon Sep 17 00:00:00 2001 From: James Shiffer <2191476+scoliono@users.noreply.github.com> Date: Sat, 27 Mar 2021 23:19:07 -0700 Subject: [PATCH] complete booru, howfeed status displays --- FemMonitor/booru.c | 245 +++++++++++++++++++++++-------------------- FemMonitor/booru.h | 3 +- FemMonitor/common.c | 98 +++++++++++++++++ FemMonitor/common.h | 55 ++++++++++ FemMonitor/howfeed.c | 177 +++++++++++++++++++++++++++++++ FemMonitor/howfeed.h | 6 +- FemMonitor/main.c | 35 +++++-- 7 files changed, 489 insertions(+), 130 deletions(-) create mode 100644 FemMonitor/common.c create mode 100644 FemMonitor/common.h diff --git a/FemMonitor/booru.c b/FemMonitor/booru.c index c25bb97..b10af7e 100644 --- a/FemMonitor/booru.c +++ b/FemMonitor/booru.c @@ -9,46 +9,21 @@ #include #include #include -#include #include +#include +#include #include "booru.h" +#include "common.h" -#define START_ROW 2 -#define START_COL 2 - -xmlDoc* doc = NULL; -WINDOW* window = NULL; -CURL* curl = NULL; -int width = 0; -int height = 0; - -struct MemoryStruct -{ - char *memory; - size_t size; -}; - -static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) -{ - size_t realsize = size * nmemb; - struct MemoryStruct *mem = (struct MemoryStruct *)userp; - - char *ptr = realloc(mem->memory, mem->size + realsize + 1); - if (ptr == NULL) - { - /* out of memory! */ - printf("not enough memory (realloc returned NULL)\n"); - return 0; - } - - mem->memory = ptr; - memcpy(&(mem->memory[mem->size]), contents, realsize); - mem->size += realsize; - mem->memory[mem->size] = 0; - - return realsize; -} +static xmlDoc* doc = NULL; +static WINDOW* window = NULL; +static CURL* curl = NULL; +static float* response_times = NULL; +static int iter = 0; +static int width = 0; +static int height = 0; +static struct Graph graph; void booru_init(WINDOW* win, int h, int w) { @@ -56,108 +31,146 @@ void booru_init(WINDOW* win, int h, int w) curl = curl_easy_init(); width = w; height = h; - window = win; - mvwprintw(window, 0, 1, " Fembooru.jp "); + + graph.win = win; + graph.row = START_ROW + 4; + graph.col = START_COL + 1; + graph.width = w - graph.col - 2; + graph.height = h - graph.row - 1; + + mvwprintw(window, 0, START_COL, " Fembooru.jp "); mvwprintw(window, START_ROW, START_COL, "Pinging..."); + mvwprintw(window, graph.row - 1, START_COL, "Response time (x100ms)"); if (curl == NULL) { - wattron(window, COLOR_PAIR(1)); - mvwprintw(window, START_ROW, START_COL, "ERROR"); - mvwprintw(window, START_ROW+1, START_COL, "Failed to initialize libcurl!"); - wattroff(window, COLOR_PAIR(1)); + wattron(window, COLOR_PAIR(COLORS_FAILURE)); + mvwprintw(window, 0, TITLE_START_COL, " ERROR "); + mvwprintw(window, START_ROW, START_COL, "Failed to initialize libcurl!"); + wattroff(window, COLOR_PAIR(COLORS_FAILURE)); curl_global_cleanup(); } + + response_times = malloc(sizeof(float) * graph.width); + + gdrawylabels(graph); wrefresh(window); } void* booru_refresh(void* arg) { - curl_easy_reset(curl); - - struct MemoryStruct chunk; - chunk.memory = malloc(1); /* will be grown as needed by the realloc above */ - chunk.size = 0; /* no data at this point */ - curl_easy_setopt(curl, CURLOPT_URL, "http://fembooru.jp/api/danbooru/find_posts"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk); - - CURLcode res = curl_easy_perform(curl); - int* status = malloc(sizeof(int)); - - // clear status lines - for (int i = START_ROW; i < START_ROW+1; ++i) + for ( ; ; sleep(1)) { - for (int j = START_COL; j < width - 1; ++j) - mvwprintw(window, i, j, " "); - } - - if (res != CURLE_OK) - { - wattron(window, COLOR_PAIR(2)); - if (res == CURLE_COULDNT_CONNECT) + // record the response time + struct timeval start, end; + gettimeofday(&start, NULL); + + CURLcode res; + struct MemoryStruct posts_data = geturl(curl, "http://fembooru.jp/api/danbooru/find_posts", &res); + + if (res != CURLE_OK) { - mvwprintw(window, START_ROW, START_COL, "OFFLINE", res); + on_curl_error(window, res); + goto cleanup; } - else + + struct MemoryStruct tags_data = geturl(curl, "http://fembooru.jp/api/danbooru/find_tags", &res); + + if (res != CURLE_OK) { - mvwprintw(window, START_ROW, START_COL, "ERROR"); - mvwprintw(window, START_ROW+1, START_COL, "CURL status %d", res); + on_curl_error(window, res); + goto cleanup; } - wattroff(window, COLOR_PAIR(2)); + + gettimeofday(&end, NULL); + int index = iter++ % graph.width; + response_times[index] = (end.tv_sec - start.tv_sec) * 1000.0f + (end.tv_usec - start.tv_usec) / 1000.0f; + + // clear status lines + for (int i = TITLE_START_COL - 1; i < TITLE_START_COL + 7; ++i) + mvwprintw(window, 0, i, " "); + + for (int i = START_ROW; i < START_ROW+1; ++i) + { + for (int j = START_COL; j < width - 1; ++j) + mvwprintw(window, i, j, " "); + } + + // clear graph if necessary + if (index == 0) + gclear(graph); + + // print status graph + gdrawbar(graph, index, response_times[index] / 100.0f); + + // get post count + doc = xmlReadMemory(posts_data.memory, posts_data.size, "noname.xml", NULL, 0); + if (doc == NULL) + { + on_xml_error(window); + goto cleanup; + } + + xmlNode* el = xmlDocGetRootElement(doc); + while (el != NULL && (el->type != XML_ELEMENT_NODE || strcmp(el->name, "posts") != 0)) + el = el->next; + + xmlAttr* attr = el == NULL ? NULL : el->properties; + while (attr != NULL && (attr->type != XML_ATTRIBUTE_NODE || strcmp(attr->name, "count") != 0)) + attr = attr->next; + + if (el == NULL || attr == NULL) + { + wattron(window, COLOR_PAIR(COLORS_SUCCESS)); + mvwprintw(window, 0, TITLE_START_COL, " ONLINE "); + wattroff(window, COLOR_PAIR(COLORS_SUCCESS)); + wattron(window, COLOR_PAIR(COLORS_WARNING)); + mvwprintw(window, START_ROW, START_COL, "Couldn't retrieve post count"); + wattroff(window, COLOR_PAIR(COLORS_WARNING)); + wrefresh(window); + goto cleanup; + } + + // we have arrived at the count attribute of the posts tag + wattron(window, COLOR_PAIR(COLORS_SUCCESS)); + mvwprintw(window, 0, TITLE_START_COL, " ONLINE "); + wattroff(window, COLOR_PAIR(COLORS_SUCCESS)); + mvwprintw(window, START_ROW, START_COL, "%s posts", attr->children->content); + + // get first 3 tags + xmlFreeDoc(doc); + doc = xmlReadMemory(tags_data.memory, tags_data.size, "noname.xml", NULL, 0); + if (doc == NULL) + { + on_xml_error(window); + goto cleanup; + } + + el = xmlDocGetRootElement(doc)->children->next; + + mvwprintw(window, START_ROW+1, START_COL, "Most recent tag:"); + // find tag name + attr = el->properties; + while (attr != NULL && (attr->type != XML_ATTRIBUTE_NODE || strcmp(attr->name, "name") != 0)) + attr = attr->next; + wattron(window, WA_BOLD); + mvwprintw(window, START_ROW+1, START_COL+16, " %s", attr->children->content); + wattroff(window, WA_BOLD); + + // refresh view wrefresh(window); - *status = 1; - pthread_exit(status); + + cleanup: + if (doc != NULL) + xmlFreeDoc(doc); + free(posts_data.memory); + free(tags_data.memory); } - - doc = xmlReadMemory(chunk.memory, chunk.size, "noname.xml", NULL, 0); - if (doc == NULL) - { - wattron(window, COLOR_PAIR(2)); - mvwprintw(window, START_ROW, START_COL, "ERROR"); - mvwprintw(window, START_ROW+1, START_COL, "Server gave empty response"); - wattroff(window, COLOR_PAIR(2)); - wrefresh(window); - *status = 2; - pthread_exit(status); - } - - xmlNode* el = xmlDocGetRootElement(doc); - while (el->type != XML_ELEMENT_NODE || strcmp(el->name, "posts") != 0) - el = el->next; - - el = el->children; - while (el->type != XML_ATTRIBUTE_NODE || strcmp(el->name, "count") != 0) - el = el->next; - - if (el == NULL) - { - wattron(window, COLOR_PAIR(1)); - mvwprintw(window, START_ROW, START_COL, "ONLINE"); - wattroff(window, COLOR_PAIR(1)); - wattron(window, COLOR_PAIR(3)); - mvwprintw(window, START_ROW+1, START_COL, "Couldn't retrieve post count"); - wattroff(window, COLOR_PAIR(3)); - wrefresh(window); - *status = 3; - pthread_exit(status); - } - - // we have arrived at the count attribute of the posts tag - wattron(window, COLOR_PAIR(1)); - mvwprintw(window, START_ROW, START_COL, "ONLINE"); - wattroff(window, COLOR_PAIR(1)); - mvwprintw(window, START_ROW+1, START_COL, "%s posts", el->content); - wrefresh(window); - - *status = 0; - pthread_exit(status); } void booru_destroy() { - if (doc != NULL) - xmlFreeDoc(doc); if (curl != NULL) curl_easy_cleanup(curl); + free(response_times); } diff --git a/FemMonitor/booru.h b/FemMonitor/booru.h index b09ee15..77a52c6 100644 --- a/FemMonitor/booru.h +++ b/FemMonitor/booru.h @@ -10,9 +10,8 @@ #define booru_h #include -#include -void booru_init(WINDOW* win, int w, int h); +void booru_init(WINDOW* win, int h, int w); void* booru_refresh(void* arg); /* Note: this function will not free the window! */ void booru_destroy(void); diff --git a/FemMonitor/common.c b/FemMonitor/common.c new file mode 100644 index 0000000..a44cc85 --- /dev/null +++ b/FemMonitor/common.c @@ -0,0 +1,98 @@ +// +// common.c +// FemMonitor +// +// Created by James Shiffer on 3/27/21. +// Copyright © 2021 FemboyFinancial. All rights reserved. +// + +#include +#include + +#include "common.h" + + +static size_t write_mem_callback(void *contents, size_t size, size_t nmemb, void *userp) +{ + size_t realsize = size * nmemb; + struct MemoryStruct *mem = (struct MemoryStruct *)userp; + + char *ptr = realloc(mem->memory, mem->size + realsize + 1); + if (ptr == NULL) + { + /* out of memory! */ + printf("not enough memory (realloc returned NULL)\n"); + return 0; + } + + mem->memory = ptr; + memcpy(&(mem->memory[mem->size]), contents, realsize); + mem->size += realsize; + mem->memory[mem->size] = 0; + + return realsize; +} + +struct MemoryStruct geturl(CURL* curl, char* url, CURLcode* res_out) +{ + curl_easy_reset(curl); + + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_mem_callback); + + struct MemoryStruct data; + data.memory = malloc(1); /* will be grown as needed by the realloc above */ + data.size = 0; /* no data at this point */ + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&data); + + CURLcode res = curl_easy_perform(curl); + if (res_out != NULL) + *res_out = res; + return data; +} + +void gdrawbar(struct Graph graph, int x, int barheight) +{ + wattron(graph.win, COLOR_PAIR(COLORS_GRAPH)); + for (int i = 0; i < graph.height && i < barheight; ++i) + mvwprintw(graph.win, graph.row + graph.height - 1 - i, graph.col + x, "*"); + wattroff(graph.win, COLOR_PAIR(COLORS_GRAPH)); +} + +void gdrawylabels(struct Graph graph) +{ + for (int i = 0; i < graph.height; ++i) + mvwprintw(graph.win, graph.row + i, graph.col - 1, "%1d", graph.height - i); +} + +void gclear(struct Graph graph) +{ + for (int i = 0; i < graph.width; ++i) + for (int j = 0; j < graph.height; ++j) + mvwprintw(graph.win, graph.row + j, graph.col + i, " "); +} + +void on_curl_error(WINDOW* window, CURLcode res) +{ + wattron(window, COLOR_PAIR(COLORS_FAILURE)); + if (res == CURLE_COULDNT_CONNECT) + { + mvwprintw(window, 0, TITLE_START_COL, " OFFLINE ", res); + } + else + { + mvwprintw(window, 0, TITLE_START_COL, " ERROR "); + mvwprintw(window, START_ROW, START_COL, "CURL status %d", res); + } + wattroff(window, COLOR_PAIR(COLORS_FAILURE)); + wrefresh(window); +} + +void on_xml_error(WINDOW* window) +{ + wattron(window, COLOR_PAIR(COLORS_FAILURE)); + mvwprintw(window, 0, TITLE_START_COL, " ERROR "); + mvwprintw(window, START_ROW, START_COL, "Server gave empty response"); + wattroff(window, COLOR_PAIR(COLORS_FAILURE)); + wrefresh(window); +} diff --git a/FemMonitor/common.h b/FemMonitor/common.h new file mode 100644 index 0000000..646352b --- /dev/null +++ b/FemMonitor/common.h @@ -0,0 +1,55 @@ +// +// common.h +// FemMonitor +// +// Created by James Shiffer on 3/27/21. +// Copyright © 2021 FemboyFinancial. All rights reserved. +// + +#ifndef common_h +#define common_h + +#include +#include + + +// GRAPHICS // + +#define START_ROW 2 +#define START_COL 2 +#define TITLE_START_COL 15 +#define COLORS_SUCCESS 1 +#define COLORS_FAILURE 2 +#define COLORS_WARNING 3 +#define COLORS_GRAPH 4 + +struct Graph +{ + WINDOW* win; + int row; + int col; + int width; + int height; +}; + +void gclear(struct Graph graph); +void gdrawbar(struct Graph graph, int x, int barheight); +void gdrawylabels(struct Graph graph); + + +// DOWNLOADS // + +struct MemoryStruct +{ + char *memory; + size_t size; +}; + +/* You must free the returned memory yourself */ +struct MemoryStruct geturl(CURL* curl, char* url, CURLcode* res_out); + +void on_curl_error(WINDOW* window, CURLcode res); +void on_xml_error(WINDOW* window); + + +#endif /* common_h */ diff --git a/FemMonitor/howfeed.c b/FemMonitor/howfeed.c index a041595..80288f4 100644 --- a/FemMonitor/howfeed.c +++ b/FemMonitor/howfeed.c @@ -6,4 +6,181 @@ // Copyright © 2021 FemboyFinancial. All rights reserved. // +#include +#include +#include +#include +#include +#include + +#include "common.h" #include "howfeed.h" + +static xmlDoc* doc = NULL; +static WINDOW* window = NULL; +static CURL* curl = NULL; +static struct Graph graph; +static float* response_times; +static int width; +static int height; + +void howfeed_init(WINDOW* win, int h, int w) +{ + curl = curl_easy_init(); + window = win; + width = w; + height = h; + + graph.win = win; + graph.row = START_ROW + 5; + graph.col = START_COL + 1; + graph.width = w - graph.col - 2; + graph.height = h - graph.row - 1; + + mvwprintw(window, 0, START_COL, " Howfeed.biz "); + mvwprintw(window, START_ROW, START_COL, "Pinging..."); + mvwprintw(window, graph.row - 1, START_COL, "Response time (x100ms)"); + if (curl == NULL) + { + wattron(window, COLOR_PAIR(COLORS_FAILURE)); + mvwprintw(window, 0, TITLE_START_COL, " ERROR "); + mvwprintw(window, START_ROW, START_COL, "Failed to initialize libcurl!"); + wattroff(window, COLOR_PAIR(COLORS_FAILURE)); + curl_global_cleanup(); + } + + response_times = malloc(sizeof(float) * graph.width); + + gdrawylabels(graph); + wrefresh(window); +} + +void* howfeed_refresh(void* arg) +{ + int iter = 0; + for ( ; ; sleep(1)) + { + // record the response time + struct timeval start, end; + gettimeofday(&start, NULL); + + CURLcode res; + struct MemoryStruct article_data = geturl(curl, "http://howfeed.biz/rss.xml", &res); + + if (res != CURLE_OK) + { + on_curl_error(window, res); + goto cleanup; + } + + gettimeofday(&end, NULL); + int index = iter++ % graph.width; + response_times[index] = (end.tv_sec - start.tv_sec) * 1000.0f + (end.tv_usec - start.tv_usec) / 1000.0f; + + // clear status lines + for (int i = TITLE_START_COL - 1; i < TITLE_START_COL + 7; ++i) + mvwprintw(window, 0, i, " "); + + for (int i = START_ROW; i < START_ROW+2; ++i) + { + for (int j = START_COL; j < width - 1; ++j) + mvwprintw(window, i, j, " "); + } + + // clear graph if necessary + if (index == 0) + gclear(graph); + + // print status graph + gdrawbar(graph, index, response_times[index] / 100.0f); + + // get article count + doc = xmlReadMemory(article_data.memory, article_data.size, "noname.xml", NULL, 0); + if (doc == NULL) + { + on_xml_error(window); + goto cleanup; + } + + xmlNode* el = xmlDocGetRootElement(doc)->children->children; + while (el != NULL && (el->type != XML_ELEMENT_NODE || strcmp(el->name, "item") != 0)) + el = el->next; + + int articles_count = 0; + while (el != NULL && el->next != NULL && el->next->type == XML_ELEMENT_NODE && strcmp(el->next->name, "item") == 0) + { + ++articles_count; + el = el->next; + } + ++articles_count; + + if (el != NULL) + el = el->children; + + while (el != NULL && (el->type != XML_ELEMENT_NODE || strcmp(el->name, "title") != 0)) + el = el->next; + + char* title; + if (el != NULL) + title = el->children->content; + + while (el != NULL && (el->type != XML_ELEMENT_NODE || strcmp(el->name, "pubDate") != 0)) + el = el->next; + + struct tm pubdate; + if (el != NULL) + strptime(el->children->content, "%a, %d %b %Y %H:%M:%S %Z", &pubdate); + + wattron(window, COLOR_PAIR(COLORS_SUCCESS)); + mvwprintw(window, 0, TITLE_START_COL, " ONLINE "); + wattroff(window, COLOR_PAIR(COLORS_SUCCESS)); + + if (el == NULL) + { + wattron(window, COLOR_PAIR(COLORS_WARNING)); + mvwprintw(window, START_ROW, START_COL, "Failed to get latest article"); + wattroff(window, COLOR_PAIR(COLORS_WARNING)); + } + else + { + mvwprintw(window, START_ROW, START_COL, "%d articles", articles_count); + mvwprintw(window, START_ROW+1, START_COL, "Latest article:"); + wattron(window, WA_BOLD); + mvwprintw(window, START_ROW+1, START_COL+15, " %s", title); + wattroff(window, WA_BOLD); + + double days_elapsed = difftime(time(NULL), timelocal(&pubdate)) / (3600.0 * 24.0); + + int colorpair; + if (days_elapsed < 7) + colorpair = COLORS_SUCCESS; + else if (days_elapsed < 14) + colorpair = COLORS_WARNING; + else + colorpair = COLORS_FAILURE; + + wattron(window, WA_BOLD); + wattron(window, COLOR_PAIR(colorpair)); + mvwprintw(window, START_ROW+2, START_COL, "%.0f days", days_elapsed); + wattroff(window, COLOR_PAIR(colorpair)); + wattroff(window, WA_BOLD); + + mvwprintw(window, START_ROW+2, START_COL+8, " since last article"); + } + + // refresh view + wrefresh(window); + + cleanup: + if (doc != NULL) + xmlFreeDoc(doc); + free(article_data.memory); + } +} + +void howfeed_destroy(void) +{ + if (curl != NULL) + curl_easy_cleanup(curl); + free(response_times); +} diff --git a/FemMonitor/howfeed.h b/FemMonitor/howfeed.h index ca331ea..7fe2d9f 100644 --- a/FemMonitor/howfeed.h +++ b/FemMonitor/howfeed.h @@ -9,7 +9,11 @@ #ifndef howfeed_h #define howfeed_h -#include +#include +void howfeed_init(WINDOW* win, int h, int w); +void* howfeed_refresh(void* arg); +/* Note: this function will not free the window! */ +void howfeed_destroy(void); #endif /* howfeed_h */ diff --git a/FemMonitor/main.c b/FemMonitor/main.c index 34f30fd..6c10c69 100644 --- a/FemMonitor/main.c +++ b/FemMonitor/main.c @@ -6,13 +6,15 @@ // Copyright © 2021 FemboyFinancial. All rights reserved. // -#include +#include #include #include +#include #include #include #include "booru.h" +#include "common.h" #include "company.h" #include "howfeed.h" #include "wiki.h" @@ -37,7 +39,7 @@ void destroy_win(WINDOW* local_win) * result of erasing the window. It will leave its four corners, * an ugly remnant of the window. */ - wborder(local_win, ' ', ' ', ' ',' ',' ',' ',' ',' '); + wborder(local_win, ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '); wrefresh(local_win); delwin(local_win); } @@ -56,33 +58,36 @@ int main(int argc, const char* argv[]) refresh(); start_color(); - init_pair(1, COLOR_GREEN, COLOR_BLACK); - init_pair(2, COLOR_RED, COLOR_BLACK); - init_pair(3, COLOR_YELLOW, COLOR_BLACK); + init_pair(COLORS_SUCCESS, COLOR_GREEN, COLOR_BLACK); + init_pair(COLORS_FAILURE, COLOR_RED, COLOR_BLACK); + init_pair(COLORS_WARNING, COLOR_YELLOW, COLOR_BLACK); + init_pair(COLORS_GRAPH, COLOR_CYAN, COLOR_BLACK); // setup various status windows + // the bottom windows will be one row shorter to fit the status line at the end WINDOW* topleft = create_newwin(LINES/2, COLS/2, 0, 0); WINDOW* topright = create_newwin(LINES/2, COLS/2, 0, COLS/2); WINDOW* botleft = create_newwin(LINES/2 - 1, COLS/2, LINES/2, 0); WINDOW* botright = create_newwin(LINES/2 - 1, COLS/2, LINES/2, COLS/2); + company_init(topleft, LINES/2, COLS/2); + howfeed_init(topright, LINES/2, COLS/2); booru_init(botleft, LINES/2 - 1, COLS/2); - mvwprintw(topleft, 0, 1, " Company "); - mvwprintw(topright, 0, 1, " Howfeed.biz "); mvwprintw(botright, 0, 1, " FemWiki "); - - wrefresh(topleft); - wrefresh(topright); wrefresh(botright); // start refresh status threads pthread_t booru_th; pthread_create(&booru_th, NULL, booru_refresh, NULL); + pthread_t howfeed_th; + pthread_create(&howfeed_th, NULL, howfeed_refresh, NULL); + pthread_t company_th; + pthread_create(&company_th, NULL, company_refresh, NULL); time_t t; struct tm tm; char ch; - while ((ch = getch()) != KEY_QUIT) + while ((ch = tolower(getch())) != KEY_QUIT) { time(&t); tm = *localtime(&t); @@ -93,12 +98,20 @@ int main(int argc, const char* argv[]) tm.tm_hour, tm.tm_min, tm.tm_sec); + mvprintw(LINES - 1, COLS - 15, "Press Q to quit"); wrefresh(stdscr); } // cleanup everything + pthread_cancel(booru_th); booru_destroy(); + pthread_cancel(howfeed_th); + howfeed_destroy(); + + pthread_cancel(company_th); + company_destroy(); + destroy_win(topleft); destroy_win(topright); destroy_win(botleft);