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