diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index e97ed0c6386..d7cfd0a2f00 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,64 @@ 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 SIGTERM. Are you running in a thread?') + + hass_proc.start() + try: + hass_proc.join() + except KeyboardInterrupt: + request_stop() + try: + hass_proc.join() + except KeyboardInterrupt: + return False + return not requested_stop.isSet() and hass_proc.exitcode == 100 + + def main(): """ Starts Home Assistant. """ validate_python() @@ -233,33 +294,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 as 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 e50d8ddb96d..cd711df44cd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -126,6 +126,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 6f95cedb9a9..936a068480f 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, ATTR_SERVICE_DATA) @@ -47,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.""" @@ -70,28 +74,33 @@ 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: - 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 RESTART_EXIT_CODE if request_restart.isSet() else 0 def stop(self): """Stop Home Assistant and shuts down all threads.""" 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))