212 lines
5.8 KiB
Tcl
Executable File
212 lines
5.8 KiB
Tcl
Executable File
#!/bin/bash
|
|
# shellbox
|
|
#
|
|
# This program serves as a box that runs a shell in it.
|
|
# stdout and stdin of the shell are connected to the box.
|
|
# If the shell uses stderr, you should wrap it in a script
|
|
# that redirects stderr to stdout.
|
|
#
|
|
# Whenever the shell dies, it will be restarted.
|
|
#
|
|
# Clients can connect to the box via TCP (e.g. with telnet).
|
|
# Any number of clients can connect at the same time. Input
|
|
# of all clients is merged. Output of the shell is broadcasted
|
|
# to all clients and to stdout of the box.
|
|
#
|
|
# When started with the -paranoid option, the box only accepts
|
|
# connections from localhost.
|
|
#
|
|
# The box intercepts the following commands:
|
|
# exit or quit disconnect from the box
|
|
# clients gives a list of all connected clients
|
|
# kill terminates the shell
|
|
#
|
|
#\
|
|
source /etc/profile
|
|
#\
|
|
exec tclsh "$0" "$@"
|
|
|
|
package req Expect
|
|
log_user 0
|
|
set timeout -1
|
|
|
|
regsub -all {\$} {shellbox by Dirk Zimoch
|
|
$Source: /cvs/G/EPICS/App/scripts/shellbox.tcl,v $
|
|
$Revision: 1.12 $
|
|
$Date: 2015/08/28 14:18:51 $} {} version
|
|
|
|
proc usage {} {
|
|
puts "usage: shellbox \[options\] port command args"
|
|
puts "options: -help : show this text"
|
|
puts " -version : show cvs release"
|
|
puts " -paranoid : allow connections from localhost only"
|
|
puts " -dir directory : set working directory"
|
|
}
|
|
|
|
proc createServer {port} {
|
|
global paranoid
|
|
if [catch {
|
|
if $paranoid {
|
|
socket -server connectionHandler -myaddr localhost $port
|
|
} else {
|
|
socket -server connectionHandler $port
|
|
}
|
|
} msg] {
|
|
puts stderr "shellbox: Can't install server on port $port"
|
|
puts stderr $msg
|
|
exit 2
|
|
}
|
|
puts "server started on port $port"
|
|
}
|
|
|
|
proc bgerror {args} {
|
|
global errorInfo
|
|
catch {puts stderr "shellbox: $errorInfo"}
|
|
}
|
|
|
|
set channels {}
|
|
|
|
proc connectionHandler {channel addr port} {
|
|
global channels command clientinfo
|
|
set peer [fconfigure $channel -peername]
|
|
set clientinfo($channel) [lindex $peer 1]:[lindex $peer 2]
|
|
fconfigure $channel -blocking no -buffering none -encoding binary
|
|
# do some telnet magic to change from line-mode to char-mode
|
|
puts -nonewline $channel "\xff\xfb\x03\xff\xfb\x01"
|
|
fileevent $channel readable "inputHandler $channel"
|
|
forwardOutput "**** new client $clientinfo($channel) ****\n"
|
|
catch {puts $channel "**** '$command' running ****"}
|
|
lappend channels $channel
|
|
catch {puts $channel "clients:\n[getClientlist]"}
|
|
send "\n"
|
|
}
|
|
|
|
proc closeChannel {channel} {
|
|
global channels clientinfo
|
|
catch {puts $channel "**** see you later ****"}
|
|
set index [lsearch $channels $channel]
|
|
set channels [lreplace $channels $index $index]
|
|
forwardOutput "**** client $clientinfo($channel) logged out ****\n"
|
|
close $channel
|
|
unset clientinfo($channel)
|
|
}
|
|
|
|
proc inputHandler {channel} {
|
|
global echo
|
|
set data [read $channel]
|
|
if [eof $channel] {
|
|
closeChannel $channel
|
|
return
|
|
}
|
|
binary scan $data H* hex
|
|
switch -glob -- $hex {
|
|
03 {closeChannel $channel}
|
|
04 {closeChannel $channel}
|
|
18 {killProgram}
|
|
fff4fffd06 {closeChannel $channel}
|
|
ffedfffd06 {closeChannel $channel}
|
|
ff* {puts "ignored $data"}
|
|
default {send -- $data}
|
|
}
|
|
}
|
|
|
|
proc forwardOutput {data {exclude ""}} {
|
|
global channels
|
|
catch {puts -nonewline $data}
|
|
foreach channel $channels {
|
|
if {$channel != $exclude} {
|
|
catch {puts -nonewline $channel $data}
|
|
}
|
|
}
|
|
}
|
|
|
|
proc getClientlist {} {
|
|
global channels clientinfo
|
|
set result ""
|
|
foreach channel $channels {
|
|
append result "$clientinfo($channel)\n"
|
|
}
|
|
return $result
|
|
}
|
|
|
|
set diedyoung 1
|
|
proc startProgram {} {
|
|
global command spawn_id diedyoung directory
|
|
if {[catch {cd $directory} msg]} {
|
|
forwardOutput "**** $msg (trying later) ****\n"
|
|
while {[catch {cd $directory}]} { after 10000 }
|
|
}
|
|
after 10000 set diedyoung 0
|
|
if [catch {
|
|
eval spawn $command
|
|
} msg] {
|
|
catch {puts stderr "shellbox: $msg"}
|
|
exit 3
|
|
}
|
|
forwardOutput "**** '$command' (pid [exp_pid]) started in [pwd] ****\n"
|
|
expect_background {
|
|
"kill" {
|
|
killProgram
|
|
}
|
|
? {
|
|
forwardOutput $expect_out(buffer)
|
|
}
|
|
eof {
|
|
forwardOutput "**** '$command' (pid [exp_pid]) sent EOF ****\n"
|
|
wait [exp_pid]
|
|
if ($diedyoung) {
|
|
forwardOutput "**** first run died within first 10 seconds. I will quit. ****\n"
|
|
exit 4
|
|
}
|
|
forwardOutput "**** '$command' died ****\n"
|
|
after idle startProgram
|
|
}
|
|
}
|
|
}
|
|
|
|
proc killProgram {} {
|
|
global command spawn_id
|
|
set pid [exp_pid]
|
|
forwardOutput "**** killing '$command' (pid $pid)****\n"
|
|
if [catch {
|
|
exec kill -s SIGTERM -$pid
|
|
wait $pid
|
|
forwardOutput "**** '$command' killed ****\n"
|
|
after idle startProgram
|
|
} msg] {
|
|
forwardOutput "**** killing failed: $msg ****\n"
|
|
}
|
|
}
|
|
|
|
set paranoid 0
|
|
set directory .
|
|
|
|
while {[string match -* $argv]} {
|
|
switch -exact -- [lindex $argv 0] {
|
|
"-?" - "-help" { usage; exit 0 }
|
|
"-v" - "-version" {puts $version; exit 0}
|
|
"-paranoid" {set paranoid 1}
|
|
"-echo" {puts stderr "shellbox: option -echo is obsolete"}
|
|
"-dir" {
|
|
set directory [lindex $argv 1]
|
|
set argv [lrange $argv 2 end]
|
|
continue
|
|
}
|
|
default {puts stderr "shellbox: unknown option [lindex $argv 0]"; usage; exit 1}
|
|
}
|
|
set argv [lrange $argv 1 end]
|
|
}
|
|
|
|
set port [lindex $argv 0]
|
|
set command [lrange $argv 1 end]
|
|
if {![regexp {^[0-9]+$} $port] || [llength $command] == 0} {
|
|
usage; exit 1
|
|
}
|
|
|
|
fconfigure stdout -buffering none
|
|
trap {killProgram; exit} {SIGTERM}
|
|
startProgram
|
|
createServer $port
|
|
vwait forever
|
|
|