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