Overview | Index by: file name | procedure name | procedure call | annotation
shell.tcl (annotations | original source)

#!/usr/bin/tclsh
#
#    Copyright (C) 2010 Alexandros Stergiakis <alsterg@gmail.com>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation, either version 3 of the
#    License, or (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

#//#
# A shell to start a session with a local MikroConf Server.
#
# This should be started, just like bash, from an external program like login, telnetd, sshd.
#
# Note: We don't recognize any espace sequense, because shell
# is normally called from another interface like telnet or ssh, that have
# their own escape sequences.
#//#

package require Tcl 8.5
package require unix 0.5
package require Tclx 8.4
package require comm 4.6.1 ;# Tcllib
package require cmdline 1.3.1 ;# Tcllib

set ESC "\033"

# Time to wait (in secs) for the user's terminal to respond to commands.
set TERM_TIMEOUT 4

# Time to wait (in secs) for for the MikroConf server to respond to commands.
set SERVER_TIMEOUT 15

# Interval to wait (in ms) between two consecutive transmittions of window size update commands to the server.
set RESIZE_DELAY 2000

set KNOWN_CONNECTION_TYPES [list console vty]

set resize_pending 0 ;# '1' indicates that a SIGWINCH has been received within $RESIZE_DELAY

# Find the current window dimentions of the remote terminal screen.
#
# @assume Dependency on the output of the 'resize' busybox applet.
# @error
proc winsize {} {
    global ROWS_DEFAULT COLS_DEFAULT
    
    # 'ttysize' returns 80x24 on error.
    lassign [exec ttysize] cols rows
    
    # On my minicom console session 'ttysize' gives 0x0, whereas 'resize' is able to give the right values.
    # Therefore use 'resize' as fallback.
    if {$cols == 0 || $rows == 0} {
        if {[catch {exec -ignorestderr -- resize} result]} {
            puts stderr "Window size autoprobing failed"
            return [list 80 24]  
        }
        
        regexp {^COLUMNS=([0-9]*);LINES=([0-9]*);.*$} $result -> cols rows
        if {$cols eq "" || $rows eq "" || $cols == 0 || $rows == 0} {
            # Finally, if all the above failed, use defaults (80x24).
            puts stderr "Window size autoprobing failed"
            return [list 80 24]  
        }
    }
    
    return [list $cols $rows]
}

# Called when SIGUSR1 arrives. It resets the terminal screen without breaking the connection
# with the server.
proc reset {} {
    global fifoIn fifoOut
    if {[catch {exec reset} result]} {
        puts stderr "Failed to reset terminal: $result"
    }
    rawInput
    fconfigure $fifoIn -blocking 1 -buffering none -translation binary
    fconfigure $fifoOut -blocking 0 -buffering none -translation binary
}

# Called either explicitly, or when SIGUSR2 arrives.
# Sends a resize command to the server to update the window size for this session.
# It is invoked either by the Server after an explicit user request, by the reception of
# SIGUSR2 signal, or by the reception of the SIGWINCH signal which is generated
# whenever a change happens in the window dimention of the remote terminal.
# Note: We cannot implement this in MikroConf server, because the server is attached to its own terminal.
#           For the same reason stty and other similar tools cannot be executed on the Server either.
proc resize {} {
    global resize_pending
    set resize_pending 0

    lassign [winsize] cols rows
    
    if {[catch {
        send_command resize $COLS $ROWS ;# no return value
    } result]} {
       puts stderr "Error: $result"
    }
}

# Send a resize command to the server to update the window size for this session.
# When using a terminal emulator and resize its window, multiple consequtive
# SIGWINCH signals are generated with very little time difference. This can
# overload the server. Thus we rate limit the commands for window resize sent
# by this procedure.
proc sigwinch {} {
    global RESIZE_DELAY resize_pending
    if {$resize_pending} {
        return
    } else {
        resize
    }
    set resize_pending 1
    after $RESIZE_DELAY resize
}

# Configure stdin & stdout for no buffering, stdin for raw input, and hide entered characters
proc rawInput {} {
    fconfigure stdin -buffering none -blocking 0 -translation auto
    fconfigure stdout -buffering none -blocking 1 -translation crlf
    fconfigure stderr -buffering none -blocking 1 -translation crlf
    if {[catch {exec stty raw -echo} result]} {
        puts stderr "Failed to set terminal options: $result"
    }
}

# Configure stdin & stdout for line buffering, stdin for cooked input, and show entered characters
proc lineInput {} {
    fconfigure stdin -buffering line -blocking 1 -translation auto
    fconfigure stdout -buffering line -blocking 1 -translation auto
    fconfigure stderr -buffering line -blocking 1 -translation auto
    if {[catch {exec stty -raw echo} result]} {
        puts stderr "Failed to set terminal options: $result"
    }
}

# Returns the current cursor position.
#
# @return The current cursor position
# @error
proc curpos {} {
    global fifoIn TERM_TIMEOUT
    
    fileevent stdin readable {}
    fconfigure stdin -blocking 1 ;# We need blocking here
    
    set resp {}
    if {[catch {
        signal trap SIGALRM {error "Timeout waiting"}
        alarm $TERM_TIMEOUT
        puts -nonewline stdout "\033\[6n"
        while {1} {
            append resp [read stdin 1]
        }
    } errMsg]} {
        # Either timeout or an error while evaluating the script.
        signal ignore SIGALRM ;# For the case of error
        error "failed to find cursor position: $errMsg"
    }
    signal ignore SIGALRM
   
    set resp [string range $resp 2 end-1]
    lassign [split $resp ";"] rows cols
    return [list $cols $rows]
}
trace add execution curpos leave restore_stdin

# Execution trace to restore redirection of stdin to FIFO, and other stdin settings.
proc restore_stdin {args} {
    rawInput
    fileevent stdin readable [list stdin2fifoin $fifoIn]
}

# Callback proc to monitor stdin and pass data on to fifoin
# fifoin is the stdin of the server for this session.
#
# @param fifo The input fifo
proc stdin2fifoin {fifo} {
    global app
    
    if {[eof stdin]} {
        puts stderr "Received termination signal from user's terminal."
        quit
    }
    if {[eof $fifo]} {
        puts stderr "Error: Connection from Shell to Server was lost."
        quit
    }
    set data [read stdin]

    if {[catch {
        puts -nonewline $fifo $data
    } result]} {
        puts stderr "Error: While writing to fifo: $result"
        quit
    }
}

# Callback proc to monitor fifoout and pass data on to stdout
# fifoin is the stdout of the server for this session.
#
# @param fifo The output fifo
proc fifoout2stdout {fifo} {
    if {[eof $fifo]} {
        puts stderr "Error: Connection from Server to Shell was lost."
        quit
    }
    
    if {[catch {
        puts -nonewline stdout [read $fifo]
    } result]} {
        puts stderr "Error: While writing to stdout: $result"
        quit
    }
}

if {0} {
    
proc send_command {args} {
    global cid SERVER_TIMEOUT SERVER_PORT

    debuglog "Debug: Sending command to server: $args"
    
    if {[catch {
        signal trap SIGALRM {error "Timeout waiting"}
        alarm $SERVER_TIMEOUT
        puts $cid $args
        flush $cid
        if {[gets $cid resp] == -1} { ;# EOF
            close $cid
            error "EOF while waiting for server response"
        }
    } errMsg]} {
        quit $errMsg
    }
    signal ignore SIGALRM

    debuglog "Debug: Response from server received: $resp"
    
    return $resp
}

}

::comm::comm hook eval {error "Execution in the context of the shell is not permitted"}

# Send a command to MikroConf server and receive the responce.
# Handle the situation where the server doesn't responce.
#
# @param args The command to send.
# @return The responce message from the server.
# @error
proc send_command {args} {
    global SERVER_TIMEOUT SERVER_PORT

    debuglog "Debug: Sending command to server ($SERVER_PORT) : $args"

    if {[catch {
        signal trap SIGALRM {error "Timeout waiting"}
        alarm $SERVER_TIMEOUT
        set resp [::comm::comm send $SERVER_PORT $args]
    } result]} {
        puts stderr "Timeout waiting: $result"
        quit
    }
    signal ignore SIGALRM

    debuglog "Debug: Response from server received: $resp"

    return $resp
}

# Called instead of 'exit' to restore proper terminal settings.
# When 'msg' param is set, we assume we have an erroneous exit condition.
proc quit {{msg ""}} {
    global ESC fifoIn fifoOut fifoInFile fifoOutFile
    if {$msg ne ""} {
        puts stderr "Error: $msg\r"
    }
    debuglog "Debug: Exitting.."
    catch { close $fifoIn; close $fifoOut }
    catch {file delete $fifoInFile $fifoOutFile}
    exec reset
    exec clear
    lineInput
    puts ""
    if {$msg ne ""} {
        exit 1
    }
    exit 0
}

# Print welcome message
proc welcome {} {
    global ESC COLS
    
    debuglog "Debug: Shell started at [clock format [clock seconds]], with Process id:[pid]"
    
    #puts -nonewline "${ESC}c" ;# Clear screen (does not always work, better combine with 'reset', 'clear')
    puts "${ESC}\[30;47m"
    puts [format {%-*s} $COLS "        MikroConf Shell"]
    puts -nonewline [format {%-*s} $COLS "        Copyright 2010 Alexandros Stergiakis <alsterg@gmail.com>"]
    puts "${ESC}\[0m\n"
}

#################
# Main
#################

set options "
    {s.arg {50004}              {Port the server is listening at}}
    {d                          {Print debugging messages}}
    {t.arg {vty}                {Provide connection type string: $KNOWN_CONNECTION_TYPES}}
"

if {[catch {
    array set params [::cmdline::getoptions argv $options "\[options]"]
} errMsg]} {
    puts stderr $errMsg
    exit 1
}

set SERVER_PORT $params(s)
set CONNECTION_TYPE $params(t)

if {$CONNECTION_TYPE ni $KNOWN_CONNECTION_TYPES} {
    puts stderr "Unknown connection type"
    exit 1
}

# Set-up logging
if {$params(d)} {
    proc debuglog {msg} {puts stderr $msg}
} else {
    proc debuglog {msg} {}
}

lassign [winsize] COLS ROWS

# Connect
#set cid [unix_sockets::connect $SERVER_PORT]
#fconfigure $cid -blocking 1 -buffering line

if {[catch {
    set resp [send_command register [pid] [exec whoami] $CONNECTION_TYPE $COLS $ROWS]
} result]} {
   puts stderr $result
   exit 1
}

welcome

debuglog "Debug: Reguest Accepted. Assigned FIFOs: $resp"
lassign $resp fifoInFile fifoOutFile

# Open session

debuglog "Debug: Setting raw input and output"
rawInput

debuglog "Debug: Opening $fifoInFile channel for writing (stdin->fifoin)"

if {[catch {
    signal trap SIGALRM {error "Timeout waiting"}
    alarm $SERVER_TIMEOUT
    set fifoIn [open $fifoInFile w]
} errMsg]} {
    quit "Timeout trying to connect to assigned Input FIFO: $errMsg"
}
signal ignore SIGALRM

fconfigure $fifoIn -blocking 1 -buffering none -translation binary

debuglog "Debug: Opening $fifoOutFile channel for reading (fifoout->stdout)"

if {[catch {
    signal trap SIGALRM {error "Timeout waiting"}
    alarm $SERVER_TIMEOUT
    set fifoOut [open $fifoOutFile r]
} errMsg]} {
    quit "Timeout trying to connect to assigned Output FIFO: $errMsg"
}
signal ignore SIGALRM

fconfigure $fifoOut -blocking 0 -buffering none -translation binary

debuglog "Debug: Registering I/O handlers"
fileevent stdin readable [list stdin2fifoin $fifoIn]
fileevent $fifoOut readable [list fifoout2stdout $fifoOut]

debuglog "Debug: Registering Signal handlers"

signal ignore SIGINT ;# Ctrl-C
signal ignore SIGABRT ;# Ctrl-\ 
signal ignore SIGTSTP ;# Ctrl-Z
signal unblock {SIGHUP QUIT TERM}
signal trap {SIGHUP QUIT TERM} {
    quit "Received signal to terminate"
}

signal trap SIGUSR1 reset
signal trap SIGUSR2 resize
signal trap SIGWINCH sigwinch

debuglog "Debug: Connected"

set __forever__ 0
vwait __forever__ 


Overview | Index by: file name | procedure name | procedure call | annotation
File generated 2010-03-13 at 22:28.