1# 2# cmdsrv.tcl -- 3# 4# Simple socket command server. Supports many simultaneous sessions. 5# Works in thread mode with each new connection receiving a new thread. 6# 7# Usage: 8# cmdsrv::create port ?-idletime value? ?-initcmd cmd? 9# 10# port Tcp port where the server listens 11# -idletime # of sec to idle before tearing down socket (def: 300 sec) 12# -initcmd script to initialize new worker thread (def: empty) 13# 14# Example: 15# 16# # tclsh8.4 17# % source cmdsrv.tcl 18# % cmdsrv::create 5000 -idletime 60 19# % vwait forever 20# 21# Starts the server on the port 5000, sets idle timer to 1 minute. 22# You can now use "telnet" utility to connect. 23# 24# Copyright (c) 2002 by Zoran Vasiljevic. 25# 26# See the file "license.terms" for information on usage and 27# redistribution of this file, and for a DISCLAIMER OF ALL WARRANTIES. 28# 29# ----------------------------------------------------------------------------- 30# RCS: @(#) $Id: cmdsrv.tcl,v 1.6 2004/12/22 15:31:05 vasiljevic Exp $ 31# 32 33package require Tcl 8.4 34package require Thread 2.5 35 36namespace eval cmdsrv { 37 variable data; # Stores global configuration options 38} 39 40# 41# cmdsrv::create -- 42# 43# Start the server on the given Tcp port. 44# 45# Arguments: 46# port Port where the server is listening 47# args Variable number of arguments 48# 49# Side Effects: 50# None. 51# 52# Results: 53# None. 54# 55 56proc cmdsrv::create {port args} { 57 58 variable data 59 60 if {[llength $args] % 2} { 61 error "wrong \# arguments, should be: key1 val1 key2 val2..." 62 } 63 64 # 65 # Setup default pool data. 66 # 67 68 array set data { 69 -idletime 300000 70 -initcmd {source cmdsrv.tcl} 71 } 72 73 # 74 # Override with user-supplied data 75 # 76 77 foreach {arg val} $args { 78 switch -- $arg { 79 -idletime {set data($arg) [expr {$val*1000}]} 80 -initcmd {append data($arg) \n $val} 81 default { 82 error "unsupported pool option \"$arg\"" 83 } 84 } 85 } 86 87 # 88 # Start the server on the given port. Note that we wrap 89 # the actual accept with a helper after/idle callback. 90 # This is a workaround for a well-known Tcl bug. 91 # 92 93 socket -server [namespace current]::_Accept -myaddr 127.0.0.1 $port 94} 95 96# 97# cmdsrv::_Accept -- 98# 99# Helper procedure to solve Tcl shared channel bug when responding 100# to incoming socket connection and transfering the channel to other 101# thread(s). 102# 103# Arguments: 104# s incoming socket 105# ipaddr IP address of the remote peer 106# port Tcp port used for this connection 107# 108# Side Effects: 109# None. 110# 111# Results: 112# None. 113# 114 115proc cmdsrv::_Accept {s ipaddr port} { 116 after idle [list [namespace current]::Accept $s $ipaddr $port] 117} 118 119# 120# cmdsrv::Accept -- 121# 122# Accepts the incoming socket connection, creates the worker thread. 123# 124# Arguments: 125# s incoming socket 126# ipaddr IP address of the remote peer 127# port Tcp port used for this connection 128# 129# Side Effects: 130# Creates new worker thread. 131# 132# Results: 133# None. 134# 135 136proc cmdsrv::Accept {s ipaddr port} { 137 138 variable data 139 140 # 141 # Configure socket for sane operation 142 # 143 144 fconfigure $s -blocking 0 -buffering none -translation {auto crlf} 145 146 # 147 # Emit the prompt 148 # 149 150 puts -nonewline $s "% " 151 152 # 153 # Create worker thread and transfer socket ownership 154 # 155 156 set tid [thread::create [append data(-initcmd) \n thread::wait]] 157 thread::transfer $tid $s ; # This flushes the socket as well 158 159 # 160 # Start event-loop processing in the remote thread 161 # 162 163 thread::send -async $tid [subst { 164 array set [namespace current]::data {[array get data]} 165 fileevent $s readable {[namespace current]::Read $s} 166 proc exit args {[namespace current]::SockDone $s} 167 [namespace current]::StartIdleTimer $s 168 }] 169} 170 171# 172# cmdsrv::Read -- 173# 174# Event loop procedure to read data from socket and collect the 175# command to execute. If the command read from socket is complete 176# it executes the command are prints the result back. 177# 178# Arguments: 179# s incoming socket 180# 181# Side Effects: 182# None. 183# 184# Results: 185# None. 186# 187 188proc cmdsrv::Read {s} { 189 190 variable data 191 192 StopIdleTimer $s 193 194 # 195 # Cover client closing connection 196 # 197 198 if {[eof $s] || [catch {read $s} line]} { 199 return [SockDone $s] 200 } 201 if {$line == "\n" || $line == ""} { 202 if {[catch {puts -nonewline $s "% "}]} { 203 return [SockDone $s] 204 } 205 return [StartIdleTimer $s] 206 } 207 208 # 209 # Construct command line to eval 210 # 211 212 append data(cmd) $line 213 if {[info complete $data(cmd)] == 0} { 214 if {[catch {puts -nonewline $s "> "}]} { 215 return [SockDone $s] 216 } 217 return [StartIdleTimer $s] 218 } 219 220 # 221 # Run the command 222 # 223 224 catch {uplevel \#0 $data(cmd)} ret 225 if {[catch {puts $s $ret}]} { 226 return [SockDone $s] 227 } 228 set data(cmd) "" 229 if {[catch {puts -nonewline $s "% "}]} { 230 return [SockDone $s] 231 } 232 StartIdleTimer $s 233} 234 235# 236# cmdsrv::SockDone -- 237# 238# Tears down the thread and closes the socket if the remote peer has 239# closed his side of the comm channel. 240# 241# Arguments: 242# s incoming socket 243# 244# Side Effects: 245# Worker thread gets released. 246# 247# Results: 248# None. 249# 250 251proc cmdsrv::SockDone {s} { 252 253 catch {close $s} 254 thread::release 255} 256 257# 258# cmdsrv::StopIdleTimer -- 259# 260# Cancel the connection idle timer. 261# 262# Arguments: 263# s incoming socket 264# 265# Side Effects: 266# After event gets cancelled. 267# 268# Results: 269# None. 270# 271 272proc cmdsrv::StopIdleTimer {s} { 273 274 variable data 275 276 if {[info exists data(idleevent)]} { 277 after cancel $data(idleevent) 278 unset data(idleevent) 279 } 280} 281 282# 283# cmdsrv::StartIdleTimer -- 284# 285# Initiates the connection idle timer. 286# 287# Arguments: 288# s incoming socket 289# 290# Side Effects: 291# After event gets posted. 292# 293# Results: 294# None. 295# 296 297proc cmdsrv::StartIdleTimer {s} { 298 299 variable data 300 301 set data(idleevent) \ 302 [after $data(-idletime) [list [namespace current]::SockDone $s]] 303} 304 305# EOF $RCSfile: cmdsrv.tcl,v $ 306 307# Emacs Setup Variables 308# Local Variables: 309# mode: Tcl 310# indent-tabs-mode: nil 311# tcl-basic-offset: 4 312# End: 313 314