- The 'server' is the machine on which the user interacts with a GUI. It needs wish.
- The 'client' is the machine where the software really turns. It only needs tclsh.
Client File #1 : distanciel.tcl
namespace eval distanciel { variable server localhost variable port 2006 variable socket variable delayed variable limit 10 variable operations array set operations {unset Unset write Write read Read} proc connect {args} { variable server variable port variable socket if {[catch {set socket [socket $server $port]}]} { error "cannot connect to server $server on port $port" } return } proc send {args} { for {set level 1} { [uplevel $level namespace current] eq "::distanciel" } {incr level} {} set current [uplevel $level namespace current] puts $current connect variable socket # write <length>,data set data [string length $args],[string length $current]\n append data $current$args for {set i 0} {$i < 3 && [set b [catch {puts -nonewline $socket $data} msg]]} {incr i} { # do nothing } catch {flush $socket} if {$b} { error "cannot write after $i attempts" } # read <length>,data gets $socket count foreach {script data} [read $socket $count] {break} close $socket puts "script $script" uplevel 1 $script return $data } # create an local incarnation of a remote (Tk-related) proc proc lambda {procname} { #puts lambda=$procname set body [send delegate [list info body $procname]] set arglist [lambdaargs $procname] # get the namespace foreach {allctx procname} [context $procname] {break} # creates locally the proc puts $allctx,$procname,$arglist,$body uplevel 1 [list proc ${allctx}::$procname $arglist $body] } source tools.tcl proc lambdaargs {procname} { set arglist [send delegate [list info args $procname]] if {[llength $arglist] == 0} {return [list]} foreach arg $arglist { if {[send delegate [list info default $procname $arg dummy]]} { lappend result [list $arg [send delegate [list default $procname $arg]]] } else { lappend result $arg } } puts "lambdaargs $result" return $result } proc delete {cmd op} { send delegate $cmd } # when a variable has to be remotely controlled # here we write data proc wvar {varname value} { trace remove variable $varname write distanciel::myvar set $varname $value trace add variable $varname write distanciel::myvar } # like incr : returns the incremented value, but without modifying the variable proc getincr {var {increment 0}} { upvar $var x incr x 0 incr increment 0 return [expr {$x + $increment}] } proc normalize {varname} { set i [string first ( $varname] if {$i<0} { return [list $varname ""] } list [string range $varname [getincr i -1]] \ [string range $varname [getincr i] end-1] } proc varread {varname {currentns ::}} { if {[string range $varname 0 1] ne "::"} { set varname $currentns$varname } puts here if {![info exists $varname]} { return } # install traces watch $varname # tell the server an update puts herebeforedelegate send delegate [list Write $varname [set $varname]] } proc watch {varname} { #puts $varname # adds the same trace proc to all possible operations : read, write and unset trace add variable $varname write distanciel::myvar # see principles : we do not need to remotely read #trace add variable $varname read distanciel::myvar trace add variable $varname unset distanciel::myvar } # given a var name and an eventual key, return the fully qualified name of the variable proc yourvar {name key} { if {$key eq ""} { return $name } return ${name}($key) } # # About -textvariable and -listvariable : these options allow the server to share # data with the client, because they exists in the client, must be displayed # and might be updated by the GUI server. # # principles : # * each time a variable is set, the other side must be telled of that # (with the problem raising when both sides update variables at the same time) # * when a variable is unset (that cannot happen at the other, server side) # it must be destroyed at the server side # * if all the above conditions are met, no problem happen with reading variables # # unsets, reads or writes to a variable proc myvar {var key op} { foreach {ns var} [context $var] {break} if {$ns eq ""} { set ns [uplevel namespace current] } variable operations set name ${ns}::[yourvar $var $key] # translates an operation (read-write-unset) into a remote command set cmd [list $operations($op) $name] #puts $cmd switch -- $op { unset - read {} write { lappend cmd [set $name] } } send delegate $cmd } proc renproc {oldname newname op} { send delegate [list rename $oldname $newname] } } package provide distanciel 0.1
Client file #2 : distclt.tcl
source distanciel.tcl # let unknown know... proc unknown {args} [string map {} { set cmd [lindex $args 0] switch -glob -- $cmd { \.* { if {[winfo exists $cmd]} { return [distanciel::send delegate $args] } } } #puts $cmd if {[string match ::tk::* $cmd] || [string match tk::* $cmd]} { # private Tk procs, or even widget commands ! puts $cmd if {![catch {distanciel::lambda $cmd} msg]} { return [uplevel $args] } puts "err: $msg" } if {[uplevel 1 namespace current] eq "::tk"} { set cmd ::tk::$cmd puts $cmd if {![catch {distanciel::lambda $cmd} msg]} { return [uplevel $args] } puts "err: $msg" } }][info body unknown] namespace eval ::tk { proc myvariable {name} { foreach {ns var} [distanciel::context $name] { if {$ns eq "::tk"} { uplevel ::variable $var return } } uplevel ::variable $name } namespace eval unsupported {} } # widget list foreach name { button label frame entry text canvas checkbutton radiobutton menu menubutton scrollbar spinbox listbox labelframe message tk_optionMenu panedwindow toplevel } { set body { # creates the widget distanciel::send create [linsert $args 0 NAME $obj] # instanciates a wrapper to handle widget methods proc $obj {args} { distanciel::send action [info level 0] } trace add command $obj rename distanciel::renproc # next line causes severe errors #trace add command $obj delete distanciel::renproc return $obj } proc $name {obj args} [string map [list NAME $name] $body] } proc . {args} { distanciel::send action [info level 0] } foreach name { bell bind bindtags clipboard tk_chooseColor tk_chooseDirectory console destroy tk_dialog event focus tk_focusNext tk_focusPrev tk_focusFollowsMouse font tk_getOpenFile tk_getSaveFile grab grid image lower tk_messageBox option pack tk_setPalette tk_bisque place tk_popup raise scale selection send tk winfo wm tk_textCopy tk_testCut tk_textPaste tkwait } { proc $name {args} [string map [list NAME $name] { distanciel::send delegate [linsert $args 0 NAME] }] } foreach name {::tk::unsupported::ExposePrivateCommand ::tk::unsupported::ExposePrivateVariable} { proc $name {args} [string map [list NAME $name] { catch {distanciel::send delegate [linsert $args 0 NAME]} }] } trace add execution exit enter distanciel::delete # some hacks # proc dputs {args} {puts $args} # because we emulate Tk, and because some megawidgets call "package require Tk" package provide Tk [package require Tcl] set argv0 [lindex $argv 0] set argv [lrange $argv 1 end] source $argv0 while {1} { update after 30 distanciel::send script }
Server : distserver.tcl
package require Tk source tools.tcl proc dputs {arg} { set fd [open traces.txt a] puts $fd $arg close $fd } file delete traces.txt set script "" set mybreak 0 array set watch "" array unset watch * set currentns :: # used by distanciel::lambda proc default {procname arg} { info default $procname $arg value return $value } proc recv {channel args} { #fconfigure $channel -translation binary gets $channel counts foreach {count ns} [split $counts ,] {break} incr count $ns if {[catch {set data [read $channel $count]}]} {return} set ::currentns [string range $data 0 [expr {$ns - 1}]] set data [string range $data $ns end] if {[llength $data]>2} { dputs $data error "internal error" } foreach {type cmd} $data {break} set quit no switch -- $type { action - create - delegate { dputs $data set data [eval [linsert $cmd 0 $type]] } script {set data ""} default {error "unknown request type, should be one of : $types"} } foreach {var keys} [array get ::watch] { #dputs var=$var if {[llength $keys] > 1} { foreach key $keys { callback [list distanciel::wvar ${var}($key) [set ${var}($key)]] } } elseif {$keys eq ""} { callback [list distanciel::wvar $var [set $var]] } else { callback [list distanciel::wvar ${var}($keys) [set ${var}($keys)]] } } # clear the array array unset ::watch * #dputs S:$::script set data [list $::script $data] set ::script "" #dputs Resp:$data puts -nonewline $channel [string length $data]\n$data flush $channel close $channel if {$quit} { # after 1000; # here to flush network buffers before exiting exit } } proc action {args} { foreach {widget cmd} $args {break} switch -- $cmd { configure { set args [linsert [configure [lrange $args 2 end]] 0 $widget $cmd] } cget { return [cget $args] } add { # $menu add command -command "MyCmd" if {[lindex $args 2] eq "command"} { dputs "menu $args" set args [linsert [configure [lrange $args 3 end]] 0 $widget add command] dputs "menu $args" } } default {} } seceval $args } # TODO : proc Set proc Set {var value} { context $var $::currentns set $var $value } # secure eval proc seceval {arg} { if {[catch {eval $arg} msg]} { callback [list error $msg] return } set msg } rename exit __exit__ proc exit {{code 0}} { callback [list exit $code] update after 2000 __exit__ } proc delegate {args} { switch -- [lindex $args 0] { exit { # delay exit script uplevel set quit yes callback $args return } bind { if {[llength $args] == 3} { #dputs $args # skip the callback set result [seceval $args] #dputs $result catch { if {[llength $result] == 2 && [lindex $result 0] eq "callback"} { set result [lindex $result 1] } } return $result } if {[llength $args] == 4} { # there is a script (bind may be called without) set script [lindex $args 3] if {[string index $script 0] eq "+"} { # append the script to the current bindings set plus + set script [string range $script 1 end] } else { set plus "" } # the third argument to the bind command is a callback lset args 3 $plus[list callback $script] } } Unset { # unset a shared variable unset [lindex $args 1] return } Write { # set the shared variable to its new value #dputs Write:$args set var [lindex $args 1] set value [lindex $args 2] # remove traces before setting the variable to avoid aller-retour trace remove variable $var write wvar Set $var $value trace add variable $var write wvar # in order to be a little more synchronous, # $value was replaced by [set $var] return [set $var] } default { # nothing to be done } } # take the first pair foreach {cmd obj} $args {break} switch -- $obj { configure { set args [linsert [lindex [configure [lrange $args 2 end]] 0] 0 $cmd $obj] } cget { return [cget $args] } default {} } #dputs del:$args seceval $args } # instanciate a variable link proc myvar {varname} { dputs "myvar $varname" set pos [string first ( $varname] if {$pos>=0} { set key [string range $varname [expr {$pos+1}] end-1] set myvar [string range $varname 0 [incr pos -1]] } if {[info exists key]} { if {![info exists $varname]} { Set $varname "" callback [list distanciel::varread $varname $::currentns] } elseif {[trace info variable $varname] eq ""} { trace add variable $varname write wvar wvar $myvar $key write if {[trace info variable $myvar] eq ""} { trace add variable $myvar array wvar } } return $varname } if {![info exists $varname]} { Set $varname "" callback [list distanciel::varread $varname $::currentns] } elseif {[trace info variable $varname] eq ""} { trace add variable $varname write wvar wvar $varname "" write } # returns the var name return $varname } proc wvar {name key op} { # it overwrites previously trace invokations, if any if {[llength [split $name ::]]==1} { set name [uplevel namespace current]::$name } #dputs var=$name if {$op eq "array"} { foreach key [array names $name] { wvar $name $key write } return } if {![info exists ::watch($name)]} { set ::watch($name) $key return } if {$key eq ""} { # scalar variable return } if {[lsearch -exact $::watch($name) $key]<0} { lappend ::watch($name) $key } } proc configure {args} { if {[llength $args] == 1} { set args [lindex $args 0] } if {[llength $args] % 2 != 0} {return $args} set l "" foreach {opt value} $args { switch -- $opt { -command { lappend l $opt [linsert $value 0 callback] } -listvariable - -textvariable - -yscrollvariable - -xscrollvariable - -variable { lappend l $opt [myvar $value] } default {lappend l $opt $value} } } #dputs config=$l set l } proc cget {arg} { foreach {obj cmd option} $arg {break} set res [seceval $arg] switch -- $option { -command { # value is {callback {cmd arg ...}} # we want the command set res [lindex $res 1] } default { # nothing to be done } } return $res } # create a widget instance proc create {args} { foreach {cmd obj} $args {break} set l [linsert [configure [lrange $args 2 end]] 0 $cmd $obj] dputs $l seceval $l } proc callback {args} { dputs callback//$args//[info level 1] if {[llength $args] == 1} {set args [lindex $args 0]} append ::script $args append ::script \n } # not needed anymore #callback {namespace eval tk {}} # to share Tk private data with the client foreach var [concat [info vars ::tk::*] [info vars ::tk_*]] { if {[array exists $var]} { foreach key [array names $var] { myvar ${var}($key) } } else { myvar $var } } socket -server recv 2006
Common file : the tools (tools.tcl) It must be present at both ends.
# common tools for distanciel # By Stéphane Arnold 2006 proc context {name {globalns ::}} { if {$globalns ne "::"} {set name $globalns$name} set ns [wsplit $name ::] set name [lindex $ns end] set ns [lrange $ns 0 end-1] # creates all intermediate namespace, if they do not exist set allctx "" foreach context $ns { if {$context eq ""} { append allctx :: continue } if {$allctx ne "::" && $allctx ne ""} { append allctx :: } append allctx $context namespace eval $allctx {} } list $allctx $name } # Split a string $str after the separator $sep # the built-in split command cannot do that proc wsplit {str sep} { set out "" set sepLen [string length $sep] if {$sepLen < 2} { return [split $str $sep] } while {[set idx [string first $sep $str]] >= 0} { if {$idx>0} { # the left part : the current element lappend out [string range $str 0 [expr {$idx-1}]] } # get the right part and iterate with it set str [string range $str [incr idx $sepLen] end] } # there is no separator anymore, but keep in mind the right part must be appended lappend out $str }
Client file #3 : the test program (mytest.tcl)
package require BWidget Label .lbl -text "Hello Stéphane!" Button .b -command exit -text Exit set enter "Enter your text here" proc echo {var key op} { if {$key ne ""} { puts [set $var($key)] return } puts [set $var] } #trace add variable ::enter write echo Entry .en -textvariable ::enter Button .echo -command {tk_messageBox -message "You have typed : $::enter"} -text Echo pack .lbl .b .en .echoHow to launch it :
# on the server wish distserver.tcl & # on the client tclsh distclt.tcl mytest.tcl &
See also Remote Script Execution.