You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
141 lines
5.5 KiB
141 lines
5.5 KiB
import asyncio |
|
import gzip |
|
import re |
|
from typing import Any, Dict |
|
|
|
import httpx |
|
import socketio |
|
from socketio import AsyncClient |
|
|
|
from . import globals # pylint: disable=redefined-builtin |
|
from .nicegui import handle_disconnect, handle_event, handle_handshake, handle_javascript_response |
|
|
|
RELAY_HOST = 'https://on-air.nicegui.io/' |
|
|
|
|
|
class Air: |
|
|
|
def __init__(self, token: str) -> None: |
|
self.token = token |
|
self.relay = AsyncClient() |
|
self.client = httpx.AsyncClient(app=globals.app) |
|
self.connecting = False |
|
|
|
@self.relay.on('http') |
|
async def on_http(data: Dict[str, Any]) -> Dict[str, Any]: |
|
headers: Dict[str, Any] = data['headers'] |
|
headers.update({'Accept-Encoding': 'identity', 'X-Forwarded-Prefix': data['prefix']}) |
|
url = 'http://test' + data['path'] |
|
request = self.client.build_request( |
|
data['method'], |
|
url, |
|
params=data['params'], |
|
headers=headers, |
|
content=data['body'], |
|
) |
|
response = await self.client.send(request) |
|
instance_id = data['instance-id'] |
|
content = response.content.replace( |
|
b'const extraHeaders = {};', |
|
(f'const extraHeaders = {{ "fly-force-instance-id" : "{instance_id}" }};').encode(), |
|
) |
|
match = re.search(b'const query = ({.*?})', content) |
|
if match: |
|
new_js_object = match.group(1).decode().rstrip('}') + ", 'fly_instance_id' : '" + instance_id + "'}" |
|
content = content.replace(match.group(0), f'const query = {new_js_object}'.encode()) |
|
response_headers = dict(response.headers) |
|
response_headers['content-encoding'] = 'gzip' |
|
compressed = gzip.compress(content) |
|
response_headers['content-length'] = str(len(compressed)) |
|
return { |
|
'status_code': response.status_code, |
|
'headers': response_headers, |
|
'content': compressed, |
|
} |
|
|
|
@self.relay.on('ready') |
|
def on_ready(data: Dict[str, Any]) -> None: |
|
print(f'NiceGUI is on air at {data["device_url"]}', flush=True) |
|
|
|
@self.relay.on('error') |
|
def on_error(data: Dict[str, Any]) -> None: |
|
print('Error:', data['message'], flush=True) |
|
|
|
@self.relay.on('handshake') |
|
def on_handshake(data: Dict[str, Any]) -> bool: |
|
client_id = data['client_id'] |
|
if client_id not in globals.clients: |
|
return False |
|
client = globals.clients[client_id] |
|
client.environ = data['environ'] |
|
client.on_air = True |
|
handle_handshake(client) |
|
return True |
|
|
|
@self.relay.on('client_disconnect') |
|
def on_disconnect(data: Dict[str, Any]) -> None: |
|
client_id = data['client_id'] |
|
if client_id not in globals.clients: |
|
return |
|
client = globals.clients[client_id] |
|
handle_disconnect(client) |
|
|
|
@self.relay.on('event') |
|
def on_event(data: Dict[str, Any]) -> None: |
|
client_id = data['client_id'] |
|
if client_id not in globals.clients: |
|
return |
|
client = globals.clients[client_id] |
|
if isinstance(data['msg']['args'], dict) and 'socket_id' in data['msg']['args']: |
|
data['msg']['args']['socket_id'] = client_id # HACK: translate socket_id of ui.scene's init event |
|
handle_event(client, data['msg']) |
|
|
|
@self.relay.on('javascript_response') |
|
def on_javascript_response(data: Dict[str, Any]) -> None: |
|
client_id = data['client_id'] |
|
if client_id not in globals.clients: |
|
return |
|
client = globals.clients[client_id] |
|
handle_javascript_response(client, data['msg']) |
|
|
|
@self.relay.on('out_of_time') |
|
async def on_move() -> None: |
|
print('Sorry, you have reached the time limit of this NiceGUI On Air preview.', flush=True) |
|
await self.connect() |
|
|
|
@self.relay.on('reconnect') |
|
async def on_reconnect(_: Dict[str, Any]) -> None: |
|
await self.connect() |
|
|
|
async def connect(self) -> None: |
|
if self.connecting: |
|
return |
|
self.connecting = True |
|
backoff_time = 1 |
|
while True: |
|
try: |
|
if self.relay.connected: |
|
await self.relay.disconnect() |
|
await self.relay.connect( |
|
f'{RELAY_HOST}?device_token={self.token}', |
|
socketio_path='/on_air/socket.io', |
|
transports=['websocket', 'polling'], # favor websocket over polling |
|
) |
|
break |
|
except socketio.exceptions.ConnectionError: |
|
pass |
|
except ValueError: # NOTE this sometimes happens when the internal socketio client is not yet ready |
|
await self.relay.disconnect() |
|
except Exception: |
|
globals.log.exception('Could not connect to NiceGUI On Air server.') |
|
|
|
await asyncio.sleep(backoff_time) |
|
backoff_time = min(backoff_time * 2, 32) |
|
self.connecting = False |
|
|
|
async def disconnect(self) -> None: |
|
await self.relay.disconnect() |
|
|
|
async def emit(self, message_type: str, data: Dict[str, Any], room: str) -> None: |
|
if self.relay.connected: |
|
await self.relay.emit('forward', {'event': message_type, 'data': data, 'room': room})
|
|
|