#
# 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 -encoding utf-8 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 -encoding utf-8 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:

