From b596fa33d6b03bb858cf6599c1d37cb4a182af33 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 26 Jan 2016 22:39:59 -0500 Subject: [PATCH 1/6] Implemented restart service Implemented an OS and environment safe restart service. This works by running Home Assistant in a child process. If the child process terminates with an exit code > 0, HASS is restarted. SIGTERM and KeyboardInterrupts to the parent process are forwarded to the child process. KeyboardInterrupts will only be forwarded once. The second KeyboardInterrupt will be handled by the parent. --- homeassistant/__main__.py | 89 +++++++++++++++++++++++++++------------ homeassistant/const.py | 2 +- homeassistant/core.py | 12 +++++- 3 files changed, 75 insertions(+), 28 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index e97ed0c6386..7da1e6658e1 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,7 +1,10 @@ """ Starts home assistant. """ from __future__ import print_function +from multiprocessing import Process +import signal import sys +import threading import os import argparse @@ -204,6 +207,61 @@ def uninstall_osx(): print("Home Assistant has been uninstalled.") +def setup_and_run_hass(config_dir, args): + """ Setup HASS and run. Block until stopped. """ + if args.demo_mode: + config = { + 'frontend': {}, + 'demo': {} + } + hass = bootstrap.from_config_dict( + config, config_dir=config_dir, daemon=args.daemon, + verbose=args.verbose, skip_pip=args.skip_pip, + log_rotate_days=args.log_rotate_days) + else: + config_file = ensure_config_file(config_dir) + print('Config directory:', config_dir) + hass = bootstrap.from_config_file( + config_file, daemon=args.daemon, verbose=args.verbose, + skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days) + + if args.open_ui: + def open_browser(event): + """ Open the webinterface in a browser. """ + if hass.config.api is not None: + import webbrowser + webbrowser.open(hass.config.api.base_url) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser) + + hass.start() + sys.exit(int(hass.block_till_stopped())) + + +def run_hass_process(hass_proc): + """ Runs a child hass process. Returns True if it should be restarted. """ + requested_stop = threading.Event() + hass_proc.daemon = True + + def request_stop(): + """ request hass stop """ + requested_stop.set() + hass_proc.terminate() + + try: + signal.signal(signal.SIGTERM, request_stop) + except ValueError: + print('Could not bind to SIGQUIT. Are you running in a thread?') + + hass_proc.start() + try: + hass_proc.join() + except KeyboardInterrupt: + request_stop() + hass_proc.join() + return not requested_stop.isSet() and hass_proc.exitcode > 0 + + def main(): """ Starts Home Assistant. """ validate_python() @@ -233,33 +291,12 @@ def main(): if args.pid_file: write_pid(args.pid_file) - if args.demo_mode: - config = { - 'frontend': {}, - 'demo': {} - } - hass = bootstrap.from_config_dict( - config, config_dir=config_dir, daemon=args.daemon, - verbose=args.verbose, skip_pip=args.skip_pip, - log_rotate_days=args.log_rotate_days) - else: - config_file = ensure_config_file(config_dir) - print('Config directory:', config_dir) - hass = bootstrap.from_config_file( - config_file, daemon=args.daemon, verbose=args.verbose, - skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days) + # Run hass is child process. Restart if necessary. + keep_running = True + while keep_running: + hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args)) + keep_running = run_hass_process(hass_proc) - if args.open_ui: - def open_browser(event): - """ Open the webinterface in a browser. """ - if hass.config.api is not None: - import webbrowser - webbrowser.open(hass.config.api.base_url) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser) - - hass.start() - hass.block_till_stopped() if __name__ == "__main__": main() diff --git a/homeassistant/const.py b/homeassistant/const.py index 143704e1968..2d605a7ee71 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -10,7 +10,6 @@ MATCH_ALL = '*' DEVICE_DEFAULT_NAME = "Unnamed Device" # #### CONFIG #### -CONF_ICON = "icon" CONF_LATITUDE = "latitude" CONF_LONGITUDE = "longitude" CONF_TEMPERATURE_UNIT = "temperature_unit" @@ -124,6 +123,7 @@ ATTR_GPS_ACCURACY = 'gps_accuracy' # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP = "stop" +SERVICE_HOMEASSISTANT_RESTART = "restart" SERVICE_TURN_ON = 'turn_on' SERVICE_TURN_OFF = 'turn_off' diff --git a/homeassistant/core.py b/homeassistant/core.py index 853d09020ce..e6b0a6ec722 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -16,7 +16,8 @@ from collections import namedtuple from homeassistant.const import ( __version__, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, + SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, + EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL, EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED, TEMP_CELCIUS, TEMP_FAHRENHEIT, ATTR_FRIENDLY_NAME) @@ -70,13 +71,21 @@ class HomeAssistant(object): def block_till_stopped(self): """Register service homeassistant/stop and will block until called.""" request_shutdown = threading.Event() + request_restart = threading.Event() def stop_homeassistant(*args): """Stop Home Assistant.""" request_shutdown.set() + def restart_homeassistant(*args): + """Reset Home Assistant.""" + request_restart.set() + request_shutdown.set() + self.services.register( DOMAIN, SERVICE_HOMEASSISTANT_STOP, stop_homeassistant) + self.services.register( + DOMAIN, SERVICE_HOMEASSISTANT_RESTART, restart_homeassistant) if os.name != "nt": try: @@ -92,6 +101,7 @@ class HomeAssistant(object): break self.stop() + return request_restart.isSet() def stop(self): """Stop Home Assistant and shuts down all threads.""" From 519abbbfa2b7bffdcb5c075ac7ec7bed535b2682 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 26 Jan 2016 22:41:57 -0500 Subject: [PATCH 2/6] Better handling of second KeyboardInterrupt Now the second KeyboardInterrupt will be cleanly handled by the parent process. --- homeassistant/__main__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 7da1e6658e1..ac9d5eabd70 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -258,7 +258,10 @@ def run_hass_process(hass_proc): hass_proc.join() except KeyboardInterrupt: request_stop() - hass_proc.join() + try: + hass_proc.join() + except KeyboardInterrupt: + return False return not requested_stop.isSet() and hass_proc.exitcode > 0 From 3534c975f371a91b00cd39d1679dea4890a97276 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 26 Jan 2016 22:46:01 -0500 Subject: [PATCH 3/6] Added missing CONF_ICON constant --- homeassistant/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2d605a7ee71..e59eb0fa64a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -10,6 +10,7 @@ MATCH_ALL = '*' DEVICE_DEFAULT_NAME = "Unnamed Device" # #### CONFIG #### +CONF_ICON = "icon" CONF_LATITUDE = "latitude" CONF_LONGITUDE = "longitude" CONF_TEMPERATURE_UNIT = "temperature_unit" From a41b66bb94ee8be5e8f02b2910a4221f535d7acb Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Fri, 29 Jan 2016 22:02:39 -0500 Subject: [PATCH 4/6] Cleaned up block_till_stop in core.py 1. Removed handling of KeyboardInterrupt. This will no longer happen now that HASS is run in a subprocess. The KeyboardInterrupt will not be sent to the parent process which will send a SIGTERM to the HASS process. 2. Fixed logger warning about not being able to bind to SIGTERM. 3. Removed check for Windows OSs when binding to SIGTERM. This check was originally put in place when HASS was binding to SIGQUIT. SIGTERM exists in NT OSs, so the check is no longer required. 3. Now returning exit code of 100 when requesting a restart. This will allow the parent process to only restart HASS if it is specifically requested and not just on any encountered crash. --- homeassistant/core.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index e6b0a6ec722..eaaecfe87ee 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -48,6 +48,9 @@ _LOGGER = logging.getLogger(__name__) # Temporary to support deprecated methods _MockHA = namedtuple("MockHomeAssistant", ['bus']) +# The exit code to send to request a restart +RESTART_EXIT_CODE = 100 + class HomeAssistant(object): """Root object of the Home Assistant home automation.""" @@ -87,21 +90,17 @@ class HomeAssistant(object): self.services.register( DOMAIN, SERVICE_HOMEASSISTANT_RESTART, restart_homeassistant) - if os.name != "nt": - try: - signal.signal(signal.SIGTERM, stop_homeassistant) - except ValueError: - _LOGGER.warning( - 'Could not bind to SIGQUIT. Are you running in a thread?') + try: + signal.signal(signal.SIGTERM, stop_homeassistant) + except ValueError: + _LOGGER.warning( + 'Could not bind to SIGTERM. Are you running in a thread?') while not request_shutdown.isSet(): - try: - time.sleep(1) - except KeyboardInterrupt: - break + time.sleep(1) self.stop() - return request_restart.isSet() + return RESTART_EXIT_CODE if request_restart.isSet() else 0 def stop(self): """Stop Home Assistant and shuts down all threads.""" From b56369855af490442f81c37ef12e5c7348e4ae88 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Fri, 29 Jan 2016 22:11:11 -0500 Subject: [PATCH 5/6] Cleaned up restart handling in __main__.py 1. Fixed logged message about SIGTERM binding failure. 2. Set to only restart HASS with an exit code of 100. 3. Fixed typo in comment. --- homeassistant/__main__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index ac9d5eabd70..d7cfd0a2f00 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -251,7 +251,7 @@ def run_hass_process(hass_proc): try: signal.signal(signal.SIGTERM, request_stop) except ValueError: - print('Could not bind to SIGQUIT. Are you running in a thread?') + print('Could not bind to SIGTERM. Are you running in a thread?') hass_proc.start() try: @@ -262,7 +262,7 @@ def run_hass_process(hass_proc): hass_proc.join() except KeyboardInterrupt: return False - return not requested_stop.isSet() and hass_proc.exitcode > 0 + return not requested_stop.isSet() and hass_proc.exitcode == 100 def main(): @@ -294,7 +294,7 @@ def main(): if args.pid_file: write_pid(args.pid_file) - # Run hass is child process. Restart if necessary. + # Run hass as child process. Restart if necessary. keep_running = True while keep_running: hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args)) From 106c53abf1414a8bc3dc0e570f8afe4ba0243db8 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Fri, 29 Jan 2016 22:42:39 -0500 Subject: [PATCH 6/6] Revised HASS Core test Changed the HASS Core test that tested KeyboardInterrupt handling to now test SIGTERM handling. KeyboardInterrupts are no longer handled in the HASS application process as they are handled in the HASS parent process. SIGTERM is the proper way to now stop HASS. --- tests/test_core.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index ca935e2d106..4a0096809c8 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -7,6 +7,7 @@ Provides tests to verify that Home Assistant core works. # pylint: disable=protected-access,too-many-public-methods # pylint: disable=too-few-public-methods import os +import signal import unittest from unittest.mock import patch import time @@ -79,15 +80,15 @@ class TestHomeAssistant(unittest.TestCase): self.assertFalse(blocking_thread.is_alive()) - def test_stopping_with_keyboardinterrupt(self): + def test_stopping_with_sigterm(self): calls = [] self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: calls.append(1)) - def raise_keyboardinterrupt(length): - raise KeyboardInterrupt + def send_sigterm(length): + os.kill(os.getpid(), signal.SIGTERM) - with patch('homeassistant.core.time.sleep', raise_keyboardinterrupt): + with patch('homeassistant.core.time.sleep', send_sigterm): self.hass.block_till_stopped() self.assertEqual(1, len(calls))