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
31 VERSION
= bytes(".".join((str(n
) for n
in netrender
.bl_info
["version"])), encoding
='utf8')
34 system
= platform
.system()
35 except UnicodeDecodeError:
40 if system
== "Darwin":
41 class ConnectionContext
:
42 def __init__(self
, timeout
= None):
43 self
.old_timeout
= socket
.getdefaulttimeout()
44 self
.timeout
= timeout
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
)
53 # On sane OSes we can use the connection timeout value correctly
54 class ConnectionContext
:
55 def __init__(self
, timeout
= None):
61 def __exit__(self
, exc_type
, exc_value
, traceback
):
64 if system
in {"Windows", "win32"}:
66 class NoErrorDialogContext
:
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
)
77 class NoErrorDialogContext
:
84 def __exit__(self
, exc_type
, exc_value
, traceback
):
87 class DirectoryContext
:
88 def __init__(self
, path
):
92 self
.curdir
= os
.path
.abspath(os
.curdir
)
95 def __exit__(self
, exc_type
, exc_value
, traceback
):
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
107 self
.current
= self
.default
110 self
.current
= min(self
.current
+ self
.increment
, self
.max)
113 for i
in range(self
.current
):
120 def responseStatus(conn
):
121 with conn
.getresponse() as response
:
122 length
= int(response
.getheader("content-length", "0"))
125 return response
.status
127 def reporting(report
, message
, errorType
= None):
137 raise errorType(message
)
141 def clientScan(report
= None):
143 s
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
144 s
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_BROADCAST
, 1)
149 buf
, address
= s
.recvfrom(64)
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
170 # bpy.ops.render.netclientscan()
175 address
, port
= clientScan()
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
)
185 conn
= HTTPConnection(address
, port
, timeout
= timeout
)
188 if clientVerifyVersion(conn
, timeout
):
192 reporting(report
, "Incorrect master version", ValueError)
193 except BaseException
as err
:
195 report({'ERROR'}, str(err
))
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
:
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'))
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
)
236 value
= hashData(f
.read())
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
):
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
)
256 print("Couldn't create directory:", directory_path
)
257 if original_path
!= directory_path
:
258 print("Expanded from the following path:", original_path
)
262 def cacheName(ob
, point_cache
):
263 name
= point_cache
.name
265 name
= "".join(["%02X" % ord(c
) for c
in ob
.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
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
):
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
)
329 finalpath
= os
.path
.join(prefixdirectory
, name
)
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
)
347 def getResults(server_address
, server_port
, job_id
, resolution_x
, resolution_y
, resolution_percentage
, frame_ranges
):
349 print("=============================================")
350 print("============= FETCHING RESULTS ==============")
353 for r
in frame_ranges
:
355 frame_arguments
.extend(["-s", str(r
[0]), "-e", str(r
[1]), "-a"])
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)
363 [bpy
.app
.binary_path
,
368 "-o", bpy
.path
.abspath(bpy
.context
.scene
.render
.filepath
),
370 ] + frame_arguments
+
378 str(resolution_percentage
),
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)
389 print(str(stdout
, encoding
='utf-8'), end
="")
392 # read leftovers if needed
393 stdout
= process
.stdout
.read()
395 print(str(stdout
, encoding
='utf-8'))
400 print("=============================================")
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
,
433 stdout
=subprocess
.PIPE
,
434 stderr
=subprocess
.STDOUT
,
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("$")]
450 if __name__
== "__main__":
452 start
= sys
.argv
.index("--") + 1
455 action
, *args
= sys
.argv
[start
:]
457 if action
== "FileInfo":
459 print("$", eval(info
))
460 elif action
== "GetResults":
461 _getResults(args
[0], args
[1], args
[2], args
[3], args
[4], args
[5])