feat: init project
						commit
						1425e8e33c
					
				| @ -0,0 +1,163 @@ | ||||
| # Byte-compiled / optimized / DLL files | ||||
| .idea | ||||
| __pycache__/ | ||||
| *.py[cod] | ||||
| *$py.class | ||||
| 
 | ||||
| # C extensions | ||||
| *.so | ||||
| 
 | ||||
| # Distribution / packaging | ||||
| .Python | ||||
| build/ | ||||
| develop-eggs/ | ||||
| dist/ | ||||
| downloads/ | ||||
| eggs/ | ||||
| .eggs/ | ||||
| lib/ | ||||
| lib64/ | ||||
| parts/ | ||||
| sdist/ | ||||
| var/ | ||||
| wheels/ | ||||
| share/python-wheels/ | ||||
| *.egg-info/ | ||||
| .installed.cfg | ||||
| *.egg | ||||
| MANIFEST | ||||
| 
 | ||||
| # PyInstaller | ||||
| #  Usually these files are written by a python script from a template | ||||
| #  before PyInstaller builds the exe, so as to inject date/other infos into it. | ||||
| *.manifest | ||||
| *.spec | ||||
| 
 | ||||
| # Installer logs | ||||
| pip-log.txt | ||||
| pip-delete-this-directory.txt | ||||
| 
 | ||||
| # Unit test / coverage reports | ||||
| htmlcov/ | ||||
| .tox/ | ||||
| .nox/ | ||||
| .coverage | ||||
| .coverage.* | ||||
| .cache | ||||
| nosetests.xml | ||||
| coverage.xml | ||||
| *.cover | ||||
| *.py,cover | ||||
| .hypothesis/ | ||||
| .pytest_cache/ | ||||
| cover/ | ||||
| 
 | ||||
| # Translations | ||||
| *.mo | ||||
| *.pot | ||||
| 
 | ||||
| # Django stuff: | ||||
| *.log | ||||
| local_settings.py | ||||
| db.sqlite3 | ||||
| db.sqlite3-journal | ||||
| 
 | ||||
| # Flask stuff: | ||||
| instance/ | ||||
| .webassets-cache | ||||
| 
 | ||||
| # Scrapy stuff: | ||||
| .scrapy | ||||
| 
 | ||||
| # Sphinx documentation | ||||
| docs/_build/ | ||||
| 
 | ||||
| # PyBuilder | ||||
| .pybuilder/ | ||||
| target/ | ||||
| 
 | ||||
| # Jupyter Notebook | ||||
| .ipynb_checkpoints | ||||
| 
 | ||||
| # IPython | ||||
| profile_default/ | ||||
| ipython_config.py | ||||
| 
 | ||||
| # pyenv | ||||
| #   For a library or package, you might want to ignore these files since the code is | ||||
| #   intended to run in multiple environments; otherwise, check them in: | ||||
| # .python-version | ||||
| 
 | ||||
| # pipenv | ||||
| #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. | ||||
| #   However, in case of collaboration, if having platform-specific dependencies or dependencies | ||||
| #   having no cross-platform support, pipenv may install dependencies that don't work, or not | ||||
| #   install all needed dependencies. | ||||
| #Pipfile.lock | ||||
| 
 | ||||
| # poetry | ||||
| #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. | ||||
| #   This is especially recommended for binary packages to ensure reproducibility, and is more | ||||
| #   commonly ignored for libraries. | ||||
| #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control | ||||
| #poetry.lock | ||||
| 
 | ||||
| # pdm | ||||
| #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. | ||||
| #pdm.lock | ||||
| #   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it | ||||
| #   in version control. | ||||
| #   https://pdm.fming.dev/#use-with-ide | ||||
| .pdm.toml | ||||
| .pdm-python | ||||
| .pdm-build/ | ||||
| 
 | ||||
| # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm | ||||
| __pypackages__/ | ||||
| 
 | ||||
| # Celery stuff | ||||
| celerybeat-schedule | ||||
| celerybeat.pid | ||||
| 
 | ||||
| # SageMath parsed files | ||||
| *.sage.py | ||||
| 
 | ||||
| # Environments | ||||
| .env | ||||
| .venv | ||||
| env/ | ||||
| venv/ | ||||
| ENV/ | ||||
| env.bak/ | ||||
| venv.bak/ | ||||
| 
 | ||||
| # Spyder project settings | ||||
| .spyderproject | ||||
| .spyproject | ||||
| 
 | ||||
| # Rope project settings | ||||
| .ropeproject | ||||
| 
 | ||||
| # mkdocs documentation | ||||
| /site | ||||
| 
 | ||||
| # mypy | ||||
| .mypy_cache/ | ||||
| .dmypy.json | ||||
| dmypy.json | ||||
| 
 | ||||
| # Pyre type checker | ||||
| .pyre/ | ||||
| 
 | ||||
| # pytype static type analyzer | ||||
| .pytype/ | ||||
| 
 | ||||
| # Cython debug symbols | ||||
| cython_debug/ | ||||
| 
 | ||||
| # PyCharm | ||||
| #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can | ||||
| #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore | ||||
| #  and can be added to the global gitignore or merged into this file.  For a more nuclear | ||||
| #  option (not recommended) you can uncomment the following to ignore the entire idea folder. | ||||
| #.idea/ | ||||
| @ -0,0 +1,95 @@ | ||||
| # This file is @generated by PDM. | ||||
| # It is not intended for manual editing. | ||||
| 
 | ||||
| [metadata] | ||||
| groups = ["default"] | ||||
| strategy = ["cross_platform"] | ||||
| lock_version = "4.4" | ||||
| content_hash = "sha256:0bb15134896c44e2b834128890e9e548258516fbc4ecc657d1355daccccfdff4" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "colorama" | ||||
| version = "0.4.6" | ||||
| requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" | ||||
| summary = "Cross-platform colored terminal text." | ||||
| files = [ | ||||
|     {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, | ||||
|     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "loguru" | ||||
| version = "0.7.2" | ||||
| requires_python = ">=3.5" | ||||
| summary = "Python logging made (stupidly) simple" | ||||
| dependencies = [ | ||||
|     "colorama>=0.3.4; sys_platform == \"win32\"", | ||||
|     "win32-setctime>=1.0.0; sys_platform == \"win32\"", | ||||
| ] | ||||
| files = [ | ||||
|     {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, | ||||
|     {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "msgpack" | ||||
| version = "1.0.7" | ||||
| requires_python = ">=3.8" | ||||
| summary = "MessagePack serializer" | ||||
| files = [ | ||||
|     {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862"}, | ||||
|     {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329"}, | ||||
|     {file = "msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b"}, | ||||
|     {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6"}, | ||||
|     {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee"}, | ||||
|     {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d"}, | ||||
|     {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d"}, | ||||
|     {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1"}, | ||||
|     {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681"}, | ||||
|     {file = "msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9"}, | ||||
|     {file = "msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415"}, | ||||
|     {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84"}, | ||||
|     {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93"}, | ||||
|     {file = "msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8"}, | ||||
|     {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46"}, | ||||
|     {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b"}, | ||||
|     {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e"}, | ||||
|     {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002"}, | ||||
|     {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c"}, | ||||
|     {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e"}, | ||||
|     {file = "msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1"}, | ||||
|     {file = "msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82"}, | ||||
|     {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b"}, | ||||
|     {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4"}, | ||||
|     {file = "msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee"}, | ||||
|     {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5"}, | ||||
|     {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672"}, | ||||
|     {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075"}, | ||||
|     {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba"}, | ||||
|     {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c"}, | ||||
|     {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5"}, | ||||
|     {file = "msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9"}, | ||||
|     {file = "msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf"}, | ||||
|     {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f"}, | ||||
|     {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81"}, | ||||
|     {file = "msgpack-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc"}, | ||||
|     {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d"}, | ||||
|     {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7"}, | ||||
|     {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61"}, | ||||
|     {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819"}, | ||||
|     {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd"}, | ||||
|     {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f"}, | ||||
|     {file = "msgpack-1.0.7-cp39-cp39-win32.whl", hash = "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad"}, | ||||
|     {file = "msgpack-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3"}, | ||||
|     {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"}, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "win32-setctime" | ||||
| version = "1.1.0" | ||||
| requires_python = ">=3.5" | ||||
| summary = "A small Python utility to set file creation time on Windows" | ||||
| files = [ | ||||
|     {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, | ||||
|     {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, | ||||
| ] | ||||
| @ -0,0 +1,18 @@ | ||||
| [project] | ||||
| name = "wrpc" | ||||
| version = "0.1.0" | ||||
| description = "winhc rpc tools" | ||||
| authors = [ | ||||
|     {name = "JimZhang", email = "zzl22100048@gmail.com"}, | ||||
| ] | ||||
| dependencies = [ | ||||
|     "msgpack>=1.0.7", | ||||
|     "loguru>=0.7.2", | ||||
| ] | ||||
| requires-python = ">=3.9" | ||||
| readme = "README.md" | ||||
| license = {text = "MIT"} | ||||
| 
 | ||||
| [build-system] | ||||
| requires = ["pdm-backend"] | ||||
| build-backend = "pdm.backend" | ||||
| @ -0,0 +1,247 @@ | ||||
| import asyncio | ||||
| from loguru import logger | ||||
| from ..base import GREETINGS, PROTOCOL_VERSION, OP_NOP, OP_PUBLISH, OP_SUBSCRIBE, OP_UNSUBSCRIBE, OP_MESSAGE, \ | ||||
|     OP_ACK, RESPONSE_OK, PING_FRAME | ||||
| 
 | ||||
| 
 | ||||
| async def on_frame_default(frame): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class Client: | ||||
| 
 | ||||
|     def __init__(self, path, name): | ||||
|         self.path = path | ||||
|         self.writer = None | ||||
|         self.reader_fut = None | ||||
|         self.pinger_fut = None | ||||
|         self.buf_size = 8192 | ||||
|         self.name = name | ||||
|         self.frame_id = 0 | ||||
|         self.ping_interval = 1 | ||||
|         self.on_frame = on_frame_default | ||||
|         self.socket_lock = asyncio.Lock() | ||||
|         self.mgmt_lock = asyncio.Lock() | ||||
|         self.connected = False | ||||
|         self.frames = {} | ||||
|         self.timeout = 1 | ||||
| 
 | ||||
|     async def connect(self): | ||||
|         async with self.mgmt_lock: | ||||
|             if self.path.endswith('.sock') or self.path.endswith( | ||||
|                     '.socket') or self.path.endswith( | ||||
|                 '.ipc') or self.path.startswith('/'): | ||||
|                 reader, writer = await asyncio.open_unix_connection( | ||||
|                     self.path, limit=self.buf_size) | ||||
|             else: | ||||
|                 host, port = self.path.rsplit(':', maxsplit=2) | ||||
|                 reader, writer = await asyncio.open_connection( | ||||
|                     host, int(port), limit=self.buf_size) | ||||
|             buf = await asyncio.wait_for(self.readexactly(reader, 3), | ||||
|                                          timeout=self.timeout) | ||||
|             if buf[0] != GREETINGS: | ||||
|                 raise RuntimeError('Unsupported protocol') | ||||
|             if int.from_bytes(buf[1:3], 'little') != PROTOCOL_VERSION: | ||||
|                 raise RuntimeError('Unsupported protocol version') | ||||
|             writer.write(buf) | ||||
|             await asyncio.wait_for(writer.drain(), timeout=self.timeout) | ||||
|             buf = await asyncio.wait_for(self.readexactly(reader, 1), | ||||
|                                          timeout=self.timeout) | ||||
|             if buf[0] != RESPONSE_OK: | ||||
|                 raise RuntimeError(f'Server response: {hex(buf[0])}') | ||||
|             name = self.name.encode() | ||||
|             writer.write(len(name).to_bytes(2, 'little') + name) | ||||
|             await asyncio.wait_for(writer.drain(), timeout=self.timeout) | ||||
|             buf = await asyncio.wait_for(self.readexactly(reader, 1), | ||||
|                                          timeout=self.timeout) | ||||
|             if buf[0] != RESPONSE_OK: | ||||
|                 raise RuntimeError(f'Server response: {hex(buf[0])}') | ||||
|             self.writer = writer | ||||
|             self.connected = True | ||||
|             self.pinger_fut = asyncio.ensure_future(self._t_pinger()) | ||||
|             self.reader_fut = asyncio.ensure_future(self._t_reader(reader)) | ||||
| 
 | ||||
|     async def handle_daemon_exception(self, e): | ||||
|         async with self.mgmt_lock: | ||||
|             if self.connected: | ||||
|                 await self._disconnect() | ||||
|                 import traceback | ||||
|                 logger.error(traceback.format_exc()) | ||||
| 
 | ||||
|     async def _t_pinger(self): | ||||
|         try: | ||||
|             while True: | ||||
|                 await asyncio.sleep(self.ping_interval) | ||||
|                 async with self.socket_lock: | ||||
|                     self.writer.write(PING_FRAME) | ||||
|                     await asyncio.wait_for(self.writer.drain(), | ||||
|                                            timeout=self.timeout) | ||||
|         except Exception as e: | ||||
|             asyncio.ensure_future(self.handle_daemon_exception(e)) | ||||
| 
 | ||||
|     async def _t_reader(self, reader): | ||||
|         try: | ||||
|             while True: | ||||
|                 buf = await self.readexactly(reader, 6) | ||||
|                 if buf[0] == OP_NOP: | ||||
|                     continue | ||||
|                 elif buf[0] == OP_ACK: | ||||
|                     op_id = int.from_bytes(buf[1:5], 'little') | ||||
|                     try: | ||||
|                         o = self.frames.pop(op_id) | ||||
|                         o.result = buf[5] | ||||
|                         o.completed.set() | ||||
|                     except KeyError: | ||||
|                         logger.warning(f'orphaned BUS/RT frame ack {op_id}') | ||||
|                 else: | ||||
| 
 | ||||
|                     async def read_frame(tp, data_len): | ||||
|                         frame = Frame() | ||||
|                         frame.type = tp | ||||
|                         sender = await reader.readuntil(b'\x00') | ||||
|                         data_len -= len(sender) | ||||
|                         frame.sender = sender[:-1].decode() | ||||
|                         frame.primary_sender = frame.sender.split('%%', 1)[0] | ||||
|                         if buf[0] == OP_PUBLISH: | ||||
|                             topic = await reader.readuntil(b'\x00') | ||||
|                             data_len -= len(topic) | ||||
|                             frame.topic = topic[:-1].decode() | ||||
|                         else: | ||||
|                             frame.topic = None | ||||
|                         data = b'' | ||||
|                         while len(data) < data_len: | ||||
|                             buf_size = data_len - len(data) | ||||
|                             prev_len = len(data) | ||||
|                             try: | ||||
|                                 data += await reader.readexactly( | ||||
|                                     buf_size if buf_size < self.buf_size else | ||||
|                                     self.buf_size) | ||||
|                             except asyncio.IncompleteReadError: | ||||
|                                 pass | ||||
|                         frame.payload = data | ||||
|                         return frame | ||||
| 
 | ||||
|                     try: | ||||
|                         data_len = int.from_bytes(buf[1:5], 'little') | ||||
|                         frame = await read_frame(buf[0], data_len) | ||||
|                     except Exception as e: | ||||
|                         logger.error(f'Invalid frame from the server: {e}') | ||||
|                         raise | ||||
|                     asyncio.ensure_future(self.on_frame(frame)) | ||||
|         except Exception as e: | ||||
|             asyncio.ensure_future(self.handle_daemon_exception(e)) | ||||
| 
 | ||||
|     async def readexactly(self, reader, data_len): | ||||
|         data = b'' | ||||
|         while len(data) < data_len: | ||||
|             buf_size = data_len - len(data) | ||||
|             try: | ||||
|                 chunk = await reader.readexactly( | ||||
|                     buf_size if buf_size < self.buf_size else self.buf_size) | ||||
|                 data += chunk | ||||
|             except asyncio.IncompleteReadError: | ||||
|                 await asyncio.sleep(0.01) | ||||
|         return data | ||||
| 
 | ||||
|     async def disconnect(self): | ||||
|         async with self.mgmt_lock: | ||||
|             await self._disconnect() | ||||
| 
 | ||||
|     async def _disconnect(self): | ||||
|         self.connected = False | ||||
|         self.writer.close() | ||||
|         if self.reader_fut is not None: | ||||
|             self.reader_fut.cancel() | ||||
|         if self.pinger_fut is not None: | ||||
|             self.pinger_fut.cancel() | ||||
| 
 | ||||
|     async def send(self, target=None, frame=None): | ||||
|         try: | ||||
|             async with self.socket_lock: | ||||
|                 self.frame_id += 1 | ||||
|                 if self.frame_id > 0xffff_ffff: | ||||
|                     self.frame_id = 1 | ||||
|                 frame_id = self.frame_id | ||||
|                 o = ClientFrame(frame.qos) | ||||
|                 if frame.qos & 0b1 != 0: | ||||
|                     self.frames[frame_id] = o | ||||
|                 flags = frame.type | frame.qos << 6 | ||||
|                 if frame.type == OP_SUBSCRIBE or frame.type == OP_UNSUBSCRIBE: | ||||
|                     topics = frame.topic if isinstance(frame.topic, | ||||
|                                                        list) else [frame.topic] | ||||
|                     payload = b'\x00'.join(t.encode() for t in topics) | ||||
|                     self.writer.write( | ||||
|                         frame_id.to_bytes(4, 'little') + | ||||
|                         flags.to_bytes(1, 'little') + | ||||
|                         len(payload).to_bytes(4, 'little') + payload) | ||||
|                 else: | ||||
|                     frame_len = len(target) + len(frame.payload) + 1 | ||||
|                     if frame.header is not None: | ||||
|                         frame_len += len(frame.header) | ||||
|                     if frame_len > 0xffff_ffff: | ||||
|                         raise ValueError('frame too large') | ||||
|                     self.writer.write( | ||||
|                         frame_id.to_bytes(4, 'little') + | ||||
|                         flags.to_bytes(1, 'little') + | ||||
|                         frame_len.to_bytes(4, 'little') + target.encode() + | ||||
|                         b'\x00' + | ||||
|                         (frame.header if frame.header is not None else b'')) | ||||
|                     self.writer.write(frame.payload.encode( | ||||
|                     ) if isinstance(frame.payload, str) else frame.payload) | ||||
|                 await self.writer.drain() | ||||
|                 return o | ||||
|         except: | ||||
|             try: | ||||
|                 del self.frames[frame_id] | ||||
|             except KeyError: | ||||
|                 pass | ||||
|             raise | ||||
| 
 | ||||
|     def subscribe(self, topics): | ||||
|         frame = Frame(tp=OP_SUBSCRIBE) | ||||
|         frame.topic = topics | ||||
|         return self.send(None, frame) | ||||
| 
 | ||||
|     def unsubscribe(self, topics): | ||||
|         frame = Frame(tp=OP_UNSUBSCRIBE) | ||||
|         frame.topic = topics | ||||
|         return self.send(None, frame) | ||||
| 
 | ||||
|     def is_connected(self): | ||||
|         return self.connected | ||||
| 
 | ||||
| 
 | ||||
| class ClientFrame: | ||||
| 
 | ||||
|     def __init__(self, qos): | ||||
|         self.qos = qos | ||||
|         self.result = 0 | ||||
|         if qos & 0b1 != 0: | ||||
|             self.completed = asyncio.Event() | ||||
|         else: | ||||
|             self.completed = None | ||||
| 
 | ||||
|     def is_completed(self): | ||||
|         if self.qos & 0b1 != 0: | ||||
|             return self.completed.is_set() | ||||
|         else: | ||||
|             return True | ||||
| 
 | ||||
|     async def wait_completed(self, timeout=None): | ||||
|         if self.qos & 0b1 == 0: | ||||
|             return RESPONSE_OK | ||||
|         elif timeout: | ||||
|             await asyncio.wait_for(self.completed.wait(), timeout=timeout) | ||||
|         else: | ||||
|             await self.completed.wait() | ||||
|         return self.result | ||||
| 
 | ||||
| 
 | ||||
| class Frame: | ||||
| 
 | ||||
|     def __init__(self, payload=None, tp=OP_MESSAGE, qos=0): | ||||
|         self.payload = payload | ||||
|         # used for zero-copy | ||||
|         self.header = None | ||||
|         self.type = tp | ||||
|         self.qos = qos | ||||
| @ -0,0 +1,237 @@ | ||||
| import asyncio | ||||
| import functools | ||||
| 
 | ||||
| from loguru import logger | ||||
| 
 | ||||
| from .client import OP_MESSAGE | ||||
| from ..base import ERR_IO, ERR_TIMEOUT, RESPONSE_OK, RPC_NOTIFICATION_HEADER, RPC_REQUEST_HEADER, RPC_REPLY_HEADER, \ | ||||
|     RPC_ERROR_REPLY_HEADER, RPC_NOTIFICATION, RPC_REQUEST, RPC_REPLY, RPC_ERROR, RPC_ERROR_CODE_METHOD_NOT_FOUND, \ | ||||
|     RPC_ERROR_CODE_INTERNAL, serialize, deserialize,RpcException | ||||
| 
 | ||||
| from .client import on_frame_default | ||||
| 
 | ||||
| 
 | ||||
| async def on_call_default(event): | ||||
|     raise RpcException('RPC Engine not initialized', | ||||
|                        RPC_ERROR_CODE_METHOD_NOT_FOUND) | ||||
| 
 | ||||
| 
 | ||||
| async def on_notification_default(event): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| def format_rpc_e_msg(e): | ||||
|     if isinstance(e, RpcException): | ||||
|         return e.rpc_error_payload | ||||
|     else: | ||||
|         return str(e) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| class Rpc: | ||||
| 
 | ||||
|     def __init__(self, client): | ||||
|         self.client = client | ||||
|         self.client.on_frame = self._handle_frame | ||||
|         self.call_id = 0 | ||||
|         self.call_lock = asyncio.Lock() | ||||
|         self.calls = {} | ||||
|         self.on_frame = on_frame_default | ||||
|         self.on_call = on_call_default | ||||
|         self.on_notification = on_notification_default | ||||
| 
 | ||||
|     def is_connected(self): | ||||
|         return self.client.connected | ||||
| 
 | ||||
|     def notify(self, target, notification): | ||||
|         return self.client.send(target, notification) | ||||
| 
 | ||||
|     def call0(self, target, request): | ||||
|         request.header = RPC_REQUEST_HEADER + b'\x00\x00\x00\x00' + \ | ||||
|                          request.method + b'\x00' | ||||
|         return self.client.send(target, request) | ||||
| 
 | ||||
|     async def call(self, target, request): | ||||
|         async with self.call_lock: | ||||
|             call_id = self.call_id + 1 | ||||
|             if call_id == 0xffff_ffff: | ||||
|                 self.call_id = 0 | ||||
|             else: | ||||
|                 self.call_id = call_id | ||||
|         call_event = RpcCallEvent() | ||||
|         self.calls[call_id] = call_event | ||||
|         request.header = RPC_REQUEST_HEADER + call_id.to_bytes( | ||||
|             4, 'little') + request.method + b'\x00' | ||||
|         try: | ||||
|             try: | ||||
|                 code = await (await self.client.send( | ||||
|                     target, | ||||
|                     request)).wait_completed(timeout=self.client.timeout) | ||||
|                 if code != RESPONSE_OK: | ||||
|                     try: | ||||
|                         del self.calls[call_id] | ||||
|                     except KeyError: | ||||
|                         pass | ||||
|                     err_code = -32000 - code | ||||
|                     call_event.error = RpcException('RPC error', code=err_code) | ||||
|                     call_event.completed.set() | ||||
|             except asyncio.TimeoutError: | ||||
|                 try: | ||||
|                     del self.calls[call_id] | ||||
|                 except KeyError: | ||||
|                     pass | ||||
|                 err_code = -32000 - ERR_TIMEOUT | ||||
|                 call_event.error = RpcException('RPC timeout', code=err_code) | ||||
|                 call_event.completed.set() | ||||
|         except Exception as e: | ||||
|             try: | ||||
|                 del self.calls[call_id] | ||||
|             except KeyError: | ||||
|                 pass | ||||
|             call_event.error = RpcException(str(e), code=-32000 - ERR_IO) | ||||
|             call_event.completed.set() | ||||
|         return call_event | ||||
| 
 | ||||
|     def __getattr__(self, method): | ||||
|         return functools.partial(self.params_call, method=method) | ||||
| 
 | ||||
|     async def params_call(self, target: str, method: str, **kwargs): | ||||
|         params = kwargs | ||||
|         if '_raw' in kwargs: | ||||
|             params = kwargs.pop('_raw') | ||||
|         params = params or None | ||||
|         c0 = False | ||||
|         if method.endswith('0'): | ||||
|             c0 = True | ||||
|             method = method[:-1] | ||||
|         request = Request(method, serialize(params)) | ||||
|         if c0: | ||||
|             await self.call0(target, request) | ||||
|             return None | ||||
|         result = await self.call(target, request) | ||||
|         return deserialize((await result.wait_completed()).get_payload()) | ||||
| 
 | ||||
|     async def _handle_frame(self, frame): | ||||
|         try: | ||||
|             if frame.type == OP_MESSAGE: | ||||
|                 if frame.payload[0] == RPC_NOTIFICATION: | ||||
|                     event = Event(RPC_NOTIFICATION, frame, 1) | ||||
|                     await self.on_notification(event) | ||||
|                 elif frame.payload[0] == RPC_REQUEST: | ||||
|                     sender = frame.sender | ||||
|                     call_id_b = frame.payload[1:5] | ||||
|                     call_id = int.from_bytes(call_id_b, 'little') | ||||
|                     method = frame.payload[5:5 + | ||||
|                                              frame.payload[5:].index(b'\x00')] | ||||
|                     event = Event(RPC_REQUEST, frame, 6 + len(method)) | ||||
|                     event.call_id = call_id | ||||
|                     event.method = method | ||||
|                     if call_id == 0: | ||||
|                         await self.on_call(event) | ||||
|                     else: | ||||
|                         reply = Reply() | ||||
|                         try: | ||||
|                             reply.payload = await self.on_call(event) | ||||
|                             if reply.payload is None: | ||||
|                                 reply.payload = b'' | ||||
|                             reply.header = RPC_REPLY_HEADER + call_id_b | ||||
|                         except Exception as e: | ||||
|                             try: | ||||
|                                 code = e.rpc_error_code | ||||
|                             except AttributeError: | ||||
|                                 code = RPC_ERROR_CODE_INTERNAL | ||||
|                             reply.header = ( | ||||
|                                     RPC_ERROR_REPLY_HEADER + call_id_b + | ||||
|                                     code.to_bytes(2, 'little', signed=True)) | ||||
|                             reply.payload = format_rpc_e_msg(e) | ||||
|                         await self.client.send(sender, reply) | ||||
|                 elif frame.payload[0] == RPC_REPLY or frame.payload[ | ||||
|                     0] == RPC_ERROR: | ||||
|                     call_id = int.from_bytes(frame.payload[1:5], 'little') | ||||
|                     try: | ||||
|                         call_event = self.calls.pop(call_id) | ||||
|                         call_event.frame = frame | ||||
|                         if frame.payload[0] == RPC_ERROR: | ||||
|                             err_code = int.from_bytes(frame.payload[5:7], | ||||
|                                                       'little', | ||||
|                                                       signed=True) | ||||
|                             call_event.error = RpcException(frame.payload[7:], | ||||
|                                                             code=err_code) | ||||
|                         call_event.completed.set() | ||||
|                     except KeyError: | ||||
|                         logger.warning(f'orphaned RPC response: {call_id}') | ||||
|                 else: | ||||
|                     logger.error(f'Invalid RPC frame code: {frame.payload[0]}') | ||||
|             else: | ||||
|                 await self.on_frame(frame) | ||||
|         except Exception: | ||||
|             import traceback | ||||
|             logger.error(traceback.format_exc()) | ||||
| 
 | ||||
| 
 | ||||
| class RpcCallEvent: | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         self.frame = None | ||||
|         self.error = None | ||||
|         self.completed = asyncio.Event() | ||||
| 
 | ||||
|     def is_completed(self): | ||||
|         return self.completed.is_set() | ||||
| 
 | ||||
|     async def wait_completed(self, timeout=None): | ||||
|         if timeout: | ||||
|             if not await asyncio.wait_for(self.completed.wait(), | ||||
|                                           timeout=timeout): | ||||
|                 raise TimeoutError | ||||
|         else: | ||||
|             await self.completed.wait() | ||||
|         if self.error is None: | ||||
|             return self | ||||
|         else: | ||||
|             raise self.error | ||||
| 
 | ||||
|     def get_payload(self): | ||||
|         return self.frame.payload[5:] | ||||
| 
 | ||||
|     def is_empty(self): | ||||
|         return len(self.frame.payload) <= 5 | ||||
| 
 | ||||
| 
 | ||||
| class Event: | ||||
| 
 | ||||
|     def __init__(self, tp, frame, payload_pos): | ||||
|         self.tp = tp | ||||
|         self.frame = frame | ||||
|         self._payload_pos = payload_pos | ||||
| 
 | ||||
|     def get_payload(self): | ||||
|         return self.frame.payload[self._payload_pos:] | ||||
| 
 | ||||
| 
 | ||||
| class Notification: | ||||
| 
 | ||||
|     def __init__(self, payload=b''): | ||||
|         self.payload = payload | ||||
|         self.type = OP_MESSAGE | ||||
|         self.qos = 1 | ||||
|         self.header = RPC_NOTIFICATION_HEADER | ||||
| 
 | ||||
| 
 | ||||
| class Request: | ||||
| 
 | ||||
|     def __init__(self, method, params=b''): | ||||
|         self.payload = b'' if params is None else params | ||||
|         self.type = OP_MESSAGE | ||||
|         self.qos = 1 | ||||
|         self.method = method.encode() if isinstance(method, str) else method | ||||
|         self.header = None | ||||
| 
 | ||||
| 
 | ||||
| class Reply: | ||||
| 
 | ||||
|     def __init__(self, result=b''): | ||||
|         self.payload = result | ||||
|         self.type = OP_MESSAGE | ||||
|         self.qos = 1 | ||||
|         self.header = None | ||||
| @ -0,0 +1,64 @@ | ||||
| import msgpack | ||||
| 
 | ||||
| GREETINGS = 0xEB | ||||
| 
 | ||||
| PROTOCOL_VERSION = 1 | ||||
| 
 | ||||
| OP_NOP = 0 | ||||
| OP_PUBLISH = 1 | ||||
| OP_SUBSCRIBE = 2 | ||||
| OP_UNSUBSCRIBE = 3 | ||||
| OP_MESSAGE = 0x12 | ||||
| OP_BROADCAST = 0x13 | ||||
| OP_ACK = 0xFE | ||||
| 
 | ||||
| RESPONSE_OK = 0x01 | ||||
| ERR_CLIENT_NOT_REGISTERED = 0x71 | ||||
| ERR_DATA = 0x72 | ||||
| ERR_IO = 0x73 | ||||
| ERR_OTHER = 0x74 | ||||
| ERR_NOT_SUPPORTED = 0x75 | ||||
| ERR_BUSY = 0x76 | ||||
| ERR_NOT_DELIVERED = 0x77 | ||||
| ERR_TIMEOUT = 0x78 | ||||
| 
 | ||||
| PING_FRAME = b'\x00' * 9 | ||||
| 
 | ||||
| RPC_NOTIFICATION_HEADER = b'\x00' | ||||
| RPC_REQUEST_HEADER = b'\x01' | ||||
| RPC_REPLY_HEADER = b'\x11' | ||||
| RPC_ERROR_REPLY_HEADER = b'\x12' | ||||
| 
 | ||||
| RPC_NOTIFICATION = 0x00 | ||||
| RPC_REQUEST = 0x01 | ||||
| RPC_REPLY = 0x11 | ||||
| RPC_ERROR = 0x12 | ||||
| 
 | ||||
| RPC_ERROR_CODE_PARSE = -32700 | ||||
| RPC_ERROR_CODE_INVALID_REQUEST = -32600 | ||||
| RPC_ERROR_CODE_METHOD_NOT_FOUND = -32601 | ||||
| RPC_ERROR_CODE_INVALID_METHOD_PARAMS = -32602 | ||||
| RPC_ERROR_CODE_INTERNAL = -32603 | ||||
| 
 | ||||
| 
 | ||||
| def serialize(data): | ||||
|     if data is None: | ||||
|         return None | ||||
|     return msgpack.dumps(data) | ||||
| 
 | ||||
| 
 | ||||
| def deserialize(data): | ||||
|     if data is None: | ||||
|         return None | ||||
|     return msgpack.unpackb(data) | ||||
| 
 | ||||
| 
 | ||||
| class RpcException(Exception): | ||||
| 
 | ||||
|     def __init__(self, msg='', code=RPC_ERROR_CODE_INTERNAL): | ||||
|         self.rpc_error_code = code | ||||
|         self.rpc_error_payload = msg | ||||
|         super().__init__(msg if isinstance(msg, str) else msg.decode()) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return super().__str__() + f' (code: {self.rpc_error_code})' | ||||
| @ -0,0 +1,234 @@ | ||||
| import socket | ||||
| import threading | ||||
| import time | ||||
| import traceback | ||||
| from loguru import logger | ||||
| from .base import GREETINGS, PROTOCOL_VERSION, OP_NOP, OP_PUBLISH, OP_SUBSCRIBE, OP_UNSUBSCRIBE, OP_MESSAGE, \ | ||||
|     OP_ACK, RESPONSE_OK, PING_FRAME | ||||
| 
 | ||||
| 
 | ||||
| def on_frame_default(frame): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class Client: | ||||
|     def __init__(self, path, name): | ||||
|         self.path = path | ||||
|         self.socket = None | ||||
|         self.buf_size = 8192 | ||||
|         self.name = name | ||||
|         self.frame_id = 0 | ||||
|         self.ping_interval = 1 | ||||
|         self.on_frame = on_frame_default | ||||
|         self.socket_lock = threading.Lock() | ||||
|         self.mgmt_lock = threading.Lock() | ||||
|         self.connected = False | ||||
|         self.frames = {} | ||||
|         self.timeout = 1 | ||||
| 
 | ||||
|     def connect(self): | ||||
|         with self.mgmt_lock: | ||||
|             if self.path.endswith('.sock') or self.path.endswith( | ||||
|                     '.socket') or self.path.endswith( | ||||
|                 '.ipc') or self.path.startswith('/'): | ||||
|                 self.socket = socket.socket(socket.AF_UNIX) | ||||
|                 path = self.path | ||||
|             else: | ||||
|                 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||
|                 self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, | ||||
|                                        1) | ||||
|                 path = self.path.rsplit(':', maxsplit=2) | ||||
|                 path[1] = int(path[1]) | ||||
|                 path = tuple(path) | ||||
|             self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, | ||||
|                                    self.buf_size) | ||||
|             self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, | ||||
|                                    self.buf_size) | ||||
|             self.socket.settimeout(self.timeout) | ||||
|             self.socket.connect(path) | ||||
|             buf = self.read_exact(3) | ||||
|             if buf[0] != GREETINGS: | ||||
|                 raise RuntimeError('Unsupported protocol') | ||||
|             if int.from_bytes(buf[1:3], 'little') != PROTOCOL_VERSION: | ||||
|                 raise RuntimeError('Unsupported protocol version') | ||||
|             self.socket.sendall(buf) | ||||
|             buf = self.socket.recv(1) | ||||
|             if buf[0] != RESPONSE_OK: | ||||
|                 raise RuntimeError(f'Server response: {hex(buf[0])}') | ||||
|             name = self.name.encode() | ||||
|             self.socket.sendall(len(name).to_bytes(2, 'little') + name) | ||||
|             buf = self.socket.recv(1) | ||||
|             if buf[0] != RESPONSE_OK: | ||||
|                 raise RuntimeError(f'Server response: {hex(buf[0])}') | ||||
|             self.connected = True | ||||
|             threading.Thread(target=self._t_reader, daemon=True).start() | ||||
|             threading.Thread(target=self._t_pinger, daemon=True).start() | ||||
| 
 | ||||
|     def _handle_daemon_exception(self): | ||||
|         with self.mgmt_lock: | ||||
|             if self.connected: | ||||
|                 try: | ||||
|                     self.socket.close() | ||||
|                 except: | ||||
|                     pass | ||||
|                 self.connected = False | ||||
|                 logger.error(traceback.format_exc()) | ||||
| 
 | ||||
|     def _t_pinger(self): | ||||
|         try: | ||||
|             while True: | ||||
|                 time.sleep(self.ping_interval) | ||||
|                 with self.socket_lock: | ||||
|                     self.socket.sendall(PING_FRAME) | ||||
|         except: | ||||
|             self._handle_daemon_exception() | ||||
| 
 | ||||
|     def _t_reader(self): | ||||
|         try: | ||||
|             while True: | ||||
|                 buf = self.read_exact(6) | ||||
|                 if buf[0] == OP_NOP: | ||||
|                     continue | ||||
|                 elif buf[0] == OP_ACK: | ||||
|                     op_id = int.from_bytes(buf[1:5], 'little') | ||||
|                     try: | ||||
|                         o = self.frames.pop(op_id) | ||||
|                         o.result = buf[5] | ||||
|                         o.completed.set() | ||||
|                     except KeyError: | ||||
|                         logger.warning(f'orphaned BUS/RT frame ack {op_id}') | ||||
|                 else: | ||||
|                     data_len = int.from_bytes(buf[1:5], 'little') | ||||
|                     # do not use read_exact for max zero-copy | ||||
|                     data = b'' | ||||
|                     while len(data) < data_len: | ||||
|                         buf_size = data_len - len(data) | ||||
|                         data += self.socket.recv(buf_size if buf_size < self. | ||||
|                                                  buf_size else self.buf_size) | ||||
|                     frame = Frame() | ||||
|                     try: | ||||
|                         frame.type = buf[0] | ||||
|                         if buf[0] == OP_PUBLISH: | ||||
|                             sender, topic, frame.payload = data.split( | ||||
|                                 b'\x00', maxsplit=2) | ||||
|                             frame.topic = topic.decode() | ||||
|                         else: | ||||
|                             sender, frame.payload = data.split(b'\x00', | ||||
|                                                                maxsplit=1) | ||||
|                             frame.topic = None | ||||
|                         frame.sender = sender.decode() | ||||
|                         frame.primary_sender = frame.sender.split('%%', 1)[0] | ||||
|                     except Exception as e: | ||||
|                         logger.error(f'Invalid frame from the server: {e}') | ||||
|                         raise | ||||
|                     try: | ||||
|                         self.on_frame(frame) | ||||
|                     except: | ||||
| 
 | ||||
|                         logger.error(traceback.format_exc()) | ||||
|         except: | ||||
|             self._handle_daemon_exception() | ||||
| 
 | ||||
|     def disconnect(self): | ||||
|         with self.mgmt_lock: | ||||
|             self.socket.close() | ||||
|             self.connected = False | ||||
| 
 | ||||
|     def read_exact(self, data_len): | ||||
|         data = b'' | ||||
|         while len(data) < data_len: | ||||
|             buf_size = data_len - len(data) | ||||
|             try: | ||||
|                 data += self.socket.recv( | ||||
|                     buf_size if buf_size < self.buf_size else self.buf_size) | ||||
|             except socket.timeout: | ||||
|                 if not self.connected: | ||||
|                     break | ||||
|         return data | ||||
| 
 | ||||
|     def send(self, target=None, frame=None): | ||||
|         try: | ||||
|             with self.socket_lock: | ||||
|                 self.frame_id += 1 | ||||
|                 if self.frame_id > 0xffff_ffff: | ||||
|                     self.frame_id = 1 | ||||
|                 frame_id = self.frame_id | ||||
|                 o = ClientFrame(frame.qos) | ||||
|                 if frame.qos & 0b1 != 0: | ||||
|                     self.frames[frame_id] = o | ||||
|                 flags = frame.type | frame.qos << 6 | ||||
|                 if frame.type == OP_SUBSCRIBE or frame.type == OP_UNSUBSCRIBE: | ||||
|                     topics = frame.topic if isinstance(frame.topic, | ||||
|                                                        list) else [frame.topic] | ||||
|                     payload = b'\x00'.join(t.encode() for t in topics) | ||||
|                     self.socket.sendall( | ||||
|                         frame_id.to_bytes(4, 'little') + | ||||
|                         flags.to_bytes(1, 'little') + | ||||
|                         len(payload).to_bytes(4, 'little') + payload) | ||||
|                 else: | ||||
|                     frame_len = len(target) + len(frame.payload) + 1 | ||||
|                     if frame.header is not None: | ||||
|                         frame_len += len(frame.header) | ||||
|                     if frame_len > 0xffff_ffff: | ||||
|                         raise ValueError('frame too large') | ||||
|                     self.socket.sendall( | ||||
|                         frame_id.to_bytes(4, 'little') + | ||||
|                         flags.to_bytes(1, 'little') + | ||||
|                         frame_len.to_bytes(4, 'little') + target.encode() + | ||||
|                         b'\x00' + | ||||
|                         (frame.header if frame.header is not None else b'')) | ||||
|                     self.socket.sendall(frame.payload.encode( | ||||
|                     ) if isinstance(frame.payload, str) else frame.payload) | ||||
|                 return o | ||||
|         except: | ||||
|             try: | ||||
|                 del self.frames[frame_id] | ||||
|             except KeyError: | ||||
|                 pass | ||||
|             raise | ||||
| 
 | ||||
|     def subscribe(self, topics): | ||||
|         frame = Frame(tp=OP_SUBSCRIBE) | ||||
|         frame.topic = topics | ||||
|         return self.send(None, frame) | ||||
| 
 | ||||
|     def unsubscribe(self, topics): | ||||
|         frame = Frame(tp=OP_UNSUBSCRIBE) | ||||
|         frame.topic = topics | ||||
|         return self.send(None, frame) | ||||
| 
 | ||||
|     def is_connected(self): | ||||
|         return self.connected | ||||
| 
 | ||||
| 
 | ||||
| class ClientFrame: | ||||
| 
 | ||||
|     def __init__(self, qos): | ||||
|         self.qos = qos | ||||
|         self.result = 0 | ||||
|         if qos & 0b1 != 0: | ||||
|             self.completed = threading.Event() | ||||
| 
 | ||||
|     def is_completed(self): | ||||
|         if self.qos & 0b1 != 0: | ||||
|             return self.completed.is_set() | ||||
|         else: | ||||
|             return True | ||||
| 
 | ||||
|     def wait_completed(self, *args, **kwargs): | ||||
|         if self.qos & 0b1 == 0: | ||||
|             return RESPONSE_OK | ||||
|         elif not self.completed.wait(*args, **kwargs): | ||||
|             raise TimeoutError | ||||
|         else: | ||||
|             return self.result | ||||
| 
 | ||||
| 
 | ||||
| class Frame: | ||||
| 
 | ||||
|     def __init__(self, payload=None, tp=OP_MESSAGE, qos=0): | ||||
|         self.payload = payload | ||||
|         # used for zero-copy | ||||
|         self.header = None | ||||
|         self.type = tp | ||||
|         self.qos = qos | ||||
| @ -0,0 +1,71 @@ | ||||
| import asyncio | ||||
| import functools | ||||
| import os | ||||
| import random | ||||
| import threading | ||||
| 
 | ||||
| from .asyncio.client import Client as AClient | ||||
| from .asyncio.rpc import Rpc as ARpc | ||||
| from .client import Client | ||||
| from .rpc import Rpc | ||||
| 
 | ||||
| 
 | ||||
| class RpcEndpoint: | ||||
|     def __init__(self, rpc, endpoints, get_endpoints, target): | ||||
|         self.__rpc = rpc | ||||
|         self.__endpoints = endpoints | ||||
|         self.__get_endpoints = get_endpoints | ||||
|         self.__target = target | ||||
| 
 | ||||
|     def refresh_endpoint(self): | ||||
|         return self.__get_endpoints(self.__target) | ||||
| 
 | ||||
|     def __getattr__(self, name): | ||||
|         return functools.partial(getattr(self.__rpc, name), target=random.choice(self.__endpoints)) | ||||
| 
 | ||||
|     def set_endpoints(self, endpoints): | ||||
|         self.__endpoints = endpoints | ||||
| 
 | ||||
| 
 | ||||
| class AsyncHelper: | ||||
|     def __init__(self, uri): | ||||
|         self.bus = AClient(uri, f'python.dynamic.caller.{os.urandom(3).hex()}') | ||||
|         self.rpc = ARpc(self.bus) | ||||
|         self.locker = asyncio.Lock() | ||||
| 
 | ||||
|     async def get_endpoints(self, target): | ||||
|         async with self.locker: | ||||
|             endpoints = {await self.rpc.get_worker(f'js.{target}.api') for _ in range(10)} | ||||
|             return endpoints | ||||
| 
 | ||||
|     async def enter(self, target): | ||||
|         if not self.bus.connected: | ||||
|             async with self.locker: | ||||
|                 if not self.bus.connected: | ||||
|                     await self.bus.connect() | ||||
|         return RpcEndpoint(self.rpc, await self.get_endpoints(target), self.get_endpoints, target) | ||||
| 
 | ||||
|     async def close(self): | ||||
|         await self.bus.disconnect() | ||||
| 
 | ||||
| 
 | ||||
| class Helper: | ||||
|     def __init__(self, uri): | ||||
|         self.bus = Client(uri, f'python.dynamic.caller.{os.urandom(3).hex()}') | ||||
|         self.rpc = Rpc(self.bus) | ||||
|         self.locker = threading.Lock() | ||||
| 
 | ||||
|     def get_endpoints(self, target): | ||||
|         with self.locker: | ||||
|             endpoints = {self.rpc.get_worker(f'js.{target}.api') for _ in range(10)} | ||||
|             return endpoints | ||||
| 
 | ||||
|     def enter(self, target): | ||||
|         if not self.bus.connected: | ||||
|             with self.locker: | ||||
|                 if not self.bus.connected: | ||||
|                     self.bus.connect() | ||||
|         return RpcEndpoint(self.rpc, self.get_endpoints(target), self.get_endpoints, target) | ||||
| 
 | ||||
|     def close(self): | ||||
|         self.bus.disconnect() | ||||
| @ -0,0 +1,236 @@ | ||||
| import functools | ||||
| import threading | ||||
| from loguru import logger | ||||
| 
 | ||||
| from .client import OP_MESSAGE | ||||
| from .base import ERR_IO, ERR_TIMEOUT, RESPONSE_OK, RPC_NOTIFICATION_HEADER, RPC_REQUEST_HEADER, RPC_REPLY_HEADER, \ | ||||
|     RPC_ERROR_REPLY_HEADER, RPC_NOTIFICATION, RPC_REQUEST, RPC_REPLY, RPC_ERROR, RPC_ERROR_CODE_METHOD_NOT_FOUND, \ | ||||
|     RPC_ERROR_CODE_INTERNAL, serialize, deserialize,RpcException | ||||
| from .client import on_frame_default | ||||
| 
 | ||||
| 
 | ||||
| def on_call_default(event): | ||||
|     raise RpcException('RPC Engine not initialized', | ||||
|                        RPC_ERROR_CODE_METHOD_NOT_FOUND) | ||||
| 
 | ||||
| 
 | ||||
| def on_notification_default(event): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| def format_rpc_e_msg(e): | ||||
|     if isinstance(e, RpcException): | ||||
|         return e.rpc_error_payload | ||||
|     else: | ||||
|         return str(e) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| class Rpc: | ||||
| 
 | ||||
|     def __init__(self, client): | ||||
|         self.client = client | ||||
|         self.client.on_frame = self._handle_frame | ||||
|         self.call_id = 0 | ||||
|         self.call_lock = threading.Lock() | ||||
|         self.calls = {} | ||||
|         self.on_frame = on_frame_default | ||||
|         self.on_call = on_call_default | ||||
|         self.on_notification = on_notification_default | ||||
| 
 | ||||
|     def is_connected(self): | ||||
|         return self.client.connected | ||||
| 
 | ||||
|     def notify(self, target, notification): | ||||
|         return self.client.send(target, notification) | ||||
| 
 | ||||
|     def call0(self, target, request): | ||||
|         request.header = RPC_REQUEST_HEADER + b'\x00\x00\x00\x00' + \ | ||||
|                          request.method + b'\x00' | ||||
|         return self.client.send(target, request) | ||||
| 
 | ||||
|     def call(self, target, request): | ||||
|         with self.call_lock: | ||||
|             call_id = self.call_id + 1 | ||||
|             if call_id == 0xffff_ffff: | ||||
|                 self.call_id = 0 | ||||
|             else: | ||||
|                 self.call_id = call_id | ||||
|         call_event = RpcCallEvent() | ||||
|         self.calls[call_id] = call_event | ||||
|         request.header = RPC_REQUEST_HEADER + call_id.to_bytes( | ||||
|             4, 'little') + request.method + b'\x00' | ||||
|         try: | ||||
|             try: | ||||
|                 code = self.client.send( | ||||
|                     target, request).wait_completed(timeout=self.client.timeout) | ||||
|                 if code != RESPONSE_OK: | ||||
|                     try: | ||||
|                         del self.calls[call_id] | ||||
|                     except KeyError: | ||||
|                         pass | ||||
|                     err_code = -32000 - code | ||||
|                     call_event.error = RpcException('RPC error', code=err_code) | ||||
|                     call_event.completed.set() | ||||
|             except TimeoutError: | ||||
|                 try: | ||||
|                     del self.calls[call_id] | ||||
|                 except KeyError: | ||||
|                     pass | ||||
|                 err_code = -32000 - ERR_TIMEOUT | ||||
|                 call_event.error = RpcException('RPC timeout', code=err_code) | ||||
|                 call_event.completed.set() | ||||
|         except Exception as e: | ||||
|             try: | ||||
|                 del self.calls[call_id] | ||||
|             except KeyError: | ||||
|                 pass | ||||
|             call_event.error = RpcException(str(e), code=-32000 - ERR_IO) | ||||
|             call_event.completed.set() | ||||
|         return call_event | ||||
| 
 | ||||
|     def __getattr__(self, method): | ||||
|         return functools.partial(self.params_call, method=method) | ||||
| 
 | ||||
|     def params_call(self, target: str, method: str, **kwargs): | ||||
|         params = kwargs | ||||
|         if '_raw' in kwargs: | ||||
|             params = kwargs.pop('_raw') | ||||
|         params = params or None | ||||
|         c0 = False | ||||
|         if method.endswith('0'): | ||||
|             c0 = True | ||||
|             method = method[:-1] | ||||
|         request = Request(method, serialize(params)) | ||||
|         if c0: | ||||
|             self.call0(target, request) | ||||
|             return None | ||||
|         result = self.call(target, request) | ||||
|         return deserialize((result.wait_completed()).get_payload()) | ||||
| 
 | ||||
|     def _handle_frame(self, frame): | ||||
|         self.spawn(self._t_handler, frame) | ||||
| 
 | ||||
|     def spawn(self, f, *args, **kwargs): | ||||
|         threading.Thread(target=f, args=args, kwargs=kwargs).start() | ||||
| 
 | ||||
|     def _t_handler(self, frame): | ||||
|         try: | ||||
|             if frame.type == OP_MESSAGE: | ||||
|                 if frame.payload[0] == RPC_NOTIFICATION: | ||||
|                     event = Event(RPC_NOTIFICATION, frame, 1) | ||||
|                     self.on_notification(event) | ||||
|                 elif frame.payload[0] == RPC_REQUEST: | ||||
|                     sender = frame.sender | ||||
|                     call_id_b = frame.payload[1:5] | ||||
|                     call_id = int.from_bytes(call_id_b, 'little') | ||||
|                     method = frame.payload[5:5 + | ||||
|                                              frame.payload[5:].index(b'\x00')] | ||||
|                     event = Event(RPC_REQUEST, frame, 6 + len(method)) | ||||
|                     event.call_id = call_id | ||||
|                     event.method = method | ||||
|                     if call_id == 0: | ||||
|                         self.on_call(event) | ||||
|                     else: | ||||
|                         reply = Reply() | ||||
|                         try: | ||||
|                             reply.payload = self.on_call(event) | ||||
|                             if reply.payload is None: | ||||
|                                 reply.payload = b'' | ||||
|                             reply.header = RPC_REPLY_HEADER + call_id_b | ||||
|                         except Exception as e: | ||||
|                             try: | ||||
|                                 code = e.rpc_error_code | ||||
|                             except AttributeError: | ||||
|                                 code = RPC_ERROR_CODE_INTERNAL | ||||
|                             reply.header = ( | ||||
|                                     RPC_ERROR_REPLY_HEADER + call_id_b + | ||||
|                                     code.to_bytes(2, 'little', signed=True)) | ||||
|                             reply.payload = format_rpc_e_msg(e) | ||||
|                         self.client.send(sender, reply) | ||||
|                 elif frame.payload[0] == RPC_REPLY or frame.payload[ | ||||
|                     0] == RPC_ERROR: | ||||
|                     call_id = int.from_bytes(frame.payload[1:5], 'little') | ||||
|                     try: | ||||
|                         call_event = self.calls.pop(call_id) | ||||
|                         call_event.frame = frame | ||||
|                         if frame.payload[0] == RPC_ERROR: | ||||
|                             err_code = int.from_bytes(frame.payload[5:7], | ||||
|                                                       'little', | ||||
|                                                       signed=True) | ||||
|                             call_event.error = RpcException(frame.payload[7:], | ||||
|                                                             code=err_code) | ||||
|                         call_event.completed.set() | ||||
|                     except KeyError: | ||||
|                         logger.warning(f'orphaned RPC response: {call_id}') | ||||
|                 else: | ||||
|                     logger.error(f'Invalid RPC frame code: {frame.payload[0]}') | ||||
|             else: | ||||
|                 self.on_frame(frame) | ||||
|         except Exception: | ||||
|             import traceback | ||||
|             logger.error(traceback.format_exc()) | ||||
| 
 | ||||
| 
 | ||||
| class RpcCallEvent: | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         self.frame = None | ||||
|         self.error = None | ||||
|         self.completed = threading.Event() | ||||
| 
 | ||||
|     def is_completed(self): | ||||
|         return self.completed.is_set() | ||||
| 
 | ||||
|     def wait_completed(self, *args, **kwargs): | ||||
|         if not self.completed.wait(*args, **kwargs): | ||||
|             raise TimeoutError | ||||
|         if self.error is None: | ||||
|             return self | ||||
|         else: | ||||
|             raise self.error | ||||
| 
 | ||||
|     def get_payload(self): | ||||
|         return self.frame.payload[5:] | ||||
| 
 | ||||
|     def is_empty(self): | ||||
|         return len(self.frame.payload) <= 5 | ||||
| 
 | ||||
| 
 | ||||
| class Event: | ||||
| 
 | ||||
|     def __init__(self, tp, frame, payload_pos): | ||||
|         self.tp = tp | ||||
|         self.frame = frame | ||||
|         self._payload_pos = payload_pos | ||||
| 
 | ||||
|     def get_payload(self): | ||||
|         return self.frame.payload[self._payload_pos:] | ||||
| 
 | ||||
| 
 | ||||
| class Notification: | ||||
| 
 | ||||
|     def __init__(self, payload=b''): | ||||
|         self.payload = payload | ||||
|         self.type = OP_MESSAGE | ||||
|         self.qos = 1 | ||||
|         self.header = RPC_NOTIFICATION_HEADER | ||||
| 
 | ||||
| 
 | ||||
| class Request: | ||||
| 
 | ||||
|     def __init__(self, method, params=b''): | ||||
|         self.payload = b'' if params is None else params | ||||
|         self.type = OP_MESSAGE | ||||
|         self.qos = 1 | ||||
|         self.method = method.encode() if isinstance(method, str) else method | ||||
|         self.header = None | ||||
| 
 | ||||
| 
 | ||||
| class Reply: | ||||
| 
 | ||||
|     def __init__(self, result=b''): | ||||
|         self.payload = result | ||||
|         self.type = OP_MESSAGE | ||||
|         self.qos = 1 | ||||
|         self.header = None | ||||
| @ -0,0 +1,61 @@ | ||||
| from typing import Union, Awaitable, Dict | ||||
| 
 | ||||
| from .helpers import AsyncHelper, Helper, RpcEndpoint | ||||
| from .rpc import RpcException | ||||
| 
 | ||||
| 
 | ||||
| class _Caller: | ||||
|     def __init__(self, caller: RpcEndpoint): | ||||
|         self.caller = caller | ||||
| 
 | ||||
|     def get_url(self, js_code): | ||||
|         try: | ||||
|             return self.caller.get_url(js_code=js_code) | ||||
|         except RpcException as e: | ||||
|             if e.rpc_error_code == -32113: | ||||
|                 endpoints = self.caller.refresh_endpoints() | ||||
|                 self.caller.set_endpoints(endpoints) | ||||
|                 return self.get_url(js_code) | ||||
|             raise e | ||||
| 
 | ||||
|     async def async_get_url(self, js_code): | ||||
|         try: | ||||
|             return await self.caller.get_url(js_code=js_code) | ||||
|         except RpcException as e: | ||||
|             if e.rpc_error_code == -32113: | ||||
|                 endpoints = await self.caller.refresh_endpoints() | ||||
|                 self.caller.set_endpoints(endpoints) | ||||
|                 return self.get_url(js_code) | ||||
|             raise e | ||||
| 
 | ||||
| 
 | ||||
| class _ACaller: | ||||
|     def __init__(self, caller: RpcEndpoint): | ||||
|         self.caller = caller | ||||
| 
 | ||||
|     async def get_url(self, js_code): | ||||
|         try: | ||||
|             return await self.caller.get_url(js_code=js_code) | ||||
|         except RpcException as e: | ||||
|             if e.rpc_error_code == -32113: | ||||
|                 endpoints = await self.caller.refresh_endpoints() | ||||
|                 self.caller.set_endpoints(endpoints) | ||||
|                 return self.get_url(js_code) | ||||
|             raise e | ||||
| 
 | ||||
| 
 | ||||
| class ZXGKCaller: | ||||
|     callers: Dict[str, Union[_Caller, _ACaller]] = {} | ||||
| 
 | ||||
|     def __init__(self, uri, async_: bool): | ||||
|         self.async_ = async_ | ||||
|         self.helper = AsyncHelper(uri) if async_ else Helper(uri) | ||||
| 
 | ||||
|     def get_endpoint_caller(self, target) -> Union[RpcEndpoint, Awaitable[RpcEndpoint]]: | ||||
|         return self.helper.enter(target) | ||||
| 
 | ||||
|     def set_endpoint_caller(self, name: str, caller: RpcEndpoint): | ||||
|         self.callers[name] = _ACaller(caller) if self.async_ else _Caller(caller) | ||||
| 
 | ||||
|     def __getattr__(self, item): | ||||
|         return self.callers[item] | ||||
					Loading…
					
					
				
		Reference in New Issue