netrender. first draft of html master details. Just point a browser at the master...
[blender.git] / release / io / netrender / master.py
1 import sys, os
2 import http, http.client, http.server, urllib, socket
3 import subprocess, shutil, time, hashlib
4
5 from netrender.utils import *
6 import netrender.model
7 import netrender.balancing
8 import netrender.master_html
9
10 class MRenderFile:
11         def __init__(self, filepath, start, end):
12                 self.filepath = filepath
13                 self.start = start
14                 self.end = end
15                 self.found = False
16         
17         def test(self):
18                 self.found = os.path.exists(self.filepath)
19                 return self.found
20
21
22 class MRenderSlave(netrender.model.RenderSlave):
23         def __init__(self, name, address, stats):
24                 super().__init__()
25                 self.id = hashlib.md5(bytes(repr(name) + repr(address), encoding='utf8')).hexdigest()
26                 self.name = name
27                 self.address = address
28                 self.stats = stats
29                 self.last_seen = time.time()
30                 
31                 self.job = None
32                 self.frame = None
33                 
34                 netrender.model.RenderSlave._slave_map[self.id] = self
35
36         def seen(self):
37                 self.last_seen = time.time()
38
39 class MRenderJob(netrender.model.RenderJob):
40         def __init__(self, job_id, name, files, chunks = 1, priority = 1, credits = 100.0, blacklist = []):
41                 super().__init__()
42                 self.id = job_id
43                 self.name = name
44                 self.files = files
45                 self.frames = []
46                 self.chunks = chunks
47                 self.priority = priority
48                 self.credits = credits
49                 self.blacklist = blacklist
50                 self.last_dispatched = time.time()
51         
52                 # special server properties
53                 self.save_path = ""
54                 self.files_map = {path: MRenderFile(path, start, end) for path, start, end in files}
55                 self.status = JOB_WAITING
56         
57         def save(self):
58                 if self.save_path:
59                         f = open(self.save_path + "job.txt", "w")
60                         f.write(repr(self.serialize()))
61                         f.close()
62         
63         def testStart(self):
64                 for f in self.files_map.values():
65                         if not f.test():
66                                 return False
67                 
68                 self.start()
69                 return True
70         
71         def start(self):
72                 self.status = JOB_QUEUED
73         
74         def update(self):
75                 self.credits -= 5 # cost of one frame
76                 self.credits += (time.time() - self.last_dispatched) / 60
77                 self.last_dispatched = time.time()
78         
79         def addLog(self, frames):
80                 log_name = "_".join(("%04d" % f for f in frames)) + ".log"
81                 log_path = self.save_path + log_name
82                 
83                 for number in frames:
84                         frame = self[number]
85                         if frame:
86                                 frame.log_path = log_path
87         
88         def addFrame(self, frame_number):
89                 frame = MRenderFrame(frame_number)
90                 self.frames.append(frame)
91                 return frame
92                 
93         def reset(self, all):
94                 for f in self.frames:
95                         f.reset(all)
96         
97         def getFrames(self):
98                 frames = []
99                 for f in self.frames:
100                         if f.status == QUEUED:
101                                 self.update()
102                                 frames.append(f)
103                                 if len(frames) >= self.chunks:
104                                         break
105                 
106                 return frames
107
108 class MRenderFrame(netrender.model.RenderFrame):
109         def __init__(self, frame):
110                 super().__init__()
111                 self.number = frame
112                 self.slave = None
113                 self.time = 0
114                 self.status = QUEUED
115                 self.log_path = None
116                 
117         def reset(self, all):
118                 if all or self.status == ERROR:
119                         self.slave = None
120                         self.time = 0
121                         self.status = QUEUED
122
123
124 # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
125 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
126 # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
127 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
128
129 class RenderHandler(http.server.BaseHTTPRequestHandler):
130         def send_head(self, code = http.client.OK, headers = {}, content = "application/octet-stream"):
131                 self.send_response(code)
132                 self.send_header("Content-type", content)
133                 
134                 for key, value in headers.items():
135                         self.send_header(key, value)
136                 
137                 self.end_headers()
138
139         def do_HEAD(self):
140                 print(self.path)
141         
142                 if self.path == "/status":
143                         job_id = self.headers.get('job-id', "")
144                         job_frame = int(self.headers.get('job-frame', -1))
145                         
146                         if job_id:
147                                 print("status:", job_id, "\n")
148                                 
149                                 job = self.server.getJobByID(job_id)
150                                 if job:
151                                         if job_frame != -1:
152                                                 frame = job[frame]
153                                                 
154                                                 if not frame:
155                                                         # no such frame
156                                                         self.send_heat(http.client.NO_CONTENT)
157                                                         return
158                                 else:
159                                         # no such job id
160                                         self.send_head(http.client.NO_CONTENT)
161                                         return
162                         
163                         self.send_head()
164         
165         # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
166         # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
167         # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
168         # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
169         # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
170         
171         def do_GET(self):
172                 print(self.path)
173                 
174                 if self.path == "/version":
175                         self.send_head()
176                         self.server.stats("", "New client connection")
177                         self.wfile.write(VERSION)
178                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
179                 elif self.path == "/render":
180                         job_id = self.headers['job-id']
181                         job_frame = int(self.headers['job-frame'])
182                         print("render:", job_id, job_frame)
183                         
184                         job = self.server.getJobByID(job_id)
185                         
186                         if job:
187                                 frame = job[job_frame]
188                                 
189                                 if frame:
190                                         if frame.status in (QUEUED, DISPATCHED):
191                                                 self.send_head(http.client.ACCEPTED)
192                                         elif frame.status == DONE:
193                                                 self.server.stats("", "Sending result back to client")
194                                                 f = open(job.save_path + "%04d" % job_frame + ".exr", 'rb')
195                                                 
196                                                 self.send_head()
197                                                 
198                                                 shutil.copyfileobj(f, self.wfile)
199                                                 
200                                                 f.close()
201                                         elif frame.status == ERROR:
202                                                 self.send_head(http.client.PARTIAL_CONTENT)
203                                 else:
204                                         # no such frame
205                                         self.send_head(http.client.NO_CONTENT)
206                         else:
207                                 # no such job id
208                                 self.send_head(http.client.NO_CONTENT)
209                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
210                 elif self.path == "/log":
211                         job_id = self.headers['job-id']
212                         job_frame = int(self.headers['job-frame'])
213                         print("log:", job_id, job_frame)
214                         
215                         job = self.server.getJobByID(job_id)
216                         
217                         if job:
218                                 frame = job[job_frame]
219                                 
220                                 if frame:
221                                         if not frame.log_path or frame.status in (QUEUED, DISPATCHED):
222                                                 self.send_head(http.client.PROCESSING)
223                                         else:
224                                                 self.server.stats("", "Sending log back to client")
225                                                 f = open(frame.log_path, 'rb')
226                                                 
227                                                 self.send_head()
228                                                 
229                                                 shutil.copyfileobj(f, self.wfile)
230                                                 
231                                                 f.close()
232                                 else:
233                                         # no such frame
234                                         self.send_head(http.client.NO_CONTENT)
235                         else:
236                                 # no such job id
237                                 self.send_head(http.client.NO_CONTENT)
238                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
239                 elif self.path == "/status":
240                         job_id = self.headers.get('job-id', "")
241                         job_frame = int(self.headers.get('job-frame', -1))
242                         
243                         if job_id:
244                                 print("status:", job_id, "\n")
245                                 
246                                 job = self.server.getJobByID(job_id)
247                                 if job:
248                                         if job_frame != -1:
249                                                 frame = job[frame]
250                                                 
251                                                 if frame:
252                                                         message = frame.serialize()
253                                                 else:
254                                                         # no such frame
255                                                         self.send_heat(http.client.NO_CONTENT)
256                                                         return
257                                         else:
258                                                 message = job.serialize()
259                                 else:
260                                         # no such job id
261                                         self.send_head(http.client.NO_CONTENT)
262                                         return
263                         else: # status of all jobs
264                                 message = []
265                                 
266                                 for job in self.server:
267                                         message.append(job.serialize())
268                         
269                         self.send_head()
270                         self.wfile.write(bytes(repr(message), encoding='utf8'))
271                         
272                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
273                 elif self.path == "/job":
274                         self.server.update()
275                         
276                         slave_id = self.headers['slave-id']
277                         
278                         print("slave-id", slave_id)
279                         
280                         slave = self.server.updateSlave(slave_id)
281                         
282                         if slave: # only if slave id is valid
283                                 job, frames = self.server.getNewJob(slave_id)
284                                 
285                                 if job and frames:
286                                         for f in frames:
287                                                 print("dispatch", f.number)
288                                                 f.status = DISPATCHED
289                                                 f.slave = slave
290                                         
291                                         self.send_head(headers={"job-id": job.id})
292                                         
293                                         message = job.serialize(frames)
294                                         
295                                         self.wfile.write(bytes(repr(message), encoding='utf8'))
296                                         
297                                         self.server.stats("", "Sending job frame to render node")
298                                 else:
299                                         # no job available, return error code
300                                         self.send_head(http.client.ACCEPTED)
301                         else: # invalid slave id
302                                 self.send_head(http.client.NO_CONTENT)
303                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
304                 elif self.path == "/file":
305                         slave_id = self.headers['slave-id']
306                         
307                         slave = self.server.updateSlave(slave_id)
308                         
309                         if slave: # only if slave id is valid
310                                 job_id = self.headers['job-id']
311                                 job_file = self.headers['job-file']
312                                 print("job:", job_id, "\n")
313                                 print("file:", job_file, "\n")
314                                 
315                                 job = self.server.getJobByID(job_id)
316                                 
317                                 if job:
318                                         render_file = job.files_map.get(job_file, None)
319                                         
320                                         if render_file:
321                                                 self.server.stats("", "Sending file to render node")
322                                                 f = open(render_file.filepath, 'rb')
323                                                 
324                                                 self.send_head()
325                                                 shutil.copyfileobj(f, self.wfile)
326                                                 
327                                                 f.close()
328                                         else:
329                                                 # no such file
330                                                 self.send_head(http.client.NO_CONTENT)
331                                 else:
332                                         # no such job id
333                                         self.send_head(http.client.NO_CONTENT)
334                         else: # invalid slave id
335                                 self.send_head(http.client.NO_CONTENT)
336                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
337                 elif self.path == "/slave":
338                         message = []
339                         
340                         for slave in self.server.slaves:
341                                 message.append(slave.serialize())
342                         
343                         self.send_head()
344                         
345                         self.wfile.write(bytes(repr(message), encoding='utf8'))
346                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
347                 else:
348                         # hand over the rest to the html section
349                         netrender.master_html.get(self)
350
351         # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
352         # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
353         # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
354         # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
355         # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
356         def do_POST(self):
357                 print(self.path)
358         
359                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
360                 if self.path == "/job":
361                         print("posting job info")
362                         self.server.stats("", "Receiving job")
363                         
364                         length = int(self.headers['content-length'])
365                         
366                         job_info = netrender.model.RenderJob.materialize(eval(str(self.rfile.read(length), encoding='utf8')))
367                         
368                         job_id = self.server.nextJobID()
369                         
370                         print(job_info.files)
371                         
372                         job = MRenderJob(job_id, job_info.name, job_info.files, chunks = job_info.chunks, priority = job_info.priority, blacklist = job_info.blacklist)
373                         
374                         for frame in job_info.frames:
375                                 frame = job.addFrame(frame.number)
376                         
377                         self.server.addJob(job)
378                         
379                         headers={"job-id": job_id}
380                         
381                         if job.testStart():
382                                 self.send_head(headers=headers)
383                         else:
384                                 self.send_head(http.client.ACCEPTED, headers=headers)
385                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
386                 elif self.path == "/cancel":
387                         job_id = self.headers.get('job-id', "")
388                         if job_id:
389                                 print("cancel:", job_id, "\n")
390                                 self.server.removeJob(job_id)
391                         else: # cancel all jobs
392                                 self.server.clear()
393                                 
394                         self.send_head()
395                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
396                 elif self.path == "/reset":
397                         job_id = self.headers.get('job-id', "")
398                         job_frame = int(self.headers.get('job-frame', "-1"))
399                         all = bool(self.headers.get('reset-all', "False"))
400                         
401                         job = self.server.getJobByID(job_id)
402                         
403                         if job:
404                                 if job_frame != -1:
405                                         job[job_frame].reset(all)
406                                 else:
407                                         job.reset(all)
408                                         
409                                 self.send_head()
410                         else: # job not found
411                                 self.send_head(http.client.NO_CONTENT)
412                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
413                 elif self.path == "/slave":
414                         length = int(self.headers['content-length'])
415                         job_frame_string = self.headers['job-frame']
416                         
417                         slave_info = netrender.model.RenderSlave.materialize(eval(str(self.rfile.read(length), encoding='utf8')))
418                         
419                         slave_id = self.server.addSlave(slave_info.name, self.client_address, slave_info.stats)
420                         
421                         self.send_head(headers = {"slave-id": slave_id})
422                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
423                 elif self.path == "/log":
424                         slave_id = self.headers['slave-id']
425                         
426                         slave = self.server.updateSlave(slave_id)
427                         
428                         if slave: # only if slave id is valid
429                                 length = int(self.headers['content-length'])
430                                 
431                                 log_info = netrender.model.LogFile.materialize(eval(str(self.rfile.read(length), encoding='utf8')))
432                                 
433                                 job = self.server.getJobByID(log_info.job_id)
434                                 
435                                 if job:
436                                         job.addLog(log_info.frames)
437                                         self.send_head(http.client.OK)
438                                 else:
439                                         # no such job id
440                                         self.send_head(http.client.NO_CONTENT)
441                         else: # invalid slave id
442                                 self.send_head(http.client.NO_CONTENT)  
443         # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
444         # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
445         # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
446         # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
447         # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
448         def do_PUT(self):
449                 print(self.path)
450                 
451                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
452                 if self.path == "/file":
453                         print("writing blend file")
454                         self.server.stats("", "Receiving job")
455                         
456                         length = int(self.headers['content-length'])
457                         job_id = self.headers['job-id']
458                         job_file = self.headers['job-file']
459                         
460                         job = self.server.getJobByID(job_id)
461                         
462                         if job:
463                                 
464                                 render_file = job.files_map.get(job_file, None)
465                                 
466                                 if render_file:
467                                         main_file = job.files[0][0] # filename of the first file
468                                         
469                                         main_path, main_name = os.path.split(main_file)
470                                         
471                                         if job_file != main_file:
472                                                 file_path = prefixPath(job.save_path, job_file, main_path)
473                                         else:
474                                                 file_path = job.save_path + main_name
475                                         
476                                         buf = self.rfile.read(length)
477                                         
478                                         # add same temp file + renames as slave
479                                         
480                                         f = open(file_path, "wb")
481                                         f.write(buf)
482                                         f.close()
483                                         del buf
484                                         
485                                         render_file.filepath = file_path # set the new path
486                                         
487                                         if job.testStart():
488                                                 self.send_head(http.client.OK)
489                                         else:
490                                                 self.send_head(http.client.ACCEPTED)
491                                 else: # invalid file
492                                         self.send_head(http.client.NO_CONTENT)
493                         else: # job not found
494                                 self.send_head(http.client.NO_CONTENT)
495                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
496                 elif self.path == "/render":
497                         print("writing result file")
498                         self.server.stats("", "Receiving render result")
499                         
500                         slave_id = self.headers['slave-id']
501                         
502                         slave = self.server.updateSlave(slave_id)
503                         
504                         if slave: # only if slave id is valid
505                                 job_id = self.headers['job-id']
506                                 
507                                 job = self.server.getJobByID(job_id)
508                                 
509                                 if job:
510                                         job_frame = int(self.headers['job-frame'])
511                                         job_result = int(self.headers['job-result'])
512                                         job_time = float(self.headers['job-time'])
513                                         
514                                         frame = job[job_frame]
515                                         
516                                         if job_result == DONE:
517                                                 length = int(self.headers['content-length'])
518                                                 buf = self.rfile.read(length)
519                                                 f = open(job.save_path + "%04d" % job_frame + ".exr", 'wb')
520                                                 f.write(buf)
521                                                 f.close()
522                                                 
523                                                 del buf
524                                         elif job_result == ERROR:
525                                                 # blacklist slave on this job on error
526                                                 job.blacklist.append(slave.id)
527                                                 
528                                         frame.status = job_result
529                                         frame.time = job_time
530                         
531                                         self.server.updateSlave(self.headers['slave-id'])
532                                         
533                                         self.send_head()
534                                 else: # job not found
535                                         self.send_head(http.client.NO_CONTENT)
536                         else: # invalid slave id
537                                 self.send_head(http.client.NO_CONTENT)
538                 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
539                 elif self.path == "/log":
540                         print("writing log file")
541                         self.server.stats("", "Receiving log file")
542                         
543                         job_id = self.headers['job-id']
544                         
545                         job = self.server.getJobByID(job_id)
546                         
547                         if job:
548                                 job_frame = int(self.headers['job-frame'])
549                                 
550                                 frame = job[job_frame]
551                                 
552                                 if frame and frame.log_path:
553                                         length = int(self.headers['content-length'])
554                                         buf = self.rfile.read(length)
555                                         f = open(frame.log_path, 'ab')
556                                         f.write(buf)
557                                         f.close()
558                                                 
559                                         del buf
560                                         
561                                         self.server.updateSlave(self.headers['slave-id'])
562                                         
563                                         self.send_head()
564                                 else: # frame not found
565                                         self.send_head(http.client.NO_CONTENT)
566                         else: # job not found
567                                 self.send_head(http.client.NO_CONTENT)
568
569 class RenderMasterServer(http.server.HTTPServer):
570         def __init__(self, address, handler_class, path):
571                 super().__init__(address, handler_class)
572                 self.jobs = []
573                 self.jobs_map = {}
574                 self.slaves = []
575                 self.slaves_map = {}
576                 self.job_id = 0
577                 self.path = path + "master_" + str(os.getpid()) + os.sep
578                 
579                 self.balancer = netrender.balancing.Balancer()
580                 self.balancer.addRule(netrender.balancing.RatingCredit())
581                 self.balancer.addException(netrender.balancing.ExcludeQueuedEmptyJob())
582                 self.balancer.addException(netrender.balancing.ExcludeSlavesLimit(self.countJobs, self.countSlaves))
583                 self.balancer.addPriority(netrender.balancing.NewJobPriority())
584                 self.balancer.addPriority(netrender.balancing.MinimumTimeBetweenDispatchPriority())
585                 
586                 if not os.path.exists(self.path):
587                         os.mkdir(self.path)
588         
589         def nextJobID(self):
590                 self.job_id += 1
591                 return str(self.job_id)
592         
593         def addSlave(self, name, address, stats):
594                 slave = MRenderSlave(name, address, stats)
595                 self.slaves.append(slave)
596                 self.slaves_map[slave.id] = slave
597                 
598                 return slave.id
599         
600         def getSlave(self, slave_id):
601                 return self.slaves_map.get(slave_id, None)
602         
603         def updateSlave(self, slave_id):
604                 slave = self.getSlave(slave_id)
605                 if slave:
606                         slave.seen()
607                         
608                 return slave
609         
610         def clear(self):
611                 self.jobs_map = {}
612                 self.jobs = []
613         
614         def update(self):
615                 self.balancer.balance(self.jobs)
616         
617         def countJobs(self, status = JOB_QUEUED):
618                 total = 0
619                 for j in self.jobs:
620                         if j.status == status:
621                                 total += 1
622                 
623                 return total
624         
625         def countSlaves(self):
626                 return len(self.slaves)
627         
628         def removeJob(self, id):
629                 job = self.jobs_map.pop(id)
630
631                 if job:
632                         self.jobs.remove(job)
633         
634         def addJob(self, job):
635                 self.jobs.append(job)
636                 self.jobs_map[job.id] = job
637                 
638                 # create job directory
639                 job.save_path = self.path + "job_" + job.id + os.sep
640                 if not os.path.exists(job.save_path):
641                         os.mkdir(job.save_path)
642                         
643                 job.save()
644         
645         def getJobByID(self, id):
646                 return self.jobs_map.get(id, None)
647         
648         def __iter__(self):
649                 for job in self.jobs:
650                         yield job
651         
652         def getNewJob(self, slave_id):
653                 if self.jobs:
654                         for job in self.jobs:
655                                 if not self.balancer.applyExceptions(job) and slave_id not in job.blacklist:
656                                         return job, job.getFrames()
657                 
658                 return None, None
659
660 def runMaster(address, broadcast, path, update_stats, test_break):
661                 httpd = RenderMasterServer(address, RenderHandler, path)
662                 httpd.timeout = 1
663                 httpd.stats = update_stats
664                 
665                 if broadcast:
666                         s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
667                         s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
668
669                         start_time = time.time()
670                         
671                 while not test_break():
672                         httpd.handle_request()
673                         
674                         if broadcast:
675                                 if time.time() - start_time >= 10: # need constant here
676                                         print("broadcasting address")
677                                         s.sendto(bytes("%i" % address[1], encoding='utf8'), 0, ('<broadcast>', 8000))
678                                         start_time = time.time()