cadquery-freecad-module/CadQuery/Libs/pyqode/core/managers/backend.py

170 lines
5.7 KiB
Python

"""
This module contains the backend controller
"""
import logging
import socket
import sys
from pyqode.core.api.client import JsonTcpClient, BackendProcess
from pyqode.core.api.manager import Manager
from pyqode.core.backend import NotRunning
import time
def _logger():
return logging.getLogger(__name__)
class BackendManager(Manager):
"""
The backend controller takes care of controlling the client-server
architecture.
It is responsible of starting the backend process and the client socket and
exposes an API to easily control the backend:
- start
- stop
- send_request
"""
def __init__(self, editor):
super(BackendManager, self).__init__(editor)
self._process = None
self._sockets = []
self.server_script = None
self.interpreter = None
self.args = None
@staticmethod
def pick_free_port():
""" Picks a free port """
test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_socket.bind(('127.0.0.1', 0))
free_port = int(test_socket.getsockname()[1])
test_socket.close()
return free_port
def start(self, script, interpreter=sys.executable, args=None, error_callback=None):
"""
Starts the backend process.
The backend is a python script that starts a
:class:`pyqode.core.backend.JsonServer`. You must write the backend
script so that you can apply your own backend configuration.
The script can be run with a custom interpreter. The default is to use
sys.executable.
:param script: Path to the backend script.
:param interpreter: The python interpreter to use to run the backend
script. If None, sys.executable is used unless we are in a frozen
application (frozen backends do not require an interpreter).
:param args: list of additional command line args to use to start
the backend process.
"""
if self.running:
# already started
return
self.server_script = script
self.interpreter = interpreter
self.args = args
backend_script = script.replace('.pyc', '.py')
self._port = self.pick_free_port()
if hasattr(sys, "frozen") and not backend_script.endswith('.py'):
# frozen backend script on windows/mac does not need an interpreter
program = backend_script
pgm_args = [str(self._port)]
else:
program = interpreter
pgm_args = [backend_script, str(self._port)]
if args:
pgm_args += args
self._process = BackendProcess(self.editor)
if error_callback:
self._process.error.connect(error_callback)
self._process.start(program, pgm_args)
_logger().debug('starting backend process: %s %s', program,
' '.join(pgm_args))
def stop(self):
"""
Stops the backend process.
"""
# close all sockets
for socket in self._sockets:
socket._callback = None
socket.close()
t = time.time()
while self._process.state() != self._process.NotRunning:
self._process.waitForFinished(1)
if sys.platform == 'win32':
# Console applications on Windows that do not run an event
# loop, or whose event loop does not handle the WM_CLOSE
# message, can only be terminated by calling kill().
self._process.kill()
else:
self._process.terminate()
_logger().info('stopping backend took %f [s]', time.time() - t)
_logger().info('backend process terminated')
def send_request(self, worker_class_or_function, args, on_receive=None):
"""
Requests some work to be done by the backend. You can get notified of
the work results by passing a callback (on_receive).
:param worker_class_or_function: Worker class or function
:param args: worker args, any Json serializable objects
:param on_receive: an optional callback executed when we receive the
worker's results. The callback will be called with one arguments:
the results of the worker (object)
:raise: backend.NotRunning if the backend process is not running.
"""
if not self.running:
raise NotRunning()
else:
# create a socket, the request will be send as soon as the socket
# has connected
socket = JsonTcpClient(
self.editor, self._port, worker_class_or_function, args,
on_receive=on_receive)
socket.finished.connect(self._rm_socket)
self._sockets.append(socket)
def _rm_socket(self, socket):
self._sockets.remove(socket)
@property
def running(self):
"""
Tells whether the backend process is running.
:return: True if the process is running, otherwise False
"""
return (self._process is not None and
self._process.state() != self._process.NotRunning)
@property
def connected(self):
"""
Checks if the client socket is connected to the backend.
.. deprecated: Since v2.3, a socket is created per request. Checking
for global connection status does not make any sense anymore. This
property now returns ``running``. This will be removed in v2.5
"""
return self.running
@property
def exit_code(self):
"""
Returns the backend process exit status or None if the
process is till running.
"""
if self.running:
return None
else:
return self._process.exitCode()