cadquery-freecad-module/CadQuery/Libs/pyqode/core/backend/server.py
2015-10-26 16:25:38 -04:00

210 lines
6.7 KiB
Python

# -*- coding: utf-8 -*-
"""
This module contains the server socket definition.
"""
import argparse
import inspect
import logging
import json
import struct
import sys
import traceback
try:
import socketserver
PY33 = True
except ImportError:
import SocketServer as socketserver
PY33 = False
def _logger():
""" Returns the module's logger """
return logging.getLogger(__name__)
def import_class(klass):
"""
Imports a class from a fully qualified name string.
:param klass: class string, e.g.
"pyqode.core.backend.workers.CodeCompletionWorker"
:return: The corresponding class
"""
path = klass.rfind(".")
class_name = klass[path + 1: len(klass)]
try:
module = __import__(klass[0:path], globals(), locals(), [class_name])
klass = getattr(module, class_name)
except ImportError as e:
raise ImportError('%s: %s' % (klass, str(e)))
except AttributeError:
raise ImportError(klass)
else:
return klass
class JsonServer(socketserver.TCPServer):
"""
A server socket based on a json messaging system.
"""
class _Handler(socketserver.BaseRequestHandler):
"""
Our custom request handler. There will always be only 1 request that
establish the communication, this is a 1 to 1.
Once the connection has been establish will loop forever waiting for
pending command or for the shutdown signal.
The handler also implements all the logic for packing/unpacking
messages and calling the requested worker instance.
"""
def read_bytes(self, size):
"""
Read x bytes
:param size: number of bytes to read.
"""
if not PY33:
data = ''
else:
data = bytes()
while len(data) < size:
tmp = self.request.recv(size - len(data))
data += tmp
if tmp == '':
raise RuntimeError("socket connection broken")
return data
def get_msg_len(self):
""" Gets message len """
data = self.read_bytes(4)
payload = struct.unpack('=I', data)
return payload[0]
def read(self):
""" Reads a json string from socket and load it. """
size = self.get_msg_len()
data = self.read_bytes(size).decode('utf-8')
return json.loads(data)
def send(self, obj):
"""
Sends a python obj as a json string on the socket.
:param obj: The object to send, must be Json serializable.
"""
msg = json.dumps(obj).encode('utf-8')
_logger().debug('sending %d bytes for the payload', len(msg))
header = struct.pack('=I', len(msg))
self.request.sendall(header)
self.request.sendall(msg)
def handle(self):
"""
Hanlde the request and keep it alive while shutdown signal
has not been received
"""
data = self.read()
self._handle(data)
def _handle(self, data):
"""
Handles a work request.
"""
try:
_logger().debug('handling request %r', data)
assert data['worker']
assert data['request_id']
assert data['data'] is not None
response = {'request_id': data['request_id'], 'results': []}
try:
worker = import_class(data['worker'])
except ImportError:
_logger().exception('Failed to import worker class')
else:
if inspect.isclass(worker):
worker = worker()
_logger().debug('worker: %r', worker)
_logger().debug('data: %r', data['data'])
try:
ret_val = worker(data['data'])
except Exception:
_logger().exception(
'something went bad with worker %r(data=%r)',
worker, data['data'])
ret_val = None
if ret_val is None:
ret_val = []
response = {'request_id': data['request_id'],
'results': ret_val}
finally:
_logger().debug('sending response: %r', response)
try:
self.send(response)
except ConnectionAbortedError:
pass
except:
_logger().debug('error with data=%r', data)
exc1, exc2, exc3 = sys.exc_info()
traceback.print_exception(exc1, exc2, exc3, file=sys.stderr)
def __init__(self, args=None):
"""
:param args: Argument parser args. If None, the server will setup and
use its own argument parser (using
:meth:`pyqode.core.backend.default_parser`)
"""
if not args:
args = default_parser().parse_args()
self.port = args.port
self._Handler.srv = self
self._running = True
# print('server running on port %s' % args.port)
socketserver.TCPServer.__init__(
self, ('127.0.0.1', int(args.port)), self._Handler)
_logger().debug('started on 127.0.0.1:%d' % int(args.port))
_logger().debug('running with python %d.%d.%d' %
(sys.version_info[:3]))
def default_parser():
"""
Configures and return the default argument parser. You should use this
parser as a base if you want to add custom arguments.
The default parser only has one argument, the tcp port used to start the
server socket. *(CodeEdit picks up a free port and use it to run
the server and connect its client socket)*
:returns: The default server argument parser.
"""
parser = argparse.ArgumentParser()
parser.add_argument("port", help="the local tcp port to use to run "
"the server")
return parser
def serve_forever(args=None):
"""
Creates the server and serves forever
:param args: Optional args if you decided to use your own
argument parser. Default is None to let the JsonServer setup its own
parser and parse command line arguments.
"""
server = JsonServer(args=args)
server.serve_forever()
# Server script example
if __name__ == '__main__':
from pyqode.core import backend
backend.CodeCompletionWorker.providers.append(
backend.DocumentWordsProvider())
serve_forever()