Package web2py :: Package gluon :: Module main
[hide private]
[frames] | no frames]

Source Code for Module web2py.gluon.main

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