From e724e13564d6ae8c13aa6067d0afdb5867bbd4e4 Mon Sep 17 00:00:00 2001 From: Massimo Date: Tue, 3 Sep 2013 11:53:18 -0500 Subject: [PATCH] fixed cron deadlock on windows, thanks Helko Besemann --- VERSION | 2 +- gluon/contrib/shell.py | 14 +++++---- gluon/newcron.py | 13 ++++++-- gluon/winservice.py | 68 ++++++++++++++++++++++++++++++++---------- 4 files changed, 74 insertions(+), 23 deletions(-) diff --git a/VERSION b/VERSION index bc7a475e..619badf2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.6.0-development+timestamp.2013.09.03.09.52.37 +Version 2.6.0-development+timestamp.2013.09.03.11.52.13 diff --git a/gluon/contrib/shell.py b/gluon/contrib/shell.py index e875a57a..f04a3fc4 100755 --- a/gluon/contrib/shell.py +++ b/gluon/contrib/shell.py @@ -48,12 +48,12 @@ _DEBUG = True _HISTORY_KIND = '_Shell_History' # Types that can't be pickled. -UNPICKLABLE_TYPES = ( +UNPICKLABLE_TYPES = [ types.ModuleType, types.TypeType, types.ClassType, types.FunctionType, -) +] # Unpicklable statements to seed new historys with. INITIAL_UNPICKLABLES = [ @@ -244,8 +244,8 @@ def run(history, statement, env={}): for name, val in statement_module.__dict__.items(): if name not in old_globals or represent(val) != old_globals[name]: new_globals[name] = val - - if True in [isinstance(val, UNPICKLABLE_TYPES) + + if True in [isinstance(val, tuple(UNPICKLABLE_TYPES)) for val in new_globals.values()]: # this statement added an unpicklable global. store the statement and # the names of all of the globals it added in the unpicklables. @@ -256,7 +256,11 @@ def run(history, statement, env={}): # new globals back into the datastore. for name, val in new_globals.items(): if not name.startswith('__'): - history.set_global(name, val) + try: + history.set_global(name, val) + except TypeError, ex: + UNPICKLABLE_TYPES.append(type(val)) + history.add_unpicklable(statement, new_globals.keys()) finally: sys.modules['__main__'] = old_main diff --git a/gluon/newcron.py b/gluon/newcron.py index 90e6eacd..00d5e549 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -117,6 +117,10 @@ class Token(object): if a cron job started before 60 seconds and did not stop, a warning is issue "Stale cron.master detected" """ + if sys.platform == 'win32': + locktime = 59.5 + else: + locktime = 59.99 if portalocker.LOCK_EX is None: logger.warning('WEB2PY CRON: Disabled because no file locking') return None @@ -128,7 +132,7 @@ class Token(object): (start, stop) = cPickle.load(self.master) except: (start, stop) = (0, 1) - if startup or self.now - start > 59.99: + if startup or self.now - start > locktime: ret = self.now if not stop: # this happens if previous cron job longer than 1 minute @@ -136,6 +140,7 @@ class Token(object): logger.debug('WEB2PY CRON: Acquiring lock') self.master.seek(0) cPickle.dump((self.now, 0), self.master) + self.master.flush() finally: portalocker.unlock(self.master) if not ret: @@ -304,7 +309,11 @@ def crondance(applications_parent, ctype='soft', startup=False, apps=None): for task in tasks: if _cron_stopping: break - commands = [sys.executable] + if sys.executable.lower().endswith('pythonservice.exe'): + _python_exe = os.path.join(sys.exec_prefix, 'python.exe') + else: + _python_exe = sys.executable + commands = [_python_exe] w2p_path = fileutils.abspath('web2py.py', gluon=True) if os.path.exists(w2p_path): commands.append(w2p_path) diff --git a/gluon/winservice.py b/gluon/winservice.py index e08f3132..36cb75df 100644 --- a/gluon/winservice.py +++ b/gluon/winservice.py @@ -36,6 +36,7 @@ class Service(win32serviceutil.ServiceFramework): _svc_name_ = '_unNamed' _svc_display_name_ = '_Service Template' + _svc_description_ = '_Service Template description' def __init__(self, *args): win32serviceutil.ServiceFramework.__init__(self, *args) @@ -44,6 +45,12 @@ class Service(win32serviceutil.ServiceFramework): def log(self, msg): servicemanager.LogInfoMsg(str(msg)) + def log_error(self, msg): + """ + Log an error message to windows application event log. + """ + servicemanager.LogErrorMsg(str(msg)) + def SvcDoRun(self): self.ReportServiceStatus(win32service.SERVICE_START_PENDING) try: @@ -80,6 +87,7 @@ class Web2pyService(Service): _svc_name_ = 'web2py' _svc_display_name_ = 'web2py Service' + _svc_description_ = 'Web2py application framework service' _exe_args_ = 'options' server = None @@ -132,17 +140,16 @@ class Web2pyService(Service): server_name=options.server_name, request_queue_size=options.request_queue_size, timeout=options.timeout, + socket_timeout=options.socket_timeout, shutdown_timeout=options.shutdown_timeout, - path=options.folder + path=options.folder, + interfaces=options.interfaces ) try: from rewrite import load load() self.server.start() except: - - # self.server.stop() - self.server = None raise @@ -159,12 +166,18 @@ class Web2pyCronService(Web2pyService): _svc_name_ = 'web2py_cron' _svc_display_name_ = 'web2py Cron Service' + _svc_description_ = 'web2py Windows cron service for scheduled tasks' _exe_args_ = 'options' def start(self): import newcron - import global_settings - self.log('web2py server starting') + import logging + import logging.config + from settings import global_settings + from fileutils import abspath + from os.path import exists, join + + self.log('web2py Cron service starting') if not self.chdir(): return if len(sys.argv) == 2: @@ -172,25 +185,50 @@ class Web2pyCronService(Web2pyService): else: opt_mod = self._exe_args_ options = __import__(opt_mod, [], [], '') - global_settings.global_settings.web2py_crontype = 'external' + logpath = abspath(join(options.folder, "logging.conf")) + + if exists(logpath): + logging.config.fileConfig(logpath) + else: + logging.basicConfig() + logger = logging.getLogger("web2py.cron") + global_settings.web2py_crontype = 'external' if options.scheduler: # -K apps = [app.strip() for app in options.scheduler.split( ',') if check_existent_app(options, app.strip())] else: apps = None - self.extcron = newcron.extcron(options.folder, apps=apps) - try: - self.extcron.start() - except: - # self.server.stop() - self.extcron = None - raise + + misfire_gracetime = float(options.misfire_gracetime) if 'misfire_gracetime' in dir(options) else 0.0 + logger.info('Starting Window cron service with %0.2f secs gracetime.' % misfire_gracetime) + self._started = True + wait_full_min = lambda: 60 - time.time() % 60 + while True: + try: + if wait_full_min() >= misfire_gracetime: # an offset of max. 5 secs before full minute (e.g. time.sleep(60) == 58.99 secs) + self.extcron = newcron.extcron(options.folder, apps=apps) + self.extcron.start() + time.sleep(wait_full_min()) + else: + logger.debug('time.sleep() offset detected: %0.3f s' % wait_full_min()) + while wait_full_min() <= misfire_gracetime: + pass + if apps != None: + break + if not self._started: + break + except Exception, ex: + self.extcron = None + self.log_error('%s, restarting service.' % ex) + logger.exception('Exception! Restarting Windows cron service.' % ex) + self.start() def stop(self): - self.log('web2py cron stopping') + self.log('web2py Cron service stopping') if not self.chdir(): return if self.extcron: + self._started = False self.extcron.join() def register_service_handler(argv=None, opt_file='options', cls=Web2pyService):