1r""" 2Starting in CherryPy 3.1, cherrypy.server is implemented as an 3:ref:`Engine Plugin<plugins>`. It's an instance of 4:class:`cherrypy._cpserver.Server`, which is a subclass of 5:class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class 6is designed to control other servers, as well. 7 8Multiple servers/ports 9====================== 10 11If you need to start more than one HTTP server (to serve on multiple ports, or 12protocols, etc.), you can manually register each one and then start them all 13with engine.start:: 14 15 s1 = ServerAdapter( 16 cherrypy.engine, 17 MyWSGIServer(host='0.0.0.0', port=80) 18 ) 19 s2 = ServerAdapter( 20 cherrypy.engine, 21 another.HTTPServer(host='127.0.0.1', SSL=True) 22 ) 23 s1.subscribe() 24 s2.subscribe() 25 cherrypy.engine.start() 26 27.. index:: SCGI 28 29FastCGI/SCGI 30============ 31 32There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in 33:mod:`cherrypy.process.servers`. To start an fcgi server, for example, 34wrap an instance of it in a ServerAdapter:: 35 36 addr = ('0.0.0.0', 4000) 37 f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr) 38 s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr) 39 s.subscribe() 40 41The :doc:`cherryd</deployguide/cherryd>` startup script will do the above for 42you via its `-f` flag. 43Note that you need to download and install `flup <http://trac.saddi.com/flup>`_ 44yourself, whether you use ``cherryd`` or not. 45 46.. _fastcgi: 47.. index:: FastCGI 48 49FastCGI 50------- 51 52A very simple setup lets your cherry run with FastCGI. 53You just need the flup library, 54plus a running Apache server (with ``mod_fastcgi``) or lighttpd server. 55 56CherryPy code 57^^^^^^^^^^^^^ 58 59hello.py:: 60 61 #!/usr/bin/python 62 import cherrypy 63 64 class HelloWorld: 65 '''Sample request handler class.''' 66 @cherrypy.expose 67 def index(self): 68 return "Hello world!" 69 70 cherrypy.tree.mount(HelloWorld()) 71 # CherryPy autoreload must be disabled for the flup server to work 72 cherrypy.config.update({'engine.autoreload.on':False}) 73 74Then run :doc:`/deployguide/cherryd` with the '-f' arg:: 75 76 cherryd -c <myconfig> -d -f -i hello.py 77 78Apache 79^^^^^^ 80 81At the top level in httpd.conf:: 82 83 FastCgiIpcDir /tmp 84 FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4 85 86And inside the relevant VirtualHost section:: 87 88 # FastCGI config 89 AddHandler fastcgi-script .fcgi 90 ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1 91 92Lighttpd 93^^^^^^^^ 94 95For `Lighttpd <http://www.lighttpd.net/>`_ you can follow these 96instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is 97active within ``server.modules``. Then, within your ``$HTTP["host"]`` 98directive, configure your fastcgi script like the following:: 99 100 $HTTP["url"] =~ "" { 101 fastcgi.server = ( 102 "/" => ( 103 "script.fcgi" => ( 104 "bin-path" => "/path/to/your/script.fcgi", 105 "socket" => "/tmp/script.sock", 106 "check-local" => "disable", 107 "disable-time" => 1, 108 "min-procs" => 1, 109 "max-procs" => 1, # adjust as needed 110 ), 111 ), 112 ) 113 } # end of $HTTP["url"] =~ "^/" 114 115Please see `Lighttpd FastCGI Docs 116<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for 117an explanation of the possible configuration options. 118""" 119 120import os 121import sys 122import time 123import warnings 124import contextlib 125 126import portend 127 128 129class Timeouts: 130 occupied = 5 131 free = 1 132 133 134class ServerAdapter(object): 135 136 """Adapter for an HTTP server. 137 138 If you need to start more than one HTTP server (to serve on multiple 139 ports, or protocols, etc.), you can manually register each one and then 140 start them all with bus.start:: 141 142 s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80)) 143 s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True)) 144 s1.subscribe() 145 s2.subscribe() 146 bus.start() 147 """ 148 149 def __init__(self, bus, httpserver=None, bind_addr=None): 150 self.bus = bus 151 self.httpserver = httpserver 152 self.bind_addr = bind_addr 153 self.interrupt = None 154 self.running = False 155 156 def subscribe(self): 157 self.bus.subscribe('start', self.start) 158 self.bus.subscribe('stop', self.stop) 159 160 def unsubscribe(self): 161 self.bus.unsubscribe('start', self.start) 162 self.bus.unsubscribe('stop', self.stop) 163 164 def start(self): 165 """Start the HTTP server.""" 166 if self.running: 167 self.bus.log('Already serving on %s' % self.description) 168 return 169 170 self.interrupt = None 171 if not self.httpserver: 172 raise ValueError('No HTTP server has been created.') 173 174 if not os.environ.get('LISTEN_PID', None): 175 # Start the httpserver in a new thread. 176 if isinstance(self.bind_addr, tuple): 177 portend.free(*self.bind_addr, timeout=Timeouts.free) 178 179 import threading 180 t = threading.Thread(target=self._start_http_thread) 181 t.name = 'HTTPServer ' + t.name 182 t.start() 183 184 self.wait() 185 self.running = True 186 self.bus.log('Serving on %s' % self.description) 187 start.priority = 75 188 189 @property 190 def description(self): 191 """ 192 A description about where this server is bound. 193 """ 194 if self.bind_addr is None: 195 on_what = 'unknown interface (dynamic?)' 196 elif isinstance(self.bind_addr, tuple): 197 on_what = self._get_base() 198 else: 199 on_what = 'socket file: %s' % self.bind_addr 200 return on_what 201 202 def _get_base(self): 203 if not self.httpserver: 204 return '' 205 host, port = self.bound_addr 206 if getattr(self.httpserver, 'ssl_adapter', None): 207 scheme = 'https' 208 if port != 443: 209 host += ':%s' % port 210 else: 211 scheme = 'http' 212 if port != 80: 213 host += ':%s' % port 214 215 return '%s://%s' % (scheme, host) 216 217 def _start_http_thread(self): 218 """HTTP servers MUST be running in new threads, so that the 219 main thread persists to receive KeyboardInterrupt's. If an 220 exception is raised in the httpserver's thread then it's 221 trapped here, and the bus (and therefore our httpserver) 222 are shut down. 223 """ 224 try: 225 self.httpserver.start() 226 except KeyboardInterrupt: 227 self.bus.log('<Ctrl-C> hit: shutting down HTTP server') 228 self.interrupt = sys.exc_info()[1] 229 self.bus.exit() 230 except SystemExit: 231 self.bus.log('SystemExit raised: shutting down HTTP server') 232 self.interrupt = sys.exc_info()[1] 233 self.bus.exit() 234 raise 235 except Exception: 236 self.interrupt = sys.exc_info()[1] 237 self.bus.log('Error in HTTP server: shutting down', 238 traceback=True, level=40) 239 self.bus.exit() 240 raise 241 242 def wait(self): 243 """Wait until the HTTP server is ready to receive requests.""" 244 while not getattr(self.httpserver, 'ready', False): 245 if self.interrupt: 246 raise self.interrupt 247 time.sleep(.1) 248 249 # bypass check when LISTEN_PID is set 250 if os.environ.get('LISTEN_PID', None): 251 return 252 253 # bypass check when running via socket-activation 254 # (for socket-activation the port will be managed by systemd) 255 if not isinstance(self.bind_addr, tuple): 256 return 257 258 # wait for port to be occupied 259 with _safe_wait(*self.bound_addr): 260 portend.occupied(*self.bound_addr, timeout=Timeouts.occupied) 261 262 @property 263 def bound_addr(self): 264 """ 265 The bind address, or if it's an ephemeral port and the 266 socket has been bound, return the actual port bound. 267 """ 268 host, port = self.bind_addr 269 if port == 0 and self.httpserver.socket: 270 # Bound to ephemeral port. Get the actual port allocated. 271 port = self.httpserver.socket.getsockname()[1] 272 return host, port 273 274 def stop(self): 275 """Stop the HTTP server.""" 276 if self.running: 277 # stop() MUST block until the server is *truly* stopped. 278 self.httpserver.stop() 279 # Wait for the socket to be truly freed. 280 if isinstance(self.bind_addr, tuple): 281 portend.free(*self.bound_addr, timeout=Timeouts.free) 282 self.running = False 283 self.bus.log('HTTP Server %s shut down' % self.httpserver) 284 else: 285 self.bus.log('HTTP Server %s already shut down' % self.httpserver) 286 stop.priority = 25 287 288 def restart(self): 289 """Restart the HTTP server.""" 290 self.stop() 291 self.start() 292 293 294class FlupCGIServer(object): 295 296 """Adapter for a flup.server.cgi.WSGIServer.""" 297 298 def __init__(self, *args, **kwargs): 299 self.args = args 300 self.kwargs = kwargs 301 self.ready = False 302 303 def start(self): 304 """Start the CGI server.""" 305 # We have to instantiate the server class here because its __init__ 306 # starts a threadpool. If we do it too early, daemonize won't work. 307 from flup.server.cgi import WSGIServer 308 309 self.cgiserver = WSGIServer(*self.args, **self.kwargs) 310 self.ready = True 311 self.cgiserver.run() 312 313 def stop(self): 314 """Stop the HTTP server.""" 315 self.ready = False 316 317 318class FlupFCGIServer(object): 319 320 """Adapter for a flup.server.fcgi.WSGIServer.""" 321 322 def __init__(self, *args, **kwargs): 323 if kwargs.get('bindAddress', None) is None: 324 import socket 325 if not hasattr(socket, 'fromfd'): 326 raise ValueError( 327 'Dynamic FCGI server not available on this platform. ' 328 'You must use a static or external one by providing a ' 329 'legal bindAddress.') 330 self.args = args 331 self.kwargs = kwargs 332 self.ready = False 333 334 def start(self): 335 """Start the FCGI server.""" 336 # We have to instantiate the server class here because its __init__ 337 # starts a threadpool. If we do it too early, daemonize won't work. 338 from flup.server.fcgi import WSGIServer 339 self.fcgiserver = WSGIServer(*self.args, **self.kwargs) 340 # TODO: report this bug upstream to flup. 341 # If we don't set _oldSIGs on Windows, we get: 342 # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", 343 # line 108, in run 344 # self._restoreSignalHandlers() 345 # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", 346 # line 156, in _restoreSignalHandlers 347 # for signum,handler in self._oldSIGs: 348 # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' 349 self.fcgiserver._installSignalHandlers = lambda: None 350 self.fcgiserver._oldSIGs = [] 351 self.ready = True 352 self.fcgiserver.run() 353 354 def stop(self): 355 """Stop the HTTP server.""" 356 # Forcibly stop the fcgi server main event loop. 357 self.fcgiserver._keepGoing = False 358 # Force all worker threads to die off. 359 self.fcgiserver._threadPool.maxSpare = ( 360 self.fcgiserver._threadPool._idleCount) 361 self.ready = False 362 363 364class FlupSCGIServer(object): 365 366 """Adapter for a flup.server.scgi.WSGIServer.""" 367 368 def __init__(self, *args, **kwargs): 369 self.args = args 370 self.kwargs = kwargs 371 self.ready = False 372 373 def start(self): 374 """Start the SCGI server.""" 375 # We have to instantiate the server class here because its __init__ 376 # starts a threadpool. If we do it too early, daemonize won't work. 377 from flup.server.scgi import WSGIServer 378 self.scgiserver = WSGIServer(*self.args, **self.kwargs) 379 # TODO: report this bug upstream to flup. 380 # If we don't set _oldSIGs on Windows, we get: 381 # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", 382 # line 108, in run 383 # self._restoreSignalHandlers() 384 # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", 385 # line 156, in _restoreSignalHandlers 386 # for signum,handler in self._oldSIGs: 387 # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' 388 self.scgiserver._installSignalHandlers = lambda: None 389 self.scgiserver._oldSIGs = [] 390 self.ready = True 391 self.scgiserver.run() 392 393 def stop(self): 394 """Stop the HTTP server.""" 395 self.ready = False 396 # Forcibly stop the scgi server main event loop. 397 self.scgiserver._keepGoing = False 398 # Force all worker threads to die off. 399 self.scgiserver._threadPool.maxSpare = 0 400 401 402@contextlib.contextmanager 403def _safe_wait(host, port): 404 """ 405 On systems where a loopback interface is not available and the 406 server is bound to all interfaces, it's difficult to determine 407 whether the server is in fact occupying the port. In this case, 408 just issue a warning and move on. See issue #1100. 409 """ 410 try: 411 yield 412 except portend.Timeout: 413 if host == portend.client_host(host): 414 raise 415 msg = 'Unable to verify that the server is bound on %r' % port 416 warnings.warn(msg) 417