cron and Tcl

People often ask in comp.lang.tcl about problems they're having with their Tcl-coded cron applications. Almost without exception, the problems have nothing to do with cron (in the usual sense programmers mean that). However, until one of us can locate a good online introduction to cron and its maladies, we'll record here the things a newcomer should know.


First, of course, is to decompose the problem. If you have a cron application that's misbehaving, how does the same program act outside cron? Answering that question typically is the most valuable first step in diagnosis.


effective user


env vars

Remember that cron provides to the application a SEVERELY limited environment. You might, for fun's sake, add something like:

0 8 * * * /bin/env

to your crontab . This should , in most cases, result in you receiving an email with the environment variables that cron sets for the average app.

Why should this matter? Because many apps depend on things like $HOME, $PATH, $DISPLAY, $LD_LIBRARY_PATH, $TCLLIB, or other environment variables none of which are set by cron. In fact, I have seen people do things like invoke /etc/profile (or some similar file for other shells) as well as a specific home/.profile type file within a wrapper shell before running their c code.


#!-non-portabilities


Trying to run X apps from within cron

Marty Backe - Running a wish application from a cronjob simply requires that the display be specified, since the DISPLAY environmental variable doesn't exist. Therefore, your cronjob might look something like this:

    5 12 * * * /usr/local/bin/wish8.3 -display localhost:0 /home/smith/foobar.tcl

Also potentially useful:

    set ::env(XAUTHORITY) $::env(HOME)/.Xauthority

or, with wish as here, more likely

    export XAUTHORITY=$HOME/.Xauthority

#!-32 character limit (is it 32? for which Unixes?) (what about when symlinked? Where on the Web is this documented?

At least one place that #! is documented, on Solaris, is exec(2). What the doc discusses is general terms under errors returned by exec (which is ultimately what is being invoked to get to the #! ...)

ENAMETOOLONG
The length of the file or path argument exceeds PATH_MAX, or the length of a file or path component exceeds {NAME_MAX} while {_POSIX_NO_TRUNC} is in effect.

(along with quite a few other errors). Of course, it depends on what headers, etc. are included what the value of these are. However, frankly, this value appears to be 1024 on Solaris. On other systems, the values will likely be different - and in fact, there may be limits on #! other than this , depending on the OS.

glennj: specifically, #! is discussed in execve(2). The man page on my HP-UX at work states:

The initial line of a script file must begin with #! as the first two bytes, followed by zero or more spaces, followed by interpreter or interpreter argument, as in:
#! interpreter argument
...
When the script file is executed, the system executes the specified interpreter as an executable object file. Even in the case of execlp() or execvp(), no path searching is done of the interpreter name.
...
If the initial line of the script file exceeds a system-defined maximum number of characters, exec*() fails. The minimum value for this limit is 32.

I vaguely remember that the 32 character limit held for HP-UX 9. I suspect that limit is now obsolete.

dizzy For Linux (kernel 2.6) the limit is 125 characters.


[Point to a description (in the FAQ?) of the telnet bug in case TERM is undefined, unrecognized, ...]


Arjen Markus Another possibility that is quite straightforward and as portable as Tcl itself, is to use a Tcl script that schedules the jobs that need to be done. This is easier than it seems - you just have to look for the next job in the list of jobs, determine when this is due and sleep until that time has come.

The advantages are legion: you have all the facilities you need (such as environment variables and a working directory) and a well-known programming environment. The only drawbacks: your jobs probably do not have the same rights as a true cron job and you have to start the controlling script yourself.

Some time ago, I wrote just such a controlling script. I will put it on the Internet and announce it.

Arjen Markus See: [L1 ] and look for the application "cronjob"


RS You might start from this (from after page): Here's a sugaring for after where you specify absolute time, like for a scheduler:

 proc at {time args} {
   if {[llength $args]==1} {set args [lindex $args 0]}
   set dt [expr {([clock scan $time]-[clock seconds])*1000}]
   after $dt $args
 } ;# RS
 at 9:31 puts Hello
 at 9:32 {puts "Hello again!"}

MHo Things like "at next saturday 10:00" are required sometimes... My version is this:

 proc atNext {time fmt cmd} {
      return [after [expr {([clock scan $time -format $fmt]-[clock seconds])*1000}] $cmd]
 }
 # Example:
 puts [atNext {Saturday 10:00} {%A %H:%M} {puts "Hallo Welt"}]
 vwait forever

Things like "every sunday..." should also be possible with combinations of at and every.... But it may not work, depending on when exactly the event is scheduled... the expr may yield a negative result and the event is executed immediately.... Hm....

 proc cron { when action } {
    switch -regexp -matchvar matches $when {
        {^ *Sun|Mon|Tue|Wed|Thu|Fri|Sat +at +[0-9][0-9]:[0-9][0-9] *$} {
            cron:at $when {%A at %H:%M} $action [milliseconds 7d]
        }   
        {^ *[0-9][0-9]:[0-9][0-9 *]$} {
            cron:at $when {%H:%M} $action [milliseconds 1d]
        }
        {^ *every +hour +at +([0-9][0-9]?) +minutes.*$} {
            lassign $matches -> minute
            cron:every_interval 3600 [expr {$minute * 60}] $action
        }
        {^ *every +minute +at +([0-9][0-9]?) +seconds.*$} {
            lassign $matches -> second
            cron:every_interval 60 $second $action
        }
        {.*} {
            error "No match for cron request $when"
        }
    }
 }

 proc milliseconds { { time -0 } { now 0 } { scale 1 } } {

    set op [string index $time 0]
    if { $op eq "+" || $op eq "-" } {
        set now [clock milliseconds]
    }

    set time [string map {
        s *1000
        m *60*1000
        h *3600*1000 
        d *60*60*24*1000 
        w *60*60*24*7*1000
        t *60*60*24*30*1000 
        y *60*60*24*365*1000
    } $time]

    return [expr "($time + $now) * $scale"]
 }
 proc truncate_timestamp {timestamp interval} {
    return [expr {$timestamp - ($timestamp % $interval)}]
 }           
 proc cron:now {} {
    return [clock seconds]
 }       
 proc cron:at { time fmt cmd period } {
    set next [expr {int([clock scan $time -format $fmt]-[cron:now])*1000}]
    if { $next <= 0 } {
        set next [expr { int($period + $next) }]
    }
    return [after $next "try { $cmd }; after 1000; after idle [namespace code [info level 0]]"]
 }
 proc cron:every_interval { interval offset cmd } {
    set current_time [cron:now]
    set next [expr {[truncate_timestamp $current_time $interval] + $offset}]

    while {$next <= $current_time} {
        set next [expr {$next + $interval}]
    }

    set delay [expr {($next - $current_time) * 1000}]

    return [after $delay "try { $cmd }; after 1000; after idle [namespace code [list cron:every_interval $interval $offset $cmd]]"]
 }