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 \
$errorCode]\n
# 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]"
|