TCL interpreter through socket

sergio: Hi,

we have our application written in Agilent VEE. From our application we want to manage a network tester equipment. The network tester provides us with TCL API functions to control it. Agilent VEE application does not work with TCL at all. So we would like to develop a TCL interpreter that running on localhost. This TCL interpreter is supposed to load network test TCL API package and open a socket to accept API commands or even scripts from our Agilent VEE application.

Any ideas where should we start?

AMG, PYK 2016-04-07: It's pretty easy to glue an interpreter eval loop to a Tcl TCP/IP socket, but you have to come up with a network protocol that you can use in VEE. I know nothing about VEE, so I'm no help there.

Here's some simple code:

package require Tcl 8.4
proc accept {sock peeraddr peerport} {
    fconfigure $sock -buffering line
    set slave [interp create]
    set script ""
    while {[gets $sock line] != -1} {

        # Make sure script ends in a newline so that [info complete $script] doesn't
        # procuce false positives.
        append script $line\n

        if {[info complete $script]} {
            puts $sock [list [catch [list $slave eval $script] result options] $result $options]
            set script ""
        }
    }
    interp delete $slave
    close $sock
}
set serv [socket -server accept 9999]
vwait forever

This program listens on TCP port 9999. When a connection is made, it reads a line of text and adds it to its script buffer. If the script buffer is complete (no mismatched open braces and brackets), it evaluates it in a slave interpreter, and the result is sent back to the client. Only one connection may be made at a time.

The result is formatted as a two-element Tcl list followed by a newline. The first element is 0 if the script executed without error, 1 if there was a problem. The second element is the result, which is either the normal return value or the error message. The third element is the dictionary of return options from the evalutaion. An empty result is indicated by 0 {} <options dictionary> followed by a line ending, which by default depends on the current platform. For more information on how Tcl lists are formatted, see the rules of Tcl.

Try it out using telnet to see how it behaves.

For more advanced ideas, you can have a look at the comm package.

By the way, you might want a little explanation of how this line works:

puts $sock [list [catch [list $slave eval $script] result] $result]

Let's break it down, step-by-step:

  • $1 = [list $slave eval $script] - Construct a three-element list that will be the first argument to [catch].
    • $slave - Name of the slave interpreter, which is also a command.
    • eval - Interpreter subcommand that means to evaluate the concatenated arguments inside the interpreter, like [interp eval].
    • $script - Script to be executed in the slave interpreter.
  • $2 = [catch $1 result options] - Execute the script denoted by $1, store the result in $result, the options dictionary in options, and return 0 or 1 on success or error.
    • $1 - Script to execute.
    • result - Name of the variable into which the result will be stored.
  • $3 = [list $2 $result $options] - Construct a three-element list which will be sent to the client.
    • $2 - Success/error return from [catch].
    • $result - Result obtained by [catch].
  • puts $sock $3 - Send the two-element list $3 to the client.
    • $sock - Name of the TCP communication channel.
    • $3 - Text to send. A newline will be appended.

sergio: thanks for such a detailed information. That's pretty enough for start.

Actually we want to open socket and keep it with created TCL interpreter as long as it is required.

Let's call it as a connection session to the network tester. The session is over when socket is being closed.

AMG: That's pretty much how it works right now. Each connection gets its own interpreter, so there's fairly minimal interference between connections; they're separate sessions, as you say, and the interpreter is destroyed when the connection is closed. To eliminate interference, use the -safe option to [interp], which prevents access to commands like [cd] that can change application state that is shared between interpreters. Of course, there would still be the restriction on having more than one connection at a time.

If you want the interpreter to not be destroyed, you should create it once globally instead of every time [accept] is called. Either store the name of the interpreter in a global variable, or explicitly give it a name and use that same name to access it inside [accept].

Good luck implementing Tcl lists and quoting inside VEE. You will have to go both ways: write code to construct a Tcl list (formatted as a quoted string) and write code to decipher a Tcl list into whatever VEE's data structures are. If that's too much, Tcl can meet VEE halfway. Make your VEE code send commands to Tcl in a format convenient for VEE, then write Tcl code to translate that into a Tcl list. But you will still need to write VEE code to read Tcl lists. A consequence of Tcl's "everything is a string" design philosophy is that in most cases it's impossible to distinguish between an arbitrary list and a string, so a type tagging mechanism is sometimes required when interfacing with other languages like Python or JavaScript that do distinguish between types.

Some examples of Tcl quoting:

foo\ bar
"foo bar"
{foo bar}

All three lines encode the string f-o-o-space-b-a-r (without the -'s), which could also be viewed as a two-element list containing foo and bar.

Backslash (\) is used to encode special characters: $, ", {, }, [, ], (, ), #, \, :, ;, space, and non-printable. Circumstances dictate which must be quoted, but you can always add a backslash if you're unsure.

sergio: with minor changes it works fine so far. The only concern is I am not getting the same what is printed out in stdout. A TCL script is being executed in the slave interpreter prints out some strings. I expect them to see in result variable. But the result variable contains empty string. On the other hand stdout contains the proper results. Most probably I need to redirect stdout to result variable. In meantime checking why result variable is always empty.

nb Try flushing the channel sergio, or don't use buffering when configuring the socket.

sergio: For now the result variable does not contain the output of script execution in slave interpreter. The result of execution shows up in stdout only.

Nothing to do with socket yet.

AMG: You're trying to print the result instead of return it. Instead of printing, append it all into a variable then return it at the end of your code. There's some refchan or memchan trickery you can do to make stdout or some other channel be redirected into a variable, or you can replace [puts] with a command that collects data for you to return later. Remember, the code I gave you sends back the result returned by the script.

sergio: Using variable and returning it works fine. In this case I can only intercept results produced by my puts. But there still some results are being displayed in stdout.

I need to figure out how to collect all results from stdout.

AMG: I take it you're calling someone else's code which is also puts'ing. Maybe you're calling extension code that's using printf() directly. I dunno. The former can be handled by redefining the Tcl I/O channel called "stdout" to something that collects results; see refchan and memchan for some possibilities. I'm not sure about how to handle the latter, though if you're not doing that, don't worry about it.

sergio: Just continue this topic, I am going to send a string with full path of a TCL script + arguments to this socket server. socket server opens, reads and executes the script file with arguments. How this can be done quickly?

AMG: Use the [source] command, which takes the name of the script to execute as its argument. To execute a script file with arguments, you must first put the arguments in predefined variables, agreed upon between the executed scripts and the executing script. Probably you will want to use the $argv variable, since that's what's used to pass arguments from the command line to the main script.

sergio: my point is to break one big TCL script into smaller ones; send one by one to socket; so all smaller scripts to be executed in one context (interpreter); smaller scripts can be executed (source) with arguments; playing with it right now. removed slave interpreter and trying to do all in parent one. but without any success.

aspect: Sorry for butting in, but setting $argv and using [source] sounds incredibly error-prone to me. When I read your comment yesterday I had the impression you were trying to execute the local script file similarly to how you would from the shell, and return its output -- for this use open or exec:

exec [info nameofexecutable] /path/to/script.tcl arg1 arg2

You could just send this through the socket of the original program and it should DWYW.

That said, it sounds as though you're trying to pluck a chicken with a hammer. The architecture I would expect (and I think AMG was leading towards) from what you have described so far is more like the above program, using the [slave interp], but defining a set of additional commands which can be called by the remote client. First, add a line to the server script following set $slave [interp create]:

$slave eval {source lib.tcl}

Then you can put commands in lib.tcl that can be called by remote clients. An example might be:

# lib.tcl
proc run_command {cmd args} {
    exec [info nameofexecutable] /my/script/location/$cmd {*}$args
    # {*} requires tcl8.5 -- if you're on 8.4 you could use:
    #eval "exec [info nameofexecutable] /my/script/location/$cmd $args"
}

You might get some more realtime feedback on your problem in the Tcl Chatroom.

AMG: If you want to send a large script in pieces before executing it, use the [append] command repeatedly to assemble a variable containing the script, then [eval] the contents of the script variable. [eval], like [source], doesn't pass arguments to the script, so you'd have to pass them via agreed-upon global variables. If you don't want globals, you can formulate the script as a [proc] body, and invoke that proc with arguments. That way they'll be local to the proc invocation's stack frame.

sergio: Let me bring more details about my project: we have an instrument that understands TCL. So together with this instrument there come a TCL package and APIs to control this instrument. My test application needs to communicate and control this instrument. I decided to write a socket server between my application and the instrument. This socket server must have a TCL interpreter to receive and execute instrument's TCL API commands.

My test process is divided to some parts: use instrument's TCL package, establish connection to the instrument, send some commands and close connection. Actually this can be done using one big TCL script. But establishing connection needs to be done only one time. Then I will be sending TCL scripts paths, containing those commands, to socket at different points of time. So execution context (if I execute all commands in one interpreter or tclsh) must be the same until I send a command to get disconnected from the instrument.The execution results are forwarded to a log file which my application can read. If this is still vague I will try Tcl Chatroom.

PYK 2016-04-07: Asynchronous Script Evaluation via Coroutines and Channels is exactly what's needed in this case.