// This file is part of BOINC. // http://boinc.berkeley.edu // Copyright (C) 2009 University of California // // BOINC is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License // as published by the Free Software Foundation, // either version 3 of the License, or (at your option) any later version. // // BOINC is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // See the GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with BOINC. If not, see . #include "cpp.h" #ifdef _WIN32 #include "boinc_win.h" #else #include "config.h" #include #endif #include "parse.h" #include "url.h" #include "filesys.h" #include "client_state.h" #include "client_msgs.h" #include "file_names.h" #include "project.h" #include "cs_notice.h" using std::vector; using std::string; using std::deque; NOTICES notices; RSS_FEEDS rss_feeds; RSS_FEED_OP rss_feed_op; ////////////// UTILITY FUNCTIONS /////////////// static bool cmp(NOTICE n1, NOTICE n2) { if (n1.arrival_time > n2.arrival_time) return true; if (n1.arrival_time < n2.arrival_time) return false; return (strcmp(n1.guid, n2.guid) > 0); } static void project_feed_list_file_name(PROJ_AM* p, char* buf) { char url[256]; escape_project_url(p->master_url, url); sprintf(buf, "notices/feeds_%s.xml", url); } // parse feed descs from scheduler reply or feed list file // int parse_rss_feed_descs(XML_PARSER& xp, vector& feeds) { int retval; while (!xp.get_tag()) { if (!xp.is_tag) continue; if (xp.match_tag("/rss_feeds")) return 0; if (xp.match_tag("rss_feed")) { RSS_FEED rf; retval = rf.parse_desc(xp); if (retval) { if (log_flags.sched_op_debug) { msg_printf(0, MSG_INFO, "[sched_op] error in element" ); } } else { feeds.push_back(rf); } } } return ERR_XML_PARSE; } // write a list of feeds to a file // static void write_rss_feed_descs(MIOFILE& fout, vector& feeds) { if (!feeds.size()) return; fout.printf("\n"); for (unsigned int i=0; i\n"); } static void write_project_feed_list(PROJ_AM* p) { char buf[256]; project_feed_list_file_name(p, buf); FILE* f = fopen(buf, "w"); if (!f) return; MIOFILE fout; fout.init_file(f); write_rss_feed_descs(fout, p->proj_feeds); fclose(f); } // A scheduler RPC returned a list (possibly empty) of feeds. // Add new ones to the project's set, // and remove ones from the project's set that aren't in the list. // void handle_sr_feeds(vector& feeds, PROJ_AM* p) { unsigned int i, j; bool feed_set_changed = false; // mark current feeds as not found // for (i=0; iproj_feeds.size(); i++) { p->proj_feeds[i].found = false; } for (i=0; iproj_feeds.size(); j++) { RSS_FEED& rf2 = p->proj_feeds[j]; if (!strcmp(rf.url, rf2.url)) { rf2 = rf; rf2.found = true; present = true; break; } } if (!present) { rf.found = true; p->proj_feeds.push_back(rf); feed_set_changed = true; } } // remove ones no longer present // vector::iterator iter = p->proj_feeds.begin(); while (iter != p->proj_feeds.end()) { RSS_FEED& rf = *iter; if (rf.found) { iter++; } else { iter = p->proj_feeds.erase(iter); feed_set_changed = true; } } // if anything was added or removed, update master set // if (feed_set_changed) { write_project_feed_list(p); rss_feeds.update_feed_list(); } } #ifdef _WIN32 // compensate for lameness static int month_index(char* x) { if (strstr(x, "Jan")) return 0; if (strstr(x, "Feb")) return 1; if (strstr(x, "Mar")) return 2; if (strstr(x, "Apr")) return 3; if (strstr(x, "May")) return 4; if (strstr(x, "Jun")) return 5; if (strstr(x, "Jul")) return 6; if (strstr(x, "Aug")) return 7; if (strstr(x, "Sep")) return 8; if (strstr(x, "Oct")) return 9; if (strstr(x, "Nov")) return 10; if (strstr(x, "Dec")) return 11; return 0; } #endif // convert a date-time string (assumed GMT) to Unix time static int parse_rss_time(char* buf) { #ifdef _WIN32 char day_name[64], month_name[64]; int day_num, year, h, m, s; sscanf(buf, "%s %d %s %d %d:%d:%d", day_name, &day_num, month_name, &year, &h, &m, &s ); struct tm tm; tm.tm_sec = s; tm.tm_min = m; tm.tm_hour = h; tm.tm_mday = day_num; tm.tm_mon = month_index(month_name); tm.tm_year = year-1900; tm.tm_wday = 0; tm.tm_yday = 0; tm.tm_isdst = 0; return (int)mktime(&tm); #else struct tm tm; memset(&tm, 0, sizeof(tm)); strptime(buf, "%a, %d %b %Y %H:%M:%S", &tm); return mktime(&tm); #endif } ///////////// NOTICE //////////////// int NOTICE::parse_rss(XML_PARSER& xp) { char buf[256]; clear(); while (!xp.get_tag()) { if (!xp.is_tag) continue; if (xp.match_tag("/item")) return 0; if (xp.parse_str("title", title, sizeof(title))) continue; if (xp.parse_str("link", link, sizeof(link))) continue; if (xp.parse_str("guid", guid, sizeof(guid))) continue; if (xp.parse_string("description", description)) continue; if (xp.parse_str("pubDate", buf, sizeof(buf))) { create_time = parse_rss_time(buf); continue; } } return ERR_XML_PARSE; } ///////////// NOTICES //////////////// // called at the start of client initialization // void NOTICES::init() { #if 0 read_archive_file(NOTICES_DIR"/archive.xml", NULL); if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "read %d BOINC notices", (int)notices.size()); } write_archive(NULL); #endif } // called at the end of client initialization // void NOTICES::init_rss() { rss_feeds.init(); if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "read %d total notices", (int)notices.size()); } // sort by decreasing arrival time, then assign seqnos // sort(notices.begin(), notices.end(), cmp); size_t n = notices.size(); for (unsigned int i=0; i::iterator i = notices.begin(); while (i != notices.end()) { NOTICE& n = *i; n.keep = false; i++; } } void NOTICES::unkeep(const char* url) { deque::iterator i = notices.begin(); bool removed_something = false; while (i != notices.end()) { NOTICE& n = *i; if (!strcmp(url, n.feed_url) && !n.keep) { i = notices.erase(i); removed_something = true; } else { i++; } } #ifndef SIM if (removed_something) { gstate.gui_rpcs.set_notice_refresh(); } #endif } static inline bool same_guid(NOTICE& n1, NOTICE& n2) { if (!strlen(n1.guid)) return false; return !strcmp(n1.guid, n2.guid); } // we're considering adding a notice n. // If there's already an identical message n2 // return false (don't add n) // If there's a message n2 with same title and text, // and n is significantly newer than n2, // delete n2 // // Also remove notices older than 30 days // bool NOTICES::remove_dups(NOTICE& n) { deque::iterator i = notices.begin(); bool removed_something = false; bool retval = true; double min_time = gstate.now - 30*86400; while (i != notices.end()) { NOTICE& n2 = *i; if (n2.arrival_time < min_time || (n2.create_time && n2.create_time < min_time) ) { i = notices.erase(i); removed_something = true; } else if (same_guid(n, n2)) { n2.keep = true; return false; } else if (same_text(n, n2)) { int min_diff = 0; // show a given scheduler notice at most once a week // if (!strcmp(n.category, "scheduler")) { min_diff = 7*86400; } if (n.create_time > n2.create_time + min_diff) { i = notices.erase(i); removed_something = true; } else { n2.keep = true; retval = false; ++i; } } else { ++i; } } #ifndef SIM if (removed_something) { gstate.gui_rpcs.set_notice_refresh(); } #endif return retval; } // add a notice. // bool NOTICES::append(NOTICE& n) { if (!remove_dups(n)) { return false; } if (notices.empty()) { n.seqno = 1; } else { n.seqno = notices.front().seqno + 1; } if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "[notice] appending notice %d: %s", n.seqno, strlen(n.title)?n.title:n.description.c_str() ); } notices.push_front(n); #if 0 if (!strlen(n.feed_url)) { write_archive(NULL); } #endif return true; } // read and parse the contents of an archive file. // If rfp is NULL it's a system msg, else a feed msg. // insert items in NOTICES // int NOTICES::read_archive_file(const char* path, RSS_FEED* rfp) { FILE* f = fopen(path, "r"); if (!f) { if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "[notice] no archive file %s", path ); } return 0; } MIOFILE fin; fin.init_file(f); XML_PARSER xp(&fin); while (!xp.get_tag()) { if (!xp.is_tag) continue; if (xp.match_tag("/notices")) { fclose(f); return 0; } if (xp.match_tag("notice")) { NOTICE n; int retval = n.parse(xp); if (retval) { if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "[notice] archive item parse error: %d", retval ); } } else { if (rfp) { strcpy(n.feed_url, rfp->url); strcpy(n.project_name, rfp->project_name); } append(n); } } } if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "[notice] archive parse error"); } fclose(f); return ERR_XML_PARSE; } // write archive file for the given RSS feed // (or, if NULL, non-RSS notices) // void NOTICES::write_archive(RSS_FEED* rfp) { char path[MAXPATHLEN]; if (rfp) { rfp->archive_file_name(path); } else { strcpy(path, NOTICES_DIR"/archive.xml"); } FILE* f = fopen(path, "w"); if (!f) return; MIOFILE fout; fout.init_file(f); fout.printf("\n"); if (!f) return; for (unsigned int i=0; iurl, n.feed_url)) continue; } else { if (strlen(n.feed_url)) continue; } n.write(fout, false); } fout.printf("\n"); fclose(f); } // Remove "need network access" notices // void NOTICES::remove_network_msg() { deque::iterator i = notices.begin(); while (i != notices.end()) { NOTICE& n = *i; if (!strcmp(n.description.c_str(), NEED_NETWORK_MSG)) { i = notices.erase(i); #ifndef SIM gstate.gui_rpcs.set_notice_refresh(); #endif if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "REMOVING NETWORK MESSAGE"); } } else { ++i; } } } // write notices newer than seqno as XML (for GUI RPC). // Write them in order of increasing seqno // void NOTICES::write(int seqno, GUI_RPC_CONN& grc, bool public_only) { size_t i; MIOFILE mf; if (!net_status.need_physical_connection) { remove_network_msg(); } if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "NOTICES::write: seqno %d, refresh %s, %d notices", seqno, grc.get_notice_refresh()?"true":"false", (int)notices.size() ); } grc.mfout.printf("\n"); if (grc.get_notice_refresh()) { grc.clear_notice_refresh(); NOTICE n; n.seqno = -1; seqno = -1; i = notices.size(); n.write(grc.mfout, true); if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "NOTICES::write: sending -1 seqno notice"); } } else { for (i=0; i0; i--) { NOTICE& n = notices[i-1]; if (public_only && n.is_private) continue; if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "NOTICES::write: sending notice %d", n.seqno); } n.write(grc.mfout, true); } grc.mfout.printf("\n"); } ///////////// RSS_FEED //////////////// void RSS_FEED::feed_file_name(char* path) { char buf[256]; escape_project_url(url_base, buf); sprintf(path, NOTICES_DIR"/%s.xml", buf); } void RSS_FEED::archive_file_name(char* path) { char buf[256]; escape_project_url(url_base, buf); sprintf(path, NOTICES_DIR"/archive_%s.xml", buf); } // read and parse the contents of the archive file; // insert items in NOTICES // int RSS_FEED::read_archive_file() { char path[MAXPATHLEN]; archive_file_name(path); return notices.read_archive_file(path, this); } // parse a feed descriptor (in scheduler reply or feed list file) // int RSS_FEED::parse_desc(XML_PARSER& xp) { strcpy(url, ""); poll_interval = 0; next_poll_time = 0; while (!xp.get_tag()) { if (!xp.is_tag) continue; if (xp.match_tag("/rss_feed")) { if (!poll_interval || !strlen(url)) { if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "[notice] URL or poll interval missing in sched reply feed" ); } return ERR_XML_PARSE; } strcpy(url_base, url); char* p = strchr(url_base, '?'); if (p) *p = 0; return 0; } if (xp.parse_str("url", url, sizeof(url))) { xml_unescape(url); } if (xp.parse_double("poll_interval", poll_interval)) continue; if (xp.parse_double("next_poll_time", next_poll_time)) continue; } return ERR_XML_PARSE; } void RSS_FEED::write(MIOFILE& fout) { char buf[256]; strcpy(buf, url); xml_escape(url, buf, sizeof(buf)); fout.printf( " \n" " %s\n" " %f\n" " %f\n" " \n", buf, poll_interval, next_poll_time ); } static inline bool create_time_asc(NOTICE n1, NOTICE n2) { return n1.create_time < n2.create_time; } // parse the actual RSS feed. // int RSS_FEED::parse_items(XML_PARSER& xp, int& nitems) { nitems = 0; int ntotal = 0, nerror = 0; int retval, func_ret = ERR_XML_PARSE; vector new_notices; notices.clear_keep(); while (!xp.get_tag()) { if (!xp.is_tag) continue; if (xp.match_tag("/rss")) { if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "[notice] parsed RSS feed: total %d error %d added %d", ntotal, nerror, nitems ); } func_ret = 0; break; } if (xp.match_tag("item")) { NOTICE n; ntotal++; retval = n.parse_rss(xp); if (retval) { nerror++; } else if (n.create_time < gstate.now - 30*86400) { if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "[notice] item is older than 30 days: %s", n.title ); } } else { n.arrival_time = gstate.now; n.keep = true; strcpy(n.feed_url, url); strcpy(n.project_name, project_name); new_notices.push_back(n); } continue; } if (xp.parse_int("error_num", retval)) { if (log_flags.notice_debug) { msg_printf(0,MSG_INFO, "[notice] RSS fetch returned error %d (%s)", retval, boincerror(retval) ); } return retval; } } // sort new notices by increasing create time, and append them // std::sort(new_notices.begin(), new_notices.end(), create_time_asc); for (unsigned int i=0; i rf.next_poll_time) { rf.next_poll_time = gstate.now + rf.poll_interval; char filename[256]; rf.feed_file_name(filename); rfp = &rf; if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "[notice] start fetch from %s", rf.url ); } char url[256]; strcpy(url, rf.url); gstate.gui_http.do_rpc(this, url, filename, true); break; } } return false; } // handle a completed RSS feed fetch // void RSS_FEED_OP::handle_reply(int http_op_retval) { char filename[256]; int nitems; if (!rfp) return; // op was canceled if (http_op_retval) { if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "[notice] fetch of %s failed: %d", rfp->url, http_op_retval ); } return; } if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "[notice] handling reply from %s", rfp->url ); } rfp->feed_file_name(filename); FILE* f = fopen(filename, "r"); if (!f) { msg_printf(0, MSG_INTERNAL_ERROR, "RSS feed file '%s' not found", filename ); return; } MIOFILE fin; fin.init_file(f); XML_PARSER xp(&fin); int retval = rfp->parse_items(xp, nitems); if (retval) { if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "[notice] RSS parse error: %d", retval ); } } fclose(f); notices.write_archive(rfp); } ///////////// RSS_FEEDS //////////////// static void init_proj_am(PROJ_AM* p) { FILE* f; MIOFILE fin; char path[MAXPATHLEN]; project_feed_list_file_name(p, path); f = fopen(path, "r"); if (f) { fin.init_file(f); XML_PARSER xp(&fin); parse_rss_feed_descs(xp, p->proj_feeds); fclose(f); } } // called on startup. Get list of feeds. Read archives. // void RSS_FEEDS::init() { unsigned int i; boinc_mkdir(NOTICES_DIR); for (i=0; iproj_feeds.size(); i++) { RSS_FEED& rf = p->proj_feeds[i]; RSS_FEED* rfp = lookup_url(rf.url); if (rfp) { rfp->next_poll_time = 0; } } } void RSS_FEEDS::update_proj_am(PROJ_AM* p) { unsigned int j; for (j=0; jproj_feeds.size(); j++) { RSS_FEED& rf = p->proj_feeds[j]; RSS_FEED* rfp = lookup_url(rf.url); if (rfp) { rfp->found = true; } else { rf.found = true; strcpy(rf.project_name, p->get_project_name()); feeds.push_back(rf); if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "[notice] adding feed: %s, %.0f sec", rf.url, rf.poll_interval ); } } } } // the set of project feeds has changed. // update the master list. // void RSS_FEEDS::update_feed_list() { unsigned int i; for (i=0; i::iterator iter = feeds.begin(); while (iter != feeds.end()) { RSS_FEED& rf = *iter; if (rf.found) { iter++; } else { // cancel op if active // if (rss_feed_op.rfp == &(*iter)) { if (rss_feed_op.gui_http->is_busy()) { gstate.http_ops->remove(&rss_feed_op.gui_http->http_op); } rss_feed_op.rfp = NULL; } if (log_flags.notice_debug) { msg_printf(0, MSG_INFO, "[notice] removing feed: %s", rf.url ); } iter = feeds.erase(iter); } } write_feed_list(); } void RSS_FEEDS::write_feed_list() { FILE* f = fopen(NOTICES_DIR"/feeds.xml", "w"); if (!f) return; MIOFILE fout; fout.init_file(f); write_rss_feed_descs(fout, feeds); fclose(f); }