/*
Copyright 2010 Nathan Phillip Brink, Ethan Zonca, Matthew Orlando
This file is a part of DistRen.
DistRen is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
DistRen 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with DistRen. If not, see .
*/
/* This file contains the code which both processes (renders) jobs as a slave, and the code which distributes frames to slaves after receiving them from the client portion of the codebase. */
#include "common/config.h"
#include "distrenjob.h"
#include "listen.h"
#include "slavefuncs.h"
#include "mysql.h"
#include "common/asprintf.h"
#include "common/execio.h"
#include "common/options.h"
#include "common/protocol.h"
#include "common/request.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/* ******************* Structs ************************ */
struct general_info
{
struct distrenjob head;
distrend_mysql_conn_t conn;
struct distrend_config *config;
struct
{
/** general_info.xml */
char *geninfo;
/**
* where to store in-progress uploads or things that should
* otherwise be on the same filesystem as the rest of the datadir
* so that it may be rename()d.
*/
char *tmpdir;
} files;
int jobs_in_queue;
unsigned int free_clients;
unsigned int rendering_clients;
unsigned int total_finished_jobs;
unsigned int total_frames_rendered;
unsigned int highest_jobnum;
int hibernate;
time_t timestamp;
unsigned long total_render_power;
unsigned long total_priority_pieces;
};
/* *********************************************
Function Prototypes
********************************************* */
/* ************General Functions************* */
int distrend_do();
int distrend_do_config(int argc, char *argv[], struct distrend_config **config, multiio_context_t multiio);
int distrend_config_free(struct distrend_config *config);
int distrend_handle_request(struct distrend_listens *listens, struct distrend_client *client, struct distren_request *req, void *reqdata, struct general_info *geninfo);
/**
* client request handlers
*/
int distrend_handle_version(struct general_info *geninfo, struct distrend_client *client, struct distren_request *req, void *req_data);
int distrend_handle_file_post_start(struct general_info *geninfo, struct distrend_client *client, struct distren_request *req, void *req_data);
int distrend_handle_file_post(struct general_info *geninfo, struct distrend_client *client, struct distren_request *req, void *req_data);
int distrend_handle_file_post_finish(struct general_info *geninfo, struct distrend_client *client, struct distren_request *req, void *req_data);
/* functions of some generic sort ...ish */
int distrend_handle_successful_upload(struct distrend_client *client, struct distrend_client_file_post *client_file_post);
/* **************XML Functions**************** */
void update_general_info(struct general_info *geninfo);
int import_general_info(struct general_info *general_info);
int update_xml_joblist(struct general_info *geninfo);
/* **************Test Functions**************** */
int interactiveTest(int test, multiio_context_t multiio, struct general_info *general_info);
/* **************** Main ********************* */
int main(int argc, char *argv[])
{
/* Parse arguments */
int counter;
int test = 0; /*< Interactive mode if 1 */
int tmp;
struct general_info general_info;
multiio_context_t multiio;
enum clientstatus
{
CLIENTSTATUS_UNINITIALIZED = 0,
CLIENTSTATUS_BUSY = 1,
CLIENTSTATUS_IDLE = 2
} clientstatus;
fprintf(stderr, PACKAGE_STRING "\n\
Nathan Phillip Brink \n\
Ethan Zonca \n\
\n");
#ifdef HAVE_LIST_BRAG
fprintf(stderr, "Using %s\n", list_brag);
#endif
clientstatus = CLIENTSTATUS_UNINITIALIZED;
// xmlinit();
for(counter = 0; counter < argc; counter ++)
{
if(strcmp(argv[counter], "-h") == 0)
{
fprintf(stderr, "Usage: distrend [option] \nStarts the distrend server\n\t-h\tshow this help\n\t-t\tlaunches queue testing interface \n");
return 2;
}
else if(strcmp(argv[counter], "-t") == 0)
{
fprintf(stderr, "Entering into test mode...\n\n");
test = 1;
}
}
multiio = multiio_context_new();
if(distrend_do_config(argc, argv, &general_info.config, multiio))
return 1;
/** preset paths */
_distren_asprintf(&general_info.files.geninfo, "%s/general_info.xml",
general_info.config->datadir);
_distren_asprintf(&general_info.files.tmpdir, "%s/tmp",
general_info.config->datadir);
distren_mkdir_recurse(general_info.files.tmpdir);
/** MySQL Connection */
fprintf(stderr,"Connecting to mysql...\n");
if(mysqlConnect(&general_info.conn,
general_info.config->mysql_user,
general_info.config->mysql_host,
general_info.config->mysql_pass,
general_info.config->mysql_database) )
{
fprintf(stderr, "%s:%d: mysqlConnect() failed\n", __FILE__, __LINE__);
fprintf(stderr, "don't test mysql stuff\n");
interactiveTest(test, multiio, &general_info);
return 1;
}
fprintf(stderr,"Finished connecting!\n");
/** Execute test function */
interactiveTest(test, multiio, &general_info);
general_info.config->listens = distrend_listens_new(multiio, &general_info, general_info.config->options);
if(!general_info.config->listens)
{
fprintf(stderr, "error initializing listens\n");
return 1;
}
remoteio_generic_data_set(general_info.config->options->remoteio, general_info.config->listens);
for(counter = 0; general_info.config->listen_ports[counter]; counter ++)
{
tmp = distrend_listen_add(general_info.config->listens, general_info.config->listen_ports[counter]);
if(tmp)
{
fprintf(stderr, "Error listening on port %d\n", general_info.config->listen_ports[counter]);
return 1;
}
}
distrend_listen_handler_add(general_info.config->listens, DISTREN_REQUEST_VERSION, &distrend_handle_version);
distrend_listen_handler_add(general_info.config->listens, DISTREN_REQUEST_FILE_POST_START, &distrend_handle_file_post_start);
distrend_listen_handler_add(general_info.config->listens, DISTREN_REQUEST_FILE_POST, &distrend_handle_file_post);
distrend_listen_handler_add(general_info.config->listens, DISTREN_REQUEST_FILE_POST_FINISH, &distrend_handle_file_post_finish);
/* Main Loop */
general_info.config->die = 0;
while(!general_info.config->die)
{
multiio_poll(multiio, 15000);
tabletennis_serve(general_info.config->listens->tabletennis);
/* Run the watchdog, @TODO: like every 10 mins or something */
frame_watchdog(general_info.conn);
}
distrend_listen_free(general_info.config->listens);
distrend_config_free(general_info.config);
xmlcleanup();
/** free() paths */
free(general_info.files.geninfo);
mysqlDisconnect(general_info.conn);
return 0;
}
/* ********************** Functions ************************* */
int distrend_handle_version(struct general_info *geninfo, struct distrend_client *client, struct distren_request *req, void *req_data)
{
char *tmp_str;
struct distren_request_version version;
if(distren_request_parse_version(req, req_data, &version))
{
distrend_send_disconnect(client, "Invalid DISTREN_REQUEST_VERSION packet.");
return 1;
}
if(client->state != DISTREND_CLIENT_PREVERSION)
{
distrend_send_disconnect(client, "You have already sent the VERSION command.");
return 1;
}
if(!strncmp(PACKAGE_STRING, version.package_string, DISTREN_REQUEST_VERSION_PACKAGE_STRING_LEN))
{
/**
* The client and I claim to be of the same version of distren :-D
* Now we will mark the client as valid.
*
* We won't increment his time to live, though, because it shouldn't take
* him that long to auth.
*/
client->state = DISTREND_CLIENT_PREAUTH;
}
else
{
/**
* The client claims to be of a different version of distren.
* Now we will just send a disconnect packet and disconnect the client.
*/
_distren_asprintf(&tmp_str, "You have tried to connect to a %s server when your client claims to be running %s. Bye ;-)\n", PACKAGE_STRING, version.package_string);
if(tmp_str)
{
distrend_send_disconnect(client, tmp_str);
free(tmp_str);
}
else
distrend_send_disconnect(client, "Invalid PACKAGE_VERSION :-|.");
return 1;
}
return 0;
}
/**
* Traversal helper for distrend_client_find_post().
*/
int distrend_client_find_post_traverse(uint32_t *post_id, struct distrend_client_file_post *client_file_post)
{
if(*post_id == client_file_post->post_id)
return FALSE;
return TRUE;
}
/**
* Find the record for an in-progress client's file posting.
*/
struct distrend_client_file_post *distrend_client_find_post(struct distrend_client *client, uint32_t post_id)
{
if(list_traverse(client->file_post_list, &post_id, (list_traverse_func_t)&distrend_client_find_post_traverse, LIST_ALTR | LIST_FORW | LIST_FRNT) != LIST_EXTENT)
return list_curr(client->file_post_list);
return NULL;
}
/**
* Finds a post_context based on the post_id and client.
*
* Compatible the distren_request_parse_file_post_find_context_func_t.
*/
static distren_request_file_post_context_t distrend_client_find_file_post_context(uint32_t post_id, void *client)
{
struct distrend_client_file_post *client_file_post;
client_file_post = distrend_client_find_post(client, post_id);
if(client_file_post)
return client_file_post->post_context;
return NULL;
}
/**
* Clean up and free a client_file_post
*
* Whenever calling this functino, you almost _always_ have to call
* list_remove_element(client->file_post_list, client_file_post);
* first.
*/
void distrend_client_file_post_free(struct distrend_client_file_post *client_file_post)
{
fclose(client_file_post->fd);
free(client_file_post->filename);
unlink(client_file_post->file_save_path);
free(client_file_post->file_save_path);
free(client_file_post);
}
int distrend_handle_file_post_start(struct general_info *geninfo, struct distrend_client *client, struct distren_request *req, void *req_data)
{
struct distrend_client_file_post *client_file_post;
distren_request_file_post_context_t post_context;
uint32_t post_id;
char *filename;
char *str_tmp;
int ret;
/**
* @todo access check!
*/
fprintf(stderr, __FILE__ ":%d:distrend_handle_file_post_start(): You need to check if a client is actually allowed to upload files somehow!\n", __LINE__);
/*
* other servers should be excluded from this check, but we don't
* store the servertype in client yet.
*/
if(list_size(client->file_post_list) > 1)
{
distrend_send_disconnect(client, "You are trying to upload too many files at once!");
return 1;
}
ret = distren_request_parse_file_post_start(req, req_data, &post_context, &post_id, &filename);
if(ret)
{
distrend_send_disconnect(client, "You sent me an invalid DISTREN_REQUEST_FILE_POST_START packet");
return 1;
}
if(distrend_client_find_post(client, post_id))
{
_distren_asprintf(&str_tmp, "Err accepting file: You are trying to upload using post_id=%d while you have already started another upload using the same post_id", post_id);
distrend_send_disconnect(client, str_tmp);
free(str_tmp);
distren_request_file_post_context_free(post_context);
return 1;
}
client_file_post = malloc(sizeof(struct distrend_client_file_post));
if(!client_file_post)
{
distrend_send_disconnect(client, "Error accepting file: out of memory");
distren_request_file_post_context_free(post_context);
return 1;
}
client_file_post->post_context = post_context;
client_file_post->post_id = post_id;
client_file_post->filename = filename;
_distren_asprintf(&client_file_post->file_save_path, "%s/conn-%d_file_post-%d",
geninfo->files.tmpdir, client->connection_id, post_id);
client_file_post->fd = fopen(client_file_post->file_save_path, "w");
if(!client_file_post->fd)
{
perror("fopen");
fprintf(stderr, "error: Unable to open ``%s''. See above ``fopen'' error for more details.\n", client_file_post->file_save_path);
distrend_send_disconnect(client, "Error accepting file: unable to store the file n disk");
distren_request_file_post_context_free(post_context);
list_remove_element(client->file_post_list, client_file_post);
distrend_client_file_post_free(client_file_post);
return 1;
}
list_insert_after(client->file_post_list, client_file_post, 0);
return 0;
}
int distrend_handle_file_post(struct general_info *geninfo, struct distrend_client *client, struct distren_request *req, void *req_data)
{
struct distrend_client_file_post *client_file_post;
void *file_data;
size_t file_data_len;
uint32_t post_id;
char *tmp_str;
size_t written_len;
int ret;
ret = distren_request_parse_file_post(req, req_data, &post_id,
&distrend_client_find_file_post_context, client,
&file_data, &file_data_len);
if(ret)
{
distrend_send_disconnect(client, "You sent me an invalid DISTREN_REQUEST_FILE_POST packet");
return 1;
}
client_file_post = distrend_client_find_post(client, post_id);
if(!client_file_post)
{
_distren_asprintf(&tmp_str, "You are attempting to upload post_id=%d when you haven't given a DISTREN_REQUEST_FILE_POST_START packet", post_id);
distrend_send_disconnect(client, tmp_str);
free(tmp_str);
return 1;
}
written_len = fwrite(file_data, 1, file_data_len, client_file_post->fd);
if(written_len < file_data_len)
{
distrend_send_disconnect(client, "Error saving upload: error while writing to the temporary upload file");
list_remove_element(client->file_post_list, client_file_post);;
/* closes the file being written, and free()s everything, unlinks the file */
distrend_client_file_post_free(client_file_post);
return 1;
}
return 0;
}
/**
* here be magic
*/
int distrend_handle_file_post_finish(struct general_info *geninfo, struct distrend_client *client, struct distren_request *req, void *req_data)
{
struct distrend_client_file_post *client_file_post;
uint32_t post_id;
int ret;
ret = distren_request_parse_file_post_finish(req, req_data, &post_id,
&distrend_client_find_file_post_context, client);
if(ret)
{
switch(ret)
{
case 2:
/* client asked to cancel a file upload */
client_file_post = distrend_client_find_post(client, post_id);
if(client_file_post)
{
list_remove_element(client->file_post_list, client_file_post);
distrend_client_file_post_free(client_file_post);
}
return 0;
case 3:
/* checksuming failed */
distrend_send_disconnect(client, "You have uploaded a file that doesn't match its checksum somehow... which should be pretty much impossible");
client_file_post = distrend_client_find_post(client, post_id);
if(client_file_post)
{
list_remove_element(client->file_post_list, client_file_post);
distrend_client_file_post_free(client_file_post);
}
return 1;
default:
distrend_send_disconnect(client, "You sent me an invalid DISTREN_REQUEST_FILE_POST_FINISH packet");
}
return 1;
}
client_file_post = distrend_client_find_post(client, post_id);
if(!client_file_post)
return 1;
/*
* Here it is... manage a file being submitted for rendering... or
* for whatever purpose it was uploaded for somehow...
*/
distrend_handle_successful_upload(client, client_file_post);
list_remove_element(client->file_post_list, client_file_post);
distrend_client_file_post_free(client_file_post);
return 0;
}
/**
* Does stuff with file uploads after they've been successfully acquired.
*
* @todo this should probably be genericized to handle 1. file uploads and 2. server-initiated file _downloads_, i.e., using libcurl. In that case, struct distrend_client_file_post shouldn't be passed directly to ourself but some common struct might be passed here.
*/
int distrend_handle_successful_upload(struct distrend_client *client, struct distrend_client_file_post *client_file_post)
{
fprintf(stderr, __FILE__ ":%d: STUB: I don't know what to do with %s[%s] :-/\n", __LINE__,
client_file_post->filename, client_file_post->file_save_path);
return 0;
}
/**
Performs command stored in a client's request. @TODO: Fill stub
*/
int distrend_do()
{
return 0;
}
/* Grabs config info from confs */
int distrend_do_config(int argc, char *argv[], struct distrend_config **config, multiio_context_t multiio)
{
unsigned int counter;
cfg_opt_t myopts_listen[] =
{
CFG_SIMPLE_STR("type", NULL),
CFG_SIMPLE_STR("path", NULL),
CFG_SIMPLE_INT("port", NULL),
CFG_END()
};
cfg_opt_t myopts[] =
{
CFG_SEC("listen", /* this must be imported into struct listens (which must still be declared) */
myopts_listen,
CFGF_MULTI),
CFG_SIMPLE_STR("datadir", NULL),
CFG_STR_LIST("render_types", NULL, CFGF_NONE),
CFG_SIMPLE_STR("mysql_user", NULL),
CFG_SIMPLE_STR("mysql_host", NULL),
CFG_SIMPLE_STR("mysql_pass", NULL),
CFG_SIMPLE_STR("mysql_database", NULL),
CFG_END()
};
cfg_t *cfg_listen;
fprintf(stderr, "%s:%d: running config\n", __FILE__, __LINE__);
*config = malloc(sizeof(struct distrend_config));
myopts[1].simple_value = &(*config)->datadir;
myopts[3].simple_value = &(*config)->mysql_user;
myopts[4].simple_value = &(*config)->mysql_host;
myopts[5].simple_value = &(*config)->mysql_pass;
myopts[6].simple_value = &(*config)->mysql_database;
if(options_init(argc, argv, &(*config)->mycfg, myopts, "daemon", &(*config)->options, multiio))
return 1;
/**
grab listen blocks:
*/
(*config)->listen_ports = malloc(sizeof(int) * (cfg_size((*config)->mycfg, "listen") + 1));
for(counter = 0; counter < cfg_size((*config)->mycfg, "listen"); counter ++)
{
cfg_listen = cfg_getnsec((*config)->mycfg, "listen", counter);
(*config)->listen_ports[counter] = cfg_getint(cfg_listen, "port");
}
(*config)->listen_ports[counter] = 0;
fprintf(stderr, "using %s as datadir\n", (*config)->datadir);
return 0;
}
int distrend_config_free(struct distrend_config *config)
{
distrend_listen_free(config->listens);
options_free(config->options);
free(config->listen_ports);
free(config);
return 0;
}
/* ************************** XML Functions ************************* */
// writes the general_info.xml file which is a copy of the general_info structure
// except that it doesn't hold free_clients and rendering_clients
void update_general_info(struct general_info *geninfo)
{
xmlTextWriterPtr writer;
char *tmp;
writer = xmlNewTextWriterFilename(geninfo->files.geninfo, 0);
xmlTextWriterStartDocument(writer, NULL, "utf-8", NULL);
xmlTextWriterStartElement(writer, (xmlChar*)"general_info");
_distren_asprintf(&tmp, "%d", geninfo->jobs_in_queue);
xmlTextWriterWriteElement(writer, (xmlChar*)"jobs_in_queue", (xmlChar*)tmp);
free(tmp);
_distren_asprintf(&tmp, "%d", geninfo->total_finished_jobs);
xmlTextWriterWriteElement(writer, (xmlChar*)"total_finished_jobs", (xmlChar*)tmp);
free(tmp);
_distren_asprintf(&tmp, "%d", geninfo->total_frames_rendered);
xmlTextWriterWriteElement(writer, (xmlChar*)"total_frames_rendered", (xmlChar*)tmp);
free(tmp);
_distren_asprintf(&tmp, "%d", geninfo->highest_jobnum);
xmlTextWriterWriteElement(writer, (xmlChar*)"highest_jobnum", (xmlChar*)tmp);
free(tmp);
xmlTextWriterEndDocument(writer);
xmlFreeTextWriter(writer);
}
/**
Reads general state information from general_info.xml
into the general_info structure.
*/
int import_general_info(struct general_info *general_info)
{
xmlDocPtr doc;
xmlNodePtr cur;
doc = xmlParseFile(general_info->files.geninfo);
cur = xmlDocGetRootElement(doc);
if (xmlStrcmp(cur->name, (xmlChar*)"general_info"))
{
fprintf(stderr, "xml document is wrong type");
xmlFreeDoc(doc);
return 1;
}
cur = cur->xmlChildrenNode;
general_info->jobs_in_queue = atoi((char*)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1));
cur = cur->next;
general_info->total_finished_jobs = atoi((char*)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1));
cur = cur->next;
general_info->total_frames_rendered = atoi((char*)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1));
cur = cur->next;
general_info->highest_jobnum = atoi((char*)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1));
general_info->hibernate = 0;
general_info->free_clients = 0;
general_info->rendering_clients = 0;
general_info->timestamp = 0;
general_info->total_render_power = 0;
general_info->total_priority_pieces = 0;
xmlFreeDoc(doc);
return 0;
}
/**
updates job_list.xml which lists all the jobs in the queue
@return 0 means success
*/
// @QUERY: Likely obselete (don't remove at request of ohnobinki)
int update_xml_joblist(struct general_info *geninfo)
{
struct distrenjob *job;
xmlTextWriterPtr writer;
char *tmp;
int counter;
/**
update timestamp
*/
geninfo->timestamp ++;
if(geninfo->timestamp > 65530)
geninfo->timestamp = 0;
_distren_asprintf(&tmp, "%s/job_list.xml",
geninfo->config->datadir);
writer = xmlNewTextWriterFilename(tmp, 0);
free(tmp);
xmlTextWriterStartDocument(writer, NULL, "utf-8", NULL);
/**
create root element job_list
*/
xmlTextWriterStartElement(writer, (xmlChar*)"job_list");
_distren_asprintf(&tmp, "%d", geninfo->timestamp);
xmlTextWriterWriteAttribute(writer, (xmlChar*)"timestamp", (xmlChar*)tmp);
free(tmp);
geninfo->total_priority_pieces = 0;
counter = 0;
for(job = geninfo->head.next; job; job = job->next)
{
_distren_asprintf(&tmp, "%d", job->jobnum);
xmlTextWriterWriteElement(writer, (xmlChar*)"jobnum", (xmlChar*)tmp);
free(tmp);
/**
this is needed for the new frame finder to work
Why the random constant numeral 11? --ohnobinki
*/
geninfo->total_priority_pieces = geninfo->total_priority_pieces + job->priority;
counter++;
}
xmlTextWriterEndElement(writer);
/**
close elements and end document
*/
xmlTextWriterEndDocument(writer);
/**
free writer and save xml file to disk
*/
xmlFreeTextWriter(writer);
return 0;
}
/* ************************** Test Functions ************************* */
/** Interactive test for the queuing system */
/* @QUEUE: Test uses methods not present in C code using mysql web-based system */
int interactiveTest(int test, multiio_context_t multiio, struct general_info *geninfo)
{
int command;
int32_t slaveKey = 1;
jobnum_t jobKey = 0;
int32_t frameNum = 0;
int32_t newPriority = 0;
int tmp = 0;
fprintf(stderr,"Hello!\n");
while(test == 1)
{
fprintf(stderr, "Welcome to DistRen Alpha Interactive Test Mode\n\n");
fprintf(stderr, "\t1 \tGet a frame to render\n");
fprintf(stderr, "\t2 \tChange job priority\n");
fprintf(stderr, "\t3 \tSet frame finished\n");
fprintf(stderr, "\t4 \tSet frame started\n");
fprintf(stderr, "\t5 \tStart listener\n");
fprintf(stderr, "\t0 \tQuit\n");
scanf("%d", &command);
switch(command)
{
case 1:
fprintf(stderr,"Slave Key: ");
scanf("%d", &slaveKey);
fprintf(stderr, "Got frame: ");
if(find_jobframe(geninfo->conn, slaveKey, &jobKey, &frameNum))
fprintf(stderr,"No frames available to render!\n");
else if(jobKey == -1)
fprintf(stderr,"Slave %d has no render power!", slaveKey);
else
fprintf(stderr, "jobKey: %d, frameNum: %d\n",jobKey,frameNum);
break;
case 2:
fprintf(stderr,"Job key: ");
scanf("%d", &tmp);
jobKey = tmp;
fprintf(stderr,"New priority: ");
scanf("%d", &tmp);
newPriority = tmp;
change_job_priority(geninfo->conn, jobKey, newPriority);
fprintf(stderr,"Changed!");
break;
case 3:
fprintf(stderr,"Slave Key: ");
scanf("%d", &tmp);
slaveKey = tmp;
fprintf(stderr,"Job Key: ");
scanf("%d", &tmp);
jobKey = tmp;
fprintf(stderr,"Frame Number: ");
scanf("%d", &tmp);
frameNum = tmp;
finish_frame(geninfo->conn, slaveKey, jobKey, frameNum);
fprintf(stderr,"Finished Frame!\n");
break;
case 4:
fprintf(stderr,"Slave Key: ");
scanf("%d", &tmp);
slaveKey = tmp;
fprintf(stderr,"Job Key: ");
scanf("%d", &tmp);
jobKey = tmp;
fprintf(stderr,"Frame Number: ");
scanf("%d", &tmp);
frameNum = tmp;
start_frame(geninfo->conn, slaveKey, jobKey, frameNum);
fprintf(stderr,"Started Frame!\n");
break;
case 5:
while(1)
{
multiio_poll(multiio, 15000);
tabletennis_serve(geninfo->config->listens->tabletennis);
}
break;
case 0:
test = 0;
break;
default:
fprintf(stderr, "Invalid input, please try again.\n");
break;
}
}
return 0;
}