PageRenderTime 45ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/gluon/main.py

https://github.com/clach04/web2py
Python | 877 lines | 815 code | 22 blank | 40 comment | 7 complexity | 4cbae01d0b3db955147bdae59960c26e MD5 | raw file
Possible License(s): MIT, BSD-3-Clause, BSD-2-Clause
  1. #!/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. This file is part of the web2py Web Framework
  5. Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
  6. License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
  7. Contains:
  8. - wsgibase: the gluon wsgi application
  9. """
  10. if False: import import_all # DO NOT REMOVE PART OF FREEZE PROCESS
  11. import gc
  12. import cgi
  13. import cStringIO
  14. import Cookie
  15. import os
  16. import re
  17. import copy
  18. import sys
  19. import time
  20. import datetime
  21. import signal
  22. import socket
  23. import tempfile
  24. import random
  25. import string
  26. import urllib2
  27. from thread import allocate_lock
  28. from fileutils import abspath, write_file, parse_version, copystream
  29. from settings import global_settings
  30. from admin import add_path_first, create_missing_folders, create_missing_app_folders
  31. from globals import current
  32. # Remarks:
  33. # calling script has inserted path to script directory into sys.path
  34. # applications_parent (path to applications/, site-packages/ etc)
  35. # defaults to that directory set sys.path to
  36. # ("", gluon_parent/site-packages, gluon_parent, ...)
  37. #
  38. # this is wrong:
  39. # web2py_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  40. # because we do not want the path to this file which may be Library.zip
  41. # gluon_parent is the directory containing gluon, web2py.py, logging.conf
  42. # and the handlers.
  43. # applications_parent (web2py_path) is the directory containing applications/
  44. # and routes.py
  45. # The two are identical unless web2py_path is changed via the web2py.py -f folder option
  46. # main.web2py_path is the same as applications_parent (for backward compatibility)
  47. web2py_path = global_settings.applications_parent # backward compatibility
  48. create_missing_folders()
  49. # set up logging for subsequent imports
  50. import logging
  51. import logging.config
  52. # This needed to prevent exception on Python 2.5:
  53. # NameError: name 'gluon' is not defined
  54. # See http://bugs.python.org/issue1436
  55. import gluon.messageboxhandler
  56. logging.gluon = gluon
  57. exists = os.path.exists
  58. pjoin = os.path.join
  59. logpath = abspath("logging.conf")
  60. if exists(logpath):
  61. logging.config.fileConfig(abspath("logging.conf"))
  62. else:
  63. logging.basicConfig()
  64. logger = logging.getLogger("web2py")
  65. from restricted import RestrictedError
  66. from http import HTTP, redirect
  67. from globals import Request, Response, Session
  68. from compileapp import build_environment, run_models_in, \
  69. run_controller_in, run_view_in
  70. from contenttype import contenttype
  71. from dal import BaseAdapter
  72. from settings import global_settings
  73. from validators import CRYPT
  74. from cache import CacheInRam
  75. from html import URL, xmlescape
  76. from utils import is_valid_ip_address
  77. from rewrite import load, url_in, THREAD_LOCAL as rwthread, \
  78. try_rewrite_on_error, fixup_missing_path_info
  79. import newcron
  80. __all__ = ['wsgibase', 'save_password', 'appfactory', 'HttpServer']
  81. requests = 0 # gc timer
  82. # Security Checks: validate URL and session_id here,
  83. # accept_language is validated in languages
  84. # pattern used to validate client address
  85. regex_client = re.compile('[\w\-:]+(\.[\w\-]+)*\.?') # ## to account for IPV6
  86. #try:
  87. if 1:
  88. version_info = open(pjoin(global_settings.gluon_parent, 'VERSION'), 'r')
  89. raw_version_string = version_info.read().strip()
  90. version_info.close()
  91. global_settings.web2py_version = parse_version(raw_version_string)
  92. #except:
  93. # raise RuntimeError("Cannot determine web2py version")
  94. web2py_version = global_settings.web2py_version
  95. try:
  96. import rocket
  97. except:
  98. if not global_settings.web2py_runtime_gae:
  99. logger.warn('unable to import Rocket')
  100. load()
  101. HTTPS_SCHEMES = set(('https', 'HTTPS'))
  102. def get_client(env):
  103. """
  104. guess the client address from the environment variables
  105. first tries 'http_x_forwarded_for', secondly 'remote_addr'
  106. if all fails, assume '127.0.0.1' or '::1' (running locally)
  107. """
  108. g = regex_client.search(env.get('http_x_forwarded_for', ''))
  109. client = (g.group() or '').split(',')[0] if g else None
  110. if client in (None, '', 'unkown'):
  111. g = regex_client.search(env.get('remote_addr', ''))
  112. if g:
  113. client = g.group()
  114. elif env.http_host.startswith('['): # IPv6
  115. client = '::1'
  116. else:
  117. client = '127.0.0.1' # IPv4
  118. if not is_valid_ip_address(client):
  119. raise HTTP(400, "Bad Request (request.client=%s)" % client)
  120. return client
  121. def copystream_progress(request, chunk_size=10 ** 5):
  122. """
  123. copies request.env.wsgi_input into request.body
  124. and stores progress upload status in cache_ram
  125. X-Progress-ID:length and X-Progress-ID:uploaded
  126. """
  127. env = request.env
  128. if not env.content_length:
  129. return cStringIO.StringIO()
  130. source = env.wsgi_input
  131. try:
  132. size = int(env.content_length)
  133. except ValueError:
  134. raise HTTP(400, "Invalid Content-Length header")
  135. dest = tempfile.TemporaryFile()
  136. if not 'X-Progress-ID' in request.vars:
  137. copystream(source, dest, size, chunk_size)
  138. return dest
  139. cache_key = 'X-Progress-ID:' + request.vars['X-Progress-ID']
  140. cache_ram = CacheInRam(request) # same as cache.ram because meta_storage
  141. cache_ram(cache_key + ':length', lambda: size, 0)
  142. cache_ram(cache_key + ':uploaded', lambda: 0, 0)
  143. while size > 0:
  144. if size < chunk_size:
  145. data = source.read(size)
  146. cache_ram.increment(cache_key + ':uploaded', size)
  147. else:
  148. data = source.read(chunk_size)
  149. cache_ram.increment(cache_key + ':uploaded', chunk_size)
  150. length = len(data)
  151. if length > size:
  152. (data, length) = (data[:size], size)
  153. size -= length
  154. if length == 0:
  155. break
  156. dest.write(data)
  157. if length < chunk_size:
  158. break
  159. dest.seek(0)
  160. cache_ram(cache_key + ':length', None)
  161. cache_ram(cache_key + ':uploaded', None)
  162. return dest
  163. def serve_controller(request, response, session):
  164. """
  165. this function is used to generate a dynamic page.
  166. It first runs all models, then runs the function in the controller,
  167. and then tries to render the output using a view/template.
  168. this function must run from the [application] folder.
  169. A typical example would be the call to the url
  170. /[application]/[controller]/[function] that would result in a call
  171. to [function]() in applications/[application]/[controller].py
  172. rendered by applications/[application]/views/[controller]/[function].html
  173. """
  174. # ##################################################
  175. # build environment for controller and view
  176. # ##################################################
  177. environment = build_environment(request, response, session)
  178. # set default view, controller can override it
  179. response.view = '%s/%s.%s' % (request.controller,
  180. request.function,
  181. request.extension)
  182. # also, make sure the flash is passed through
  183. # ##################################################
  184. # process models, controller and view (if required)
  185. # ##################################################
  186. run_models_in(environment)
  187. response._view_environment = copy.copy(environment)
  188. page = run_controller_in(request.controller, request.function, environment)
  189. if isinstance(page, dict):
  190. response._vars = page
  191. response._view_environment.update(page)
  192. run_view_in(response._view_environment)
  193. page = response.body.getvalue()
  194. # logic to garbage collect after exec, not always, once every 100 requests
  195. global requests
  196. requests = ('requests' in globals()) and (requests + 1) % 100 or 0
  197. if not requests:
  198. gc.collect()
  199. # end garbage collection logic
  200. # ##################################################
  201. # set default headers it not set
  202. # ##################################################
  203. default_headers = [
  204. ('Content-Type', contenttype('.' + request.extension)),
  205. ('Cache-Control',
  206. 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'),
  207. ('Expires', time.strftime('%a, %d %b %Y %H:%M:%S GMT',
  208. time.gmtime())),
  209. ('Pragma', 'no-cache')]
  210. for key, value in default_headers:
  211. response.headers.setdefault(key, value)
  212. raise HTTP(response.status, page, **response.headers)
  213. def start_response_aux(status, headers, exc_info, response=None):
  214. """
  215. in controller you can use::
  216. - request.wsgi.environ
  217. - request.wsgi.start_response
  218. to call third party WSGI applications
  219. """
  220. response.status = str(status).split(' ', 1)[0]
  221. response.headers = dict(headers)
  222. return lambda *args, **kargs: response.write(escape=False, *args, **kargs)
  223. def middleware_aux(request, response, *middleware_apps):
  224. """
  225. In you controller use::
  226. @request.wsgi.middleware(middleware1, middleware2, ...)
  227. to decorate actions with WSGI middleware. actions must return strings.
  228. uses a simulated environment so it may have weird behavior in some cases
  229. """
  230. def middleware(f):
  231. def app(environ, start_response):
  232. data = f()
  233. start_response(response.status, response.headers.items())
  234. if isinstance(data, list):
  235. return data
  236. return [data]
  237. for item in middleware_apps:
  238. app = item(app)
  239. def caller(app):
  240. wsgi = request.wsgi
  241. return app(wsgi.environ, wsgi.start_response)
  242. return lambda caller=caller, app=app: caller(app)
  243. return middleware
  244. def environ_aux(environ, request):
  245. new_environ = copy.copy(environ)
  246. new_environ['wsgi.input'] = request.body
  247. new_environ['wsgi.version'] = 1
  248. return new_environ
  249. def parse_get_post_vars(request, environ):
  250. # always parse variables in URL for GET, POST, PUT, DELETE, etc. in get_vars
  251. env = request.env
  252. dget = cgi.parse_qsl(env.query_string or '', keep_blank_values=1)
  253. for (key, value) in dget:
  254. if key in request.get_vars:
  255. if isinstance(request.get_vars[key], list):
  256. request.get_vars[key] += [value]
  257. else:
  258. request.get_vars[key] = [request.get_vars[key]] + [value]
  259. else:
  260. request.get_vars[key] = value
  261. request.vars[key] = request.get_vars[key]
  262. # parse POST variables on POST, PUT, BOTH only in post_vars
  263. try:
  264. request.body = body = copystream_progress(request)
  265. except IOError:
  266. raise HTTP(400, "Bad Request - HTTP body is incomplete")
  267. if (body and env.request_method in ('POST', 'PUT', 'BOTH')):
  268. dpost = cgi.FieldStorage(fp=body, environ=environ, keep_blank_values=1)
  269. # The same detection used by FieldStorage to detect multipart POSTs
  270. is_multipart = dpost.type[:10] == 'multipart/'
  271. body.seek(0)
  272. isle25 = sys.version_info[1] <= 5
  273. def listify(a):
  274. return (not isinstance(a, list) and [a]) or a
  275. try:
  276. keys = sorted(dpost)
  277. except TypeError:
  278. keys = []
  279. for key in keys:
  280. if key is None:
  281. continue # not sure why cgi.FieldStorage returns None key
  282. dpk = dpost[key]
  283. # if en element is not a file replace it with its value else leave it alone
  284. if isinstance(dpk, list):
  285. value = []
  286. for _dpk in dpk:
  287. if not _dpk.filename:
  288. value.append(_dpk.value)
  289. else:
  290. value.append(_dpk)
  291. elif not dpk.filename:
  292. value = dpk.value
  293. else:
  294. value = dpk
  295. pvalue = listify(value)
  296. if key in request.vars:
  297. gvalue = listify(request.vars[key])
  298. if isle25:
  299. value = pvalue + gvalue
  300. elif is_multipart:
  301. pvalue = pvalue[len(gvalue):]
  302. else:
  303. pvalue = pvalue[:-len(gvalue)]
  304. request.vars[key] = value
  305. if len(pvalue):
  306. request.post_vars[key] = (len(pvalue) >
  307. 1 and pvalue) or pvalue[0]
  308. def wsgibase(environ, responder):
  309. """
  310. this is the gluon wsgi application. the first function called when a page
  311. is requested (static or dynamic). it can be called by paste.httpserver
  312. or by apache mod_wsgi.
  313. - fills request with info
  314. - the environment variables, replacing '.' with '_'
  315. - adds web2py path and version info
  316. - compensates for fcgi missing path_info and query_string
  317. - validates the path in url
  318. The url path must be either:
  319. 1. for static pages:
  320. - /<application>/static/<file>
  321. 2. for dynamic pages:
  322. - /<application>[/<controller>[/<function>[/<sub>]]][.<extension>]
  323. - (sub may go several levels deep, currently 3 levels are supported:
  324. sub1/sub2/sub3)
  325. The naming conventions are:
  326. - application, controller, function and extension may only contain
  327. [a-zA-Z0-9_]
  328. - file and sub may also contain '-', '=', '.' and '/'
  329. """
  330. current.__dict__.clear()
  331. request = Request()
  332. response = Response()
  333. session = Session()
  334. env = request.env
  335. env.web2py_path = global_settings.applications_parent
  336. env.web2py_version = web2py_version
  337. env.update(global_settings)
  338. static_file = False
  339. try:
  340. try:
  341. try:
  342. # ##################################################
  343. # handle fcgi missing path_info and query_string
  344. # select rewrite parameters
  345. # rewrite incoming URL
  346. # parse rewritten header variables
  347. # parse rewritten URL
  348. # serve file if static
  349. # ##################################################
  350. fixup_missing_path_info(environ)
  351. (static_file, version, environ) = url_in(request, environ)
  352. response.status = env.web2py_status_code or response.status
  353. if static_file:
  354. if environ.get('QUERY_STRING', '').startswith(
  355. 'attachment'):
  356. response.headers['Content-Disposition'] \
  357. = 'attachment'
  358. if version:
  359. response.headers['Cache-Control'] = 'max-age=315360000'
  360. response.headers[
  361. 'Expires'] = 'Thu, 31 Dec 2037 23:59:59 GMT'
  362. response.stream(static_file, request=request)
  363. # ##################################################
  364. # fill in request items
  365. # ##################################################
  366. app = request.application # must go after url_in!
  367. if not global_settings.local_hosts:
  368. local_hosts = set(['127.0.0.1', '::ffff:127.0.0.1', '::1'])
  369. if not global_settings.web2py_runtime_gae:
  370. try:
  371. fqdn = socket.getfqdn()
  372. local_hosts.add(socket.gethostname())
  373. local_hosts.add(fqdn)
  374. local_hosts.update([
  375. ip[4][0] for ip in socket.getaddrinfo(
  376. fqdn, 0)])
  377. if env.server_name:
  378. local_hosts.add(env.server_name)
  379. local_hosts.update([
  380. ip[4][0] for ip in socket.getaddrinfo(
  381. env.server_name, 0)])
  382. except (socket.gaierror, TypeError):
  383. pass
  384. global_settings.local_hosts = list(local_hosts)
  385. else:
  386. local_hosts = global_settings.local_hosts
  387. client = get_client(env)
  388. x_req_with = str(env.http_x_requested_with).lower()
  389. request.update(
  390. client = client,
  391. folder = abspath('applications', app) + os.sep,
  392. ajax = x_req_with == 'xmlhttprequest',
  393. cid = env.http_web2py_component_element,
  394. is_local = env.remote_addr in local_hosts,
  395. is_https = env.wsgi_url_scheme in HTTPS_SCHEMES or \
  396. request.env.http_x_forwarded_proto in HTTPS_SCHEMES \
  397. or env.https == 'on')
  398. request.compute_uuid() # requires client
  399. request.url = environ['PATH_INFO']
  400. # ##################################################
  401. # access the requested application
  402. # ##################################################
  403. if not exists(request.folder):
  404. if app == rwthread.routes.default_application \
  405. and app != 'welcome':
  406. redirect(URL('welcome', 'default', 'index'))
  407. elif rwthread.routes.error_handler:
  408. _handler = rwthread.routes.error_handler
  409. redirect(URL(_handler['application'],
  410. _handler['controller'],
  411. _handler['function'],
  412. args=app))
  413. else:
  414. raise HTTP(404, rwthread.routes.error_message
  415. % 'invalid request',
  416. web2py_error='invalid application')
  417. elif not request.is_local and \
  418. exists(pjoin(request.folder, 'DISABLED')):
  419. raise HTTP(503, "<html><body><h1>Temporarily down for maintenance</h1></body></html>")
  420. # ##################################################
  421. # build missing folders
  422. # ##################################################
  423. create_missing_app_folders(request)
  424. # ##################################################
  425. # get the GET and POST data
  426. # ##################################################
  427. parse_get_post_vars(request, environ)
  428. # ##################################################
  429. # expose wsgi hooks for convenience
  430. # ##################################################
  431. request.wsgi.environ = environ_aux(environ, request)
  432. request.wsgi.start_response = \
  433. lambda status='200', headers=[], \
  434. exec_info=None, response=response: \
  435. start_response_aux(status, headers, exec_info, response)
  436. request.wsgi.middleware = \
  437. lambda *a: middleware_aux(request, response, *a)
  438. # ##################################################
  439. # load cookies
  440. # ##################################################
  441. if env.http_cookie:
  442. try:
  443. request.cookies.load(env.http_cookie)
  444. except Cookie.CookieError, e:
  445. pass # invalid cookies
  446. # ##################################################
  447. # try load session or create new session file
  448. # ##################################################
  449. if not env.web2py_disable_session:
  450. session.connect(request, response)
  451. # ##################################################
  452. # run controller
  453. # ##################################################
  454. if global_settings.debugging and app != "admin":
  455. import gluon.debug
  456. # activate the debugger
  457. gluon.debug.dbg.do_debug(mainpyfile=request.folder)
  458. serve_controller(request, response, session)
  459. except HTTP, http_response:
  460. if static_file:
  461. return http_response.to(responder, env=env)
  462. if request.body:
  463. request.body.close()
  464. # ##################################################
  465. # on success, try store session in database
  466. # ##################################################
  467. session._try_store_in_db(request, response)
  468. # ##################################################
  469. # on success, commit database
  470. # ##################################################
  471. if response.do_not_commit is True:
  472. BaseAdapter.close_all_instances(None)
  473. # elif response._custom_commit:
  474. # response._custom_commit()
  475. elif response.custom_commit:
  476. BaseAdapter.close_all_instances(response.custom_commit)
  477. else:
  478. BaseAdapter.close_all_instances('commit')
  479. # ##################################################
  480. # if session not in db try store session on filesystem
  481. # this must be done after trying to commit database!
  482. # ##################################################
  483. session._try_store_in_cookie_or_file(request, response)
  484. if request.cid:
  485. if response.flash:
  486. http_response.headers['web2py-component-flash'] = \
  487. urllib2.quote(xmlescape(response.flash)\
  488. .replace('\n',''))
  489. if response.js:
  490. http_response.headers['web2py-component-command'] = \
  491. urllib2.quote(response.js.replace('\n',''))
  492. # ##################################################
  493. # store cookies in headers
  494. # ##################################################
  495. rcookies = response.cookies
  496. if session._forget and response.session_id_name in rcookies:
  497. del rcookies[response.session_id_name]
  498. elif session._secure:
  499. rcookies[response.session_id_name]['secure'] = True
  500. http_response.cookies2headers(rcookies)
  501. ticket = None
  502. except RestrictedError, e:
  503. if request.body:
  504. request.body.close()
  505. # ##################################################
  506. # on application error, rollback database
  507. # ##################################################
  508. ticket = e.log(request) or 'unknown'
  509. if response._custom_rollback:
  510. response._custom_rollback()
  511. else:
  512. BaseAdapter.close_all_instances('rollback')
  513. http_response = \
  514. HTTP(500, rwthread.routes.error_message_ticket %
  515. dict(ticket=ticket),
  516. web2py_error='ticket %s' % ticket)
  517. except:
  518. if request.body:
  519. request.body.close()
  520. # ##################################################
  521. # on application error, rollback database
  522. # ##################################################
  523. try:
  524. if response._custom_rollback:
  525. response._custom_rollback()
  526. else:
  527. BaseAdapter.close_all_instances('rollback')
  528. except:
  529. pass
  530. e = RestrictedError('Framework', '', '', locals())
  531. ticket = e.log(request) or 'unrecoverable'
  532. http_response = \
  533. HTTP(500, rwthread.routes.error_message_ticket
  534. % dict(ticket=ticket),
  535. web2py_error='ticket %s' % ticket)
  536. finally:
  537. if response and hasattr(response, 'session_file') \
  538. and response.session_file:
  539. response.session_file.close()
  540. session._unlock(response)
  541. http_response, new_environ = try_rewrite_on_error(
  542. http_response, request, environ, ticket)
  543. if not http_response:
  544. return wsgibase(new_environ, responder)
  545. if global_settings.web2py_crontype == 'soft':
  546. newcron.softcron(global_settings.applications_parent).start()
  547. return http_response.to(responder, env=env)
  548. def save_password(password, port):
  549. """
  550. used by main() to save the password in the parameters_port.py file.
  551. """
  552. password_file = abspath('parameters_%i.py' % port)
  553. if password == '<random>':
  554. # make up a new password
  555. chars = string.letters + string.digits
  556. password = ''.join([random.choice(chars) for i in range(8)])
  557. cpassword = CRYPT()(password)[0]
  558. print '******************* IMPORTANT!!! ************************'
  559. print 'your admin password is "%s"' % password
  560. print '*********************************************************'
  561. elif password == '<recycle>':
  562. # reuse the current password if any
  563. if exists(password_file):
  564. return
  565. else:
  566. password = ''
  567. elif password.startswith('<pam_user:'):
  568. # use the pam password for specified user
  569. cpassword = password[1:-1]
  570. else:
  571. # use provided password
  572. cpassword = CRYPT()(password)[0]
  573. fp = open(password_file, 'w')
  574. if password:
  575. fp.write('password="%s"\n' % cpassword)
  576. else:
  577. fp.write('password=None\n')
  578. fp.close()
  579. def appfactory(wsgiapp=wsgibase,
  580. logfilename='httpserver.log',
  581. profilerfilename='profiler.log'):
  582. """
  583. generates a wsgi application that does logging and profiling and calls
  584. wsgibase
  585. .. function:: gluon.main.appfactory(
  586. [wsgiapp=wsgibase
  587. [, logfilename='httpserver.log'
  588. [, profilerfilename='profiler.log']]])
  589. """
  590. if profilerfilename and exists(profilerfilename):
  591. os.unlink(profilerfilename)
  592. locker = allocate_lock()
  593. def app_with_logging(environ, responder):
  594. """
  595. a wsgi app that does logging and profiling and calls wsgibase
  596. """
  597. status_headers = []
  598. def responder2(s, h):
  599. """
  600. wsgi responder app
  601. """
  602. status_headers.append(s)
  603. status_headers.append(h)
  604. return responder(s, h)
  605. time_in = time.time()
  606. ret = [0]
  607. if not profilerfilename:
  608. ret[0] = wsgiapp(environ, responder2)
  609. else:
  610. import cProfile
  611. import pstats
  612. logger.warn('profiler is on. this makes web2py slower and serial')
  613. locker.acquire()
  614. cProfile.runctx('ret[0] = wsgiapp(environ, responder2)',
  615. globals(), locals(), profilerfilename + '.tmp')
  616. stat = pstats.Stats(profilerfilename + '.tmp')
  617. stat.stream = cStringIO.StringIO()
  618. stat.strip_dirs().sort_stats("time").print_stats(80)
  619. profile_out = stat.stream.getvalue()
  620. profile_file = open(profilerfilename, 'a')
  621. profile_file.write('%s\n%s\n%s\n%s\n\n' %
  622. ('=' * 60, environ['PATH_INFO'], '=' * 60, profile_out))
  623. profile_file.close()
  624. locker.release()
  625. try:
  626. line = '%s, %s, %s, %s, %s, %s, %f\n' % (
  627. environ['REMOTE_ADDR'],
  628. datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S'),
  629. environ['REQUEST_METHOD'],
  630. environ['PATH_INFO'].replace(',', '%2C'),
  631. environ['SERVER_PROTOCOL'],
  632. (status_headers[0])[:3],
  633. time.time() - time_in,
  634. )
  635. if not logfilename:
  636. sys.stdout.write(line)
  637. elif isinstance(logfilename, str):
  638. write_file(logfilename, line, 'a')
  639. else:
  640. logfilename.write(line)
  641. except:
  642. pass
  643. return ret[0]
  644. return app_with_logging
  645. class HttpServer(object):
  646. """
  647. the web2py web server (Rocket)
  648. """
  649. def __init__(
  650. self,
  651. ip='127.0.0.1',
  652. port=8000,
  653. password='',
  654. pid_filename='httpserver.pid',
  655. log_filename='httpserver.log',
  656. profiler_filename=None,
  657. ssl_certificate=None,
  658. ssl_private_key=None,
  659. ssl_ca_certificate=None,
  660. min_threads=None,
  661. max_threads=None,
  662. server_name=None,
  663. request_queue_size=5,
  664. timeout=10,
  665. socket_timeout=1,
  666. shutdown_timeout=None, # Rocket does not use a shutdown timeout
  667. path=None,
  668. interfaces=None # Rocket is able to use several interfaces - must be list of socket-tuples as string
  669. ):
  670. """
  671. starts the web server.
  672. """
  673. if interfaces:
  674. # if interfaces is specified, it must be tested for rocket parameter correctness
  675. # not necessarily completely tested (e.g. content of tuples or ip-format)
  676. import types
  677. if isinstance(interfaces, types.ListType):
  678. for i in interfaces:
  679. if not isinstance(i, types.TupleType):
  680. raise "Wrong format for rocket interfaces parameter - see http://packages.python.org/rocket/"
  681. else:
  682. raise "Wrong format for rocket interfaces parameter - see http://packages.python.org/rocket/"
  683. if path:
  684. # if a path is specified change the global variables so that web2py
  685. # runs from there instead of cwd or os.environ['web2py_path']
  686. global web2py_path
  687. path = os.path.normpath(path)
  688. web2py_path = path
  689. global_settings.applications_parent = path
  690. os.chdir(path)
  691. [add_path_first(p) for p in (path, abspath('site-packages'), "")]
  692. if exists("logging.conf"):
  693. logging.config.fileConfig("logging.conf")
  694. save_password(password, port)
  695. self.pid_filename = pid_filename
  696. if not server_name:
  697. server_name = socket.gethostname()
  698. logger.info('starting web server...')
  699. rocket.SERVER_NAME = server_name
  700. rocket.SOCKET_TIMEOUT = socket_timeout
  701. sock_list = [ip, port]
  702. if not ssl_certificate or not ssl_private_key:
  703. logger.info('SSL is off')
  704. elif not rocket.ssl:
  705. logger.warning('Python "ssl" module unavailable. SSL is OFF')
  706. elif not exists(ssl_certificate):
  707. logger.warning('unable to open SSL certificate. SSL is OFF')
  708. elif not exists(ssl_private_key):
  709. logger.warning('unable to open SSL private key. SSL is OFF')
  710. else:
  711. sock_list.extend([ssl_private_key, ssl_certificate])
  712. if ssl_ca_certificate:
  713. sock_list.append(ssl_ca_certificate)
  714. logger.info('SSL is ON')
  715. app_info = {'wsgi_app': appfactory(wsgibase,
  716. log_filename,
  717. profiler_filename)}
  718. self.server = rocket.Rocket(interfaces or tuple(sock_list),
  719. method='wsgi',
  720. app_info=app_info,
  721. min_threads=min_threads,
  722. max_threads=max_threads,
  723. queue_size=int(request_queue_size),
  724. timeout=int(timeout),
  725. handle_signals=False,
  726. )
  727. def start(self):
  728. """
  729. start the web server
  730. """
  731. try:
  732. signal.signal(signal.SIGTERM, lambda a, b, s=self: s.stop())
  733. signal.signal(signal.SIGINT, lambda a, b, s=self: s.stop())
  734. except:
  735. pass
  736. write_file(self.pid_filename, str(os.getpid()))
  737. self.server.start()
  738. def stop(self, stoplogging=False):
  739. """
  740. stop cron and the web server
  741. """
  742. newcron.stopcron()
  743. self.server.stop(stoplogging)
  744. try:
  745. os.unlink(self.pid_filename)
  746. except:
  747. pass