├── sublime-plugin └── RemoteCodeRunner │ ├── .gitignore │ ├── Main.sublime-menu │ └── send_python_code.py ├── preview.png ├── .gitignore ├── README.md ├── LICENSE └── remote_code_runner.pyp /sublime-plugin/RemoteCodeRunner/.gitignore: -------------------------------------------------------------------------------- 1 | /store.json -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markkorput/sublime-script/HEAD/preview.png -------------------------------------------------------------------------------- /sublime-plugin/RemoteCodeRunner/Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "tools", 4 | "children": 5 | [ 6 | {"caption": "-"}, 7 | { "command": "send_python_code", "caption": "Send Python Code" }, 8 | { "command": "set_python_code_destination", "caption": "Set Python Code destination ..." } 9 | ] 10 | } 11 | ] -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remote Code Runner - Cinema 4D & Sublime Text plugin 2 | 3 | *Remote Code Runner* is a plugin for Cinema 4D and Sublime Text 4 | that allows you to sent code from Sublime Text to Cinema 4D and execute 5 | it like a script. 6 | 7 | ![Preview Image](preview.png) 8 | 9 | ## Installation 10 | 11 | 1. Download the latest code from GitHub and unpack the content into 12 | your Cinema 4D plugin directory. 13 | 14 | ``` 15 | Cinema 4D RXX/ 16 | plugins/ 17 | remote-code-runner/ 18 | remote_code_runner.pyp 19 | sublime-plugin/ 20 | RemoteCodeRunner/ 21 | ... 22 | ... 23 | ``` 24 | 25 | 2. Copy (or symlink) the `RemoteCodeRunner` folder to your sublime 26 | package directory. You can open this directory by heading to Sublime 27 | "Preferences > Browse Packages ...". 28 | 29 | 3. Start Cinema 4D and run the "Tools > Send Python Code" command from 30 | Sublime! The default settings should work fine. 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 nr-plugins 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. 22 | 23 | -------------------------------------------------------------------------------- /sublime-plugin/RemoteCodeRunner/send_python_code.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | # 3 | # Copyright (C) 2014 Niklas Rosenstein 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | __author__ = 'Niklas Rosenstein ' 24 | __version__ = '1.0' 25 | 26 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 27 | # Shared Code (SocketFile wrapper class) 28 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | import sys 31 | if sys.version_info[0] < 3: 32 | try: from cStringIO import StringIO as BytesIO 33 | except ImportError: from StringIO import StringIO as BytesIO 34 | else: 35 | from io import BytesIO 36 | 37 | class SocketFile(object): 38 | """ 39 | File-like wrapper for reading socket objects. 40 | """ 41 | 42 | def __init__(self, socket, encoding=None): 43 | super(SocketFile, self).__init__() 44 | self._socket = socket 45 | self._buffer = BytesIO() 46 | self.encoding = encoding 47 | 48 | def _append_buffer(self, data): 49 | pos = self._buffer.tell() 50 | self._buffer.seek(0, 2) 51 | self._buffer.write(data) 52 | self._buffer.seek(pos) 53 | 54 | def bind(self, *args, **kwargs): 55 | return self._socket.bind(*args, **kwargs) 56 | 57 | def connect(self, *args, **kwargs): 58 | return self._socket.connect(*args, **kwargs) 59 | 60 | def read(self, length, blocking=True): 61 | data = self._buffer.read(length) 62 | delta = length - len(data) 63 | if delta > 0: 64 | self._socket.setblocking(blocking) 65 | try: 66 | data += self._socket.recv(delta) 67 | except socket.error: 68 | pass 69 | return data 70 | 71 | def readline(self): 72 | parts = [] 73 | while True: 74 | # Read the waiting data from the socket. 75 | data = self.read(1024, blocking=False) 76 | 77 | # If it contains a line-feed character, we add it 78 | # to the result list and append the rest of the data 79 | # to the buffer. 80 | if b'\n' in data: 81 | left, right = data.split(b'\n', 1) 82 | parts.append(left + b'\n') 83 | self._append_buffer(right) 84 | break 85 | 86 | else: 87 | if data: 88 | parts.append(data) 89 | 90 | # Read a blocking byte for which we will get an empty 91 | # bytes object if the socket is closed- 92 | byte = self.read(1, blocking=True) 93 | if not byte: 94 | break 95 | 96 | # Add the byte to the buffer. Stop here if it is a 97 | # newline character. 98 | parts.append(byte) 99 | if byte == b'\n': 100 | break 101 | 102 | return b''.join(parts) 103 | 104 | def write(self, data): 105 | if isinstance(data, str): 106 | if not self.encoding: 107 | raise ValueError('got str object and no encoding specified') 108 | data = data.encode(self.encoding) 109 | 110 | return self._socket.send(data) 111 | 112 | def close(self): 113 | return self._socket.close() 114 | 115 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 116 | # Communication with code reciever server 117 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 118 | 119 | import socket 120 | import hashlib 121 | 122 | def parse_credentials(creds): 123 | """ 124 | Parses a credentials string. The first part is the password which 125 | is separated by a forward-slash to the host-name. The host-name is 126 | separated by a double-colon to the port-name. 127 | 128 | Returns a tuple of ``(password, host, port)``. A *ValueError* is 129 | raised if the format is invalid. The password is optional and 130 | None is returned if it is not specified. 131 | """ 132 | 133 | password, _, creds = creds.rpartition('/') 134 | if not password: 135 | password = None 136 | 137 | host, _, creds = creds.rpartition(':') 138 | if not host: 139 | raise ValueError('no host') 140 | 141 | if not creds: 142 | raise ValueError('no port') 143 | try: 144 | port = int(creds) 145 | except ValueError: 146 | raise ValueError('invalid port') 147 | 148 | return (password, host, port) 149 | 150 | def send_code(filename, code, encoding, password, host, port, origin): 151 | """ 152 | Sends Python code to Cinema 4D running at the specified location 153 | using the supplied password. 154 | 155 | :raise ConnectionRefusedError: 156 | """ 157 | 158 | client = SocketFile(socket.socket()) 159 | client.connect((host, port)) #! ConnectionRefusedError 160 | 161 | if isinstance(code, str): 162 | code = code.encode(encoding) 163 | 164 | client.encoding = 'ascii' 165 | client.write("Content-length: {0}\n".format(len(code))) 166 | # The Python instance on the other end will check for a coding 167 | # declaration or otherwise raise a SyntaxError if an invalid 168 | # character was found. 169 | client.write("Encoding: binary\n") 170 | client.write("Filename: {0}\n".format(filename)) 171 | client.write("Origin: {0}\n".format(origin)) 172 | 173 | if password: 174 | passhash = hashlib.md5(password.encode('utf8')).hexdigest() 175 | client.write("Password: {0}\n".format(passhash)) 176 | client.write('\n') # end headers 177 | 178 | client.encoding = None 179 | client.write(code) 180 | 181 | # Read the response from the server. 182 | result = client.readline().decode('ascii') 183 | client.close() 184 | 185 | status = result.partition(':')[2].strip() 186 | if status == 'ok': 187 | return None 188 | return status # error code 189 | 190 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 191 | # Sublime Integration 192 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 193 | 194 | import os, json 195 | import traceback 196 | import sublime, sublime_plugin 197 | settings_filename = os.path.join(os.path.dirname(__file__), 'store.json') 198 | 199 | def show_console(): 200 | window = sublime.active_window() 201 | window.run_command('show_panel', {'panel': 'console'}) 202 | 203 | def default_settings(): 204 | return { 205 | 'credentials': 'alpine/localhost:2900', 206 | } 207 | 208 | def load_settings(): 209 | data = default_settings() 210 | 211 | try: 212 | with open(settings_filename) as fp: 213 | json_data = json.load(fp) 214 | except (IOError, ValueError) as exc: 215 | pass 216 | else: 217 | if isinstance(json_data, dict): 218 | data.update(json_data) 219 | 220 | return data 221 | 222 | def save_settings(): 223 | global settings 224 | try: 225 | with open(settings_filename, 'w') as fp: 226 | json.dump(settings, fp) 227 | except (IOError, OSError) as exc: 228 | print("RemoteCodeRunner: could not save settings") 229 | print(exc) 230 | print() 231 | show_console() 232 | 233 | settings = load_settings() 234 | assert isinstance(settings, dict) 235 | 236 | class SetPythonCodeDestinationCommand(sublime_plugin.ApplicationCommand): 237 | 238 | def run(self): 239 | credentials = settings['credentials'] 240 | window = sublime.active_window() 241 | window.show_input_panel("Set Python Code send credentials:", credentials, self.on_done, None, None) 242 | 243 | def on_done(self, text): 244 | try: 245 | password, host, port = parse_credentials(text) 246 | except ValueError as exc: 247 | sublime.status_message('Credentials format is invalid, must be like "password/host:port"') 248 | return 249 | 250 | settings['credentials'] = text 251 | save_settings() 252 | sublime.status_message('Credentials saved.') 253 | 254 | class SendPythonCodeCommand(sublime_plugin.ApplicationCommand): 255 | 256 | def run(self): 257 | global settings 258 | credentials = settings['credentials'] 259 | 260 | view = sublime.active_window().active_view() 261 | code = view.substr(sublime.Region(0, view.size())) 262 | filename = view.file_name() or 'untitled' 263 | encoding = view.encoding() 264 | if encoding == 'Undefined': 265 | encoding = 'UTF-8' 266 | 267 | try: 268 | password, host, port = parse_credentials(credentials) 269 | except ValueError as exc: 270 | sublime.status_message('Invalid credentials saved.') 271 | return 272 | 273 | try: 274 | error = send_code(filename, code, encoding, password, host, port, 'Sublime Text') 275 | except ConnectionRefusedError as exc: 276 | sublime.status_message('Could not connect to {0}:{1}'.format(host, port)) 277 | return 278 | except socket.error as exc: 279 | sublime.status_message('socket.error occured, see console') 280 | show_console() 281 | traceback.print_exc() 282 | return 283 | 284 | if error == 'invalid-password': 285 | sublime.status_message('Password was not accepted by the Remote Code Executor Server at {0}:{1}'.format(host, port)) 286 | elif error == 'invalid-request': 287 | sublime.status_message('Request was invalid, maybe this plugin is outdated?') 288 | elif error is not None: 289 | sublime.status_message('error (unexpected): {0}'.format(error)) 290 | else: 291 | sublime.status_message('Code sent to {0}:{1}'.format(host, port)) 292 | 293 | -------------------------------------------------------------------------------- /remote_code_runner.pyp: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | # 3 | # Copyright (C) 2014 Niklas Rosenstein 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 13 | # all 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 21 | # THE SOFTWARE. 22 | 23 | __author__ = 'Niklas Rosenstein ' 24 | __version__ = '1.0' 25 | 26 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 27 | # Shared Code (SocketFile wrapper class) 28 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | import sys 31 | if sys.version_info[0] < 3: 32 | try: from cStringIO import StringIO as BytesIO 33 | except ImportError: from StringIO import StringIO as BytesIO 34 | else: 35 | from io import BytesIO 36 | 37 | class SocketFile(object): 38 | """ 39 | File-like wrapper for reading socket objects. 40 | """ 41 | 42 | def __init__(self, socket, encoding=None): 43 | super(SocketFile, self).__init__() 44 | self._socket = socket 45 | self._buffer = BytesIO() 46 | self.encoding = encoding 47 | 48 | def _append_buffer(self, data): 49 | pos = self._buffer.tell() 50 | self._buffer.seek(0, 2) 51 | self._buffer.write(data) 52 | self._buffer.seek(pos) 53 | 54 | def bind(self, *args, **kwargs): 55 | return self._socket.bind(*args, **kwargs) 56 | 57 | def connect(self, *args, **kwargs): 58 | return self._socket.connect(*args, **kwargs) 59 | 60 | def read(self, length, blocking=True): 61 | data = self._buffer.read(length) 62 | delta = length - len(data) 63 | if delta > 0: 64 | self._socket.setblocking(blocking) 65 | try: 66 | data += self._socket.recv(delta) 67 | except socket.error: 68 | pass 69 | return data 70 | 71 | def readline(self): 72 | parts = [] 73 | while True: 74 | # Read the waiting data from the socket. 75 | data = self.read(1024, blocking=False) 76 | 77 | # If it contains a line-feed character, we add it 78 | # to the result list and append the rest of the data 79 | # to the buffer. 80 | if b'\n' in data: 81 | left, right = data.split(b'\n', 1) 82 | parts.append(left + b'\n') 83 | self._append_buffer(right) 84 | break 85 | 86 | else: 87 | if data: 88 | parts.append(data) 89 | 90 | # Read a blocking byte for which we will get an empty 91 | # bytes object if the socket is closed- 92 | byte = self.read(1, blocking=True) 93 | if not byte: 94 | break 95 | 96 | # Add the byte to the buffer. Stop here if it is a 97 | # newline character. 98 | parts.append(byte) 99 | if byte == b'\n': 100 | break 101 | 102 | return b''.join(parts) 103 | 104 | def write(self, data): 105 | if isinstance(data, str): 106 | if not self.encoding: 107 | raise ValueError('got str object and no encoding specified') 108 | data = data.encode(self.encoding) 109 | 110 | return self._socket.send(data) 111 | 112 | def close(self): 113 | return self._socket.close() 114 | 115 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 116 | # Request Handling and Server thread 117 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 118 | 119 | import sys, codecs, socket, hashlib, threading 120 | 121 | class SourceObject(object): 122 | """ 123 | Represents source-code sent over from another machine or 124 | process which can be executed later. 125 | """ 126 | 127 | def __init__(self, addr, filename, source, origin): 128 | super(SourceObject, self).__init__() 129 | self.host, self.port = addr 130 | self.filename = filename 131 | self.source = source 132 | self.origin = origin 133 | 134 | def __repr__(self): 135 | return ''.format( 136 | self.filename, self.origin, self.host, self.port) 137 | 138 | def execute(self, scope): 139 | """ 140 | Execute the source in the specified scope. 141 | """ 142 | 143 | code = compile(self.source, self.filename, 'exec') 144 | exec code in scope 145 | 146 | def parse_headers(fp): 147 | """ 148 | Parses HTTP-like headers into a dictionary until an empty line 149 | is found. Invalid headers are ignored and if a header is found 150 | twice, it won't overwrite its previous value. Header-keys are 151 | converted to lower-case and stripped of whitespace at both ends. 152 | """ 153 | 154 | headers = {} 155 | while True: 156 | line = fp.readline().strip() 157 | if not line: break 158 | 159 | key, _, value = line.partition(':') 160 | key = key.rstrip().lower() 161 | if key not in headers: 162 | headers[key] = value.lstrip() 163 | 164 | return headers 165 | 166 | def parse_request(conn, addr, required_password): 167 | """ 168 | Communicates with the client parsing the headers and source 169 | code that is to be queued to be executed any time soon. 170 | 171 | Writes on of these lines back to the client: 172 | 173 | - status: invalid-password 174 | - status: invalid-request 175 | - status: encoding-error 176 | - status: ok 177 | 178 | :pass conn: The socket to the client. 179 | :pass addr: The client address tuple. 180 | :pass required_password: The password that must match the 181 | password sent with the "Password" header (as encoded 182 | utf8 converted to md5). Will be converted to md5 by 183 | this function. 184 | :return: :class:`SourceObject` or None 185 | """ 186 | 187 | client = SocketFile(conn, encoding='utf8') 188 | headers = parse_headers(client) 189 | 190 | # Get the password and validate it. 191 | if required_password is not None: 192 | passhash = hashlib.md5(required_password.encode('utf8')).hexdigest() 193 | if passhash != headers['password']: 194 | client.write('status: invalid-password') 195 | return None 196 | 197 | # Get the content-length of the request. 198 | content_length = headers.get('content-length', None) 199 | if content_length is None: 200 | client.write('status: invalid-request') 201 | return None 202 | try: 203 | content_length = int(content_length) 204 | except ValueError as exc: 205 | client.write('status: invalid-request') 206 | return None 207 | 208 | # Get the encoding, default to binary. 209 | encoding = headers.get('encoding', None) 210 | if encoding is None: 211 | encoding = 'binary' 212 | else: 213 | # default to binary if the encoding does not exist. 214 | try: codecs.lookup(encoding) 215 | except LookupError as exc: 216 | encoding = 'binary' 217 | 218 | # Get the filename, origin and source code. 219 | origin = headers.get('origin', 'unknown') 220 | filename = headers.get('filename', 'untitled') 221 | try: 222 | source = client.read(content_length) 223 | if encoding != 'binary': 224 | source = source.decode(encoding) 225 | except UnicodeDecodeError as exc: 226 | client.write('status: encoding-error') 227 | return None 228 | 229 | client.write('status: ok') 230 | return SourceObject(addr, filename, source, origin) 231 | 232 | class ServerThread(threading.Thread): 233 | """ 234 | When the thread is started, the thread binds a server to the 235 | specified host and port accepting incoming source code, optionally 236 | password protected, and appends it to the specified queue. A lock 237 | for synchronization must be passed along with the queue. 238 | """ 239 | 240 | def __init__(self, queue, queue_lock, host, port, password=None): 241 | super(ServerThread, self).__init__() 242 | self._queue = queue 243 | self._queue_lock = queue_lock 244 | self._socket = None 245 | self._addr = (host, port) 246 | self._running = False 247 | self._lock = threading.Lock() 248 | self._password = password 249 | 250 | @property 251 | def running(self): 252 | with self._lock: 253 | return self._running 254 | 255 | @running.setter 256 | def running(self, value): 257 | with self._lock: 258 | self._running = value 259 | 260 | def run(self): 261 | try: 262 | while self.running: 263 | source = self.handle_request() 264 | if source: 265 | with self._queue_lock: 266 | self._queue.append(source) 267 | finally: 268 | self._socket.close() 269 | 270 | def start(self): 271 | self._socket = socket.socket() 272 | self._socket.bind(self._addr) 273 | self._socket.listen(5) 274 | self._socket.settimeout(0.5) 275 | self.running = True 276 | return super(ServerThread, self).start() 277 | 278 | def handle_request(self): 279 | conn = None 280 | try: 281 | conn, addr = self._socket.accept() 282 | source = parse_request(conn, addr, self._password) 283 | return source 284 | except socket.timeout: 285 | return None 286 | finally: 287 | if conn: 288 | conn.close() 289 | 290 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 291 | # Cinema 4D integration 292 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 293 | 294 | import c4d, collections, traceback 295 | 296 | plugins = [] 297 | 298 | class CodeExecuterMessageHandler(c4d.plugins.MessageData): 299 | 300 | PLUGIN_ID = 1033731 301 | PLUGIN_NAME = "Remote Code Executor" 302 | 303 | def __init__(self, host, port, password): 304 | super(CodeExecuterMessageHandler, self).__init__() 305 | self.queue = collections.deque() 306 | self.queue_lock = threading.Lock() 307 | self.thread = ServerThread(self.queue, self.queue_lock, host, port, password) 308 | 309 | print "Binding Remote Code Executor Server to {0}:{1} ...".format(host, port) 310 | try: 311 | self.thread.start() 312 | except socket.error as exc: 313 | print "Failed to bind to {0}:{1}".format(host, port) 314 | self.thread = None 315 | 316 | def register(self): 317 | return c4d.plugins.RegisterMessagePlugin( 318 | self.PLUGIN_ID, self.PLUGIN_NAME, 0, self) 319 | 320 | def get_scope(self): 321 | doc = c4d.documents.GetActiveDocument() 322 | op = doc.GetActiveObject() 323 | mat = doc.GetActiveMaterial() 324 | tp = doc.GetParticleSystem() 325 | return { 326 | '__name__': '__main__', 327 | 'doc': doc, 'op': op, 'mat': mat, 'tp': tp} 328 | 329 | def on_shutdown(self): 330 | if self.thread: 331 | print "Shutting down Remote Code Executor Server thread ..." 332 | self.thread.running = False 333 | self.thread.join() 334 | self.thread = None 335 | 336 | def GetTimer(self): 337 | if self.thread: 338 | return 500 339 | return 0 340 | 341 | def CoreMessage(self, kind, bc): 342 | # Execute source code objects while they're available. 343 | while True: 344 | with self.queue_lock: 345 | if not self.queue: break 346 | source = self.queue.popleft() 347 | try: 348 | print "RemoteCodeRunner: running", source 349 | scope = self.get_scope() 350 | source.execute(scope) 351 | except Exception as exc: 352 | traceback.print_exc() 353 | return True 354 | 355 | def main(): 356 | global plugins 357 | handler = CodeExecuterMessageHandler('localhost', 2900, 'alpine') 358 | handler.register() 359 | plugins.append(handler) 360 | 361 | def PluginMessage(kind, data): 362 | if kind in [c4d.C4DPL_ENDACTIVITY, c4d.C4DPL_RELOADPYTHONPLUGINS]: 363 | for plugin in plugins: 364 | method = getattr(plugin, 'on_shutdown', None) 365 | if callable(method): 366 | try: method() 367 | except Exception as exc: 368 | traceback.print_exc() 369 | return True 370 | 371 | if __name__ == "__main__": 372 | main() 373 | 374 | --------------------------------------------------------------------------------