# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ruff: noqa: F841 """RPC proxy, allows both client/server to connect and match connection. In normal RPC, client directly connect to server's IP address. Sometimes this cannot be done when server do not have a static address. RPCProxy allows both client and server connect to the proxy server, the proxy server will forward the message between the client and server. """ # pylint: disable=unused-variable, unused-argument import asyncio import errno import logging import os import socket import struct import threading import time try: import tornado from tornado import gen, ioloop, websocket from . import tornado_util except ImportError as error_msg: raise ImportError( f"RPCProxy module requires tornado package {error_msg}. Try 'pip install tornado'." ) from tvm.support.popen_pool import PopenWorker from . import _ffi_api, base from .base import TrackerCode from .server import _server_env class ForwardHandler: """Forward handler to forward the message.""" def _init_handler(self): """Initialize handler.""" self._init_message = b"" self._init_req_nbytes = 4 self._magic = None self.timeout = None self._rpc_key_length = None self._done = False self._proxy = ProxyServerHandler.current assert self._proxy self.rpc_key = None self.match_key = None self.forward_proxy = None self.alloc_time = None def __del__(self): logging.info("Delete %s...", self.name()) def name(self): """Name of this connection.""" return "RPCConnection" def _init_step(self, message): if self._magic is None: assert len(message) == 4 self._magic = struct.unpack(" len(self._init_message): nbytes = self._init_req_nbytes - len(self._init_message) self._init_message += message[:nbytes] message = message[nbytes:] if self._init_req_nbytes == len(self._init_message): temp = self._init_message self._init_req_nbytes = 0 self._init_message = b"" self._init_step(temp) if message: logging.info("Invalid RPC protocol, too many bytes %s", self.name()) self.close() def on_error(self, err): logging.info("%s: Error in RPC %s", self.name(), err) self.close_pair() def close_pair(self): if self.forward_proxy: self.forward_proxy.signal_close() self.forward_proxy = None self.close() def on_close_event(self): """on close event""" assert not self._done logging.info("RPCProxy:on_close_event %s ...", self.name()) if self.match_key: key = self.match_key if self._proxy._client_pool.get(key, None) == self: self._proxy._client_pool.pop(key) if self._proxy._server_pool.get(key, None) == self: self._proxy._server_pool.pop(key) self._done = True self.forward_proxy = None class TCPHandler(tornado_util.TCPHandler, ForwardHandler): """Event driven TCP handler.""" def __init__(self, sock, addr): super().__init__(sock) self._init_handler() self.addr = addr def name(self): return f"TCPSocketProxy:{self.addr[0]!s}:{self.rpc_key}" def send_data(self, message, binary=True): self.write_message(message, True) def on_message(self, message): self.on_data(message) def on_close(self): logging.info("RPCProxy: on_close %s ...", self.name()) self._close_process = True if self.forward_proxy: self.forward_proxy.signal_close() self.forward_proxy = None self.on_close_event() class WebSocketHandler(websocket.WebSocketHandler, ForwardHandler): """Handler for websockets.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._init_handler() def name(self): return f"WebSocketProxy:{self.rpc_key}" def on_message(self, message): self.on_data(message) def data_received(self, _): raise NotImplementedError() def send_data(self, message): try: self.write_message(message, True) except websocket.WebSocketClosedError as err: self.on_error(err) def on_close(self): logging.info("RPCProxy: on_close %s ...", self.name()) if self.forward_proxy: self.forward_proxy.signal_close() self.forward_proxy = None self.on_close_event() def signal_close(self): self.close() MIME_MAP = { "js": "text/javascript", "css": "text/css", "wasm": "application/wasm", "json": "application/json", } class RequestHandler(tornado.web.RequestHandler): """Handles html request.""" def __init__(self, *args, **kwargs): file_path = kwargs.pop("file_path") self.format = file_path.split(".")[-1] if file_path.endswith("html"): self.page = open(file_path).read() web_port = kwargs.pop("rpc_web_port", None) if web_port: self.page = self.page.replace( "ws://localhost:9190/ws", f"ws://localhost:{web_port}/ws" ) else: self.page = open(file_path, "rb").read() super().__init__(*args, **kwargs) def data_received(self, _): pass def get(self, *args, **kwargs): if self.format in MIME_MAP: self.set_header("Content-Type", MIME_MAP[self.format]) self.write(self.page) class ProxyServerHandler: """Internal proxy server handler class.""" current = None def __init__( self, sock, listen_port, web_port, timeout_client, timeout_server, tracker_addr, index_page=None, resource_files=None, ): assert ProxyServerHandler.current is None ProxyServerHandler.current = self if web_port: handlers = [(r"/ws", WebSocketHandler)] if index_page: handlers.append( (r"/", RequestHandler, {"file_path": index_page, "rpc_web_port": web_port}) ) logging.info("Serving RPC index html page at http://localhost:%d", web_port) resource_files = resource_files if resource_files else [] for item in resource_files: prefix, fname = item if not prefix.endswith("/"): prefix += "/" if not prefix.startswith("/"): prefix = "/" + prefix basename = os.path.basename(fname) pair = (rf"{prefix}{basename}", RequestHandler, {"file_path": fname}) handlers.append(pair) logging.info(pair) self.app = tornado.web.Application(handlers) self.app.listen(web_port) self.sock = sock self.sock.setblocking(0) self.loop = ioloop.IOLoop.current() def event_handler(_, events): self._on_event(events) self.loop.add_handler(self.sock.fileno(), event_handler, self.loop.READ) self._client_pool = {} self._server_pool = {} self.timeout_alloc = 5 self.timeout_client = timeout_client self.timeout_server = timeout_server # tracker information self._listen_port = listen_port self._tracker_addr = tracker_addr self._tracker_conn = None self._tracker_pending_puts = [] self._key_set = set() self.update_tracker_period = 2 if tracker_addr: logging.info("Tracker address:%s", str(tracker_addr)) def _callback(): self._update_tracker(True) self.loop.call_later(self.update_tracker_period, _callback) logging.info("RPCProxy: Websock port bind to %d", web_port) def _on_event(self, _): while True: try: conn, addr = self.sock.accept() TCPHandler(conn, addr) except OSError as err: if err.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK): break def _pair_up(self, lhs, rhs): lhs.forward_proxy = rhs rhs.forward_proxy = lhs lhs.send_data(struct.pack(" self.timeout_alloc: update_keys.append(k) v.alloc_time = None if update_keys: logging.info( "RPCProxy: No incoming conn on %s, regenerate keys...", str(update_keys) ) new_keys = self._regenerate_server_keys(update_keys) self._tracker_pending_puts += new_keys need_update_info = False # report new connections for key in self._tracker_pending_puts: rpc_key, _ = base.split_random_key(key) base.sendjson( self._tracker_conn, [TrackerCode.PUT, rpc_key, (self._listen_port, key), None] ) assert base.recvjson(self._tracker_conn) == TrackerCode.SUCCESS if rpc_key not in self._key_set: self._key_set.add(rpc_key) need_update_info = True if need_update_info: keylist = "[" + ",".join(self._key_set) + "]" cinfo = {"key": "server:proxy" + keylist, "addr": [None, self._listen_port]} base.sendjson(self._tracker_conn, [TrackerCode.UPDATE_INFO, cinfo]) assert base.recvjson(self._tracker_conn) == TrackerCode.SUCCESS self._tracker_pending_puts = [] except OSError as err: logging.info( "Lost tracker connection: %s, try reconnect in %g sec", str(err), self.update_tracker_period, ) self._tracker_conn.close() self._tracker_conn = None self._regenerate_server_keys(self._server_pool.keys()) if period_update: def _callback(): self._update_tracker(True) self.loop.call_later(self.update_tracker_period, _callback) def _handler_ready_tracker_mode(self, handler): """tracker mode to handle handler ready.""" if handler.rpc_key.startswith("server:"): key = base.random_key(handler.match_key, cmap=self._server_pool) handler.match_key = key self._server_pool[key] = handler self._tracker_pending_puts.append(key) self._update_tracker() else: if handler.match_key in self._server_pool: self._pair_up(self._server_pool.pop(handler.match_key), handler) else: handler.send_data(struct.pack("= 4 magic = struct.unpack("