Results 1 to 4 of 4

Thread: CronJob class. Parsing Included.

  1. #1

    Thread Starter
    Fanatic Member
    Join Date
    Aug 2010
    Posts
    624

    CronJob class. Parsing Included.

    Well I had the need to write my own internal task scheduler, and I've used the Crontab utility on linux multiple times so I decided to write my own Cron parsing methods.

    Error reporting on failed "Parse" operations is pretty much non-existant, I just wrote a catchall (NullFields) to check a cronjob for null fields which would indicate a failed parse. If you wish to make some more in-depth error-reporting, just modify the NullFields function to throw some exceptions with varying messages depending on which field references are Nothing.

    As far as I know this will parse all valid Crontab strings into a valid CronJob object. If there's some standard I've missed, feel free to let me know.

    I used regular expressions for the parsing, but if anyone has any other, more efficent, methods, I'm all ears.

    I used byte-fields instead of Integers to save space, it seemed appropriate seeing as the only valid fields are between 0-59 inclusive, which is well within byte range, and as there could potentially be around 135 total items in all the arrays combined (if someone made a cron entry of the equivalent "* * * * *" but with each item explicitly stated), that's a net memory save of 405 bytes per CronJob object (yeah, I know it's not much, but it can add up if you start getting larger lists)

    Suggestions or improvements are always welcome. This was written in the VB 2010 Express Edition IDE targetting .NET 3.5 (if you want to replace the LINQ "contains" extension functions in the IsRunnable() function, you can get the framework down to 2.0 instead, but I was lazy).

    EDIT: According to the Linux MAN pages, the DayOfWeek value can be between 0-7 inclusive, and Sunday is both 0 and 7. In my code I ignore this and accept between 0-6 inclusive simply because fixing it to comply with the MAN page would mean a lot of fiddly hardcoding for such a small improvement.
    vb Code:
    1. Imports System.Text.RegularExpressions
    2. Public Class CronJob
    3.     'the following instance variables are arrays that contain the appropriate values for when this
    4.     'particular cronjob can be run. If a "*" was found with no modifier, a single entry of Cron.ANY value
    5.     'will be the contents of the array, this indicates that the cronjob can run every minute/hour/day/month/weekday
    6.     'as the case may be.
    7.     Private _minutes As Byte()
    8.     Private _hours As Byte()
    9.     Private _days As Byte()
    10.     Private _months As Byte()
    11.     Private _weekdays As Byte()
    12.     'the command to be executed (i.e "php")
    13.     Private _command As String
    14.     'the command-line arguments to be passed when executing the above command.
    15.     Private _args As String()
    16.     'original string given when parsing.
    17.     Private _cronstring As String
    18.  
    19.     'value that indicates the field is valid at any value. As no proper cron values can be > 59, this
    20.     'is a safe value to use. Also, the cron pattern doesn't match more than 2 digits in any single match.
    21.     Public Const ANY As Byte = Byte.MaxValue - 1
    22.  
    23.     'pattern that matches a singular part of the cron pattern. At this stage it will match the following formats.
    24.     'NB: Assume the dummy w,x,y,z values are integer values between 1 and 2 digits long.
    25.     'Any:                        *
    26.     'Any + Modifier:             */x
    27.     'Lower + Upper:              x-y
    28.     'Lower + Upper + Modifier:   x-y/z
    29.     'Explicit Values:            w,x,y,z
    30.     Public Const CRON_PATTERN As String = "^(?:(?:(?:(?<any>\*)|(?<lower>\d{1,2})-(?<upper>\d{1,2}))(?:/(?<mod>\d{1,2}))?)|(?<explicit>\d{1,2},(?:\d{1,2},)*\d{1,2})|(?<single>\d{1,2}))$"
    31.  
    32.     Private Structure Boundary
    33.         Public lower As Byte
    34.         Public upper As Byte
    35.         Public modifier As Byte
    36.     End Structure
    37.  
    38.     Public Shared Function TryParse(ByVal cString As String, ByRef result As CronJob) As Boolean
    39.         Dim parsed As CronJob = CronJob.Parse(cString)
    40.         If parsed Is Nothing Then
    41.             Return False
    42.         Else
    43.             result = parsed
    44.             Return True
    45.         End If
    46.     End Function
    47.  
    48.     Public Shared Function Parse(ByVal cstring As String) As CronJob
    49.         Dim mCollect As MatchCollection = Regex.Matches(cstring, "[^""^\s]+(?=\s*)|""[^""]+""(?=\s*)", RegexOptions.Compiled)
    50.         Dim parts As String() = mCollect.Cast(Of Match)().Select(Function(m As Match) m.Value).ToArray()
    51.         Dim cronjob As CronJob = Nothing
    52.  
    53.         If parts.Length > 5 Then
    54.             cronjob = New CronJob()
    55.  
    56.             cronjob._cronstring = cstring
    57.             cronjob._minutes = cronjob.ParseTime(parts(0), 0, 59)
    58.             cronjob._hours = cronjob.ParseTime(parts(1), 0, 23)
    59.             cronjob._days = cronjob.ParseTime(parts(2), 1, 31)
    60.             cronjob._months = cronjob.ParseTime(parts(3), 1, 12)
    61.             cronjob._weekdays = cronjob.ParseTime(parts(4), 0, 7)
    62.             cronjob._command = parts(5)
    63.  
    64.             If parts.Length > 6 Then
    65.                 ReDim cronjob._args(parts.Length - 7)
    66.                 Array.Copy(parts, 6, cronjob.Args, 0, parts.Length - 6)
    67.             Else
    68.                 cronjob._args = New String() {}
    69.             End If
    70.  
    71.             If cronjob.NullFields(cronjob) Then cronjob = Nothing
    72.         End If
    73.  
    74.         Return cronjob
    75.     End Function
    76.  
    77.     Public Function IsRunnable(ByVal time As Date) As Boolean
    78.         If CronJob.NullFields(Me) Then
    79.             Return False
    80.         Else
    81.             Return (Me.Minutes(0) = CronJob.ANY OrElse Me.Minutes.Contains(time.Minute)) _
    82.                     AndAlso (Me.Hours(0) = CronJob.ANY OrElse Me.Hours.Contains(time.Hour)) _
    83.                     AndAlso (Me.Days(0) = CronJob.ANY OrElse Me.Days.Contains(time.Day)) _
    84.                     AndAlso (Me.Months(0) = CronJob.ANY OrElse Me.Months.Contains(time.Month)) _
    85.                     AndAlso (Me.Weekdays(0) = CronJob.ANY OrElse Me.Weekdays.Contains(time.DayOfWeek))
    86.         End If
    87.     End Function
    88.  
    89.  
    90.     Private Shared Function GetBoundaries(ByVal data As Match, ByVal lower As Byte, ByVal upper As Byte) As Boundary
    91.         Dim bounds As New Boundary() With {.lower = lower, .upper = upper, .modifier = 1}
    92.  
    93.         If data.Groups("mod").Success Then bounds.modifier = Byte.Parse(data.Groups("mod").Value)
    94.  
    95.         If data.Groups("any").Success AndAlso bounds.modifier = 1 Then
    96.             bounds.lower = CronJob.ANY
    97.             bounds.upper = CronJob.ANY
    98.         ElseIf data.Groups("any").Success AndAlso bounds.modifier <> 1 Then
    99.             bounds.lower = lower
    100.             bounds.upper = upper
    101.         ElseIf data.Groups("single").Success Then
    102.             bounds.upper = Byte.Parse(data.Groups("single").Value)
    103.             bounds.lower = bounds.upper
    104.         ElseIf data.Groups("lower").Success AndAlso data.Groups("upper").Success Then
    105.             bounds.lower = Byte.Parse(data.Groups("lower").Value)
    106.             bounds.upper = Byte.Parse(data.Groups("upper").Value)
    107.         Else
    108.             bounds.modifier = 0
    109.         End If
    110.  
    111.         Return bounds
    112.     End Function
    113.  
    114.     Private Shared Function ParseTime(ByVal time As String, ByVal lower As Byte, ByVal upper As Byte) As Byte()
    115.         Dim cronRegex As New Regex(CRON_PATTERN, RegexOptions.Compiled)
    116.         Dim m As Match = cronRegex.Match(time.Trim())
    117.         Dim retval As Byte() = Nothing
    118.  
    119.         If m.Success Then
    120.             If m.Groups("explicit").Success Then
    121.                 Dim parts As String() = m.Groups("explicit").Value.Split(New Char() {","c})
    122.                 Dim values As Byte() = Array.ConvertAll(Of String, Byte)(parts, Function(s As String) Byte.Parse(s))
    123.  
    124.                 If values.All(Function(b As Byte) Not (b > upper OrElse b < lower)) Then
    125.                     retval = values
    126.                 End If
    127.             Else
    128.                 Dim bounds As Boundary = CronJob.GetBoundaries(m, lower, upper)
    129.                 If Not ((bounds.modifier = 0) OrElse (bounds.upper < bounds.lower) OrElse (bounds.lower < lower) OrElse (bounds.upper > upper AndAlso bounds.upper <> CronJob.ANY)) Then
    130.  
    131.                     Dim result((bounds.upper - bounds.lower) \ bounds.modifier) As Byte
    132.                     For i As Integer = bounds.lower To bounds.upper Step bounds.modifier
    133.                         result((i - bounds.lower) \ bounds.modifier) = i
    134.                     Next i
    135.                     retval = result
    136.                 End If
    137.             End If
    138.         End If
    139.         Return retval
    140.     End Function
    141.  
    142.     Private Shared Function NullFields(ByVal cronjob As CronJob) As Boolean
    143.         Dim fields As Object() = New Object() {cronjob._args, cronjob._command, _
    144.                                                cronjob._days, cronjob._hours, _
    145.                                                cronjob._minutes, cronjob._months, _
    146.                                                cronjob._weekdays}
    147.         Return Not fields.All(Function(o) o IsNot Nothing)
    148.     End Function
    149. End Class
    150.  
    151. Partial Public Class CronJob
    152.     Public ReadOnly Property Minutes() As Byte()
    153.         Get
    154.             Return Me._minutes
    155.         End Get
    156.     End Property
    157.  
    158.     Public ReadOnly Property Hours() As Byte()
    159.         Get
    160.             Return Me._hours
    161.         End Get
    162.     End Property
    163.  
    164.     Public ReadOnly Property Days() As Byte()
    165.         Get
    166.             Return Me._days
    167.         End Get
    168.     End Property
    169.  
    170.     Public ReadOnly Property Months() As Byte()
    171.         Get
    172.             Return Me._months
    173.         End Get
    174.     End Property
    175.  
    176.     Public ReadOnly Property Weekdays() As Byte()
    177.         Get
    178.             Return Me._weekdays
    179.         End Get
    180.     End Property
    181.  
    182.     Public ReadOnly Property Command() As String
    183.         Get
    184.             Return Me._command
    185.         End Get
    186.     End Property
    187.  
    188.     Public ReadOnly Property Args() As String()
    189.         Get
    190.             Return Me._args
    191.         End Get
    192.     End Property
    193.  
    194.     Public ReadOnly Property CronString As String
    195.         Get
    196.             Return Me._cronstring
    197.         End Get
    198.     End Property
    199. End Class

    And an example of how you might use it, though this is a very simple usage.
    vb Code:
    1. 'The following cron job is set to run on weekdays between 9am-5pm every 2nd hour starting from 9am at 15 minutes past the hour.
    2. 'It will attempt to run "C:\myjob.exe" with the command line arguments {"argument1", "argument2"}
    3. Dim cJob As CronJob = CronJob.Parse("15 9-17/2 * * 1-5 ""C:\myjob.exe"" ""argument1"" ""argument2""")
    4. If cJob IsNot Nothing Then
    5.     Do
    6.         If cJob.IsRunnable(Date.Now)
    7.              Dim info As New ProcessStartInfo(cJob.Command, String.Join(" ", cJob.Args))
    8.              Dim cProcess As New Process() With { .StartInfo = info }
    9.              Try
    10.                  cProcess.Start()
    11.              Catch ex As Exception
    12.                  Debug.WriteLine("Unable to run CRON command: " + ex.Message)
    13.              End Try
    14.         End If
    15.         Threading.Thread.Sleep(60 * 950) 'sleep for approximately 1 minute, but allow 50ms for processing time.
    16.     Loop
    17. End If
    Last edited by J-Deezy; Dec 5th, 2011 at 09:28 AM.
    If I helped you out, please take the time to rate me

  2. #2

    Thread Starter
    Fanatic Member
    Join Date
    Aug 2010
    Posts
    624

    Re: CronJob class. Parsing Included.

    Any comments?
    If I helped you out, please take the time to rate me

  3. #3
    New Member
    Join Date
    Apr 2012
    Posts
    1

    Thumbs up Re: CronJob class. Parsing Included.

    Thanks for this cool and great code!

  4. #4
    New Member
    Join Date
    Jan 2023
    Posts
    1

    Re: CronJob class. Parsing Included.

    Hello. Thank you for the code. I will like to extend this to get the next occurrence(s) for a cron. If I have a cron like * 10 5 * * and I will like to know the next occurrence of the cron from today. I will appreciate any assistance on this. Thank you

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width