Previous section   Next section

Practical Programming in Tcl & Tk, Third Edition
By Brent B. Welch

Table of Contents
Chapter 40.  Send

Remote eval through Sockets

Network sockets provide another communication mechanism you can use to evaluate Tcl commands in another application. The "name" of the application is just the host and port for the socket connection. There are a variety of schemes you can use to manage names. A crude, but effective way to manage host and ports for your servers is to record them in a file in your network file system. These examples ignore this problem. The server chooses a port and the client is expected to know what it is.

Example 40-4 implements Eval_Server that lets other applications connect and evaluate Tcl commands. The interp argument specifies the interpreter in which to evaluate the Tcl commands. If the caller of Eval_Server specifies {} for the interpreter, then the commands are evaluated in the current interpreter. The openCmd is called when the connection is made. It can do whatever setup or authentication is required. If it doesn't like the connection, it can close the socket:

Example 40-4 Remote eval using sockets.
proc Eval_Server {port {interp {}} {openCmd EvalOpenProc}} {
   socket -server [list EvalAccept $interp $openCmd] $port
proc EvalAccept {interp openCmd newsock addr port} {
   global eval
   set eval(cmdbuf,$newsock) {}
   fileevent $newsock readable [list EvalRead $newsock $interp]
   if [catch {
      interp eval $interp $openCmd $newsock $addr $port
   }] {
      close $newsock
proc EvalOpenProc {sock addr port} {
   # do authentication here
   # close $sock to deny the connection

Example 40-5 shows EvalRead that reads commands and evaluates them in an interpreter. If the interp is {}, it causes the commands to execute in the current interpreter. In this case an uplevel #0 is necessary to ensure the command is executed in the global scope. If you use interp eval to execute something in yourself, it executes in the current scope:

Example 40-5 Reading commands from a socket.
proc EvalRead {sock interp} {
   global eval errorInfo errorCode
   if [eof $sock] {
      close $sock
   } else {
      gets $sock line
      append eval(cmdbuf,$sock) $line\n
      if {[string length $eval(cmdbuf,$sock)] && \
             [info complete $eval(cmdbuf,$sock)]} {
         set code [catch {
           if {[string length $interp] == 0} {
              uplevel #0 $eval(cmdbuf,$sock)
           } else {
              interp eval $interp $eval(cmdbuf,$sock)
         } result]
         set reply [list $code $result $errorInfo \
         # Use regsub to count newlines
         set lines [regsub -all \n $reply {} junk]
         # The reply is a line count followed
         # by a Tcl list that occupies that number of lines
         puts $sock $lines
         puts -nonewline $sock $reply
         flush $sock
         set eval(cmdbuf,$sock) {}

Example 40-6 presents Eval_Open and Eval_Remote that implement the client side of the eval connection. Eval_Open connects to the server and returns a token, which is just the socket. The main task of Eval_Remote is to preserve the information generated when the remote command raises an error

The network protocol is line-oriented. The Eval_Remote command writes the command on the socket. The EvalRead procedure uses info complete to detect the end of the command. The reply is more arbitrary, so server sends a line count and that number of lines. The regsub command counts up all the newlines because it returns the number of matches it finds. The reply is a list of error codes, results, and trace information. These details of the return command are described on page 80.

Example 40-6 The client side of remote evaluation.
proc Eval_Open {server port} {
   global eval
   set sock [socket $server $port]
   # Save this info for error reporting
   set eval(server,$sock) $server:$port
   return $sock
proc Eval_Remote {sock args} {
   global eval
   # Preserve the concat semantics of eval
   if {[llength $args] > 1} {
      set cmd [concat $args]
   } else {
      set cmd [lindex $args 0]
   puts $sock $cmd
   flush $sock
   # Read return line count and the result.
   gets $sock lines
   set result {}
   while {$lines > 0} {
      gets $sock x
      append result $x\n
      incr lines -1
   set code [lindex $result 0]
   set x [lindex $result 1]
   # Cleanup the end of the stack
   regsub "\[^\n]+$" [lindex $result 2] \
      "*Remote Server $eval(server,$sock)*" stack
   set ec [lindex $result 3]
   return -code $code -errorinfo $stack -errorcode $ec $x
proc Eval_Close {sock} {
   close $sock

If an error occurs in the remote command, then a stack trace is returned. This includes the command used inside EvalRead to invoke the command, which is either the uplevel or interp eval command. This is the very last line in the stack that is returned, and regsub is used to replace this with an indication of where control transferred to the remote server:

catch [Eval_Remote sock6 set xx]
=> 1
set errorInfo
=> can't read "xx": no such variable
    while executing
"set xx
    ("uplevel" body line 1)
    invoked from within
*Remote Server sage:4000*
    invoked from within
"catch [Eval_Remote sock6 set xx]"

      Previous section   Next section