├── .gitignore ├── LICENSE ├── README.md └── standaloneRPC.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Steve Theodore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | standaloneRPC 2 | ============= 3 | 4 | Provides a simple RPC server for controlling a maya.standalone instance remotely. 5 | 6 | To use as a server, execute this file from the MayaPy interpeter: 7 | 8 | mayapy.exe path/to/standaloneRPC.py 9 | 10 | to connect to a server and issue commands: 11 | 12 | import standaloneRPC as srpc 13 | cmd = srpc.CMD('cmds.ls', type='transform') 14 | srpc.send_command(cmd) 15 | >>> {success:True, result:[u'persp', u'top', u'side', u'front'} 16 | 17 | See the CMD class and send_command methods for details. 18 | 19 | 20 | IMPORTANT 21 | ========= 22 | 23 | This module is NOT an attempt to provide a full-blown rpc server! It's a quick way 24 | for maya users to control a maya standalone instance without the commandPort! 25 | 26 | As such, this provides **NO security** so anyone who knew that an instance was 27 | running will have complete control over the target machine! This is **DANGEROUS** 28 | outside of controlled conditions! Do not expose a standalone running this to 29 | the internet. 30 | 31 | This also makes no effort to providing proxy services or marshalling - only basic data 32 | types can be sent and received. 33 | 34 | To drive these points home, we do not use the standard JSON-RPC protocol. That is to make sure 35 | this doesn't get confused with a real, robust, and secure RPC serrver. 36 | 37 | If you want something more robust, sophisticated and general you should check out 38 | 39 | JSON-RPC https://pypi.python.org/pypi/json-rpc 40 | or 41 | 42 | TINYRPC https://pypi.python.org/pypi/tinyrpc/0.5. 43 | 44 | or 45 | 46 | RPYC http://rpyc.readthedocs.org/en/latest/ 47 | -------------------------------------------------------------------------------- /standaloneRPC.py: -------------------------------------------------------------------------------- 1 | ''' 2 | standaloneRPC 3 | 4 | Cheapass server to control a remote instance of maya.standalone using a bare-bones RPC setup. 5 | 6 | To use as a server, execute this file from the MayaPy interpeter: 7 | 8 | mayapy.exe path/to/standaloneRPC.py 9 | 10 | to connect to a server and issue commands: 11 | 12 | import standaloneRPC as srpc 13 | cmd = srpc.CMD('cmds.ls', type='transform') 14 | srpc.send_command(cmd) 15 | >>> {success:True, result:[u'persp', u'top', u'side', u'front'} 16 | 17 | See the CMD class and send_command methods for details. 18 | 19 | 20 | IMPORTANT 21 | 22 | This is NOT an attempt at a full-blown rpc server! It's a quick way for maya users 23 | to control a maya standalone instance without the commandPort! 24 | 25 | As such, this provides NO security so anyone who knew that an instance was 26 | running will have complete control over the target machine! This is DANGEROUS. 27 | Do not expose a standlone running this directly to the internet! 28 | 29 | This also makes no effort to providing proxy services or marshalling - only basic data 30 | types can be sent and received. 31 | 32 | To drive these points home, we do not use the standard JSON-RPC protocol. That is to make sure 33 | this doesn't get confused with a real, robust, and secure RPC serrver. 34 | 35 | If you want something more robust, sophisticated and general you should check out 36 | 37 | JSON-RPC https://pypi.python.org/pypi/json-rpc 38 | or 39 | 40 | TINYRPC https://pypi.python.org/pypi/tinyrpc/0.5. 41 | 42 | or 43 | 44 | RPYC http://rpyc.readthedocs.org/en/latest/ 45 | 46 | ''' 47 | 48 | import sys 49 | import json 50 | import urlparse 51 | import socket 52 | import urllib2 53 | import traceback 54 | from wsgiref.simple_server import make_server 55 | import urllib 56 | import maya.cmds as cmds 57 | 58 | 59 | 60 | 61 | class CMD(str): 62 | ''' 63 | turn a command with arguments and keywords into a web-friendly encoded string 64 | 65 | print CMD('cmds.ls', type='transform') 66 | 'command=cmds.ls&kwargs=%7B%22type%22%3A+%22transform%22%7D' 67 | 68 | pass this to send_command so you don't have to manually create query strings 69 | ''' 70 | def __new__(cls, cmd, *args, **kwargs): 71 | result = {'command': str(cmd)} 72 | if args: 73 | result['args'] = json.dumps(args) 74 | if kwargs: 75 | result['kwargs'] = json.dumps(kwargs) 76 | return urllib.urlencode(result) 77 | 78 | 79 | def send_command(cmd, address = '127.0.0.1', port = 8000): 80 | ''' 81 | send the CMD object 'cmd' to the server at
:. Returns the 82 | json-decoded results 83 | 84 | The return value will always be a json object with the keyword 'succes'. If 85 | 'success' is true, the command executed; if it is false, it excepted on the 86 | server side. 87 | 88 | For successful queries, the object will include a field called 'results' 89 | containg a json-encoded version of the results: 90 | 91 | cmd = CMD('cmds.ls', type='transform') 92 | print send_command(cmd) 93 | >>> {success:True, result:[u'persp', u'top', u'side', u'front'} 94 | 95 | For failed queries, the result includes the exception and a traceback string: 96 | 97 | cmd = CMD('cmds.fred') # nonexistent command 98 | print send_command(cmd) 99 | >>> {"exception": "", 100 | "traceback": "Traceback (most recent call last)... #SNIP#", 101 | "success": false, 102 | "args": "[]", 103 | "kwargs": "{}", 104 | "cmd_name": "cmds.fred"} 105 | 106 | Sending the string 'shutdown' on its own as the command (not encoded as a 107 | CMD) will request the server to quit. 108 | ''' 109 | 110 | url = "http://{address}:{port}/?{cmd}".format(address = address, port = port, cmd = cmd) 111 | q= urllib2.urlopen(url) 112 | raw = q.read() 113 | try: 114 | results = json.loads(raw) 115 | return results 116 | except: 117 | raise ValueError ("Could not parse server responss", raw) 118 | 119 | 120 | 121 | def handle_command (environ, response): 122 | ''' 123 | look for a query string with 'command' in it; eval the string and 124 | execute. Args and KWargs can be passed as json objects. The command will 125 | be evaluated in the global namespace. 126 | 127 | This can be done by hand in a browser address bar, but it's easies to 128 | use the CMD class 129 | 130 | http://192.168.1.105:8000/?command=cmds.ls 131 | http://192.168.1.105:8000/?command=cmds.ls&kwargs{"type":"transform"} 132 | 133 | If the query string command is 'shutdown', quit maya.standalone 134 | ''' 135 | 136 | if not environ.get('QUERY_STRING'): 137 | status = '404 Not Found' 138 | headers = [('Content-type', 'text/plain')] 139 | response(status, headers) 140 | return ["You must supply a command as a query string"] 141 | 142 | query = urlparse.parse_qs( environ['QUERY_STRING'] ) 143 | status = '200 OK' 144 | headers = [('Content-type', 'text/plain')] 145 | response(status, headers) 146 | 147 | cmds_string = query.get('command') 148 | if not cmds_string: 149 | return ['No recognized command'] 150 | 151 | cmd_name = "-" 152 | args = "-" 153 | kwargs = "-" 154 | 155 | try: 156 | cmd_name = cmds_string[0] 157 | args = query.get('args') or [] 158 | if args: 159 | args = json.loads(args[0]) 160 | 161 | kwargs = query.get('kwargs') or {} 162 | if kwargs: 163 | # convert json dictionary to a string rather than unicode keyed dict 164 | unicode_kwargs = json.loads(kwargs[0]) 165 | kwargs = dict( ( str(k), v) for k, v in unicode_kwargs.items()) 166 | 167 | cmd_proc = eval(cmd_name) 168 | if cmd_name == 'shutdown': 169 | try: 170 | return ['SERVER SHUTTING DOWN'] 171 | finally: 172 | cmd_proc() 173 | 174 | result = cmd_proc(*args, **kwargs) 175 | result_js = {'success':True, 'result': result } 176 | return [json.dumps(result_js)] 177 | 178 | except: 179 | result_js = {"success": False, 180 | "cmd_name": cmd_name, 181 | "args": str(args) or "-", 182 | "kwargs" : str(kwargs)or "-", 183 | "exception": str(sys.exc_info()[0]), 184 | "traceback": traceback.format_exc()} 185 | return [json.dumps(result_js)] 186 | 187 | 188 | 189 | 190 | 191 | 192 | def create_server(port=None): 193 | ''' 194 | create a server instance 195 | ''' 196 | port = port or 8000 197 | address = socket.gethostbyname(socket.gethostname()) 198 | server = make_server(address, port, handle_command) 199 | return server, address, port 200 | 201 | 202 | 203 | #=============================================================================== 204 | # #Run the server when run as a script 205 | #=============================================================================== 206 | if __name__ == '__main__': 207 | import maya.standalone 208 | maya.standalone.initialize() 209 | server_instance, address, port = create_server() 210 | 211 | # defined here so we don't need a global server reference... 212 | def shutdown(): 213 | print "*" * 80 214 | print "shutting down" 215 | print "*" * 80 216 | cmds.quit(force=True) 217 | server_instance.shutdown() 218 | raise sys.exit(0) 219 | 220 | print "=" * 80 221 | print ("starting server on %s:%s" % (address,port)).center(80) 222 | print "=" * 80 223 | 224 | server_instance.serve_forever() 225 | --------------------------------------------------------------------------------