From 7e94537e36c820896ad3f3738cf99c3035b035d0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 21 Nov 2020 12:48:16 +0100 Subject: [PATCH] Soft restart supervisor (#2281) * Add softrestart to supervisor * decouble * adjust logger * make sure it need run * Use job condition * add more job running --- rootfs/etc/services.d/supervisor/finish | 5 ++++- supervisor/__main__.py | 2 +- supervisor/api/__init__.py | 1 + supervisor/api/supervisor.py | 5 +++++ supervisor/core.py | 3 ++- supervisor/jobs/decorator.py | 9 +++++++++ supervisor/misc/tasks.py | 14 +++++++++++++- supervisor/supervisor.py | 8 ++++++++ tests/jobs/test_job_decorator.py | 24 ++++++++++++++++++++++++ 9 files changed, 67 insertions(+), 4 deletions(-) diff --git a/rootfs/etc/services.d/supervisor/finish b/rootfs/etc/services.d/supervisor/finish index 400618b55..437013ef4 100644 --- a/rootfs/etc/services.d/supervisor/finish +++ b/rootfs/etc/services.d/supervisor/finish @@ -1,5 +1,8 @@ -#!/usr/bin/execlineb -S0 +#!/usr/bin/execlineb -S1 # ============================================================================== # Take down the S6 supervision tree when Supervisor fails # ============================================================================== +if { s6-test ${1} -ne 100 } +if { s6-test ${1} -ne 256 } + redirfd -w 2 /dev/null s6-svscanctl -t /var/run/s6/services diff --git a/supervisor/__main__.py b/supervisor/__main__.py index 5e093ccdc..f8d7fbdc9 100644 --- a/supervisor/__main__.py +++ b/supervisor/__main__.py @@ -60,4 +60,4 @@ if __name__ == "__main__": loop.close() _LOGGER.info("Closing Supervisor") - sys.exit(0) + sys.exit(coresys.core.exit_code) diff --git a/supervisor/api/__init__.py b/supervisor/api/__init__.py index 59fbef0e1..1006abd0f 100644 --- a/supervisor/api/__init__.py +++ b/supervisor/api/__init__.py @@ -251,6 +251,7 @@ class RestAPI(CoreSysAttributes): web.get("/supervisor/logs", api_supervisor.logs), web.post("/supervisor/update", api_supervisor.update), web.post("/supervisor/reload", api_supervisor.reload), + web.post("/supervisor/restart", api_supervisor.restart), web.post("/supervisor/options", api_supervisor.options), web.post("/supervisor/repair", api_supervisor.repair), ] diff --git a/supervisor/api/supervisor.py b/supervisor/api/supervisor.py index 065c5bffc..be053b83c 100644 --- a/supervisor/api/supervisor.py +++ b/supervisor/api/supervisor.py @@ -195,6 +195,11 @@ class APISupervisor(CoreSysAttributes): """Try to repair the local setup / overlayfs.""" return asyncio.shield(self.sys_core.repair()) + @api_process + def restart(self, request: web.Request) -> Awaitable[None]: + """Soft restart Supervisor.""" + return asyncio.shield(self.sys_supervisor.restart()) + @api_process_raw(CONTENT_TYPE_BINARY) def logs(self, request: web.Request) -> Awaitable[bytes]: """Return supervisor Docker logs.""" diff --git a/supervisor/core.py b/supervisor/core.py index 7b4443a32..5b7cbb6a8 100644 --- a/supervisor/core.py +++ b/supervisor/core.py @@ -26,6 +26,7 @@ class Core(CoreSysAttributes): """Initialize Supervisor object.""" self.coresys: CoreSys = coresys self._state: Optional[CoreState] = None + self.exit_code: int = 0 @property def state(self) -> CoreState: @@ -257,7 +258,7 @@ class Core(CoreSysAttributes): _LOGGER.warning("Stage 2: Force Shutdown!") self.state = CoreState.CLOSE - _LOGGER.info("Supervisor is down") + _LOGGER.info("Supervisor is down - %d", self.exit_code) self.sys_loop.stop() async def shutdown(self): diff --git a/supervisor/jobs/decorator.py b/supervisor/jobs/decorator.py index b6a5b2970..700d2e1ac 100644 --- a/supervisor/jobs/decorator.py +++ b/supervisor/jobs/decorator.py @@ -20,6 +20,7 @@ class JobCondition(str, Enum): HEALTHY = "healthy" INTERNET_SYSTEM = "internet_system" INTERNET_HOST = "internet_host" + RUNNING = "running" class Job: @@ -83,6 +84,14 @@ class Job: ) return False + if JobCondition.RUNNING in self.conditions: + if self._coresys.core.state != CoreState.RUNNING: + _LOGGER.warning( + "'%s' blocked from execution, system is not running", + self._method.__qualname__, + ) + return False + if JobCondition.FREE_SPACE in self.conditions: free_space = self._coresys.host.info.free_space if free_space < MINIMUM_FREE_SPACE_THRESHOLD: diff --git a/supervisor/misc/tasks.py b/supervisor/misc/tasks.py index 17979e52c..d234665e4 100644 --- a/supervisor/misc/tasks.py +++ b/supervisor/misc/tasks.py @@ -125,6 +125,7 @@ class Tasks(CoreSysAttributes): JobCondition.HEALTHY, JobCondition.FREE_SPACE, JobCondition.INTERNET_HOST, + JobCondition.RUNNING, ] ) async def _update_addons(self): @@ -150,7 +151,13 @@ class Tasks(CoreSysAttributes): except AddonsError: _LOGGER.error("Can't auto update Add-on %s", addon.slug) - @Job(conditions=[JobCondition.FREE_SPACE, JobCondition.INTERNET_HOST]) + @Job( + conditions=[ + JobCondition.FREE_SPACE, + JobCondition.INTERNET_HOST, + JobCondition.RUNNING, + ] + ) async def _update_supervisor(self): """Check and run update of Supervisor Supervisor.""" if not self.sys_supervisor.need_update: @@ -231,6 +238,7 @@ class Tasks(CoreSysAttributes): finally: self._cache[HASS_WATCHDOG_API] = 0 + @Job(conditions=JobCondition.RUNNING) async def _update_cli(self): """Check and run update of cli.""" if not self.sys_plugins.cli.need_update: @@ -241,6 +249,7 @@ class Tasks(CoreSysAttributes): ) await self.sys_plugins.cli.update() + @Job(conditions=JobCondition.RUNNING) async def _update_dns(self): """Check and run update of CoreDNS plugin.""" if not self.sys_plugins.dns.need_update: @@ -252,6 +261,7 @@ class Tasks(CoreSysAttributes): ) await self.sys_plugins.dns.update() + @Job(conditions=JobCondition.RUNNING) async def _update_audio(self): """Check and run update of PulseAudio plugin.""" if not self.sys_plugins.audio.need_update: @@ -263,6 +273,7 @@ class Tasks(CoreSysAttributes): ) await self.sys_plugins.audio.update() + @Job(conditions=JobCondition.RUNNING) async def _update_observer(self): """Check and run update of Observer plugin.""" if not self.sys_plugins.observer.need_update: @@ -274,6 +285,7 @@ class Tasks(CoreSysAttributes): ) await self.sys_plugins.observer.update() + @Job(conditions=JobCondition.RUNNING) async def _update_multicast(self): """Check and run update of multicast.""" if not self.sys_plugins.multicast.need_update: diff --git a/supervisor/supervisor.py b/supervisor/supervisor.py index b8488b089..afb643c24 100644 --- a/supervisor/supervisor.py +++ b/supervisor/supervisor.py @@ -11,6 +11,8 @@ import aiohttp from aiohttp.client_exceptions import ClientError from packaging.version import parse as pkg_parse +from supervisor.jobs.decorator import Job, JobCondition + from .const import SUPERVISOR_VERSION, URL_HASSIO_APPARMOR from .coresys import CoreSys, CoreSysAttributes from .docker.stats import DockerStats @@ -144,6 +146,12 @@ class Supervisor(CoreSysAttributes): await self.update_apparmor() self.sys_create_task(self.sys_core.stop()) + @Job(conditions=[JobCondition.RUNNING]) + async def restart(self) -> None: + """Restart Supervisor soft.""" + self.sys_core.exit_code = 100 + self.sys_create_task(self.sys_core.stop()) + @property def in_progress(self) -> bool: """Return True if a task is in progress.""" diff --git a/tests/jobs/test_job_decorator.py b/tests/jobs/test_job_decorator.py index 26a2f1b5d..df79827f6 100644 --- a/tests/jobs/test_job_decorator.py +++ b/tests/jobs/test_job_decorator.py @@ -181,3 +181,27 @@ async def test_exception_not_handle(coresys: CoreSys): with pytest.raises(JobException): assert await test.execute() + + +async def test_running(coresys: CoreSys): + """Test the running decorator.""" + + class TestClass: + """Test class.""" + + def __init__(self, coresys: CoreSys): + """Initialize the test class.""" + self.coresys = coresys + + @Job(conditions=[JobCondition.RUNNING]) + async def execute(self): + """Execute the class method.""" + return True + + test = TestClass(coresys) + + coresys.core.state = CoreState.RUNNING + assert await test.execute() + + coresys.core.state = CoreState.FREEZE + assert not await test.execute()