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