chan pipe

Synopsis

package require Tcl 8.6
lassign [chan pipe] readChanId writeChanId
...
close $readChanId; close $writeChanId

Description

Creates a standalone anonymous operating system pipe and returns a pair of channel handles for its read and write ends, in this order.

The most useful way to use such pipes is to redirect standard channel(s) of a process to be run using [exec ... &] in which case it becomes possible to close the stdin of the spawned process forcing it to detect EOF and exit while collecting what it writes on its stdout/stderr. For instance, the tclgpg extension makes heavy use of this technique to control the gpg binary being executed.

This subcommand was introduced by TIP #304 [1 ] and is mostly a rip of the pipe command implementation from Tclx.

Availability

The implementation is available in Tcl ≥ 8.6.

kostix has created a TEA-based extension out of this implementation called tclpipe. It can be used with (supposedly) any version of Tcl (tested on 8.4 and 8.5).

Capture stderr of and open command pipeline

AMG: [chan pipe] can be used to access the stderr of an [open |] pipeline.

lassign [chan pipe] rderr wrerr
set stdio [open |[concat $command [list 2>@ $wrerr]] a+]
# write to $stdio -> pipeline's stdin
# read from $stdio <- pipeline's stdout
# read from $rderr <- pipeline's stderr

PYK 2015-04-13: I think it's also necessary to close $wrerr so that EOF happens on $rderr when the executed command finishes:

lassign [chan pipe] rderr wrerr
set stdio [open |[concat $command [list 2>@ $wrerr]] a+]
close $wrerr
# write to $stdio -> pipeline's stdin
# read from $stdio <- pipeline's stdout
# read from $rderr <- pipeline's stderr
close $rderr

PYK 2015-04-27: Also, In order to avoid having the $wrerr buffer get full and block, $wrerr should be set to nonblocking, in which case Tcl buffers as much data as needed for $wrerr:

lassign [chan pipe] rderr wrerr
chan configure $wrerr -blocking 0
set stdio [open |[concat $command [list 2>@ $wrerr]] a+]
close $wrerr
# write to $stdio -> pipeline's stdin
# read from $stdio <- pipeline's stdout
# read from $rderr <- pipeline's stderr
close $rderr

For this to work robustly, $stdio must be read and closed before attempting to read $rderr. Otherwise, reading too much from $rderr could cause it to block if the executed program meanwhile has to block in order to push more data to stdout. If there were some way in Tcl to close the write side of $stdio, it could be set to non-blocking so that data coming in from the executed program would be buffered as necessary. That effect can be achieved with ycl::chan::tplex.

APN 2016-11-22: I'm confused by the above snippet. The setting of $wrerr to non-blocking happens in the parent process, not the child that is actually writing to wrerr. So what effect does setting $wrerr to non-blocking have? That configuration is not passed down to the child is it? Which may not even be a Tcl process so I don't understand the comment "Tcl buffers as much data as needed".

Should This Work?

PYK 2015-04-18: The following code hangs at read $pr1. Is it coded wrong, is it a bug, or is it a "don't do that" scenario?

#! /bin/env tclsh
package require Thread

lassign [chan pipe] pr1 pw1
lassign [chan pipe] pr2 pw2

set tid [thread::create]

thread::transfer $tid $pw1
thread::transfer $tid $pw2
thread::send $tid [
    list exec [info nameofexecutable] << {
        for {set i 0} {$i < 10000} {incr i} {
            puts hello
            puts stderr goodbye
        }
    } >@$pw1 2>@$pw2 &
]
thread::send $tid [list close $pw1]
thread::send $tid [list close $pw2]
puts [list reading $pr1]
set out [read $pr1]
puts done
puts [string length $out]
close $pr1

set errout [read $pr2]
puts [string length $errout]
close $pr2

DKF: The problem is that the puts stderr is in the loop, which fills up that pipe and stops things from working. It's important to use asynchronous processing (which Tcl is good at!) when working with multiple pipes so that neither blocks.

Try this version instead:

package require Thread

lassign [chan pipe] pr1 pw1
lassign [chan pipe] pr2 pw2

set tid [thread::create]

thread::transfer $tid $pw1
thread::transfer $tid $pw2
thread::send $tid {
    proc doStuff {out err} {
        exec [info nameofexecutable] << {
            for {set i 0} {$i < 10000} {incr i} {
                puts hello
                puts stderr goodbye
            }
        } >@ $out 2>@ $err &
        close $out
        close $err
    }
}
thread::send $tid [list doStuff $pw1 $pw2]
fileevent $pr1 readable "readpipe $pr1 $pr2"
fconfigure $pr1 -blocking 0
fileevent $pr2 readable "readpipe $pr2 $pr1"
fconfigure $pr2 -blocking 0
proc readpipe {p1 p2} {
    append ::out($p1) [read $p1]
    if {[eof $p1]} {
        close $p1
        if {$p2 ni [chan names]} {
            set ::done ok
        }
    }
}
puts [list reading $pr1]
vwait done
puts [string length $out($pr1)]
set errout $out($pr2)
puts [string length $errout]

Thread Notification

PYK 2015-04-25: thread already has thread::cond, but chan pipe provides another convenient notification mehanism between threads. Here's a skeleton example:

#! /bin/env tclsh

package require Thread

lassign [chan pipe] pr pw

set tid [thread::create]
thread::transfer $tid $pw
thread::send $tid [list variable pw $pw]
thread::send $tid [list lappend notify_condition $pw]
thread::send -async $tid {
        apply {{} {
                variable notify_condition
                variable pw
                after 5000
                #do some real work
                foreach chan $notify_condition {
                        close $chan
                }
        }}
    thread::release
}

set data [read $pr]
close $pr
puts {condition occured}

An asynchronous signaler is only a little more work:

#! /bin/env tclsh

package require Thread

lassign [chan pipe] pr pw

set tid [thread::create]
thread::transfer $tid $pw
thread::send $tid [list variable pw $pw]
thread::send $tid [list lappend notify_condition $pw]
thread::send -async $tid {
        apply {{} {
                variable notify_condition
                variable pw
                after 5000
                #do some real work
                foreach chan $notify_condition {
                        close $chan
                }
        }}
    thread::release
}

chan event $pr readable [list apply [list pr {
        if {[eof $pr]} {
                close $pr
                set [namespace current]::done 1
        }
        read $pr

        set data [read $pr]
} [namespace current]] $pr]
vwait [namespace current]::done
puts {condition occured}

aspect: Sounds similar in application to String channels, which works on 8.4/5 at least.

kostix comments: aspect, I doubt this: pipes [chan pipe] creates are operating system objects which have real handles hidden behind Tcl channels. So when you, for instance, redirect the stdin of a spawned process to the pipe, on the OS level the handle of the process stdin will be the read handle of the OS pipe. And this is the OS who transports the data over such a pipe between Tcl channels.

aspect: Right you are .. string channels are a much more simple-minded facility, and not nearly as useful.