garbage collect app versions; bandwidth limit interface

svn path=/trunk/boinc/; revision=997
This commit is contained in:
David Anderson 2003-03-02 19:24:09 +00:00
parent 1a29d7d11b
commit 76a3468b54
11 changed files with 148 additions and 142 deletions

View File

@ -3569,3 +3569,23 @@ David Feb 28 2003
client/
client_types.C,h
David Mar 2 2003
- added code to garbage-collect APP_VERSIONs:
delete any APP_VERSION that's not referenced and for which
there's a later version of the same app.
Also garbage-collect executable files
(previously, they were never deleted).
- fixed minor bug (file offset on redirected POST2 HTTP op)
- added network bandwidth limits to global prefs web interface
todo
client/
client_state.C,h
cs_files.C
hostinfo_unix.C
http.C,h
html_user/
prefs.inc
sched/
handle_request.C

View File

@ -575,9 +575,7 @@ int CLIENT_STATE::parse_state_file() {
// could put logic here to detect incompatible state files
// after core client update
} else if (match_tag(buf, "<core_client_major_version>")) {
// TODO: handle old client state file if different version
} else if (match_tag(buf, "<core_client_minor_version>")) {
// TODO: handle old client state file if different version
} else if (match_tag(buf, "<confirm_before_connect/>")) {
global_prefs.confirm_before_connecting = true;
} else if (match_tag(buf, "<hangup_if_dialed/>")) {
@ -590,8 +588,6 @@ int CLIENT_STATE::parse_state_file() {
} else if (parse_str(buf, "<socks_user_passwd>", socks_user_passwd, sizeof(socks_user_passwd))) {
} else {
fprintf(stderr, "CLIENT_STATE::parse_state_file: unrecognized: %s\n", buf);
retval = ERR_XML_PARSE;
goto done;
}
}
done:
@ -678,8 +674,8 @@ int CLIENT_STATE::write_state_file() {
return 0;
}
// TODO: write no more often than X seconds
// Write the client_state.xml file if necessary
// TODO: write no more often than X seconds
//
int CLIENT_STATE::write_state_file_if_needed() {
int retval;
@ -937,18 +933,20 @@ void CLIENT_STATE::print_summary() {
// delete unneeded records and files
//
bool CLIENT_STATE::garbage_collect() {
unsigned int i;
unsigned int i, j;
int failnum;
FILE_INFO* fip;
RESULT* rp;
WORKUNIT* wup;
APP_VERSION* avp, *avp2;
vector<RESULT*>::iterator result_iter;
vector<WORKUNIT*>::iterator wu_iter;
vector<FILE_INFO*>::iterator fi_iter;
bool action = false;
vector<APP_VERSION*>::iterator avp_iter;
bool action = false, found;
// zero references counts on WUs and FILE_INFOs
// zero references counts on WUs, FILE_INFOs and APP_VERSIONs
for (i=0; i<workunits.size(); i++) {
wup = workunits[i];
wup->ref_cnt = 0;
@ -957,6 +955,10 @@ bool CLIENT_STATE::garbage_collect() {
fip = file_infos[i];
fip->ref_cnt = 0;
}
for (i=0; i<app_versions.size(); i++) {
avp = app_versions[i];
avp->ref_cnt = 0;
}
// delete RESULTs that have been finished and reported;
// reference-count files referred to by other results
@ -1010,7 +1012,7 @@ bool CLIENT_STATE::garbage_collect() {
}
// delete WORKUNITs not referenced by any result;
// reference-count files referred to by other WUs
// reference-count files and APP_VERSIONs referred to by other WUs
//
wu_iter = workunits.begin();
while (wu_iter != workunits.end()) {
@ -1026,18 +1028,56 @@ bool CLIENT_STATE::garbage_collect() {
for (i=0; i<wup->input_files.size(); i++) {
wup->input_files[i].file_info->ref_cnt++;
}
wup->avp->ref_cnt++;
wu_iter++;
}
}
// delete FILE_INFOs (and corresponding files)
// that are not referenced by any WORKUNIT or RESULT,
// and are not sticky.
// go through APP_VERSIONs;
// delete any not referenced by any WORKUNIT
// and having a more recent version.
//
avp_iter = app_versions.begin();
while (avp_iter != app_versions.end()) {
avp = *avp_iter;
if (avp->ref_cnt == 0) {
found = false;
for (j=0; j<app_versions.size(); j++) {
avp2 = app_versions[j];
if (avp2->app==avp->app && avp2->version_num>avp->version_num) {
found = true;
break;
}
}
if (found) {
delete avp;
avp_iter = app_versions.erase(avp_iter);
action = true;
} else {
avp_iter++;
}
} else {
avp_iter++;
}
}
// Then go through remaining APP_VERSIONs,
// bumping refcnt of associated files.
//
for (i=0; i<app_versions.size(); i++) {
avp = app_versions[i];
for (j=0; j<avp->app_files.size(); j++) {
avp->app_files[i].file_info->ref_cnt++;
}
}
// delete FILE_INFOs (and corresponding files) that are not sticky
// and are not referenced by any WORKUNIT, RESULT or APP_VERSION
//
fi_iter = file_infos.begin();
while (fi_iter != file_infos.end()) {
fip = *fi_iter;
if (fip->ref_cnt==0 && !fip->sticky && !fip->executable) {
if (fip->ref_cnt==0 && !fip->sticky) {
fip->delete_file();
if (log_flags.state_debug) printf("deleting file %s\n", fip->name);
delete fip;
@ -1048,8 +1088,6 @@ bool CLIENT_STATE::garbage_collect() {
}
}
// TODO: delete obsolete APP_VERSIONs
if (action && log_flags.state_debug) {
print_summary();
}

View File

@ -170,6 +170,7 @@ struct APP_VERSION {
APP* app;
PROJECT* project;
vector<FILE_REF> app_files;
int ref_cnt;
int parse(FILE*);
int write(FILE*);

View File

@ -39,16 +39,15 @@
#include "client_state.h"
#include "error_numbers.h"
// Decide whether to start a new file transfer
// Decide whether to consider starting a new file transfer
// TODO: limit the number of file xfers in some way
//
bool CLIENT_STATE::start_new_file_xfer() {
// TODO: limit the number of file xfers in some way
return true;
}
// Make a directory for each of the projects present
// in the client state
// Make a directory for each of the projects in the client state
//
int CLIENT_STATE::make_project_dirs() {
unsigned int i;
@ -102,7 +101,6 @@ bool CLIENT_STATE::handle_pers_file_xfers() {
FILE_INFO* fip;
PERS_FILE_XFER *pfx;
bool action = false;
int retval;
// Look for FILE_INFOs for which we should start a transfer,
// and make PERS_FILE_XFERs for them
@ -115,15 +113,14 @@ bool CLIENT_STATE::handle_pers_file_xfers() {
pfx = new PERS_FILE_XFER;
pfx->init(fip, false);
fip->pers_file_xfer = pfx;
retval = pers_xfers->insert(fip->pers_file_xfer);
// TODO: return error?
pers_xfers->insert(fip->pers_file_xfer);
action = true;
} else if (fip->upload_when_present && fip->status == FILE_PRESENT && !fip->uploaded) {
pfx = new PERS_FILE_XFER;
pfx->init(fip, true);
fip->pers_file_xfer = pfx;
retval = pers_xfers->insert(fip->pers_file_xfer);
pers_xfers->insert(fip->pers_file_xfer);
action = true;
}
}

View File

@ -94,7 +94,6 @@ char* ip_addr_string(int ip_addr) {
//
int get_timezone( void ) {
tzset();
// TODO: get this to work on all platforms
// TODO: take daylight savings time into account
#ifdef HAVE_GMTOFF
time_t cur_time;
@ -136,7 +135,8 @@ bool host_is_running_on_batteries() {
#ifdef linux
// Determine the memory specifications for this host, including RAM and swap space
// Determine the memory sizes for this host,
// including RAM and swap space
//
void parse_meminfo(HOST_INFO& host) {
char buf[256];

View File

@ -41,9 +41,7 @@
#define HTTP_BLOCKSIZE 16384
// Breaks a url down into its server and file path components
// TODO: deal with alternate protocols (ftp, gopher, etc) or disallow
// them and parse accordingly
// Breaks a HTTP url down into its server and file path components
//
void parse_url(char* url, char* host, int &port, char* file) {
char* p;
@ -121,42 +119,6 @@ static void http_post_request_header(
);
}
#if 0
// Do we still need this?
//
void http_put_request_header(
char* buf, char* host, char* file, int size, int offset
) {
if (offset) {
sprintf(buf,
"PUT /%s HTTP/1.1\015\012"
"Pragma: no-cache\015\012"
"Cache-Control: no-cache\015\012"
"Host: %s:80\015\012"
"Range: bytes=%d-\015\012"
"Connection: close\015\012"
"Content-Type: application/octet-stream\015\012"
"Content-Length: %d\015\012"
"\015\012",
file, host, offset, size
);
} else {
sprintf(buf,
"PUT /%s HTTP/1.1\015\012"
"Pragma: no-cache\015\012"
"Cache-Control: no-cache\015\012"
"Host: %s:80\015\012"
"Connection: close\015\012"
"Content-Type: application/octet-stream\015\012"
"Content-Length: %d\015\012"
"\015\012",
file,
host, size
);
}
}
#endif
// Parse an http reply header into the header struct
//
int read_http_reply_header(int socket, HTTP_REPLY_HEADER& header) {
@ -191,7 +153,7 @@ int read_http_reply_header(int socket, HTTP_REPLY_HEADER& header) {
header.redirect_location[n] = p[n];
n++;
}
p[n] = '\0';
header.redirect_location[n] = 0;
}
return 0;
}
@ -231,7 +193,7 @@ HTTP_OP::HTTP_OP() {
HTTP_OP::~HTTP_OP() {
}
// Initialize HTTP HEAD operation to url
// Initialize HTTP HEAD operation
//
int HTTP_OP::init_head(char* url) {
char proxy_buf[256];
@ -248,7 +210,7 @@ int HTTP_OP::init_head(char* url) {
return 0;
}
// Initialize HTTP GET operation to url
// Initialize HTTP GET operation
//
int HTTP_OP::init_get(char* url, char* out, bool del_old_file, double off) {
char proxy_buf[256];
@ -271,7 +233,7 @@ int HTTP_OP::init_get(char* url, char* out, bool del_old_file, double off) {
return 0;
}
// Initialize HTTP POST operation to url
// Initialize HTTP POST operation
//
int HTTP_OP::init_post(char* url, char* in, char* out) {
int retval;
@ -301,7 +263,7 @@ int HTTP_OP::init_post(char* url, char* in, char* out) {
return 0;
}
// Initialize HTTP POST operation to url including file offset
// Initialize HTTP POST operation
//
int HTTP_OP::init_post2(
char* url, char* r1, char* in, double offset
@ -337,26 +299,6 @@ int HTTP_OP::init_post2(
return 0;
}
#if 0
// not currently used
int HTTP_OP::init_put(char* url, char* in, int off) {
int retval;
offset = off;
parse_url(url, hostname, port, filename);
NET_XFER::init(use_http_proxy?proxy_server_name:hostname, use_http_proxy?proxy_server_port:port, HTTP_BLOCKSIZE);
strcpy(infile, in);
retval = file_size(infile, content_length);
if (retval) return retval;
http_op_type = HTTP_OP_PUT;
http_op_state = HTTP_STATE_CONNECTING;
http_put_request_header(
request_header, hostname, filename, content_length, offset
);
return 0;
}
#endif
// Returns true if the HTTP operation is complete
//
bool HTTP_OP::http_op_done() {
@ -367,7 +309,7 @@ HTTP_OP_SET::HTTP_OP_SET(NET_XFER_SET* p) {
net_xfers = p;
}
// Inserts an HTTP_OP into the set
// Adds an HTTP_OP to the set
//
int HTTP_OP_SET::insert(HTTP_OP* ho) {
int retval;
@ -388,8 +330,6 @@ bool HTTP_OP_SET::poll() {
htp = http_ops[i];
switch(htp->http_op_state) {
case HTTP_STATE_CONNECTING:
// If the op is in the connecting state, and we notice it is done
// connecting, move it to the HTTP_STATE_REQUEST_HEADER state
if (htp->is_connected) {
htp->http_op_state = HTTP_STATE_REQUEST_HEADER;
htp->want_upload = true;
@ -399,7 +339,10 @@ bool HTTP_OP_SET::poll() {
case HTTP_STATE_REQUEST_HEADER:
if (htp->io_ready) {
action = true;
n = send(htp->socket, htp->request_header, strlen(htp->request_header), 0);
n = send(
htp->socket, htp->request_header,
strlen(htp->request_header), 0
);
if (log_flags.http_debug) {
printf(
"wrote HTTP header to socket %d: %d bytes\n%s",
@ -409,7 +352,6 @@ bool HTTP_OP_SET::poll() {
htp->io_ready = false;
switch(htp->http_op_type) {
case HTTP_OP_POST:
//case HTTP_OP_PUT:
htp->http_op_state = HTTP_STATE_REQUEST_BODY;
htp->file = fopen(htp->infile, "rb");
if (!htp->file) {
@ -438,8 +380,7 @@ bool HTTP_OP_SET::poll() {
action = true;
n = send(htp->socket, htp->req1, strlen(htp->req1), 0);
htp->http_op_state = HTTP_STATE_REQUEST_BODY;
// If there's a file we also want to send, then start transferring
// it, otherwise, go on to the next step
if (htp->infile && strlen(htp->infile) > 0) {
htp->file = fopen(htp->infile, "rb");
if (!htp->file) {
@ -480,27 +421,28 @@ bool HTTP_OP_SET::poll() {
action = true;
if (log_flags.http_debug) printf("got reply header; %x io_done %d\n", (unsigned int)htp, htp->io_done);
read_http_reply_header(htp->socket, htp->hrh);
// TODO: handle all kinds of redirects here
if (htp->hrh.status == HTTP_STATUS_MOVED_PERM || htp->hrh.status == HTTP_STATUS_MOVED_TEMP) {
// Close the old socket
htp->close_socket();
switch (htp->http_op_type) {
case HTTP_OP_HEAD:
htp->init_head( htp->hrh.redirect_location );
htp->init_head(htp->hrh.redirect_location);
break;
case HTTP_OP_GET:
// *** Not sure if delete_old_file should be true
htp->init_get( htp->hrh.redirect_location, htp->outfile, true );
htp->init_get(htp->hrh.redirect_location, htp->outfile, false);
break;
case HTTP_OP_POST:
htp->init_post( htp->hrh.redirect_location, htp->infile, htp->outfile );
htp->init_post(htp->hrh.redirect_location, htp->infile, htp->outfile);
break;
case HTTP_OP_POST2:
// TODO: Change offset to correct value
htp->init_post2( htp->hrh.redirect_location, htp->req1, htp->infile, 0 );
htp->init_post2(htp->hrh.redirect_location, htp->req1, htp->infile, htp->file_offset);
break;
}
// Open connection to the redirected server
//
htp->open_server();
break;
}
@ -522,10 +464,6 @@ bool HTTP_OP_SET::poll() {
case HTTP_OP_GET:
htp->http_op_state = HTTP_STATE_REPLY_BODY;
// TODO:
// Append to a file if it already exists, otherwise
// create a new one. init_get should have already
// deleted the file if necessary
htp->file = fopen(htp->outfile, "ab");
if (!htp->file) {
fprintf(stderr,
@ -544,11 +482,6 @@ bool HTTP_OP_SET::poll() {
htp->io_ready = false;
htp->io_done = true;
break;
#if 0
case HTTP_OP_PUT:
htp->http_op_state = HTTP_STATE_DONE;
htp->http_op_retval = 0;
#endif
}
}
break;

View File

@ -42,7 +42,6 @@ struct HTTP_REPLY_HEADER {
// For the first 4, data source/sink are files
#define HTTP_OP_GET 1
#define HTTP_OP_POST 2
//#define HTTP_OP_PUT 3
#define HTTP_OP_HEAD 4
#define HTTP_OP_POST2 5
// a POST operation where the request comes from a combination
@ -77,7 +76,6 @@ public:
int init_post2(
char* url, char* req1, char* infile, double offset
);
//int init_put(char* url, char* infile, int offset=0);
bool http_op_done();
};

View File

@ -48,7 +48,6 @@ void show_message(PROJECT *p, char* message, char* priority) {
// Prompt user for project URL and authenticator,
// and create an account file
// TODO: use better input method here, backspace doesn't always seem to work
//
int add_new_project() {
char master_url[256];
@ -63,11 +62,6 @@ int add_new_project() {
);
scanf("%s", authenticator);
// TODO: might be a good idea to verify the account key
// by doing an RPC to a scheduling server.
// But this would require fetching and parsing the master file,
// so to heck with it.
write_account_file(master_url, authenticator);
return 0;
}

View File

@ -37,7 +37,7 @@ global $text;
global $parse_result;
global $in_project_specific;
// the following will parse either global or project prefs
// the following parses either global or project prefs
// TODO: split up into separate functions
//
function element_start($parser, $name, $attrs) {
@ -93,6 +93,12 @@ function element_end($parser, $name) {
case "disk_min_free_gb":
$parse_result->disk_min_free_gb = $text;
break;
case "max_bytes_sec_down":
$parse_result->max_bytes_sec_down = $text;
break;
case "max_bytes_sec_up":
$parse_result->max_bytes_sec_up = $text;
break;
case "resource_share":
$parse_result->resource_share = $text;
break;
@ -135,6 +141,8 @@ function default_prefs() {
$p->disk_max_used_gb = 100;
$p->disk_max_used_pct = 50;
$p->disk_min_free_gb = 1;
$p->max_bytes_sec_down = 0;
$p->max_bytes_sec_up = 0;
$p->resource_share = 100;
$p->show_email = false;
@ -146,7 +154,7 @@ function default_prefs() {
// state of prefs before parsing; initialize all booleans to false
//
function initial_prefs() {
function initialize_prefs_before_parsing() {
$p = default_prefs();
$p->show_email = false;
$p->send_email = false;
@ -161,7 +169,7 @@ function initial_prefs() {
function prefs_parse($prefs_xml) {
global $parse_result;
$parse_result = initial_prefs();
$parse_result = initialize_prefs_before_parsing();
$xml_parser = xml_parser_create();
xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, 0);
@ -173,16 +181,20 @@ function prefs_parse($prefs_xml) {
////////////////////////////////////////////
//
// display preference subsets, with Edit buttons
// display preference subsets
//
function prefs_show_global($prefs) {
row2("Work if computer on batteries:", $prefs->run_on_batteries?"Yes":"No");
row2("Work if computer in use:", $prefs->run_if_user_active?"Yes":"No");
row2("Confirm before connecting to network:", $prefs->confirm_before_connecting?"Yes":"No");
row2("Work if computer on batteries?", $prefs->run_on_batteries?"Yes":"No");
row2("Work if computer in use?", $prefs->run_if_user_active?"Yes":"No");
row2("Confirm before connecting to network?", $prefs->confirm_before_connecting?"Yes":"No");
row2("Amount of work to buffer:", "$prefs->work_buf_min_days to $prefs->work_buf_max_days days");
row2("Maximum disk space to use:", "$prefs->disk_max_used_gb GB");
row2("Minimum disk space to leave free:", "$prefs->disk_min_free_gb GB");
row2("Maximum % of disk allowed to used:", "$prefs->disk_max_used_pct %");
$x = $prefs->max_bytes_sec_down;
row2("Maximum bytes/sec download:", $x?"$x":"No limit");
$x = $prefs->max_bytes_sec_up;
row2("Maximum bytes/sec upload:", $x?"$x":"No limit");
}
function prefs_show_resource($prefs) {
@ -264,15 +276,23 @@ function prefs_form_global($user, $prefs) {
<td align=right valign=top>Use no more than</td>
<td><input size=7 name=disk_max_used_gb value='$prefs->disk_max_used_gb'> Gbytes</td>
</tr>
<tr>
<td align=right valign=top>Leave at least</td>
<td><input size=7 name=disk_min_free_gb value='$prefs->disk_min_free_gb'> Gbytes free</td>
</tr>
<tr><td align=right valign=top>Leave at least</td>
<td><input size=7 name=disk_min_free_gb value='$prefs->disk_min_free_gb'> Gbytes free</td></tr>
<tr>
<td align=right valign=top>Use no more than</td>
<td><input size=5 name=disk_max_used_pct value='$prefs->disk_max_used_pct'> % of total space</td>
</tr>
";
$d = $prefs->max_bytes_sec_down;
$dt = $d?"$d":"";
$u = $prefs->max_bytes_sec_up;
$ut = $u?"$u":"";
echo "
<tr><td align=right valign=top>Maximum bytes/sec download</td>
<td><input size=7 name=max_bytes_sec_down value='$dt'></td></tr>
<tr><td align=right valign=top>Maximum bytes/sec upload</td>
<td><input size=7 name=max_bytes_sec_up value='$ut'></td></tr>
";
}
function prefs_form_email($prefs) {
@ -350,6 +370,8 @@ function prefs_global_parse_form(&$prefs) {
$disk_max_used_gb = $_GET["disk_max_used_gb"];
$disk_max_used_pct = $_GET["disk_max_used_pct"];
$disk_min_free_gb = $_GET["disk_min_free_gb"];
$max_bytes_sec_down = $_GET["max_bytes_sec_down"];
$max_bytes_sec_up = $_GET["max_bytes_sec_up"];
$prefs->run_on_batteries = ($run_on_batteries == "yes");
$prefs->run_if_user_active = ($run_if_user_active == "yes");
@ -368,6 +390,8 @@ function prefs_global_parse_form(&$prefs) {
$prefs->disk_max_used_gb = $disk_max_used_gb;
$prefs->disk_max_used_pct = $disk_max_used_pct;
$prefs->disk_min_free_gb = $disk_min_free_gb;
$prefs->max_bytes_sec_down = $max_bytes_sec_down;
$prefs->max_bytes_sec_up = $max_bytes_sec_up;
}
function prefs_resource_parse_form(&$prefs) {
@ -407,7 +431,9 @@ function global_prefs_make_xml($prefs) {
$xml = $xml
."<disk_max_used_gb>$prefs->disk_max_used_gb</disk_max_used_gb>\n"
."<disk_max_used_pct>$prefs->disk_max_used_pct</disk_max_used_pct>\n"
."<disk_min_free_gb>$prefs->disk_min_free_gb</disk_min_free_gb>\n";
."<disk_min_free_gb>$prefs->disk_min_free_gb</disk_min_free_gb>\n"
."<max_bytes_sec_down>$prefs->max_bytes_sec_down</max_bytes_sec_down>\n"
."<max_bytes_sec_up>$prefs->max_bytes_sec_up</max_bytes_sec_up>\n";
$xml = $xml."</global_preferences>\n";
return $xml;
}

View File

@ -215,8 +215,9 @@ int authenticate_user(SCHEDULER_REQUEST& sreq, SCHEDULER_REPLY& reply) {
retval = db_user_lookup_auth(reply.user);
if (retval) {
strcpy(reply.message,
"Invalid or missing authenticator. "
"Visit this project's web site to get an authenticator.");
"Invalid or missing account ID. "
"Visit this project's web site to get an account ID."
);
strcpy(reply.message_priority, "low");
reply.request_delay = 120;
sprintf(buf, "Bad authenticator: %s\n", sreq.authenticator);
@ -280,7 +281,8 @@ int update_host_record(SCHEDULER_REQUEST& sreq, HOST& host) {
host.connected_frac = sreq.host.connected_frac;
host.active_frac = sreq.host.active_frac;
host.p_ncpus = sreq.host.p_ncpus;
strncpy(host.p_vendor, sreq.host.p_vendor, sizeof(host.p_vendor)); // unlikely this will change
strncpy(host.p_vendor, sreq.host.p_vendor, sizeof(host.p_vendor));
// unlikely this will change
strncpy(host.p_model, sreq.host.p_model, sizeof(host.p_model));
host.p_fpops = sreq.host.p_fpops;
host.p_iops = sreq.host.p_iops;

11
todo
View File

@ -1,13 +1,10 @@
-----------------------
BUGS (arranged from high to low priority)
-----------------------
- Test suspend/resume functionality on Windows,
no way to suspend/resume on UNIX
- "Show Graphics" menu item brings up minimized window,
client does not remember window size/pos after close/reopen,
window closes and does not reopen when workunit finishes
and new workunit starts
- Screensaver "blank screen" functionality not implemented
- CPU time updates infrequently (every 10 seconds),
should there be a user control for this?
- Client treats URL "maggie/ap/" different than URL "maggie/ap",
@ -22,6 +19,8 @@ BUGS (arranged from high to low priority)
HIGH-PRIORITY (should do for beta test)
-----------------------
- Implement Screensaver "blank screen" functionality
multiple preference sets
implement server watchdogs
@ -36,6 +35,8 @@ Messages from core client
-----------------------
THINGS TO TEST (preferably with test scripts)
-----------------------
- Test suspend/resume functionality on Windows,
no way to suspend/resume on UNIX
- verify that if file xfer is interrupted, it resumes at right place
(and progress bar is correct)
- result reissue
@ -48,10 +49,6 @@ MEDIUM-PRIORITY (should do before public release)
make get_local_ip_addr() work in all cases
implement max bytes/sec network preferences
implement bandwidth limiting
current code assumes 1-second poll loop; wrong
Implement FIFO mechanism in scheduler for results that can't be sent
user profiles on web (borrow logic from SETI@home)