tentative projects

svn path=/trunk/boinc/; revision=1187
This commit is contained in:
David Anderson 2003-05-13 18:55:07 +00:00
parent f08517a666
commit 7d6fce4cf4
21 changed files with 336 additions and 176 deletions

View File

@ -56,7 +56,7 @@ ARCHIVE_TARGETS = \
mac_build/boinc.pbproj/*.pbxproj mac_build/*.lproj/*.nib/*.* \
mac_build/*.lproj/*.strings \
win_build/*.* win_build/*/*.dsp win_build/*/*.rct \
INSTALL LICENSE configure \
INSTALL LICENSE configure todo checkin_notes \
*.in *.sub *.guess
tar:

View File

@ -4101,3 +4101,45 @@ Erik May 8 2003
lib/
Makefile.in
app_ipc.C,h
Erik May 13 2003
- Added a notion of "tentative" project.
This means that the (URL, account ID) pair hasn't been verified,
i.e. we haven't fetched the master page, found a scheduler,
and done a scheduler op that verified the account ID.
If anything fails for a tentative project, call project_add_failed().
in the cmdline version this prints an error message,
deletes all evidence of the project, and exits.
TODO: implement for GUI
- Make write_account_file() into a member function of PROJECT.
- Got rid of CLIENT_STATE::change_project().
The user must detach and attach.
- CLIENT_STATE::quit_project() now takes a PROJECT*, not an int.
Need to change the GUI code.
- got rid of separate WIN_32 implementations of
make_project_dir(), remove_project_dir(), make_slot_dir().
THIS IS THE WRONG LEVEL FOR PLATFORM-DEPENDENT CODE!
Instead, added boinc_mkdir() and boinc_rmdir()
- added implementation docs for screensaver logic
- test_uc.php: wait for a second before running client.
On fast computers the client runs before feeder has seeded shmem.
client/
account.C,h
client_state.C,h
client_types.C,h
cs_scheduler.C
file_names.C
main.C
scheduler_op.C,h
ss_logic.C
win/
wingui.cpp
wingui_mainwindow.cpp
doc/
client_app.html
client_app_graphic.html
lib/
filesys.C,y
test/
test_uc.php

View File

@ -26,34 +26,6 @@
#include "account.h"
int write_account_file(
char* master_url, char* authenticator, char* project_prefs
) {
char path[256];
FILE* f;
int retval;
get_account_filename(master_url, path);
f = fopen(TEMP_FILE_NAME, "w");
if (!f) return ERR_FOPEN;
fprintf(f,
"<account>\n"
" <master_url>%s</master_url>\n"
" <authenticator>%s</authenticator>\n",
master_url,
authenticator
);
if (project_prefs) {
fprintf(f, "%s", project_prefs);
}
fprintf(f, "</account>\n");
fclose(f);
retval = boinc_rename(TEMP_FILE_NAME, path);
if (retval) return ERR_RENAME;
return 0;
}
int CLIENT_STATE::parse_account_files() {
DIRREF dir;
char name[256];
@ -87,17 +59,19 @@ int CLIENT_STATE::add_project(char* master_url, char* authenticator) {
// create project state
//
retval = write_account_file(master_url, authenticator);
project = new PROJECT;
strcpy(project->master_url, master_url);
strcpy(project->authenticator, authenticator);
project->tentative = true;
retval = project->write_account_file();
if (retval) return retval;
get_account_filename(master_url, path);
f = fopen(path, "r");
if (!f) return ERR_FOPEN;
project = new PROJECT;
retval = project->parse_account(f);
fclose(f);
if(retval) {
return retval;
}
if (retval) return retval;
// remove any old files
//
@ -109,6 +83,13 @@ int CLIENT_STATE::add_project(char* master_url, char* authenticator) {
return 0;
}
#if 0
// Wrong approach.
// The user must detach and reattach.
// In any case the following has a major bug:
// the call to remove_project_dir() won't work because
// it gets the new, not the old, project URL
//
int CLIENT_STATE::change_project(
int index, char* master_url, char* authenticator
) {
@ -121,7 +102,7 @@ int CLIENT_STATE::change_project(
//
if (lookup_project(master_url)) return -1;
// delete old file
// delete old account file
//
project = projects[index];
get_account_filename(project->master_url, path);
@ -147,33 +128,42 @@ int CLIENT_STATE::change_project(
}
return 0;
}
#endif
int CLIENT_STATE::quit_project(int index) {
PROJECT* project = NULL;
// TODO: see if any activities are in progress for this project, and stop them
//
int CLIENT_STATE::quit_project(PROJECT* project) {
PROJECT* p;
vector <PROJECT*>::iterator iter;
int curindex = 0;
char path[256];
int retval;
// find project and remove it from the vector
//
for (iter = projects.begin(); iter != projects.end(); iter++) {
if (curindex == index) {
project = *iter;
p = *iter;
if (p == project) {
projects.erase(iter);
break;
}
curindex++;
}
if (project == NULL) return -1;
// delete file
char path[256];
int retval;
// delete account file
//
get_account_filename(project->master_url, path);
retval = file_delete(path);
// delete project; //also somewhere else?
// if tentative, there are no activities so we can safely delete everything
//
if (project->tentative) {
// remove project directory and its contents
//
remove_project_dir(*project);
delete project;
} else {
#ifdef _WIN32
AfxMessageBox("Please restart the client to complete the quit.", MB_OK, 0);
AfxMessageBox("Please restart the client to complete the quit.", MB_OK, 0);
#endif
}
return 0;
}

View File

@ -1,9 +1,6 @@
#ifndef _ACCOUNT_
#define _ACCOUNT_
extern int write_account_file(
char* master_url, char* authenticator, char* project_prefs=0
);
extern int add_new_project();
extern int parse_account_files();

View File

@ -488,13 +488,11 @@ int CLIENT_STATE::check_suspend_activities() {
if (should_suspend) {
if (!activities_suspended) {
if (log_flags.task_debug) printf("SUSPENDING ACTIVITIES\n");
active_tasks.suspend_all();
show_message(NULL, susp_msg, MSG_INFO);
}
} else {
if (activities_suspended) {
if (log_flags.task_debug) printf("UNSUSPENDING ACTIVITIES\n");
active_tasks.unsuspend_all();
show_message(NULL, "Resuming activity", MSG_INFO);
}

View File

@ -168,8 +168,8 @@ public:
int report_result_error(RESULT &res, int err_num, char *err_msg);
// flag a result as having an error
int add_project(char* master_url, char* authenticator);
int change_project(int index, char* master_url, char* authenticator);
int quit_project(int index);
//int change_project(int index, char* master_url, char* authenticator);
int quit_project(PROJECT*);
private:
PROJECT* find_project_with_overdue_results();
bool some_project_rpc_ok();

View File

@ -57,11 +57,43 @@ PROJECT::PROJECT() {
debt_order = 0;
master_url_fetch_pending = false;
sched_rpc_pending = false;
tentative = false;
}
PROJECT::~PROJECT() {
}
// write account_*.xml file
//
int PROJECT::write_account_file() {
char path[256];
FILE* f;
int retval;
get_account_filename(master_url, path);
f = fopen(TEMP_FILE_NAME, "w");
if (!f) return ERR_FOPEN;
fprintf(f,
"<account>\n"
" <master_url>%s</master_url>\n"
" <authenticator>%s</authenticator>\n",
master_url,
authenticator
);
if (strlen(project_specific_prefs)) {
fprintf(f, "%s", project_specific_prefs);
}
if (tentative) {
fprintf(f, " <tentative/>\n");
}
fprintf(f, "</account>\n");
fclose(f);
retval = boinc_rename(TEMP_FILE_NAME, path);
if (retval) return ERR_RENAME;
return 0;
}
// parse project fields from account_*.xml
//
int PROJECT::parse_account(FILE* in) {
@ -83,6 +115,10 @@ int PROJECT::parse_account(FILE* in) {
else if (parse_double(buf, "<resource_share>", resource_share)) continue;
else if (match_tag(buf, "<send_email/>")) continue;
else if (match_tag(buf, "<show_email/>")) continue;
else if (match_tag(buf, "<tentative/>")) {
tentative = true;
continue;
}
else if (match_tag(buf, "<project_specific>")) {
retval = copy_element_contents(
in,
@ -243,11 +279,12 @@ void PROJECT::copy_state_fields(PROJECT& p) {
safe_strcpy(code_sign_key, p.code_sign_key);
}
char *PROJECT::get_project_name() {
if(!strcmp(project_name, ""))
return master_url;
else
char* PROJECT::get_project_name() {
if (strlen(project_name)) {
return project_name;
} else {
return master_url;
}
}
int APP::parse(FILE* in) {

View File

@ -59,7 +59,7 @@ public:
// the following items come from client_state.xml
// They may depend on the host as well as user and project
// NOTE: if you add anything, add it copy_state_fields() as well!!!
// NOTE: if you add anything, add it to copy_state_fields() also!!!
//
vector<STRING256> scheduler_urls; // where to find scheduling servers
char project_name[256]; // descriptive. not unique
@ -82,8 +82,8 @@ public:
// of this project (or zero)
bool master_url_fetch_pending;
// need to fetch and parse the master URL
bool sched_rpc_pending;
// need to contact the scheduling server for user/project info
bool sched_rpc_pending; // contact scheduling server for preferences
bool tentative; // master URL and account ID not confirmed
char code_sign_key[MAX_BLOB_LEN];
// the following items are transient; not saved in state file
@ -95,6 +95,7 @@ public:
~PROJECT();
void copy_state_fields(PROJECT&);
char *get_project_name();
int write_account_file();
int parse_account(FILE*);
int parse_state(FILE*);
int write_state(FILE*);

View File

@ -396,17 +396,18 @@ int CLIENT_STATE::handle_scheduler_reply(
// deal with project preferences (should always be there)
//
if (sr.project_prefs_xml) {
char path[256];
retval = write_account_file(
project->master_url, project->authenticator, sr.project_prefs_xml
);
strcpy(project->project_specific_prefs, sr.project_prefs_xml);
retval = project->write_account_file();
if (retval) return retval;
#if 0
char path[256];
get_account_filename(project->master_url, path);
f = fopen(path, "r");
if (!f) return ERR_FOPEN;
project->parse_account(f);
fclose(f);
#endif
}
// if the scheduler reply includes a code-signing key,

View File

@ -89,55 +89,15 @@ void get_slot_dir(int slot, char* path) {
sprintf(path, "%s%s%d", SLOTS_DIR, PATH_SEPARATOR, slot);
}
#ifdef _WIN32
// Double check permissions for CreateDirectory
int make_project_dir(PROJECT& p) {
char buf[256],buf2[256];
CreateDirectory(PROJECTS_DIR, NULL);
escape_project_url(p.master_url, buf);
sprintf(buf2, "%s%s%s", PROJECTS_DIR, PATH_SEPARATOR, buf);
CreateDirectory(buf2, NULL);
return 0;
}
int remove_project_dir(PROJECT& p) {
char buf[256],buf2[256];
escape_project_url(p.master_url, buf);
sprintf(buf2, "%s%s%s", PROJECTS_DIR, PATH_SEPARATOR, buf);
clean_out_dir(buf2);
RemoveDirectory(buf2);
return 0;
}
// Returns the location of a numbered slot directory
//
int make_slot_dir(int slot) {
if(slot<0) {
fprintf(stderr, "error: make_slot_dir: negative slot\n");
return ERR_NEG;
}
char buf[256];
CreateDirectory(SLOTS_DIR, NULL);
get_slot_dir(slot, buf);
CreateDirectory(buf, NULL);
return 0;
}
#else
// Create the directory for the project p
//
int make_project_dir(PROJECT& p) {
char buf[256],buf2[256];
mkdir(PROJECTS_DIR, 0777);
boinc_mkdir(PROJECTS_DIR);
escape_project_url(p.master_url, buf);
sprintf(buf2, "%s%s%s", PROJECTS_DIR, PATH_SEPARATOR, buf);
mkdir(buf2, 0777);
boinc_mkdir(buf2);
return 0;
}
@ -147,7 +107,7 @@ int remove_project_dir(PROJECT& p) {
escape_project_url(p.master_url, buf);
sprintf(buf2, "%s%s%s", PROJECTS_DIR, PATH_SEPARATOR, buf);
clean_out_dir(buf2);
// rmdir(buf2);
boinc_rmdir(buf2);
return 0;
}
@ -159,14 +119,12 @@ int make_slot_dir(int slot) {
fprintf(stderr, "error: make_slot_dir: negative slot\n");
return ERR_NEG;
}
mkdir(SLOTS_DIR, 0777);
boinc_mkdir(SLOTS_DIR);
get_slot_dir(slot, buf);
mkdir(buf, 0777);
boinc_mkdir(buf);
return 0;
}
#endif
void get_account_filename(char* master_url, char* path) {
char buf[256];
escape_project_url(master_url, buf);

View File

@ -42,16 +42,42 @@
void create_curtain(){}
void delete_curtain(){}
// This gets called when the client fails to add a project
//
void project_add_failed(PROJECT* project) {
if (project->scheduler_urls.size()) {
printf(
"BOINC failed to log in to %s.\n "
"Please check your account ID and try again.\n",
project->master_url
);
} else {
printf(
"BOINC couldn't get main page for %s.\n"
"Please check the URL and try again.\n",
project->master_url
);
}
gstate.quit_project(project);
exit(1);
}
// Display a message to the user.
// Depending on the priority, the message may be more or less obtrusive
//
void show_message(PROJECT *p, char* message, int priority) {
const char* proj = p?p->project_name:"BOINC";
char* x;
if (p) {
x = p->get_project_name();
} else {
x = "BOINC";
}
switch (priority) {
case MSG_ERROR:
fprintf(stderr, "%s [%s] %s\n", timestamp(), proj, message);
fprintf(stderr, "%s [%s] %s\n", timestamp(), x, message);
default:
printf("%s [%s] %s\n", timestamp(), proj, message);
printf("%s [%s] %s\n", timestamp(), x, message);
}
}
@ -59,19 +85,19 @@ void show_message(PROJECT *p, char* message, int priority) {
// and create an account file
//
int add_new_project() {
char master_url[256];
char authenticator[256];
PROJECT project;
printf("Enter the URL of the project: ");
scanf("%s", master_url);
scanf("%s", project.master_url);
printf(
"You should have already registered with the project\n"
"and received an account key by email.\n"
"Paste the account key here: "
);
scanf("%s", authenticator);
scanf("%s", project.authenticator);
write_account_file(master_url, authenticator);
project.tentative = true;
project.write_account_file();
return 0;
}

View File

@ -32,12 +32,35 @@
#include "log_flags.h"
#include "scheduler_op.h"
extern void project_add_failed(PROJECT*);
SCHEDULER_OP::SCHEDULER_OP(HTTP_OP_SET* h) {
state = SCHEDULER_OP_STATE_IDLE;
http_op.http_op_state = HTTP_STATE_IDLE;
http_ops = h;
}
// see if there's a pending master file fetch. start it if so.
//
bool SCHEDULER_OP::check_master_fetch_start() {
int retval;
project = gstate.next_project_master_pending();
if (project) {
retval = init_master_fetch(project);
if (retval) {
if (project->tentative) {
project_add_failed(project);
} else {
project->master_fetch_failures++;
backoff(project, "Master file fetch failed\n");
}
}
return true;
}
return false;
}
// try to get enough work to bring us up to max buffer level
//
int SCHEDULER_OP::init_get_work() {
@ -55,14 +78,7 @@ int SCHEDULER_OP::init_get_work() {
return retval;
}
} else {
project = gstate.next_project_master_pending();
if (project) {
retval = init_master_fetch(project);
if (retval) {
sprintf(err_msg, "init_master_fetch failed, error %d\n", retval);
backoff(project, err_msg);
}
}
check_master_fetch_start();
}
return 0;
@ -344,26 +360,23 @@ bool SCHEDULER_OP::poll() {
// it may be the wrong URL. notify the user
//
if (project->scheduler_urls.size() == 0) {
sprintf(err_msg,
"Could not contact %s. Make sure this is the correct project URL.",
err_url
);
show_message(project, err_msg, MSG_ERROR);
project->master_fetch_failures++;
backoff(project, err_msg);
if (project->tentative) {
project_add_failed(project);
} else {
sprintf(err_msg,
"Could not contact any schedulers for %s.",
err_url
);
show_message(project, err_msg, MSG_ERROR);
project->master_fetch_failures++;
backoff(project, err_msg);
}
}
// See if need to read master file for another project
// See if need to read master file for another project;
// if not, we're done for now
//
project = gstate.next_project_master_pending();
if (project) {
retval = init_master_fetch(project);
if (retval) {
project->master_fetch_failures++;
backoff(project, "Master file fetch failed\n");
err_url = project->master_url;
}
} else {
if (!check_master_fetch_start()) {
state = SCHEDULER_OP_STATE_IDLE;
if (log_flags.sched_op_debug) {
printf("Scheduler_op: return to idle state\n");
@ -402,8 +415,7 @@ bool SCHEDULER_OP::poll() {
} else {
scheduler_op_done = true;
}
}
else {
} else {
scheduler_op_done = true;
}
}
@ -416,14 +428,27 @@ bool SCHEDULER_OP::poll() {
}
gstate.handle_scheduler_reply(project, scheduler_url, nresults);
// if we asked for work and didn't get any,
// back off this project
// if this was a tentative project and we didn't get user name,
// the account ID must be bad. Tell the user.
//
if (must_get_work && nresults==0) {
backoff(project, "No work from project\n");
if (project->tentative) {
if (strlen(project->user_name)) {
project->tentative = false;
project->write_account_file();
} else {
project_add_failed(project);
}
} else {
project->nrpc_failures = 0;
project->min_rpc_time = 0;
// if we asked for work and didn't get any,
// back off this project
//
if (must_get_work && nresults==0) {
backoff(project, "No work from project\n");
} else {
project->nrpc_failures = 0;
project->min_rpc_time = 0;
}
}
// if we didn't get all the work we needed,

View File

@ -87,6 +87,7 @@ struct SCHEDULER_OP {
int set_min_rpc_time(PROJECT*);
bool update_urls(PROJECT& project, vector<STRING256> &urls);
int start_op(PROJECT*);
bool check_master_fetch_start();
void backoff(PROJECT* p, char *error_msg);
int start_rpc();
int parse_master_file(vector<STRING256>&);

View File

@ -71,7 +71,7 @@ void SS_LOGIC::poll() {
} else {
if (time(0)>ack_deadline) {
do_boinc_logo_ss = true;
sprintf(ss_msg, "App can't display graphics");
strcpy(ss_msg, "App can't display graphics");
}
}
} else {
@ -81,7 +81,7 @@ void SS_LOGIC::poll() {
ack_deadline = time(0) + 5;
} else {
do_boinc_logo_ss = true;
sprintf(ss_msg, "No work available");
strcpy(ss_msg, "No work available");
}
}
}

View File

@ -23,18 +23,18 @@
#include "wingui_mainwindow.h"
void show_message(PROJECT* p, char* message, int priority) {
char proj_name[256];
char* x;
if (p) {
safe_strncpy( proj_name, p->get_project_name(), sizeof(proj_name) );
x = p->get_project_name();
} else {
safe_strncpy( proj_name, "BOINC", sizeof(proj_name) );
x = "BOINC";
}
if(g_myWnd) {
g_myWnd->MessageUser(proj_name, message, priority);
g_myWnd->MessageUser(x, message, priority);
} else {
fprintf(stderr, "%s: %s (priority: %s)\n", proj_name, message, priority);
fprintf(stderr, "%s: %s (priority: %s)\n", x, message, priority);
}
}

View File

@ -1057,6 +1057,7 @@ void CMainWindow::OnCommandConnectionConnectNow()
NetClose();
}
#if 0
//////////
// CMainWindow::OnCommandProjectRelogin
// arguments: void
@ -1083,6 +1084,7 @@ void CMainWindow::OnCommandProjectRelogin()
gstate.change_project(i, dlg.m_strUrl.GetBuffer(0), dlg.m_strAuth.GetBuffer(0));
}
}
#endif
//////////
// CMainWindow::OnCommandProjectQuit

9
doc/client_app.html Normal file
View File

@ -0,0 +1,9 @@
<h2>Core/app interaction (basic)</h2>
TO BE WRITTEN.
<p>
Explain startup files
<p>
Explain shared memory mechanism in general
<p>
Explain work-related use of shmem

View File

@ -0,0 +1,14 @@
<h2>Screensaver/core/app interaction (graphics)</h2>
TO BE WRITTEN
<p>
Explain graphics startup files
<p>
Explain graphics modes of apps
<p>
Explain graphics use of shmem
<p>
Explain screensaver module
<p>
Explain screensaver logic in core

View File

@ -263,6 +263,22 @@ int boinc_rename(char* old, char* newf) {
return rename(old, newf);
}
int boinc_mkdir(char* name) {
#ifdef _WIN32
return CreateDirectory(name, NULL);
#else
return mkdir(name, 0777);
#endif
}
int boinc_rmdir(char* name) {
#ifdef _WIN32
return RemoveDirectory(name, NULL);
#else
return rmdir(name);
#endif
}
#ifdef _WIN32
void full_path(char* relname, char* path) {
_getcwd(path, 256);

View File

@ -42,6 +42,8 @@ extern int boinc_link(char *existing, char *new_link);
extern int clean_out_dir(char*);
extern int dir_size(char* dirpath, double&);
extern int boinc_rename(char* old, char* newf);
extern int boinc_mkdir(char*);
extern int boinc_rmdir(char*);
#ifdef _WIN32
extern void full_path(char* relname, char* path);
#endif

69
todo
View File

@ -1,11 +1,13 @@
-----------------------
BUGS (arranged from high to low priority)
-----------------------
- CPU benchmarks take way too long (linux)
- Client treats URL "maggie/ap/" different than URL "maggie/ap",
though this isn't really a bug it might be good to fix anyway
- global battery/user active prefs are always true in the client
- Client should display "Upload failed" and "Download failed" when failure occurs
- Result status should say "downloading files", "uploading files", etc.
- GUI client should display "Upload failed" and "Download failed"
in transfers tab when failure occurs
- GUI: Result status should say "downloading files", "uploading files", etc.
- message window should reposition to bottom when new message
- show_message should expect \n, discard it if GUI
- Win GUI: line between menus and tabs
@ -19,29 +21,68 @@ BUGS (arranged from high to low priority)
run_on_startup
hangup_if_dialed
- trim leading/trailing spaces from account ID (Win GUI)
- I entered in a wrong URL - there was no obvious feedback that it wasn't correct.
Messages showed up in the messages tab, but I was looking at the progress tab.
Shouldn't the client expect something from the server? If it doesn't get it,
especially on logging in for the first time, you should get an obvious warning.
- when i quit a project, I have to exit and restart the client, which is ugly.
- after quitting a project, the project name still showed up in gray in the projects
list - I could right click on it to "relogin" (which did nothing) or "quit project"
- after quitting a project, the project name still showed up in gray
in the projects list - I could right click on it to "relogin"
(which did nothing) or "quit project"
which I thought I already did.
- consider warning message during windows (and perhaps other platforms)
install that checks to see if the BOINC directory already exists,
and if so, should the user overwrite it? or upgrade it?
- After running all night (on Win98) I shook the mouse to wake up the blank screen, and
all I saw was the top half of the screen was solid gray, and the bottom half the
bottom half of the astropulse graphics. They weren't moving. The computer was frozen.
- After running all night (on Win98) I shook the mouse to wake up
the blank screen, and all I saw was the top half of the screen
was solid gray, and the bottom half the
bottom half of the astropulse graphics.
They weren't moving. The computer was frozen.
I had to ctrl-alt-del to restart.
-----------------------
HIGH-PRIORITY (should do for beta test)
-----------------------
On "add project" the core client should immediately attempt to
get project master page and verify user account.
If failure, let user retype URL/ID
"Add project" behavior:
Goal: give user timely feedback if bad project URL or account ID;
don't leave bad project files sitting around
A project addition is "successful" when
1) the client fetches the master page,
2) the master page has at least one scheduler URL
3) the client contacts a scheduler and gets a user name back.
The cmdline and GUI clients need to inform the user if a project
add is not successful, since it probably means the master URL
or account ID were typed in wrong.
A project is "tentative" if the above hasn't happened yet.
This is flagged in the project file (<tentative/>) and in memory
A "failure event" is
- master fetch fails
- master page has no scheduler URLs
- scheduler RPC fails
- scheduler RPC returns no user name
A "success event" is
- scheduler RPC returns user name
cmdline client
first time (no projects initially) or -add_new_project flag:
new project is tentative (write flag to project file)
if failure event occurs for tentative project:
project_add_fail(PROJECT&)
print "check URL and account ID" message
delete project file
exit
If success event occurs for tentative project
project_add_succeed(PROJECT&)
clear tentative flag, rewrite account file
GUI client:
first-time dialog or "attach to project" command:
show a modal dialog
("verifying account" w/ "cancel" button)
project_add_fail(PROJECT&)
replace w/ modal error dialog w/ retry, cancel
project_add_succeed(PROJECT&)
Delete files if needed to honor disk usage constraint
should include per-result constraints (e.g. giant stderr files)