From b596fa33d6b03bb858cf6599c1d37cb4a182af33 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 26 Jan 2016 22:39:59 -0500 Subject: [PATCH] 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."""