1
0
Fork 0
mirror of git://git.code.sf.net/p/zsh/code synced 2025-10-23 04:30:24 +02:00

18202: New TCP function system plus small error message change in ztcp.

This commit is contained in:
Peter Stephenson 2003-02-06 12:21:49 +00:00
parent 809ab19dff
commit 5c1f3b65a6
27 changed files with 2139 additions and 5 deletions

156
Functions/TCP/tcp_alias Normal file
View file

@ -0,0 +1,156 @@
# Create an alias for a TCP session.
#
# The syntax is similar to the `alias' builtin. Aliases with a trailing
# `=' are assigned, while those without are listed.
#
# The alias can be used to refer to the session, however any output
# from the session will be shown using information for the base
# session name. Likewise, any other reference to the session's file
# descriptor will cause the original session name rather than the alias to
# be used.
#
# It is an error to attempt to create an alias for a non-existent session.
# The alias will be removed when the session is closed.
#
# An alias can be reused without the session having to be closed.
# However, a base session name cannot be used as an alias. If this
# becomes necessary, the base session should be renamed with tcp_rename
# first.
#
# With no arguments, list aliases.
#
# With the option -d, delete the alias. No value is allowed in this case.
#
# With the option -q (quiet), just return status 1 on failure. This
# does not apply to bad syntax, which is always reported. Bad syntax
# includes deleting aliases when supplying a value.
emulate -L zsh
setopt extendedglob cbases
local opt quiet base value alias delete arg match mbegin mend fd array
integer stat index
while getopts "qd" opt; do
case $opt in
(q) quiet=1
;;
(d) delete=1
;;
(*) return 1
;;
esac
done
(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
if (( ! $# )); then
if (( ${#tcp_aliases} )); then
for fd value in ${(kv)tcp_aliases}; do
for alias in ${=value}; do
print -r - \
"${alias}: alias for session ${tcp_by_fd[$fd]:-unnamed fd $fd}"
done
done
fi
return 0
fi
for arg in $*; do
if [[ $arg = (#b)([^=]##)=(*) ]]; then
if [[ -n $delete ]]; then
print "$0: value given with deletion command." >&2
stat=1
continue
fi
alias=$match[1]
base=$match[2]
if [[ -z $base ]]; then
# hmm, this is very nearly a syntax error...
[[ -z $quiet ]] && print "$0: empty value for alias $alias" >&2
stat=1
continue
fi
if [[ ${+tcp_by_name} -eq 0 || -z ${tcp_by_name[$base]} ]]; then
[[ -z $quiet ]] && print "$0: no base session \`$base' for alias"
stat=1
continue
fi
if [[ -n ${tcp_by_name[$alias]} ]]; then
# already exists, OK if this is an alias...
fd=${tcp_by_name[$alias]}
array=(${=tcp_aliases[$fd]})
if [[ -n ${array[(r)$alias]} ]]; then
# yes, we're OK; delete the old alias.
unset "tcp_by_name[$alias]"
index=${array[(i)$alias]}
array=(${array[1,index-1]} ${array[index+1,-1]})
if [[ -z "$array" ]]; then
unset "tcp_aliase[$fd]"
else
tcp_aliases[$fd]="$array"
fi
else
# oops
if [[ -z $quiet ]]; then
print "$0: \`$alias' is already a session name." >&2
fi
stat=1
continue
fi
fi
(( ! ${+tcp_aliases} )) && typeset -gA tcp_aliases
fd=${tcp_by_name[$base]}
if [[ -n ${tcp_aliases[$fd]} ]]; then
tcp_aliases[$fd]+=" $alias"
else
tcp_aliases[$fd]=$alias
fi
tcp_by_name[$alias]=$fd
if zmodload -i zsh/parameter; then
if (( ${+functions[tcp_on_alias]} )); then
tcp_on_alias $alias $fd
fi
fi
else
alias=$arg
fd=${tcp_by_name[$alias]}
if [[ -z $fd ]]; then
print "$0: no such alias \`$alias'" >&2
stat=1
continue
fi
# OK if this is an alias...
array=(${=tcp_aliases[$fd]})
if [[ -n ${array[(r)$alias]} ]]; then
# yes, we're OK
if [[ -n $delete ]]; then
unset "tcp_by_name[$alias]"
index=${array[(i)$alias]}
array=(${array[1,index-1]} ${array[index+1,-1]})
if [[ -z "$array" ]]; then
unset "tcp_aliases[$fd]"
else
tcp_aliases[$fd]="$array"
fi
if zmodload -i zsh/parameter; then
if (( ${+functions[tcp_on_unalias]} )); then
tcp_on_unalias $alias $fd
fi
fi
else
print -r - \
"${alias}: alias for session ${tcp_by_fd[$fd]:-unnamed fd $fd}"
fi
else
# oops
if [[ -z $quiet ]]; then
print "$0: \`$alias' is a session name." >&2
fi
stat=1
continue
fi
fi
done
return $stat

134
Functions/TCP/tcp_close Normal file
View file

@ -0,0 +1,134 @@
# Usage:
# tcp_close [-q] [ -a | session ... ]
# -a means all sessions.
# -n means don't close a fake session's fd.
# -q means quiet.
#
# Accepts the -s and -l arguments for consistenty with other functions,
# but there is no particular gain in using them
emulate -L zsh
setopt extendedglob cbases
local all quiet opt alias noclose
local -a sessnames
while getopts "aql:ns:" opt; do
case $opt in
(a) all=1
;;
(q) quiet=1
;;
(l) sessnames+=(${(s.,.)OPTARG})
;;
(n) noclose=1
;;
(s) sessnames+=($OPTARG)
;;
(*) return 1
;;
esac
done
(( OPTIND > 1 )) && shift $(( OPTIND - 1))
if [[ -n $all ]]; then
if (( $# )); then
print "Usage: $0 [ -q ] [ -a | [ session ... ] ]" >&2
return 1
fi
sessnames=(${(k)tcp_by_name})
if (( ! ${#sessnames} )); then
[[ -z $quiet ]] && print "No TCP sessions open." >&2
return 1
fi
fi
sessnames+=($*)
if (( ! ${#sessnames} )); then
sessnames+=($TCP_SESS)
fi
if (( ! ${#sessnames} )); then
[[ -z $quiet ]] && print "No current TCP session." >&2
return 1
fi
local tcp_sess fd
integer stat curstat
# Check to see if the fd is opened for a TCP session, or was opened
# to a pre-existing fd. We could remember this from tcp_open.
local -A ztcp_fds
local line match mbegin mend
if zmodload -e zsh/net/tcp; then
ztcp | while read line; do
if [[ $line = (#b)*fd\ ([0-9]##) ]]; then
ztcp_fds[$match[1]]=1
fi
done
fi
for tcp_sess in $sessnames; do
curstat=0
fd=${tcp_by_name[$tcp_sess]}
if [[ -z $fd ]]; then
print "No TCP session $tcp_sess!" >&2
stat=1
continue
fi
# We need the base name if this is an alias.
tcp_sess=${tcp_by_fd[$fd]}
if [[ -z $tcp_sess ]]; then
if [[ -z $quiet ]]; then
print "Aaargh! Session for fd $fd has disappeared!" >&2
fi
stat=1
continue
fi
if [[ ${+tcp_aliases} -ne 0 && -n ${tcp_aliases[$fd]} ]]; then
for alias in ${=tcp_aliases[$fd]}; do
if (( ${+functions[tcp_on_unalias]} )); then
tcp_on_unalias $alias $fd
fi
unset "tcp_by_name[$alias]"
done
unset "tcp_aliases[$fd]"
fi
# Don't return just because the zle handler couldn't be uninstalled...
if [[ -o zle ]]; then
zle -F $fd || print "[Ignoring...]" >&2
fi
if [[ -n $ztcp_fds[$fd] ]]; then
# It's a ztcp session.
if ! ztcp -c $fd; then
stat=1
curstat=1
fi
elif [[ -z $noclose ]]; then
# It's not, just close it normally.
# Careful: syntax for closing fd's is quite strict.
if [[ ${#fd} -gt 1 ]]; then
[[ -z $quiet ]] && print "Can't close fd $fd; will leave open." >&2
else
eval "exec $fd>&-"
fi
fi
unset "tcp_by_name[$tcp_sess]"
unset "tcp_by_fd[$fd]"
if [[ -z $quiet && $curstat -eq 0 ]]; then
print "Session $tcp_sess successfully closed."
fi
[[ $tcp_sess = $TCP_SESS ]] && unset TCP_SESS
if (( ${+functions[tcp_on_close]} )); then
tcp_on_close $tcp_sess $fd
fi
done
return $stat

View file

@ -0,0 +1,3 @@
tcp_send $* || return 1
tcp_read -d -t ${TCP_TIMEOUT:=0.3}
return 0

115
Functions/TCP/tcp_expect Normal file
View file

@ -0,0 +1,115 @@
# Expect one of a series of regular expressions from $TCP_SESS.
# Can make backreferences to be handled by $match. Returns 0 for
# successful match, 1 for error, 2 for timeout.
#
# This function has no facility for conditionally calling code based
# the regular expression found. This should be done in the calling code
# by testing $TCP_LINE, which contains the line which matched the
# regular expression. The complete set of lines read while waiting for
# this line is available in the array $tcp_expect_lines (including $TCP_LINE
# itself which will be the final element). Alternatively, use -p pind
# which sets $pind to the index of the pattern which matched. It
# will be set to 0 otherwise.
#
# Many of the options are passed straight down to tcp_read.
#
# Options:
# -a Run tcp_expect across all sessions; the first pattern matched
# from any session is used. The TCP output prompt can be
# used to decide which session matched.
# -l list
# Comma-separated list of sessions as for tcp_read.
# -p pv If the Nth of a series of patterns matches, set the parameter
# whose name is given by $pv to N; in the case of a timeout,
# set it to -1; otherwise (unless the function exited prematurely),
# set it to 0.
# To avoid namespace clashes, the parameter's name must
# not begin with `_expect'.
# -q Quiet, passed down to tcp_read. Bad option and argument
# usage is always reported.
# -s sess
# Expect from session sess. May be repeated for multiple sessions.
# -t to Timeout in seconds (may be floating point) per read operation.
# tcp_expect will only time out if every read operation takes longer
# than to
# -T TO Overall timeout; tcp_expect will time out if the overall operation
# takes longer than this many seconds.
emulate -L zsh
setopt extendedglob
# Get extra accuracy by making SECONDS floating point locally
typeset -F SECONDS
# Variables are all named _expect_* to avoid problems with the -p param.
local _expect_opt _expect_pvar
local -a _expect_read_args
float _expect_to1 _expect_to_all _expect_to _expect_new_to
integer _expect_i _expect_stat
while getopts "al:p:qs:t:T:" _expect_opt; do
case $_expect_opt in
(a) _expect_read_args+=(-a)
;;
(l) _expect_read_args+=(-l $OPTARG)
;;
(p) _expect_pvar=$OPTARG
if [[ $_expect_pvar != [a-zA-Z_][a-zA-Z_0-9]# ]]; then
print "invalid parameter name: $_expect_pvar" >&2
return 1
fi
if [[ $_expect_pvar = _expect* ]]; then
print "$0: parameter names staring \`_expect' are reserved."
return 1
fi
eval "$_expect_pvar=0"
;;
(q) _expect_read_args+=(-q)
;;
(s) _expect_read_args+=(-s $OPTARG)
;;
(t) _expect_to1=$OPTARG
;;
(T) _expect_to_all=$(( SECONDS + $OPTARG ))
;;
(\?) return 1
;;
(*) print Unhandled option $_expect_opt, complain >&2
return 1
;;
esac
done
(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
tcp_expect_lines=()
while true; do
if (( _expect_to_all || _expect_to1 )); then
_expect_to=0
(( _expect_to1 )) && (( _expect_to = _expect_to1 ))
if (( _expect_to_all )); then
# overall timeout, see if it has already triggered
if (( (_expect_new_to = (_expect_to_all - SECONDS)) <= 0 )); then
[[ -n $_expect_pvar ]] && eval "$_expect_pvar=-1"
return 2
fi
if (( _expect_to <= 0 || _expect_new_to < _expect_to )); then
_expect_to=$_expect_new_to
fi
fi
tcp_read $_expect_read_args -t $_expect_to
_expect_stat=$?
else
tcp_read $_expect_read_args -b
_expect_stat=$?
fi
if (( _expect_stat )); then
[[ -n $_expect_pvar ]] && eval "$_expect_pvar=-1"
return $_expect_stat
fi
tcp_expect_lines+=($TCP_LINE)
for (( _expect_i = 1; _expect_i <= $#; _expect_i++ )); do
if [[ "$TCP_LINE" = ${~argv[_expect_i]} ]]; then
[[ -n $_expect_pvar ]] && eval "$_expect_pvar=\$_expect_i"
return 0
fi
done
done

View file

@ -0,0 +1,35 @@
local line name=${tcp_by_fd[$1]}
if [[ -n $name ]]
then
local TCP_INVALIDATE_ZLE
if (( $# > 2 )); then
zle -I
## debugging only
# print "Flags on the play:" ${argv[3,-1]}
else
TCP_INVALIDATE_ZLE=1
fi
if ! tcp_read -d -u $1; then
[[ -n $TCP_INVALIDATE_ZLE ]] && zle -I
print "[TCP fd $1 (session $name) gone awol; removing from poll list]" >& 2
zle -F $1
return 1
fi
return 0
else
zle -I
# Handle fds not in the TCP set similarly.
# This does the drain thing, to try and get as much data out as possible.
if ! read line <&$1; then
print "[Reading on $1 failed; removing from poll list]" >& 2
zle -F $1
return 1
fi
line="fd$1:$line"
local newline
while read -t newline <&$1; do
line="${line}
fd$1:$newline"
done
fi
print -r - $line

94
Functions/TCP/tcp_log Normal file
View file

@ -0,0 +1,94 @@
# Log TCP output.
#
# Argument: Output filename.
#
# Options:
# -a Append. Otherwise the existing file is truncated without warning.
# (N.B.: even if logging was already active to it!)
# -s Per-session logs. Output to <filename>1, <filename>2, etc.
# -c Close logging.
# -n/-N Turn off or on normal output; output only goes to the logfile, if
# any. Otherwise, output also appears interactively. This
# can be given with -c (or any other option), then no output
# goes anywhere. However, input is still handled by the usual
# mechanisms --- $tcp_lines and $TCP_LINE are still set, hence
# tcp_expect still works. Equivalent to (un)setting TCP_SILENT.
#
# With no options and no arguments, print the current configuration.
#
# Per-session logs are raw output, otherwise $TCP_PROMPT is prepended
# to each line.
emulate -L zsh
setopt cbases extendedglob
local opt append sess close
integer activity
while getopts "ascnN" opt; do
(( activity++ ))
case $opt in
# append to existing file
a) append=1
;;
# per-session
s) sess=1
;;
# close
c) close=1
;;
# turn off interactive output
n) TCP_SILENT=1
;;
# turn on interactive output
N) unset TCP_SILENT
;;
# incorrect option
\?) return 1
;;
# correct option I forgot about
*) print "$0: option -$opt not handled, oops." >&2
return 1
;;
esac
done
(( OPTIND > 1 )) && shift $(( OPTIND - 1))
if [[ -n $close ]]; then
if (( $# )); then
print "$0: too many arguments for -c" >&2
return 1
fi
unset TCP_LOG TCP_LOG_SESS
return 0
fi
if (( $# == 0 && ! activity )); then
print "\
Per-session log: ${TCP_LOG_SESS:-<none>}
Overall log: ${TCP_LOG:-<none>}
Silent? ${${TCP_SILENT:+yes}:-no}"
return 0
fi
if (( $# != 1 )); then
print "$0: wrong number of arguments" >&2
return 1
fi
if [[ -n $sess ]]; then
TCP_LOG_SESS=$1
if [[ -z $append ]]; then
local sesslogs
integer i
sesslogs=(${TCP_LOG_SESS}*(N))
# yes, i know i can do this with multios
for (( i = 1; i <= $#sesslogs; i++ )); do
: >$sesslogs[$i]
done
fi
else
TCP_LOG=$1
[[ -z $append ]] && : >$TCP_LOG
fi
return 0

197
Functions/TCP/tcp_open Normal file
View file

@ -0,0 +1,197 @@
# Open a TCP session, add it to the list, handle it with zle if that's running.
# Unless using -a, -f, -l or -s, first two arguments are host and port.
#
# Remaining argument, if any, is the name of the session, which mustn't
# clash with an existing one. If none is given, the number of the
# connection is used (i.e. first connection is 1, etc.), or the first
# available integer if that is already in use.
#
# Session names, whether provided on the command line or in the
# .ztcp_sessions file should not be `clever'. A clever name is one
# with characters that won't work. This includes whitespace and an
# inconsistent set of punctuation characters. If in doubt, stick
# to alphanumeric, underscore and non-initial hyphen.
#
# -a fd Accept a connection on fd and make that the session.
# This will block until a successful incoming connection is received.
#
# fd is probably a value returned by ztcp -l; no front-end
# is currently provided for that but it should simply be
# a matter of calling `ztcp -l port' and storing $REPLY, then
# closing the listened port with `ztcp -c $stored_fd'.
#
# -f fd `Fake' tcp connection on the given file descriptor. This
# could be, for example, a file descriptor already opened to
# a named pipe. It should not be a regular file, however.
# Note that it is not a good idea for two different sessions
# to be attempting to read from the same named pipe, so if
# both ends of the pipe are to be handled by zsh, at least
# one should use the `-z' option.
#
# -l sesslist
# -s sessname
# Open by session name or comma separated list; either may
# be repeated as many times as necessary. The session must be
# listed in the file ${ZDOTDIR:-$HOME}/.ztcp_sessions. Lines in
# this file look exactly like a tcp_open command line except the
# session name is at the start, for example
# sess1 pwspc 2811
# has the effect of
# tcp_open pwspc 2811 sess1
# Remaining arguments (other than options) to tcp_open are
# not allowed. Options in .ztcp_sessions are not handled.
# The file must be edited by hand.
#
# -z Don't install a zle handler for reading on the file descriptor.
# Otherwise, if zle is enabled, the file descriptor will
# be tested while at the shell prompt and any input automatically
# printed in the same way as job control notification.
#
# If a session is successfully opened, and if the function `tcp_on_open'
# exists, it is run with the arguments session_name, session_fd.
emulate -L zsh
setopt extendedglob cbases
zmodload -i zsh/net/tcp || return 1
autoload -U zgprintf tcp_alias tcp_close tcp_command tcp_expect tcp_fd_handler
autoload -U tcp_log tcp_output tcp_proxy tcp_read tcp_rename tcp_send
autoload -U tcp_sess tcp_spam tcp_talk tcp_wait
local opt accept fake nozle sessfile sess quiet
local -a sessnames sessargs
integer stat
while getopts "a:f:l:qs:z" opt; do
case $opt in
(a) accept=$OPTARG
if [[ $accept != [[:digit:]]## ]]; then
print "option -a takes a file descriptor" >&2
return 1
fi
;;
(f) fake=$OPTARG
if [[ $fake != [[:digit:]]## ]]; then
print "option -f takes a file descriptor" >&2
return 1
fi
;;
(l) sessnames+=(${(s.,.)OPTARG})
;;
(q) quiet=1
;;
(s) sessnames+=($OPTARG)
;;
(z) nozle=1
;;
(*) return 1
;;
esac
done
(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
(( ${+tcp_by_fd} )) || typeset -gA tcp_by_fd
(( ${+tcp_by_name} )) || typeset -gA tcp_by_name
typeset -A sessassoc
if (( ${#sessnames} )); then
if [[ $# -ne 0 || -n $accept || -n $fake ]]; then
print "Incompatible arguments with \`-s' option." >&2
return 1
fi
for sess in ${sessnames}; do
sessassoc[$sess]=
done
sessfile=${ZDOTDIR:-$HOME}/.ztcp_sessions
if [[ ! -r $sessfile ]]; then
print "No session file: $sessfile" >&2
return 1
fi
while read -A sessargs; do
[[ ${sessargs[1]} = '#'* ]] && continue
if (( ${+sessassoc[${sessargs[1]}]} )); then
sessassoc[${sessargs[1]}]="${sessargs[2,-1]}"
fi
done < $sessfile
for sess in ${sessnames}; do
if [[ -z $sessassoc[$sess] ]]; then
print "Couldn't find session $sess in $sessfile." >&2
return 1
fi
done
else
if [[ -z $accept && -z $fake ]]; then
if (( $# < 2 )); then
set -- wrong number of arguments
else
host=$1 port=$2
shift $(( $# > 1 ? 2 : 1 ))
fi
fi
if [[ -n $1 ]]; then
sessnames=($1)
shift
else
sessnames=($(( ${#tcp_by_fd} + 1 )))
while [[ -n $tcp_by_name[$sessnames[1]] ]]; do
(( sessnames[1]++ ))
done
fi
sessassoc[$sessnames[1]]="$host $port"
fi
if (( $# )); then
print "Usage: $0 [-z] [-a fd | -f fd | host port [ session ] ]
$0 [-z] [ -s session | -l sesslist ] ..." >&2
return 1
fi
local REPLY fd
for sess in $sessnames; do
if [[ -n $tcp_by_name[$sess] ]]; then
print "Session \`$sess' already exists." >&2
return 1
fi
sessargs=()
if [[ -n $fake ]]; then
fd=$fake;
else
if [[ -n $accept ]]; then
ztcp -a $accept || return 1
else
sessargs=(${=sessassoc[$sess]})
ztcp $sessargs || return 1
fi
fd=$REPLY
fi
tcp_by_fd[$fd]=$sess
tcp_by_name[$sess]=$fd
[[ -o zle && -z $nozle ]] && zle -F $fd tcp_fd_handler
if [[ -z $quiet ]]; then
if (( ${#sessargs} )); then
print "Session $sess" \
"(host $sessargs[1], port $sessargs[2] fd $fd) opened OK."
else
print "Session $sess (fd $fd) opened OK."
fi
fi
# needed for new completion system, so I'm not too sanguine
# about requiring this here...
if zmodload -i zsh/parameter; then
if (( ${+functions[tcp_on_open]} )); then
tcp_on_open $sess $fd
fi
fi
done
if [[ -z $TCP_SESS ]]; then
[[ -z $quiet ]] && print "Setting default TCP session $sessnames[1]"
TCP_SESS=$sessnames[1]
fi
return $stat

65
Functions/TCP/tcp_output Normal file
View file

@ -0,0 +1,65 @@
emulate -L zsh
setopt extendedglob
local opt tprompt sess read_fd tpat quiet
while getopts "F:P:qS:" opt; do
case $opt in
(F) read_fd=$OPTARG
;;
(P) tprompt=$OPTARG
;;
(q) quiet=1
;;
(S) sess=$OPTARG
;;
(*) [[ $opt != \? ]] && print -r "Can't handle option $opt" >&2
return 1
;;
esac
done
(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
# Per-session logs don't have the session discriminator in front.
if [[ -n $TCP_LOG_SESS ]]; then
print -r -- "$*" >>${TCP_LOG_SESS}.$sess
fi
# Always add the TCP prompt. We used only to do this with
# multiple sessions, but it seems always to be useful to know
# where data is coming from; also, it allows more predictable
# behaviour in tcp_expect.
if [[ -n $tprompt ]]; then
zgprintf -R -%s=$sess -%f=$read_fd -- $tprompt
# We will pass this back up.
REPLY="$REPLY$*"
else
REPLY="$*"
fi
if [[ -n $TCP_LOG ]]; then
print -r -- $REPLY >>${TCP_LOG}
fi
if [[ -z $quiet ]]; then
local skip=
if [[ ${#tcp_filter} -ne 0 ]]; then
# Allow tcp_filter to be an associative array, though
# it doesn't *need* to be.
for tpat in ${(v)tcp_filter}; do
[[ $REPLY = ${~tpat} ]] && skip=1 && break
done
fi
if [[ -z $skip ]]; then
# Check flag passed down probably from tcp_fd_handler:
# if we have output, we are in zle and need to fix the display first.
# (The shell is supposed to be smart enough that you can replace
# all the following with
# [[ -o zle ]] && zle -I
# but I haven't dared try it yet.)
if [[ -n $TCP_INVALIDATE_ZLE ]]; then
zle -I
# Only do this the first time.
unset TCP_INVALIDATE_ZLE
fi
print -r -- $REPLY
fi
fi

31
Functions/TCP/tcp_proxy Normal file
View file

@ -0,0 +1,31 @@
# Listen on the given port and for every connection, start a new
# command (defaults to $SHELL) in the background on the accepted fd.
# WARNING: this can leave your host open to the universe. For use
# in a restricted fashion on a secure network.
#
# Remote logins are much more efficient...
local TCP_LISTEN_FD
trap '[[ -n $TCP_LISTEN_FD ]] && ztcp -c $TCP_LISTEN_FD; return 1' \
HUP INT TERM EXIT PIPE
if [[ $1 != <-> ]]; then
print "Usage: $0 port [cmd args... ]" >&2
return 1
fi
integer port=$1
shift
ztcp -l $port || return 1
TCP_LISTEN_FD=$REPLY
(( $# )) || set -- ${SHELL:-zsh}
local cmd=$1
shift
while ztcp -a $TCP_LISTEN_FD; do
# hack to expand aliases without screwing up arguments
eval $cmd '$* <&$REPLY >&$REPLY 2>&$REPLY &'
# Close the session fd; we don't need it here any more.
ztcp -c $REPLY
done

207
Functions/TCP/tcp_read Normal file
View file

@ -0,0 +1,207 @@
# Helper function for reading input from a TCP connection.
# Actually, the input doesn't need to be a TCP connection at all, it
# is simply an input file descriptor. However, it must be contained
# in ${tcp_by_fd[$TCP_SESS]}. This is set set by tcp_open, but may be
# set by hand. (Note, however, the blocking/timeout behaviour is usually
# not implemented for reading from regular files.)
#
# The default behaviour is simply to read any single available line from
# the input fd and print it. If a line is read, it is stored in the
# parameter $TCP_LINE; this always contains the last line successfully
# read. Any chunk of lines read in are stored in the array $tcp_lines;
# this always contains a complete list of all lines read in by a single
# execution of this function and hence may be empty. The fd corresponding
# to $TCP_LINE is stored in $TCP_LINE_FD (this can be turned into a
# session by looking up in $tcp_by_fd).
#
# Printed lines are preceded by $TCP_PROMPT. This may contain two
# percent escapes: %s for the current session, %f for the current file
# descriptor. The default is `T[%s]:'. The prompt is not printed
# to per-session logs where the source is unambiguous.
#
# The function returns 0 if a read succeeded, even if (using -d) a
# subsequent read failed.
#
# The behaviour is modified by the following options.
#
# -a Read from all fds, not just the one given by TCP_SESS.
#
# -b The first read blocks until some data is available for reading.
#
# -d Drain all pending input; loop until no data is available.
#
# -l sess1,sess2,...
# Gives a list of sessions to read on. Equivalent to
# -u ${tcp_by_name[sess1]} -u ${tcp_by_name[sess2]} ...
# Multiple -l options also work.
#
# -q Quiet; if $TCP_SESS is not set, just return 1, but don't print
# an error message.
#
# -s sess
# Gives a single session; the option may be repeated.
#
# -t TO On each read (the only read unless -d was also given), time out
# if nothing was available after TO seconds (may be floating point).
# Otherwise, the function will return immediately when no data is
# available.
#
# If combined with -b, the function will always wait for the
# first data to become available; hence this is not useful unless
# -d is specified along with -b, in which case the timeout applies
# to data after the first line.
# -u fd Read from fd instead of the default session; may be repeated for
# multiple sessions. Can be a comma-separated list, too.
# -T TO This sets an overall timeout, again in seconds.
emulate -L zsh
setopt extendedglob cbases
# set -x
zmodload -i zsh/mathfunc
local opt drain line quiet block read_fd all sess
local -A read_fds
read_fds=()
float timeout timeout_all endtime
integer stat
while getopts "abdl:qs:t:T:u:" opt; do
case $opt in
# Read all sessions.
(a) all=1
;;
# Block until we receive something.
(b) block=1
;;
# Drain all pending input.
(d) drain=1
;;
(l) for sess in ${(s.,.)OPTARG}; do
read_fd=${tcp_by_name[$sess]}
if [[ -z $read_fd ]]; then
print "$0: no such session: $sess" >&2
return 1
fi
read_fds[$read_fd]=1
done
;;
# Don't print an error mesage if there is no TCP connection,
# just return 1.
(q) quiet=1
;;
# Add a single session to the list
(s) read_fd=${tcp_by_name[$OPTARG]}
if [[ -z $read_fd ]]; then
print "$0: no such session: $sess" >&2
return 1
fi
read_fds[$read_fd]=1
;;
# Per-read timeout: wait this many seconds before
# each read.
(t) timeout=$OPTARG
[[ -n $TCP_READ_DEBUG ]] && print "Timeout per-operations is $timeout" >&2
;;
# Overall timeout: return after this many seconds.
(T) timeout_all=$OPTARG
;;
# Read from given fd(s).
(u) for read_fd in ${(s.,.)OPTARG}; do
if [[ $read_fd != (0x[[:xdigit:]]##|[[:digit:]]##) ]]; then
print "Bad fd in $OPTARG" >&2
return 1
fi
read_fds[$((read_fd))]=1
done
;;
(*) [[ $opt != \? ]] && print Unhandled option, complain: $opt >&2
return 1
;;
esac
done
if [[ -n $all ]]; then
read_fds=(${(kv)tcp_by_fd})
elif (( ! $#read_fds )); then
if [[ -z $TCP_SESS ]]; then
[[ -z $quiet ]] && print "No tcp connection open." >&2
return 1
elif [[ -z $tcp_by_name[$TCP_SESS] ]]; then
print "TCP session $TCP_SESS has gorn!" >&2
return 1
fi
read_fds[$tcp_by_name[$TCP_SESS]]=1
fi
tcp_lines=()
local helper_stat=2 skip tpat reply REPLY
float newtimeout
# Get extra accuracy by making SECONDS floating point locally
typeset -F SECONDS
if (( timeout_all )); then
(( endtime = SECONDS + timeout_all ))
fi
zmodload -i zsh/zselect
if [[ -n $block ]]; then
if (( timeout_all )); then
# zselect -t uses 100ths of a second
zselect -t $(( int(100*timeout_all + 0.5) )) ${(k)read_fds} ||
return $helper_stat
else
zselect ${(k)read_fds} || return $helper_stat
fi
fi
while (( ${#read_fds} )); do
if [[ -n $block ]]; then
# We already have data waiting this time through.
unset block
else
if (( timeout_all )); then
(( (newtimeout = endtime - SECONDS) <= 0 )) && return 2
if (( ! timeout || newtimeout < timeout )); then
(( timeout = newtimeout ))
fi
fi
if (( timeout )); then
if [[ -n $TCP_READ_DEBUG ]]; then
print "[tcp_read: selecting timeout $timeout on ${(k)read_fds}]" >&2
fi
zselect -t $(( int(timeout*100 + 0.5) )) ${(k)read_fds} ||
return $helper_stat
else
if [[ -n $TCP_READ_DEBUG ]]; then
print "[tcp_read: selecting no timeout on ${(k)read_fds}]" >&2
fi
zselect -t 0 ${(k)read_fds} || return $helper_stat
fi
fi
if [[ -n $TCP_READ_DEBUG ]]; then
print "[tcp_read: returned fds ${reply}]" >&2
fi
for read_fd in ${reply[2,-1]}; do
if ! read -r line <&$read_fd; then
unset "read_fds[$read_fd]"
stat=1
continue
fi
helper_stat=0
sess=${tcp_by_fd[$read_fd]}
tcp_output -P "${TCP_PROMPT:=<-[%s] }" -S $sess -F $read_fd \
${TCP_SILENT:+-q} "$line"
# REPLY is now set to the line with an appropriate prompt.
tcp_lines+=($REPLY)
TCP_LINE=$REPLY TCP_LINE_FD=$read_fd
# Only handle one line from one device at a time unless draining.
[[ -z $drain ]] && return $stat
done
done
return $stat

43
Functions/TCP/tcp_rename Normal file
View file

@ -0,0 +1,43 @@
# Rename session OLD (defaults to current session) to session NEW.
# Does not handle aliases; use tcp_alias for all alias redefinitions.
local old new
if (( $# == 1 )); then
old=$TCP_SESS
new=$1
elif (( $# == 2 )); then
old=$1
new=$2
else
print "Usage: $0 OLD NEW" >&2
return 1
fi
local fd=$tcp_by_name[$old]
if [[ -z $fd ]]; then
print "No such session: $old" >&2
return 1
fi
if [[ -n $tcp_by_name[$new] ]]; then
print "Session $new already exists." >&2
return 1
fi
# Can't rename an alias
if [[ $tcp_by_fd[$fd] != $old ]]; then
print "Use tcp_alias to redefine an alias." >&2
return 1
fi
tcp_by_name[$new]=$fd
unset "tcp_by_name[$old]"
tcp_by_fd[$fd]=$new
[[ $TCP_SESS = $old ]] && TCP_SESS=$new
if zmodload -i zsh/parameter; then
if (( ${+functions[tcp_on_rename]} )); then
tcp_on_rename $new $fd $old
fi
fi

67
Functions/TCP/tcp_send Normal file
View file

@ -0,0 +1,67 @@
emulate -L zsh
setopt extendedglob cbases
local opt quiet all sess fd nonewline
local -a sessions write_fds
while getopts "al:nqs:" opt; do
case $opt in
(a) all=1
;;
(n) nonewline=-n
;;
(q) quiet=1
;;
(l) for sess in ${(s.,.)OPTARG}; do
if [[ -z ${tcp_by_name[$sess]} ]]; then
print "$0: no such session: $sess" >&2
return 1
fi
sessions+=($sess)
done
;;
(s) if [[ -z $tcp_by_name[$OPTARG] ]]; then
print "No such session: $OPTARG" >&2
return 1
fi
sessions+=($OPTARG)
;;
(*) [[ $opt != '?' ]] && print Unhandled option, complain: $opt >&2
return 1
;;
esac
done
(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
if [[ -n $all ]]; then
sessions=(${(k)tcp_by_name})
elif (( ! ${#sessions} )); then
sessions=($TCP_SESS)
fi
if (( ! $#sessions )); then
if [[ -z $quiet ]]; then
print "No current TCP session open." >&2
fi
return 1
fi
# Writing on a TCP connection closed by the remote end can cause SIGPIPE.
# The following test is reasonably robust, though in principle we can
# mistake a SIGPIPE owing to another fd. That doesn't seem like a big worry.
# `emulate -L zsh' will already have set localtraps.
local TCP_FD_CLOSED
trap 'TCP_FD_CLOSED=1' PIPE
local TCP_SESS
for TCP_SESS in $sessions; do
fd=${tcp_by_name[$TCP_SESS]}
print $nonewline -r -- $* >&$fd
if [[ $? -ne 0 || -n $TCP_FD_CLOSED ]]; then
print "Session ${TCP_SESS}: fd $fd unusable." >&2
unset TCP_FD_CLOSED
fi
if [[ -n $TCP_OUTPUT ]]; then
tcp_output -P "$TCP_OUTPUT" -S $TCP_SESS -F $fd -q "${(j. .)*}"
fi
done

39
Functions/TCP/tcp_sess Normal file
View file

@ -0,0 +1,39 @@
# try to disguise parameters from the eval'd command in case it's a function.
integer __myfd=1
if [[ -n $1 ]]; then
if [[ -z $tcp_by_name[$1] ]]; then
print no such session: $1
__myfd=2
elif [[ -n $2 ]]; then
local TCP_SESS=$1
shift
# A bit tricky: make sure the first argument gets re-evaluated,
# so as to get aliases etc. to work, but make sure the remainder
# don't, so as not to bugger up quoting. This ought to work the
# vast majority of the time, anyway.
local __cmd=$1
shift
eval $__cmd \$\*
return
else
TCP_SESS=$1
return 0;
fi
fi
# Print out the list of sessions, first the number, than the corresponding
# file descriptor. The current session, if any, is marked with an asterisk.
local cur name fd
for name in ${(ko)tcp_by_name}; do
fd=${tcp_by_name[$name]}
# mark current session with an asterisk
if [[ ${TCP_SESS} = $name ]]; then
cur=" *"
else
cur=
fi
print "sess:$name; fd:$fd$cur" >&$__myfd
done
return $(( __myfd - 1 ))

97
Functions/TCP/tcp_spam Normal file
View file

@ -0,0 +1,97 @@
# SPAM is a registered trademark of Hormel Foods Corporation.
#
# -a all connections, override $tcp_spam_list and $tcp_no_spam_list.
# If not given and tcp_spam_list is set to a list of sessions,
# only those will be spammed. If tcp_no_spam_list is set, those
# will (also) be excluded from spamming.
# -l sess1,sess2 give comma separated list of sessions to spam
# -r reverse, spam in opposite order (default is alphabetic, -r means
# omegapsiic). Note tcp_spam_list is not sorted (but may be reversed).
# -t transmit, send data to slave rather than executing command for eac
# session.
# -v verbose, list session being spammed in turn
#
# If the function tcp_on_spam is defined, it is called for each link
# with the first argument set to the session name, and the remainder the
# command line to be executed. If it sets the parameter REPLY to `done',
# the command line will not then be executed by tcp_spam, else it will.
emulate -L zsh
setopt extendedglob
local TCP_SESS cmd opt verbose reverse sesslist transmit all
local match mbegin mend REPLY
local -a sessions
while getopts "al:rtv" opt; do
case $opt in
(a) all=1
;;
(l) sessions+=(${(s.,.)OPTARG})
;;
(r) reverse=1
;;
(s) sessions+=($OPTARG)
;;
(t) transmit=-t
;;
(v) verbose=1
;;
(*) [[ $opt != '?' ]] && print "Option $opt not handled." >&2
print "Sorry, spam's off." >&2
return 1
;;
esac
done
(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
local name
if [[ -n $all ]]; then
sessions=(${(ko)tcp_by_name})
elif (( ! ${#sessions} )); then
if (( ${#tcp_spam_list} )); then
sessions=($tcp_spam_list)
else
sessions=(${(ko)tcp_by_name})
fi
if (( ${#tcp_no_spam_list} )); then
for name in ${tcp_no_spam_list}; do
sessions=(${sessions:#$name})
done
fi
fi
if [[ -n $reverse ]]; then
local tmp
integer i
for (( i = 1; i <= ${#sessions}/2; i++ )); do
tmp=${sessions[i]}
sessions[i]=${sessions[-i]}
sessions[-i]=$tmp
done
fi
if (( ! ${#sessions} )); then
print "No connections to spam." >&2
return 1
fi
if [[ -n $transmit ]]; then
cmd=tcp_send
else
cmd=$1
shift
fi
: ${TCP_PROMPT:=T[%s]:}
for TCP_SESS in $sessions; do
REPLY=
if (( ${+functions[tcp_on_spam]} )); then
tcp_on_spam $TCP_SESS $cmd $*
[[ $REPLY = done ]] && continue
fi
[[ -n $verbose ]] && zgprintf -R -%s=$TCP_SESS \
-%f=${tcp_by_name[$TCP_SESS]} -- $TCP_PROMPT
eval $cmd '$*'
done

50
Functions/TCP/tcp_talk Normal file
View file

@ -0,0 +1,50 @@
# Make line editor input go straight to the current TCP session.
# Returns when the string $TCP_TALK_ESCAPE (default :) is read on its own.
# Otherwise, $TCP_TALK_ESCAPE followed by whitespace at the start of a line
# is stripped off and the rest of the line passed to the shell.
#
# History is not currently handled, because this is difficult.
: ${TCP_TALK_ESCAPE:=:}
tcp-accept-line-or-exit() {
emulate -L zsh
setopt extendedglob
local match mbegin mend
if [[ $BUFFER = ${TCP_TALK_ESCAPE}[[:blank:]]#(#b)(*) ]]; then
if [[ -z $match[1] ]]; then
BUFFER=
zle -A .accept-line accept-line
PS1=$TCP_SAVE_PS1
unset TCP_SAVE_PS1
zle -I
print '\r[Normal keyboard input restored]' >&2
else
BUFFER=$match[1]
fi
zle .accept-line
else
# BUGS: is deleted from the command line and doesn't appear in
# the history.
# The following attempt to get the BUFFER into the history falls
# foul of the fact that we need to accept the current line first.
# But we don't actually want to accept the current line at all.
# print -s -r - $BUFFER
# This is my function to send data over a TCP connection; replace
# it with something else or nothing.
tcp_send $BUFFER
BUFFER=
fi
}
TCP_SAVE_PS1=${PS1##\[T*\]}
if [[ -o prompt_subst ]]; then
PS1="T[\$TCP_SESS]$TCP_SAVE_PS1"
else
PS1="[T]$TCP_SAVE_PS1"
fi
zle -N tcp-accept-line-or-exit
zle -A tcp-accept-line-or-exit accept-line

11
Functions/TCP/tcp_wait Normal file
View file

@ -0,0 +1,11 @@
# Wait for given number of seconds, reading any data from
# all TCP connections while doing so.
typeset -F SECONDS to end
(( to = $1, end = SECONDS + to ))
while (( SECONDS < end )); do
tcp_read -a -T $to
(( to = end - SECONDS ))
done
return

70
Functions/TCP/zgprintf Normal file
View file

@ -0,0 +1,70 @@
# Generalised printf.
# Arguments of the form -%X=... give the output to be used with
# the directive %x.
#
# -P indicates that any unhandled directives are to be
# passed to printf. With this option, any %-escapes passed to printf
# are assumed to consume exactly one argument from the command line.
# Unused command line arguments are ignored. This is only minimally
# implemented.
#
# -R indicates the value is to be put into REPLY rather than printed.
#
# -r indicates that print formatting (backslash escapes etc.) should
# not be replied to the result. When using -R, no print formatting
# is applied in any case.
emulate -L zsh
setopt extendedglob
local opt printf fmt usereply match mbegin mend raw c
typeset -A chars
chars=(% %)
while getopts "%:PrR" opt; do
case $opt in
(%) if [[ $OPTARG != ?=* ]]; then
print -r "Bad % option: should be -%${OPTARG[1]}=..." >&2
return 1
fi
chars[${OPTARG[1]}]=${OPTARG[3,-1]}
;;
(P) printf=1
;;
(r) raw=-r
;;
(R) usereply=1
;;
esac
done
(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
[[ -z $usereply ]] && local REPLY
REPLY=
if (( $# )); then
fmt=$1
shift
fi
while [[ $fmt = (#b)([^%]#)%([-0-9.*]#?)(*) ]]; do
REPLY+=$match[1]
c=$match[2]
fmt=$match[3]
if [[ -n ${chars[$c]} ]]; then
REPLY+=${chars[$c]}
elif [[ -n $P ]]; then
# hmmm, we need sprintf...
# TODO: %ld etc.
REPLY+=`printf "%$c" $1`
(( $? )) && return 1
shift
else
print -r "Format not handled: %$c" >&2
return 1
fi
done
REPLY+=$fmt
[[ -z $usereply ]] && print -n $raw - $REPLY
return 0