home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: SysTools / SysTools.zip / cronrgf3.zip / CRONrgf.cmd < prev    next >
OS/2 REXX Batch file  |  1996-04-30  |  39KB  |  1,115 lines

  1. /*
  2. program: cronrgf.cmd
  3. type:    REXXSAA-OS/2, OS/2 2.x
  4. purpose: Unix-like cron; allow to repeatedly execute commands at given date/time
  5.          defined in a control file
  6. version: 2.0.2
  7. date:    1992-06-10
  8. changed: 1993-03-19, bug fixed (caused no scheduling between 23:00 and 23:59),
  9.                            logging added (so you can check for yourself)
  10.          1993-11-05, added option to reread (/R) the cronfile at specified times (default every 60 minutes),
  11.                            suggested by fmerrow@nyx10.cs.du.edu (Frank Merrow);
  12.                      added option to execute all programs once since midnight (/M) and the time cronrgf was invoked,
  13.                            suggested by sktoni@uta.fi (Tommi Nieminen);
  14.                      added colorized output and option (/B) to use black and white
  15.                            instead of ANSI-colors on output
  16.          1993-11-22, added ability to look for cronfile in the directory in which cronrgf.cmd is stored,
  17.                            if no path was explicitly given; hence one can start cronrgf.cmd from
  18.                            any drive/directory without the need to specify a full path for the cronfile
  19.          1994-04-04, changed the way the log-file is handled (will be closed after it was written to, hence one can
  20.                            delete the log-file after a certain while, if one feels it has become too large)
  21.          1996-04-30, changed testing of valid date from flag "M" to "C", in order to prohibit an error message,
  22.                            if an illegal date was produced (needs datergf.cmd version 1.6 for this to be true)
  23.  
  24. author:  Rony G. Flatscher
  25.          Rony.Flatscher@wu-wien.ac.at
  26.  
  27. needs:   DATERGF.CMD, SCRCOLOR.CMD, some RxFuncts (loaded automatically)
  28.  
  29. usage:   CRONRGF [/T] [/M] [/R[nn]] [/B] cronfile
  30.  
  31.  
  32.     Reads file 'cronfile' and executes command[s] repeatedly according to it.
  33.     'cronfile' has to be formatted like in Unix; unlike the Unix-version a '%' is
  34.     treated like any other character; empty lines and ones starting with
  35.     a semi-colon (;) or a (#) are ignored.
  36.  
  37.     Switch '/T[estmode]': the cronfile is read and the user is presented with the date/times
  38.           and commands to be executed upon them. The planned and truly scheduled
  39.           times are written to the log-file.
  40.  
  41.     Switch '/M[idnight]': all commands which were scheduled the same day, starting from
  42.           midnight to the time of first invocation are executed once.
  43.  
  44.     Switch '/R[nn]': check cronfile at least every 'nn' (default 60) minutes whether it changed; if so, reread
  45.           it immediately and set the new schedule-times. If set, the cronfile will be checked for changes after
  46.           executing commands
  47.  
  48.     Switch '/B[lackAndWhite]': do not colorize output strings (e.g. for usage in more, less etc.).
  49.           This switch merely suppresses the ANSI-color-sequences attached to the
  50.           strings.
  51.  
  52.     example:  CRONRGF /TEST testcron
  53.               execute statements in file 'testcron' in testmode
  54.  
  55.     example for a control-file:
  56.  
  57.         ; Sample file for CRONRGF.CMD
  58.         ;
  59.         ; This is a comment (starts with a semicolumn)
  60.         # This is a comment too (starts with the Unix-comment symbol)
  61.         ; empty lines are ignored too...
  62.  
  63.  
  64.         ; LAYOUT OF THE CRON-FILE:
  65.         ;    * * * * * command
  66.         ; or
  67.         ;    minute hour day month weekday command
  68.         ; where minute  ranges from 0-59,
  69.         ;       hour    ranges from 0-23,
  70.         ;       day     ranges from 1-31,
  71.         ;       month   ranges from 1-12,
  72.         ;       weekday ranges from 1-7 (1 = Monday, 2 = Tuesday, ..., 7 = Sunday)
  73.         ;
  74.         ; you can give a list of values, separated by a comma (,), e.g. "1,3,7"
  75.         ; you can give a range of values, separated by a dash (-), e.g. "1-5"
  76.         ; you can give a star (*) instead of a value, meaning entire range of all valid values
  77.         ;
  78.         ; the given command is only executed when all criteriae are fullfilled !
  79.         ;
  80.         ; restriction: unlike to Unix, the percent-sign (%) is treated like any other character and
  81.         ;              not as a new-line
  82.         ;
  83.  
  84.         # the following command "@ECHO HI, I am Nr. 1 to be echoed every minute" would be
  85.         # executed every minute
  86.         *  *  *  *  *  @ECHO Hi, I am Nr. 1 to be echoed every minute & pause
  87.  
  88.         59 23 31 12 5 command, one minute before year's end, and only if the last day is a Friday
  89.  
  90.         ; comment: every year at 17:45 on June 7th:
  91.         45 17  7  6  *  dir c:\*.exe
  92.  
  93.         ; comment: on every full quarter of an hour
  94.         ;          at midnight, 6 in the morning, noon, 6 in the evening
  95.         ;          on the 1st, 15th and 31st of
  96.         ;          every month on
  97.         0,15,30,45   0,6,12,18   1,15,31   *   *   backup c:\*.* d:\ /s
  98.  
  99.         ; at noon on every day, if it is a weekday (Mo-Fri):
  100.         0 12 * * 1-5 XCOPY Q:\* D:\ /s
  101.  
  102.         ; every minute in January, March, May, July, September and November:
  103.         *  *  *  1,3,5,7,9,11  *  dir c:\*.cmd
  104.  
  105.         # at the last day of the year at 23:01, 23:02, 23:03, 23:05, 23:20, 23:21,
  106.         # 23:22, 23:23, 23:24, 23:25, 23:30, 23:31, 23:32, 23:33, 23:34, 23:35,
  107.         # 23:59
  108.         1,2,3,5,20-25,30-35,59   23   31   12   *   COPY D:\*.log E:\backup
  109.  
  110.         ; make backups of OS2.INI and OS2SYS.INI on every first monday of a month,
  111.         ; at 9 o'clock in the morning
  112.         0 9 1-7 * 1 showini /bt d:\os2\os2.ini
  113.         0 9 1-7 * 1 showini /bt d:\os2\os2sys.ini
  114.  
  115.         ; at midnight on every month:
  116.         0 0 1 * * tapebackup /all
  117.  
  118.         ; execute every minute, no restrictions:
  119.         *  *  *  *  *  @ECHO Hi, I am Nr. 2 to be echoed every minute & pause
  120.  
  121.         # execute every minute in January, February, March only !
  122.         * *  *  1,2,3  *  any-command any-arguments
  123.  
  124.         # execute every day at midnight
  125.         0 0 * * * any-command any-arguments
  126.  
  127.         # execute every wednesday at midnigth !
  128.         0 0 * * 3 any-command any-arguments
  129.  
  130.         # this is a comment which concludes the sample file ===========================
  131.  
  132. All rights reserved, copyrighted 1992, no guarantee that it works without
  133. errors, etc. etc.
  134.  
  135. donated to the public domain granted that you are not charging anything (money
  136. etc.) for it and derivates based upon it, as you did not write it,
  137. etc. if that holds you may bundle it with commercial programs too
  138.  
  139. you may freely distribute this program, granted that no changes are made
  140. to it
  141.  
  142. Please, if you find an error, post me a message describing it, I will
  143. try to fix and rerelease it to the net.
  144.  
  145. */
  146. SIGNAL ON HALT
  147. SIGNAL ON ERROR
  148. SIGNAL ON FAILURE    NAME ERROR
  149. SIGNAL ON NOTREADY   NAME ERROR
  150. SIGNAL ON NOVALUE    NAME ERROR
  151. SIGNAL ON SYNTAX     NAME ERROR
  152.  
  153. global. = ""                    /* default for global */
  154. global.eTestmode = "0"          /* default: no testmode */
  155. global.eFirstInvocation = 0     /* don't execute commands since midnight up to invocation time */
  156. global.eCheckCronFile = 0       /* don't check whether cronfile was changed */
  157.  
  158. stemSchedule. = ""      /* default for empty array elements */
  159.  
  160. pos = POS("/B", TRANSLATE(ARG(1)))
  161.  
  162. IF pos > 0 THEN         /* no screen-colors */
  163. DO
  164.    arg_1 = SUBSTR(ARG(1), 1, pos-1)
  165.  
  166.    /* ignore characters up to next blank or slash */
  167.    DO i = pos + 1 TO LENGTH(ARG(1))
  168.       IF VERIFY(SUBSTR(ARG(1), i, 1), "/ ") = 0 THEN LEAVE
  169.    END
  170.  
  171.    arg_1 = arg_1 || SUBSTR(ARG(1), i)
  172. END
  173. ELSE
  174. DO
  175.    /* get screen-colors */
  176.    PARSE VALUE ScrColor() WITH global.eScrNorm    global.eScrInv,
  177.                                global.eTxtNorm    global.eTxtInf,
  178.                                global.eTxtHi      global.eTxtAla,
  179.                                global.eTxtNormInv global.eTxtInfInv,
  180.                                global.eTxtHiInv   global.eTxtAlaInv .
  181.    arg_1 = ARG(1)
  182. END
  183.  
  184. IF arg_1 = "" THEN SIGNAL usage
  185.  
  186. /* three arguments ? */
  187. PARSE VAR arg_1 "/"switch1 "/"switch2 "/"switch3 filein
  188.  
  189. IF filein = "" THEN
  190.    PARSE VAR arg_1 filein "/"switch1 "/"switch2 "/"switch3
  191.  
  192.  
  193. /* two arguments ? */
  194. IF filein = "" THEN
  195.    PARSE VAR arg_1 "/"switch1 "/"switch2 filein
  196.  
  197. IF filein = "" THEN
  198.    PARSE VAR arg_1 filein "/"switch1 "/"switch2
  199.  
  200.  
  201. /* one argument ? */
  202. IF filein = "" THEN
  203.    PARSE VAR arg_1 "/"switch1 filein
  204.  
  205. IF filein = "" THEN
  206.    PARSE VAR arg_1 filein "/"switch1
  207.  
  208.  
  209. /* no argument */
  210. IF filein = "" THEN
  211.    PARSE VAR arg_1 filein                 /* get filename */
  212.  
  213. IF filein = "" | filein = "?" THEN
  214.    SIGNAL usage
  215.  
  216. switches = TRANSLATE(LEFT(switch1, 1) || LEFT(switch2, 1) || LEFT(switch3, 1))
  217.  
  218. /* check whether switches are valid */
  219. IF VERIFY(switches, "TMR ") <> 0 THEN
  220. DO
  221.    CALL say_c global.eTxtAla || "CRONRGF: unknown switch in [" || global.eTxtHi || arg_1 || global.eTxtAla || "]."
  222.    CALL say_c
  223.    CALL BEEP 2500, 100
  224.    SIGNAL usage              /* wrong switch */
  225. END
  226.  
  227. switch_text = ""
  228.  
  229. IF POS("T", switches) > 0 THEN
  230. DO
  231.    global.eTestmode = "1"
  232.    switch_text = switch_text "/T"
  233. END
  234.  
  235. IF POS("M", switches) > 0 THEN
  236. DO
  237.    global.eFirstInvocation = 1
  238.    switch_text = switch_text "/M"
  239. END
  240.  
  241. IF POS("R", switches) > 0 THEN
  242. DO
  243.    pos = POS("R", switches)
  244.    SELECT
  245.       WHEN pos = 1 THEN tmp = switch1
  246.       WHEN pos = 2 THEN tmp = switch2
  247.       OTHERWISE tmp = switch3
  248.    END
  249.  
  250.    PARSE UPPER VAR tmp "R" minutes
  251.  
  252.    IF minutes = "" THEN minutes = 60    /* default to 60 minutes */
  253.    ELSE IF \DATATYPE(minutes, "N") | minutes < 5 | minutes > 43200 THEN
  254.    DO
  255.       CALL say_c  global.eTxtAla || "CRONRGF: wrong minute-value in switch [/" || global.eTxtHi || tmp || global.eTxtAla || "]."
  256.       CALL say_c  global.eTxtAla || "         (valid range 5-43200 minutes = 5 minutes to 30 days)."
  257.       CALL say_c
  258.       CALL BEEP 2500, 100
  259.       SIGNAL usage              /* wrong switch */
  260.    END
  261.    switch_text = switch_text "/R" || minutes
  262.    global.eMinutesToSleep = minutes
  263.    global.eSecondsToSleep = minutes * 60
  264.    global.eCheckCronFile = 1
  265. END
  266.  
  267. /* construct name of LOG-file */
  268. PARSE SOURCE . . full_path_of_this_procedure
  269. global.eLogFile = SUBSTR(full_path_of_this_procedure, 1,,
  270.                          LASTPOS(".", full_path_of_this_procedure)) || "LOG"
  271.  
  272. filein = STRIP(filein)          /* get rid of leading and trailing spaces */
  273. global.eFilein = STREAM(filein, "C", "QUERY EXISTS")
  274.  
  275. IF global.eFilein = "" THEN
  276. DO
  277.    /* supply drive & path of cronrgf.cmd, if cronfile does not have any */
  278.    IF FILESPEC("Drive", filein) = "" & FILESPEC("Path", filein) = "" THEN
  279.    DO
  280.       PARSE SOURCE . . this
  281.       filein = FILESPEC("Drive", this) || FILESPEC("Path", this) || filein
  282.       global.eFilein = STREAM(filein, "C", "QUERY EXISTS")
  283.    END
  284.  
  285.    IF global.eFilein = "" THEN          /* still no cronfile ? */
  286.       CALL stop_it "cronfile [" || filein || "] does not exist."
  287. END
  288.  
  289. /* check whether RxFuncs are loaded, if not, load them */
  290. IF RxFuncQuery('SysLoadFuncs') THEN
  291. DO
  292.     /* load the load-function */
  293.     CALL RxFuncAdd 'SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs'
  294.  
  295.     /* load the Sys* utilities */
  296.     CALL SysLoadFuncs
  297. END
  298.  
  299. CALL WRITE_LOG LEFT("", 80, "-")
  300. CALL WRITE_LOG "(Logging-session started:" date("S") time() || ")"
  301. CALL WRITE_LOG "  (Switches in effect) [" || STRIP(switch_text) || "]"
  302.  
  303. CALL read_cronfile     /* read given CRON-file */
  304.  
  305. IF global.0 > 0 THEN
  306.    CALL dispatch                                /* start dispatching */
  307.  
  308. CALL stop_it "Nothing to schedule. Program ended."
  309.  
  310.  
  311.  
  312.  
  313. /*
  314.         FOREVER-LOOP
  315. */
  316. DISPATCH: PROCEDURE EXPOSE global. stemSchedule.
  317.  
  318.    IF global.eCheckCronFile THEN
  319.       sleeping_time = global.eSecondsToSleep
  320.    ELSE                        /* take care of DosSleep()-unsigned long; to be safe just sleep a maximum */
  321.       sleeping_time = 2678400  /* of 31 days (= 31 * 24 * 60 * 60  == 2.678.400 seconds) at a time       */
  322.  
  323.  
  324.  
  325.    DO forever_loop = 1 TO 5
  326.       forever_loop = 1          /* don't let the forever-loop expire */
  327.       CALL say_c RIGHT("", 79, "=")
  328.       CALL say_c
  329.       CALL say_c "Scheduling commands given in cronfile:"      /* show user which file is being used */
  330.       CALL say_c
  331.  
  332.       CALL say_c "  [" || global.eTxtHi || global.eFilein || global.eTxtInf || "]"
  333.  
  334.       IF global.eCheckCronFile THEN
  335.          CALL say_c "  (being checked after executing commands, at least every"  global.eTxtHi || global.eMinutesToSleep  global.eTxtInf || "minutes)"
  336.  
  337.       IF global.eFirstInvocation THEN
  338.          CALL say_c "  (executing once all commands between midnight and now, switch:" global.eTxtHi ||  "/M" || global.eTxtInf || ")"
  339.  
  340.       CALL say_c
  341.  
  342.       CALL schedule_next
  343.       /* show user which command(s) will be executed when */
  344.       PARSE VAR stemSchedule.1 1 next_date_time 18      /* get next date/time */
  345.  
  346.       /* get actual DATE/TIME */
  347.       act_date_time = DATE("S") TIME()
  348.  
  349.       IF global.eTestmode THEN
  350.       DO
  351.          IF \global.eFirstInvocation THEN
  352.              act_date_time = next_date_time
  353.  
  354.          CALL say_c "command[s] being scheduled on:"  global.eTxtHi || act_date_time
  355.       END
  356.       ELSE
  357.          CALL say_c "command[s] being scheduled on:"  global.eTxtHi || next_date_time
  358.  
  359.       CALL say_c
  360.  
  361.       DO i = 1 TO stemSchedule.0
  362.          PARSE VAR stemSchedule.i 1 tmp_date_time 18 status index
  363.  
  364.          IF tmp_date_time > next_date_time THEN LEAVE
  365.  
  366.          IF status = "OK" THEN
  367.          DO
  368.             CALL say_c "  [" || global.eTxtHi || VALUE("global." || index || ".eCommand.eValues") || global.eTxtInf || "]"
  369.             IF global.eTestmode THEN           /* write info to LOG-File */
  370.             DO
  371.                CALL WRITE_LOG "  (Testmode)" act_date_time "(scheduled for" next_date_time ||,
  372.                                              ") [" || VALUE("global." || index || ".eCommand.eValues") || "]"
  373.             END
  374.          END
  375.       END
  376.       CALL say_c
  377.  
  378.       /* get actual DATE/TIME */
  379.       difference = DATERGF(next_date_time, "-S", act_date_time)
  380.  
  381.       IF difference > 0 THEN seconds_to_sleep = DATERGF(difference, "SEC") % 1
  382.                         ELSE seconds_to_sleep = 0
  383.  
  384.       IF global.eTestmode THEN
  385.       DO
  386.          IF \global.eFirstInvocation THEN
  387.             act_date_time = next_date_time
  388.  
  389.          CALL say_c RIGHT("", 79, "=")
  390.          CALL say_c "Testmode (dispatch):    next_invocation ="  global.eTxtHi || next_date_time
  391.          CALL say_c "Testmode (dispatch):   actual date/time ="  global.eTxtHi || act_date_time
  392.          IF difference < 0 THEN
  393.             CALL say_c "Testmode (dispatch):                     " global.eTxtHi || "Immediate !"
  394.          ELSE
  395.             CALL say_c "Testmode (dispatch): difference in days =" global.eTxtHi || difference,
  396.                        global.eTxtInf || "=" global.eTxtHi || seconds_to_sleep  global.eTxtInf || "seconds"
  397.  
  398.          CALL say_c RIGHT("", 79, "=")
  399.  
  400.          CALL say_c "Testmode (dispatch):"
  401.          CALL say_c "   input:"
  402.          DO i = 1 TO global.0
  403.             CALL say_c "     [" || global.eTxtHi || RIGHT(i, LENGTH(global.0)) || global.eTxtInf || "]" global.original.i
  404.          END
  405.          CALL say_c RIGHT("", 79, "=")
  406.  
  407.          CALL say_c "   schedule list:"
  408.          DO i = 1 TO stemSchedule.0
  409.             PARSE VAR stemSchedule.i . . . gIndex
  410.             CALL say_c "    " stemSchedule.i "[" || global.eTxtHi || VALUE("global." || gIndex || ".eCommand.eValues") || global.eTxtInf || "]"
  411.          END
  412.          CALL say_c RIGHT("", 79, "=")
  413.  
  414.          CALL say_c 'Press any key to continue, "q" to quit.'
  415.          CALL BEEP 500, 100
  416.          IF TRANSLATE(SysGetKey("NOECHO")) = "Q" THEN SIGNAL halt
  417.       END
  418.       ELSE
  419.       DO
  420.          DO WHILE seconds_to_sleep > 0
  421.             sleeping         = MIN(seconds_to_sleep, sleeping_time)
  422.             seconds_to_sleep = MAX(0, seconds_to_sleep - sleeping_time)
  423.  
  424.             CALL SysSleep sleeping                      /* sleep */
  425.  
  426.             /* update sleeping-time, get actual DATE/TIME */
  427.             difference = DATERGF(next_date_time, "-S", DATE("S") TIME())
  428.  
  429.             IF difference > 0 THEN seconds_to_sleep = DATERGF(difference, "SEC") % 1
  430.                               ELSE seconds_to_sleep = 0
  431.  
  432.             /* if cronfile should be checked and another sleeping-turn is coming up, then reread the cronfile */
  433.             IF global.eCheckCronFile & seconds_to_sleep > 0 THEN
  434.             DO
  435.                IF cronfile_changed() THEN
  436.                DO
  437.                   CALL do_the_cronfile_reread
  438.                   ITERATE forever_loop        /* reschedule */
  439.                END
  440.             END
  441.          END
  442.  
  443.          DO i = 1 TO stemSchedule.0
  444.            PARSE VAR stemSchedule.i 1 date_time 18 status index
  445.            IF date_time > act_date_time THEN LEAVE
  446.  
  447.            IF status = "OK" THEN
  448.            DO
  449.               /*
  450.                  start an own minimized session which closes automatically after the
  451.                  command was executed:
  452.               */
  453.               commandString = VALUE('global.' || index || '.eCommand.eValues')
  454.               title = '"CRONRGF:' date_time STRIP(commandString) '"'
  455.               ADDRESS CMD "@START" title '/C /WIN /MIN /B "' || commandString || '"'
  456.  
  457.               CALL WRITE_LOG "(dispatched)" date("S") time() "(scheduled for" date_time || ") [" || commandString || "]"
  458.            END
  459.          END
  460.       END
  461.  
  462.       global.eFirstInvocation = 0 /* programs from midnight up to invocation time were executed, stop that behavior */
  463.  
  464.       /* reread cronfile, if set and if cronfile changed or in testmode */
  465.       IF global.eCheckCronFile & (cronfile_changed() | global.eTestmode) THEN
  466.       DO
  467.          CALL do_the_cronfile_reread
  468.          ITERATE forever_loop        /* reschedule */
  469.       END
  470.  
  471.  
  472.       /* change the status of the executed programs to "NOK" */
  473.       DO i = 1 TO stemSchedule.0
  474.          PARSE VAR stemSchedule.i 1 date_time 18 status index
  475.  
  476.          IF date_time > act_date_time THEN LEAVE
  477.  
  478.          stemSchedule.i = act_date_time "NOK" index
  479.       END
  480.  
  481.    END
  482.  
  483.    RETURN
  484.  
  485.  
  486.  
  487. /*
  488.     calculate the schedule times, sort them in ascending order
  489. */
  490. SCHEDULE_NEXT: PROCEDURE EXPOSE global. stemSchedule.
  491.    /*
  492.       as long as no viable date/time to schedule was found, iterate
  493.    */
  494.    main_run = 0
  495.    DO WHILE WORD(stemSchedule.1, 3) <> "OK"
  496.       main_run = main_run + 1                           /* count loops until a valid day was found for any of the commands */
  497.  
  498.       IF global.eTestmode THEN
  499.          CALL say_c "Testmode (scheduling): main loop ="  global.eTxtHi || main_run
  500.  
  501.       IF main_run > 50 THEN
  502.       DO
  503.          CALL stop_it "SCHEDULE_NEXT(): aborting after 50 attempts to produce a valid date!"
  504.       END
  505.  
  506.       DO i = 1 TO stemSchedule.0
  507.          PARSE VAR stemSchedule.i 1 year 5 month 7 day 9 10 hour 12 13 minute 15 18 disp_status glob_index
  508.  
  509.          IF disp_status = "OK" THEN ITERATE             /* not yed scheduled */
  510.          old_year  = year
  511.          old_month = month
  512.          old_day   = day
  513.          old_hour  = hour
  514.  
  515.          /* defaults */
  516.          first_minute = WORD(global.glob_index.eMinute.eValues, 1)
  517.          first_hour   = WORD(global.glob_index.eHOur.eValues, 1)
  518.          first_day    = WORD(global.glob_index.eDay.eValues, 1)
  519.          first_month  = WORD(global.glob_index.eMonth.eValues, 1)
  520.  
  521.  
  522.          /* minute */
  523.          DO j = 1 TO global.glob_index.eMinute.0
  524.             tmp = WORD(global.glob_index.eMinute.eValues, j)
  525.             IF tmp > minute THEN LEAVE
  526.          END
  527.  
  528.          IF j > global.glob_index.eMinute.0 THEN        /* minutes to wrap around */
  529.             hour   = hour + 1
  530.          ELSE                                           /* minutes within same hour */
  531.             minute = tmp
  532.  
  533.          /* hour */
  534.          DO j = 1 TO global.glob_index.eHour.0
  535.             tmp = WORD(global.glob_index.eHour.eValues, j)
  536.             IF tmp >= hour THEN LEAVE
  537.          END
  538.          IF j > global.glob_index.eHour.0 THEN          /* hours to wrap around */
  539.             day    = day + 1
  540.          ELSE                                           /* hours within same day */
  541.             hour   = tmp
  542.  
  543.          ok = "NOK"                                     /* default: no date found yet */
  544.          run = 0
  545.          DO 50                                          /* try 50 times to produce a valid date */
  546.             run = run + 1
  547.             /* day */
  548.             DO j = 1 TO global.glob_index.eDay.0
  549.                tmp = WORD(global.glob_index.eDay.eValues, j)
  550.                IF tmp >= day THEN LEAVE
  551.             END
  552.  
  553.             IF j > global.glob_index.eDay.0 THEN        /* days to wrap around */
  554.             DO
  555.                day    = first_day
  556.                month  = month + 1
  557.             END
  558.             ELSE                                        /* days within same month */
  559.                day    = tmp
  560.  
  561.  
  562.             /* month */
  563.             DO j = 1 TO global.glob_index.eMonth.0
  564.                tmp = WORD(global.glob_index.eMonth.eValues, j)
  565.                IF tmp >= month THEN LEAVE
  566.             END
  567.  
  568.             IF j > global.glob_index.eMonth.0 THEN      /* months to wrap around */
  569.             DO
  570.                day    = first_day
  571.                month  = first_month
  572.                year   = year + 1
  573.             END
  574.             ELSE                                        /* months within same year */
  575.             DO
  576.                IF month <> tmp THEN                     /* did the month change ? */
  577.                   day = first_day
  578.  
  579.                month  = tmp
  580.             END
  581.  
  582.  
  583.             SELECT
  584.                WHEN old_year < year | old_month < month | old_day < day THEN
  585.                     next_invocation = year || month || day first_hour || ":" || first_minute || ":00"
  586.                WHEN old_hour < hour THEN
  587.                     next_invocation = year || month || day       hour || ":" || first_minute || ":00"
  588.                OTHERWISE
  589.                     next_invocation = year || month || day       hour || ":" ||       minute || ":00"
  590.             END
  591.  
  592.             IF global.eTestmode THEN
  593.                CALL say_c "Testmode (scheduling): next_invocation ="  global.eTxtHi || next_invocation DATERGF(next_invocation, "DN")
  594.  
  595.             /* check whether day-of-week is o.k. */
  596.             IF DATERGF(next_invocation, "C") = "" THEN  /* illegal date produced, e.g. 19950231 ? */
  597.             DO
  598.                day   = first_day
  599.                month = month + 1
  600.                IF month > 12 THEN
  601.                DO
  602.                  month = first_month
  603.                  year  = year + 1
  604.                END
  605.                ITERATE
  606.             END
  607.  
  608.  
  609.             next_Weekday = DATERGF(next_invocation, "DI")  /* get weekday */
  610.  
  611.             /* using POS because weekdays are in the form of 01, 02, ..., 07 */
  612.             IF POS(next_Weekday, global.glob_index.eWeekday.eValues) = 0 THEN      /* invalid weekday ? */
  613.             DO
  614.                next_invocation = DATERGF(next_invocation, "+", "1")     /* add one day to present date */
  615.                PARSE VAR next_invocation 1 year 5 month 7 day 9
  616.  
  617.                ITERATE
  618.             END
  619.  
  620.             ok = " OK"                                  /* o.k. to invoke, because valid date */
  621.             LEAVE
  622.          END
  623.  
  624.          IF global.eTestmode THEN
  625.          DO
  626.             CALL say_c "Testmode (scheduling): date/time-loop ="  global.eTxtHi || run  global.eTxtInf || "time[s]"
  627.             CALL say_c
  628.          END
  629.  
  630.  
  631.          /*
  632.             format in schedule list:
  633.             DATE TIME STATUS INDEX-INTO-GLOBAL-ARRAY
  634.          */
  635.          stemSchedule.i = next_invocation ok glob_index
  636.       END
  637.  
  638.       CALL sort_schedule_list
  639.    END
  640.  
  641.    RETURN
  642.  
  643. /*
  644.     sort the schedule list in ascending order
  645. */
  646. SORT_SCHEDULE_LIST: PROCEDURE EXPOSE global. stemSchedule.
  647.    length = 21          /* length of SUBSTR to compare, includes status */
  648.    /* define M for passes */
  649.    M = 1
  650.    DO WHILE (9 * M + 4) < stemSchedule.0
  651.       M = M * 3 + 1
  652.    END
  653.  
  654.    /* sort stem */
  655.    DO WHILE M > 0
  656.       K = stemSchedule.0 - M
  657.       DO J = 1 TO K
  658.          Q = J
  659.          DO WHILE Q > 0
  660.             L = Q + M
  661.             IF SUBSTR(stemSchedule.Q, 1, length) <= SUBSTR(stemSchedule.L, 1, length) THEN LEAVE
  662.             /* switch elements */
  663.             tmp            = stemSchedule.Q
  664.             stemSchedule.Q = stemSchedule.L
  665.             stemSchedule.L = tmp
  666.             Q = Q - M
  667.          END
  668.       END
  669.       M = M % 3
  670.    END
  671.  
  672.    RETURN
  673.  
  674.  
  675.  
  676.  
  677.  
  678. /*
  679.    analyze
  680.  
  681. */
  682.  
  683. CHECK_IT_OUT: PROCEDURE EXPOSE global. stemSchedule.
  684.  
  685. to_parse = ARG(1)
  686. line_no  = ARG(2)
  687.  
  688. PARSE VAR to_parse sMinute sHour sDay sMonth sWeekday sCommand
  689.  
  690. line_no = setup_minutes(sMinute, line_no)       /* setup minute-values */
  691.  
  692. IF line_no <> 0 THEN                            /* setup hour-values */
  693.    line_no = setup_hours(sHour, line_no)
  694.  
  695. IF line_no <> 0 THEN                            /* setup day-values */
  696.    line_no = setup_days(sDay, line_no)
  697.  
  698. IF line_no <> 0 THEN                            /* setup month-values */
  699.    line_no = setup_months(sMonth, line_no)
  700.  
  701. IF line_no <> 0 THEN                            /* setup weekday-values */
  702.    line_no = setup_weekdays(sWeekday, line_no)
  703.  
  704. IF line_no <> 0 THEN                            /* setup command-values */
  705. DO
  706.    global.line_no.eCommand.0       = 1
  707.    global.line_no.eCommand.eValues = sCommand
  708. END
  709.  
  710. RETURN line_no
  711.  
  712. /*
  713.         parse and setup minutes
  714.         ARG(1) - minute string
  715.         ARG(2) - index into global array
  716. */
  717. SETUP_MINUTES: PROCEDURE EXPOSE global. stemSchedule.
  718.    sMinute = ARG(1)
  719.    iIndex = ARG(2)
  720.    default.0 = 60
  721.    default.values = "00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19",
  722.                     "20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39",
  723.                     "40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59"
  724.  
  725.    string = parse_it("eMinute", sMinute, iIndex, 0, 59, default.0, default.values)
  726.  
  727.    IF string <> "" THEN
  728.    DO
  729.       CALL say_c  global.eTxtAla || "CRONRGF: error in minute-format" global.eTxtHi || "[" || string || "]"
  730.       CALL say_c
  731.       RETURN 0
  732.    END
  733.  
  734.    RETURN iIndex
  735.  
  736.  
  737.  
  738. /*
  739.         parse and setup hours
  740.         ARG(1) - hour string
  741.         ARG(2) - index into global array
  742. */
  743. SETUP_HOURS: PROCEDURE EXPOSE global. stemSchedule.
  744.    sHour = ARG(1)
  745.    iIndex = ARG(2)
  746.    default.0 = 24
  747.    default.values = "00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19",
  748.                     "20 21 22 23"
  749.  
  750.    string = parse_it("eHour", sHour, iIndex, 0, 23, default.0, default.values)
  751.  
  752.    IF string <> "" THEN
  753.    DO
  754.       CALL say_c  global.eTxtAla || "CRONRGF: error in hour-format" global.eTxtHi || "[" || string || "]"
  755.       CALL say_c
  756.       RETURN 0
  757.    END
  758.  
  759.    RETURN iIndex
  760.  
  761.  
  762.  
  763.  
  764. /*
  765.         parse and setup days
  766.         ARG(1) - day string
  767.         ARG(2) - index into global array
  768. */
  769. SETUP_DAYS: PROCEDURE EXPOSE global. stemSchedule.
  770.    sDay = ARG(1)
  771.    iIndex = ARG(2)
  772.    default.0 = 31
  773.    default.values = "   01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19",
  774.                     "20 21 22 23 24 25 26 27 28 29 30 31"
  775.  
  776.    string = parse_it("eDay", sDay, iIndex, 1, 31, default.0, default.values)
  777.  
  778.    IF string <> "" THEN
  779.    DO
  780.       CALL say_c  global.eTxtAla || "CRONRGF: error in day-format" global.eTxtHi || "[" || string || "]"
  781.       CALL say_c
  782.       RETURN 0
  783.    END
  784.  
  785.    RETURN iIndex
  786.  
  787.  
  788.  
  789.  
  790. /*
  791.         parse and setup months
  792.         ARG(1) - month string
  793.         ARG(2) - index into global array
  794. */
  795. SETUP_MONTHS: PROCEDURE EXPOSE global. stemSchedule.
  796.    sMonth = ARG(1)
  797.    iIndex = ARG(2)
  798.    default.0 = 12
  799.    default.values = "01 02 03 04 05 06 07 08 09 10 11 12"
  800.  
  801.    string = parse_it("eMonth", sMonth, iIndex, 1, 12, default.0, default.values)
  802.  
  803.    IF string <> "" THEN
  804.    DO
  805.       CALL say_c  global.eTxtAla || "CRONRGF: error in month-format" global.eTxtHi || "[" || string || "]"
  806.       CALL say_c
  807.       RETURN 0
  808.    END
  809.  
  810.    RETURN iIndex
  811.  
  812.  
  813.  
  814.  
  815. /*
  816.         parse and setup weekdays
  817.         ARG(1) - weekday string
  818.         ARG(2) - index into global array
  819. */
  820. SETUP_WEEKDAYS: PROCEDURE EXPOSE global. stemSchedule.
  821.    sWeekday = ARG(1)
  822.    iIndex = ARG(2)
  823.    default.0 = 7
  824.    default.values = "01 02 03 04 05 06 07"
  825.  
  826.    string = parse_it("eWeekday", sWeekday, iIndex, 1, 7, default.0, default.values)
  827.  
  828.    IF string <> "" THEN
  829.    DO
  830.       CALL say_c  global.eTxtAla || "CRONRGF: error in weekday-format" global.eTxtHi || "[" || string || "]"
  831.       CALL say_c
  832.       RETURN 0
  833.    END
  834.  
  835.    RETURN iIndex
  836.  
  837.  
  838.  
  839.  
  840.  
  841.  
  842.  
  843. /*
  844.         parse values, list, setup array
  845.         ARG(1) = eName ("element name" in array)
  846.         ARG(2) = string containing numbers
  847.         ARG(3) = index into global array
  848.         ARG(4) = lower bound (inclusive)
  849.         ARG(5) = upper bound (inclusive)
  850.         ARG(6) = default number of elements
  851.         ARG(7) = default values
  852. */
  853.  
  854. PARSE_IT: PROCEDURE EXPOSE global. stemSchedule.
  855.    eName       = ARG(1)
  856.    sValues        = ARG(2)
  857.    iIndex         = ARG(3)
  858.    lower          = ARG(4)
  859.    upper          = ARG(5)
  860.    default.0      = ARG(6)
  861.    default.values = ARG(7)
  862.  
  863.  
  864.    tmp = "global.iIndex." || eName || "."            /* build string of array-e-Name */
  865.    lastValue = 0
  866.  
  867.    IF sValues = "*" THEN                /* build all legal values */
  868.    DO
  869.       INTERPRET(tmp || "0 =" default.0)
  870.       INTERPRET(tmp || "eValues =" default.values)
  871.       RETURN ""
  872.    END
  873.  
  874.    INTERPRET(tmp || "0 = 0")                            /* set number of elements to 0 */
  875.    INTERPRET(tmp || 'eValues = ""')                     /* delete values */
  876.  
  877.    DO WHILE sValues <> ""
  878.       IF POS(",", sValues) > 0 THEN                     /* list of values ? */
  879.         PARSE VAR sValues tmpValue "," sValues
  880.       ELSE
  881.       DO
  882.         tmpValue = sValues
  883.         sValues = ""
  884.       END
  885.  
  886.       IF POS("-", tmpValue) > 0 THEN                    /* range of values ? */
  887.       DO
  888.          PARSE VAR tmpValue start "-" end
  889.       END
  890.       ELSE                                              /* single value */
  891.       DO
  892.          start = tmpValue
  893.          end   = tmpValue
  894.       END
  895.  
  896.  
  897.  
  898.       /* error in values ? */
  899.       IF start < lastValue | start < lower | start > end | ,
  900.          end > upper | ,
  901.          \DATATYPE(start, "N") | \DATATYPE(end, "N") THEN
  902.       DO
  903.          INTERPRET(tmp || '0  = ""')                    /* delete number of array elements */
  904.          INTERPRET(tmp || 'eValues = ""')               /* delete values */
  905.          SELECT
  906.             WHEN \DATATYPE(start, "N") THEN err_msg = '"' || start || '"' "is not numeric" '(part in error: "' || tmpValue || '")'
  907.             WHEN \DATATYPE(end,   "N") THEN err_msg = '"' || end || '"' "is not numeric" '(part in error: "' || tmpValue || '")'
  908.             WHEN start < lastValue     THEN err_msg = start "<" lastValue "= lower bound"  '(part in error: "' || tmpValue || '")'
  909.             WHEN start < lower         THEN err_msg = start "<" lower "= lower bound"      '(part in error: "' || tmpValue || '")'
  910.             WHEN start > end           THEN err_msg = start ">" end                        '(part in error: "' || tmpValue || '")'
  911.             WHEN end   > upper         THEN err_msg = end   ">" upper "= upper bound"      '(part in error: "' || tmpValue || '")'
  912.             OTHERWISE NOP
  913.          END
  914.  
  915.          RETURN err_msg
  916.       END
  917.  
  918.       /* build values */
  919.       DO i = start TO end
  920.          INTERPRET(tmp || "0 = " || tmp || "0 + 1")     /* increase counter for number of elements */
  921.          INTERPRET(tmp || "eValues = " || tmp || "eValues" RIGHT(i, 2, "0"))   /* add the next value */
  922.       END
  923.  
  924.       lastValue = i
  925.    END
  926.  
  927.    RETURN ""
  928.  
  929.  
  930.  
  931. /*
  932.    control-routine for rereading cronfile
  933. */
  934. DO_THE_CRONFILE_REREAD: PROCEDURE EXPOSE global. stemSchedule.
  935.    CALL say_c
  936.    CALL say_c "CRONRGF: cronfile [" || global.eTxtHi || global.eFilein || global.eTxtInf || "] changed, rereading ..."
  937.  
  938.    IF global.eTestmode THEN
  939.       tmp_leadin = "  (Testmode)"
  940.    ELSE
  941.       tmp_leadin = "            "
  942.  
  943.    CALL WRITE_LOG tmp_leadin date("S") time() "cronfile [" || global.eFilein || "] changed, rereading ..."
  944.  
  945.    CALL read_cronfile          /* read new cronfile */
  946.  
  947.    IF global.0 = 0 THEN        /* no valid entries found */
  948.       CALL stop_it "cronfile [" || global.eFilein || "] not a cronfile anymore ! Aborting ..."
  949.  
  950.    RETURN
  951.  
  952.  
  953.  
  954. /*
  955.    Read contents of CRON-file
  956. */
  957.  
  958. READ_CRONFILE: PROCEDURE EXPOSE global. stemSchedule.
  959.    DROP stemSchedule.
  960.    stemSchedule. = ""   /* default for empty array-elements */
  961.    iSchedule = 0        /* schedule counter */
  962.    line_no = 1          /* line-number */
  963.  
  964.    IF global.eFirstInvocation THEN      /* execute once from midnight to now */
  965.       date_time = DATERGF(DATE("S"), "-", DATERGF("1", "SECR")) /* Subtract 1 second from 00:00am */
  966.    ELSE
  967.       date_time = DATE("S") TIME()
  968.  
  969.    CALL SysFileTree global.eFilein, "aTmp", "F"
  970.    IF aTmp.0 <> 1 THEN
  971.    DO
  972.       CALL stop_it "control-file [" || global.eFilein || "] does not exist anymore !"
  973.    END
  974.    global.eFilein.eState = aTmp.1       /* assign present value of control-file */
  975.  
  976.    global.0 = 0
  977.    DROP stemSchedule.
  978.    stemSchedule. = ""
  979.  
  980.    DO WHILE LINES(global.eFilein) > 0
  981.       line = LINEIN(global.eFilein)
  982.  
  983.       /* no empty lines and no comments */
  984.       tmp = LEFT(STRIP(line), 1)
  985.       IF line = "" | tmp = ";" | tmp = "#" THEN ITERATE
  986.  
  987.       CALL say_c "parsing ["  global.eTxtInf || line || global.eTxtInf || "]"
  988.  
  989.       IF check_it_out(line, line_no) > 0 THEN
  990.       DO
  991.          iSchedule = iSchedule + 1
  992.          global.original.line_no = line
  993.          stemSchedule.iSchedule =  date_time "NOK" line_no  /* NOK = not o.k., get next date/time */
  994.          line_no = line_no + 1
  995.       END
  996.    END
  997.    CALL say_c
  998.  
  999.    CALL WRITE_LOG"  (cronfile)" date("S") time() "[" || STRIP(global.eFilein.eState) || "]"
  1000.  
  1001.    global.eOriginal.0 = line_no - 1     /* original number of lines */
  1002.    global.0 = iSchedule                 /* set number of array elements */
  1003.    stemSchedule.0 = iSchedule           /* set number of array elements */
  1004.    CALL STREAM global.eFilein, "C", "CLOSE"     /* make sure, file is closed */
  1005.  
  1006.    RETURN
  1007.  
  1008.  
  1009.  
  1010. /*
  1011.    check, whether control-file changed
  1012. */
  1013. CRONFILE_CHANGED: PROCEDURE EXPOSE global. stemSchedule.
  1014.    CALL SysFileTree global.eFilein, "aTmp", "F"
  1015.    IF aTmp.0 <> 1 THEN
  1016.    DO
  1017.       CALL stop_it "cronfile [" || global.eFilein || "] does not exist anymore ! Aborting ..."
  1018.    END
  1019.  
  1020.    RETURN global.eFilein.eState <> aTmp.1
  1021.  
  1022.  
  1023.  
  1024. /*
  1025.    User pressed ctl-c or closed session
  1026. */
  1027. HALT:
  1028.    CALL STOP_IT "User interrupted program."
  1029.  
  1030. /*
  1031.    Clean up and close open files
  1032. */
  1033. STOP_IT:
  1034.    IF global.eLogFile <> "" THEN
  1035.    DO
  1036.       IF ARG(1) <> "" THEN
  1037.          CALL WRITE_LOG "  (***ERROR:" date("S") time() "-"  ARG(1) || ")"
  1038.  
  1039.       CALL WRITE_LOG "(Logging-session ended:" date("S") time() || ")"
  1040.       IF LEFT(STREAM(global.eLogFile), 1) = "R" THEN
  1041.          CALL STREAM global.eLogFile, "C", "CLOSE"    /* close log-file */
  1042.    END
  1043.  
  1044.    IF ARG(1) <> "" THEN CALL say_c  global.eTxtAla || "CRONRGF:"  global.eTxtAlaInv || ARG(1)
  1045.  
  1046.    IF global.eFilein <> "" THEN
  1047.       IF LEFT(STREAM(global.eFilein), 1) = "R" THEN
  1048.          tmp = STREAM(global.eFilein, "C", "CLOSE")    /* make sure, file is closed */
  1049.  
  1050.    EXIT -1
  1051.  
  1052.  
  1053.  
  1054. USAGE:
  1055.    CALL say_c  global.eTxtHi || "CRONRGF.CMD"  global.eTxtInf || "- Unix-like cron; executes commands in file repeatedly."
  1056.    CALL say_c
  1057.    CALL say_c "usage:"
  1058.    CALL say_c
  1059.    CALL say_c  global.eTxtHi || "      CRONRGF [/T] [/M] [/R[nn]] [/B] cronfile"
  1060.    CALL say_c
  1061.    CALL say_c "Reads file '" || global.eTxtHi || "cronfile" || global.eTxtInf || "' and executes command[s] repeatedly according to it."
  1062.    CALL say_c "'cronfile' has to be formatted like in Unix; unlike the Unix-version a '%' is"
  1063.    CALL say_c "treated like any other character; empty lines and ones starting with"
  1064.    CALL say_c "a semi-colon (;) or with '#' are ignored."
  1065.    CALL say_c
  1066.    CALL say_c "Switch '" || global.eTxtHi || "/T" || global.eTxtInf || "': the cronfile is read and the user is presented with the date/times"
  1067.    CALL say_c "       and commands to be executed upon them."
  1068.    CALL say_c "Switch '" || global.eTxtHi || "/M" || global.eTxtInf || "': execute once the programs which would have been scheduled"
  1069.    CALL say_c "       between midnight and time of invocation."
  1070.    CALL say_c "Switch '" || global.eTxtHi || "/R[nn]" || global.eTxtInf || "': check cronfile at least every 'nn' (default 60) minutes"
  1071.    CALL say_c "       whether it changed; if so, reread it immediately and set the new"
  1072.    CALL say_c "       schedule-times. If given, the cronfile will be checked after dispatching"
  1073.    CALL say_c "       commands. If not given, cronfile will be read only once."
  1074.    CALL say_c "Switch '" || global.eTxtHi || "/B" || global.eTxtInf || "': do not colorize output strings (e.g. for usage in more, less"
  1075.    CALL say_c "       etc.). This switch merely suppresses the ANSI-color-sequences attached"
  1076.    CALL say_c "       to the strings (hence BlackAndWhite)."
  1077.    CALL say_c
  1078.    CALL say_c "examples:" global.eTxtHi || "CRONRGF /T crontest"
  1079.    CALL say_c "          .. execute statements in file 'crontest' in testmode"
  1080.    CALL say_c  global.eTxtHi || "          CRONRGF /T /M /R30 cronfile"
  1081.    CALL say_c "          .. execute statements in file 'cronfile' in testmode, schedule"
  1082.    CALL say_c "             programs which would have been started since midnight and time"
  1083.    CALL say_c "             of invocation, reread 'cronfile' at least every 30 minutes."
  1084.    CALL say_c  global.eTxtHi || "          CRONRGF /M /R some_file"
  1085.    CALL say_c "          .. execute statements in file 'some_file', schedule programs which"
  1086.    CALL say_c "             would have been started since midnight and time of invocation,"
  1087.    CALL say_c "             reread 'some_file' at least every 60 minutes."
  1088.    CALL say_c  global.eTxtHi || "          CRONRGF /B"
  1089.    CALL say_c "          .. show usage of CRONRGF without colors"
  1090.  
  1091.    EXIT 0
  1092.  
  1093. /*
  1094.    write to logfile & close logfile thereafter
  1095. */
  1096. WRITE_LOG: PROCEDURE EXPOSE global.
  1097.    CALL LINEOUT global.eLogFile, ARG(1)         /* write log-entry */
  1098.    CALL STREAM global.eLogFile, "C", "CLOSE"    /* close logfile */
  1099.    RETURN
  1100.  
  1101.  
  1102. SAY_C: PROCEDURE EXPOSE global. stemSchedule.
  1103.    SAY global.eTxtInf || ARG(1) || global.eScrNorm
  1104.    RETURN
  1105.  
  1106.  
  1107. ERROR:
  1108.    myrc        = RC
  1109.    errorlineno = SIGL
  1110.    errortext   = ERRORTEXT(myrc)
  1111.    errortype   = CONDITION("C")
  1112.    problem     = "Cause: Probably caused by a user-interrupt while in INTERPRET-statement."
  1113.    CALL stop_it myrc":" errortext "in line # ["errorlineno"] REXX-SIGNAL: ["errortype"] ===>" problem
  1114.  
  1115.