From 5c1f3b65a6f5abeae8459f41adb8fd2316971515 Mon Sep 17 00:00:00 2001 From: Peter Stephenson Date: Thu, 6 Feb 2003 12:21:49 +0000 Subject: 18202: New TCP function system plus small error message change in ztcp. --- ChangeLog | 8 + Doc/Makefile.in | 6 +- Doc/Zsh/manual.yo | 8 + Doc/Zsh/modules.yo | 2 +- Doc/Zsh/tcpsys.yo | 694 +++++++++++++++++++++++++++++++++++++++++++ Doc/Zsh/zftpsys.yo | 2 +- Doc/zsh.yo | 2 + Doc/zshtcpsys.yo | 3 + Functions/TCP/tcp_alias | 156 ++++++++++ Functions/TCP/tcp_close | 134 +++++++++ Functions/TCP/tcp_command | 3 + Functions/TCP/tcp_expect | 115 +++++++ Functions/TCP/tcp_fd_handler | 35 +++ Functions/TCP/tcp_log | 94 ++++++ Functions/TCP/tcp_open | 197 ++++++++++++ Functions/TCP/tcp_output | 65 ++++ Functions/TCP/tcp_proxy | 31 ++ Functions/TCP/tcp_read | 207 +++++++++++++ Functions/TCP/tcp_rename | 43 +++ Functions/TCP/tcp_send | 67 +++++ Functions/TCP/tcp_sess | 39 +++ Functions/TCP/tcp_spam | 97 ++++++ Functions/TCP/tcp_talk | 50 ++++ Functions/TCP/tcp_wait | 11 + Functions/TCP/zgprintf | 70 +++++ Src/Modules/tcp.c | 4 +- Src/Modules/tcp.mdd | 1 + 27 files changed, 2139 insertions(+), 5 deletions(-) create mode 100644 Doc/Zsh/tcpsys.yo create mode 100644 Doc/zshtcpsys.yo create mode 100644 Functions/TCP/tcp_alias create mode 100644 Functions/TCP/tcp_close create mode 100644 Functions/TCP/tcp_command create mode 100644 Functions/TCP/tcp_expect create mode 100644 Functions/TCP/tcp_fd_handler create mode 100644 Functions/TCP/tcp_log create mode 100644 Functions/TCP/tcp_open create mode 100644 Functions/TCP/tcp_output create mode 100644 Functions/TCP/tcp_proxy create mode 100644 Functions/TCP/tcp_read create mode 100644 Functions/TCP/tcp_rename create mode 100644 Functions/TCP/tcp_send create mode 100644 Functions/TCP/tcp_sess create mode 100644 Functions/TCP/tcp_spam create mode 100644 Functions/TCP/tcp_talk create mode 100644 Functions/TCP/tcp_wait create mode 100644 Functions/TCP/zgprintf diff --git a/ChangeLog b/ChangeLog index 413752da6..582faf109 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,13 @@ 2003-02-06 Peter Stephenson + * 18202: Functions/TCP/*, Doc/Makefile.in, Doc/zsh.yo, + Doc/zshtcpsys.yo, Doc/Zsh/manual.yo, Doc/Zsh/modules.yo, + Doc/zsh/tcpsys.yo, Doc/Zsh/zftpsys.yo, Src/Modules/tcp.c, + Src/Modules/tcp.mdd: New set of TCP functions tcp_* which + run on top of ztcp, documented in zshtcpsys manual. Also + sneaked in more informative error message in zsh/net/tcp + for failure to bind to a port. + * Greg Klanderman : 18191: Src/Zle/compresult.c: `compctl -y' didn't obey the listpacked and listrowsfirst options. diff --git a/Doc/Makefile.in b/Doc/Makefile.in index 7ed9d02d4..e5f691f08 100644 --- a/Doc/Makefile.in +++ b/Doc/Makefile.in @@ -47,7 +47,7 @@ TEXI2HTML = texi2html -expand info -split chapter # man pages to install MAN = zsh.1 zshbuiltins.1 zshcompctl.1 zshcompwid.1 zshcompsys.1 \ zshcontrib.1 zshexpn.1 zshmisc.1 zshmodules.1 \ -zshoptions.1 zshparam.1 zshzftpsys.1 zshzle.1 zshall.1 +zshoptions.1 zshparam.1 zshtcpsys.1 zshzftpsys.1 zshzle.1 zshall.1 # yodl documentation @@ -72,7 +72,7 @@ Zsh/filelist.yo Zsh/files.yo Zsh/func.yo Zsh/grammar.yo Zsh/manual.yo \ Zsh/index.yo Zsh/intro.yo Zsh/invoke.yo Zsh/jobs.yo Zsh/metafaq.yo \ Zsh/modules.yo Zsh/modlist.yo Zsh/modmenu.yo Zsh/manmodmenu.yo $(MODDOCSRC) \ Zsh/options.yo Zsh/params.yo Zsh/prompt.yo Zsh/redirect.yo Zsh/restricted.yo \ -Zsh/seealso.yo Zsh/zftpsys.yo Zsh/zle.yo +Zsh/seealso.yo Zsh/tcpsys.yo Zsh/zftpsys.yo Zsh/zle.yo # ========== DEPENDENCIES FOR BUILDING ========== @@ -182,6 +182,8 @@ zshoptions.1: Zsh/options.yo zshparam.1: Zsh/params.yo +zshtcpsys.1: Zsh/tcpsys.yo + zshzftpsys.1: Zsh/zftpsys.yo zshzle.1: Zsh/zle.yo diff --git a/Doc/Zsh/manual.yo b/Doc/Zsh/manual.yo index fb23e6fcf..9fb81d99a 100644 --- a/Doc/Zsh/manual.yo +++ b/Doc/Zsh/manual.yo @@ -33,6 +33,7 @@ menu(Completion Widgets) menu(Completion System) menu(Completion Using compctl) menu(Zsh Modules) +menu(TCP Function System) menu(Zftp Function System) menu(User Contributions) @@ -137,6 +138,13 @@ Zsh Modules includefile(Zsh/manmodmenu.yo) +TCP Function System + +menu(TCP Functions) +menu(TCP Parameters) +menu(TCP Examples) +menu(TCP Bugs) + Zftp Function System menu(Installation) diff --git a/Doc/Zsh/modules.yo b/Doc/Zsh/modules.yo index 828b97d37..cbec478e3 100644 --- a/Doc/Zsh/modules.yo +++ b/Doc/Zsh/modules.yo @@ -1,4 +1,4 @@ -texinode(Zsh Modules)(Zftp Function System)(Completion Using compctl)(Top) +texinode(Zsh Modules)(TCP Function System)(Completion Using compctl)(Top) chapter(Zsh Modules) cindex(modules) sect(Description) diff --git a/Doc/Zsh/tcpsys.yo b/Doc/Zsh/tcpsys.yo new file mode 100644 index 000000000..40c92c1cb --- /dev/null +++ b/Doc/Zsh/tcpsys.yo @@ -0,0 +1,694 @@ +texinode(TCP Function System)(Zftp Function System)(Zsh Modules)(Top) +chapter(TCP Function System) +cindex(TCP function system) +cindex(ztcp, function system based on) +sect(Description) + +A module tt(zsh/net/tcp) is provided to provide network I/O over +TCP/IP from within the shell; see its description in +ifzman(\ +zmanref(zshmodules) +)\ +ifnzman(\ +noderef(Zsh Modules) +). This manual page describes a function suite based on the module. The +functions will usually be installed at the same time as the module if that +is present on your system, in which case they will be available for +autoloading in the default function search path. In addition to the +tt(zsh/net/tcp) module, the tt(zsh/zselect) module is used to implement +timeouts on read operations. For troubleshooting tips, consult the +corresponding advice for the tt(zftp) functions described in +ifzman(\ +zmanref(zshftpsys) +)\ +ifnzman(\ +noderef(Zftp Function System) +). + +There are functions corresponding to the basic I/O operations open, close, +read and send, named tt(tcp_open) etc., as well as a function +tt(tcp_expect) for pattern match analysis of data read as input. The +system makes it easy to receive data from and send data to multiple named +sessions at once. In addition, it can be linked with the shell's line +editor in such a way that input data is automatically shown at the +terminal. Other facilities available including logging, filtering and +configurable output prompts. + +To use the system where it is available, it should be enough to +`tt(autoload -U tcp_open)' and run tt(tcp_open) as documented below to +start a session. The tt(tcp_open) function will autoload the remaining +functions. + +startmenu() +menu(TCP Functions) +menu(TCP Parameters) +menu(TCP Examples) +menu(TCP Bugs) +endmenu() + +texinode(TCP Functions)(TCP Parameters)()(TCP Function System) +sect(TCP User Functions) + +subsect(Basic I/O) + +startitem() +findex(tcp_open) +xitem(tt(tcp_open [-qz]) var(host port) tt([) var(sess) tt(])) +xitem(tt(tcp_open [-qz] [ -s) var(sess) tt(| -l) var(sess)tt(,... ] ... )) +item(tt(tcp_open [-qz] [-a) var(fd) tt(| -f) var(fd) tt(] [) var(sess) tt(]))( +Open a new session. In the first and simplest form, open a TCP connection +to host var(host) at port var(port); numeric and symbolic forms are +understood for both. + +If var(sess) is given, this becomes the name of the session which can be +used to refer to multiple different TCP connections. If var(sess) is +not given, the function will invent a numeric name value (note this is +em(not) the same as the file descriptor to which the session is attached). +It is recommended that session names not include `funny' characters, where +funny characters are not well-defined but certainly do not include +alphanumerics or underscores, and certainly do include whitespace. + +In the second case, one or more sessions to be opened are given by name. +A single session name is given after tt(-s) and a comma-separated list +after tt(-l); both options may be repeated as many times as necessary. +The host and port are read from the file tt(.ztcp_sessions) in the same +directory as the user's zsh initialisation files, i.e. usually the home +directory, but tt($ZDOTDIR) if that is set. The file consists of lines +each giving a session name and the corresponding host and port, in that +order (note the session name comes first, not last), separated by +whitespace. + +The third form allows passive and fake TCP connections. If the option +tt(-a) is used, its argument is a file descriptor open for listening for +connections. No function front-end is provided to open such a file +descriptor, but a call to `tt(ztcp -l) var(port)' will create one with the +file descriptor stored in the parameter tt($REPLY). The listening port can +be closed with `tt(ztcp -c) var(fd)'. A call to `tt(tcp_open -a) var(fd)' +will block until a remote TCP connection is made to var(port) on the local +machine. At this point, a session is created in the usual way and is +largely indistinguishable from an active connection created with one of the +first two forms. + +If the option tt(-f) is used, its argument is a file descriptor which is +used directly as if it were a TCP session. How well the remainder of the +TCP function system copes with this depends on what actually underlies this +file descriptor. A regular file is likely to be unusable; a FIFO (pipe) of +some sort will work better, but note that it is not a good idea for two +different sessions to attempt to read from the same FIFO at once. + +If the option tt(-q) is given with any of the three forms, tt(tcp_open) +will not print informational messages, although it will in any case exit +with an appropriate status. + +If the line editor (zle) is in use, which it usually is if and only if the +shell is interactive, tt(tcp_open) installs a handler inside tt(zle) which +will check for new data at the same time as it checks for keyboard input. +This is convenient as the shell consumes no CPU time while waiting; the +test is performed by the operating systems. However, if incoming data +is only to be read explicitly, the option tt(-z) to any of the forms of +tt(tcp_open) prevents the handler from being installed. Note this is not +necessary for executing complete sets of send and read commands from a +function, as zle is not active at this point. Generally speaking, the +handler is only active when the shell is waiting for input at a command +prompt or in the tt(vared) builtin. The option has no effect if zle is not +active; `tt([[ -o zle]])' will test for this. + +The first session to be opened becomes the current session; subsequent +calls to tt(tcp_open) will not change this. The current session is stored +in the parameter tt($TCP_SESS); see below for more detail about the +parameters used by the system. +) +findex(tcp_close) +item(tt(tcp_close [-qn] [ -a | -l) var(sess)tt(,... |) var(sess) tt(... ]))( +Close the named sessions, or the current session if none is given, +or all open sessions if tt(-a) is given. The options tt(-l) and tt(-s) are +both handled for consistency with tt(tcp_open), although the latter is +redundant. + +If the session being closed is the current one, tt($TCP_SESS) is unset, +leaving no current session, even if there are other sessions still open. + +If the session was opened with tt(tcp_open -f), the file descriptor is +closed so long as it is in the range 0 to 9 accessible directly from the +command line. If the option tt(-n) is given, no attempt will be made to +close file descriptors in this case. The tt(-n) option is not used for +genuine tt(ztcp) session; the file descriptors are always closed with the +session. + +If the option tt(-q) is given, no informational messages will be printed. +) +findex(tcp_read) +xitem(tt(tcp_read [-bdq] [ -t) var(TO) tt(] [ -T) var(TO) tt(])) +item( tt([ -a | -u) var(fd) tt(... | -l) var(sess)tt(,... | -s) var(sess) tt(...]))( +Perform a read operation on the current session, or on a list of sessions +if any are given (the first form), or all open sessions (the second form). +Any of the tt(-u), tt(-l) or tt(-s) options may be repeated or mixed +together. The tt(-u) option specifies a file descriptor directly (only +those managed by this system are useful), the other two specify sessions as +described for tt(tcp_open) above. If tt(-a) is given, all sessions ares +examined for new data. + +The function checks for new data available on all the sessions listed. +Unless the tt(-b) option is given, it will not block waiting for new data. +Any one line of data from any of the available sessions will be read, +stored in the parameter tt($TCP_LINE), and displayed to standard output +unless tt($TCP_SILENT) contains a non-empty string. When printed to +standard output the string tt($TCP_PROMPT) will be shown at the start of +the line; the default form for this includes the name of the session being +read. See below for more information on these parameters. In this mode, +tt(tcp_read) can be called repeatedly until it returns status 2 which +indicates all pending input from all specified sessions has been handled. + +With the option tt(-b), equivalent to an infinite timeout, the function +will block until a line is available to read from one of the specified +sessions. However, only a single line is returned. + +The option tt(-d) indicates that all pending input should be drained. In +this case tt(tcp_read) may process multiple lines in the manner given +above; only the last is stored in tt($TCP_LINE), but the complete set is +stored in the array tt($tcp_lines). This is cleared at the start of each +call to tt(tcp_read). + +The options tt(-t) and tt(-T) specify a timeout in seconds, which may be a +floating point number for increased accuracy. With tt(-t) the timeout is +applied before each line read. With tt(-T), the timeout applies to the +overall operation, possibly including multiple read operations if the +option tt(-d) is present; without this option, there is no distinction +between tt(-t) and tt(-T). + +The function does not print informational messages, but if the option +tt(-q) is given, no error message is printed for a non-existent session. + +A return value of 2 indicates a timeout or no data to read. Any other +non-zero return value indicates some error condition. + +See tt(tcp_log) for how to control where data is sent by tt(tcp_read). +) +findex(tcp_send) +xitem(tt(tcp_send [-nq] [ -s) var(sess) tt(| -l) var(sess)tt(,... ]) var(data) tt(...)) +item(tt(tcp_send [-nq] -a) var(data) tt(...))( +Send the supplied data strings to all the specified sessions in turn. The +underlying operation differs little from a `tt(print -r)' to the session's +file descriptor, although it attempts to prevent the shell from dying owing +to a tt(SIGPIPE) caused by an attempt to write to a defunct session. + +The option tt(-n) prevents tt(tcp_send) from putting a newline at the end +of the data strings. + +The remaining options all behave as for tt(tcp_read). + +The data arguments are not further processed once they have been passed to +tt(tcp_send); they are simply passed down to tt(print -r). + +If the parameter tt($TCP_OUTPUT) is a non-empty string and logging is +enabled then the data sent to each session will be echoed to the log +file(s) with tt($TCP_OUTPUT) in front where appropriate, much in the manner +of tt($TCP_PROMPT). +) +enditem() + +subsect(Session Management) + +startitem() +findex(tcp_alias) +xitem(tt(tcp_alias [-q]) var(alias)tt(=)var(sess) tt(...)) +xitem(tt(tcp_alias [-q] [) var(alias) tt(] ...)) +item(tt(tcp_alias -d [-q]) var(alias) tt(...))( +This function is not particularly well tested. + +The first form creates an alias for a session name; var(alias) can then be +used to refer to the existing session var(sess). As many aliases may be +listed as required. + +The second form lists any aliases specified, or all aliases if none. + +The third form deletes all the aliases listed. The underlying sessions are +not affected. + +The option tt(-q) suppresses an inconsistently chosen subset of error +messages. +) +findex(tcp_log) +item(tt(tcp_log [-asc] [ -n | -N ] [) var(logfile) tt(]))( +With an argument var(logfile), all future input from tt(tcp_read) will be +logged to the named file. Unless tt(-a) (append) is given, this file will +first be truncated or created empty. With no arguments, show the current +status of logging. + +With the option tt(-s), per-session logging is enabled. Input from +tt(tcp_read) is output to the file var(logfile).var(sess). As the +session is automatically discriminated by the filename, the contents are +raw (no tt($TCP_PROMPT)). The option tt(-a) applies as above. +Per-session logging and logging of all data in one file are not mutually +exclusive. + +The option tt(-c) closes all logging, both complete and per-session logs. + +The options tt(-n) and tt(-N) respectively turn off or restore output of +data read by tt(tcp_read) to standard output; hence `tt(tcp_log -cn)' turns +off all output by tt(tcp_read). + +The function is purely a convenient front end to setting the parameters +tt($TCP_LOG), tt($TCP_LOG_SESS), tt($TCP_SILENT), which are described below. +) +findex(tcp_rename) +item(tt(tcp_rename) var(old) var(new))( +Rename session var(old) to session var(new). The old name becomes invalid. +) +findex(tcp_sess) +item(tt(tcp_sess [) var(sess) tt([) var(command) tt(... ] ]))( +With no arguments, list all the open sessions and associated file +descriptors. The current session is marked with a star. For use in +functions, direct access to the parameters tt($tcp_by_name), tt($tcp_by_fd) +and tt($TCP_SESS) is probably more convenient; see below. + +With a var(sess) argument, set the current session to var(sess). +This is equivalent to changing tt($TCP_SESS) directly. + +With additional arguments, temporarily set the current session while +executing the string tt(command ...). The first argument is re-evaluated +so as to expand aliases etc., but the remaining arguments are passed +through as the appear to tt(tcp_sess). The original session is restored +when tt(tcp_sess) exits. +) +enditem() + +subsect(Advanced I/O) + +startitem() +findex(tcp_command) +item(tt(tcp_command) var(send-options) tt(...) var(send-arguments) tt(...))( +This is a convenient front-end to tt(tcp_send). All arguments are passed +to tt(tcp_send), then the function pauses waiting for data. While data is +arriving at least every tt($TCP_TIMEOUT) (default 0.3) seconds, data is +handled and printed out according to the current settings. Status 0 is +always returned. + +This is generally only useful for interactive use, to prevent the display +becomming fragmented by output returned from the connection. Within a +programe or function it is generally better to handle reading data by a +more explicit method. +) +findex(tcp_expect) +xitem(tt(tcp_expect [ -q ] [ -p) var(var) tt(] [ -t ) var(to) tt(| -T) var(TO)tt(])) +item(tt( [ -a | -s) var(sess) tt(... | -l) var(sess)tt(,... ]) var(pattern) ...)( +Wait for input matching any of the given var(pattern)s from any of the +specified sessions. Input is ignored until an input line matches one of +the given patterns; at this point status zero is returned, the matching +line is stored in tt($TCP_LINE), and the full set of lines read during the +call to tt(tcp_expect) is stored in the array tt($tcp_expect_lines). + +Sessions are specified in the same way as tt(tcp_read): the default is to +use the current session, otherwise the sessions specified by tt(-a), +tt(-s), or tt(-l) are used. + +Each var(pattern) is a standard zsh extended-globbing pattern; note that it +needs to be quoted to avoid it being expanded immediately by filename +generation. It must match the full line, so to match a substring there +must be a `tt(*)' at the start and end. The line matched against includes +the tt($TCP_PROMPT) added by tt(tcp_read). It is possible to include the +globbing flags `tt(#b)' or `tt(#m)' in the patterns to make backreferences +available in the parameters tt($MATCH), tt($match), etc., as described in +the base zsh documentation on pattern matching. + +Unlike tt(tcp_read), the default behaviour of tt(tcp_expect) is to block +indefinitely until the required input is found. This can be modified by +specifying a timeout with tt(-t) or tt(-T); these function as in +tt(tcp_read), specifying a per-read or overall timeout, respectively, in +seconds, as an integer or floating-point number. As tt(tcp_read), the +function returns status 2 if a timeout occurs. + +The function returns as soon as any one of the patterns given match. If +the caller needs to know which of the patterns matched, the option tt(-p) +var(var) can be used; on return, tt($var) is set to the number of the +pattern using ordinary zsh indexing, i.e. the first is 1, and so on. Note +tha absence of a `tt($)' in front of var(var). To avoid clashes, the +parameter cannot begin with `tt(_expect)'. + +The option tt(-q) is passed directly down to tt(tcp_read). + +As all input is done via tt(tcp_read), all the usual rules about output of +lines read apply. One exception is that the parameter tt($tcp_lines) will +only reflect the line actually matched by tt(tcp_expect); use +tt($tcp_expect_lines) for the full set of lines read during the function +call. +) +findex(tcp_proxy) +item(tt(tcp_proxy))( +This is a simple-minded function to accept a TCP connection and execute a +command with I/O redirected to the connection. Extreme caution should be +taken as there is no security whatsoever and this can leave your computer +open to the world. Ideally, it should only be used behind a firewall. + +The first argument is a TCP port on which the function will listen. + +The remaining arguments give a command and its arguments to execute with +standard input, standard output and standard error redirected to the +file descriptor on which the TCP session has been accepted. +If no command is given, a new zsh is started. This gives everyone on +your network direct access to your account, which in many cases will be a +bad thing. + +The command is run in the background, so tt(tcp_proxy) can then accept new +connections. It continues to accept new connections until interrupted. +) +findex(tcp_spam) +item(tt(tcp_spam [-rtv] [ -a | -s ) var(sess) tt(| -l) var(sess)tt(,... ]) var(cmd) tt(...))( +Execute `var(cmd) tt(...)' for each session in turn. Note this executes +the command and arguments; it does not send the command line as data +unless the tt(-t) (transmit) option is given. + +The sessions may be selected explicitly with the standard tt(-a), tt(-s) or +tt(-l) options, or may be chosen implicitly. If none of the three options +is given the rules are: first, if the array tt($tcp_spam_list) is set, this +is taken as the list of sessions, otherwise all sessions are taken. +Second, any sessions given in the array tt($tcp_no_spam_list) are removed +from the list of sessions. + +Normally, any sessions added by the `tt(-a)' flag or when all sessions are +chosen implicitly are spammed in alphabetic order; sessions given by the +tt($tcp_spam_list) array or on the command line are spammed in the order +given. The tt(-r) flag reverses the order however it was arrived it. + +The tt(-v) flag specifies that a tt($TCP_PROMPT) will be output before each +session. This is output after any modfication to TCP_SESS by the +user-defined tt(tcp_on_spam) function described below. (Obviously that +function is able to generate its own output.) +) +findex(tcp_talk) +item(tt(tcp_talk))( +This is a fairly simple-minded attempt to force input to the line editor to +go straight to the default TCP_SESSION. + +An escape string, tt($TCP_TALK_ESCAPE), default `:', is used to allow +access to normal shell operation. If it is on its own at the start of the +line, or followed only by whitespace, the line editor returns to normal +operation. Otherwise, the string and any following whitespace are skipped +and the remainder of the line executed as shell input without any change of +the line editor's operating mode. + +The current implementation is somewhat deficient in terms of use of the +command history. For this reason, many users will prefer to use some form +of alternative approach for sending data easily to the current session. +One simple approach is to alias some special character (such as `tt(%)') to +`tt(tcp_command --)'. +) +findex(tcp_wait) +item(tt(tcp_wait))( +The sole argument is an integer or floating point number which gives the +seconds to delay. The shell will do nothing for that period except wait +for input on all TCP sessions by calling tt(tcp_read -a). This is similar +to the interactive behaviour at the command prompt when zle handlers are +installed. +) +enditem() + +sect(TCP User-defined Function) + +Certain functions, if defined by the user, will be called by the function +system in certain contexts. This facility depends on the module +tt(zsh/parameter), which is usually available in interactive shells as the +completion system depends on it. None of the functions need by defined; +they simply provide convenient hooks when necessary. + +Typically, these are called after the requested action has been taken, so +that the various parameters will reflect the new state. + +startitem() +findex(tcp_on_alias) +item(tt(tcp_on_alias) var(alias) var(fd))( +When an alias is defined, this function will be called with two arguments: +the name of the alias, and the file descriptor of the corresponding session. +) +findex(tcp_on_close) +item(tt(tcp_on_close) var(sess) var(fd))( +This is called with the name of a session being closed and the file +descriptor which corresponded to that session. Both will be invalid by +the time the function is called. +) +findex(tcp_on_open) +item(tt(tcp_on_open) var(sess) var(fd))( +This is called after a new session has been defined with the session name +and file descriptor as arguments. +) +findex(tcp_on_rename) +item(tt(tcp_on_rename) var(oldsess) var(fd) var(newsess))( +This is called after a session has been renamed with the three arguments +old session name, file descriptor, new session name. +) +findex(tcp_on_spam) +item(tt(tcp_on_spam) var(sess) var(command) tt(...))( +This is called once for each session spammed, just em(before) a command is +executed for a session by tt(tcp_spam). The arguments are the session name +followed by the command list to be executed. If tt(tcp_spam) was called +with the option tt(-t), the first command will be tt(tcp_send). + +This function is called after tt($TCP_SESS) is set to reflect the session +to be spammed, but before any use of it is made. Hence it is possible to +alter the value of tt($TCP_SESS) within this function. For example, the +session arguments to tt(tcp_spam) could include extra information to be +stripped off and processed in tt(tcp_on_spam). + +If the function sets the parameter tt($REPLY) to `tt(done)', the command +line is not executed; in addition, no prompt is printed for the tt(-v) +option to tt(tcp_spam). +) +findex(tcp_on_unalias) +item(tt(tcp_on_unalias) var(alias) var(fd))( +This is called with the name of an alias and the corresponding session's +file descriptor after an alias has been deleted. +) +enditem() + +sect(TCP Utility Functions) + +The following functions are used by the TCP function system but will rarely +if ever need to be called directly. + +startitem() +findex(tcp_fd_handler) +item(tt(tcp_fd_handler))( +This is the function installed by tt(tcp_open) for handling input from +within the line editor, if that is required. It is in the format +documented for the builtin `tt(zle -F)' in +ifzman(\ +zmanref(zshzle) +)\ +ifnzman(\ +noderef(Zle Builtins) +). +) +findex(tcp_output) +item(tt(tcp_output [ -q ] -P) var(prompt) tt(-F) var(fd) tt(-S) var(sess))( +This function is used for both logging and handling output to standard +output, from within tt(tcp_read) and (if tt($TCP_OUTPUT) is set) +tt(tcp_send). + +The var(prompt) to use is specified by tt(-P); the default is the empty +string. It can contain `tt(%s)' which is replaced by the session name, or +`tt(%f)' which is replaced by the session's file descriptor; `tt(%%)' is +replaced by a single `tt(%)'. + +The option tt(-q) suppresses output to standard output, but not to any log +files which are configured. + +The tt(-S) and tt(-F) options are used to pass in the session name and file +descriptor for possible replacement in the prompt. +) +findex(zgprintf) +item(tt(zgprintf) tt(-rPR -%)var(X)tt(=)var(subst) var(fmt) tt([) var(val) tt(... ]))( +This function is used for performing tt(%)-replacement in prompts supplied +to tt(tcp_output). The var(fmt) string is printed to standard output. +The option tt(-%)var(X)tt(=)var(subst) specifies that any occurrence +of tt(%)var(X) in the var(fmt) string should be replaced by var(subst). +These arguments may be repeated for arbitrary var(X). + +The option tt(-r) specifies that the normal tt(print) conventions are not +to be used, as with the corresponding argument to the tt(print) builtin. + +The option tt(-R) specifies that the output is to be left in the parameter +tt($REPLY) instead of being printed. + +The option tt(-P) specifies that unhandled tt(%)-escapes should be +formatted by a call to tt(printf). Each is assumed to consume exactly one +additional var(val) argument. This option is only minimally implemented. +) +enditem() + +texinode(TCP Parameters)(TCP Examples)(TCP Functions)(TCP Function System) +sect(TCP User Parameters) + +Parameters follow the usual convention that uppercase is used for scalars +and integers, while lowercase is used for normal and associative array. +It is always safe for user code to read these parameters; some parameters +may also be set, which are noted explicitly. Other are included in this +group as they are set by the function system for the user's benefit, +i.e. setting them is typically not useful but is benign. + +It is often also useful to make settable parameters local to a function. +For example, `tt(local TCP_SILENT=1)' specifies that data read during the +function call will not be printed to standard output, regardless of the +setting outside the function. Likewise, `tt(local TCP_SESS=)var(sess)' +sets a session for the duration of a function. + +startitem() +findex(tcp_expect_lines) +item(tt(tcp_expect_lines))( +Array. The set of lines read during the last call to tt(tcp_expect), +including the last (tt($TCP_LINE)). +) +findex(tcp_filter) +item(tt(tcp_filter))( +Array. May be set directly. A set of extended globbing patterns which, +if matched in tt(tcp_output), will cause the line not to be printed to +standard output. The patterns should be defined as described for the +arguments to tt(tcp_expect). Output of line to log files is not affected. +) +findex(TCP_LINE) +item(tt(TCP_LINE))( +The last line read by tt(tcp_read), and hence also tt(tcp_expect). +) +findex(TCP_LINE_FD) +item(tt(TCP_LINE_FD))( +The file descriptor from which tt($TCP_LINE) was read. +tt(${tcp_by_fd[$TCP_LINE_FD]}) will give the corresponding session name. +) +findex(tcp_lines) +item(tt(tcp_lines))( +Array. The set of lines read during the last call to tt(tcp_read), +including the last (tt($TCP_LINE)). +) +findex(TCP_LOG) +item(tt(TCP_LOG))( +May be set directly, although it is also controlled by tt(tcp_log). +The name of a file to which output from all sessions will be sent. +The output is proceeded by the usual tt($TCP_PROMPT). If it is not an +absolute path name, it will follow the user's current directory. +) +findex(TCP_LOG_SESS) +item(tt(TCP_LOG_SESS))( +May be set directly, although it is also controlled by tt(tcp_log). +The prefix for a set of files to which output from each session separately +will be sent; the full filename is tt(${TCP_LOG_SESS}.)var(sess). +Output to each file is raw; no prompt is added. If it is not an absolute +path name, it will follow the user's current directory. +) +findex(tcp_nospam_list) +item(tt(tcp_nospam_list))( +Array. May be set directly. See tt(tcp_spam) for how this is used. +) +findex(TCP_OUTPUT) +item(tt(TCP_OUTPUT))( +May be set directly. If a non-empty string, any data sent to a session by +tt(tcp_send) will be logged. The prompt has the same format as +tt(TCP_PROMPT) and the same rules for its use apply: it is used in a file +specified by tt($TCP_LOG), but not in a file generated from +tt($TCP_LOG_SESS). +) +findex(TCP_PROMPT) +item(tt(TCP_PROMPT))( +May be set directly. Used as the prefix for data read by tt(tcp_read) +which is printed to standard output or to the log file given by +tt($TCP_LOG), if any. Any `tt(%s)', `tt(%f)' or `tt(%%)' occurring in the +string will be replaced by the name of the session, the session's +underlying file descriptor, or a single `tt(%)', respectively. +) +findex(TCP_READ_DEBUG) +item(tt(TCP_READ_DEBUG))( +May be set directly. If this has non-zero length, tt(tcp_read) will give +some limited diagnostics about data being read. +) +findex(TCP_SESS) +item(tt(TCP_SESS))( +May be set directly. The current session; must refer to one of the +sessions established by tt(tcp_open). +) +findex(TCP_SILENT) +item(tt(TCP_SILENT))( +May be set directly, although it is also controlled by tt(tcp_log). +If of non-zero length, data read by tt(tcp_read) will not be written to +standard output, though may still be written to a log file. +) +findex(tcp_spam_list) +item(tt(tcp_spam_list))( +Array. May be set directly. See the description of the function +tt(tcp_spam) for how this is used. +) +findex(TCP_TALK_ESCAPE) +item(tt(TCP_TALK_ESCAPE))( +May be set directly. See the description of the function tt(tcp_talk) for +how this is used. +) +findex(TCP_TIMEOUT) +item(tt(TCP_TIMEOUT))( +May be set directly. Currently this is only used by the function +tt(tcp_command), see above. +) +enditem() + +sect(TCP Utility Parameters) + +These parameters are controlled by the function system; they may be read +directly, but should not usually be set by user code. + +startitem() +findex(tcp_aliases) +item(tt(tcp_aliases))( +Associative array. The keys are the names of sessions established with +tt(tcp_open); each value is a space-separated list of aliases which refer +to that session. +) +findex(tcp_by_fd) +item(tt(tcp_by_fd))( +Associative array. The keys are session file descriptors; each +value is the name of that session. +) +findex(tcp_by_name) +item(tt(tcp_by_name))( +Associative array. The keys are the names of sessions; each value is the +file descriptor associated with that session. +) +enditem() + +texinode(TCP Examples)(TCP Bugs)(TCP Parameters)(TCP Function System) +sect(TCP Examples) + +Here is a trivial example using a remote calculator. + +TO create a calculator server on port 7337 (see the tt(dc) manual page for +quite how infuriating the underlying command is): + +example(tcp_proxy 7337 dc) + +To connect to this from the same host with a session also named `tt(dc)': + +example(tcp_open localhost 7337 dc) + +To send a command to the remote session and wait a short while for output +(assuming tt(dc) is the current session): + +example(tcp_command 2 4 + p) + +To close the session: + +example(tcp_close) + +The tt(tcp_proxy) needs to be killed to be stopped. Note this will not +usually kill any connections which have already been accepted, and also +that the port is not immediately available for reuse. + +The following chunk of code puts a list of sessions into an xterm header, +with the current session followed by a star. + +example(print -n "\033]2;TCP:" ${(k)tcp_by_name:/$TCP_SESS/$TCP_SESS\*} "\a") + +texinode(TCP Bugs)()(TCP Examples)(TCP Function System) +sect(TCP Bugs) + +The function tt(tcp_read) uses the shell's normal tt(read) builtin. As +this reads a complete line at once, data arriving without a terminating +newline can cause the function to block indefinitely. + +Though the function suite works well for interactive use and for data +arriving in small amounts, the performance when large amounts of data are +being exchanged is likely to be extremely poor. diff --git a/Doc/Zsh/zftpsys.yo b/Doc/Zsh/zftpsys.yo index b8ffa562b..95cb6f5d2 100644 --- a/Doc/Zsh/zftpsys.yo +++ b/Doc/Zsh/zftpsys.yo @@ -1,4 +1,4 @@ -texinode(Zftp Function System)(User Contributions)(Zsh Modules)(Top) +texinode(Zftp Function System)(User Contributions)(TCP Function System)(Top) chapter(Zftp Function System) cindex(zftp function system) cindex(FTP, functions for using shell as client) diff --git a/Doc/zsh.yo b/Doc/zsh.yo index e6bf38310..5258555a8 100644 --- a/Doc/zsh.yo +++ b/Doc/zsh.yo @@ -63,6 +63,7 @@ ifnzman(includefile(Zsh/compwid.yo)) ifnzman(includefile(Zsh/compsys.yo)) ifnzman(includefile(Zsh/compctl.yo)) ifnzman(includefile(Zsh/modules.yo)) +ifnzman(includefile(Zsh/tcpsys.yo)) ifnzman(includefile(Zsh/zftpsys.yo)) ifnzman(includefile(Zsh/contrib.yo)) ifzshall(\ @@ -78,6 +79,7 @@ source(zshcompwid) source(zshcompsys) source(zshcompctl) source(zshmodules) +source(zshtcpsys) source(zshzftpsys) source(zshcontrib) manpage(ZSHALL)(1)(date())(zsh version()) diff --git a/Doc/zshtcpsys.yo b/Doc/zshtcpsys.yo new file mode 100644 index 000000000..52cd86153 --- /dev/null +++ b/Doc/zshtcpsys.yo @@ -0,0 +1,3 @@ +manpage(ZSHTCPSYS)(1)(date())(zsh version()) +manpagename(zshtcpsys)(zsh tcpletion system) +includefile(Zsh/tcpsys.yo) diff --git a/Functions/TCP/tcp_alias b/Functions/TCP/tcp_alias new file mode 100644 index 000000000..9d6a28da0 --- /dev/null +++ b/Functions/TCP/tcp_alias @@ -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 diff --git a/Functions/TCP/tcp_close b/Functions/TCP/tcp_close new file mode 100644 index 000000000..61508f4c6 --- /dev/null +++ b/Functions/TCP/tcp_close @@ -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 diff --git a/Functions/TCP/tcp_command b/Functions/TCP/tcp_command new file mode 100644 index 000000000..8a4f02504 --- /dev/null +++ b/Functions/TCP/tcp_command @@ -0,0 +1,3 @@ +tcp_send $* || return 1 +tcp_read -d -t ${TCP_TIMEOUT:=0.3} +return 0 diff --git a/Functions/TCP/tcp_expect b/Functions/TCP/tcp_expect new file mode 100644 index 000000000..14963a3e6 --- /dev/null +++ b/Functions/TCP/tcp_expect @@ -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 diff --git a/Functions/TCP/tcp_fd_handler b/Functions/TCP/tcp_fd_handler new file mode 100644 index 000000000..012fd4d87 --- /dev/null +++ b/Functions/TCP/tcp_fd_handler @@ -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 diff --git a/Functions/TCP/tcp_log b/Functions/TCP/tcp_log new file mode 100644 index 000000000..e8ebaca23 --- /dev/null +++ b/Functions/TCP/tcp_log @@ -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 1, 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:-} +Overall log: ${TCP_LOG:-} +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 diff --git a/Functions/TCP/tcp_open b/Functions/TCP/tcp_open new file mode 100644 index 000000000..d9d5a96da --- /dev/null +++ b/Functions/TCP/tcp_open @@ -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 diff --git a/Functions/TCP/tcp_output b/Functions/TCP/tcp_output new file mode 100644 index 000000000..b22b79412 --- /dev/null +++ b/Functions/TCP/tcp_output @@ -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 diff --git a/Functions/TCP/tcp_proxy b/Functions/TCP/tcp_proxy new file mode 100644 index 000000000..3f19bd3de --- /dev/null +++ b/Functions/TCP/tcp_proxy @@ -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 diff --git a/Functions/TCP/tcp_read b/Functions/TCP/tcp_read new file mode 100644 index 000000000..97da8bf21 --- /dev/null +++ b/Functions/TCP/tcp_read @@ -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 diff --git a/Functions/TCP/tcp_rename b/Functions/TCP/tcp_rename new file mode 100644 index 000000000..8d926ca0d --- /dev/null +++ b/Functions/TCP/tcp_rename @@ -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 diff --git a/Functions/TCP/tcp_send b/Functions/TCP/tcp_send new file mode 100644 index 000000000..e7dfca771 --- /dev/null +++ b/Functions/TCP/tcp_send @@ -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 diff --git a/Functions/TCP/tcp_sess b/Functions/TCP/tcp_sess new file mode 100644 index 000000000..ee3d268b3 --- /dev/null +++ b/Functions/TCP/tcp_sess @@ -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 )) diff --git a/Functions/TCP/tcp_spam b/Functions/TCP/tcp_spam new file mode 100644 index 000000000..f5c612bee --- /dev/null +++ b/Functions/TCP/tcp_spam @@ -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 diff --git a/Functions/TCP/tcp_talk b/Functions/TCP/tcp_talk new file mode 100644 index 000000000..9376b9436 --- /dev/null +++ b/Functions/TCP/tcp_talk @@ -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 diff --git a/Functions/TCP/tcp_wait b/Functions/TCP/tcp_wait new file mode 100644 index 000000000..d18068a66 --- /dev/null +++ b/Functions/TCP/tcp_wait @@ -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 diff --git a/Functions/TCP/zgprintf b/Functions/TCP/zgprintf new file mode 100644 index 000000000..c448b35a2 --- /dev/null +++ b/Functions/TCP/zgprintf @@ -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 diff --git a/Src/Modules/tcp.c b/Src/Modules/tcp.c index 96dde66e3..58ab8c090 100644 --- a/Src/Modules/tcp.c +++ b/Src/Modules/tcp.c @@ -433,7 +433,9 @@ bin_ztcp(char *nam, char **args, Options ops, int func) if (bind(sess->fd, (struct sockaddr *)&sess->sock.in, sizeof(struct sockaddr_in))) { - zwarnnam(nam, "could not bind to %s: %e", "0.0.0.0", errno); + char buf[DIGBUFSIZE]; + convbase(buf, (zlong)lport, 10); + zwarnnam(nam, "could not bind to port %s: %e", buf, errno); tcp_close(sess); return 1; } diff --git a/Src/Modules/tcp.mdd b/Src/Modules/tcp.mdd index 041d9138a..88874cd7d 100644 --- a/Src/Modules/tcp.mdd +++ b/Src/Modules/tcp.mdd @@ -1,6 +1,7 @@ name=zsh/net/tcp link=dynamic load=no +functions='Functions/TCP/*' objects="tcp.o" autobins="ztcp" -- cgit 1.4.1