/*
Copyright 2009 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. */
/* Just some notes -- Ethan Zonca
* ++ Make data availible for other apps to parse
* Server<==>Client Communication
* Upload while rendering
*/
/* XML notes --
*
* First off, is XML in our build environment?
* Next, what files are we going to have?:
* - XML file sent with (.blend/.pov/.lux(xml)/whatever) by distren telling the server what to do
* + Includes submitter name, filename, emailaddress, etc
* - XML file on server storing info on current and finished jobs
* - XML file sent by distren when submitting a render to the server
*
* Overall, we could use XML to make our client/server communication load
* a lot lighter, especially if clients are sent customized xml files telling
* them what frame(s) to render... although that would be inefficient if rendering
* single frames. Maybe not tell it what frames to render in the xml file, the server
* can spit out that info.
*
* Please edit my ramblings if you please,
* --ethanzonca
*
*/
#include "execio.h"
#include "options.h"
#include
#include
#include
#include
#include /* getopt */
#include
typedef unsigned int jobnum_t;
/* ******************* Structs ************************ */
// Stores config info? editmycomment
struct distrend_config
{
cfg_t *mycfg;
struct options_common *options;
struct distrend_listen **listens; /*< Null terminated array of structs */
};
/*
frame[frame] Status Assignments:
"NULL" - don't render me
"0" - canceled
"1" - unassigned
"2" - assigned to slave
"3" - completed by slave and uploaded
Have a script crawl through each job in the arrays, priority-biased, and assign a frame to each slave.
Then we will need some sort of watchdog to monitor slaves on the main server to check for stales. Maybe not worry about this for now.
*/
struct general_info {
short int jobs_in_queue; //
unsigned short int free_clients;
unsigned short int rendering_clients;//
unsigned short int total_finished_jobs; //
unsigned int total_frames_rendered; //
} general_info;
// Stores Blender Job Info
struct blendjob {
struct blendjob *next; /* next will be NULL unless if there is another blendjob */
char *name;
char *submitter;
char *email;
jobnum_t jobnum;
int priority; // 1 is lowest, 10 is highest, 0 means the job is done
int percent_done;
int completed_frames; // number of completed frames for stats/etc
int assigned_frames; // number of assigned frames (that are not yet completed) for stats/etc
int total_frames; // how many frames are in the animation for stats/etc (unassigned frames)
int avg_render_time; // average seconds it took to render a frame
unsigned int time_remaining; // estimated seconds remaining till render is complete (up to 49, 710 days)
// we can have the client computer convert it into days, hours, etc if they wish to view it
struct frameset *frameset;
};
// Frameset Structure
struct frameset {
int frame_num; // frame number to render
char slave_name; // user that frame is assigned to
int frame_status; // status of frame, 0= unassigned, 1= taken, 2= done
clock_t start_time; // time the frame was started
int time_to_render; // the total seconds it took to render the frame
} frameset[]; // Frameset array is generated by status_report_generator function
// Using this method to save memory, because if animation starts on a high frame number, it would waste a lot of RAM on empty structures
/*
internally defined funcs's prototypes
*/
void status_report_generator(struct blendjob **blendjobs_head);
void blendjob_remove(struct blendjob **head, struct blendjob *bj);
struct blendjob *blendjob_get(struct blendjob *head, jobnum_t jobnum);
/* Global Vars, try to cut down on these */
jobnum_t jobnum = 0; // The next job number to create in the queue
int hcfjob; // Highest consecutively finished job
int highest_jobnum; // The job number of the most recently created job, this is used when creating new jobs
/* ********************** Functions ************************* */
void start_data(){
if(1 == 0){
// retrieve total_finished_jobs and total_finished_frames from xml file
}
else{
general_info.total_finished_jobs = 0;
general_info.total_frames_rendered = 0;
}
}
// **** Finish-Setter: Sets a frame to the "completed" status.
void finish_frame(struct blendjob *blendjob, int frame){
blendjob->frameset[frame].frame_status = 2;
blendjob->frameset[frame].time_to_render = (clock() - blendjob[jobnum].frameset[frame].start_time); // Consider changin time-to-render to time-for-frame or something?
general_info.total_frames_rendered++; // Increase total frames var for stats
}
// **** Queuer: Adds files to the queue
void queue(struct blendjob *blendjob, int type, char *name, char *submitter, char *email, int priority, int mode, int spp, struct frameset *frameset) {
// Type: 1 = blender, add more types later
// jobnum is the next available job number
if(type == 1){
blendjob->name = name;
blendjob->submitter = submitter;
blendjob->email = email;
blendjob->priority = priority;
blendjob->frameset = frameset;
}
else{
// Throw error.
}
jobnum++; // Advance the jobnumber for the next queued job
}
/*
Status Report Generator: figures out how much of the job is done, where jobnum corresponds to the job number
blendjobs_head is a pointer to a pointer because the head of the blendjobs linked list may need to be changed by blendjob_remove
*/
void status_report_generator(struct blendjob **blendjobs_head)
{
struct blendjob *blendjob_ptr;
unsigned short workers_working; /*< used to count the total number of workers working */
unsigned int numjobs; /*< used to track number of jobs */
blendjob_ptr = *blendjobs_head;
workers_working = 0;
numjobs = 0;
while(blendjob_ptr)
{
if(blendjob_ptr->priority != 0)
/* If the job is not done, scan it */
{
unsigned int framecounter; /*< to scan through frames */
unsigned long finished_frames; /*< variable that counts the completed frames */
unsigned int pending_frames; /*< variable that counts the assigned frames */
float percent_frames_finished; /*< variable that stores the percent done of the blendjob */
unsigned int total_time; /*< total time taken to render all the completed frames for a job */
framecounter = 0;
finished_frames = 0;
pending_frames = 0;
percent_frames_finished = 0;
total_time = 0;
while(framecounter < blendjob_ptr->total_frames)
/* scans through frames, based on their status it runs a statement(s) */
{
if(blendjob_ptr->frameset[framecounter].frame_status == 2)
/* If the frame is done */
{
finished_frames ++;
total_time += blendjob_ptr->frameset[framecounter].time_to_render;
}
if(blendjob_ptr->frameset[framecounter].frame_status == 1)
/* If the frame is assigned */
{
pending_frames ++;
workers_working ++;
}
framecounter ++;
} /* while(framecounter < blendjob_ptr->total_frames) */
// find the percent of completed frames
percent_frames_finished = (finished_frames / blendjob_ptr->total_frames) * 100; /*< @LordofWar: extraneous parentheses! */
/* updates values in the blendjob struct */
blendjob_ptr->completed_frames = finished_frames;
blendjob_ptr->assigned_frames = pending_frames;
blendjob_ptr->percent_done = percent_frames_finished;
blendjob_ptr->avg_render_time = (total_time / finished_frames); /*< extraneous parentheses! */
blendjob_ptr->time_remaining = (blendjob_ptr->avg_render_time * (blendjob_ptr->total_frames - finished_frames)); /*< extraneous parentheses! */
if(finished_frames == blendjob_ptr->total_frames)
/* If all frames are complete */
{
blendjob_ptr->priority = 0; /*< set priority to zero to indicate job is complete */
blendjob_remove(blendjobs_head, blendjob_ptr); /*< remove this job from the linkedlist */
general_info.total_finished_jobs++; /*< add one to the total finished jobs */
}
else if (finished_frames > blendjob_ptr->total_frames)
/* just in case ;-) */
{
fprintf(stderr, "%s:%d: finished_frames (%lu) > blendjob_ptr->total_frames (%d)",
__FILE__, __LINE__,
finished_frames,
blendjob_ptr->total_frames);
abort();
}
}
general_info.rendering_clients = workers_working; /*< should this be a +=? */
blendjob_ptr = blendjob_ptr->next; /*< This is the essence of linked lists and iterating through them */
numjobs ++;
} /* while(blendjob_ptr) */
general_info.jobs_in_queue = (highest_jobnum - general_info.total_finished_jobs); /*< extraneous parentheses! */
}
// **** Structure Builder: This function creates frame array based on the total number of frames to be rendered, which will then be parsed by function frame_farmer.
void frame_num_struct_builder(struct blendjob *job, unsigned int startframe, unsigned int numframes) {
int jobnum_new = highest_jobnum + 1; /* global vars will someday leave us */
unsigned int counter;
job->frameset = malloc(sizeof(struct frameset) * numframes);
if(!job->frameset)
fprintf(stderr, "error allocating memory");
job->total_frames = numframes; // sets the total number of frames in animation for status purposes
job->jobnum = jobnum_new;
for(counter = 0; counter < numframes; counter ++)
/* This builds the array, with the array starting at zero and the frame_num starting at sframe */
job->frameset[counter].frame_num = counter + startframe;
highest_jobnum++; // After it has created the job, it adds one to the highest_jobnum interger
}
// Frame Assigner: matches your computer up with a lovely frame to render
// Major issue here, the client needs to know the frame number, AND the job number!
int frame_finder(){
int your_frame = 0; // your_frame is an interger value that will be given to the client as the frame number to render
int finder_jobnum = 0;
int frameset_count = 0; // the frameset number, note* frames in an animation don't start at zero
short int done = 0;
short int priority = 10;
while(priority >= 1){ // start the scan for the next job with the highest priority, decreases priority before it loops
finder_jobnum = hcfjob + 1; // reset it to start scanning at first uncompleted job for the pass at each priority level
while(finder_jobnum <= highest_jobnum){ // This keeps increasing the finder_jobnum until it is higher than the highest_jobnum
if(blendjob[finder_jobnum].priority == priority){ // looks for a job with the current priority value
done = 1; // notice it starts by looking at the oldest job first
break;
}
if((done) == 1) // If it has found a job with the current priority value, it will break out of the loop
break; // If none is found it goes to the next job to see if it is of the current priority value
else
finder_jobnum++;
} // End of increasing finder_jobnum
if(done == 1) // if job has been found, it lets it out of the priority changer loop
break;
priority--; // end of decreasing priority
}
while(your_frame < blendjob[finder_jobnum].total_frames){ // Finds the frameset number with a frame that needs to be rendered
if (blendjob[finder_jobnum].frameset[frameset_count].frame_status == 0) // If frame that is not assigned has been found, frameset_count is not changed
break; // and frameset_count is used to give the frame number later in this funciton
frameset_count++; // If frame is assigned or done, it goes to next frame
}
blendjob[jobnum].frameset[frameset_count].frame_status++; // sets the value of the frame to 2, which means its taken
your_frame = blendjob[jobnum].frameset[frameset_count].frame_num; // Takes the frameset found in the while statement above, and extracts the frame number from it and assigns it to the int your_frame
if(your_frame == 0) // If that job had no open frames for some reason, run the status report generator so that
status_report_generator(blendjobs_head); //the job priority can be changed to 0
blendjob[jobnum].frameset[frameset_count].start_time = clock();
return your_frame; // your_frame is returned as the frame to be rendered
}
void blend_frame_watchdog(struct blendjob *blendjob_head)
{
unsigned short int watchdog_forgiveness; /*< seconds to wait on a frame before re-assigning it */
struct blendjob *blendjob_ptr;
unsigned int counter;
watchdog_forgiveness = 3; /*< hours of forgiveness before frame is re-assigned */
blendjob_ptr = blendjob_head;
for(blendjob_ptr = blendjob_head; blendjob_ptr; blendjob_ptr = blendjob_ptr->next)
/* iterate through jobs */
for(counter = 0; counter < blendjob_ptr->total_frames; counter ++)
/* iterate through all frames for this job*/
{
if((blendjob_ptr->frameset[counter].start_time + (watchdog_forgiveness * 3600)) < clock())
/*
If frame is not completed within the number of hours specified by watchdog_forgiveness
Then change the frame status to unassigned
*/
blendjob_ptr->frameset[counter].frame_status = 0;
}
}
/**
Finds a blendjob struct based on the jobnum
@arg jobnum job number to search for
@return NULL on job doesn't exist
*/
struct blendjob *blendjob_get(struct blendjob *head, jobnum_t jobnum)
{
struct blendjob *blendjob_ptr;
/*
The conditions of the for loop will leave blendjob_ptr at NULL if the end of the list is reached. It will leave it pointing to the correct job if it is found.
*/
for(blendjob_ptr = head;
blendjob_ptr
&& blendjob_ptr->jobnum != jobnum;
blendjob_ptr = blendjob_ptr->next);
return blendjob_ptr;
}
/**
Removes a blendjob from the blendjob linkelist.
@arg head a double pointer. the head pointer will have to be changed if blendjob == *head. Thus, make sure that the pointer points to the pointer to the head that all functions use. (I'm going to come back to this and misunderstand myself ;-))
*/
void blendjob_remove(struct blendjob **head, struct blendjob *bj)
{
struct blendjob *previous_blendjob;
if(bj == *head)
*head = bj->next;
else
{
for(previous_blendjob = *head;
previous_blendjob
&& previous_blendjob->next != bj; /*< stop on the blendjob that comes before bj */
previous_blendjob = previous_blendjob->next)
/* all of the action is in the definition of the for loop itself */;
/*
This removes references to bj from the linked list. I.E., we now skip bj when iterating through the list
*/
previous_blendjob->next = bj->next;
}
/*
@lordofwar: the magic deallocation of memory ;-)
*/
free(bj);
}
/* ************************** Main ************************* */
int main(int argc, char *argv[])
{
//// Begin Getopt Sample, EDITME!Sometime
int aflag = 0;
int bflag = 0;
char *cvalue = NULL;
int index;
int c;
opterr = 0;
while ((c = getopt (argc, argv, "abc:")) != -1)
switch (c)
{
case 'a':
aflag = 1;
break;
case 'b':
bflag = 1;
break;
case 'c':
cvalue = optarg;
break;
case '?':
if (optopt == 'c')
fprintf (stderr, "Option -%c requires an argument.\n", optopt);
else if (isprint (optopt))
fprintf (stderr, "Unknown option `-%c'.\n", optopt);
else
fprintf (stderr,
"Unknown option character `\\x%x'.\n",
optopt);
return 1;
default:
abort ();
}
printf ("aflag = %d, bflag = %d, cvalue = %s\n",
aflag, bflag, cvalue);
for (index = optind; index < argc; index++)
printf ("Non-option argument %s\n", argv[index]);
//// End getopt sample
// Just wrote this to try and piece everything together after writing it on paper :D --ethanzonca
///////////////////// Semi-pseudo-code for server //////////////////////////////////////////
/*
start_data();
status_report_generator();
blend_frame_watchdog();
if(client says "i ain't got nuthin to render!"){
frame_finder() ==> returns jobnum/framenum that are sent to slave, "render jobnum framenum"
}
if(client says "done with frame # in job #"){
finish_frame(jobnum, frame);
frame_finder() ==> returns jobnum/framenum that are sent to slave, "render jobnum framenum"
}
*/
///////////////////////////////////////////////////////////////////////////////////////
// Begin non-working framework?
int distrend_do_config(int argc, char *argv[], struct distrend_config *config)
{
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_END()
};
config = malloc(sizeof(struct distrend_config));
options_init(argc, argv, &config->mycfg, myopts, "server", &config->options);
return 0;
}
int distrend_config_free(struct distrend_config *config)
{
options_free(config->options);
free(config);
return 0;
}
// End non-working framework?
int cont = 1;
struct distrend_listenset *listenset;
struct distrend_config *config;
distrend_do_config(argc, argv, config);
distrend_listen(&listenset, config);
/* This is called the ``main loop'' */
while(cont)
{
struct distren_action *action;
distrend_accept(&action);
cont = distrend_do(action);
distrend_action_free(action);
}
distrend_unlisten(listenset);
distrend_config_free(config);
return 0;
}
/*
* **********************************************************************************
* Slave functions / etc resides below. Wouldn't a seperate file make this easier??
*
* Slave listens on server for a command in the format of each function...
* We need if's for returns... ==> watchdog
* **********************************************************************************
*/
///////////////////// Semi-pseudo Slave Code ///////////////////////////
/*
int slavestatus = 0;
if(no username in config file, no key present){
fprintf(stderr "run distrend -c username] [emailaddr] to register")
}
if(they use a -c flag according to getopt, with 2 args){
register_user(username, email); which returns a uniquely random url to a key
get(uniquely-random-url-to-key);
fprintf(stderr, "you registered, hopefully successfully. Invoke distrend with no args now.");
}
if(username is in conf file and key is present){
loginuser(username);
}
if(slave recieves "start frame#, job#"){
get(http://distren.protofusion.org/srv/job#.tgz);
tar -xvf job#.tgz /tmp/distren/job#; somehow
exec_blender(job#.blend, job#.frame#.JPG, job#); (check the args, we'll need to adjust for different output formats)... set SLAVESTATUS=1 while rendering, SLAVESTATUS=2 when done
}
if(SLAVESTATUS==2){
tell the server "done with frame# in job#";
SLAVESTATUS=0
}
if(SLAVESTATUS==0){
tell the server "i ain't got no frames to render";
}
while(SLAVESTATUS==1){
tell the server "rendering this friggn frame";
delay(1000);
}
*/
////////////////////////////////////////////////////////////////////////
// Registration on server. Needs attention. Prevent account spamming.
// Key transfer?
// Set up something like: distrend -c username email@example.com
void registeruser(char *username, char *email){
// Logs into sandboxed user on zserver2 and registers a user. Should eventually generate a key on the server and return it to the user.
// All created user accounts should be sandboxed accordingly, requiring a different skel, and the default shell to be rbash. Also,
// a custom path defined in the .bash_profile of the skel is needed.
char buf[10];
struct execio *testrem;
char *execargv[] =
{
"ssh",
"distren_setup@protofusion.org",
"-i",
"setup.rsa", // default distributed key, account can only create users.
"-p",
"23",
"sudo /usr/sbin/useradd",
"-M",
"-c",
email,
"-d",
"/home/distren",
"--gid",
"541", // Add in shellscript to generate ssh key and return it to the user somehow
username,
(char *)NULL
};
size_t readlen;
fprintf(stderr, "Opening stream:\n", execio_open(&testrem, "ssh", execargv));
buf[9] = '\0'; // null-terminating the array...
while(!execio_read(testrem, buf, 9, &readlen)) // What's with the readlen stuff?
{
if(readlen > 9) {
fprintf(stderr, "!!!! Something is terribly wrong!\n");
}
buf[readlen] = '\0'; // Null-terminating the end of it again based on how large the data is?
fprintf(stderr, "read \"%s\"\n", buf);
}
execio_close(testrem);
}
void loginuser(char *username){
// Logs into sandboxed user on zserver2 as a client, currently does nothing
char buf[10];
struct execio *testrem;
char *execargv[] =
{
"ssh",
"username@protofusion.org", // username must be read from the conf
"-i",
"username.rsa", // Key created from registeruser()
"-p",
"23",
"echo",
"hello", // This should eventually open a non-terminating connection to the server for communication
(char *)NULL
};
size_t readlen;
fprintf(stderr, "Opening stream:\n", execio_open(&testrem, "ssh", execargv));
buf[9] = '\0'; // null-terminating the array...
while(!execio_read(testrem, buf, 9, &readlen)) // What's with the readlen stuff?
{
if(readlen > 9) {
fprintf(stderr, "!!!! Something is terribly wrong!\n");
}
buf[readlen] = '\0'; // Null-terminating the end of it again based on how large the data is?
fprintf(stderr, "read \"%s\"\n", buf);
}
execio_close(testrem);
}
// Executors
/*
It seems that the client will need to know the job number. Is finish_frame going to be on the client or the server? we gotta figure that out!
*/
void exec_blender(struct blendjob* blendjob, char *input, char *output, int frame)
{
char *frame_str;
asprintf(frame,frame_str); // GNU/*nix compatible only, fix before releasing win32, although dll for windows for asprintf exists!
int ret;
char *cmd[] = { "blender", "-b", "-o", output, input, "-f", frame_str, (char *)0 };
ret = execv("/usr/bin/blender", cmd);
finish_frame(blendjob, frame);
}