2021-05-27 21:13:20 +00:00
|
|
|
# piker: trading gear for hackers
|
|
|
|
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
|
|
|
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
"""
|
|
|
|
ToOlS fOr CoPInG wITh "tHE wEB" protocols.
|
|
|
|
|
|
|
|
"""
|
|
|
|
from contextlib import asynccontextmanager, AsyncExitStack
|
2022-08-25 15:08:19 +00:00
|
|
|
from itertools import count
|
2021-05-27 21:13:20 +00:00
|
|
|
from types import ModuleType
|
2022-08-22 02:01:03 +00:00
|
|
|
from typing import Any, Optional, Callable, AsyncGenerator
|
2021-05-27 21:13:20 +00:00
|
|
|
import json
|
|
|
|
|
|
|
|
import trio
|
|
|
|
import trio_websocket
|
2022-12-10 19:57:50 +00:00
|
|
|
from wsproto.utilities import LocalProtocolError
|
2021-05-27 21:13:20 +00:00
|
|
|
from trio_websocket._impl import (
|
|
|
|
ConnectionClosed,
|
|
|
|
DisconnectionTimeout,
|
|
|
|
ConnectionRejected,
|
|
|
|
HandshakeError,
|
|
|
|
ConnectionTimeout,
|
|
|
|
)
|
|
|
|
|
|
|
|
from ..log import get_logger
|
|
|
|
|
2022-08-25 15:08:19 +00:00
|
|
|
from .types import Struct
|
|
|
|
|
2021-05-27 21:13:20 +00:00
|
|
|
log = get_logger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class NoBsWs:
|
|
|
|
"""Make ``trio_websocket`` sockets stay up no matter the bs.
|
|
|
|
|
|
|
|
"""
|
|
|
|
recon_errors = (
|
|
|
|
ConnectionClosed,
|
|
|
|
DisconnectionTimeout,
|
|
|
|
ConnectionRejected,
|
|
|
|
HandshakeError,
|
|
|
|
ConnectionTimeout,
|
2022-12-10 19:57:50 +00:00
|
|
|
LocalProtocolError,
|
2021-05-27 21:13:20 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
url: str,
|
|
|
|
stack: AsyncExitStack,
|
2022-08-22 02:01:03 +00:00
|
|
|
fixture: Optional[Callable] = None,
|
|
|
|
serializer: ModuleType = json
|
2021-05-27 21:13:20 +00:00
|
|
|
):
|
|
|
|
self.url = url
|
|
|
|
self.fixture = fixture
|
|
|
|
self._stack = stack
|
|
|
|
self._ws: 'WebSocketConnection' = None # noqa
|
|
|
|
|
|
|
|
async def _connect(
|
|
|
|
self,
|
2021-06-01 14:37:36 +00:00
|
|
|
tries: int = 1000,
|
2021-05-27 21:13:20 +00:00
|
|
|
) -> None:
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
await self._stack.aclose()
|
|
|
|
except (DisconnectionTimeout, RuntimeError):
|
2021-06-01 14:37:36 +00:00
|
|
|
await trio.sleep(0.5)
|
2021-05-27 21:13:20 +00:00
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
last_err = None
|
|
|
|
for i in range(tries):
|
|
|
|
try:
|
|
|
|
self._ws = await self._stack.enter_async_context(
|
|
|
|
trio_websocket.open_websocket_url(self.url)
|
|
|
|
)
|
2022-02-10 02:30:39 +00:00
|
|
|
|
2022-08-22 02:01:03 +00:00
|
|
|
if self.fixture is not None:
|
|
|
|
# rerun user code fixture
|
|
|
|
ret = await self._stack.enter_async_context(
|
|
|
|
self.fixture(self)
|
|
|
|
)
|
|
|
|
|
|
|
|
assert ret is None
|
2021-05-27 21:13:20 +00:00
|
|
|
|
|
|
|
log.info(f'Connection success: {self.url}')
|
|
|
|
return self._ws
|
|
|
|
|
|
|
|
except self.recon_errors as err:
|
|
|
|
last_err = err
|
|
|
|
log.error(
|
|
|
|
f'{self} connection bail with '
|
|
|
|
f'{type(err)}...retry attempt {i}'
|
|
|
|
)
|
2021-06-01 14:37:36 +00:00
|
|
|
await trio.sleep(0.5)
|
2021-05-27 21:13:20 +00:00
|
|
|
continue
|
|
|
|
else:
|
|
|
|
log.exception('ws connection fail...')
|
|
|
|
raise last_err
|
|
|
|
|
|
|
|
async def send_msg(
|
|
|
|
self,
|
|
|
|
data: Any,
|
|
|
|
) -> None:
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
return await self._ws.send_message(json.dumps(data))
|
|
|
|
except self.recon_errors:
|
|
|
|
await self._connect()
|
|
|
|
|
|
|
|
async def recv_msg(
|
|
|
|
self,
|
|
|
|
) -> Any:
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
return json.loads(await self._ws.get_message())
|
|
|
|
except self.recon_errors:
|
|
|
|
await self._connect()
|
|
|
|
|
2022-08-24 01:21:27 +00:00
|
|
|
def __aiter__(self):
|
|
|
|
return self
|
|
|
|
|
|
|
|
async def __anext__(self):
|
|
|
|
return await self.recv_msg()
|
|
|
|
|
2021-05-27 21:13:20 +00:00
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
async def open_autorecon_ws(
|
|
|
|
url: str,
|
|
|
|
|
|
|
|
# TODO: proper type annot smh
|
2022-08-22 02:01:03 +00:00
|
|
|
fixture: Optional[Callable] = None,
|
2022-07-03 21:07:35 +00:00
|
|
|
|
2021-10-22 17:02:45 +00:00
|
|
|
) -> AsyncGenerator[tuple[...], NoBsWs]:
|
2021-05-27 21:13:20 +00:00
|
|
|
"""Apparently we can QoS for all sorts of reasons..so catch em.
|
|
|
|
|
|
|
|
"""
|
|
|
|
async with AsyncExitStack() as stack:
|
2022-07-03 21:07:35 +00:00
|
|
|
ws = NoBsWs(url, stack, fixture=fixture)
|
2021-05-27 21:13:20 +00:00
|
|
|
await ws._connect()
|
|
|
|
|
|
|
|
try:
|
|
|
|
yield ws
|
|
|
|
|
|
|
|
finally:
|
|
|
|
await stack.aclose()
|
2022-08-25 15:08:19 +00:00
|
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
JSONRPC response-request style machinery for transparent multiplexing of msgs
|
|
|
|
over a NoBsWs.
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
class JSONRPCResult(Struct):
|
|
|
|
jsonrpc: str = '2.0'
|
|
|
|
id: int
|
|
|
|
result: Optional[dict] = None
|
|
|
|
error: Optional[dict] = None
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
async def open_jsonrpc_session(
|
|
|
|
url: str,
|
|
|
|
start_id: int = 0,
|
2022-10-19 12:59:29 +00:00
|
|
|
response_type: type = JSONRPCResult,
|
|
|
|
request_type: Optional[type] = None,
|
|
|
|
request_hook: Optional[Callable] = None
|
2022-08-25 15:08:19 +00:00
|
|
|
) -> Callable[[str, dict], dict]:
|
|
|
|
|
|
|
|
async with (
|
|
|
|
trio.open_nursery() as n,
|
|
|
|
open_autorecon_ws(url) as ws
|
|
|
|
):
|
2022-08-27 12:12:02 +00:00
|
|
|
rpc_id: Iterable = count(start_id)
|
|
|
|
rpc_results: dict[int, dict] = {}
|
2022-08-25 15:08:19 +00:00
|
|
|
|
|
|
|
async def json_rpc(method: str, params: dict) -> dict:
|
|
|
|
'''
|
|
|
|
perform a json rpc call and wait for the result, raise exception in
|
|
|
|
case of error field present on response
|
|
|
|
'''
|
|
|
|
msg = {
|
|
|
|
'jsonrpc': '2.0',
|
2022-08-27 12:12:02 +00:00
|
|
|
'id': next(rpc_id),
|
2022-08-25 15:08:19 +00:00
|
|
|
'method': method,
|
|
|
|
'params': params
|
|
|
|
}
|
|
|
|
_id = msg['id']
|
|
|
|
|
2022-08-27 12:12:02 +00:00
|
|
|
rpc_results[_id] = {
|
2022-08-25 15:08:19 +00:00
|
|
|
'result': None,
|
|
|
|
'event': trio.Event()
|
|
|
|
}
|
|
|
|
|
|
|
|
await ws.send_msg(msg)
|
|
|
|
|
2022-08-27 12:12:02 +00:00
|
|
|
await rpc_results[_id]['event'].wait()
|
2022-08-25 15:08:19 +00:00
|
|
|
|
2022-08-27 12:12:02 +00:00
|
|
|
ret = rpc_results[_id]['result']
|
2022-08-25 15:08:19 +00:00
|
|
|
|
2022-08-27 12:12:02 +00:00
|
|
|
del rpc_results[_id]
|
2022-08-25 15:08:19 +00:00
|
|
|
|
|
|
|
if ret.error is not None:
|
|
|
|
raise Exception(json.dumps(ret.error, indent=4))
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
2022-08-27 12:12:02 +00:00
|
|
|
async def recv_task():
|
2022-08-25 15:08:19 +00:00
|
|
|
'''
|
|
|
|
receives every ws message and stores it in its corresponding result
|
|
|
|
field, then sets the event to wakeup original sender tasks.
|
2022-10-19 18:38:35 +00:00
|
|
|
also, recieves responses to requests originated from the server side.
|
2022-08-25 15:08:19 +00:00
|
|
|
'''
|
|
|
|
async for msg in ws:
|
2022-10-19 18:38:35 +00:00
|
|
|
match msg:
|
|
|
|
case {
|
|
|
|
'result': _
|
|
|
|
}:
|
|
|
|
msg = response_type(**msg)
|
|
|
|
|
|
|
|
if msg.id not in rpc_results:
|
|
|
|
log.warning(f'Wasn\'t expecting ws msg: {json.dumps(msg, indent=4)}')
|
|
|
|
|
|
|
|
res = rpc_results.setdefault(
|
|
|
|
msg.id,
|
|
|
|
{'result': None, 'event': trio.Event()}
|
|
|
|
)
|
|
|
|
|
|
|
|
res['result'] = msg
|
|
|
|
res['event'].set()
|
|
|
|
|
|
|
|
case {
|
|
|
|
'method': _,
|
|
|
|
'params': _
|
|
|
|
}:
|
|
|
|
|
|
|
|
if request_hook:
|
|
|
|
await request_hook(request_type(**msg))
|
2022-08-25 15:08:19 +00:00
|
|
|
|
|
|
|
|
2022-08-27 12:12:02 +00:00
|
|
|
n.start_soon(recv_task)
|
2022-08-25 15:08:19 +00:00
|
|
|
yield json_rpc
|
|
|
|
n.cancel_scope.cancel()
|