xch-blockchain/chia/daemon/server.py

1387 lines
53 KiB
Python

from __future__ import annotations
import asyncio
import functools
import json
import logging
import os
import signal
import ssl
import subprocess
import sys
import time
import traceback
import uuid
from concurrent.futures import ThreadPoolExecutor
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, TextIO, Tuple
from chia import __version__
from chia.cmds.init_funcs import check_keys, chia_full_version_str, chia_init
from chia.cmds.passphrase_funcs import default_passphrase, using_default_passphrase
from chia.daemon.keychain_server import KeychainServer, keychain_commands
from chia.daemon.windows_signal import kill
from chia.plotters.plotters import get_available_plotters
from chia.plotting.util import add_plot_directory
from chia.server.server import ssl_context_for_root, ssl_context_for_server
from chia.ssl.create_ssl import get_mozilla_ca_crt
from chia.util.beta_metrics import BetaMetricsLogger
from chia.util.chia_logging import initialize_service_logging
from chia.util.config import load_config
from chia.util.errors import KeychainCurrentPassphraseIsInvalid
from chia.util.json_util import dict_to_json_str
from chia.util.keychain import Keychain, passphrase_requirements, supports_os_passphrase_storage
from chia.util.lock import Lockfile, LockfileError
from chia.util.network import WebServer
from chia.util.service_groups import validate_service
from chia.util.setproctitle import setproctitle
from chia.util.ws_message import WsRpcMessage, create_payload, format_response
io_pool_exc = ThreadPoolExecutor()
try:
from aiohttp import ClientSession, WSMsgType, web
from aiohttp.web_ws import WebSocketResponse
except ModuleNotFoundError:
print("Error: Make sure to run . ./activate from the project folder before starting Chia.")
quit()
log = logging.getLogger(__name__)
service_plotter = "chia_plotter"
async def fetch(url: str):
async with ClientSession() as session:
try:
mozilla_root = get_mozilla_ca_crt()
ssl_context = ssl_context_for_root(mozilla_root, log=log)
response = await session.get(url, ssl=ssl_context)
if not response.ok:
log.warning("Response not OK.")
return None
return await response.text()
except Exception as e:
log.error(f"Exception while fetching {url}, exception: {e}")
return None
class PlotState(str, Enum):
SUBMITTED = "SUBMITTED"
RUNNING = "RUNNING"
REMOVING = "REMOVING"
FINISHED = "FINISHED"
class PlotEvent(str, Enum):
LOG_CHANGED = "log_changed"
STATE_CHANGED = "state_changed"
# determine if application is a script file or frozen exe
if getattr(sys, "frozen", False):
name_map = {
"chia": "chia",
"chia_data_layer": "start_data_layer",
"chia_data_layer_http": "start_data_layer_http",
"chia_wallet": "start_wallet",
"chia_full_node": "start_full_node",
"chia_harvester": "start_harvester",
"chia_farmer": "start_farmer",
"chia_introducer": "start_introducer",
"chia_timelord": "start_timelord",
"chia_timelord_launcher": "timelord_launcher",
"chia_full_node_simulator": "start_simulator",
"chia_seeder": "start_seeder",
"chia_crawler": "start_crawler",
}
def executable_for_service(service_name: str) -> str:
application_path = os.path.dirname(sys.executable)
if sys.platform == "win32" or sys.platform == "cygwin":
executable = name_map[service_name]
path = f"{application_path}/{executable}.exe"
return path
else:
path = f"{application_path}/{name_map[service_name]}"
return path
else:
application_path = os.path.dirname(__file__)
def executable_for_service(service_name: str) -> str:
return service_name
async def ping() -> Dict[str, Any]:
response = {"success": True, "value": "pong"}
return response
class WebSocketServer:
def __init__(
self,
root_path: Path,
ca_crt_path: Path,
ca_key_path: Path,
crt_path: Path,
key_path: Path,
shutdown_event: asyncio.Event,
run_check_keys_on_unlock: bool = False,
):
self.root_path = root_path
self.log = log
self.services: Dict = dict()
self.plots_queue: List[Dict] = []
self.connections: Dict[str, List[WebSocketResponse]] = dict() # service_name : [WebSocket]
self.remote_address_map: Dict[WebSocketResponse, str] = dict() # socket: service_name
self.ping_job: Optional[asyncio.Task] = None
self.net_config = load_config(root_path, "config.yaml")
self.self_hostname = self.net_config["self_hostname"]
self.daemon_port = self.net_config["daemon_port"]
self.daemon_max_message_size = self.net_config.get("daemon_max_message_size", 50 * 1000 * 1000)
self.webserver: Optional[WebServer] = None
self.ssl_context = ssl_context_for_server(ca_crt_path, ca_key_path, crt_path, key_path, log=self.log)
self.keychain_server = KeychainServer()
self.run_check_keys_on_unlock = run_check_keys_on_unlock
self.shutdown_event = shutdown_event
async def start(self) -> None:
self.log.info("Starting Daemon Server")
# Note: the minimum_version has been already set to TLSv1_2
# in ssl_context_for_server()
# Daemon is internal connections, so override to TLSv1_3 only
if ssl.HAS_TLSv1_3:
try:
self.ssl_context.minimum_version = ssl.TLSVersion.TLSv1_3
except ValueError:
# in case the attempt above confused the config, set it again (likely not needed but doesn't hurt)
self.ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
if self.ssl_context.minimum_version is not ssl.TLSVersion.TLSv1_3:
self.log.warning(
(
"Deprecation Warning: Your version of SSL (%s) does not support TLS1.3. "
"A future version of Chia will require TLS1.3."
),
ssl.OPENSSL_VERSION,
)
self.webserver = await WebServer.create(
hostname=self.self_hostname,
port=self.daemon_port,
keepalive_timeout=300,
shutdown_timeout=3,
routes=[web.get("/", self.incoming_connection)],
ssl_context=self.ssl_context,
logger=self.log,
)
async def setup_process_global_state(self) -> None:
try:
asyncio.get_running_loop().add_signal_handler(
signal.SIGINT,
functools.partial(self._accept_signal, signal_number=signal.SIGINT),
)
asyncio.get_running_loop().add_signal_handler(
signal.SIGTERM,
functools.partial(self._accept_signal, signal_number=signal.SIGTERM),
)
except NotImplementedError:
self.log.info("Not implemented")
def _accept_signal(self, signal_number: int, stack_frame=None):
asyncio.create_task(self.stop())
def cancel_task_safe(self, task: Optional[asyncio.Task]):
if task is not None:
try:
task.cancel()
except Exception as e:
self.log.error(f"Error while canceling task.{e} {task}")
async def stop(self) -> Dict[str, Any]:
self.cancel_task_safe(self.ping_job)
service_names = list(self.services.keys())
stop_service_jobs = [
asyncio.create_task(kill_service(self.root_path, self.services, s_n)) for s_n in service_names
]
if stop_service_jobs:
await asyncio.wait(stop_service_jobs)
self.services.clear()
asyncio.create_task(self.exit())
log.info(f"Daemon Server stopping, Services stopped: {service_names}")
return {"success": True, "services_stopped": service_names}
async def incoming_connection(self, request):
ws: WebSocketResponse = web.WebSocketResponse(max_msg_size=self.daemon_max_message_size, heartbeat=30)
await ws.prepare(request)
while True:
msg = await ws.receive()
self.log.debug("Received message: %s", msg)
if msg.type == WSMsgType.TEXT:
try:
decoded = json.loads(msg.data)
if "data" not in decoded:
decoded["data"] = {}
response, sockets_to_use = await self.handle_message(ws, decoded)
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Error while handling message: {tb}")
error = {"success": False, "error": f"{e}"}
response = format_response(decoded, error)
sockets_to_use = []
if len(sockets_to_use) > 0:
for socket in sockets_to_use:
try:
await socket.send_str(response)
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Unexpected exception trying to send to websocket: {e} {tb}")
self.remove_connection(socket)
await socket.close()
else:
service_name = "Unknown"
if ws in self.remote_address_map:
service_name = self.remote_address_map[ws]
if msg.type == WSMsgType.CLOSE:
self.log.info(f"ConnectionClosed. Closing websocket with {service_name}")
elif msg.type == WSMsgType.ERROR:
self.log.info(f"Websocket exception. Closing websocket with {service_name}. {ws.exception()}")
self.remove_connection(ws)
await ws.close()
break
def remove_connection(self, websocket: WebSocketResponse):
service_name = None
if websocket in self.remote_address_map:
service_name = self.remote_address_map[websocket]
self.remote_address_map.pop(websocket)
if service_name in self.connections:
after_removal = []
for connection in self.connections[service_name]:
if connection == websocket:
continue
else:
after_removal.append(connection)
self.connections[service_name] = after_removal
async def ping_task(self) -> None:
restart = True
await asyncio.sleep(30)
for remote_address, service_name in self.remote_address_map.items():
if service_name in self.connections:
sockets = self.connections[service_name]
for socket in sockets:
try:
self.log.debug(f"About to ping: {service_name}")
await socket.ping()
except asyncio.CancelledError:
self.log.warning("Ping task received Cancel")
restart = False
break
except Exception:
self.log.exception("Ping error")
self.log.error("Ping failed, connection closed.")
self.remove_connection(socket)
await socket.close()
if restart is True:
self.ping_job = asyncio.create_task(self.ping_task())
async def handle_message(
self, websocket: WebSocketResponse, message: WsRpcMessage
) -> Tuple[Optional[str], List[Any]]:
"""
This function gets called when new message is received via websocket.
"""
command = message["command"]
destination = message["destination"]
if destination != "daemon":
if destination in self.connections:
sockets = self.connections[destination]
return dict_to_json_str(message), sockets
return None, []
data = message["data"]
commands_with_data = [
"start_service",
"start_plotting",
"stop_plotting",
"stop_service",
"is_running",
"register_service",
]
if len(data) == 0 and command in commands_with_data:
response = {"success": False, "error": f'{command} requires "data"'}
# Keychain commands should be handled by KeychainServer
elif command in keychain_commands:
response = await self.keychain_server.handle_command(command, data)
elif command == "ping":
response = await ping()
elif command == "start_service":
response = await self.start_service(data)
elif command == "start_plotting":
response = await self.start_plotting(data)
elif command == "stop_plotting":
response = await self.stop_plotting(data)
elif command == "stop_service":
response = await self.stop_service(data)
elif command == "running_services":
response = await self.running_services(data)
elif command == "is_running":
response = await self.is_running(data)
elif command == "is_keyring_locked":
response = await self.is_keyring_locked()
elif command == "keyring_status":
response = await self.keyring_status()
elif command == "unlock_keyring":
response = await self.unlock_keyring(data)
elif command == "validate_keyring_passphrase":
response = await self.validate_keyring_passphrase(data)
elif command == "set_keyring_passphrase":
response = await self.set_keyring_passphrase(data)
elif command == "remove_keyring_passphrase":
response = await self.remove_keyring_passphrase(data)
elif command == "exit":
response = await self.stop()
elif command == "register_service":
response = await self.register_service(websocket, data)
elif command == "get_status":
response = self.get_status()
elif command == "get_version":
response = self.get_version()
elif command == "get_plotters":
response = await self.get_plotters()
else:
self.log.error(f"UK>> {message}")
response = {"success": False, "error": f"unknown_command {command}"}
full_response = format_response(message, response)
return full_response, [websocket]
async def is_keyring_locked(self) -> Dict[str, Any]:
locked: bool = Keychain.is_keyring_locked()
response: Dict[str, Any] = {"success": True, "is_keyring_locked": locked}
return response
async def keyring_status(self) -> Dict[str, Any]:
can_save_passphrase: bool = supports_os_passphrase_storage()
user_passphrase_is_set: bool = Keychain.has_master_passphrase() and not using_default_passphrase()
locked: bool = Keychain.is_keyring_locked()
can_set_passphrase_hint: bool = True
passphrase_hint: str = Keychain.get_master_passphrase_hint() or ""
requirements: Dict[str, Any] = passphrase_requirements()
response: Dict[str, Any] = {
"success": True,
"is_keyring_locked": locked,
"can_save_passphrase": can_save_passphrase,
"user_passphrase_is_set": user_passphrase_is_set,
"can_set_passphrase_hint": can_set_passphrase_hint,
"passphrase_hint": passphrase_hint,
"passphrase_requirements": requirements,
}
# Help diagnose GUI launch issues
self.log.debug(f"Keyring status: {response}")
return response
async def unlock_keyring(self, request: Dict[str, Any]) -> Dict[str, Any]:
success: bool = False
error: Optional[str] = None
key: Optional[str] = request.get("key", None)
if type(key) is not str:
return {"success": False, "error": "missing key"}
try:
if Keychain.master_passphrase_is_valid(key, force_reload=True):
Keychain.set_cached_master_passphrase(key)
success = True
# Inform the GUI of keyring status changes
self.keyring_status_changed(await self.keyring_status(), "wallet_ui")
else:
error = "bad passphrase"
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Keyring passphrase validation failed: {e} {tb}")
error = "validation exception"
if success and self.run_check_keys_on_unlock:
try:
self.log.info("Running check_keys now that the keyring is unlocked")
check_keys(self.root_path)
self.run_check_keys_on_unlock = False
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"check_keys failed after unlocking keyring: {e} {tb}")
response: Dict[str, Any] = {"success": success, "error": error}
return response
async def validate_keyring_passphrase(self, request: Dict[str, Any]) -> Dict[str, Any]:
success: bool = False
error: Optional[str] = None
key: Optional[str] = request.get("key", None)
if type(key) is not str:
return {"success": False, "error": "missing key"}
try:
success = Keychain.master_passphrase_is_valid(key, force_reload=True)
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Keyring passphrase validation failed: {e} {tb}")
error = "validation exception"
response: Dict[str, Any] = {"success": success, "error": error}
return response
async def set_keyring_passphrase(self, request: Dict[str, Any]) -> Dict[str, Any]:
success: bool = False
error: Optional[str] = None
current_passphrase: Optional[str] = None
new_passphrase: Optional[str] = None
passphrase_hint: Optional[str] = request.get("passphrase_hint", None)
save_passphrase: bool = request.get("save_passphrase", False)
if using_default_passphrase():
current_passphrase = default_passphrase()
if Keychain.has_master_passphrase() and not current_passphrase:
current_passphrase = request.get("current_passphrase", None)
if type(current_passphrase) is not str:
return {"success": False, "error": "missing current_passphrase"}
new_passphrase = request.get("new_passphrase", None)
if type(new_passphrase) is not str:
return {"success": False, "error": "missing new_passphrase"}
if not Keychain.passphrase_meets_requirements(new_passphrase):
return {"success": False, "error": "passphrase doesn't satisfy requirements"}
try:
assert new_passphrase is not None # mypy, I love you
Keychain.set_master_passphrase(
current_passphrase,
new_passphrase,
passphrase_hint=passphrase_hint,
save_passphrase=save_passphrase,
)
except KeychainCurrentPassphraseIsInvalid:
error = "current passphrase is invalid"
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Failed to set keyring passphrase: {e} {tb}")
else:
success = True
# Inform the GUI of keyring status changes
self.keyring_status_changed(await self.keyring_status(), "wallet_ui")
response: Dict[str, Any] = {"success": success, "error": error}
return response
async def remove_keyring_passphrase(self, request: Dict[str, Any]) -> Dict[str, Any]:
success: bool = False
error: Optional[str] = None
current_passphrase: Optional[str] = None
if not Keychain.has_master_passphrase():
return {"success": False, "error": "passphrase not set"}
current_passphrase = request.get("current_passphrase", None)
if type(current_passphrase) is not str:
return {"success": False, "error": "missing current_passphrase"}
try:
Keychain.remove_master_passphrase(current_passphrase)
except KeychainCurrentPassphraseIsInvalid:
error = "current passphrase is invalid"
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Failed to remove keyring passphrase: {e} {tb}")
else:
success = True
# Inform the GUI of keyring status changes
self.keyring_status_changed(await self.keyring_status(), "wallet_ui")
response: Dict[str, Any] = {"success": success, "error": error}
return response
def get_status(self) -> Dict[str, Any]:
response = {"success": True, "genesis_initialized": True}
return response
def get_version(self) -> Dict[str, Any]:
response = {"success": True, "version": __version__}
return response
async def get_plotters(self) -> Dict[str, Any]:
plotters: Dict[str, Any] = get_available_plotters(self.root_path)
response: Dict[str, Any] = {"success": True, "plotters": plotters}
return response
async def _keyring_status_changed(self, keyring_status: Dict[str, Any], destination: str):
"""
Attempt to communicate with the GUI to inform it of any keyring status changes
(e.g. keyring becomes unlocked)
"""
websockets = self.connections.get("wallet_ui", None)
if websockets is None:
return None
if keyring_status is None:
return None
response = create_payload("keyring_status_changed", keyring_status, "daemon", destination)
for websocket in websockets.copy():
try:
await websocket.send_str(response)
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Unexpected exception trying to send to websocket: {e} {tb}")
websockets.remove(websocket)
await websocket.close()
def keyring_status_changed(self, keyring_status: Dict[str, Any], destination: str):
asyncio.create_task(self._keyring_status_changed(keyring_status, destination))
def plot_queue_to_payload(self, plot_queue_item, send_full_log: bool) -> Dict[str, Any]:
error = plot_queue_item.get("error")
has_error = error is not None
item = {
"id": plot_queue_item["id"],
"queue": plot_queue_item["queue"],
"size": plot_queue_item["size"],
"parallel": plot_queue_item["parallel"],
"delay": plot_queue_item["delay"],
"state": plot_queue_item["state"],
"error": str(error) if has_error else None,
"deleted": plot_queue_item["deleted"],
"log_new": plot_queue_item.get("log_new"),
}
if send_full_log:
item["log"] = plot_queue_item.get("log")
return item
def prepare_plot_state_message(self, state: PlotEvent, id):
message = {
"state": state,
"queue": self.extract_plot_queue(id),
}
return message
def extract_plot_queue(self, id=None) -> List[Dict]:
send_full_log = id is None
data = []
for item in self.plots_queue:
if id is None or item["id"] == id:
data.append(self.plot_queue_to_payload(item, send_full_log))
return data
async def _state_changed(self, service: str, message: Dict[str, Any]):
"""If id is None, send the whole state queue"""
if service not in self.connections:
return None
websockets = self.connections[service]
if message is None:
return None
response = create_payload("state_changed", message, service, "wallet_ui")
for websocket in websockets.copy():
try:
await websocket.send_str(response)
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Unexpected exception trying to send to websocket: {e} {tb}")
websockets.remove(websocket)
await websocket.close()
def state_changed(self, service: str, message: Dict[str, Any]):
asyncio.create_task(self._state_changed(service, message))
async def _watch_file_changes(self, config, fp: TextIO, loop: asyncio.AbstractEventLoop):
id: str = config["id"]
plotter: str = config["plotter"]
final_words: List[str] = []
if plotter == "chiapos":
final_words = ["Renamed final file"]
elif plotter == "bladebit":
final_words = ["Finished plotting in"]
elif plotter == "bladebit2":
final_words = ["Finished plotting in"]
elif plotter == "madmax":
temp_dir = config["temp_dir"]
final_dir = config["final_dir"]
if temp_dir == final_dir:
final_words = ["Total plot creation time was"]
else:
# "Renamed final plot" if moving to a final dir on the same volume
# "Copy to <path> finished, took..." if copying to another volume
final_words = ["Renamed final plot", "finished, took"]
while True:
new_data = await loop.run_in_executor(io_pool_exc, fp.readline)
if config["state"] is not PlotState.RUNNING:
return None
if new_data not in (None, ""):
config["log"] = new_data if config["log"] is None else config["log"] + new_data
config["log_new"] = new_data
self.state_changed(service_plotter, self.prepare_plot_state_message(PlotEvent.LOG_CHANGED, id))
if new_data:
for word in final_words:
if word in new_data:
return None
else:
time.sleep(0.5)
async def _track_plotting_progress(self, config, loop: asyncio.AbstractEventLoop):
file_path = config["out_file"]
with open(file_path, "r") as fp:
await self._watch_file_changes(config, fp, loop)
def _common_plotting_command_args(self, request: Any, ignoreCount: bool) -> List[str]:
n = 1 if ignoreCount else request["n"] # Plot count
d = request["d"] # Final directory
r = request["r"] # Threads
f = request.get("f") # Farmer pubkey
p = request.get("p") # Pool pubkey
c = request.get("c") # Pool contract address
command_args: List[str] = []
command_args.append(f"-n{n}")
command_args.append(f"-d{d}")
command_args.append(f"-r{r}")
if f is not None:
command_args.append(f"-f{f}")
if p is not None:
command_args.append(f"-p{p}")
if c is not None:
command_args.append(f"-c{c}")
return command_args
def _chiapos_plotting_command_args(self, request: Any, ignoreCount: bool) -> List[str]:
k = request["k"] # Plot size
t = request["t"] # Temp directory
t2 = request["t2"] # Temp2 directory
b = request["b"] # Buffer size
u = request["u"] # Buckets
a = request.get("a") # Fingerprint
e = request["e"] # Disable bitfield
x = request["x"] # Exclude final directory
override_k = request["overrideK"] # Force plot sizes < k32
command_args: List[str] = []
command_args.append(f"-k{k}")
command_args.append(f"-t{t}")
command_args.append(f"-2{t2}")
command_args.append(f"-b{b}")
command_args.append(f"-u{u}")
if a is not None:
command_args.append(f"-a{a}")
if e is True:
command_args.append("-e")
if x is True:
command_args.append("-x")
if override_k is True:
command_args.append("--override-k")
return command_args
def _bladebit_plotting_command_args(self, request: Any, ignoreCount: bool) -> List[str]:
w = request.get("w", False) # Warm start
m = request.get("m", False) # Disable NUMA
no_cpu_affinity = request.get("no_cpu_affinity", False)
command_args: List[str] = []
if w is True:
command_args.append("-w")
if m is True:
command_args.append("-m")
if no_cpu_affinity is True:
command_args.append("--no-cpu-affinity")
return command_args
def _bladebit2_plotting_command_args(self, request: Any, ignoreCount: bool) -> List[str]:
w = request.get("w", False) # Warm start
m = request.get("m", False) # Disable NUMA
no_cpu_affinity = request.get("no_cpu_affinity", False)
# memo = request["memo"]
t1 = request["t"] # Temp directory
t2 = request.get("t2") # Temp2 directory
u = request.get("u") # Buckets
cache = request.get("cache")
f1_threads = request.get("f1_threads")
fp_threads = request.get("fp_threads")
c_threads = request.get("c_threads")
p2_threads = request.get("p2_threads")
p3_threads = request.get("p3_threads")
alternate = request.get("alternate", False)
no_t1_direct = request.get("no_t1_direct", False)
no_t2_direct = request.get("no_t2_direct", False)
command_args: List[str] = []
if w is True:
command_args.append("-w")
if m is True:
command_args.append("-m")
if no_cpu_affinity is True:
command_args.append("--no-cpu-affinity")
command_args.append(f"-t{t1}")
if t2:
command_args.append(f"-2{t2}")
if u:
command_args.append(f"-u{u}")
if cache:
command_args.append("--cache")
command_args.append(str(cache))
if f1_threads:
command_args.append("--f1-threads")
command_args.append(str(f1_threads))
if fp_threads:
command_args.append("--fp-threads")
command_args.append(str(fp_threads))
if c_threads:
command_args.append("--c-threads")
command_args.append(str(c_threads))
if p2_threads:
command_args.append("--p2-threads")
command_args.append(str(p2_threads))
if p3_threads:
command_args.append("--p3-threads")
command_args.append(str(p3_threads))
if alternate:
command_args.append("--alternate")
if no_t1_direct:
command_args.append("--no-t1-direct")
if no_t2_direct:
command_args.append("--no-t2-direct")
return command_args
def _madmax_plotting_command_args(self, request: Any, ignoreCount: bool, index: int) -> List[str]:
k = request["k"] # Plot size
t = request["t"] # Temp directory
t2 = request["t2"] # Temp2 directory
u = request["u"] # Buckets
v = request["v"] # Buckets for phase 3 & 4
K = request.get("K", 1) # Thread multiplier for phase 2
G = request.get("G", False) # Alternate tmpdir/tmp2dir
command_args: List[str] = []
command_args.append(f"-k{k}")
command_args.append(f"-u{u}")
command_args.append(f"-v{v}")
command_args.append(f"-K{K}")
# Handle madmax's tmptoggle option ourselves when managing GUI plotting
if G is True and t != t2 and index % 2:
# Swap tmp and tmp2
command_args.append(f"-t{t2}")
command_args.append(f"-2{t}")
else:
command_args.append(f"-t{t}")
command_args.append(f"-2{t2}")
return command_args
def _build_plotting_command_args(self, request: Any, ignoreCount: bool, index: int) -> List[str]:
plotter: str = request.get("plotter", "chiapos")
command_args: List[str] = ["chia", "plotters", plotter]
command_args.extend(self._common_plotting_command_args(request, ignoreCount))
if plotter == "chiapos":
command_args.extend(self._chiapos_plotting_command_args(request, ignoreCount))
elif plotter == "madmax":
command_args.extend(self._madmax_plotting_command_args(request, ignoreCount, index))
elif plotter == "bladebit":
command_args.extend(self._bladebit_plotting_command_args(request, ignoreCount))
elif plotter == "bladebit2":
command_args.extend(self._bladebit2_plotting_command_args(request, ignoreCount))
return command_args
def _is_serial_plotting_running(self, queue: str = "default") -> bool:
response = False
for item in self.plots_queue:
if item["queue"] == queue and item["parallel"] is False and item["state"] is PlotState.RUNNING:
response = True
return response
def _get_plots_queue_item(self, id: str):
config = next(item for item in self.plots_queue if item["id"] == id)
return config
def _run_next_serial_plotting(self, loop: asyncio.AbstractEventLoop, queue: str = "default"):
next_plot_id = None
if self._is_serial_plotting_running(queue) is True:
return None
for item in self.plots_queue:
if item["queue"] == queue and item["state"] is PlotState.SUBMITTED and item["parallel"] is False:
next_plot_id = item["id"]
break
if next_plot_id is not None:
loop.create_task(self._start_plotting(next_plot_id, loop, queue))
def _post_process_plotting_job(self, job: Dict[str, Any]):
id: str = job["id"]
final_dir: str = job["final_dir"]
exclude_final_dir: bool = job["exclude_final_dir"]
log.info(f"Post-processing plotter job with ID {id}") # lgtm [py/clear-text-logging-sensitive-data]
if not exclude_final_dir:
try:
add_plot_directory(self.root_path, final_dir)
except ValueError as e:
log.warning(f"_post_process_plotting_job: {e}")
async def _start_plotting(self, id: str, loop: asyncio.AbstractEventLoop, queue: str = "default"):
current_process = None
try:
log.info(f"Starting plotting with ID {id}") # lgtm [py/clear-text-logging-sensitive-data]
config = self._get_plots_queue_item(id)
if config is None:
raise Exception(f"Plot queue config with ID {id} does not exist")
state = config["state"]
if state is not PlotState.SUBMITTED:
raise Exception(f"Plot with ID {id} has no state submitted")
assert id == config["id"]
delay = config["delay"]
await asyncio.sleep(delay)
if config["state"] is not PlotState.SUBMITTED:
return None
service_name = config["service_name"]
command_args = config["command_args"]
# Set the -D/--connect_to_daemon flag to signify that the child should connect
# to the daemon to access the keychain
command_args.append("-D")
self.log.debug(f"command_args before launch_plotter are {command_args}")
self.log.debug(f"self.root_path before launch_plotter is {self.root_path}")
process, pid_path = launch_plotter(self.root_path, service_name, command_args, id)
current_process = process
config["state"] = PlotState.RUNNING
config["out_file"] = plotter_log_path(self.root_path, id).absolute()
config["process"] = process
self.state_changed(service_plotter, self.prepare_plot_state_message(PlotEvent.STATE_CHANGED, id))
if service_name not in self.services:
self.services[service_name] = []
self.services[service_name].append(process)
await self._track_plotting_progress(config, loop)
self.log.debug("finished tracking plotting progress. setting state to FINISHED")
config["state"] = PlotState.FINISHED
self.state_changed(service_plotter, self.prepare_plot_state_message(PlotEvent.STATE_CHANGED, id))
self._post_process_plotting_job(config)
except (subprocess.SubprocessError, IOError):
log.exception(f"problem starting {service_name}") # lgtm [py/clear-text-logging-sensitive-data]
error = Exception("Start plotting failed")
config["state"] = PlotState.FINISHED
config["error"] = error
self.state_changed(service_plotter, self.prepare_plot_state_message(PlotEvent.STATE_CHANGED, id))
raise error
finally:
if current_process is not None:
self.services[service_name].remove(current_process)
current_process.wait() # prevent zombies
self._run_next_serial_plotting(loop, queue)
async def start_plotting(self, request: Dict[str, Any]):
service_name = request["service"]
plotter = request.get("plotter", "chiapos")
delay = int(request.get("delay", 0))
parallel = request.get("parallel", False)
size = request.get("k")
temp_dir = request.get("t")
final_dir = request.get("d")
exclude_final_dir = request.get("x", False)
count = int(request.get("n", 1))
queue = request.get("queue", "default")
if ("p" in request) and ("c" in request):
response = {
"success": False,
"service_name": service_name,
"error": "Choose one of pool_contract_address and pool_public_key",
}
return response
ids: List[str] = []
for k in range(count):
id = str(uuid.uuid4())
ids.append(id)
config = {
"id": id, # lgtm [py/clear-text-logging-sensitive-data]
"size": size,
"queue": queue,
"plotter": plotter,
"service_name": service_name,
"command_args": self._build_plotting_command_args(request, True, k),
"parallel": parallel,
"delay": delay * k if parallel is True else delay,
"state": PlotState.SUBMITTED,
"deleted": False,
"error": None,
"log": None,
"process": None,
"temp_dir": temp_dir,
"final_dir": final_dir,
"exclude_final_dir": exclude_final_dir,
}
self.plots_queue.append(config)
# notify GUI about new plot queue item
self.state_changed(service_plotter, self.prepare_plot_state_message(PlotEvent.STATE_CHANGED, id))
# only the first item can start when user selected serial plotting
can_start_serial_plotting = k == 0 and self._is_serial_plotting_running(queue) is False
if parallel is True or can_start_serial_plotting:
log.info(f"Plotting will start in {config['delay']} seconds")
# TODO: loop gets passed down a lot, review for potential removal
loop = asyncio.get_running_loop()
loop.create_task(self._start_plotting(id, loop, queue))
else:
log.info("Plotting will start automatically when previous plotting finish")
response = {
"success": True,
"ids": ids,
"service_name": service_name,
}
return response
async def stop_plotting(self, request: Dict[str, Any]) -> Dict[str, Any]:
id = request["id"]
config = self._get_plots_queue_item(id)
if config is None:
return {"success": False}
id = config["id"]
state = config["state"]
process = config["process"]
queue = config["queue"]
if config["state"] is PlotState.REMOVING:
return {"success": False}
try:
run_next = False
if process is not None and state == PlotState.RUNNING:
run_next = True
config["state"] = PlotState.REMOVING
self.state_changed(service_plotter, self.prepare_plot_state_message(PlotEvent.STATE_CHANGED, id))
await kill_process(process, self.root_path, service_plotter, id)
config["state"] = PlotState.FINISHED
config["deleted"] = True
self.state_changed(service_plotter, self.prepare_plot_state_message(PlotEvent.STATE_CHANGED, id))
self.plots_queue.remove(config)
if run_next:
# TODO: review to see if we can remove this
loop = asyncio.get_running_loop()
self._run_next_serial_plotting(loop, queue)
return {"success": True}
except Exception as e:
log.error(f"Error during killing the plot process: {e}")
config["state"] = PlotState.FINISHED
config["error"] = str(e)
self.state_changed(service_plotter, self.prepare_plot_state_message(PlotEvent.STATE_CHANGED, id))
return {"success": False}
async def start_service(self, request: Dict[str, Any]):
service_command = request["service"]
error = None
success = False
testing = False
already_running = False
if "testing" in request:
testing = request["testing"]
if not validate_service(service_command):
error = "unknown service"
if service_command in self.services:
service = self.services[service_command]
r = service is not None and service.poll() is None
if r is False:
self.services.pop(service_command)
error = None
else:
self.log.info(f"Service {service_command} already running")
already_running = True
elif len(self.connections.get(service_command, [])) > 0:
# If the service was started manually (not launched by the daemon), we should
# have a connection to it.
self.log.info(f"Service {service_command} already registered")
already_running = True
if already_running:
success = True
elif error is None:
try:
exe_command = service_command
if testing is True:
exe_command = f"{service_command} --testing=true"
process, pid_path = launch_service(self.root_path, exe_command)
self.services[service_command] = process
success = True
except (subprocess.SubprocessError, IOError):
log.exception(f"problem starting {service_command}")
error = "start failed"
response = {"success": success, "service": service_command, "error": error}
return response
async def stop_service(self, request: Dict[str, Any]) -> Dict[str, Any]:
service_name = request["service"]
result = await kill_service(self.root_path, self.services, service_name)
response = {"success": result, "service_name": service_name}
return response
def is_service_running(self, service_name: str) -> bool:
if service_name == service_plotter:
processes = self.services.get(service_name)
is_running = processes is not None and len(processes) > 0
else:
process = self.services.get(service_name)
is_running = process is not None and process.poll() is None
if not is_running:
# Check if we have a connection to the requested service. This might be the
# case if the service was started manually (i.e. not started by the daemon).
service_connections = self.connections.get(service_name)
if service_connections is not None:
is_running = len(service_connections) > 0
return is_running
async def running_services(self, request: Dict[str, Any]) -> Dict[str, Any]:
services = list({*self.services.keys(), *self.connections.keys()})
running_services = [service_name for service_name in services if self.is_service_running(service_name)]
return {"success": True, "running_services": running_services}
async def is_running(self, request: Dict[str, Any]) -> Dict[str, Any]:
service_name = request["service"]
is_running = self.is_service_running(service_name)
return {"success": True, "service_name": service_name, "is_running": is_running}
async def exit(self) -> None:
if self.webserver is not None:
self.webserver.close()
await self.webserver.await_closed()
self.shutdown_event.set()
log.info("chia daemon exiting")
async def register_service(self, websocket: WebSocketResponse, request: Dict[str, Any]) -> Dict[str, Any]:
self.log.info(f"Register service {request}")
service = request.get("service")
if service is None:
self.log.error("Service Name missing from request to 'register_service'")
return {"success": False}
if service not in self.connections:
self.connections[service] = []
self.connections[service].append(websocket)
response: Dict[str, Any] = {"success": True}
if service == service_plotter:
response = {
"success": True,
"service": service,
"queue": self.extract_plot_queue(),
}
else:
self.remote_address_map[websocket] = service
if self.ping_job is None:
self.ping_job = asyncio.create_task(self.ping_task())
self.log.info(f"registered for service {service}")
log.info(f"{response}")
return response
def daemon_launch_lock_path(root_path: Path) -> Path:
"""
A path to a file that is lock when a daemon is launching but not yet started.
This prevents multiple instances from launching.
"""
return service_launch_lock_path(root_path, "daemon")
def service_launch_lock_path(root_path: Path, service: str) -> Path:
"""
A path that is locked when a service is running.
"""
service_name = service.replace(" ", "-").replace("/", "-")
return root_path / "run" / service_name
def pid_path_for_service(root_path: Path, service: str, id: str = "") -> Path:
"""
Generate a path for a PID file for the given service name.
"""
pid_name = service.replace(" ", "-").replace("/", "-")
return root_path / "run" / f"{pid_name}{id}.pid"
def plotter_log_path(root_path: Path, id: str):
return root_path / "plotter" / f"plotter_log_{id}.txt"
def launch_plotter(root_path: Path, service_name: str, service_array: List[str], id: str):
# we need to pass on the possibly altered CHIA_ROOT
os.environ["CHIA_ROOT"] = str(root_path)
service_executable = executable_for_service(service_array[0])
# Swap service name with name of executable
service_array[0] = service_executable
startupinfo = None
creationflags = 0
if sys.platform == "win32" or sys.platform == "cygwin":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
# If the current process group is used, CTRL_C_EVENT will kill the parent and everyone in the group!
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
plotter_path = plotter_log_path(root_path, id)
if plotter_path.parent.exists():
if plotter_path.exists():
plotter_path.unlink()
else:
plotter_path.parent.mkdir(parents=True, exist_ok=True)
outfile = open(plotter_path.resolve(), "w")
log.info(f"Service array: {service_array}") # lgtm [py/clear-text-logging-sensitive-data]
process = subprocess.Popen(
service_array,
shell=False,
stderr=outfile,
stdout=outfile,
startupinfo=startupinfo,
creationflags=creationflags,
)
pid_path = pid_path_for_service(root_path, service_name, id)
try:
pid_path.parent.mkdir(parents=True, exist_ok=True)
with open(pid_path, "w") as f:
f.write(f"{process.pid}\n")
except Exception:
pass
return process, pid_path
def launch_service(root_path: Path, service_command) -> Tuple[subprocess.Popen, Path]:
"""
Launch a child process.
"""
# set up CHIA_ROOT
# invoke correct script
# save away PID
# we need to pass on the possibly altered CHIA_ROOT
os.environ["CHIA_ROOT"] = str(root_path)
# Insert proper e
service_array = service_command.split()
service_executable = executable_for_service(service_array[0])
service_array[0] = service_executable
startupinfo = None
if sys.platform == "win32" or sys.platform == "cygwin":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
log.debug(f"Launching service {service_array} with CHIA_ROOT: {os.environ['CHIA_ROOT']}")
# CREATE_NEW_PROCESS_GROUP allows graceful shutdown on windows, by CTRL_BREAK_EVENT signal
if sys.platform == "win32" or sys.platform == "cygwin":
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
else:
creationflags = 0
environ_copy = os.environ.copy()
process = subprocess.Popen(
service_array, shell=False, startupinfo=startupinfo, creationflags=creationflags, env=environ_copy
)
pid_path = pid_path_for_service(root_path, service_command)
try:
pid_path.parent.mkdir(parents=True, exist_ok=True)
with open(pid_path, "w") as f:
f.write(f"{process.pid}\n")
except Exception:
pass
return process, pid_path
async def kill_process(
process: subprocess.Popen, root_path: Path, service_name: str, id: str, delay_before_kill: int = 15
) -> bool:
pid_path = pid_path_for_service(root_path, service_name, id)
if sys.platform == "win32" or sys.platform == "cygwin":
log.info("sending CTRL_BREAK_EVENT signal to %s", service_name)
# pylint: disable=E1101
kill(process.pid, signal.SIGBREAK)
else:
log.info("sending term signal to %s", service_name)
process.terminate()
count: float = 0
while count < delay_before_kill:
if process.poll() is not None:
break
await asyncio.sleep(0.5)
count += 0.5
else:
process.kill()
log.info("sending kill signal to %s", service_name)
r = process.wait()
log.info("process %s returned %d", service_name, r)
try:
pid_path_killed = pid_path.with_suffix(".pid-killed")
if pid_path_killed.exists():
pid_path_killed.unlink()
os.rename(pid_path, pid_path_killed)
except Exception:
pass
return True
async def kill_service(
root_path: Path, services: Dict[str, subprocess.Popen], service_name: str, delay_before_kill: int = 15
) -> bool:
process = services.get(service_name)
if process is None:
return False
del services[service_name]
result = await kill_process(process, root_path, service_name, "", delay_before_kill)
return result
def is_running(services: Dict[str, subprocess.Popen], service_name: str) -> bool:
process = services.get(service_name)
return process is not None and process.poll() is None
async def async_run_daemon(root_path: Path, wait_for_unlock: bool = False) -> int:
# When wait_for_unlock is true, we want to skip the check_keys() call in chia_init
# since it might be necessary to wait for the GUI to unlock the keyring first.
chia_init(root_path, should_check_keys=(not wait_for_unlock))
config = load_config(root_path, "config.yaml")
setproctitle("chia_daemon")
initialize_service_logging("daemon", config)
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"]
sys.stdout.flush()
json_msg = dict_to_json_str(
{
"message": "cert_path",
"success": True,
"cert": f"{crt_path}",
"key": f"{key_path}",
"ca_crt": f"{ca_crt_path}",
}
)
sys.stdout.write("\n" + json_msg + "\n")
sys.stdout.flush()
try:
with Lockfile.create(daemon_launch_lock_path(root_path), timeout=1):
log.info(f"chia-blockchain version: {chia_full_version_str()}")
beta_metrics: Optional[BetaMetricsLogger] = None
if config.get("beta", {}).get("enabled", False):
beta_metrics = BetaMetricsLogger(root_path)
beta_metrics.start_logging()
shutdown_event = asyncio.Event()
ws_server = WebSocketServer(
root_path,
ca_crt_path,
ca_key_path,
crt_path,
key_path,
shutdown_event,
run_check_keys_on_unlock=wait_for_unlock,
)
await ws_server.setup_process_global_state()
await ws_server.start()
await shutdown_event.wait()
if beta_metrics is not None:
await beta_metrics.stop_logging()
log.info("Daemon WebSocketServer closed")
sys.stdout.close()
return 0
except LockfileError:
print("daemon: already launching")
return 2
def run_daemon(root_path: Path, wait_for_unlock: bool = False) -> int:
result = asyncio.run(async_run_daemon(root_path, wait_for_unlock))
return result
def main() -> int:
from chia.util.default_root import DEFAULT_ROOT_PATH
from chia.util.keychain import Keychain
wait_for_unlock = "--wait-for-unlock" in sys.argv[1:] and Keychain.is_keyring_locked()
return run_daemon(DEFAULT_ROOT_PATH, wait_for_unlock)
if __name__ == "__main__":
main()