Extending the Team Render Server with Plugins

In the last article I showed how the Cinema 4D Team Render Flask server works and how we could add our own routes. Instead of editing the open source server directly one can also write plugins that will add functionality to the server. Plugins can also react to various messages sent by the server.

Simple Example

To add a route to the server we can simply create an ordinary Python plugin. In this plugin’s PluginMessage() function we wait for the C4DPL_WEBSERVER_START message. When we catch this message we can register our new routes:

import c4d

MY_ROUTE = '/pluginHelloWorld'

def SimpleFunction():
return "Hello World Plugin!"

def Register(app, route, function, **options):
app.add_url_rule(route, options.pop('endpoint', None), function, **options)

def PluginMessage(id, data):
if id == c4d.C4DPL_WEBSERVER_START:

# getting the webserver object
app = c4d.modules.net.webserver.app

# getting the "register" function
register = c4d.modules.net.webserver.main["register"]

Register(app, register(MY_ROUTE), SimpleFunction)

return True

The route’s address is defined in a global constant. This constant is used in the utility function “Register” that allows us to easily add a new route. To access the original “register” function and other components of the webserver we can access the c4d.modules.net.webserver object. This object allows us to get useful functions and objects. The functionality is implemented in the “SimpleFunction” which now just returns a string.

Resources

For meaningful functions we have to access different parts of the Team Render system. The global NetRenderService can easily be obtained with:

service = c4d.modules.net.GetGlobalNetRenderService()

To handle the actual requests to the server we have to use the flask module. This can be accessed via:

flask = c4d.modules.net.webserver.flask
print (flask.session)

The current jobs of the render server are obtained with:

webjobs = c4d.modules.net.webserver.jobs

If we want to get the server itself this can be done with:

server = c4d.modules.net.webserver.app

Using Decorators

In the last article we saw how decorators can be used to add standard functionality to our route function. For example we can check if the user calling this route is logged in or if this user has admin privileges.

The decorator function calls are not available on start up. But we can use a little trick to use them. Inside our function we can define another sub-function. This sub-function will only be defined and called when the host function is called. At this time, the decorators are available.

This simple example returns a JSON object with all jobs of the calling user. This is only available for logged in users.


MY_JOBS_ROUTE = '/getMyJobs'

def GetMyJobs():

# get the "RequireLogin" function
RequireLogin = c4d.modules.net.webserver.main["RequireLogin"]

@RequireLogin
def GetMyJobsSub():

jsonify = c4d.modules.net.webserver.main["jsonify"]
flask = c4d.modules.net.webserver.flask
jobs = c4d.modules.net.webserver.jobs

#get the jobs of the current user
foundjobs = jobs.GetJobsOrJob(flask.session[u'useruuid'],0,0,0,0)

# prepare result
result = {}
result['jobs'] = foundjobs

# add userid for convenience
result['useruuid'] = flask.session[u'useruuid']

return jsonify(result)

return GetMyJobsSub()

This example shows how to check if the given user-id corresponds to a user account with admin privileges. This route is only accessible for admins.

ADMIN_ROUTE = '/checkAdmin/<userid>'

def CheckAdmin(userid):

RequireLogin = c4d.modules.net.webserver.main["RequireLogin"]
RequiredAdmin = c4d.modules.net.webserver.main["RequiredAdmin"]

@RequireLogin
@RequiredAdmin
def CheckAdminSub(userid):

userUuid = uuid.UUID(userid)

service = c4d.modules.net.GetGlobalNetRenderService()
userPool = service.GetUserPool()

#get users
users = userPool.GetUsers()

# check if given user is admin
for user in users:
if user['uuid'] == userUuid:
if user['isadmin'] == True:
return "true"

return "false"

return CheckAdminSub(userid)

We must pay attention to the fact that the user-id is stored as an object of the type UUID.

Messages

The Team Render server will send different messages that we can catch in our PluginMessage() function. Messages related to render jobs will contain the job uuid in the message data. The IDs are defined in the lib_net.h header file.

  • C4DPL_JOBCREATED_PRE: message send directly before job is added
  • C4DPL_JOBCREATED_POST: message send after a job was added
  • C4DPL_JOBSTARTED_PRE: message send before rendering starts
  • C4DPL_JOBSTARTED_POST: message send after rendering started
  • C4DPL_JOBFINISHED_PRE: message send directly before job is finished
  • C4DPL_JOBFINISHED_POST: message send after job is finished

Some messages are related to the state of the webserver. In this case the message data is None.

  • C4DPL_WEBSERVER_START: webserver started
  • C4DPL_WEBSERVER_STOP: webserver stopped

This final example shows how to react to such a message. Simply cast the data into an UUID object and use that ID to get more information on the specific render job:

if id == c4d.C4DPL_JOBFINISHED_POST:

jobUuid = c4d.Cast(c4d.modules.uuid.UUID, data)
service = c4d.modules.net.GetGlobalNetRenderService()
jobs = service.GetJobsList(triggerWatchDog=False, rdata=True, assets=False, results=True, log=True, selectedJob=jobUuid, selectedJobOnly=True)

if jobs:
job = jobs[0]
print ("end of job '%s'" % job[c4d.JOB_NAME])
Sebastian Bach

Sebastian Bach

Former SDK Support Specialist I have a degree in media production and specialized in 3D graphics and programming. In the last years I worked on several commercial plugins for Cinema 4D and 3ds max including render engines, asset management and modeling tools.