Skip to content

Commit

Permalink
Migrate to cherrypy wsgi from eventlet (home-assistant#2387)
Browse files Browse the repository at this point in the history
  • Loading branch information
balloob authored Jun 30, 2016
1 parent 7582eb9 commit d1f4901
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 166 deletions.
37 changes: 11 additions & 26 deletions homeassistant/components/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""
import json
import logging
from time import time
import queue

import homeassistant.core as ha
import homeassistant.remote as rem
Expand Down Expand Up @@ -72,19 +72,14 @@ class APIEventStream(HomeAssistantView):

def get(self, request):
"""Provide a streaming interface for the event bus."""
from eventlet.queue import LightQueue, Empty
import eventlet

cur_hub = eventlet.hubs.get_hub()
request.environ['eventlet.minimum_write_chunk_size'] = 0
to_write = LightQueue()
stop_obj = object()
to_write = queue.Queue()

restrict = request.args.get('restrict')
if restrict:
restrict = restrict.split(',')
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]

def thread_forward_events(event):
def forward_events(event):
"""Forward events to the open request."""
if event.event_type == EVENT_TIME_CHANGED:
return
Expand All @@ -99,28 +94,20 @@ def thread_forward_events(event):
else:
data = json.dumps(event, cls=rem.JSONEncoder)

cur_hub.schedule_call_global(0, lambda: to_write.put(data))
to_write.put(data)

def stream():
"""Stream events to response."""
self.hass.bus.listen(MATCH_ALL, thread_forward_events)
self.hass.bus.listen(MATCH_ALL, forward_events)

_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))

last_msg = time()
# Fire off one message right away to have browsers fire open event
to_write.put(STREAM_PING_PAYLOAD)

while True:
try:
# Somehow our queue.get sometimes takes too long to
# be notified of arrival of data. Probably
# because of our spawning on hub in other thread
# hack. Because current goal is to get this out,
# We just timeout every second because it will
# return right away if qsize() > 0.
# So yes, we're basically polling :(
payload = to_write.get(timeout=1)
payload = to_write.get(timeout=STREAM_PING_INTERVAL)

if payload is stop_obj:
break
Expand All @@ -129,15 +116,13 @@ def stream():
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
msg.strip())
yield msg.encode("UTF-8")
last_msg = time()
except Empty:
if time() - last_msg > 50:
to_write.put(STREAM_PING_PAYLOAD)
except queue.Empty:
to_write.put(STREAM_PING_PAYLOAD)
except GeneratorExit:
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
break

self.hass.bus.remove_listener(MATCH_ALL, thread_forward_events)
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
self.hass.bus.remove_listener(MATCH_ALL, forward_events)

return self.Response(stream(), mimetype='text/event-stream')

Expand Down
5 changes: 2 additions & 3 deletions homeassistant/components/camera/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
https://home-assistant.io/components/camera/
"""
import logging
import time

from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
Expand Down Expand Up @@ -81,8 +82,6 @@ def camera_image(self):

def mjpeg_stream(self, response):
"""Generate an HTTP MJPEG stream from camera images."""
import eventlet

def stream():
"""Stream images as mjpeg stream."""
try:
Expand All @@ -99,7 +98,7 @@ def stream():

last_image = img_bytes

eventlet.sleep(0.5)
time.sleep(0.5)
except GeneratorExit:
pass

Expand Down
54 changes: 41 additions & 13 deletions homeassistant/components/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@
import ssl
import voluptuous as vol

import homeassistant.core as ha
import homeassistant.remote as rem
from homeassistant import util
from homeassistant.const import (
SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS)
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
from homeassistant.helpers.entity import split_entity_id
import homeassistant.util.dt as dt_util
import homeassistant.helpers.config_validation as cv

DOMAIN = "http"
REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.10")
REQUIREMENTS = ("cherrypy==6.0.2", "static3==0.7.0", "Werkzeug==0.11.10")

CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host"
Expand Down Expand Up @@ -118,11 +118,17 @@ def setup(hass, config):
cors_origins=cors_origins
)

hass.bus.listen_once(
ha.EVENT_HOMEASSISTANT_START,
lambda event:
threading.Thread(target=server.start, daemon=True,
name='WSGI-server').start())
def start_wsgi_server(event):
"""Start the WSGI server."""
server.start()

hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_wsgi_server)

def stop_wsgi_server(event):
"""Stop the WSGI server."""
server.stop()

hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wsgi_server)

hass.wsgi = server
hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
Expand Down Expand Up @@ -241,6 +247,7 @@ def __init__(self, hass, development, api_password, ssl_certificate,
self.server_port = server_port
self.cors_origins = cors_origins
self.event_forwarder = None
self.server = None

def register_view(self, view):
"""Register a view with the WSGI server.
Expand Down Expand Up @@ -308,17 +315,34 @@ def register_wsgi_app(self, url_root, app):

def start(self):
"""Start the wsgi server."""
from eventlet import wsgi
import eventlet
from cherrypy import wsgiserver
from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter

# pylint: disable=too-few-public-methods,super-init-not-called
class ContextSSLAdapter(BuiltinSSLAdapter):
"""SSL Adapter that takes in an SSL context."""

def __init__(self, context):
self.context = context

# pylint: disable=no-member
self.server = wsgiserver.CherryPyWSGIServer(
(self.server_host, self.server_port), self,
server_name='Home Assistant')

sock = eventlet.listen((self.server_host, self.server_port))
if self.ssl_certificate:
context = ssl.SSLContext(SSL_VERSION)
context.options |= SSL_OPTS
context.set_ciphers(CIPHERS)
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
sock = context.wrap_socket(sock, server_side=True)
wsgi.server(sock, self, log=_LOGGER)
self.server.ssl_adapter = ContextSSLAdapter(context)

threading.Thread(target=self.server.start, daemon=True,
name='WSGI-server').start()

def stop(self):
"""Stop the wsgi server."""
self.server.stop()

def dispatch_request(self, request):
"""Handle incoming request."""
Expand Down Expand Up @@ -365,6 +389,10 @@ def __call__(self, environ, start_response):
"""Handle a request for base app + extra apps."""
from werkzeug.wsgi import DispatcherMiddleware

if not self.hass.is_running:
from werkzeug.exceptions import BadRequest
return BadRequest()(environ, start_response)

app = DispatcherMiddleware(self.base_app, self.extra_apps)
# Strip out any cachebusting MD5 fingerprints
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
Expand Down
24 changes: 24 additions & 0 deletions homeassistant/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@
_LOGGER = logging.getLogger(__name__)


class CoreState(enum.Enum):
"""Represent the current state of Home Assistant."""

not_running = "NOT_RUNNING"
starting = "STARTING"
running = "RUNNING"
stopping = "STOPPING"

def __str__(self):
"""Return the event."""
return self.value


class HomeAssistant(object):
"""Root object of the Home Assistant home automation."""

Expand All @@ -59,14 +72,23 @@ def __init__(self):
self.services = ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus)
self.config = Config()
self.state = CoreState.not_running

@property
def is_running(self):
"""Return if Home Assistant is running."""
return self.state == CoreState.running

def start(self):
"""Start home assistant."""
_LOGGER.info(
"Starting Home Assistant (%d threads)", self.pool.worker_count)
self.state = CoreState.starting

create_timer(self)
self.bus.fire(EVENT_HOMEASSISTANT_START)
self.pool.block_till_done()
self.state = CoreState.running

def block_till_stopped(self):
"""Register service homeassistant/stop and will block until called."""
Expand Down Expand Up @@ -113,8 +135,10 @@ def restart_homeassistant(*args):
def stop(self):
"""Stop Home Assistant and shuts down all threads."""
_LOGGER.info("Stopping")
self.state = CoreState.stopping
self.bus.fire(EVENT_HOMEASSISTANT_STOP)
self.pool.stop()
self.state = CoreState.not_running


class JobPriority(util.OrderedEnum):
Expand Down
15 changes: 11 additions & 4 deletions homeassistant/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import enum
import json
import logging
import time
import threading
import urllib.parse

Expand Down Expand Up @@ -123,6 +124,7 @@ def __init__(self, remote_api, local_api=None):
self.services = ha.ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus, self.remote_api)
self.config = ha.Config()
self.state = ha.CoreState.not_running

self.config.api = local_api

Expand All @@ -134,17 +136,20 @@ def start(self):
raise HomeAssistantError(
'Unable to setup local API to receive events')

self.state = ha.CoreState.starting
ha.create_timer(self)

self.bus.fire(ha.EVENT_HOMEASSISTANT_START,
origin=ha.EventOrigin.remote)

# Give eventlet time to startup
import eventlet
eventlet.sleep(0.1)
# Ensure local HTTP is started
self.pool.block_till_done()
self.state = ha.CoreState.running
time.sleep(0.05)

# Setup that events from remote_api get forwarded to local_api
# Do this after we fire START, otherwise HTTP is not started
# Do this after we are running, otherwise HTTP is not started
# or requests are blocked
if not connect_remote_events(self.remote_api, self.config.api):
raise HomeAssistantError((
'Could not setup event forwarding from api {} to '
Expand All @@ -153,6 +158,7 @@ def start(self):
def stop(self):
"""Stop Home Assistant and shuts down all threads."""
_LOGGER.info("Stopping")
self.state = ha.CoreState.stopping

self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP,
origin=ha.EventOrigin.remote)
Expand All @@ -161,6 +167,7 @@ def stop(self):

# Disconnect master event forwarding
disconnect_remote_events(self.remote_api, self.config.api)
self.state = ha.CoreState.not_running


class EventBus(ha.EventBus):
Expand Down
7 changes: 3 additions & 4 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ pytz>=2016.4
pip>=7.0.0
jinja2>=2.8
voluptuous==0.8.9
eventlet==0.19.0

# homeassistant.components.isy994
PyISY==1.0.6
Expand Down Expand Up @@ -48,6 +47,9 @@ blockchain==1.3.3
# homeassistant.components.notify.aws_sqs
boto3==1.3.1

# homeassistant.components.http
cherrypy==6.0.2

# homeassistant.components.notify.xmpp
dnspython3==1.12.0

Expand All @@ -61,9 +63,6 @@ eliqonline==1.0.12
# homeassistant.components.enocean
enocean==0.31

# homeassistant.components.http
eventlet==0.19.0

# homeassistant.components.thermostat.honeywell
evohomeclient==0.2.5

Expand Down
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
'pip>=7.0.0',
'jinja2>=2.8',
'voluptuous==0.8.9',
'eventlet==0.19.0',
]

setup(
Expand Down
9 changes: 3 additions & 6 deletions tests/components/device_tracker/test_locative.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""The tests the for Locative device tracker platform."""
import time
import unittest
from unittest.mock import patch

import eventlet
import requests

from homeassistant import bootstrap, const
Expand Down Expand Up @@ -32,12 +32,9 @@ def setUpModule(): # pylint: disable=invalid-name
bootstrap.setup_component(hass, http.DOMAIN, {
http.DOMAIN: {
http.CONF_SERVER_PORT: SERVER_PORT
}
},
})

# Set up API
bootstrap.setup_component(hass, 'api')

# Set up device tracker
bootstrap.setup_component(hass, device_tracker.DOMAIN, {
device_tracker.DOMAIN: {
Expand All @@ -46,7 +43,7 @@ def setUpModule(): # pylint: disable=invalid-name
})

hass.start()
eventlet.sleep(0.05)
time.sleep(0.05)


def tearDownModule(): # pylint: disable=invalid-name
Expand Down
Loading

0 comments on commit d1f4901

Please sign in to comment.