File: cmdsrv.tcl

package info (click to toggle)
tclthread3 3.0.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,752 kB
  • sloc: ansic: 8,259; tcl: 1,711; sh: 436; makefile: 38
file content (310 lines) | stat: -rw-r--r-- 5,879 bytes parent folder | download | duplicates (2)
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: