mirror of https://github.com/zulip/zulip.git
run-dev: Use tornado for dev proxy server for HTTP requests.
- Use tornado as proxy server for development environment, replacing twisted (this doesn't support websockets). - Upgrade tornado version to 4.4.1 (needs to be coupled to the above since neither change works without the other)
This commit is contained in:
parent
6a8f8c2abf
commit
09e17fbe17
|
@ -21,6 +21,9 @@ SQLAlchemy==1.1.1
|
||||||
# Needed for Tornado >3.2 compatibility
|
# Needed for Tornado >3.2 compatibility
|
||||||
backports-abc==0.4
|
backports-abc==0.4
|
||||||
|
|
||||||
|
# Needed for Tornado 4 compatibility
|
||||||
|
backports.ssl-match-hostname==3.5.0.1
|
||||||
|
|
||||||
# Needed for S3 file uploads
|
# Needed for S3 file uploads
|
||||||
boto==2.42.0
|
boto==2.42.0
|
||||||
|
|
||||||
|
@ -127,7 +130,7 @@ sourcemap==0.1.8
|
||||||
statsd==3.2.1
|
statsd==3.2.1
|
||||||
|
|
||||||
# Tornado used for server->client push system
|
# Tornado used for server->client push system
|
||||||
tornado==3.2.2
|
tornado==4.4.2
|
||||||
|
|
||||||
# Needed for Python static typing
|
# Needed for Python static typing
|
||||||
typing==3.5.2.2
|
typing==3.5.2.2
|
||||||
|
|
222
tools/run-dev.py
222
tools/run-dev.py
|
@ -2,27 +2,23 @@
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import optparse
|
import optparse
|
||||||
import pwd
|
|
||||||
import subprocess
|
|
||||||
import signal
|
|
||||||
import traceback
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import pwd
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
if False: from typing import Any
|
from six.moves.urllib.parse import urlunparse
|
||||||
|
|
||||||
from twisted.internet import reactor
|
from tornado import httpclient
|
||||||
from twisted.web import proxy, server, resource
|
from tornado import httputil
|
||||||
|
from tornado import gen
|
||||||
|
from tornado import web
|
||||||
|
from tornado.ioloop import IOLoop
|
||||||
|
|
||||||
# Monkey-patch twisted.web.http to avoid request.finish exceptions
|
if False: from typing import Any, Callable, Generator, Optional
|
||||||
# https://trac.zulip.net/ticket/1728
|
|
||||||
from twisted.web.http import Request
|
|
||||||
orig_finish = Request.finish
|
|
||||||
def patched_finish(self):
|
|
||||||
# type: (Any) -> None
|
|
||||||
if not self._disconnected:
|
|
||||||
orig_finish(self)
|
|
||||||
Request.finish = patched_finish
|
|
||||||
|
|
||||||
if 'posix' in os.name and os.geteuid() == 0:
|
if 'posix' in os.name and os.geteuid() == 0:
|
||||||
raise RuntimeError("run-dev.py should not be run as root.")
|
raise RuntimeError("run-dev.py should not be run as root.")
|
||||||
|
@ -53,7 +49,7 @@ parser.add_option('--no-clear-memcached',
|
||||||
action='store_false', dest='clear_memcached',
|
action='store_false', dest='clear_memcached',
|
||||||
default=True, help='Do not clear memcached')
|
default=True, help='Do not clear memcached')
|
||||||
|
|
||||||
(options, args) = parser.parse_args()
|
(options, arguments) = parser.parse_args()
|
||||||
|
|
||||||
if options.interface is None:
|
if options.interface is None:
|
||||||
user_id = os.getuid()
|
user_id = os.getuid()
|
||||||
|
@ -113,7 +109,7 @@ cmds = [['./tools/compile-handlebars-templates', 'forever'],
|
||||||
['python', '-u', 'manage.py', 'runtornado'] +
|
['python', '-u', 'manage.py', 'runtornado'] +
|
||||||
manage_args + ['127.0.0.1:%d' % (tornado_port,)],
|
manage_args + ['127.0.0.1:%d' % (tornado_port,)],
|
||||||
['./tools/run-dev-queue-processors'] + manage_args,
|
['./tools/run-dev-queue-processors'] + manage_args,
|
||||||
['env', 'PGHOST=127.0.0.1', # Force password authentication using .pgpass
|
['env', 'PGHOST=127.0.0.1', # Force password authentication using .pgpass
|
||||||
'./puppet/zulip/files/postgresql/process_fts_updates']]
|
'./puppet/zulip/files/postgresql/process_fts_updates']]
|
||||||
if options.test:
|
if options.test:
|
||||||
# Webpack doesn't support 2 copies running on the same system, so
|
# Webpack doesn't support 2 copies running on the same system, so
|
||||||
|
@ -126,38 +122,176 @@ else:
|
||||||
for cmd in cmds:
|
for cmd in cmds:
|
||||||
subprocess.Popen(cmd)
|
subprocess.Popen(cmd)
|
||||||
|
|
||||||
class Resource(resource.Resource):
|
|
||||||
def getChild(self, name, request):
|
|
||||||
# type: (bytes, server.Request) -> resource.Resource
|
|
||||||
|
|
||||||
# Assume an HTTP 1.1 request
|
def transform_url(protocol, path, query, target_port, target_host):
|
||||||
proxy_host = request.requestHeaders.getRawHeaders('Host')
|
# type: (str, str, str, int, str) -> str
|
||||||
request.requestHeaders.setRawHeaders('X-Forwarded-Host', proxy_host)
|
# generate url with target host
|
||||||
if (request.uri.startswith(b'/json/events') or
|
host = ":".join((target_host, str(target_port)))
|
||||||
request.uri.startswith(b'/api/v1/events') or
|
newpath = urlunparse((protocol, host, path, '', query, ''))
|
||||||
request.uri.startswith(b'/sockjs')):
|
return newpath
|
||||||
return proxy.ReverseProxyResource('127.0.0.1', tornado_port, b'/' + name)
|
|
||||||
|
|
||||||
elif (request.uri.startswith(b'/webpack') or
|
|
||||||
request.uri.startswith(b'/socket.io')):
|
|
||||||
return proxy.ReverseProxyResource('127.0.0.1', webpack_port, b'/' + name)
|
|
||||||
|
|
||||||
return proxy.ReverseProxyResource('127.0.0.1', django_port, b'/'+name)
|
|
||||||
|
|
||||||
|
|
||||||
# log which services/ports will be started
|
@gen.engine
|
||||||
print("Starting Zulip services on ports: web proxy: {},".format(proxy_port),
|
def fetch_request(url, callback, **kwargs):
|
||||||
"Django: {}, Tornado: {}".format(django_port, tornado_port), end='')
|
# type: (str, Any, **Any) -> Generator[Callable[..., Any], Any, None]
|
||||||
if options.test:
|
# use large timeouts to handle polling requests
|
||||||
print("") # no webpack for --test
|
req = httpclient.HTTPRequest(url, connect_timeout=240.0, request_timeout=240.0, **kwargs)
|
||||||
|
client = httpclient.AsyncHTTPClient()
|
||||||
|
# wait for response
|
||||||
|
response = yield gen.Task(client.fetch, req)
|
||||||
|
callback(response)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHandler(web.RequestHandler):
|
||||||
|
# target server ip
|
||||||
|
target_host = '127.0.0.1' # type: str
|
||||||
|
# target server port
|
||||||
|
target_port = None # type: int
|
||||||
|
|
||||||
|
def _add_request_headers(self, exclude_lower_headers_list=None):
|
||||||
|
# type: (Optional[List[str]]) -> httputil.HTTPHeaders
|
||||||
|
exclude_lower_headers_list = exclude_lower_headers_list or []
|
||||||
|
headers = httputil.HTTPHeaders()
|
||||||
|
for header, v in self.request.headers.get_all():
|
||||||
|
if header.lower() not in exclude_lower_headers_list:
|
||||||
|
headers.add(header, v)
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
# type: () -> None
|
||||||
|
pass
|
||||||
|
|
||||||
|
def head(self):
|
||||||
|
# type: () -> None
|
||||||
|
pass
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
# type: () -> None
|
||||||
|
pass
|
||||||
|
|
||||||
|
def put(self):
|
||||||
|
# type: () -> None
|
||||||
|
pass
|
||||||
|
|
||||||
|
def patch(self):
|
||||||
|
# type: () -> None
|
||||||
|
pass
|
||||||
|
|
||||||
|
def options(self):
|
||||||
|
# type: () -> None
|
||||||
|
pass
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
# type: () -> None
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_response(self, response):
|
||||||
|
# type: (Any) -> None
|
||||||
|
if response.error and not isinstance(response.error, httpclient.HTTPError):
|
||||||
|
self.set_status(500)
|
||||||
|
self.write('Internal server error:\n' + str(response.error))
|
||||||
|
else:
|
||||||
|
self.set_status(response.code, response.reason)
|
||||||
|
self._headers = httputil.HTTPHeaders() # clear tornado default header
|
||||||
|
|
||||||
|
for header, v in response.headers.get_all():
|
||||||
|
if header != 'Content-Length':
|
||||||
|
# some header appear multiple times, eg 'Set-Cookie'
|
||||||
|
self.add_header(header, v)
|
||||||
|
if response.body:
|
||||||
|
# rewrite Content-Length Header by the response
|
||||||
|
self.set_header('Content-Length', len(response.body))
|
||||||
|
self.write(response.body)
|
||||||
|
self.finish()
|
||||||
|
|
||||||
|
@web.asynchronous
|
||||||
|
def prepare(self):
|
||||||
|
# type: () -> None
|
||||||
|
url = transform_url(
|
||||||
|
self.request.protocol,
|
||||||
|
self.request.path,
|
||||||
|
self.request.query,
|
||||||
|
self.target_port,
|
||||||
|
self.target_host,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
fetch_request(
|
||||||
|
url=url,
|
||||||
|
callback=self.handle_response,
|
||||||
|
method=self.request.method,
|
||||||
|
headers=self._add_request_headers(["upgrade-insecure-requests"]),
|
||||||
|
follow_redirects=False,
|
||||||
|
body=getattr(self.request, 'body'),
|
||||||
|
allow_nonstandard_methods=True
|
||||||
|
)
|
||||||
|
except httpclient.HTTPError as e:
|
||||||
|
if hasattr(e, 'response') and e.response:
|
||||||
|
self.handle_response(e.response)
|
||||||
|
else:
|
||||||
|
self.set_status(500)
|
||||||
|
self.write('Internal server error:\n' + str(e))
|
||||||
|
self.finish()
|
||||||
|
|
||||||
|
|
||||||
|
class WebPackHandler(BaseHandler):
|
||||||
|
target_port = webpack_port
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoHandler(BaseHandler):
|
||||||
|
target_port = django_port
|
||||||
|
|
||||||
|
|
||||||
|
class TornadoHandler(BaseHandler):
|
||||||
|
target_port = tornado_port
|
||||||
|
|
||||||
|
|
||||||
|
class Application(web.Application):
|
||||||
|
def __init__(self):
|
||||||
|
# type: () -> None
|
||||||
|
handlers = [
|
||||||
|
(r"/sockjs/.*/websocket$", TornadoHandler),
|
||||||
|
(r"/json/events.*", TornadoHandler),
|
||||||
|
(r"/api/v1/events.*", TornadoHandler),
|
||||||
|
(r"/webpack.*", WebPackHandler),
|
||||||
|
(r"/sockjs.*", TornadoHandler),
|
||||||
|
(r"/socket.io.*", WebPackHandler),
|
||||||
|
(r"/.*", DjangoHandler)
|
||||||
|
]
|
||||||
|
super(Application, self).__init__(handlers)
|
||||||
|
|
||||||
|
|
||||||
|
def on_shutdown():
|
||||||
|
# type: () -> None
|
||||||
|
IOLoop.instance().stop()
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown_handler(*args, **kwargs):
|
||||||
|
# type: (*Any, **Any) -> None
|
||||||
|
io_loop = IOLoop.instance()
|
||||||
|
if io_loop._callbacks:
|
||||||
|
io_loop.add_timeout(time.time() + 1, shutdown_handler)
|
||||||
else:
|
else:
|
||||||
print(", webpack: {}".format(webpack_port))
|
io_loop.stop()
|
||||||
|
|
||||||
print(WARNING + "Note: only port {} is exposed to the host in a Vagrant environment.".format(proxy_port) + ENDC)
|
# log which services/ports will be started
|
||||||
|
print("Starting Zulip services on ports: web proxy: {},".format(proxy_port),
|
||||||
|
"Django: {}, Tornado: {}".format(django_port, tornado_port), end='')
|
||||||
|
if options.test:
|
||||||
|
print("") # no webpack for --test
|
||||||
|
else:
|
||||||
|
print(", webpack: {}".format(webpack_port))
|
||||||
|
|
||||||
|
print("".join((WARNING,
|
||||||
|
"Note: only port {} is exposed to the host in a Vagrant environment.".format(
|
||||||
|
proxy_port), ENDC)))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
reactor.listenTCP(proxy_port, server.Site(Resource()), interface=options.interface)
|
app = Application()
|
||||||
reactor.run()
|
app.listen(proxy_port)
|
||||||
|
ioloop = IOLoop.instance()
|
||||||
|
for s in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
signal.signal(s, shutdown_handler)
|
||||||
|
ioloop.start()
|
||||||
except:
|
except:
|
||||||
# Print the traceback before we get SIGTERM and die.
|
# Print the traceback before we get SIGTERM and die.
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
ZULIP_VERSION = "1.4.1+git"
|
ZULIP_VERSION = "1.4.1+git"
|
||||||
PROVISION_VERSION = '1.00'
|
PROVISION_VERSION = '2.0.0'
|
||||||
|
|
Loading…
Reference in New Issue