Update for 2.8
[blender-addons.git] / netrender / master_html.py
blob375328de709105d560b125db85b347ef3cb226b1
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 import os
20 import shutil
21 from netrender.utils import *
22 import netrender.model
23 import json
24 #import rpdb2
26 # bitwise definition of the different files type
28 CACHE_FILES=1
29 FLUID_FILES=2
30 OTHER_FILES=4
32 src_folder = os.path.split(__file__)[0]
34 #function to return counter of different type of files
35 # job: the job that contain files
37 def countFiles(job):
38 tot_cache = 0
39 tot_fluid = 0
40 tot_other = 0
41 for file in job.files:
42 if file.filepath.endswith(".bphys"):
43 tot_cache += 1
44 elif file.filepath.endswith((".bobj.gz", ".bvel.gz")):
45 tot_fluid += 1
46 elif not file == job.files[0]:
47 tot_other += 1
48 return tot_cache,tot_fluid,tot_other;
51 def get(handler):
52 def output(text):
53 handler.wfile.write(bytes(text, encoding='utf8'))
55 def head(title, refresh = False):
56 output("<html><head>")
57 if refresh:
58 output("<meta http-equiv='refresh' content=5>")
59 output("<script src='/html/netrender.js' type='text/javascript'></script>")
60 output("<title>")
61 output(title)
62 output("</title></head><body>")
63 output("<link rel='stylesheet' href='/html/netrender.css' type='text/css'>")
66 def link(text, url, script=""):
67 return "<a href='%s' %s>%s</a>" % (url, script, text)
69 def tag(name, text, attr=""):
70 return "<%s %s>%s</%s>" % (name, attr, text, name)
72 def startTable(border=1, class_style = None, caption = None):
73 output("<table border='%i'" % border)
75 if class_style:
76 output(" class='%s'" % class_style)
78 output(">")
80 if caption:
81 output("<caption>%s</caption>" % caption)
83 def headerTable(*headers):
84 output("<thead><tr>")
86 for c in headers:
87 output("<td>" + c + "</td>")
89 output("</tr></thead>")
91 def rowTable(*data, id = None, class_style = None, extra = None):
92 output("<tr")
94 if id:
95 output(" id='%s'" % id)
97 if class_style:
98 output(" class='%s'" % class_style)
100 if extra:
101 output(" %s" % extra)
103 output(">")
105 for c in data:
106 output("<td>" + str(c) + "</td>")
108 output("</tr>")
110 def endTable():
111 output("</table>")
113 def checkbox(title, value, script=""):
114 return """<input type="checkbox" title="%s" %s %s>""" % (title, "checked" if value else "", ("onclick=\"%s\"" % script) if script else "")
116 def sendjson(message):
117 handler.send_head(content = "application/json")
118 output(json.dumps(message,sort_keys=False))
120 def sendFile(filename,content_type):
121 f = open(os.path.join(src_folder,filename), 'rb')
123 handler.send_head(content = content_type)
124 shutil.copyfileobj(f, handler.wfile)
126 f.close()
127 # return serialized version of job for html interface
128 # job: the base job
129 # includeFiles: boolean to indicate if we want file to be serialized too into job
130 # includeFrames; boolean to indicate if we want frame to be serialized too into job
131 def gethtmlJobInfo(job,includeFiles=True,includeFrames=True):
132 if (job):
133 results = job.framesStatus()
134 serializedJob = job.serialize(withFiles=includeFiles, withFrames=includeFrames)
135 serializedJob["p_rule"] = handler.server.balancer.applyPriorities(job)
136 serializedJob["e_rule"] = handler.server.balancer.applyExceptions(job)
137 serializedJob["wait"] = int(time.time() - job.last_dispatched) if job.status != netrender.model.JOB_FINISHED else "N/A"
138 serializedJob["length"] = len(job);
139 serializedJob["done"] = results[netrender.model.FRAME_DONE]
140 serializedJob["dispatched"] = results[netrender.model.FRAME_DISPATCHED]
141 serializedJob["error"] = results[netrender.model.FRAME_ERROR]
142 tot_cache, tot_fluid, tot_other = countFiles(job)
143 serializedJob["totcache"] = tot_cache
144 serializedJob["totfluid"] = tot_fluid
145 serializedJob["totother"] = tot_other
146 serializedJob["wktime"] = (time.time()-job.start_time ) if job.status != netrender.model.JOB_FINISHED else (job.finish_time-job.start_time)
147 else:
148 serializedJob={"name":"invalid job"}
150 return serializedJob;
152 # return serialized files based on cumulative file_type
153 # job_id: id of the job
154 # message: serialized content
155 # file_type: any combinaison of CACHE_FILE,FLUID_FILES, OTHER_FILES
157 def getFiles(job_id,message,file_type):
159 job=handler.server.getJobID(job_id)
160 print ("job.files.length="+str(len(job.files)))
162 for file in job.files:
163 filedata=file.serialize()
164 filedata["name"] = os.path.split(file.filepath)[1]
166 if file.filepath.endswith(".bphys") and (file_type & CACHE_FILES):
167 message.append(filedata);
168 continue
169 if file.filepath.endswith((".bobj.gz", ".bvel.gz")) and (file_type & FLUID_FILES):
170 message.append(filedata);
171 continue
172 if (not file == job.files[0]) and (file_type & OTHER_FILES) and (not file.filepath.endswith((".bobj.gz", ".bvel.gz"))) and not file.filepath.endswith(".bphys"):
173 message.append(filedata);
174 continue
178 if handler.path == "/html/netrender.js":
179 sendFile("netrender.js","text/javascript")
181 elif handler.path == "/html/netrender.css":
182 sendFile("netrender.css","text/css")
184 elif handler.path =="/html/newui":
185 sendFile("newui.html","text/html")
187 elif handler.path.startswith("/html/js"):
188 path, filename = os.path.split(handler.path)
189 sendFile("js/"+filename,"text/javascript")
191 elif handler.path.startswith("/html/css/images"):
192 path, filename = os.path.split(handler.path)
193 sendFile("css/images/"+filename,"image/png")
195 elif handler.path.startswith("/html/css"):
196 path, filename = os.path.split(handler.path)
197 sendFile("css/"+filename,"text/css")
198 # return all master rules information
199 elif handler.path == "/html/rules":
200 message = []
201 for rule in handler.server.balancer.rules:
202 message.append(rule.serialize())
203 for rule in handler.server.balancer.priorities:
204 message.append(rule.serialize())
205 for rule in handler.server.balancer.exceptions:
206 message.append(rule.serialize())
207 sendjson(message)
208 #return all slaves list
209 elif handler.path == "/html/slaves":
210 message = []
211 for slave in handler.server.slaves:
212 serializedSlave = slave.serialize()
213 if slave.job:
214 serializedSlave["job_name"] = slave.job.name
215 serializedSlave["job_id"] = slave.job.id
216 else:
217 serializedSlave["job_name"] = "None"
218 serializedSlave["job_id"] = "0"
219 message.append(serializedSlave)
220 sendjson(message)
221 # return all job list
222 elif handler.path == "/html/jobs":
223 message = []
224 for job in handler.server.jobs:
225 if job:
226 message.append(gethtmlJobInfo(job, False, False))
227 sendjson(message)
228 #return a job information
229 elif handler.path.startswith("/html/job_"):
231 job_id = handler.path[10:]
232 job = handler.server.getJobID(job_id)
234 message = []
235 if job:
237 message.append(gethtmlJobInfo(job, includeFiles=False))
238 sendjson(message)
239 # return all frames for a job
240 elif handler.path.startswith("/html/frames_"):
242 job_id = handler.path[13:]
243 job = handler.server.getJobID(job_id)
245 message = []
246 if job:
247 for f in job.frames:
248 message.append(f.serialize())
250 sendjson(message)
251 # return physic cache files
252 elif handler.path.startswith("/html/cachefiles_"):
253 job_id = handler.path[17:]
254 message = []
255 getFiles(job_id, message, CACHE_FILES);
256 sendjson(message)
257 #return fluid cache files
258 elif handler.path.startswith("/html/fluidfiles_"):
259 job_id = handler.path[17:]
261 message = []
262 getFiles(job_id, message, FLUID_FILES);
263 sendjson(message)
265 #return list of other files ( images, sequences ...)
266 elif handler.path.startswith("/html/otherfiles_"):
267 job_id = handler.path[17:]
269 message = []
270 getFiles(job_id, message, OTHER_FILES);
271 sendjson(message)
272 # return blend file info
273 elif handler.path.startswith("/html/blendfile_"):
274 job_id = handler.path[16:]
275 job = handler.server.getJobID(job_id)
276 message = []
277 if job:
278 if job.files:
279 message.append(job.files[0].serialize())
280 sendjson(message)
281 # return black listed slaves for a job
282 elif handler.path.startswith("/html/blacklist_"):
284 job_id = handler.path[16:]
285 job = handler.server.getJobID(job_id)
287 message = []
288 if job:
289 for slave_id in job.blacklist:
290 slave = handler.server.slaves_map.get(slave_id, None)
291 message.append(slave.serialize())
292 sendjson(message)
293 # return all slaves currently assigned to a job
295 elif handler.path.startswith("/html/slavesjob_"):
297 job_id = handler.path[16:]
298 job = handler.server.getJobID(job_id)
299 message = []
300 if job:
301 for slave in handler.server.slaves:
302 if slave.job and slave.job == job:
303 message.append(slave.serialize())
304 sendjson(message)
305 # here begin code for simple ui
306 elif handler.path == "/html" or handler.path == "/":
307 handler.send_head(content = "text/html")
308 head("NetRender", refresh = True)
310 output("<h2>Jobs</h2>")
312 startTable()
313 headerTable(
314 "&nbsp;",
315 "id",
316 "name",
317 "category",
318 "tags",
319 "type",
320 "chunks",
321 "priority",
322 "usage",
323 "wait",
324 "status",
325 "total",
326 "done",
327 "dispatched",
328 "error",
329 "priority",
330 "exception",
331 "started",
332 "finished"
335 handler.server.balance()
337 for job in handler.server.jobs:
338 results = job.framesStatus()
340 time_finished = job.time_finished
341 time_started = job.time_started
343 rowTable(
344 """<button title="cancel job" onclick="cancel_job('%s');">X</button>""" % job.id +
345 """<button title="pause job" onclick="request('/pause_%s', null);">P</button>""" % job.id +
346 """<button title="reset all frames" onclick="request('/resetall_%s_0', null);">R</button>""" % job.id,
347 job.id,
348 link(job.name, "/html/job" + job.id),
349 job.category if job.category else "<i>None</i>",
350 ";".join(sorted(job.tags)) if job.tags else "<i>None</i>",
351 "%s [%s]" % (netrender.model.JOB_TYPES[job.type], netrender.model.JOB_SUBTYPES[job.subtype]),
352 str(job.chunks) +
353 """<button title="increase chunks size" onclick="request('/edit_%s', &quot;{'chunks': %i}&quot;);">+</button>""" % (job.id, job.chunks + 1) +
354 """<button title="decrease chunks size" onclick="request('/edit_%s', &quot;{'chunks': %i}&quot;);" %s>-</button>""" % (job.id, job.chunks - 1, "disabled=True" if job.chunks == 1 else ""),
355 str(job.priority) +
356 """<button title="increase priority" onclick="request('/edit_%s', &quot;{'priority': %i}&quot;);">+</button>""" % (job.id, job.priority + 1) +
357 """<button title="decrease priority" onclick="request('/edit_%s', &quot;{'priority': %i}&quot;);" %s>-</button>""" % (job.id, job.priority - 1, "disabled=True" if job.priority == 1 else ""),
358 "%0.1f%%" % (job.usage * 100),
359 "%is" % int(time.time() - job.last_dispatched) if job.status != netrender.model.JOB_FINISHED else "N/A",
360 job.statusText(),
361 len(job),
362 results[netrender.model.FRAME_DONE],
363 results[netrender.model.FRAME_DISPATCHED],
364 str(results[netrender.model.FRAME_ERROR]) +
365 """<button title="reset error frames" onclick="request('/reset_%s_0', null);" %s>R</button>""" % (job.id, "disabled=True" if not results[netrender.model.FRAME_ERROR] else ""),
366 "yes" if handler.server.balancer.applyPriorities(job) else "no",
367 "yes" if handler.server.balancer.applyExceptions(job) else "no",
368 time.ctime(time_started) if time_started else "Not Started",
369 time.ctime(time_finished) if time_finished else "Not Finished"
372 endTable()
374 output("<h2>Slaves</h2>")
376 startTable()
377 headerTable("name", "address", "tags", "last seen", "stats", "job")
379 for slave in handler.server.slaves:
380 rowTable(slave.name, slave.address[0], ";".join(sorted(slave.tags)) if slave.tags else "<i>All</i>", time.ctime(slave.last_seen), slave.stats, link(slave.job.name, "/html/job" + slave.job.id) if slave.job else "None")
381 endTable()
383 output("<h2>Configuration</h2>")
385 output("""<button title="remove all jobs" onclick="clear_jobs();">CLEAR JOB LIST</button>""")
387 output("<br />")
389 output(link("new interface", "/html/newui"))
391 startTable(caption = "Rules", class_style = "rules")
393 headerTable("type", "enabled", "description", "limit")
395 for rule in handler.server.balancer.rules:
396 rowTable(
397 "rating",
398 checkbox("", rule.enabled, "balance_enable('%s', '%s')" % (rule.id(), str(not rule.enabled).lower())),
399 rule,
400 rule.str_limit() +
401 """<button title="edit limit" onclick="balance_edit('%s', '%s');">edit</button>""" % (rule.id(), str(rule.limit)) if hasattr(rule, "limit") else "&nbsp;"
404 for rule in handler.server.balancer.priorities:
405 rowTable(
406 "priority",
407 checkbox("", rule.enabled, "balance_enable('%s', '%s')" % (rule.id(), str(not rule.enabled).lower())),
408 rule,
409 rule.str_limit() +
410 """<button title="edit limit" onclick="balance_edit('%s', '%s');">edit</button>""" % (rule.id(), str(rule.limit)) if hasattr(rule, "limit") else "&nbsp;"
413 for rule in handler.server.balancer.exceptions:
414 rowTable(
415 "exception",
416 checkbox("", rule.enabled, "balance_enable('%s', '%s')" % (rule.id(), str(not rule.enabled).lower())),
417 rule,
418 rule.str_limit() +
419 """<button title="edit limit" onclick="balance_edit('%s', '%s');">edit</button>""" % (rule.id(), str(rule.limit)) if hasattr(rule, "limit") else "&nbsp;"
422 endTable()
423 output("</body></html>")
425 elif handler.path.startswith("/html/job"):
426 handler.send_head(content = "text/html")
427 job_id = handler.path[9:]
429 head("NetRender")
431 output(link("Back to Main Page", "/html"))
433 job = handler.server.getJobID(job_id)
435 if job:
436 output("<h2>Job Information</h2>")
438 job.initInfo()
440 startTable()
442 rowTable("resolution", "%ix%i at %i%%" % job.resolution)
444 rowTable("tags", ";".join(sorted(job.tags)) if job.tags else "<i>None</i>")
446 rowTable("results", link("download all", resultURL(job_id)))
448 endTable()
451 if job.type == netrender.model.JOB_BLENDER:
452 output("<h2>Files</h2>")
454 startTable()
455 headerTable("path")
457 tot_cache = 0
458 tot_fluid = 0
459 tot_other = 0
461 rowTable(job.files[0].original_path)
462 tot_cache, tot_fluid, tot_other = countFiles(job)
464 if tot_cache > 0:
465 rowTable("%i physic cache files" % tot_cache, class_style = "toggle", extra = "onclick='toggleDisplay(&quot;.cache&quot;, &quot;none&quot;, &quot;table-row&quot;)'")
466 for file in job.files:
467 if file.filepath.endswith(".bphys"):
468 rowTable(os.path.split(file.filepath)[1], class_style = "cache")
470 if tot_fluid > 0:
471 rowTable("%i fluid bake files" % tot_fluid, class_style = "toggle", extra = "onclick='toggleDisplay(&quot;.fluid&quot;, &quot;none&quot;, &quot;table-row&quot;)'")
472 for file in job.files:
473 if file.filepath.endswith((".bobj.gz", ".bvel.gz")):
474 rowTable(os.path.split(file.filepath)[1], class_style = "fluid")
476 if tot_other > 0:
477 rowTable("%i other files" % tot_other, class_style = "toggle", extra = "onclick='toggleDisplay(&quot;.other&quot;, &quot;none&quot;, &quot;table-row&quot;)'")
478 for file in job.files:
479 if (
480 not file.filepath.endswith(".bphys")
481 and not file.filepath.endswith((".bobj.gz", ".bvel.gz"))
482 and not file == job.files[0]
485 rowTable(file.filepath, class_style = "other")
487 endTable()
488 elif job.type == netrender.model.JOB_VCS:
489 output("<h2>Versioning</h2>")
491 startTable()
493 rowTable("System", job.version_info.system.name)
494 rowTable("Remote Path", job.version_info.rpath)
495 rowTable("Working Path", job.version_info.wpath)
496 rowTable("Revision", job.version_info.revision)
497 rowTable("Render File", job.files[0].filepath)
499 endTable()
501 if job.blacklist:
502 output("<h2>Blacklist</h2>")
504 startTable()
505 headerTable("name", "address")
507 for slave_id in job.blacklist:
508 slave = handler.server.slaves_map.get(slave_id, None)
509 if slave:
510 rowTable(slave.name, slave.address[0])
512 endTable()
514 output("<h2>Transitions</h2>")
516 startTable()
517 headerTable("Event", "Time")
519 for transition, time_value in job.transitions:
520 rowTable(transition, time.ctime(time_value))
522 endTable()
524 output("<h2>Frames</h2>")
526 startTable()
528 if job.hasRenderResult():
529 headerTable("no", "status", "render time", "slave", "log", "result", "")
531 for frame in job.frames:
532 rowTable(
533 frame.number,
534 frame.statusText(),
535 "%.1fs" % frame.time,
536 frame.slave.name if frame.slave else "&nbsp;",
537 link("view log", logURL(job_id, frame.number)) if frame.log_path else "&nbsp;",
538 link("view result", renderURL(job_id, frame.number)) + " [" +
539 tag("span", "show", attr="class='thumb' onclick='showThumb(%s, %i)'" % (job.id, frame.number)) + "]" if frame.status == netrender.model.FRAME_DONE else "&nbsp;",
540 "<img name='thumb%i' title='hide thumbnails' src='' class='thumb' onclick='showThumb(%s, %i)'>" % (frame.number, job.id, frame.number)
542 else:
543 headerTable("no", "status", "process time", "slave", "log")
545 for frame in job.frames:
546 rowTable(
547 frame.number,
548 frame.statusText(),
549 "%.1fs" % frame.time,
550 frame.slave.name if frame.slave else "&nbsp;",
551 link("view log", logURL(job_id, frame.number)) if frame.log_path else "&nbsp;"
554 endTable()
555 else:
556 output("no such job")
558 output(link("Back to Main Page", "/html"))
560 output("</body></html>")