This chapter describes the send
command that invokes Tcl commands in other applications. This chapter also presents an alternative to send
that uses network sockets.
The send
command lets Tk applications on the same display send each other Tcl commands and cooperate in very flexible ways. A large application can be structured as a set of smaller tools that cooperate instead of one large monolith. This encourages reuse, and it exploits your workstation's multiprogramming capabilities.
The send
facility provides a name space for Tk applications. The winfo interps
command returns the names of all the Tk applications reachable with send
. The send
communication mechanism is limited to applications running on one display. Multiple screens on one workstation still count as the same display on X. In UNIX, send
uses properties on the X display for communication and to record the application names. As of Tk 8.0, send
is not yet implemented on Macintosh or Windows. There is an extension for Windows that uses DDE to emulate send
.
This chapter also describes an alternative to send
that uses network sockets. The facility is not limited to a single display, and can be used in conjunction with safe interpreters to limit the capabilities of remote operations. A number of Tcl extensions provide similar functionality, including GroupKit and Tcl-DP. Of particular note is the comm
package, which is a part of the Standard Tcl Library. The comm
package was designed as a sockets-based replacement for send
that would work on any platform. You can find more information about comm
from the tcllib project page on SourceForge:
The send
command invokes a Tcl command in another application. The general form of the command is:
send options interp arg ?arg...?
The send
command behaves like eval
; if you give it extra arguments, it concatenates them to form a single command. If your argument structure is important, use list
to build the command. Table 43-1 lists the options to send
:
The interp
argument is the name of the other application. An application defines its own name when it creates its main window. The wish shell uses as its name the last component of the file name of the script. For example, when wish interprets /usr/local/bin/exmh
, it sets its application name to exmh
. However, if another instance of the exmh
application is already running, wish chooses the name exmh #2
, and so on. If wish is not executing from a file, its name is just wish
. You may have noticed wish #2
or wish #3
in your window title bars, and this reflects the fact that multiple wish applications are running on your display.
A script can find out its own name, so you can pass names around or put them into files in order to set up communications. The tk appname
command queries or changes the application name:
set myname [tk appname]
tk appname aNewName
The send
command relies on the X authority mechanism for authorization. A command is rejected by the target interpreter if you do not have X authority set up. There are two ways around this problem. First, you can disable the access check by compiling the tkSend.c
file with the -DTK_NO_SECURITY
compile flag. If you must worry about malicious programs that send your programs commands, then you should not do this.
The second option is to start your X server with its -auth
flag, which initializes the X authority mechanism. The details vary depending on your X server, and most modern X servers do this automatically. The general picture is that you generate a pseudo-random string and store it into a file, which is usually named ~/.Xauthority
and must be readable only by your account. The -auth
flag specifies the name of this file to the X server. Each X application reads this file and sends the contents to the X server when opening the connection to the server. If the contents match what the server read when it started, then the connection is allowed. The system is slightly more complicated than described here. The file actually contains a sequence of records to support multiple displays and client hosts. Consult your local X guru or the documentation for the details particular to your system.
Tk also requires that the xhost list be empty. The xhost mechanism is the old, not-so-secure authentication mechanism in X. With xhost you allow all programs on a list of hosts to connect to your display. The problem with this is that multiuser workstations allow remote login, so essentially anybody could log in to a workstation on the xhost list and gain access to your display. The Xauthority mechanism is much stronger because it restricts access to your account, or to accounts that you explicitly give a secret token to. The problem is that even if Xauthority is set up, the user or a program can turn on xhosts and open up access to your display.
If you run the xhost program with no argument, it reports the status and what hosts are on the list. The following output is generated when access control is restricted, but programs running on sage
are allowed to connect to the display:
exec xhost => Access control enabled: all hosts being restricted sage
This is not good enough for Tk send
. It will fail because sage
is on the list. I work in an environment where old scripts and programs are constantly adding things to my xhost list for reasons that are no longer valid. I developed a version of send
that checks for errors and then does the following to clean out the xhost list. You have to enable access control and then explicitly remove any hosts on the list. These are reported after an initial line that says whether or not hosts are restricted:
xhost - ;# enable access control in general foreach host [lrange [split [exec xhost] ] 1 end] { exec xhost -$host ;# clear out exceptions }
The following example is a general-purpose script that reads input and then sends it to another application. You can put this at the end of a pipeline to get a loopback effect to the main application, although you can also use fileevent
for similar effects. One advantage of send
over fileevent
is that the sender and receiver can be more independent. A logging application, for example, can come and go independently of the applications that log error messages:
Example 43-1. The sender application
#!/usr/local/bin/wish # sender takes up to four arguments: # 1) the name of the application to send to. # 2) a command prefix. # 3) the name of another application to notify # after the end of the data. # 4) the command to use in the notification. # Hide the unneeded window wm withdraw . # Process command line arguments if {$argc == 0} { puts stderr "Usage: send name ?cmd? ?uiName? ?uiCmd?" exit 1 } else { set app [lindex $argv 0] } if {$argc > 1} { set cmd [lindex $argv 1] } else { set cmd Send_Insert } if {$argc > 2} { set ui [lindex $argv 2] set uiCmd Send_Done } if {$argc > 3} { set uiCmd [lindex $argv 3] } # Read input and send it to the logger while {[gets stdin input] >= 0} { # Ignore errors with the logger catch {send $app [concat $cmd [list $input ]]} } # Notify the controller, if any if [info exists ui] { if [catch {send $ui $uiCmd} msg] { puts stderr "send.tcl could not notify $ui $msg" } } # This is necessary to force wish to exit. exit
The sender application supports communication with two processes. It sends all its input to a primary "logging" application. When the input finishes, it can send a notification message to another "controller" application. The logger and the controller could be the same application.
Consider the send
command used in the example:
send $app [concat $cmd [list $input ]]
The combination of concat
and list
is tricky. The list
command quotes the value of the input line. This quoted value is then appended to the command, so it appears as a single extra argument. Without the quoting by list
, the value of the input line will affect the way the remote interpreter parses the command. Consider these alternatives:
send $app [list $cmd $input]
This form is safe, except that it limits $cmd
to a single word. If cmd
contains a value like the ones given below, the remote interpreter will not parse it correctly. It will treat the whole multiword value as the name of a command:
.log insert end .log see end ; .log insert end
This is the most common wrong answer:
send $app $cmd $input
The send
command concatenates $cmd
and $input
together, and the result will be parsed again by the remote interpreter. The success or failure of the remote command depends on the value of the input data. If the input included Tcl syntax like $
or [ ]
, errors or other unexpected behavior would result.
Chapter 24 presented two examples: a browser for the examples in this book, and a simple shell in which to try out Tcl commands. In that chapter they are put into the same application. The two examples shown below hook these two applications together using the send
command. Example 43-2 changes the Run
and Reset
procedures of the browser to send EvalEcho
commands to the shell.
Example 43-2. Hooking the browser to an eval
server
# Replace the Run and Reset procedures of the browser in # Example 24–3 on page 384 with these procedures # Start up the evalsrv.tcl script. proc StartEvalServer {} { global browse # Start the shell and pass it our name. exec evalsrv.tcl [tk appname] & # Wait for evalsrv.tcl to send us its name tkwait variable browse(evalInterp) } proc Run {} { global browse set apps [winfo interps] set ix [lsearch -glob $apps evalsrv.tcl*] if {$ix < 0} { # No evalsrv.tcl application running StartEvalServer } if {![info exists browse(evalInterp)]} { # Hook up to already running eval server set browse(evalInterp) [lindex $apps $ix] } if [catch {send $browse(evalInterp) {info vars}} err] { # It probably died - restart it. StartEvalServer } # Send the command asynchronously. The two # list commands foil the concat done by send and # the uplevel in EvalEcho send -async $browse(evalInterp) [list EvalEcho [list source $browse(current)]] } # Reset the shell interpreter in the eval server proc Reset {} { global browse send $browse(evalInterp) {EvalEcho reset} }
The number of lists created before the send
command may seem excessive, but they are all necessary. The send
command concatenates its arguments, so instead of letting it do that, we pass it a single list. Similarly, EvalEcho
expects a single argument that is a valid command, so list
is used to construct that.
The StartEvalServer
procedure starts up the shell. Command-line arguments are used to pass the application name of the browser to the shell. The shell completes the connection by sending its own application name back to the browser. The browser stores the name of the shell application in browser(evalInterp)
. The code that the shell uses is shown in Example 43-3:
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 43-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 43-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 43-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 43-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 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] # Use regsub to count newlines set lines [regsub -all $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 43-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 86.
Example 43-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 incr lines -1 } set code [lindex $result 0] set x [lindex $result 1] # Cleanup the end of the stack regsub "[^ ]+$" [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]"
3.14.70.163