UNIX only exec wrapper

How to fork an external program and get back its stdout and stderr without blocking and WITH a timeout!

 proc _exec { prog { var "" } { uniq "" } { i 0 } { to 5 } } {
     
     if { ! [ regexp {^file\d+$} $prog ] } {
        set uniq _exec-[ clock seconds ][ clock clicks ]
        set tmpfile /tmp/$uniq
        set tmpvar  ::$uniq
        set $tmpvar [ list ]
        if { [ catch {
           set fid [ open "|/usr/bin/env LD_PRELOAD= $prog 2>$tmpfile" ]
           fconfigure $fid -blocking off
           fconfigure $fid -buffersize 50000
           fconfigure $fid -buffering full
        } err ] } {
           file delete -force $tmpfile
           return -code error "_exec: $err"
        }
     } else {
        set fid $prog
        set tmpfile /tmp/$uniq
        set tmpvar  ::$uniq
     }
     
     set ret [ read $fid ]
    
     if { ($i / 10) >= $to } {
        if { ! [ string length $ret ] } {
           set ret timed_out
        }
     }
    
     ;## first condition is because we only ever were
     ;## reading 4096 chars ever. bizarre.
     if { [ string length $ret ] && \
        ! [ file size $tmpfile ] && \
        ! [ eof $fid ] } {
        append $tmpvar $ret
        incr i
        if { $to <= 10 } { set to 1 }
        after 100 [ list _exec $fid $var $uniq $i $to ]

     } elseif { [ string length $ret ] || \
                [ file size $tmpfile ] || \
                [ string length [ set $tmpvar ] ] } {
        set ret [ set $tmpvar ]$ret
        set pids [ pid $fid ]
        
        catch { ::close $fid }
        catch { ::unset $tmpvar }
        foreach pid $pids {
           catch { ::exec kill -KILL $pid }
        }
        set fid [ open $tmpfile r ]
        set err [ read $fid [ file size $tmpfile ] ]
        catch { ::close $fid }
        catch { ::unset $tmpvar }
        file delete -force $tmpfile
        if { [ string length $var ] } {
           set ::$var [ list $ret $err ]
        }   
     } else {
        incr i
        after 100 [ list _exec $fid $var $uniq $i ]
     }
 }

Usage example:

 # of course, you really want to trace the variable,
 # not vwait on it!
 _exec "curl http://www.ligo.caltech.edu" foo
 vwait ::foo
 puts $::foo

This seems a little heavy handed in that as soon as any output (either stdout or stderr) is available then the program is killed. I would think that using a filevent handler might be a little more robust (and you could still have a timeout that DID kill it. - bbh


You're right. I made it a bit less heavy handed... specifically to deal with "ps", which seems to write its output in 4k buffers. Stderr handling could maybe do with a bit more. Anyway, it's supposed to be heavyhanded first, and as it develops bugs I'll keep coming back and ease it up a bit...