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