From 4cc00a9cb1361e65a32f8ccfe8cc6b8a34e85cb9 Mon Sep 17 00:00:00 2001 From: Roland Date: Fri, 18 Aug 2023 11:55:14 -0600 Subject: [PATCH] Delaying reboot/shutdown with asyncio sleep to give API time to shut down. --- app/src/Events.js | 8 +++ app/src/components/admin/Settings.js | 16 +++++- pi-gen/stage2/04-wrolpi/03-run-chroot.sh | 3 +- scripts/install_debian_11.sh | 3 +- scripts/install_raspberrypios.sh | 3 +- wrolpi/admin.py | 65 ++++++++++++------------ wrolpi/errors.py | 6 --- wrolpi/events.py | 8 +++ wrolpi/root_api.py | 8 +-- 9 files changed, 71 insertions(+), 49 deletions(-) diff --git a/app/src/Events.js b/app/src/Events.js index 76268e54..5537e5fc 100644 --- a/app/src/Events.js +++ b/app/src/Events.js @@ -32,6 +32,14 @@ function handleEvents(events) { eventToast('Refresh completed', 'Files have been refreshed.'); } + if (event === 'shutdown') { + eventToast('Shutdown', message, 'warning'); + } + + if (event === 'shutdown_failed') { + eventToast('Shutdown Failed', message, 'error'); + } + if (event === 'downloads_disabled') { eventToast('Downloads Disabled', message, 'info'); } diff --git a/app/src/components/admin/Settings.js b/app/src/components/admin/Settings.js index a8f44726..b760c50c 100644 --- a/app/src/components/admin/Settings.js +++ b/app/src/components/admin/Settings.js @@ -21,6 +21,13 @@ export function ShutdownButton() { time: 5000, type: "error", }) + } else if (response && response['code'] === 'NATIVE_ONLY') { + toast({ + title: 'Shutdown Failed', + description: 'Cannot shutdown while running in Docker', + time: 5000, + type: "error", + }) } else if (response !== null) { toast({ title: 'Shutdown Failed', @@ -58,13 +65,20 @@ export function RestartButton() { const handleRestart = async () => { try { const response = await postRestart(); - if (response && response['code'] === 'RESTART_FAILED') { + if (response && response['code'] === 'SHUTDOWN_FAILED') { toast({ title: 'Restart Failed', description: response['error'], time: 5000, type: "error", }) + } else if (response && response['code'] === 'NATIVE_ONLY') { + toast({ + title: 'Restart Failed', + description: 'Cannot restart while running in Docker', + time: 5000, + type: "error", + }) } else if (response !== null) { toast({ title: 'Restart Failed', diff --git a/pi-gen/stage2/04-wrolpi/03-run-chroot.sh b/pi-gen/stage2/04-wrolpi/03-run-chroot.sh index fdc54939..cede3cb1 100755 --- a/pi-gen/stage2/04-wrolpi/03-run-chroot.sh +++ b/pi-gen/stage2/04-wrolpi/03-run-chroot.sh @@ -67,8 +67,7 @@ cat >/etc/sudoers.d/90-wrolpi <<'EOF' %wrolpi ALL= NOPASSWD:/usr/bin/systemctl stop wrolpi-app.service %wrolpi ALL= NOPASSWD:/usr/bin/systemctl start wrolpi-app.service %wrolpi ALL= NOPASSWD:/opt/wrolpi/scripts/import_map.sh -%wrolpi ALL= NOPASSWD:/usr/sbin/poweroff -%wrolpi ALL= NOPASSWD:/usr/sbin/reboot +%wrolpi ALL= NOPASSWD:/usr/sbin/shutdown EOF chmod 660 /etc/sudoers.d/90-wrolpi diff --git a/scripts/install_debian_11.sh b/scripts/install_debian_11.sh index 62ab4aa1..5b0277a8 100755 --- a/scripts/install_debian_11.sh +++ b/scripts/install_debian_11.sh @@ -89,8 +89,7 @@ cat >/etc/sudoers.d/90-wrolpi <<'EOF' %wrolpi ALL= NOPASSWD:/usr/bin/systemctl stop wrolpi-app.service %wrolpi ALL= NOPASSWD:/usr/bin/systemctl start wrolpi-app.service %wrolpi ALL= NOPASSWD:/opt/wrolpi/scripts/import_map.sh -%wrolpi ALL= NOPASSWD:/usr/sbin/poweroff -%wrolpi ALL= NOPASSWD:/usr/sbin/reboot +%wrolpi ALL= NOPASSWD:/usr/sbin/shutdown EOF chmod 660 /etc/sudoers.d/90-wrolpi # Verify this new file is valid. diff --git a/scripts/install_raspberrypios.sh b/scripts/install_raspberrypios.sh index 7547fbee..0e58497c 100755 --- a/scripts/install_raspberrypios.sh +++ b/scripts/install_raspberrypios.sh @@ -99,8 +99,7 @@ cat >/etc/sudoers.d/90-wrolpi <<'EOF' %wrolpi ALL= NOPASSWD:/usr/bin/systemctl stop wrolpi-app.service %wrolpi ALL= NOPASSWD:/usr/bin/systemctl start wrolpi-app.service %wrolpi ALL= NOPASSWD:/opt/wrolpi/scripts/import_map.sh -%wrolpi ALL= NOPASSWD:/usr/sbin/poweroff -%wrolpi ALL= NOPASSWD:/usr/sbin/reboot +%wrolpi ALL= NOPASSWD:/usr/sbin/shutdown EOF chmod 660 /etc/sudoers.d/90-wrolpi # Verify this new file is valid. diff --git a/wrolpi/admin.py b/wrolpi/admin.py index ebf6397b..794bb25b 100644 --- a/wrolpi/admin.py +++ b/wrolpi/admin.py @@ -3,8 +3,9 @@ import enum import subprocess from wrolpi.cmd import NMCLI_BIN, CPUFREQ_INFO_BIN, SUDO_BIN, CPUFREQ_SET_BIN -from wrolpi.common import logger, WROLPI_CONFIG, get_warn_once -from wrolpi.errors import RestartFailed, ShutdownFailed +from wrolpi.common import logger, WROLPI_CONFIG, get_warn_once, background_task +from wrolpi.errors import ShutdownFailed +from wrolpi.events import Events from wrolpi.vars import DEFAULT_CPU_FREQUENCY, DOCKERIZED logger = logger.getChild(__name__) @@ -53,7 +54,7 @@ def enable_hotspot(): logger.info(f'Hotspot status: {status}') if status == HotspotStatus.connected: - logger.info('Hotspot already connected. Restarting hotspot.') + logger.info('Hotspot already connected. Rebooting hotspot.') disable_hotspot() return enable_hotspot() elif status == HotspotStatus.disconnected: @@ -145,39 +146,39 @@ def throttle_cpu_off() -> bool: logger.error('Could not disable CPU throttle', exc_info=True) -async def restart(): - """Restart the WROLPi. +async def shutdown(reboot: bool = False, delay: int = 10): + """Shutdown or reboot the WROLPi. Schedules an asyncio sleep to delay the shutdown. - @raise RestartFailed: If restart is not possible, or if sudo command failed.""" + @raise ShutdownFailed: If not possible, or if sudo command failed.""" if DOCKERIZED: - raise RestartFailed('Cannot restart because we are dockerized') + raise ShutdownFailed('Cannot Reboot because we are dockerized') - try: - cmd = f'{SUDO_BIN} reboot' - proc = await asyncio.create_subprocess_shell(cmd, stderr=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE) - stdout, stderr = await proc.communicate() - except subprocess.CalledProcessError as e: - raise RestartFailed() from e + reboot = '--reboot' if reboot else '' - if proc.returncode != 0: - raise RestartFailed(f'Got return code {proc.returncode}') + async def _(): + await asyncio.sleep(delay) + try: + cmd = f'{SUDO_BIN} shutdown {reboot}' + proc = await asyncio.create_subprocess_shell(cmd, stderr=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE) + stdout, stderr = await proc.communicate() + except subprocess.CalledProcessError as e: + if reboot: + Events.send_shutdown_failed('Reboot failed') + else: + Events.send_shutdown_failed('Shutdown failed') + raise ShutdownFailed() from e -async def shutdown(): - """Shutdown the WROLPi. + if proc.returncode != 0: + if reboot: + Events.send_shutdown_failed('Reboot failed') + else: + Events.send_shutdown_failed('Shutdown failed') + raise ShutdownFailed(f'Got return code {proc.returncode}') - @raise ShutdownFailed: If shutdown is not possible, or if sudo command failed.""" - if DOCKERIZED: - raise ShutdownFailed('Cannot shutdown because we are dockerized') - - try: - cmd = f'{SUDO_BIN} poweroff' - proc = await asyncio.create_subprocess_shell(cmd, stderr=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE) - stdout, stderr = await proc.communicate() - except subprocess.CalledProcessError as e: - raise ShutdownFailed() from e - - if proc.returncode != 0: - raise ShutdownFailed(f'Got return code {proc.returncode}') + background_task(_()) + if reboot: + Events.send_shutdown(f'Reboot in {delay} seconds') + else: + Events.send_shutdown(f'Shutdown in {delay} seconds') diff --git a/wrolpi/errors.py b/wrolpi/errors.py index 9fbf87e7..3b5a2b31 100644 --- a/wrolpi/errors.py +++ b/wrolpi/errors.py @@ -121,12 +121,6 @@ class HotspotPasswordTooShort(APIError): status = HTTPStatus.BAD_REQUEST -class RestartFailed(APIError): - code = 'RESTART_FAILED' - summary = 'Unable to restart the WROLPi' - status = HTTPStatus.INTERNAL_SERVER_ERROR - - class ShutdownFailed(APIError): code = 'SHUTDOWN_FAILED' summary = 'Unable to shutdown the WROLPi' diff --git a/wrolpi/events.py b/wrolpi/events.py index 3c2451a5..02901f92 100644 --- a/wrolpi/events.py +++ b/wrolpi/events.py @@ -73,6 +73,14 @@ class Events: def send_map_import_failed(message: str = None): send_event('map_import_failed', message, subject='map') + @staticmethod + def send_shutdown(message: str = None): + send_event('shutdown', message, subject='shutdown') + + @staticmethod + def send_shutdown_failed(message: str = None): + send_event('shutdown_failed', message, subject='shutdown') + def log_event(event: str, message: str = None, action: str = None, subject: str = None): log = f'{event=}' diff --git a/wrolpi/root_api.py b/wrolpi/root_api.py index abd3fa50..ee9ae79f 100644 --- a/wrolpi/root_api.py +++ b/wrolpi/root_api.py @@ -25,7 +25,7 @@ from wrolpi.common import logger, get_config, wrol_mode_enabled, Base, get_media from wrolpi.dates import now, strptime from wrolpi.downloader import download_manager from wrolpi.errors import WROLModeEnabled, APIError, HotspotError, InvalidDownload, \ - HotspotPasswordTooShort, ShutdownFailed, RestartFailed + HotspotPasswordTooShort, NativeOnly from wrolpi.events import get_events, Events from wrolpi.files.lib import get_file_statistics, estimate_search from wrolpi.vars import API_HOST, API_PORT, DOCKERIZED, API_DEBUG, API_ACCESS_LOG, API_WORKERS, API_AUTO_RELOAD, \ @@ -504,16 +504,16 @@ async def post_search_estimate(_: Request, body: schema.SearchEstimateRequest): @api_bp.post('/restart') async def post_restart(_: Request): if DOCKERIZED: - raise RestartFailed(f'Cannot restart when running in Docker') + raise NativeOnly(f'Cannot restart when running in Docker') - await admin.restart() + await admin.shutdown(reboot=True) return response.empty(HTTPStatus.NO_CONTENT) @api_bp.post('/shutdown') async def post_shutdown(_: Request): if DOCKERIZED: - raise ShutdownFailed(f'Cannot shutdown when running in Docker') + raise NativeOnly(f'Cannot shutdown when running in Docker') await admin.shutdown() return response.empty(HTTPStatus.NO_CONTENT)