Extensions: change the constant for the complete status
[blender-addons-contrib.git] / netrender / utils.py
blob367ac89766e85c03afaeae2d5d22d03a7988f3e2
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 sys, os, re, platform
20 import http, http.client, http.server, socket
21 import subprocess, time, hashlib
23 import netrender, netrender.model
26 try:
27 import bpy
28 except:
29 bpy = None
31 VERSION = bytes(".".join((str(n) for n in netrender.bl_info["version"])), encoding='utf8')
33 try:
34 system = platform.system()
35 except UnicodeDecodeError:
36 import sys
37 system = sys.platform
40 if system == "Darwin":
41 class ConnectionContext:
42 def __init__(self, timeout = None):
43 self.old_timeout = socket.getdefaulttimeout()
44 self.timeout = timeout
46 def __enter__(self):
47 if self.old_timeout != self.timeout:
48 socket.setdefaulttimeout(self.timeout)
49 def __exit__(self, exc_type, exc_value, traceback):
50 if self.old_timeout != self.timeout:
51 socket.setdefaulttimeout(self.old_timeout)
52 else:
53 # On sane OSes we can use the connection timeout value correctly
54 class ConnectionContext:
55 def __init__(self, timeout = None):
56 pass
58 def __enter__(self):
59 pass
61 def __exit__(self, exc_type, exc_value, traceback):
62 pass
64 if system in {"Windows", "win32"}:
65 import ctypes
66 class NoErrorDialogContext:
67 def __init__(self):
68 self.val = 0
70 def __enter__(self):
71 self.val = ctypes.windll.kernel32.SetErrorMode(0x0002)
72 ctypes.windll.kernel32.SetErrorMode(self.val | 0x0002)
74 def __exit__(self, exc_type, exc_value, traceback):
75 ctypes.windll.kernel32.SetErrorMode(self.val)
76 else:
77 class NoErrorDialogContext:
78 def __init__(self):
79 pass
81 def __enter__(self):
82 pass
84 def __exit__(self, exc_type, exc_value, traceback):
85 pass
87 class DirectoryContext:
88 def __init__(self, path):
89 self.path = path
91 def __enter__(self):
92 self.curdir = os.path.abspath(os.curdir)
93 os.chdir(self.path)
95 def __exit__(self, exc_type, exc_value, traceback):
96 os.chdir(self.curdir)
98 class BreakableIncrementedSleep:
99 def __init__(self, increment, default_timeout, max_timeout, break_fct):
100 self.increment = increment
101 self.default = default_timeout
102 self.max = max_timeout
103 self.current = self.default
104 self.break_fct = break_fct
106 def reset(self):
107 self.current = self.default
109 def increase(self):
110 self.current = min(self.current + self.increment, self.max)
112 def sleep(self):
113 for i in range(self.current):
114 time.sleep(1)
115 if self.break_fct():
116 break
118 self.increase()
120 def responseStatus(conn):
121 with conn.getresponse() as response:
122 length = int(response.getheader("content-length", "0"))
123 if length > 0:
124 response.read()
125 return response.status
127 def reporting(report, message, errorType = None):
128 if errorType:
129 t = 'ERROR'
130 else:
131 t = 'INFO'
133 if report:
134 report({t}, message)
135 return None
136 elif errorType:
137 raise errorType(message)
138 else:
139 return None
141 def clientScan(report = None):
142 try:
143 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
144 s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
145 s.settimeout(30)
147 s.bind(('', 8000))
149 buf, address = s.recvfrom(64)
151 address = address[0]
152 port = int(str(buf, encoding='utf8'))
154 reporting(report, "Master server found")
156 return (address, port)
157 except socket.timeout:
158 reporting(report, "No master server on network", IOError)
160 return ("", 8000) # return default values
162 def clientConnection(netsettings, report = None, scan = True, timeout = 50):
163 address = netsettings.server_address
164 port = netsettings.server_port
165 use_ssl = netsettings.use_ssl
167 if address== "[default]":
168 # calling operator from python is fucked, scene isn't in context
169 # if bpy:
170 # bpy.ops.render.netclientscan()
171 # else:
172 if not scan:
173 return None
175 address, port = clientScan()
176 if address == "":
177 return None
178 conn = None
179 try:
180 HTTPConnection = http.client.HTTPSConnection if use_ssl else http.client.HTTPConnection
181 if platform.system() == "Darwin":
182 with ConnectionContext(timeout):
183 conn = HTTPConnection(address, port)
184 else:
185 conn = HTTPConnection(address, port, timeout = timeout)
187 if conn:
188 if clientVerifyVersion(conn, timeout):
189 return conn
190 else:
191 conn.close()
192 reporting(report, "Incorrect master version", ValueError)
193 except BaseException as err:
194 if report:
195 report({'ERROR'}, str(err))
196 return None
197 else:
198 print(err)
199 return None
201 def clientVerifyVersion(conn, timeout):
202 with ConnectionContext(timeout):
203 conn.request("GET", "/version")
204 response = conn.getresponse()
206 if response.status != http.client.OK:
207 conn.close()
208 return False
210 server_version = response.read()
212 if server_version != VERSION:
213 print("Incorrect server version!")
214 print("expected", str(VERSION, encoding='utf8'), "received", str(server_version, encoding='utf8'))
215 return False
217 return True
219 def fileURL(job_id, file_index):
220 return "/file_%s_%i" % (job_id, file_index)
222 def logURL(job_id, frame_number):
223 return "/log_%s_%i.log" % (job_id, frame_number)
225 def resultURL(job_id):
226 return "/result_%s.zip" % job_id
228 def renderURL(job_id, frame_number):
229 return "/render_%s_%i.exr" % (job_id, frame_number)
231 def cancelURL(job_id):
232 return "/cancel_%s" % (job_id)
234 def hashFile(path):
235 f = open(path, "rb")
236 value = hashData(f.read())
237 f.close()
238 return value
240 def hashData(data):
241 m = hashlib.md5()
242 m.update(data)
243 return m.hexdigest()
245 def verifyCreateDir(directory_path):
246 original_path = directory_path
247 directory_path = os.path.expanduser(directory_path)
248 directory_path = os.path.expandvars(directory_path)
249 if not os.path.exists(directory_path):
250 try:
251 os.makedirs(directory_path)
252 print("Created directory:", directory_path)
253 if original_path != directory_path:
254 print("Expanded from the following path:", original_path)
255 except:
256 print("Couldn't create directory:", directory_path)
257 if original_path != directory_path:
258 print("Expanded from the following path:", original_path)
259 raise
262 def cacheName(ob, point_cache):
263 name = point_cache.name
264 if name == "":
265 name = "".join(["%02X" % ord(c) for c in ob.name])
267 return name
269 def cachePath(file_path):
270 path, name = os.path.split(file_path)
271 root, ext = os.path.splitext(name)
272 return path + os.sep + "blendcache_" + root # need an API call for that
274 # Process dependencies of all objects with user defined functions
275 # pointCacheFunction(object, owner, point_cache) (owner is modifier or psys)
276 # fluidFunction(object, modifier, cache_path)
277 # multiresFunction(object, modifier, cache_path)
278 def processObjectDependencies(pointCacheFunction, fluidFunction, multiresFunction):
279 for object in bpy.data.objects:
280 for modifier in object.modifiers:
281 if modifier.type == 'FLUID_SIMULATION' and modifier.settings.type == "DOMAIN":
282 fluidFunction(object, modifier, bpy.path.abspath(modifier.settings.filepath))
283 elif modifier.type == "CLOTH":
284 pointCacheFunction(object, modifier, modifier.point_cache)
285 elif modifier.type == "SOFT_BODY":
286 pointCacheFunction(object, modifier, modifier.point_cache)
287 elif modifier.type == "SMOKE" and modifier.smoke_type == "DOMAIN":
288 pointCacheFunction(object, modifier, modifier.domain_settings.point_cache)
289 elif modifier.type == "MULTIRES" and modifier.is_external:
290 multiresFunction(object, modifier, bpy.path.abspath(modifier.filepath))
291 elif modifier.type == "DYNAMIC_PAINT" and modifier.canvas_settings:
292 for surface in modifier.canvas_settings.canvas_surfaces:
293 pointCacheFunction(object, modifier, surface.point_cache)
295 # particles modifier are stupid and don't contain data
296 # we have to go through the object property
297 for psys in object.particle_systems:
298 pointCacheFunction(object, psys, psys.point_cache)
301 def createLocalPath(rfile, prefixdirectory, prefixpath, forcelocal):
302 filepath = rfile.original_path
303 prefixpath = os.path.normpath(prefixpath) if prefixpath else None
304 if (os.path.isabs(filepath) or
305 filepath[1:3] == ":/" or filepath[1:3] == ":\\" or # Windows absolute path don't count as absolute on unix, have to handle them ourself
306 filepath[:1] == "/" or filepath[:1] == "\\"): # and vice versa
308 # if an absolute path, make sure path exists, if it doesn't, use relative local path
309 finalpath = filepath
310 if forcelocal or not os.path.exists(finalpath):
311 path, name = os.path.split(os.path.normpath(finalpath))
313 # Don't add signatures to cache files, relink fails otherwise
314 if not name.endswith(".bphys") and not name.endswith(".bobj.gz"):
315 name, ext = os.path.splitext(name)
316 name = name + "_" + rfile.signature + ext
318 if prefixpath and path.startswith(prefixpath):
319 suffix = ""
320 while path != prefixpath:
321 path, last = os.path.split(path)
322 suffix = os.path.join(last, suffix)
324 directory = os.path.join(prefixdirectory, suffix)
325 verifyCreateDir(directory)
327 finalpath = os.path.join(directory, name)
328 else:
329 finalpath = os.path.join(prefixdirectory, name)
330 else:
331 directory, name = os.path.split(filepath)
333 # Don't add signatures to cache files
334 if not name.endswith(".bphys") and not name.endswith(".bobj.gz"):
335 name, ext = os.path.splitext(name)
336 name = name + "_" + rfile.signature + ext
338 directory = directory.replace("../")
339 directory = os.path.join(prefixdirectory, directory)
341 verifyCreateDir(directory)
343 finalpath = os.path.join(directory, name)
345 return finalpath
347 def getResults(server_address, server_port, job_id, resolution_x, resolution_y, resolution_percentage, frame_ranges):
348 if bpy.app.debug:
349 print("=============================================")
350 print("============= FETCHING RESULTS ==============")
352 frame_arguments = []
353 for r in frame_ranges:
354 if len(r) == 2:
355 frame_arguments.extend(["-s", str(r[0]), "-e", str(r[1]), "-a"])
356 else:
357 frame_arguments.extend(["-f", str(r[0])])
359 filepath = os.path.join(bpy.app.tempdir, "netrender_temp.blend")
360 bpy.ops.wm.save_as_mainfile(filepath=filepath, copy=True, check_existing=False)
362 arguments = (
363 [bpy.app.binary_path,
364 "-b",
365 "-y",
366 "-noaudio",
367 filepath,
368 "-o", bpy.path.abspath(bpy.context.scene.render.filepath),
369 "-P", __file__,
370 ] + frame_arguments +
371 ["--",
372 "GetResults",
373 server_address,
374 str(server_port),
375 job_id,
376 str(resolution_x),
377 str(resolution_y),
378 str(resolution_percentage),
381 if bpy.app.debug:
382 print("Starting subprocess:")
383 print(" ".join(arguments))
385 process = subprocess.Popen(arguments, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
386 while process.poll() is None:
387 stdout = process.stdout.read(1024)
388 if bpy.app.debug:
389 print(str(stdout, encoding='utf-8'), end="")
392 # read leftovers if needed
393 stdout = process.stdout.read()
394 if bpy.app.debug:
395 print(str(stdout, encoding='utf-8'))
397 os.remove(filepath)
399 if bpy.app.debug:
400 print("=============================================")
401 return
403 def _getResults(server_address, server_port, job_id, resolution_x, resolution_y, resolution_percentage):
404 render = bpy.context.scene.render
406 netsettings = bpy.context.scene.network_render
408 netsettings.server_address = server_address
409 netsettings.server_port = int(server_port)
410 netsettings.job_id = job_id
412 render.engine = 'NET_RENDER'
413 render.resolution_x = int(resolution_x)
414 render.resolution_y = int(resolution_y)
415 render.resolution_percentage = int(resolution_percentage)
417 render.use_full_sample = False
418 render.use_compositing = False
419 render.use_border = False
422 def getFileInfo(filepath, infos):
423 process = subprocess.Popen(
424 [bpy.app.binary_path,
425 "-b",
426 "-y",
427 "-noaudio",
428 filepath,
429 "-P", __file__,
430 "--",
431 "FileInfo",
432 ] + infos,
433 stdout=subprocess.PIPE,
434 stderr=subprocess.STDOUT,
436 stdout = bytes()
437 while process.poll() is None:
438 stdout += process.stdout.read(1024)
440 # read leftovers if needed
441 stdout += process.stdout.read()
443 stdout = str(stdout, encoding="utf8")
445 values = [eval(v[1:].strip()) for v in stdout.split("\n") if v.startswith("$")]
447 return values
450 def sendFile(conn, url, filepath, headers={}):
451 file_size = os.path.getsize(filepath)
452 if not 'content-length' in headers:
453 headers['content-length'] = file_size
454 with open(filepath, "rb") as f:
455 with ConnectionContext():
456 conn.request("PUT", url, f, headers=headers)
457 return responseStatus(conn)
460 if __name__ == "__main__":
461 try:
462 start = sys.argv.index("--") + 1
463 except ValueError:
464 start = 0
465 action, *args = sys.argv[start:]
467 if action == "FileInfo":
468 for info in args:
469 print("$", eval(info))
470 elif action == "GetResults":
471 _getResults(args[0], args[1], args[2], args[3], args[4], args[5])