2022-09-30 11:40:22 +03:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2021-03-10 05:27:27 +03:00
|
|
|
import asyncio
|
2020-05-20 10:41:10 +03:00
|
|
|
import json
|
2021-03-08 18:12:14 +03:00
|
|
|
import ssl
|
2021-08-31 20:24:45 +03:00
|
|
|
from contextlib import asynccontextmanager
|
2021-03-08 18:12:14 +03:00
|
|
|
from pathlib import Path
|
2023-07-18 20:26:48 +03:00
|
|
|
from typing import Any, AsyncIterator, Dict, List, Optional
|
2020-05-15 00:27:51 +03:00
|
|
|
|
2022-04-05 20:19:09 +03:00
|
|
|
import aiohttp
|
2020-05-15 00:27:51 +03:00
|
|
|
|
2023-07-18 20:26:48 +03:00
|
|
|
from chia.util.ints import uint32
|
2021-04-04 06:55:26 +03:00
|
|
|
from chia.util.json_util import dict_to_json_str
|
|
|
|
from chia.util.ws_message import WsRpcMessage, create_payload_dict
|
2020-05-15 00:27:51 +03:00
|
|
|
|
|
|
|
|
|
|
|
class DaemonProxy:
|
2022-04-05 20:19:09 +03:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
uri: str,
|
|
|
|
ssl_context: Optional[ssl.SSLContext],
|
2023-01-06 20:55:20 +03:00
|
|
|
heartbeat: int,
|
2022-08-05 04:28:58 +03:00
|
|
|
max_message_size: int = 50 * 1000 * 1000,
|
2022-04-05 20:19:09 +03:00
|
|
|
):
|
2020-05-20 10:41:10 +03:00
|
|
|
self._uri = uri
|
2021-12-17 02:13:41 +03:00
|
|
|
self._request_dict: Dict[str, asyncio.Event] = {}
|
2022-08-05 04:28:58 +03:00
|
|
|
self.response_dict: Dict[str, WsRpcMessage] = {}
|
2021-01-04 22:20:47 +03:00
|
|
|
self.ssl_context = ssl_context
|
2023-01-06 20:55:20 +03:00
|
|
|
self.heartbeat = heartbeat
|
2022-04-05 20:19:09 +03:00
|
|
|
self.client_session: Optional[aiohttp.ClientSession] = None
|
|
|
|
self.websocket: Optional[aiohttp.ClientWebSocketResponse] = None
|
|
|
|
self.max_message_size = max_message_size
|
2020-05-20 10:41:10 +03:00
|
|
|
|
2021-03-08 18:12:14 +03:00
|
|
|
def format_request(self, command: str, data: Dict[str, Any]) -> WsRpcMessage:
|
|
|
|
request = create_payload_dict(command, data, "client", "daemon")
|
2020-05-20 10:41:10 +03:00
|
|
|
return request
|
|
|
|
|
2022-08-05 04:28:58 +03:00
|
|
|
async def start(self) -> None:
|
2022-04-05 20:19:09 +03:00
|
|
|
try:
|
|
|
|
self.client_session = aiohttp.ClientSession()
|
|
|
|
self.websocket = await self.client_session.ws_connect(
|
|
|
|
self._uri,
|
|
|
|
autoclose=True,
|
|
|
|
autoping=True,
|
2023-01-06 20:55:20 +03:00
|
|
|
heartbeat=self.heartbeat,
|
2024-01-03 04:07:31 +03:00
|
|
|
ssl=self.ssl_context,
|
2022-04-05 20:19:09 +03:00
|
|
|
max_msg_size=self.max_message_size,
|
|
|
|
)
|
|
|
|
except Exception:
|
|
|
|
await self.close()
|
|
|
|
raise
|
2020-05-20 10:41:10 +03:00
|
|
|
|
2022-08-05 04:28:58 +03:00
|
|
|
async def listener_task() -> None:
|
2022-08-18 01:51:54 +03:00
|
|
|
try:
|
|
|
|
await self.listener()
|
|
|
|
finally:
|
|
|
|
await self.close()
|
2022-08-05 04:28:58 +03:00
|
|
|
|
|
|
|
asyncio.create_task(listener_task())
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
|
|
|
async def listener(self) -> None:
|
2022-08-18 01:51:54 +03:00
|
|
|
if self.websocket is None:
|
|
|
|
raise TypeError("Websocket is None in listener!")
|
|
|
|
while True:
|
|
|
|
message = await self.websocket.receive()
|
|
|
|
if message.type == aiohttp.WSMsgType.TEXT:
|
|
|
|
decoded: WsRpcMessage = json.loads(message.data)
|
|
|
|
request_id = decoded["request_id"]
|
|
|
|
|
|
|
|
if request_id in self._request_dict:
|
|
|
|
self.response_dict[request_id] = decoded
|
|
|
|
self._request_dict[request_id].set()
|
|
|
|
else:
|
|
|
|
return None
|
2020-05-20 10:41:10 +03:00
|
|
|
|
2021-03-08 18:12:14 +03:00
|
|
|
async def _get(self, request: WsRpcMessage) -> WsRpcMessage:
|
2020-05-20 10:41:10 +03:00
|
|
|
request_id = request["request_id"]
|
2020-05-26 22:37:27 +03:00
|
|
|
self._request_dict[request_id] = asyncio.Event()
|
2020-05-20 10:41:10 +03:00
|
|
|
string = dict_to_json_str(request)
|
2022-04-20 21:46:51 +03:00
|
|
|
if self.websocket is None or self.websocket.closed:
|
2022-04-05 20:19:09 +03:00
|
|
|
raise Exception("Websocket is not connected")
|
|
|
|
asyncio.create_task(self.websocket.send_str(string))
|
2022-08-18 01:51:54 +03:00
|
|
|
try:
|
|
|
|
await asyncio.wait_for(self._request_dict[request_id].wait(), timeout=30)
|
|
|
|
self._request_dict.pop(request_id)
|
2022-08-05 04:28:58 +03:00
|
|
|
response: WsRpcMessage = self.response_dict[request_id]
|
2020-05-20 10:41:10 +03:00
|
|
|
self.response_dict.pop(request_id)
|
2022-08-18 01:51:54 +03:00
|
|
|
return response
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
self._request_dict.pop(request_id)
|
|
|
|
raise Exception(f"No response from daemon for request_id: {request_id}")
|
2020-05-15 00:27:51 +03:00
|
|
|
|
2021-11-20 09:43:44 +03:00
|
|
|
async def get_version(self) -> WsRpcMessage:
|
|
|
|
data: Dict[str, Any] = {}
|
|
|
|
request = self.format_request("get_version", data)
|
|
|
|
response = await self._get(request)
|
|
|
|
return response
|
|
|
|
|
2021-03-08 18:12:14 +03:00
|
|
|
async def start_service(self, service_name: str) -> WsRpcMessage:
|
2020-05-20 10:41:10 +03:00
|
|
|
data = {"service": service_name}
|
|
|
|
request = self.format_request("start_service", data)
|
|
|
|
response = await self._get(request)
|
|
|
|
return response
|
2020-05-15 00:27:51 +03:00
|
|
|
|
2021-03-08 18:12:14 +03:00
|
|
|
async def stop_service(self, service_name: str, delay_before_kill: int = 15) -> WsRpcMessage:
|
2020-05-20 10:41:10 +03:00
|
|
|
data = {"service": service_name}
|
|
|
|
request = self.format_request("stop_service", data)
|
|
|
|
response = await self._get(request)
|
|
|
|
return response
|
2020-05-15 00:27:51 +03:00
|
|
|
|
2021-03-08 18:12:14 +03:00
|
|
|
async def is_running(self, service_name: str) -> bool:
|
2020-05-20 10:41:10 +03:00
|
|
|
data = {"service": service_name}
|
|
|
|
request = self.format_request("is_running", data)
|
|
|
|
response = await self._get(request)
|
2021-03-08 18:12:14 +03:00
|
|
|
if "is_running" in response["data"]:
|
|
|
|
return bool(response["data"]["is_running"])
|
|
|
|
return False
|
2020-05-15 00:27:51 +03:00
|
|
|
|
2021-08-04 22:46:55 +03:00
|
|
|
async def is_keyring_locked(self) -> bool:
|
|
|
|
data: Dict[str, Any] = {}
|
|
|
|
request = self.format_request("is_keyring_locked", data)
|
|
|
|
response = await self._get(request)
|
|
|
|
if "is_keyring_locked" in response["data"]:
|
|
|
|
return bool(response["data"]["is_keyring_locked"])
|
|
|
|
return False
|
|
|
|
|
|
|
|
async def unlock_keyring(self, passphrase: str) -> WsRpcMessage:
|
|
|
|
data = {"key": passphrase}
|
|
|
|
request = self.format_request("unlock_keyring", data)
|
|
|
|
response = await self._get(request)
|
|
|
|
return response
|
|
|
|
|
2021-03-08 18:12:14 +03:00
|
|
|
async def ping(self) -> WsRpcMessage:
|
|
|
|
request = self.format_request("ping", {})
|
2020-05-20 10:41:10 +03:00
|
|
|
response = await self._get(request)
|
|
|
|
return response
|
2020-05-15 00:27:51 +03:00
|
|
|
|
2021-04-02 20:37:01 +03:00
|
|
|
async def close(self) -> None:
|
2022-04-05 20:19:09 +03:00
|
|
|
if self.websocket is not None:
|
|
|
|
await self.websocket.close()
|
|
|
|
if self.client_session is not None:
|
|
|
|
await self.client_session.close()
|
2020-06-17 02:46:51 +03:00
|
|
|
|
2021-03-08 18:12:14 +03:00
|
|
|
async def exit(self) -> WsRpcMessage:
|
2020-05-20 10:41:10 +03:00
|
|
|
request = self.format_request("exit", {})
|
|
|
|
return await self._get(request)
|
2020-05-15 00:27:51 +03:00
|
|
|
|
2023-07-18 20:26:48 +03:00
|
|
|
async def get_keys_for_plotting(self, fingerprints: Optional[List[uint32]] = None) -> WsRpcMessage:
|
|
|
|
data = {"fingerprints": fingerprints} if fingerprints else {}
|
|
|
|
request = self.format_request("get_keys_for_plotting", data)
|
|
|
|
response = await self._get(request)
|
|
|
|
return response
|
|
|
|
|
2020-05-15 00:27:51 +03:00
|
|
|
|
2022-04-05 20:19:09 +03:00
|
|
|
async def connect_to_daemon(
|
2023-01-06 20:55:20 +03:00
|
|
|
self_hostname: str, daemon_port: int, max_message_size: int, ssl_context: ssl.SSLContext, heartbeat: int
|
2022-04-05 20:19:09 +03:00
|
|
|
) -> DaemonProxy:
|
2020-05-15 00:27:51 +03:00
|
|
|
"""
|
|
|
|
Connect to the local daemon.
|
|
|
|
"""
|
2020-05-20 10:41:10 +03:00
|
|
|
|
2023-01-06 20:55:20 +03:00
|
|
|
client = DaemonProxy(
|
|
|
|
f"wss://{self_hostname}:{daemon_port}",
|
|
|
|
ssl_context=ssl_context,
|
|
|
|
max_message_size=max_message_size,
|
|
|
|
heartbeat=heartbeat,
|
|
|
|
)
|
2020-05-20 10:41:10 +03:00
|
|
|
await client.start()
|
|
|
|
return client
|
2020-05-15 00:27:51 +03:00
|
|
|
|
|
|
|
|
2022-07-16 05:10:31 +03:00
|
|
|
async def connect_to_daemon_and_validate(
|
|
|
|
root_path: Path, config: Dict[str, Any], quiet: bool = False
|
|
|
|
) -> Optional[DaemonProxy]:
|
2020-05-15 00:27:51 +03:00
|
|
|
"""
|
|
|
|
Connect to the local daemon and do a ping to ensure that something is really
|
|
|
|
there and running.
|
|
|
|
"""
|
2021-08-04 22:46:55 +03:00
|
|
|
from chia.server.server import ssl_context_for_client
|
|
|
|
|
2020-05-15 00:27:51 +03:00
|
|
|
try:
|
2022-07-16 05:10:31 +03:00
|
|
|
daemon_max_message_size = config.get("daemon_max_message_size", 50 * 1000 * 1000)
|
2023-01-06 20:55:20 +03:00
|
|
|
daemon_heartbeat = config.get("daemon_heartbeat", 300)
|
2022-07-16 05:10:31 +03:00
|
|
|
crt_path = root_path / config["daemon_ssl"]["private_crt"]
|
|
|
|
key_path = root_path / config["daemon_ssl"]["private_key"]
|
|
|
|
ca_crt_path = root_path / config["private_ssl_ca"]["crt"]
|
|
|
|
ca_key_path = root_path / config["private_ssl_ca"]["key"]
|
2021-02-04 04:57:52 +03:00
|
|
|
ssl_context = ssl_context_for_client(ca_crt_path, ca_key_path, crt_path, key_path)
|
2022-04-05 20:19:09 +03:00
|
|
|
connection = await connect_to_daemon(
|
2023-01-06 20:55:20 +03:00
|
|
|
config["self_hostname"],
|
|
|
|
config["daemon_port"],
|
|
|
|
max_message_size=daemon_max_message_size,
|
|
|
|
ssl_context=ssl_context,
|
|
|
|
heartbeat=daemon_heartbeat,
|
2022-04-05 20:19:09 +03:00
|
|
|
)
|
2020-05-15 00:27:51 +03:00
|
|
|
r = await connection.ping()
|
2020-05-20 10:41:10 +03:00
|
|
|
|
2021-03-08 18:12:14 +03:00
|
|
|
if "value" in r["data"] and r["data"]["value"] == "pong":
|
2020-05-15 00:27:51 +03:00
|
|
|
return connection
|
2021-01-06 00:32:19 +03:00
|
|
|
except Exception:
|
2021-08-04 22:46:55 +03:00
|
|
|
if not quiet:
|
|
|
|
print("Daemon not started yet")
|
2020-05-29 03:03:51 +03:00
|
|
|
return None
|
2021-03-08 18:12:14 +03:00
|
|
|
return None
|
2021-08-31 20:24:45 +03:00
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
2022-08-05 04:28:58 +03:00
|
|
|
async def acquire_connection_to_daemon(
|
|
|
|
root_path: Path, config: Dict[str, Any], quiet: bool = False
|
|
|
|
) -> AsyncIterator[Optional[DaemonProxy]]:
|
2021-08-31 20:24:45 +03:00
|
|
|
"""
|
|
|
|
Asynchronous context manager which attempts to create a connection to the daemon.
|
|
|
|
The connection object (DaemonProxy) is yielded to the caller. After the caller's
|
|
|
|
block exits scope, execution resumes in this function, wherein the connection is
|
|
|
|
closed.
|
|
|
|
"""
|
|
|
|
|
2023-03-30 04:31:01 +03:00
|
|
|
daemon: Optional[DaemonProxy] = None
|
2021-08-31 20:24:45 +03:00
|
|
|
try:
|
2022-07-16 05:10:31 +03:00
|
|
|
daemon = await connect_to_daemon_and_validate(root_path, config, quiet=quiet)
|
2023-03-30 04:31:01 +03:00
|
|
|
yield daemon # <----
|
2021-08-31 20:24:45 +03:00
|
|
|
except Exception as e:
|
|
|
|
print(f"Exception occurred while communicating with the daemon: {e}")
|
2023-03-30 04:31:01 +03:00
|
|
|
finally:
|
|
|
|
if daemon is not None:
|
|
|
|
await daemon.close()
|