2010-07-01 02:01:05 +00:00
|
|
|
// 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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
#include "cpp.h"
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
#include "boinc_win.h"
|
|
|
|
#else
|
|
|
|
#include "config.h"
|
|
|
|
#include <string>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include "parse.h"
|
|
|
|
#include "url.h"
|
|
|
|
#include "filesys.h"
|
|
|
|
|
|
|
|
#include "client_state.h"
|
|
|
|
#include "client_msgs.h"
|
|
|
|
#include "file_names.h"
|
2012-04-30 21:00:28 +00:00
|
|
|
#include "project.h"
|
2010-07-01 02:01:05 +00:00
|
|
|
|
|
|
|
#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);
|
|
|
|
}
|
|
|
|
|
2010-09-08 18:06:56 +00:00
|
|
|
static void project_feed_list_file_name(PROJ_AM* p, char* buf) {
|
2010-07-01 02:01:05 +00:00
|
|
|
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
|
|
|
|
//
|
2011-09-20 18:49:38 +00:00
|
|
|
int parse_rss_feed_descs(XML_PARSER& xp, vector<RSS_FEED>& feeds) {
|
2010-07-01 02:01:05 +00:00
|
|
|
int retval;
|
2011-08-10 17:11:08 +00:00
|
|
|
while (!xp.get_tag()) {
|
|
|
|
if (!xp.is_tag) continue;
|
|
|
|
if (xp.match_tag("/rss_feeds")) return 0;
|
|
|
|
if (xp.match_tag("rss_feed")) {
|
2010-07-01 02:01:05 +00:00
|
|
|
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 <rss_feed> element"
|
2011-10-18 04:41:13 +00:00
|
|
|
);
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
|
|
|
} 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<RSS_FEED>& feeds) {
|
|
|
|
if (!feeds.size()) return;
|
|
|
|
fout.printf("<rss_feeds>\n");
|
|
|
|
for (unsigned int i=0; i<feeds.size(); i++) {
|
|
|
|
feeds[i].write(fout);
|
|
|
|
}
|
|
|
|
fout.printf("</rss_feeds>\n");
|
|
|
|
}
|
|
|
|
|
2010-09-08 18:06:56 +00:00
|
|
|
static void write_project_feed_list(PROJ_AM* p) {
|
2010-07-01 02:01:05 +00:00
|
|
|
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.
|
|
|
|
//
|
2010-09-08 18:06:56 +00:00
|
|
|
void handle_sr_feeds(vector<RSS_FEED>& feeds, PROJ_AM* p) {
|
2010-07-01 02:01:05 +00:00
|
|
|
unsigned int i, j;
|
|
|
|
bool feed_set_changed = false;
|
|
|
|
|
|
|
|
// mark current feeds as not found
|
|
|
|
//
|
|
|
|
for (i=0; i<p->proj_feeds.size(); i++) {
|
|
|
|
p->proj_feeds[i].found = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (i=0; i<feeds.size(); i++) {
|
|
|
|
RSS_FEED& rf = feeds[i];
|
|
|
|
bool present = false;
|
|
|
|
for (j=0; j<p->proj_feeds.size(); j++) {
|
|
|
|
RSS_FEED& rf2 = p->proj_feeds[j];
|
|
|
|
if (!strcmp(rf.url, rf2.url)) {
|
|
|
|
rf2 = rf;
|
2011-04-16 06:15:10 +00:00
|
|
|
rf2.found = true;
|
2010-07-01 02:01:05 +00:00
|
|
|
present = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!present) {
|
|
|
|
rf.found = true;
|
|
|
|
p->proj_feeds.push_back(rf);
|
|
|
|
feed_set_changed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove ones no longer present
|
|
|
|
//
|
|
|
|
vector<RSS_FEED>::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;
|
|
|
|
}
|
2010-09-21 23:49:21 +00:00
|
|
|
#endif
|
2010-07-01 02:01:05 +00:00
|
|
|
|
|
|
|
// convert a date-time string (assumed GMT) to Unix time
|
|
|
|
|
|
|
|
static int parse_rss_time(char* buf) {
|
2010-09-21 23:49:21 +00:00
|
|
|
#ifdef _WIN32
|
2010-07-01 02:01:05 +00:00
|
|
|
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);
|
2010-09-21 23:49:21 +00:00
|
|
|
#else
|
|
|
|
struct tm tm;
|
2011-08-30 15:36:31 +00:00
|
|
|
memset(&tm, 0, sizeof(tm));
|
2010-09-21 23:49:21 +00:00
|
|
|
strptime(buf, "%a, %d %b %Y %H:%M:%S", &tm);
|
|
|
|
return mktime(&tm);
|
2010-07-01 02:01:05 +00:00
|
|
|
#endif
|
2010-09-21 23:49:21 +00:00
|
|
|
}
|
2010-07-01 02:01:05 +00:00
|
|
|
|
|
|
|
///////////// NOTICE ////////////////
|
|
|
|
|
|
|
|
int NOTICE::parse_rss(XML_PARSER& xp) {
|
2011-08-10 17:11:08 +00:00
|
|
|
char buf[256];
|
2010-07-01 02:01:05 +00:00
|
|
|
|
|
|
|
clear();
|
2011-08-10 17:11:08 +00:00
|
|
|
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))) {
|
2010-07-01 02:01:05 +00:00
|
|
|
create_time = parse_rss_time(buf);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ERR_XML_PARSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////// NOTICES ////////////////
|
|
|
|
|
|
|
|
// called at the start of client initialization
|
|
|
|
//
|
|
|
|
void NOTICES::init() {
|
2010-07-20 21:15:15 +00:00
|
|
|
#if 0
|
2010-07-01 02:01:05 +00:00
|
|
|
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);
|
2010-07-20 21:15:15 +00:00
|
|
|
#endif
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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);
|
2011-09-14 23:22:48 +00:00
|
|
|
size_t n = notices.size();
|
2010-07-01 02:01:05 +00:00
|
|
|
for (unsigned int i=0; i<n; i++) {
|
2011-09-14 23:22:48 +00:00
|
|
|
notices[i].seqno = (int)(n - i);
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-05-25 22:43:07 +00:00
|
|
|
// return true if strings are the same after discarding digits.
|
|
|
|
// This eliminates showing
|
|
|
|
// "you need 25 GB more disk space" and
|
|
|
|
// "you need 24 GB more disk space" as separate notices.
|
|
|
|
//
|
|
|
|
static inline bool string_equal_nodigits(string& s1, string& s2) {
|
|
|
|
const char *p = s1.c_str();
|
|
|
|
const char *q = s2.c_str();
|
|
|
|
while (1) {
|
2011-12-04 19:02:36 +00:00
|
|
|
if (isascii(*p) && isdigit(*p)) {
|
2011-05-25 22:43:07 +00:00
|
|
|
p++;
|
|
|
|
continue;
|
|
|
|
}
|
2011-12-04 19:02:36 +00:00
|
|
|
if (isascii(*q) && isdigit(*q)) {
|
2011-05-25 22:43:07 +00:00
|
|
|
q++;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (!*p || !*q) break;
|
|
|
|
if (*p != *q) return false;
|
|
|
|
p++;
|
|
|
|
q++;
|
|
|
|
}
|
|
|
|
if (*p || *q) return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2010-07-01 02:01:05 +00:00
|
|
|
static inline bool same_text(NOTICE& n1, NOTICE& n2) {
|
|
|
|
if (strcmp(n1.title, n2.title)) return false;
|
2011-05-25 22:43:07 +00:00
|
|
|
if (!string_equal_nodigits(n1.description, n2.description)) return false;
|
2010-07-01 02:01:05 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2010-10-17 04:01:36 +00:00
|
|
|
void NOTICES::clear_keep() {
|
|
|
|
deque<NOTICE>::iterator i = notices.begin();
|
|
|
|
while (i != notices.end()) {
|
|
|
|
NOTICE& n = *i;
|
|
|
|
n.keep = false;
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void NOTICES::unkeep(const char* url) {
|
|
|
|
deque<NOTICE>::iterator i = notices.begin();
|
2010-10-17 18:02:40 +00:00
|
|
|
bool removed_something = false;
|
2010-10-17 04:01:36 +00:00
|
|
|
while (i != notices.end()) {
|
|
|
|
NOTICE& n = *i;
|
|
|
|
if (!strcmp(url, n.feed_url) && !n.keep) {
|
|
|
|
i = notices.erase(i);
|
2010-10-17 18:02:40 +00:00
|
|
|
removed_something = true;
|
2010-10-17 04:01:36 +00:00
|
|
|
} else {
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
}
|
2010-10-18 20:54:33 +00:00
|
|
|
#ifndef SIM
|
|
|
|
if (removed_something) {
|
|
|
|
gstate.gui_rpcs.set_notice_refresh();
|
|
|
|
}
|
|
|
|
#endif
|
2010-10-17 04:01:36 +00:00
|
|
|
}
|
|
|
|
|
2010-09-09 21:37:28 +00:00
|
|
|
static inline bool same_guid(NOTICE& n1, NOTICE& n2) {
|
|
|
|
if (!strlen(n1.guid)) return false;
|
|
|
|
return !strcmp(n1.guid, n2.guid);
|
|
|
|
}
|
|
|
|
|
2010-07-01 02:01:05 +00:00
|
|
|
// we're considering adding a notice n.
|
|
|
|
// If there's already an identical message n2
|
|
|
|
// return false (don't add n)
|
2010-08-26 21:26:52 +00:00
|
|
|
// If there's a message n2 with same title and text,
|
|
|
|
// and n is significantly newer than n2,
|
|
|
|
// delete n2
|
2010-07-01 02:01:05 +00:00
|
|
|
//
|
|
|
|
// Also remove notices older than 30 days
|
|
|
|
//
|
|
|
|
bool NOTICES::remove_dups(NOTICE& n) {
|
|
|
|
deque<NOTICE>::iterator i = notices.begin();
|
|
|
|
bool removed_something = false;
|
|
|
|
bool retval = true;
|
2011-04-04 17:43:36 +00:00
|
|
|
double min_time = gstate.now - 30*86400;
|
2010-07-01 02:01:05 +00:00
|
|
|
while (i != notices.end()) {
|
|
|
|
NOTICE& n2 = *i;
|
2011-04-04 17:43:36 +00:00
|
|
|
if (n2.arrival_time < min_time
|
|
|
|
|| (n2.create_time && n2.create_time < min_time)
|
|
|
|
) {
|
2010-07-01 02:01:05 +00:00
|
|
|
i = notices.erase(i);
|
|
|
|
removed_something = true;
|
2010-09-09 21:37:28 +00:00
|
|
|
} else if (same_guid(n, n2)) {
|
2010-10-17 04:01:36 +00:00
|
|
|
n2.keep = true;
|
2010-09-09 21:37:28 +00:00
|
|
|
return false;
|
2010-07-01 02:01:05 +00:00
|
|
|
} else if (same_text(n, n2)) {
|
2010-08-26 21:26:52 +00:00
|
|
|
int min_diff = 0;
|
2011-01-03 20:09:52 +00:00
|
|
|
|
2011-02-18 02:00:02 +00:00
|
|
|
// show a given scheduler notice at most once a week
|
2011-01-03 20:09:52 +00:00
|
|
|
//
|
2010-08-26 21:26:52 +00:00
|
|
|
if (!strcmp(n.category, "scheduler")) {
|
2011-01-03 20:09:52 +00:00
|
|
|
min_diff = 7*86400;
|
|
|
|
}
|
|
|
|
|
2010-08-26 21:26:52 +00:00
|
|
|
if (n.create_time > n2.create_time + min_diff) {
|
2010-07-01 02:01:05 +00:00
|
|
|
i = notices.erase(i);
|
|
|
|
removed_something = true;
|
|
|
|
} else {
|
2010-10-17 04:01:36 +00:00
|
|
|
n2.keep = true;
|
2010-07-01 02:01:05 +00:00
|
|
|
retval = false;
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
}
|
2010-09-24 20:02:42 +00:00
|
|
|
#ifndef SIM
|
2010-07-01 02:01:05 +00:00
|
|
|
if (removed_something) {
|
|
|
|
gstate.gui_rpcs.set_notice_refresh();
|
|
|
|
}
|
2010-09-24 20:02:42 +00:00
|
|
|
#endif
|
2010-07-01 02:01:05 +00:00
|
|
|
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);
|
2010-07-20 21:15:15 +00:00
|
|
|
#if 0
|
2010-07-01 02:01:05 +00:00
|
|
|
if (!strlen(n.feed_url)) {
|
|
|
|
write_archive(NULL);
|
|
|
|
}
|
2010-07-20 21:15:15 +00:00
|
|
|
#endif
|
2010-07-01 02:01:05 +00:00
|
|
|
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);
|
2011-08-10 17:11:08 +00:00
|
|
|
while (!xp.get_tag()) {
|
|
|
|
if (!xp.is_tag) continue;
|
|
|
|
if (xp.match_tag("/notices")) {
|
2010-07-01 02:01:05 +00:00
|
|
|
fclose(f);
|
|
|
|
return 0;
|
|
|
|
}
|
2011-08-10 17:11:08 +00:00
|
|
|
if (xp.match_tag("notice")) {
|
2010-07-01 02:01:05 +00:00
|
|
|
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) {
|
2012-05-09 16:11:50 +00:00
|
|
|
char path[MAXPATHLEN];
|
2010-07-01 02:01:05 +00:00
|
|
|
|
|
|
|
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("<notices>\n");
|
|
|
|
if (!f) return;
|
|
|
|
for (unsigned int i=0; i<notices.size(); i++) {
|
|
|
|
NOTICE& n = notices[i];
|
|
|
|
if (rfp) {
|
|
|
|
if (strcmp(rfp->url, n.feed_url)) continue;
|
|
|
|
} else {
|
|
|
|
if (strlen(n.feed_url)) continue;
|
|
|
|
}
|
|
|
|
n.write(fout, false);
|
|
|
|
}
|
|
|
|
fout.printf("</notices>\n");
|
|
|
|
fclose(f);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove "need network access" notices
|
|
|
|
//
|
|
|
|
void NOTICES::remove_network_msg() {
|
|
|
|
deque<NOTICE>::iterator i = notices.begin();
|
|
|
|
while (i != notices.end()) {
|
|
|
|
NOTICE& n = *i;
|
|
|
|
if (!strcmp(n.description.c_str(), NEED_NETWORK_MSG)) {
|
|
|
|
i = notices.erase(i);
|
2011-03-14 06:27:51 +00:00
|
|
|
#ifndef SIM
|
2011-02-18 02:00:02 +00:00
|
|
|
gstate.gui_rpcs.set_notice_refresh();
|
2011-03-14 06:27:51 +00:00
|
|
|
#endif
|
2011-02-19 03:32:26 +00:00
|
|
|
if (log_flags.notice_debug) {
|
|
|
|
msg_printf(0, MSG_INFO, "REMOVING NETWORK MESSAGE");
|
|
|
|
}
|
2010-07-01 02:01:05 +00:00
|
|
|
} else {
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// write notices newer than seqno as XML (for GUI RPC).
|
|
|
|
// Write them in order of increasing seqno
|
|
|
|
//
|
2011-09-18 21:06:49 +00:00
|
|
|
void NOTICES::write(int seqno, GUI_RPC_CONN& grc, bool public_only) {
|
2011-09-14 23:22:48 +00:00
|
|
|
size_t i;
|
2011-02-19 03:32:26 +00:00
|
|
|
MIOFILE mf;
|
2010-07-01 02:01:05 +00:00
|
|
|
|
2011-03-03 06:15:20 +00:00
|
|
|
if (!net_status.need_physical_connection) {
|
2010-07-01 02:01:05 +00:00
|
|
|
remove_network_msg();
|
|
|
|
}
|
2011-02-19 03:32:26 +00:00
|
|
|
if (log_flags.notice_debug) {
|
|
|
|
msg_printf(0, MSG_INFO, "NOTICES::write: seqno %d, refresh %s, %d notices",
|
2011-09-18 21:06:49 +00:00
|
|
|
seqno, grc.get_notice_refresh()?"true":"false", (int)notices.size()
|
2011-02-19 03:32:26 +00:00
|
|
|
);
|
|
|
|
}
|
2011-09-18 21:06:49 +00:00
|
|
|
grc.mfout.printf("<notices>\n");
|
|
|
|
if (grc.get_notice_refresh()) {
|
2012-02-20 06:29:46 +00:00
|
|
|
grc.clear_notice_refresh();
|
2010-07-01 02:01:05 +00:00
|
|
|
NOTICE n;
|
|
|
|
n.seqno = -1;
|
|
|
|
seqno = -1;
|
|
|
|
i = notices.size();
|
2011-09-18 21:06:49 +00:00
|
|
|
n.write(grc.mfout, true);
|
2011-02-19 03:32:26 +00:00
|
|
|
if (log_flags.notice_debug) {
|
|
|
|
msg_printf(0, MSG_INFO, "NOTICES::write: sending -1 seqno notice");
|
|
|
|
}
|
2010-07-01 02:01:05 +00:00
|
|
|
} else {
|
|
|
|
for (i=0; i<notices.size(); i++) {
|
|
|
|
NOTICE& n = notices[i];
|
|
|
|
if (n.seqno <= seqno) break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (; i>0; i--) {
|
|
|
|
NOTICE& n = notices[i-1];
|
|
|
|
if (public_only && n.is_private) continue;
|
2011-02-19 03:32:26 +00:00
|
|
|
if (log_flags.notice_debug) {
|
|
|
|
msg_printf(0, MSG_INFO, "NOTICES::write: sending notice %d", n.seqno);
|
|
|
|
}
|
2011-09-18 21:06:49 +00:00
|
|
|
n.write(grc.mfout, true);
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
2011-09-18 21:06:49 +00:00
|
|
|
grc.mfout.printf("</notices>\n");
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
///////////// 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() {
|
2012-05-09 16:11:50 +00:00
|
|
|
char path[MAXPATHLEN];
|
2010-07-01 02:01:05 +00:00
|
|
|
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;
|
2011-08-10 17:11:08 +00:00
|
|
|
while (!xp.get_tag()) {
|
|
|
|
if (!xp.is_tag) continue;
|
|
|
|
if (xp.match_tag("/rss_feed")) {
|
2010-07-01 02:01:05 +00:00
|
|
|
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;
|
|
|
|
}
|
2011-09-14 17:58:53 +00:00
|
|
|
if (xp.parse_str("url", url, sizeof(url))) {
|
|
|
|
xml_unescape(url);
|
2011-09-09 20:13:35 +00:00
|
|
|
}
|
2011-08-10 17:11:08 +00:00
|
|
|
if (xp.parse_double("poll_interval", poll_interval)) continue;
|
|
|
|
if (xp.parse_double("next_poll_time", next_poll_time)) continue;
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
|
|
|
return ERR_XML_PARSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
void RSS_FEED::write(MIOFILE& fout) {
|
2011-09-09 20:13:35 +00:00
|
|
|
char buf[256];
|
|
|
|
strcpy(buf, url);
|
|
|
|
xml_escape(url, buf, sizeof(buf));
|
2010-07-01 02:01:05 +00:00
|
|
|
fout.printf(
|
|
|
|
" <rss_feed>\n"
|
|
|
|
" <url>%s</url>\n"
|
|
|
|
" <poll_interval>%f</poll_interval>\n"
|
|
|
|
" <next_poll_time>%f</next_poll_time>\n"
|
|
|
|
" </rss_feed>\n",
|
2011-09-09 20:13:35 +00:00
|
|
|
buf,
|
2010-07-01 02:01:05 +00:00
|
|
|
poll_interval,
|
2010-10-17 04:01:36 +00:00
|
|
|
next_poll_time
|
2010-07-01 02:01:05 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2011-03-02 19:15:23 +00:00
|
|
|
static inline bool create_time_asc(NOTICE n1, NOTICE n2) {
|
|
|
|
return n1.create_time < n2.create_time;
|
|
|
|
}
|
|
|
|
|
2010-07-01 02:01:05 +00:00
|
|
|
// parse the actual RSS feed.
|
|
|
|
//
|
|
|
|
int RSS_FEED::parse_items(XML_PARSER& xp, int& nitems) {
|
|
|
|
nitems = 0;
|
|
|
|
int ntotal = 0, nerror = 0;
|
2011-02-18 02:00:02 +00:00
|
|
|
int retval, func_ret = ERR_XML_PARSE;
|
2011-03-02 19:15:23 +00:00
|
|
|
vector<NOTICE> new_notices;
|
2010-10-17 04:01:36 +00:00
|
|
|
|
|
|
|
notices.clear_keep();
|
|
|
|
|
2011-08-10 17:11:08 +00:00
|
|
|
while (!xp.get_tag()) {
|
|
|
|
if (!xp.is_tag) continue;
|
|
|
|
if (xp.match_tag("/rss")) {
|
2010-07-01 02:01:05 +00:00
|
|
|
if (log_flags.notice_debug) {
|
|
|
|
msg_printf(0, MSG_INFO,
|
|
|
|
"[notice] parsed RSS feed: total %d error %d added %d",
|
|
|
|
ntotal, nerror, nitems
|
|
|
|
);
|
|
|
|
}
|
2010-10-17 04:01:36 +00:00
|
|
|
func_ret = 0;
|
|
|
|
break;
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
2011-08-10 17:11:08 +00:00
|
|
|
if (xp.match_tag("item")) {
|
2010-07-01 02:01:05 +00:00
|
|
|
NOTICE n;
|
|
|
|
ntotal++;
|
2011-06-12 20:58:43 +00:00
|
|
|
retval = n.parse_rss(xp);
|
2010-07-01 02:01:05 +00:00
|
|
|
if (retval) {
|
|
|
|
nerror++;
|
2010-09-21 23:49:21 +00:00
|
|
|
} 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
|
|
|
|
);
|
|
|
|
}
|
2010-07-01 02:01:05 +00:00
|
|
|
} else {
|
|
|
|
n.arrival_time = gstate.now;
|
2010-10-17 04:01:36 +00:00
|
|
|
n.keep = true;
|
2010-07-01 02:01:05 +00:00
|
|
|
strcpy(n.feed_url, url);
|
2010-09-08 21:13:14 +00:00
|
|
|
strcpy(n.project_name, project_name);
|
2011-03-02 19:15:23 +00:00
|
|
|
new_notices.push_back(n);
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
2011-08-10 17:11:08 +00:00
|
|
|
if (xp.parse_int("error_num", retval)) {
|
2011-02-18 02:00:02 +00:00
|
|
|
if (log_flags.notice_debug) {
|
|
|
|
msg_printf(0,MSG_INFO,
|
|
|
|
"[notice] RSS fetch returned error %d (%s)",
|
|
|
|
retval,
|
|
|
|
boincerror(retval)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return retval;
|
|
|
|
}
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
2011-03-02 19:15:23 +00:00
|
|
|
|
|
|
|
// 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<new_notices.size(); i++) {
|
|
|
|
NOTICE& n = new_notices[i];
|
|
|
|
if (notices.append(n)) {
|
|
|
|
nitems++;
|
|
|
|
}
|
|
|
|
}
|
2010-10-17 04:01:36 +00:00
|
|
|
notices.unkeep(url);
|
|
|
|
return func_ret;
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
///////////// RSS_FEED_OP ////////////////
|
|
|
|
|
|
|
|
RSS_FEED_OP::RSS_FEED_OP() {
|
|
|
|
error_num = BOINC_SUCCESS;
|
|
|
|
gui_http = &gstate.gui_http;
|
|
|
|
}
|
|
|
|
|
|
|
|
// see if time to start new fetch
|
|
|
|
//
|
|
|
|
bool RSS_FEED_OP::poll() {
|
|
|
|
unsigned int i;
|
|
|
|
if (gstate.gui_http.is_busy()) return false;
|
|
|
|
if (gstate.network_suspended) return false;
|
|
|
|
for (i=0; i<rss_feeds.feeds.size(); i++) {
|
|
|
|
RSS_FEED& rf = rss_feeds.feeds[i];
|
|
|
|
if (gstate.now > 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];
|
2010-10-17 04:01:36 +00:00
|
|
|
strcpy(url, rf.url);
|
2010-07-22 19:13:36 +00:00
|
|
|
gstate.gui_http.do_rpc(this, url, filename, true);
|
2010-07-20 23:13:26 +00:00
|
|
|
break;
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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");
|
2011-12-30 06:03:42 +00:00
|
|
|
if (!f) {
|
|
|
|
msg_printf(0, MSG_INTERNAL_ERROR,
|
|
|
|
"RSS feed file '%s' not found", filename
|
|
|
|
);
|
2011-12-30 06:18:57 +00:00
|
|
|
return;
|
2011-12-30 06:03:42 +00:00
|
|
|
}
|
2010-07-01 02:01:05 +00:00
|
|
|
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);
|
|
|
|
|
2010-10-18 21:03:07 +00:00
|
|
|
notices.write_archive(rfp);
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
///////////// RSS_FEEDS ////////////////
|
|
|
|
|
2010-09-08 18:06:56 +00:00
|
|
|
static void init_proj_am(PROJ_AM* p) {
|
|
|
|
FILE* f;
|
|
|
|
MIOFILE fin;
|
2012-05-09 16:11:50 +00:00
|
|
|
char path[MAXPATHLEN];
|
2010-09-08 18:06:56 +00:00
|
|
|
|
|
|
|
project_feed_list_file_name(p, path);
|
|
|
|
f = fopen(path, "r");
|
|
|
|
if (f) {
|
|
|
|
fin.init_file(f);
|
2011-09-20 18:49:38 +00:00
|
|
|
XML_PARSER xp(&fin);
|
|
|
|
parse_rss_feed_descs(xp, p->proj_feeds);
|
2010-09-08 18:06:56 +00:00
|
|
|
fclose(f);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-07-01 02:01:05 +00:00
|
|
|
// called on startup. Get list of feeds. Read archives.
|
|
|
|
//
|
|
|
|
void RSS_FEEDS::init() {
|
|
|
|
unsigned int i;
|
|
|
|
|
|
|
|
boinc_mkdir(NOTICES_DIR);
|
|
|
|
|
|
|
|
for (i=0; i<gstate.projects.size(); i++) {
|
|
|
|
PROJECT* p = gstate.projects[i];
|
2010-09-08 18:06:56 +00:00
|
|
|
init_proj_am(p);
|
|
|
|
}
|
|
|
|
if (gstate.acct_mgr_info.using_am()) {
|
|
|
|
init_proj_am(&gstate.acct_mgr_info);
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
update_feed_list();
|
|
|
|
|
|
|
|
for (i=0; i<feeds.size(); i++) {
|
|
|
|
RSS_FEED& rf = feeds[i];
|
|
|
|
if (log_flags.notice_debug) {
|
|
|
|
msg_printf(0, MSG_INFO,
|
|
|
|
"[notice] feed: %s, %.0f sec",
|
|
|
|
rf.url, rf.poll_interval
|
|
|
|
);
|
|
|
|
}
|
|
|
|
rf.read_archive_file();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
RSS_FEED* RSS_FEEDS::lookup_url(char* url) {
|
|
|
|
for (unsigned int i=0; i<feeds.size(); i++) {
|
|
|
|
RSS_FEED& rf = feeds[i];
|
|
|
|
if (!strcmp(rf.url, url)) {
|
|
|
|
return &rf;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2010-07-22 18:42:15 +00:00
|
|
|
// arrange to fetch the project's feeds
|
|
|
|
//
|
2010-09-08 18:06:56 +00:00
|
|
|
void RSS_FEEDS::trigger_fetch(PROJ_AM* p) {
|
2010-07-22 18:42:15 +00:00
|
|
|
for (unsigned int i=0; i<p->proj_feeds.size(); i++) {
|
|
|
|
RSS_FEED& rf = p->proj_feeds[i];
|
|
|
|
RSS_FEED* rfp = lookup_url(rf.url);
|
|
|
|
if (rfp) {
|
|
|
|
rfp->next_poll_time = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-09-08 18:06:56 +00:00
|
|
|
void RSS_FEEDS::update_proj_am(PROJ_AM* p) {
|
|
|
|
unsigned int j;
|
|
|
|
for (j=0; j<p->proj_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
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-07-01 02:01:05 +00:00
|
|
|
// the set of project feeds has changed.
|
|
|
|
// update the master list.
|
|
|
|
//
|
|
|
|
void RSS_FEEDS::update_feed_list() {
|
2010-09-08 18:06:56 +00:00
|
|
|
unsigned int i;
|
2010-07-01 02:01:05 +00:00
|
|
|
for (i=0; i<feeds.size(); i++) {
|
|
|
|
RSS_FEED& rf = feeds[i];
|
|
|
|
rf.found = false;
|
|
|
|
}
|
|
|
|
for (i=0; i<gstate.projects.size(); i++) {
|
|
|
|
PROJECT* p = gstate.projects[i];
|
2010-09-08 18:06:56 +00:00
|
|
|
update_proj_am(p);
|
|
|
|
}
|
|
|
|
if (gstate.acct_mgr_info.using_am()) {
|
|
|
|
update_proj_am(&gstate.acct_mgr_info);
|
2010-07-01 02:01:05 +00:00
|
|
|
}
|
|
|
|
vector<RSS_FEED>::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)) {
|
2011-03-05 05:49:32 +00:00
|
|
|
if (rss_feed_op.gui_http->is_busy()) {
|
|
|
|
gstate.http_ops->remove(&rss_feed_op.gui_http->http_op);
|
|
|
|
}
|
2010-07-01 02:01:05 +00:00
|
|
|
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);
|
|
|
|
}
|