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

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