From efdb1c1057284d2e7b34d5841f14efecae617fa8 Mon Sep 17 00:00:00 2001 From: fusion44 Date: Thu, 24 Mar 2022 20:17:56 +0100 Subject: [PATCH] feat: add shutdown/reboot SSE events on initiation plus: * add some more documentation * add error handling when the script fails to run * a bit of refactoring refs #70 --- app/repositories/system.py | 25 +++++++++--- app/repositories/system_impl/native_python.py | 7 ++++ app/repositories/system_impl/raspiblitz.py | 22 +++++++--- app/routers/system.py | 40 ++++++++++--------- app/utils.py | 4 ++ 5 files changed, 70 insertions(+), 28 deletions(-) diff --git a/app/repositories/system.py b/app/repositories/system.py index ca3ed30..4ee807f 100644 --- a/app/repositories/system.py +++ b/app/repositories/system.py @@ -15,13 +15,19 @@ if PLATFORM == APIPlatform.RASPIBLITZ: HW_INFO_YIELD_TIME, get_hardware_info_impl, ) - from app.repositories.system_impl.raspiblitz import get_system_info_impl + from app.repositories.system_impl.raspiblitz import ( + get_system_info_impl, + shutdown_impl, + ) elif PLATFORM == APIPlatform.NATIVE_PYTHON: from app.repositories.hardware_impl.native_python import ( HW_INFO_YIELD_TIME, get_hardware_info_impl, ) - from app.repositories.system_impl.native_python import get_system_info_impl + from app.repositories.system_impl.native_python import ( + get_system_info_impl, + shutdown_impl, + ) else: raise RuntimeError(f"Unknown platform {PLATFORM}") @@ -83,12 +89,12 @@ def password_valid(password: str): return re.match("^[a-zA-Z0-9]*$", password) -def name_valid(password : str): +def name_valid(password: str): if len(password) < 3: return False - if password.find(' ') >= 0: + if password.find(" ") >= 0: return False - return re.match('^[\.a-zA-Z0-9-_]*$', password) + return re.match("^[\.a-zA-Z0-9-_]*$", password) async def get_system_info() -> SystemInfo: @@ -104,6 +110,15 @@ async def get_hardware_info() -> map: return await get_hardware_info_impl() +async def shutdown(reboot: bool) -> bool: + if reboot: + await send_sse_message(SSE.SYSTEM_REBOOT_NOTICE, {"reboot": True}) + else: + await send_sse_message(SSE.SYSTEM_SHUTDOWN_NOTICE, {"shutdown": True}) + + return await shutdown_impl(reboot=reboot) + + async def subscribe_hardware_info(request: Request): while True: if await request.is_disconnected(): diff --git a/app/repositories/system_impl/native_python.py b/app/repositories/system_impl/native_python.py index 160ac60..b99d10c 100644 --- a/app/repositories/system_impl/native_python.py +++ b/app/repositories/system_impl/native_python.py @@ -1,3 +1,5 @@ +import logging + from decouple import config from app.constants import API_VERSION @@ -43,3 +45,8 @@ async def get_system_info_impl() -> SystemInfo: ssh_address=ssh_address, chain=lninfo.chains[0].network, ) + + +async def shutdown_impl(reboot: bool) -> bool: + logging.info("Shutdown / reboot not supported in native_python mode.") + return False diff --git a/app/repositories/system_impl/raspiblitz.py b/app/repositories/system_impl/raspiblitz.py index 59d2a8a..aac072e 100644 --- a/app/repositories/system_impl/raspiblitz.py +++ b/app/repositories/system_impl/raspiblitz.py @@ -1,4 +1,5 @@ import asyncio +import logging import os from decouple import config @@ -12,7 +13,7 @@ from app.models.system import ( SystemInfo, ) from app.repositories.lightning import get_ln_info -from app.utils import redis_get +from app.utils import SSE, redis_get, send_sse_message SHELL_SCRIPT_PATH = config("shell_script_path") @@ -43,7 +44,7 @@ async def get_system_info_impl() -> SystemInfo: ) -async def shutdown(reboot: bool): +async def shutdown_impl(reboot: bool) -> bool: params = "" if reboot: params = "reboot" @@ -59,8 +60,19 @@ async def shutdown(reboot: bool): stdout, stderr = await proc.communicate() - print(f"[{cmd!r} exited with {proc.returncode}]") + logging.info(f"[{cmd!r} exited with {proc.returncode}]") if stdout: - print(f"[stdout]\n{stdout.decode()}") + logging.info(f"[stdout]\n{stdout.decode()}") if stderr: - print(f"[stderr]\n{stderr.decode()}") + logging.error(f"[stderr]\n{stderr.decode()}") + + if proc.returncode > 0: + err = stderr.decode() + if reboot: + await send_sse_message(SSE.SYSTEM_REBOOT_ERROR, {"error_message": err}) + else: + await send_sse_message(SSE.SYSTEM_SHUTDOWN_ERROR, {"error_message": err}) + + return False + + return True diff --git a/app/routers/system.py b/app/routers/system.py index 97dd1fb..e1fcf9f 100644 --- a/app/routers/system.py +++ b/app/routers/system.py @@ -8,25 +8,25 @@ from fastapi.params import Depends from app.auth.auth_bearer import JWTBearer from app.auth.auth_handler import sign_jwt from app.external.sse_starlette import EventSourceResponse -from app.models.system import APIPlatform, LoginInput, RawDebugLogData, SystemInfo +from app.models.system import LoginInput, RawDebugLogData, SystemInfo from app.repositories.system import ( HW_INFO_YIELD_TIME, - PLATFORM, call_script, get_debug_logs_raw, get_hardware_info, get_system_info, parse_key_value_text, password_valid, + shutdown, subscribe_hardware_info, ) -from app.repositories.system_impl.raspiblitz import shutdown from app.routers.system_docs import ( get_debug_logs_raw_desc, get_debug_logs_raw_resp_desc, get_debug_logs_raw_summary, get_hw_info_json, ) +from app.utils import SSE _PREFIX = "system" @@ -137,29 +137,33 @@ async def hw_info_sub(request: Request): "/reboot", name=f"{_PREFIX}.reboot", summary="Reboots the system", + description=f"""Attempts to reboot the system. + Will send a `{SSE.SYSTEM_REBOOT_NOTICE}` SSE message immediately to + all connected clients. + """, + response_description=f"""True if successful. False on failure. + A failure will also send an error message with id `{SSE.SYSTEM_REBOOT_ERROR}` + to all connected clients. + """, dependencies=[Depends(JWTBearer())], ) async def reboot_system() -> bool: - if PLATFORM == APIPlatform.RASPIBLITZ: - await shutdown(True) - return True - else: - raise HTTPException( - status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented on native" - ) + return await shutdown(False) @router.post( "/shutdown", name=f"{_PREFIX}.shutdown", summary="Shuts the system down", + description=f"""Attempts to shutdown the system. + Will send a `{SSE.SYSTEM_SHUTDOWN_NOTICE}` SSE message immediately to all + connected clients. + """, + response_description=f"""True if successful. False on failure. + A failure will also send an error message with id {SSE.SYSTEM_SHUTDOWN_ERROR} + to all connected clients. + """, dependencies=[Depends(JWTBearer())], ) -async def shutdown() -> bool: - if PLATFORM == APIPlatform.RASPIBLITZ: - await shutdown(False) - return True - else: - raise HTTPException( - status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented on native" - ) +async def shutdown_path() -> bool: + return await shutdown(False) diff --git a/app/utils.py b/app/utils.py index d1c8803..bdaa0b8 100644 --- a/app/utils.py +++ b/app/utils.py @@ -179,6 +179,10 @@ async def redis_get(key: str) -> str: class SSE: SYSTEM_INFO = "system_info" + SYSTEM_SHUTDOWN_NOTICE = "system_shutdown_initiated" + SYSTEM_SHUTDOWN_ERROR = "system_shutdown_error" + SYSTEM_REBOOT_NOTICE = "system_reboot_initiated" + SYSTEM_REBOOT_ERROR = "system_reboot_error" HARDWARE_INFO = "hardware_info" INSTALLED_APP_STATUS = "installed_app_status"