""" 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()