1. Yujie Wu
  2. mucron

Commits

Yujie Wu  committed e3b0f2a Merge

Release v1.0
flow: Promoted <develop> 'trunk' (0c925d19fbd3) to 'default'.

  • Participants
  • Parent commits 964448b, 0c925d1
  • Branches default
  • Tags v1.0

Comments (0)

Files changed (4)

File cool

View file
  • Ignore whitespace
+echo mucron is cool!

File crontab

View file
  • Ignore whitespace
+# Example crontab file
+01 15 * * * ${HOME}/bin/daytip >> ${HOME}/Applications/DayTip/daytip.log 2>&1
+01 0,12 * * * ${HOME}/bin/wallpapersetter >> ${HOME}/Applications/WallpaperSetter/wallpapersetter.log 2>&1
+*  *  * * * /Users/wu/Workspace/mucron/cool >> /Users/wu/Workspace/mucron/cool.log

File mucron.py

View file
  • Ignore whitespace
+#!/usr/bin/python
+#
+# This program is an implementation of the cron server/daemon.
+# - How to install
+#   1. You need to install a third party module: python-daemon. You can download it from here:
+#      http://pypi.python.org/pypi/python-daemon/
+#   2. Save 'mucron.py' and 'mucron_config.py' in the same directory (any directory is fine).
+#
+# - How to use
+#   1. Simple configuration: Set the paths of the log file, the crontab file, and the lock file, in the 'mucron_config.py' file.
+#      The lock file is a zero byte file whose purpose is to prevent running multiple 'mucron' daemons.
+#   2. Execute 'mucron.py' or 'python mucron.py'.
+#   3. Set your crontabs in the specified crontab file. The syntax of crontab follows the standard. You can modify the crontab
+#      file at any time. An example file: 'crontab' is provided in the package.
+#
+# 
+# Copyright (C) 2009-2012, Yujie Wu.
+# All rights reserved.
+#
+# The BSD 2-Clause License
+#
+# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
+# conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+#    disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+#    disclaimer in the documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+# EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+
+
+import os
+import sys
+import subprocess
+import time
+import daemon
+import logging
+import signal
+import mucron_config
+
+
+logger = None
+
+
+
+# Syntax
+# .---------------- minute (0 - 59)
+# |  .------------- hour (0 - 23)
+# |  |  .---------- day of month (1 - 31)
+# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ... 
+# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7)  OR sun,mon,tue,wed,thu,fri,sat 
+# |  |  |  |  |
+# *  *  *  *  *  command to be executed
+
+
+
+class Task( object ) :
+    """
+    Class representing a task to be invoked by the cron server
+    """
+    def __init__( self, minute, hour, day_of_month, month, day_of_week, command ) :
+        """
+        @type       minute: C{set} of C{int}
+        @type         hour: C{set} of C{int}
+        @type day_of_month: C{set} of C{int}
+        @type        month: C{set} of C{int}
+        @type  day_of_week: C{set} of C{int}
+        @type      command: C{list} of C{str}
+        @param     command: Command to invoke
+        """
+        self.minute       = minute
+        self.hour         = hour
+        self.day_of_month = day_of_month
+        self.month        = month
+        self.day_of_week  = day_of_week   # Sunday is always 0, but can be set by 7 in crontab.
+        self.command      = command
+
+
+
+    def check_time( self, tm ) :
+        """
+        Returns C{True} if it is time to invoke this task; otherwise returns C{False}.
+
+        @type  tm: C{time.struct_time}
+        @param tm: Current time
+        """
+        # tm_year=2009, tm_mon=5, tm_mday=9, tm_hour=11, tm_min=34, tm_sec=2, tm_wday=5, tm_yday=129, tm_isdst=1
+        if (tm.tm_mon  not in self.month       ) : return False
+        if (tm.tm_mday not in self.day_of_month) : return False
+        if (tm.tm_wday not in self.day_of_week ) : return False
+        if (tm.tm_hour not in self.hour        ) : return False
+        if (tm.tm_min  not in self.minute      ) : return False
+        return True
+
+
+
+def parse_field( s, value_range, alias = [] ) :
+    """
+    This function is called by the C{parse_crontab} function to parse a particular time field, which is one of the following
+    (with range in the parenthesis):
+      - minute (0 - 59)
+      - hour (0 - 23)
+      - day of month (1 - 31)
+      - month (1 - 12) OR jan,feb,mar,apr ... 
+      - day of week (0 - 6) (Sunday=0 or 7)  OR sun,mon,tue,wed,thu,fri,sat
+    If parsing succeeds, it returns a set of specified values for the particular time field; otherwise, it raises a
+    C{ValueError} exception.
+
+    @type            s: C{str}
+    @param           s: A string representing a specification of particular time field. No spaces in the string.
+    @type  value_range: C{tuple} of two C{int}
+    @param value_range: (min, max) range of all valid values for the particular time field
+    @type        alias: C{list} of C{str}
+    @param       alias: A sequence of aliases corresponding to the integer values from min to max
+    
+    @return: A C{set} object containing all specified value for the particular time field
+    """
+    def get_value( token, value_range, alias ) :
+        """
+        @type  token: C{str}
+        @param token: Should be either an integer literal or one of the aliases, otherwise a C{ValueError} exception will be
+                      raised.
+        
+        @return: A C{int} value that the C{token} represents
+        """
+        try :
+            tmp = int( token )
+        except ValueError :
+            tmp = alias.index( token ) + value_range[0]
+        if (value_range[0] > tmp or value_range[1] < tmp) :
+            raise ValueError( "value (%d) is out of range [%d, %d]" % (tmp, value_range[0], value_range[1],) )
+        return tmp
+    
+        
+    point = set()
+    for token in s.split( ',' ) :
+        if ("" == token) :
+            continue
+        if ('*' == token) :
+            point = set( range( value_range[0], value_range[1] + 1 ) )
+            break
+        r   = token.split( '-' )
+        num = len( r )
+        if   (1 == num) :
+            point.add( get_value( token, value_range, alias ) )
+        elif (2 == num) :
+            low  = get_value( r[0], value_range, alias )
+            high = get_value( r[1], value_range, alias )
+            point |= range( low, high + 1 )
+        else :
+            raise ValueError( "invalid syntax: %s" % token )
+    return point
+
+
+
+def parse_crontab( s ) :
+    """
+    Parses a crontab (i.e., an entry/row in the crontab file), and creates and returns a C{Task} object.
+
+    @type  s: C{str}
+    @param s: A row from the crontab file. C{s} can be an empty line, or a line containing only spaces, or a comment, and in
+              this case the function returns C{None}.
+
+    @return: A C{Task} object if parsing succeeds, or C{None} if parsing fails due to syntax error (in this case, error will
+             be printed into the log file), or C{None} if C{s} is an empty line, or line containing only spaces, or a comment.
+    """
+    orig_s = s.strip()
+    if (""  == orig_s   ) : return None
+    if ('#' == orig_s[0]) : return None
+    
+    token   = []
+    command = None
+    for i in range( 5 ) :
+        before, sep, after = s.partition( ' ' )
+        after = after.strip()
+        if ("" == after) :
+            logger.error( "Wrong syntax of crontab item: '%s'" % orig_s )
+            return None
+        token.append( before )
+        s = after
+    if ("" != after) :
+        command = after
+
+    try :
+        minute = parse_field( token[0], (0, 59,) )
+    except ValueError, e :
+        logger.error( "Wrong syntax in setting of minute: '%s'" % orig_s )
+        logger.error( str( e ) )
+        return None
+    
+    try :
+        hour = parse_field( token[1], (0, 23,) )
+    except ValueError, e :
+        logger.error( "Wrong syntax in setting of hour: '%s'" % orig_s )
+        logger.error( str( e ) )
+        return None
+
+    try :
+        day_of_month = parse_field( token[2], (1, 31,) )
+    except ValueError, e :
+        logger.error( "Wrong syntax in setting of day-of-month: '%s'" % orig_s )
+        logger.error( str( e ) )
+        return None
+
+    try :
+        month = parse_field( token[3], (1, 12,),
+                             ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",] )
+    except ValueError, e :
+        logger.error( "Wrong syntax in setting of month: '%s'" % orig_s )
+        logger.error( str( e ) )
+        return None
+
+    try :
+        day_of_week = parse_field( token[4], (0, 7,), ["sun", "mon", "tue", "wed", "thu", "fri", "sat",] )
+        if (7 == day_of_week) :
+            day_of_week = 0
+    except ValueError, e :
+        logger.error( "Wrong syntax in setting of day-of-week: '%s'" % orig_s )
+        logger.error( str( e ) )
+        return None
+
+    return Task( minute, hour, day_of_month, month, day_of_week, command )
+
+
+
+def get_task() :
+    """
+    Parses the crontab file and returns a list of tasks. Any parsing error of the crontab file will be recorded into the log
+    file
+
+    @return: A list of C{Task} objects
+    """
+    crontab_task = []
+    task         = []
+    if (os.path.isfile( mucron_config.crontab_filename )) :
+        crontab_task = open( mucron_config.crontab_filename, "r" ).read().split( "\n" )
+    for t in crontab_task :
+        tmp = parse_crontab( t )
+        if (tmp is not None) :
+            task.append( tmp )
+    return task
+
+
+
+# This function is temporarily unused.
+def closest( a, s ) :
+    """
+    `a' is a number, `s' is a set of numbers.
+    This function returns the smallest number in `s' that is equal or greater than `a'. If there is no such a number, it returns
+    the smallest number in `s'.
+    """
+    s = list( s )
+    s.sort()
+    for e in s :
+        if (e >= a) :
+            return e
+    return s[0]
+
+
+
+def handle_signal( signal_name ) :
+    """
+    This function is to handle the following signals: SIGTERM, SIGINT, SIGHUP: clean up the lock and then exit gracefully.
+    """
+    signal.signal( signal.SIGTERM, signal.SIG_DFL )
+    signal.signal( signal.SIGINT,  signal.SIG_DFL )
+    signal.signal( signal.SIGHUP,  signal.SIG_DFL )
+    logger.info( "%s signal received." % signal_name )
+    logger.info( "mucron was terminated." )
+    os.remove( mucron_config.lock_filename )
+    sys.exit( 0 )
+
+    
+
+def main() :
+    """
+    The main program
+    """
+    # Prepares logger
+    global logger
+    logger = logging.getLogger( "mucron" )
+    logging.basicConfig( level = logging.INFO, format = "%(asctime)s: %(levelname)s: %(message)s",
+                         filename = mucron_config.log_filename )
+
+    # Signal handling stuff
+    signal.signal( signal.SIGTERM, lambda x, stack_frame : handle_signal( "SIGTERM" ) )
+    signal.signal( signal.SIGINT,  lambda x, stack_frame : handle_signal( "SIGINT"  ) )
+    signal.signal( signal.SIGHUP,  lambda x, stack_frame : handle_signal( "SIGHUP"  ) )
+
+    logger.info( "mucron was booted." )
+    while (True) :
+        task = get_task()
+        tm   = time.localtime()
+        for t in task :
+            if (t.check_time( tm ) and t.command is not None) :
+                logger.info( "invoked '%s'." % t.command )
+                subprocess.Popen( t.command, shell = True )
+        time.sleep( 60 )
+
+    
+
+if ("__main__" == __name__) :
+    if (os.path.isfile( mucron_config.lock_filename )) :
+        print "The mucron daemon seems already running."
+        print "If that is incorrect, delete the lock file:'%s'," % mucron_config.lock_filename
+        print "and try again."
+        sys.exit( 1 )
+    open( mucron_config.lock_filename, "w" ).close()
+    with daemon.DaemonContext() :
+        main()

File mucron_config.py

View file
  • Ignore whitespace
+# This is the configuration file of mucron
+
+
+# Syntax
+# .---------------- minute (0 - 59) 
+# |  .------------- hour (0 - 23)
+# |  |  .---------- day of month (1 - 31)
+# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ... 
+# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7)  OR sun,mon,tue,wed,thu,fri,sat 
+# |  |  |  |  |
+# *  *  *  *  *  command to be executed
+
+
+
+log_filename     = "/Users/wu/Workspace/mucron/mucron.log"
+crontab_filename = "/Users/wu/Workspace/mucron/crontab"
+lock_filename    = "/Users/wu/Workspace/mucron/lock"