diff options
Diffstat (limited to 'fg21sim/webui/websocket.py')
-rw-r--r-- | fg21sim/webui/websocket.py | 436 |
1 files changed, 0 insertions, 436 deletions
diff --git a/fg21sim/webui/websocket.py b/fg21sim/webui/websocket.py deleted file mode 100644 index 00bda03..0000000 --- a/fg21sim/webui/websocket.py +++ /dev/null @@ -1,436 +0,0 @@ -# Copyright (c) 2016 Weitian LI <liweitianux@live.com> -# MIT license - -""" -Communicate with the "fg21sim" simulation program through the Web UI using -the WebSocket_ protocol, which provides full-duplex communication channels -over a single TCP connection. - -.. _WebSocket: https://en.wikipedia.org/wiki/WebSocket - - -References ----------- -- Tornado WebSocket: - http://www.tornadoweb.org/en/stable/websocket.html -- Can I Use: WebSocket: - http://caniuse.com/#feat=websockets -""" - -import os -import json -import logging - -import tornado.websocket -from tornado.options import options - -from .consolehandler import ConsoleHandler -from .utils import get_host_ip, ip_in_network -from ..errors import ConfigError - - -logger = logging.getLogger(__name__) - - -class FG21simWSHandler(tornado.websocket.WebSocketHandler): - """ - WebSocket for bi-directional communication between the Web UI and - the server, which can deal with the configurations and execute the - simulation task. - - Generally, WebSocket send and receive data as *string*. Therefore, - the more complex data are stringified as JSON string before sending, - which will be parsed after receive. - - Each message (as a JSON object or Python dictionary) has a ``type`` - field which will be used to determine the following action to take. - - Attributes - ---------- - name : str - Name to distinguish this WebSocket handle. - from_localhost : bool - Set to ``True`` if the access is from the localhost, - otherwise ``False``. - configs : `~ConfigManager` - A ``ConfigManager`` instance, for configuration manipulations when - communicating with the Web UI. - """ - name = "fg21sim" - from_localhost = None - - def check_origin(self, origin): - """Check the origin of the WebSocket access. - - Attributes - ---------- - from_localhost : bool - Set to ``True`` if the access is from the localhost, - otherwise ``False``. - - NOTE - ---- - Currently, only allow access from the ``localhost`` - (i.e., 127.0.0.1) and local LAN. - """ - self.from_localhost = False - logger.info("WebSocket: {0}: origin: {1}".format(self.name, origin)) - ip = get_host_ip(url=origin) - network = options.hosts_allowed - if ip == "127.0.0.1": - self.from_localhost = True - allow = True - logger.info("WebSocket: %s: origin is localhost" % self.name) - elif network.upper() == "ANY": - # Any hosts are allowed - allow = True - logger.warning("WebSocket: %s: any hosts are allowed" % self.name) - elif ip_in_network(ip, network): - allow = True - logger.info("WebSocket: %s: " % self.name + - "client is in the allowed network: %s" % network) - else: - allow = False - logger.error("WebSocket: %s: " % self.name + - "client is NOT in the allowed network: %s" % network) - return allow - - def open(self): - """Invoked when a new WebSocket is opened by the client.""" - # FIXME: - # * better to move to the `Application` class ?? - # * or create a ``ConfigsHandler`` similar to the ``ConsoleHandler`` - self.configs = self.application.configmanager - self.console_handler = ConsoleHandler(websocket=self) - # - logger.info("WebSocket: {0}: opened".format(self.name)) - logger.info("Allowed hosts: {0}".format(options.hosts_allowed)) - - def on_close(self): - """Invoked when a new WebSocket is closed by the client.""" - code, reason = None, None - if hasattr(self, "close_code"): - code = self.close_code - if hasattr(self, "close_reason"): - reason = self.close_reason - logger.info("WebSocket: {0}: closed by client: {1}, {2}".format( - self.name, code, reason)) - - # FIXME/XXX: - # * How to be non-blocking ?? - # NOTE: WebSocket.on_message: may NOT be a coroutine at the moment (v4.3) - # References: - # [1] https://stackoverflow.com/a/35543856/4856091 - # [2] https://stackoverflow.com/a/33724486/4856091 - def on_message(self, message): - """Handle incoming messages and dispatch task according to the - message type. - - NOTE - ---- - The received message (parsed to a Python dictionary) has a ``type`` - item which will be used to determine the following action to take. - - Currently supported message types are: - ``configs``: - Request or set the configurations - ``console``: - Control the simulation tasks, or request logging messages - ``results``: - Request the simulation results - - The sent message also has a ``type`` item of same value, which the - client can be used to figure out the proper actions. - There is a ``success`` item which indicates the status of the - requested operation, and an ``error`` recording the error message - if ``success=False``. - """ - logger.debug("WebSocket: %s: received: %s" % (self.name, message)) - try: - msg = json.loads(message) - msg_type = msg["type"] - except json.JSONDecodeError: - logger.warning("WebSocket: {0}: ".format(self.name) + - "message is not a valid JSON string") - response = {"success": False, - "type": None, - "error": "message is not a valid JSON string"} - except (KeyError, TypeError): - logger.warning("WebSocket: %s: skip invalid message" % self.name) - response = {"success": False, - "type": None, - "error": "type is missing"} - else: - # Check the message type and dispatch task - if msg_type == "configs": - # Request or set the configurations - response = self._handle_configs(msg) - elif msg_type == "console": - # Control the simulation tasks, or request logging messages - # FIXME/XXX: - # * How to make this asynchronously ?? - response = self.console_handler.handle_message(msg) - elif msg_type == "results": - # Request the simulation results - response = self._handle_results(msg) - else: - # Message of unknown type - logger.warning("WebSocket: {0}: ".format(self.name) + - "unknown message type: {0}".format(msg_type)) - response = {"success": False, - "type": msg_type, - "error": "unknown message type %s" % msg_type} - # - msg_response = json.dumps(response) - self.write_message(msg_response) - - def _handle_configs(self, msg): - """Handle the message of type "configs", which request to get or - set some configurations by the client. - - TODO: - * improve the description ... - * split these handling functions into a separate class in a module - - Parameters - ---------- - msg : dict - A dictionary parsed from the incoming JSON message, which - generally has the following syntax: - ``{"type": "configs", "action": <action>, "data": <data>}`` - where the ``<action>`` is ``set`` or ``get``, and the ``<data>`` - is a list of config keys or a dict of config key-value pairs. - - Returns - ------- - response : dict - A dictionary parsed from the incoming JSON message, which - generally has the following syntax: - ``{"type": "configs", "action": <action>, - "data": <data>, "errors": <errors>}`` - where the ``<action>`` is the same as input, the ``<data>`` is - a list of config keys or a dict of config key-value pairs, and - ``<errors>`` contains the error message for the invalid config - values. - """ - try: - msg_type = msg["type"] - msg_action = msg["action"] - response = {"type": msg_type, "action": msg_action} - logger.info("WebSocket: {0}: handle message: ".format(self.name) + - "type: {0}, action: {1}".format(msg_type, msg_action)) - if msg_action == "get": - # Get the values of the specified options - try: - data, errors = self._get_configs(keys=msg["keys"]) - response["success"] = True - response["data"] = data - response["errors"] = errors - except KeyError: - response["success"] = False - response["error"] = "'keys' is missing" - elif msg_action == "set": - # Set the values of the specified options - try: - errors = self._set_configs(data=msg["data"]) - response["success"] = True - response["data"] = {} # be more consistent - response["errors"] = errors - except KeyError: - response["success"] = False - response["error"] = "'data' is missing" - elif msg_action == "reset": - # Reset the configurations to the defaults - self._reset_configs() - response["success"] = True - elif msg_action == "load": - # Load the supplied user configuration file - try: - success, error = self._load_configs(msg["userconfig"]) - response["success"] = success - if not success: - response["error"] = error - except KeyError: - response["success"] = False - response["error"] = "'userconfig' is missing" - elif msg_action == "save": - # Save current configurations to file - try: - success, error = self._save_configs(msg["outfile"], - msg["clobber"]) - response["success"] = success - if not success: - response["error"] = error - except KeyError: - response["success"] = False - response["error"] = "'outfile' or 'clobber' is missing" - else: - logger.warning("WebSocket: {0}: ".format(self.name) + - "unknown action: {0}".format(msg_action)) - response["success"] = False - response["error"] = "unknown action: {0}".format(msg_action) - except KeyError: - # Received message has wrong syntax/format - response = {"success": False, - "type": msg_type, - "error": "no action specified"} - # - logger.debug("WebSocket: {0}: ".format(self.name) + - "response: {0}".format(response)) - return response - - def _get_configs(self, keys=None): - """Get the values of the config options specified by the given keys. - - Parameters - ---------- - keys : list[str], optional - A list of keys specifying the config options whose values will - be obtained. - If ``keys=None``, then all the configurations values are dumped. - - Returns - ------- - data : dict - A dictionary with keys the same as the input keys, and values - the corresponding config option values. - errors : dict - When error occurs (e.g., invalid key), then the specific errors - with details are stored in this dictionary. - - NOTE - ---- - Do not forget the ``userconfig`` option. - """ - if keys is None: - # Dump all the configurations - data = self.configs.dump(flatten=True) - data["userconfig"] = self.configs.userconfig - errors = {} - else: - data = {} - errors = {} - for key in keys: - if key == "userconfig": - data["userconfig"] = self.configs.userconfig - else: - try: - data[key] = self.configs.getn(key) - except KeyError as e: - errors[key] = str(e) - # - return (data, errors) - - def _set_configs(self, data): - """Set the values of the config options specified by the given keys - to the corresponding supplied data. - - NOTE - ---- - The ``userconfig`` needs special handle. - The ``workdir`` and ``configfile`` options should be ignored. - - Parameters - ---------- - data : dict - A dictionary of key-value pairs, with keys specifying the config - options whose value will be changed, and values the new values - to which config options will be set. - NOTE: - If want to set the ``userconfig`` option, an *absolute path* - must be provided. - - Returns - ------- - errors : dict - When error occurs (e.g., invalid key, invalid values), then the - specific errors with details are stored in this dictionary. - """ - errors = {} - for key, value in data.items(): - if key in ["workdir", "configfile"]: - # Ignore "workdir" and "configfile" - continue - elif key == "userconfig": - if os.path.isabs(os.path.expanduser(value)): - self.configs.userconfig = value - else: - errors[key] = "Not an absolute path" - else: - try: - self.configs.setn(key, value) - except KeyError as e: - errors[key] = str(e) - # NOTE: - # Check the whole configurations after all provided options are - # updated, and merge the validation errors. - __, cherr = self.configs.check_all(raise_exception=False) - errors.update(cherr) - return errors - - def _reset_configs(self): - """Reset the configurations to the defaults.""" - self.configs.reset() - - def _load_configs(self, userconfig): - """Load configurations from the provided user configuration file. - - Parameters - ---------- - userconfig: str - The filepath to the user configuration file, which must be - an *absolute path*. - - Returns - ------- - success : bool - ``True`` if the operation succeeded, otherwise, ``False``. - error : str - If failed, this ``error`` saves the details, otherwise, ``None``. - """ - success = False - error = None - if os.path.isabs(os.path.expanduser(userconfig)): - try: - self.configs.read_userconfig(userconfig) - success = True - except ConfigError as e: - error = str(e) - else: - error = "Not an absolute path" - return (success, error) - - def _save_configs(self, outfile, clobber=False): - """Save current configurations to file. - - Parameters - ---------- - outfile: str - The filepath to the output configuration file, which must be - an *absolute path*. - clobber : bool, optional - Whether overwrite the output file if already exists? - - Returns - ------- - success : bool - ``True`` if the operation succeeded, otherwise, ``False``. - error : str - If failed, this ``error`` saves the details, otherwise, ``None``. - """ - success = False - error = None - try: - self.configs.save(outfile, clobber=clobber) - success = True - except (ValueError, OSError) as e: - error = str(e) - return (success, error) - - def _handle_results(self, msg): - # Got a message of supported types - msg_type = msg["type"] - logger.info("WebSocket: {0}: ".format(self.name) + - "handle message of type: {0}".format(msg_type)) - response = {"success": True, "type": msg_type} - return response |