mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
194 lines
7.0 KiB
Python
194 lines
7.0 KiB
Python
import asyncio
|
|
import datetime
|
|
import json
|
|
import os
|
|
from os.path import expanduser
|
|
import subprocess
|
|
import sys
|
|
from typing import Callable, List, Optional, Union
|
|
import time
|
|
import websockets
|
|
|
|
from command import Command, ServerInfo, SubscriptionCommand
|
|
from common import eprint
|
|
from config_file import ConfigFile
|
|
|
|
|
|
class RippleClient:
|
|
'''Client to send commands to the rippled server'''
|
|
def __init__(self,
|
|
*,
|
|
config: ConfigFile,
|
|
exe: str,
|
|
command_log: Optional[str] = None):
|
|
self.config = config
|
|
self.exe = exe
|
|
self.command_log = command_log
|
|
section = config.port_ws_admin_local
|
|
self.websocket_uri = f'{section.protocol}://{section.ip}:{section.port}'
|
|
self.subscription_websockets = []
|
|
self.tasks = []
|
|
self.pid = None
|
|
if command_log:
|
|
with open(self.command_log, 'w') as f:
|
|
f.write(f'# Start \n')
|
|
|
|
@property
|
|
def config_file_name(self):
|
|
return self.config.get_file_name()
|
|
|
|
def shutdown(self):
|
|
try:
|
|
group = asyncio.gather(*self.tasks, return_exceptions=True)
|
|
group.cancel()
|
|
asyncio.get_event_loop().run_until_complete(group)
|
|
for ws in self.subscription_websockets:
|
|
asyncio.get_event_loop().run_until_complete(ws.close())
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
def set_pid(self, pid: int):
|
|
self.pid = pid
|
|
|
|
def get_pid(self) -> Optional[int]:
|
|
return self.pid
|
|
|
|
def get_config(self) -> ConfigFile:
|
|
return self.config
|
|
|
|
# Get a dict of the server_state, validated_ledger_seq, and complete_ledgers
|
|
def get_brief_server_info(self) -> dict:
|
|
ret = {
|
|
'server_state': 'NA',
|
|
'ledger_seq': 'NA',
|
|
'complete_ledgers': 'NA'
|
|
}
|
|
if not self.pid or self.pid == -1:
|
|
return ret
|
|
r = self.send_command(ServerInfo())
|
|
if 'info' not in r:
|
|
return ret
|
|
r = r['info']
|
|
for f in ['server_state', 'complete_ledgers']:
|
|
if f in r:
|
|
ret[f] = r[f]
|
|
if 'validated_ledger' in r:
|
|
ret['ledger_seq'] = r['validated_ledger']['seq']
|
|
return ret
|
|
|
|
def _write_command_log_command(self, cmd: str, cmd_index: int) -> None:
|
|
if not self.command_log:
|
|
return
|
|
with open(self.command_log, 'a') as f:
|
|
f.write(f'\n\n# command {cmd_index}\n')
|
|
f.write(f'{cmd}')
|
|
|
|
def _write_command_log_result(self, result: str, cmd_index: int) -> None:
|
|
if not self.command_log:
|
|
return
|
|
with open(self.command_log, 'a') as f:
|
|
f.write(f'\n\n# result {cmd_index}\n')
|
|
f.write(f'{result}')
|
|
|
|
def _send_command_line_command(self, cmd_id: int, *args) -> dict:
|
|
'''Send the command to the rippled server using the command line interface'''
|
|
to_run = [self.exe, '-q', '--conf', self.config_file_name, '--']
|
|
to_run.extend(args)
|
|
self._write_command_log_command(to_run, cmd_id)
|
|
max_retries = 4
|
|
for retry_count in range(0, max_retries + 1):
|
|
try:
|
|
r = subprocess.check_output(to_run)
|
|
self._write_command_log_result(r, cmd_id)
|
|
return json.loads(r.decode('utf-8'))['result']
|
|
except Exception as e:
|
|
if retry_count == max_retries:
|
|
raise
|
|
eprint(
|
|
f'Got exception: {str(e)}\nretrying..{retry_count+1} of {max_retries}'
|
|
)
|
|
time.sleep(1) # give process time to startup
|
|
|
|
async def _send_websock_command(
|
|
self,
|
|
cmd: Command,
|
|
conn: Optional[websockets.client.Connect] = None) -> dict:
|
|
assert self.websocket_uri
|
|
if conn is None:
|
|
async with websockets.connect(self.websocket_uri) as ws:
|
|
return await self._send_websock_command(cmd, ws)
|
|
|
|
to_send = json.dumps(cmd.get_websocket_dict())
|
|
self._write_command_log_command(to_send, cmd.cmd_id)
|
|
await conn.send(to_send)
|
|
r = await conn.recv()
|
|
self._write_command_log_result(r, cmd.cmd_id)
|
|
j = json.loads(r)
|
|
if not 'result' in j:
|
|
eprint(
|
|
f'Error sending websocket command: {json.dumps(cmd.get_websocket_dict(), indent=1)}'
|
|
)
|
|
eprint(f'Result: {json.dumps(j, indent=1)}')
|
|
raise ValueError('Error sending websocket command')
|
|
return j['result']
|
|
|
|
def send_command(self, cmd: Command) -> dict:
|
|
'''Send the command to the rippled server'''
|
|
if self.websocket_uri:
|
|
return asyncio.get_event_loop().run_until_complete(
|
|
self._send_websock_command(cmd))
|
|
return self._send_command_line_command(cmd.cmd_id,
|
|
*cmd.get_command_line_list())
|
|
|
|
# Need async version to close ledgers from async functions
|
|
async def async_send_command(self, cmd: Command) -> dict:
|
|
'''Send the command to the rippled server'''
|
|
if self.websocket_uri:
|
|
return await self._send_websock_command(cmd)
|
|
return self._send_command_line_command(cmd.cmd_id,
|
|
*cmd.get_command_line_list())
|
|
|
|
def send_subscribe_command(
|
|
self,
|
|
cmd: SubscriptionCommand,
|
|
callback: Optional[Callable[[dict], None]] = None) -> dict:
|
|
'''Send the command to the rippled server'''
|
|
assert self.websocket_uri
|
|
ws = cmd.websocket
|
|
if ws is None:
|
|
# subscribe
|
|
assert callback
|
|
ws = asyncio.get_event_loop().run_until_complete(
|
|
websockets.connect(self.websocket_uri))
|
|
self.subscription_websockets.append(ws)
|
|
result = asyncio.get_event_loop().run_until_complete(
|
|
self._send_websock_command(cmd, ws))
|
|
if cmd.websocket is not None:
|
|
# unsubscribed. close the websocket
|
|
self.subscription_websockets.remove(cmd.websocket)
|
|
cmd.websocket.close()
|
|
cmd.websocket = None
|
|
else:
|
|
# setup a task to read the websocket
|
|
cmd.websocket = ws # must be set after the _send_websock_command or will unsubscribe
|
|
|
|
async def subscribe_callback(ws: websockets.client.Connect,
|
|
cb: Callable[[dict], None]):
|
|
while True:
|
|
r = await ws.recv()
|
|
d = json.loads(r)
|
|
cb(d)
|
|
|
|
task = asyncio.get_event_loop().create_task(
|
|
subscribe_callback(cmd.websocket, callback))
|
|
self.tasks.append(task)
|
|
return result
|
|
|
|
def stop(self):
|
|
'''Stop the server'''
|
|
return self.send_command(Stop())
|
|
|
|
def set_log_level(self, severity: str, *, partition: Optional[str] = None):
|
|
'''Set the server log level'''
|
|
return self.send_command(LogLevel(severity, parition=parition))
|