Tracing stdout and stderr in Tcl

2019-05-30 09:15发布

问题:

I'm not sure if this absurd. Isn't it possible to trace the read and write of stdout and stderr in Tcl?

I have tried the below and found no clue.

% proc tracer {varname args} {
    upvar #0 $varname var
    puts "$varname value : $var"
}
% trace add variable stdout read tracer
% trace add variable stdout write tracer
% puts stdout hai
hai
% puts hai
hai
% trace add variable stderr write tracer
% trace add variable stderr read tracer
% puts hai
hai
% puts stderr hai
hai

As per the man page of puts, if no channelId is specified for puts command, then it defaults to stdout which means even with puts hai, stdout will be accessed. Right ? (Though it is not working even with the argument as stdout or stderr)

回答1:

The problem with your attempted solution is that stdout, stderr and stdin are not variables but names of the so-called "channels". In essence, they're in a separated namespace (not a namespace operated by the namespace Tcl command!): you can get a list of them using the chan names command but you can't, say, rename a channel, or assign a value to it or unset it: these operations simply have no sense on channels and will affect a variable by that name instead.

An approach to trace a channel is to actually subvert it with another—"script-level"—channel and "proxy" all operations. This trick uses a lesser-known Tcl's feature: when you close one of Tcl's standard channels and immediately open a channel (no matter if "real" or "script-level"), that will gets registered in place of that standard channel just closed. Hence if we close a standard channel and immediately create our own "proxy" channel in its place we hence subvert that standard channel.

Those "script-level" ("reflected") channels require Tcl ≥ 8.5.

Here's a sketch of subverting stdout which requires Linux (for /proc/self/fd/<fileno> support).

proc traceChan {cmd chan args} {
    global stdout

    puts stderr "Trace on $chan; cmd=$cmd; args=$args"

    switch -- $cmd {
        initialize {
            return [list initialize finalize watch write configure cget cgetall]
        }
        finalize {
            chan close $stdout
        }
        watch {
            # FIXME: not implemented
        }
        write {
            set data [lindex $args 0]
            chan puts -nonewline $stdout $data
            return [string length $data]
        }
        configure {
            return [chan config $stdout {*}$args]
        }
        cget {
            return [chan cget $stdout {*}$args
        }
        cgetall {
            return [chan configure $stdout]
        }
    }
}

set fn [file readlink /proc/self/fd/1]
set conf [chan config stdout]
chan close stdout
chan create write ::traceChan
set stdout [open $fn w]
chan configure stdout {*}$conf

puts [chan names]
puts test
chan flush stdout
chan close stdout

On my system it botches terminal settings (stdout is connected to a terminal), and requires me to execute reset followed by stty sane after the script exits but at least it does the job done.

Actual problems with this script:

  • In fact we lie about the number of bytes written: we have no idea how many bytes the underlying stdout channel will write as it depends on a number of things.
  • We don't handle channel events at all.

These problems can be addressed by writing a C module implementing such a proxying: with the C API, you would have access to the underlying file descriptor/handle (so no requirement for /proc/self/fd/...) and a Tcl object wrapping it (so you could possibly just clone it right away) and know how many bytes were written by the underlying channel.


Oh, and note that if you're fine with throwing the data sent by the script to the subverted standard channels away, just do no dances with re-opening the real underlying file, closing it, writing data to it etc. The solution will then boild down to performing chan create immediately after chan close and tracing writes in the tracing procedure. In response to the write call your tracing routine will still need to return a number of bytes thrown away.


Please also read the chan and refchan manual pages.