311 lines
5.9 KiB
Tcl
311 lines
5.9 KiB
Tcl
#
|
||
# 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:
|
||
#
|
||
# # tclsh8.6
|
||
# % 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.4
|
||
package require Thread 2.5
|
||
|
||
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:
|
||
|