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.