diff options
author | William Joye <wjoye@cfa.harvard.edu> | 2016-10-27 19:39:39 (GMT) |
---|---|---|
committer | William Joye <wjoye@cfa.harvard.edu> | 2016-10-27 19:39:39 (GMT) |
commit | ea28451286d3ea4a772fa174483f9a7a66bb1ab3 (patch) | |
tree | 6ee9d8a7848333a7ceeee3b13d492e40225f8b86 /tcllib/modules/comm | |
parent | b5ca09bae0d6a1edce939eea03594dd56383f2c8 (diff) | |
parent | 7c621da28f07e449ad90c387344f07a453927569 (diff) | |
download | blt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.zip blt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.tar.gz blt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.tar.bz2 |
Merge commit '7c621da28f07e449ad90c387344f07a453927569' as 'tcllib'
Diffstat (limited to 'tcllib/modules/comm')
-rw-r--r-- | tcllib/modules/comm/ChangeLog | 368 | ||||
-rw-r--r-- | tcllib/modules/comm/comm.LICENSE | 48 | ||||
-rw-r--r-- | tcllib/modules/comm/comm.man | 1230 | ||||
-rw-r--r-- | tcllib/modules/comm/comm.n.html | 1067 | ||||
-rw-r--r-- | tcllib/modules/comm/comm.pcx | 99 | ||||
-rw-r--r-- | tcllib/modules/comm/comm.slaveboot | 42 | ||||
-rw-r--r-- | tcllib/modules/comm/comm.tcl | 1818 | ||||
-rw-r--r-- | tcllib/modules/comm/comm.test | 318 | ||||
-rw-r--r-- | tcllib/modules/comm/comm_wire.man | 284 | ||||
-rw-r--r-- | tcllib/modules/comm/pkgIndex.tcl | 2 |
10 files changed, 5276 insertions, 0 deletions
diff --git a/tcllib/modules/comm/ChangeLog b/tcllib/modules/comm/ChangeLog new file mode 100644 index 0000000..afc2cff --- /dev/null +++ b/tcllib/modules/comm/ChangeLog @@ -0,0 +1,368 @@ +2013-02-01 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.15 ======================== + * + +2011-12-13 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.14 ======================== + * + +2011-01-24 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.13 ======================== + * + +2010-09-15 Andreas Kupries <andreask@activestate.com> + + * comm.tcl (::comm::commIncoming): [Bug 3066872]: Replaced + blocking gets call to read line of offered protocols by + non-blocking gets and additional event handling. The procedure + "commIncoming" is split into two. + + * comm.tcl (::comm::Word0): [Bug 2972571]: Fixed misdetection + * comm.man: of quoted brace due to not handling \\ on its + * comm.test: own. Extended testsuite. Updated docs. + + * pkgIndex.tcl: Bumped to version 4.6.2. + +2009-12-07 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.12 ======================== + * + +2009-11-04 Andreas Kupries <andreask@activestate.com> + + * comm.tcl (::comm::commCollect): [Bug 2890743]. Replaced + * comm.man: lindex/lreplace with a procedure emulating lindex's + * pkgIndex.tcl: behaviour pre Tcl 8, i.e. it needs only the first + word to be a proper list element to parse it ouf the + buffer. Bumped package version to 4.6.1. + +2009-04-10 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.tcl: Added option -socketcmd enabling users to override how + * comm.man: a socket is opened. The envisioned main use is the + * pkgIndex.tcl: specification of tls::socket to secure the + * comm.pcx: communications. Version bumped to 4.6. Extended syntax + * comm.test: definitions for tclchecker, and extended testsuite. + +2008-12-12 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.11.1 ======================== + * + +2008-10-16 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.11 ======================== + * + +2008-06-14 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.pcx: New file. Syntax definitions for the public commands + of the comm package. + +2008-05-16 Andreas Kupries <andreask@activestate.com> + + * comm_wire.man: Fixed the sectref argument order issues. + +2008-05-15 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm_wire.man: Updated to changes in doctools (sub)section + reference handling. + +2008-03-15 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm_wire.man: Changed 'require' information to show that this + manpage belongs to the documentation for the package 'comm'. + +2008-02-29 Andreas Kupries <andreask@activestate.com> + + * comm.tcl: Accepted Hemang's <hemanglavana@users.sourceforge.net> + * comm.man: patch for the [SF Tcllib Bug 1861565] he + * comm.test: reported. This changes the handling of 'port already + * pkgIndex.tcl: in use' errors to provide a clear + message. Testsuite was updated. Version bumped to 4.5.7. + +2007-09-12 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.10 ======================== + * + +2007-08-21 Andreas Kupries <andreask@activestate.com> + + * comm_wire.man: Fixed description of messages in the basic + message layer, and of EOL, per [SF Tcllib Bug 1739372] (by Lars + Hellstroem). General cleanup (spell checking). + +2007-08-20 Andreas Kupries <andreask@activestate.com> + + * comm.test: Updated to require Tcl 8.3 (for snit). + +2007-08-15 Andreas Kupries <andreask@activestate.com> + + * comm.tcl (::comm::Vwait): Fixed uplevel which failed when used + * comm.man: with variable names containing spaces. Bumped the + * pkgIndex.tcl: package version to 4.5.6. Thanks to Bryan Oakley. + +2007-08-14 Andreas Kupries <andreask@activestate.com> + + * comm.tcl (::comm::CommRunHook): Fixed typo in variable name, + * comm.man: should be 'res', not 'result'. Bumped the package + * pkgIndex.tcl: version to 4.5.5. Thanks to Bryan Oakley. + +2007-08-09 Andreas Kupries <andreask@activestate.com> + + * comm.tcl (comm::comm_cmd_send): Replaced the 'after idle unset + result' with an immediate unset, saving the information into a + local variable. The 'after idle' can spill into a forked child + process if there is no event loop between its location and the + fork. This may bork the child if the next event loop is the + 'vwait' of comm's send a few lines above, and the child used the + same serial number for its next request. In that case the + parent's 'after idle unset' will delete the very array element + the child is waiting for, unlocking the vwait, causing it to + access a now missing array element, instead of the expected + result. Fix by JeffH, Analysis by AndreasK, bugfix actually done + before the analysis. This bug happened at Cisco. + * comm.man: Bumped the package version to 4.5.4. + * pkgIndex.tcl: + +2007-06-10 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.tcl: Bugfixes in the wrapper for 'update'. + * comm.man: Bumped the package version to 4.5.3. + * pkgIndex.tcl: + +2007-05-10 Andreas Kupries <andreask@activestate.com> + + * comm.tcl: Bugfix in the wrapper for 'update'. Bumped the + * comm.man: package version to 4.5.2. + * pkgIndex.tcl: + +2007-05-04 Andreas Kupries <andreask@activestate.com> + + * comm.tcl: Bugfixes in the handling of -interp for regular + * comm.man: scripts. The handling of the buffer was wrong for + * pkgIndex.tcl: scripts which are a single statement as + list. Fixed missing argument to new command commSendReply, + introduced by version 4.5. Affected debugging. Bumped package + version to 4.5.1. + +2007-05-01 Andreas Kupries <andreask@activestate.com> + + * comm.tcl: Added ability to asynchronously generate script + * comm.man: results. Enables proper handling of long-running + * comm.test: operations (like db queries) without blocking the + * comm.slaveboot: server, nor requiring nested eventloops. + * pkgIndex.tcl: Extended documentation, and testsuite. Version + bumped to 4.5. Now depending on snit, and Tcl 8.3. + +2007-03-19 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.man: Fixed all warnings due to use of now deprecated + * comm_wire.man: commands. Added a section about how to give + feedback. + +2006-11-29 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.tcl (commRunHook): Fixed double execution of the hook + script. Thanks to Will Duquette for the report. + +2006-10-03 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.9 ======================== + * + +2006-09-19 Andreas Kupries <andreask@activestate.com> + + * pkgIndex.tcl: Bumped to version 4.4. changes were extension + * comm.tcl: of the existing API. + * comm.man: + +2006-09-05 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm_wire.man: Clarified the use of the TCP port in the initial + message a bit more, i.e. the meaning of the special value '0'. + + * comm.tcl: The rewrite of the hook handling broken the promised + * comm.man: semantics. Fixed. Also extended the handling of a + configured -interp to deal with a variety of possibilities + regarding missing or hidden commands. Updated the documentation. + + * pkgIndex.tcl: Bumped to version 4.3.2 + +2006-08-16 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.man: Bumped to version 4.3.1 + * comm.tcl: + * pkgIndex.tcl: + + * comm.tcl: Implemented the new options -interp and -events, + * comm.test: extended the testsuite to cover them. Created + utility/helper command for the execution of hook scripts, and + rewrote all hook places to use it. + +2006-08-14 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.man: Documented an easier use of slave interpreters + (-interp, -events). + +2006-08-11 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.test: Moved startup and cleanup of slave process + * comm.slaveboot: into a separate file. + +2006-01-28 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.test: Fixed cleanup of temp. files. + +2006-01-22 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.test: More boilerplate simplified via use of test support. + +2006-01-19 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.test: Hooked into the new common test support code. + +2006-01-10 Andreas Kupries <andreask@activestate.com> + + * comm.test: Fixed [SF Tcllib Bug 1316033]. Uncluttering test + output. + +2005-10-06 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.8 ======================== + * + +2005-10-03 Andreas Kupries <andreask@activestate.com> + + * comm.tcl: Accepted [SF Tcllib Bug 1006282], which is actually an + * comm.man: RFE. Comm channels are extended with an option which + allows the user to force the server side to silently + ignore connection attempts where the protocol + negotiation with the other side failed. + +2005-03-07 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm_wire.man: Added documentation for the wire protocol run by + comm internally. + +2004-10-05 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.7 ======================== + * + +2004-08-24 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.tcl: Typo police. + * comm.man: + +2004-05-23 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.6.1 ======================== + * + +2004-02-15 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.6 ======================== + * + +2003-10-23 Andreas Kupries <andreask@activestate.com> + + * comm.man: Updated version number in documentation. + +2003-10-21 Jeff Hobbs <jeffh@ActiveState.com> + + * pkgIndex.tcl: updated to v4.2. + + * comm.man: + * comm.tcl (comm_cmd_new): make 'comm::comm new ?chan?' fully + qualify the namespace of the new channel and return that. + [Bug #741653, #817351] + + * comm.tcl: change default encoding to utf-8. This should still + work with other versions of comm because the previous one-sided + binary setting only allowed for limited i18n-ness. Using this + version of comm on both sides will ensure full i18n-happiness. + [Bug #806420] + +2003-05-09 Jeff Hobbs <jeffh@ActiveState.com> + + * pkgIndex.tcl: updated to comm 4.1 + * comm.man: + * comm.tcl: rewrite of code to remove pseudo-object model. + Clean up code, add send -command callback to allow for + notification of results for asynchronous sends. + +2003-05-08 Jeff Hobbs <jeffh@ActiveState.com> + + * comm.tcl: update use of string functions to 8.2 cleanliness. + +2003-05-05 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.4 ======================== + * + +2003-04-11 Andreas Kupries <andreask@activestate.com> + + * comm.man: + * comm.tcl: + * pkgIndex.tcl: Set version of the package to to 4.0.1. + +2003-01-28 David N. Welton <davidw@dedasys.com> + + * comm.tcl (::comm::commConfigure): Use 'string is integer' + instead of regexp's. + Require Tcl 8.2. + +2003-01-16 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.man: More semantic markup, less visual one. + +2002-08-06 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.test: Removed writing of file ~/foo, was debugging + code. Changed creation and usage of file 'spawn' to allow an + arbitrary setting of -tmpdir. Fixes SF Bug #589225 reported by + Don Porter <dgp@users.sourceforge.net>. + +2002-03-06 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * Bumped version number to 4.0 per request by John LoVerso. + + * comm.tcl: Applied patch #526499 improving the handling of errors + for async invoked commands. + +2002-02-14 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.tcl: Frink run. + +2002-01-15 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * Bumped version to 3.7.1. + +2001-11-16 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * comm.n: Updated to reflect the changes in the comm code + (namespaces). This fixes SF item #480227. + + * comm.tcl: Fixed two places where namespacing was not handled + correctly. + +2001-08-22 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * Integrated into tcllib. diff --git a/tcllib/modules/comm/comm.LICENSE b/tcllib/modules/comm/comm.LICENSE new file mode 100644 index 0000000..3e87505 --- /dev/null +++ b/tcllib/modules/comm/comm.LICENSE @@ -0,0 +1,48 @@ +Copyright (C) 1995-1998, The Open Group. All Rights Reserved. + +This software was developed by the Open Group Research Institute +("RI"). This software, both binary and source (hereafter, Software) +is copyrighted by The Open Group Research Institute and ownership +remains with the RI. + +The RI hereby grants you (hereafter, Licensee) permission to use, +copy, modify, distribute, and license this Software and its +documentation for any purpose, provided that existing copyright +notices are retained in all copies and that this notice is included +verbatim in any distributions. No written agreement, license, or +royalty fee is required for any of the authorized uses provided +that the RI is publicly and prominently acknowledged as the source +of this software. + +Licensee may make derivative works. However, if Licensee distributes +any derivative work based on or derived from the Software, then +Licensee will (1) notify the RI regarding its distribution of the +derivative work, (2) clearly notify users that such derivative work +is a modified version and not the original software distributed by +the RI, and (3) the RI is publicly and prominently acknowledged as +the source of this software. + +THE RI MAKES NO REPRESENTATIONS ABOUT THE SERVICEABILITY OF THIS +SOFTWARE FOR ANY PURPOSE. IT IS PROVIDED "AS IS" WITHOUT EXPRESS +OR IMPLIED WARRANTY. THE RI SHALL NOT BE LIABLE FOR ANY DAMAGES +SUFFERED BY THE USERS OF THIS SOFTWARE. + +By using or copying this Software, Licensee agrees to abide by the +copyright law and all other applicable laws of the U.S. including, +but not limited to, export control laws, and the terms of this +license. The RI shall have the right to terminate this license +immediately by written notice upon Licensee's breach of, or +non-compliance with, any of its terms. Licensee may be held legally +responsible for any copyright infringement that is caused or +encouraged by Licensee's failure to abide by the terms of this +license. + +Comments and questions on this license are welcome and can be sent to: + + ri-software@opengroup.org + +Comments and questions on this software should be sent to the author: + + j.loverso@opengroup.org + john@loverso.southborough.ma.us + diff --git a/tcllib/modules/comm/comm.man b/tcllib/modules/comm/comm.man new file mode 100644 index 0000000..be1290a --- /dev/null +++ b/tcllib/modules/comm/comm.man @@ -0,0 +1,1230 @@ +[vset COMM_VERSION 4.6.3] +[manpage_begin comm n [vset COMM_VERSION]] +[see_also send(n)] +[keywords comm] +[keywords communication] +[keywords ipc] +[keywords message] +[keywords {remote communication}] +[keywords {remote execution}] +[keywords rpc] +[keywords secure] +[keywords send] +[keywords socket] +[keywords ssl] +[keywords tls] +[copyright {1995-1998 The Open Group. All Rights Reserved.}] +[copyright {2003-2004 ActiveState Corporation.}] +[copyright {2006-2009 Andreas Kupries <andreas_kupries@users.sourceforge.net>}] +[moddesc {Remote communication}] +[titledesc {A remote communication facility for Tcl (8.3 and later)}] +[category {Programming tools}] +[require Tcl 8.3] +[require comm [opt [vset COMM_VERSION]]] +[description] + +[para] + +The [package comm] command provides an inter-interpreter remote +execution facility much like Tk's [cmd send(n)], except that it uses +sockets rather than the X server for the communication path. As a +result, [package comm] works with multiple interpreters, works on +Windows and Macintosh systems, and provides control over the remote +execution path. + +[para] + +These commands work just like [cmd send] and [cmd {winfo interps}] : + +[para] +[example { + ::comm::comm send ?-async? id cmd ?arg arg ...? + ::comm::comm interps +}] +[para] + +This is all that is really needed to know in order to use +[package comm] + +[subsection Commands] + +The package initializes [cmd ::comm::comm] as the default [emph chan]. + +[para] +[package comm] names communication endpoints with an [emph id] unique +to each machine. Before sending commands, the [emph id] of another +interpreter is needed. Unlike Tk's send, [package comm] doesn't +implicitly know the [emph id]'s of all the interpreters on the system. + +The following four methods make up the basic [package comm] interface. + +[list_begin definitions] + +[call [cmd {::comm::comm send}] [opt -async] \ + [opt "-command [arg callback]"] \ + [arg id] [arg cmd] [opt [arg {arg arg ...}]]] + +This invokes the given command in the interpreter named by [arg id]. The +command waits for the result and remote errors are returned unless the +[option -async] or [option -command] option is given. If [option -async] +is given, send returns immediately and there is no further notification of +result. If [option -command] is used, [emph callback] specifies a command +to invoke when the result is received. These options are mutually +exclusive. The callback will receive arguments in the form +[emph {-option value}], suitable for [cmd {array set}]. + +The options are: [emph -id], the comm id of the interpreter that received +the command; [emph -serial], a unique serial for each command sent to a +particular comm interpreter; [emph -chan], the comm channel name; +[emph -code], the result code of the command; [emph -errorcode], the +errorcode, if any, of the command; [emph -errorinfo], the errorinfo, if +any, of the command; and [emph -result], the return value of the command. +If connection is lost before a reply is received, the callback will be +invoked with a connection lost message with -code equal to -1. When +[option -command] is used, the command returns the unique serial for the +command. + +[call [cmd {::comm::comm self}]] + +Returns the [emph id] for this channel. + +[call [cmd {::comm::comm interps}]] + +Returns a list of all the remote [emph id]'s to which this channel is +connected. [package comm] learns a new remote [emph id] when a command +is first issued it, or when a remote [emph id] first issues a command +to this comm channel. [cmd {::comm::comm ids}] is an alias for this +method. + +[call [cmd {::comm::comm connect}] [opt [arg id]]] + +Whereas [cmd {::comm::comm send}] will automatically connect to the +given [arg id], this forces a connection to a remote [emph id] without +sending a command. After this, the remote [emph id] will appear in +[cmd {::comm::comm interps}]. + +[list_end] + +[subsection {Eval Semantics}] +[para] + +The evaluation semantics of [cmd {::comm::comm send}] are intended to +match Tk's [cmd send] [emph exactly]. This means that [package comm] +evaluates arguments on the remote side. + +[para] + +If you find that [cmd {::comm::comm send}] doesn't work for a +particular command, try the same thing with Tk's send and see if the +result is different. If there is a problem, please report it. For +instance, there was had one report that this command produced an +error. Note that the equivalent [cmd send] command also produces the +same error. + +[para] +[example { + % ::comm::comm send id llength {a b c} + wrong # args: should be "llength list" + % send name llength {a b c} + wrong # args: should be "llength list" +}] +[para] + +The [cmd eval] hook (described below) can be used to change from +[cmd send]'s double eval semantics to single eval semantics. + +[subsection {Multiple Channels}] +[para] + +More than one [cmd comm] channel (or [emph listener]) can be created +in each Tcl interpreter. This allows flexibility to create full and +restricted channels. For instance, [term hook] scripts are specific +to the channel they are defined against. + +[list_begin definitions] + +[call [cmd {::comm::comm new}] [arg chan] [opt [arg {name value ...}]]] + +This creates a new channel and Tcl command with the given channel +name. This new command controls the new channel and takes all the +same arguments as [cmd ::comm::comm]. Any remaining arguments are +passed to the [cmd config] method. The fully qualified channel +name is returned. + +[call [cmd {::comm::comm channels}]] + +This lists all the channels allocated in this Tcl interpreter. + +[list_end] +[para] + +The default configuration parameters for a new channel are: + +[para] +[example { + "-port 0 -local 1 -listen 0 -silent 0" +}] +[para] + +The default channel [cmd ::comm::comm] is created with: + +[para] +[example { + "::comm::comm new ::comm::comm -port 0 -local 1 -listen 1 -silent 0" +}] + +[subsection {Channel Configuration}] +[para] + +The [cmd config] method acts similar to [cmd fconfigure] in that it +sets or queries configuration variables associated with a channel. + +[list_begin definitions] +[call [cmd {::comm::comm config}]] +[call [cmd {::comm::comm config}] [arg name]] +[call [cmd {::comm::comm config}] [opt "[arg name] [arg value] [arg ...]"]] + +When given no arguments, [cmd config] returns a list of all variables +and their value With one argument, [cmd config] returns the value of +just that argument. With an even number of arguments, the given +variables are set to the given values. + +[list_end] + +[para] + +These configuration variables can be changed (descriptions of them are +elsewhere in this manual page): + +[list_begin definitions] +[def "[option -listen] [opt [arg 0|1]]"] +[def "[option -local] [opt [arg 0|1]]"] +[def "[option -port] [opt [arg port]]"] +[def "[option -silent] [opt [arg 0|1]]"] +[def "[option -socketcmd] [opt [arg commandname]]"] + +[def "[option -interp] [opt [arg interpreter]]"] +[def "[option -events] [opt [arg eventlist]]"] +[list_end] + +[para] +These configuration variables are read only: + +[list_begin definitions] +[def "[option -chan] [arg chan]"] +[def "[option -serial] [arg n]"] +[def "[option -socket] sock[arg In]"] +[list_end] + +[para] + +When [cmd config] changes the parameters of an existing channel (with +the exception of [option -interp] and [option -events]), it closes and +reopens the listening socket. + +An automatically assigned channel [emph id] will change when this +happens. + +Recycling the socket is done by invoking [cmd {::comm::comm abort}], +which causes all active sends to terminate. + +[subsection {Id/port Assignments}] +[para] + +[package comm] uses a TCP port for endpoint [emph id]. The + +[method interps] (or [method ids]) method merely lists all the TCP ports +to which the channel is connected. By default, each channel's + +[emph id] is randomly assigned by the operating system (but usually +starts at a low value around 1024 and increases each time a new socket +is opened). This behavior is accomplished by giving the + +[option -port] config option a value of 0. Alternately, a specific +TCP port number may be provided for a given channel. As a special +case, comm contains code to allocate a a high-numbered TCP port +(>10000) by using [option {-port {}}]. Note that a channel won't be +created and initialized unless the specific port can be allocated. + +[para] + +As a special case, if the channel is configured with + +[option {-listen 0}], then it will not create a listening socket and +will use an id of [const 0] for itself. Such a channel is only good +for outgoing connections (although once a connection is established, +it can carry send traffic in both directions). + +As another special case, if the channel is configured with + +[option {-silent 0}], then the listening side will ignore connection +attempts where the protocol negotiation phase failed, instead of +throwing an error. + +[subsection {Execution Environment}] + +A communication channel in its default configuration will use the +current interpreter for the execution of all received scripts, and of +the event scripts associated with the various hooks. + +[para] + +This insecure setup can be changed by the user via the two options +[option -interp], and [option -events]. + +[para] + +When [option -interp] is set all received scripts are executed in the +slave interpreter specified as the value of the option. This +interpreter is expected to exist before configuration. I.e. it is the +responsibility of the user to create it. However afterward the +communication channel takes ownership of this interpreter, and will +destroy it when the communication channel is destroyed. + +Note that reconfiguration of the communication channel to either a +different interpreter or the empty string will release the ownership +[emph without] destroying the previously configured interpreter. The +empty string has a special meaning, it restores the default behaviour +of executing received scripts in the current interpreter. + +[para] + +[emph {Also of note}] is that replies and callbacks (a special form of +reply) are [emph not] considered as received scripts. They are +trusted, part of the internal machinery of comm, and therefore always +executed in the current interpreter. + +[para] + +Even if an interpreter has been configured as the execution +environment for received scripts the event scripts associated with the +various hooks will by default still be executed in the current +interpreter. To change this use the option [option -events] to declare +a list of the events whose scripts should be executed in the declared +interpreter as well. The contents of this option are ignored if the +communication channel is configured to execute received scripts in the +current interpreter. + +[subsection {Remote Interpreters}] +[para] + +By default, each channel is restricted to accepting connections from +the local system. This can be overridden by using the + +[option {-local 0}] configuration option For such channels, the + +[emph id] parameter takes the form [emph "\{ id host \}"]. + +[para] + +[emph WARNING]: The [emph host] must always be specified in the same +form (e.g., as either a fully qualified domain name, plain hostname or +an IP address). + +[subsection {Closing Connections}] +[para] + +These methods give control over closing connections: + +[list_begin definitions] + +[call [cmd {::comm::comm shutdown}] [arg id]] + +This closes the connection to [arg id], aborting all outstanding +commands in progress. Note that nothing prevents the connection from +being immediately reopened by another incoming or outgoing command. + +[call [cmd {::comm::comm abort}]] + +This invokes shutdown on all open connections in this comm channel. + +[call [cmd {::comm::comm destroy}]] + +This aborts all connections and then destroys the this comm channel +itself, including closing the listening socket. Special code allows +the default [cmd ::comm::comm] channel to be closed such that the + +[cmd ::comm::comm] command it is not destroyed. Doing so closes the +listening socket, preventing both incoming and outgoing commands on +the channel. This sequence reinitializes the default channel: + +[para] +[example { + "::comm::comm destroy; ::comm::comm new ::comm::comm" +}] + +[list_end] + +[para] + +When a remote connection is lost (because the remote exited or called +[cmd shutdown]), [package comm] can invoke an application callback. +This can be used to cleanup or restart an ancillary process, for +instance. See the [term lost] callback below. + +[subsection Callbacks] +[para] +This is a mechanism for setting hooks for particular events: + +[list_begin definitions] + +[call [cmd {::comm::comm hook}] [arg event] [opt [const +]] [opt [arg script]]] + +This uses a syntax similar to Tk's [cmd bind] command. Prefixing + +[arg script] with a [const +] causes the new script to be appended. +Without this, a new [arg script] replaces any existing script. When +invoked without a script, no change is made. In all cases, the new +hook script is returned by the command. + +[para] + +When an [arg event] occurs, the [arg script] associated with it is +evaluated with the listed variables in scope and available. The +return code ([emph not] the return value) of the script is commonly +used decide how to further process after the hook. + +[para] +Common variables include: + +[list_begin definitions] + +[def [var chan]] +the name of the comm channel (and command) + +[def [var id]] +the id of the remote in question + +[def [var fid]] +the file id for the socket of the connection + +[list_end] +[list_end] + +[para] +These are the defined [emph events]: + +[list_begin definitions] + +[def [const connecting]] + +Variables: +[var chan], [var id] +[comment {[var host], and [var port] are NOT defined when this is called}] +[para] + +This hook is invoked before making a connection to the remote named in +[arg id]. An error return (via [cmd error]) will abort the connection +attempt with the error. Example: + +[para] +[example { + % ::comm::comm hook connecting { + if {[string match {*[02468]} $id]} { + error "Can't connect to even ids" + } + } + % ::comm::comm send 10000 puts ok + Connect to remote failed: Can't connect to even ids + % +}] + +[def [const connected]] + +Variables: +[var chan], [var fid], [var id], [var host], and [var port]. +[para] + +This hook is invoked immediately after making a remote connection to +[arg id], allowing arbitrary authentication over the socket named by +[arg fid]. An error return (via [cmd error] ) will close the +connection with the error. [arg host] and [arg port] are merely +extracted from the [arg id]; changing any of these will have no effect +on the connection, however. It is also possible to substitute and +replace [arg fid]. + +[def [const incoming]] + +Variables: +[var chan], [var fid], [var addr], and [var remport]. +[para] + +Hook invoked when receiving an incoming connection, allowing arbitrary +authentication over socket named by [arg fid]. An error return (via +[cmd error]) will close the connection with the error. Note that the +peer is named by [arg remport] and [arg addr] but that the remote +[emph id] is still unknown. Example: + +[para] +[example { + ::comm::comm hook incoming { + if {[string match 127.0.0.1 $addr]} { + error "I don't talk to myself" + } + } +}] + +[def [const eval]] + +Variables: +[var chan], [var id], [var cmd], and [var buffer]. +[para] + +This hook is invoked after collecting a complete script from a remote +but [emph before] evaluating it. This allows complete control over +the processing of incoming commands. [arg cmd] contains either +[const send] or [const async]. [arg buffer] holds the script to +evaluate. At the time the hook is called, [arg {$chan remoteid}] is +identical in value to [arg id]. + +[para] + +By changing [arg buffer], the hook can change the script to be +evaluated. The hook can short circuit evaluation and cause a value to +be immediately returned by using [cmd return] [arg result] (or, from +within a procedure, [cmd {return -code return}] [arg result]). An +error return (via [cmd error]) will return an error result, as is if +the script caused the error. Any other return will evaluate the +script in [arg buffer] as normal. For compatibility with 3.2, + +[cmd break] and [cmd {return -code break}] [arg result] is supported, +acting similarly to [cmd {return {}}] and [cmd {return -code return}] +[arg result]. + +[para] + +Examples: + +[list_begin enumerated] + +[enum] +augmenting a command +[para] +[example { + % ::comm::comm send [::comm::comm self] pid + 5013 + % ::comm::comm hook eval {puts "going to execute $buffer"} + % ::comm::comm send [::comm::comm self] pid + going to execute pid + 5013 +}] + +[enum] +short circuiting a command +[para] +[example { + % ::comm::comm hook eval {puts "would have executed $buffer"; return 0} + % ::comm::comm send [::comm::comm self] pid + would have executed pid + 0 +}] + +[enum] +Replacing double eval semantics +[para] +[example { + % ::comm::comm send [::comm::comm self] llength {a b c} + wrong # args: should be "llength list" + % ::comm::comm hook eval {return [uplevel #0 $buffer]} + return [uplevel #0 $buffer] + % ::comm::comm send [::comm::comm self] llength {a b c} + 3 +}] + +[enum] +Using a slave interpreter +[para] +[example { + % interp create foo + % ::comm::comm hook eval {return [foo eval $buffer]} + % ::comm::comm send [::comm::comm self] set myvar 123 + 123 + % set myvar + can't read "myvar": no such variable + % foo eval set myvar + 123 +}] + +[enum] +Using a slave interpreter (double eval) +[para] +[example { + % ::comm::comm hook eval {return [eval foo eval $buffer]} +}] + +[enum] +Subverting the script to execute +[para] +[example { + % ::comm::comm hook eval { + switch -- $buffer { + a {return A-OK} + b {return B-OK} + default {error "$buffer is a no-no"} + } + } + % ::comm::comm send [::comm::comm self] pid + pid is a no-no + % ::comm::comm send [::comm::comm self] a + A-OK +}] + +[list_end] + +[def [const reply]] + +Variables: +[var chan], [var id], [var buffer], [var ret], and [var return()]. +[para] + +This hook is invoked after collecting a complete reply script from a +remote but [emph before] evaluating it. This allows complete +control over the processing of replies to sent commands. The reply +[arg buffer] is in one of the following forms + +[list_begin itemized] +[item] +return result +[item] +return -code code result +[item] +return -code code -errorinfo info -errorcode ecode msg +[list_end] +[para] + +For safety reasons, this is decomposed. The return result is in +[arg ret], and the return switches are in the return array: + +[list_begin itemized] +[item] +[emph return(-code)] +[item] +[emph return(-errorinfo)] +[item] +[emph return(-errorcode)] +[list_end] +[para] + +Any of these may be the empty string. Modifying these four variables +can change the return value, whereas modifying [arg buffer] has no +effect. + +[def [const callback]] + +Variables: +[var chan], [var id], [var buffer], [var ret], and [var return()]. +[para] + +Similar to [emph reply], but used for callbacks. + +[def [const lost]] + +Variables: +[var chan], [var id], and [var reason]. +[para] + +This hook is invoked when the connection to [var id] is lost. Return +value (or thrown error) is ignored. [arg reason] is an explanatory +string indicating why the connection was lost. Example: + +[para] + +[example { + ::comm::comm hook lost { + global myvar + if {$myvar(id) == $id} { + myfunc + return + } + } +}] + +[list_end] + +[subsection Unsupported] +[para] +These interfaces may change or go away in subsequence releases. + +[list_begin definitions] +[call [cmd {::comm::comm remoteid}]] + +Returns the [arg id] of the sender of the last remote command +executed on this channel. If used by a proc being invoked remotely, +it must be called before any events are processed. Otherwise, another +command may get invoked and change the value. + +[call [cmd ::comm::comm_send]] + +Invoking this procedure will substitute the Tk [cmd send] and +[cmd {winfo interps}] commands with these equivalents that use +[cmd ::comm::comm]. + +[para] + +[example { + proc send {args} { + eval ::comm::comm send $args + } + rename winfo tk_winfo + proc winfo {cmd args} { + if {![string match in* $cmd]} { + return [eval [list tk_winfo $cmd] $args] + } + return [::comm::comm interps] + } +}] + +[list_end] + +[subsection Security] + +Starting with version 4.6 of the package an option [option -socketcmd] +is supported, allowing the user of a comm channel to specify which +command to use when opening a socket. Anything which is API-compatible +with the builtin [cmd ::socket] (the default) can be used. + +[para] + +The envisioned main use is the specification of the [cmd tls::socket] +command, see package [package tls], to secure the communication. + +[para] +[example { + # Load and initialize tls + package require tls + tls::init -cafile /path/to/ca/cert -keyfile ... + + # Create secured comm channel + ::comm::comm new SECURE -socketcmd tls::socket -listen 1 + ... +}] + +[para] + +The sections [sectref {Execution Environment}] and [sectref Callbacks] +are also relevant to the security of the system, providing means to +restrict the execution to a specific environment, perform additional +authentication, and the like. + +[subsection {Blocking Semantics}] + +[para] + +There is one outstanding difference between [package comm] and + +[cmd send]. When blocking in a synchronous remote command, [cmd send] +uses an internal C hook (Tk_RestrictEvents) to the event loop to look +ahead for send-related events and only process those without +processing any other events. In contrast, [package comm] uses the + +[cmd vwait] command as a semaphore to indicate the return message has +arrived. The difference is that a synchronous [cmd send] will block +the application and prevent all events (including window related ones) +from being processed, while a synchronous [cmd {::comm::comm send}] +will block the application but still allow other events to get +processed. In particular, [cmd {after idle}] handlers will fire +immediately when comm blocks. + +[para] + +What can be done about this? First, note that this behavior will come +from any code using [cmd vwait] to block and wait for an event to +occur. At the cost of multiple channel support, [package comm] could +be changed to do blocking I/O on the socket, giving send-like blocking +semantics. However, multiple channel support is a very useful feature +of comm that it is deemed too important to lose. The remaining +approaches involve a new loadable module written in C (which is +somewhat against the philosophy of [cmd comm ]) One way would be to +create a modified version of the [cmd vwait] command that allow the +event flags passed to Tcl_DoOneEvent to be specified. For [cmd comm], +just the TCL_FILE_EVENTS would be processed. Another way would be to +implement a mechanism like Tk_RestrictEvents, but apply it to the Tcl +event loop (since [package comm] doesn't require Tk). One of these +approaches will be available in a future [package comm] release as an +optional component. + +[subsection {Asynchronous Result Generation}] + +By default the result returned by a remotely invoked command is the +result sent back to the invoker. This means that the result is +generated synchronously, and the server handling the call is blocked +for the duration of the command. + +[para] + +While this is tolerable as long as only short-running commands are +invoked on the server long-running commands, like database queries +make this a problem. One command can prevent the processing requests +of all other clients for an arbitrary period of time. + +[para] + +Before version 4.5 of comm the only solution was to rewrite the server +command to use the Tcl builtin command [cmd vwait], or one of its +relatives like [cmd tkwait], to open a new event loop which processes +requests while the long-running operation is executed. This however +has its own perils, as this makes it possible to both overflow the Tcl +stack with a large number of event loop, and to have a newer requests +block the return of older ones, as the eventloop have to be unwound in +the order of their creation. + +[para] + +The proper solution is to have the invoked command indicate to +[package comm] that it cannot or will not deliver an immediate, +synchronous result, but will do so later. At that point the framework +can put sending the actual result on hold and continue processing +requests using the main event loop. No blocking, no nesting of event +loops. At some future date the long running operation delivers the +result to comm, via the future object, which is then forwarded to the +invoker as usual. + +[para] + +The necessary support for this solution has been added to comm since +version 4.5, in the form of the new method [method return_async]. + +[list_begin definitions] +[call [cmd {::comm::comm return_async}]] + +This command is used by a remotely invoked script to notify the comm +channel which invoked it that the result to send back to the invoker +is not generated synchronously. If this command is not called the +default/standard behaviour of comm is to send the synchronously +generated result of the script itself to the invoker. + +[para] + +The result of [cmd return_async] is an object. This object, called a +[term future] is where the result of the script has to be delivered to +when it becomes ready. When that happens it will take all the +necessary actions to deliver the result to the invoker of the script, +and then destroy itself. Should comm have lost the connection to the +invoker while the result is being computed the future will not try to +deliver the result it got, but just destroy itself. The future can be +configured with a command to call when the invoker is lost. This +enables the user to implement an early abort of the long-running +operation, should this be supported by it. + +[para] +An example: + +[example { +# Procedure invoked by remote clients to run database operations. +proc select {sql} { + # Signal the async generation of the result + + set future [::comm::comm return_async] + + # Generate an async db operation and tell it where to deliver the result. + + set query [db query -command [list $future return] $sql] + + # Tell the database system which query to cancel if the connection + # goes away while it is running. + + $future configure -command [list db cancel $query] + + # Note: The above will work without problem only if the async + # query will nover run its completion callback immediately, but + # only from the eventloop. Because otherwise the future we wish to + # configure may already be gone. If that is possible use 'catch' + # to prevent the error from propagating. + return +} +}] +[para] + +The API of a future object is: + +[list_begin definitions] +[call [cmd \$future] [method return] [opt "[option -code] [arg code]"] [opt [arg value]]] + +Use this method to tell the future that long-running operation has +completed. Arguments are an optional return value (defaults to the +empty string), and the Tcl return code (defaults to OK). + +[para] + +The future will deliver this information to invoker, if the connection +was not lost in the meantime, and then destroy itself. If the +connection was lost it will do nothing but destroy itself. + +[call [cmd \$future] [method configure] [opt "[option -command] [opt [arg cmdprefix]]"]] +[call [cmd \$future] [method cget] [option -command]] + +These methods allow the user to retrieve and set a command to be +called if the connection the future belongs to has been lost. + +[list_end] + +[list_end] + +[subsection Compatibility] +[para] + +[package comm] exports itself as a package. The package version number +is in the form [emph "major . minor"], where the major version will +only change when a non-compatible change happens to the API or +protocol. Minor bug fixes and changes will only affect the minor +version. To load [package comm] this command is usually used: + +[para] +[example { + package require comm 3 +}] + +[para] +Note that requiring no version (or a specific version) can also be done. + +[para] +The revision history of [package comm] includes these releases: + +[list_begin definitions] + +[def 4.6.3] + +Fixed ticket [lb]ced0d60fc9[rb]. Added proper detection of eof on a +socket, properly closing it. + +[def 4.6.2] + +Fixed bugs 2972571 and 3066872, the first a misdetection of quoted +brace after double backslash, the other a blocking gets making for an +obvious (hinsight) DoS attack on comm channels. + +[def 4.6.1] + +Changed the implementation of [cmd comm::commCollect] to emulate +lindex's pre-Tcl 8 behaviour, i.e. it was given the ability to parse +out the first word of a list, even if the whole buffer is not a +well-formed list. Without this change the first word could only be +extracted if the whole buffer was a well-formed list (ever since Tcl +8), and in a ver-high-load situation, i.e. a server sending lots +and/or large commands very fast, this may never happen, eventually +crashing the receiver when it runs out of memory. With the change the +receiver is always able to process the first word when it becomes +well-formed, regardless of the structure of the remainder of the +buffer. + +[def 4.6] + +Added the option [option -socketcmd] enabling users to override how a +socket is opened. The envisioned main use is the specification of the +[cmd tls::socket] command, see package [package tls], to secure the +communication. + +[def 4.5.7] + +Changed handling of ports already in use to provide a proper error +message. + +[def 4.5.6] + +Bugfix in the replacement for [cmd vwait], made robust against of +variable names containing spaces. + +[def 4.5.5] + +Bugfix in the handling of hooks, typo in variable name. + +[def 4.5.4] + +Bugfix in the handling of the result received by the [method send] +method. Replaced an [emph {after idle unset result}] with an immediate +[cmd unset], with the information saved to a local variable. + +[para] + +The [cmd {after idle}] can spill into a forked child process if there +is no event loop between its setup and the fork. This may bork the +child if the next event loop is the [cmd vwait] of [package comm]'s +[method send] a few lines above the [cmd {after idle}], and the child +used the same serial number for its next request. In that case the +parent's [cmd {after idle unset}] will delete the very array element +the child is waiting for, unlocking the [cmd vwait], causing it to +access a now missing array element, instead of the expected result. + +[def 4.5.3] + +Bugfixes in the wrappers for the builtin [cmd update] and [cmd vwait] +commands. + +[def 4.5.2] + +Bugfix in the wrapper for the builtin [cmd update] command. + +[def 4.5.1] + +Bugfixes in the handling of -interp for regular scripts. The handling +of the buffer was wrong for scripts which are a single statement as +list. Fixed missing argument to new command [cmd commSendReply], +introduced by version 4.5. Affected debugging. + +[def 4.5] + +New server-side feature. The command invoked on the server can now +switch comm from the standard synchronous return of its result to an +asynchronous (defered) return. Due to the use of snit to implement the +[term future] objects used by this feature from this version on comm +requires at least Tcl 8.3 to run. Please read the section +[sectref {Asynchronous Result Generation}] for more details. + +[def 4.4.1] + +Bugfix in the execution of hooks. + +[def 4.4] + +Bugfixes in the handling of -interp for regular and hook +scripts. Bugfixes in channel cleanup. + +[def 4.3.1] + +Introduced -interp and -events to enable easy use of a slave interp +for execution of received scripts, and of event scripts. + +[def 4.3] + +Bugfixes, and introduces -silent to allow the user to force the +server/listening side to silently ignore connection attempts where the +protocol negotiation failed. + +[def 4.2] + +Bugfixes, and most important, switched to utf-8 as default encoding +for full i18n without any problems. + +[def 4.1] + +Rewrite of internal code to remove old pseudo-object model. Addition +of send -command asynchronous callback option. + +[def 4.0] + +Per request by John LoVerso. Improved handling of error for async +invoked commands. + +[def 3.7] + +Moved into tcllib and placed in a proper namespace. + +[def 3.6] + +A bug in the looking up of the remoteid for a executed command could +be triggered when the connection was closed while several asynchronous +sends were queued to be executed. + +[def 3.5] + +Internal change to how reply messages from a [cmd send] are handled. +Reply messages are now decoded into the [arg value] to pass to + +[cmd return]; a new return statement is then cons'd up to with this +value. Previously, the return code was passed in from the remote as a +command to evaluate. Since the wire protocol has not changed, this is +still the case. Instead, the reply handling code decodes the + +[const reply] message. + +[def 3.4] + +Added more source commentary, as well as documenting config variables +in this man page. Fixed bug were loss of connection would give error +about a variable named [var pending] rather than the message about +the lost connection. [cmd {comm ids}] is now an alias for + +[cmd {comm interps}] (previously, it an alias for [cmd {comm chans}]). +Since the method invocation change of 3.0, break and other exceptional +conditions were not being returned correctly from [cmd {comm send}]. +This has been fixed by removing the extra level of indirection into +the internal procedure [cmd commSend]. Also added propagation of +the [arg errorCode] variable. This means that these commands return +exactly as they would with [cmd send]: + +[para] +[example { + comm send id break + catch {comm send id break} + comm send id expr 1 / 0 +}] +[para] + +Added a new hook for reply messages. Reworked method invocation to +avoid the use of comm:* procedures; this also cut the invocation time +down by 40%. Documented [cmd {comm config}] (as this manual page +still listed the defunct [cmd {comm init}]!) + +[def 3.3] + +Some minor bugs were corrected and the documentation was cleaned up. +Added some examples for hooks. The return semantics of the [cmd eval] +hook were changed. + +[def 3.2] + +A new wire protocol, version 3, was added. This is backwards +compatible with version 2 but adds an exchange of supported protocol +versions to allow protocol negotiation in the future. Several bugs +with the hook implementation were fixed. A new section of the man +page on blocking semantics was added. + +[def 3.1] + +All the documented hooks were implemented. [cmd commLostHook] was +removed. A bug in [cmd {comm new}] was fixed. + +[def 3.0] + +This is a new version of [package comm] with several major changes. +There is a new way of creating the methods available under the + +[cmd comm] command. The [cmd {comm init}] method has been retired +and is replaced by [cmd {comm configure}] which allows access to many +of the well-defined internal variables. This also generalizes the +options available to [cmd {comm new}]. Finally, there is now a +protocol version exchanged when a connection is established. This +will allow for future on-wire protocol changes. Currently, the +protocol version is set to 2. + +[def 2.3] + +[cmd {comm ids}] was renamed to [cmd {comm channels}]. General +support for [cmd {comm hook}] was fully implemented, but only the +[term lost] hook exists, and it was changed to follow the general +hook API. [cmd commLostHook] was unsupported (replaced by + +[cmd {comm hook lost}]) and [cmd commLost] was removed. + +[def 2.2] + +The [term died] hook was renamed [term lost], to be accessed by +[cmd commLostHook] and an early implementation of +[cmd {comm lost hook}]. As such, [cmd commDied] is now +[cmd commLost]. + +[def 2.1] +Unsupported method [cmd {comm remoteid}] was added. + +[def 2.0] +[package comm] has been rewritten from scratch (but is fully compatible +with Comm 1.0, without the requirement to use obTcl). + +[list_end] + +[include ../common-text/tls-security-notes.inc] + +[section Author] + +John LoVerso, John@LoVerso.Southborough.MA.US + +[para] + +[emph http://www.opengroup.org/~loverso/tcl-tk/#comm] + +[section License] + +Please see the file [emph comm.LICENSE] that accompanied this source, +or +[uri http://www.opengroup.org/www/dist_client/caubweb/COPYRIGHT.free.html]. + +[para] + +This license for [package comm], new as of version 3.2, allows it to be +used for free, without any licensing fee or royalty. + +[section Bugs] +[list_begin itemized] +[item] + +If there is a failure initializing a channel created with +[cmd {::comm::comm new}], then the channel should be destroyed. +Currently, it is left in an inconsistent state. + +[item] + +There should be a way to force a channel to quiesce when changing the +configuration. + +[list_end] + +[para] +The following items can be implemented with the existing hooks and are +listed here as a reminder to provide a sample hook in a future +version. + +[list_begin itemized] +[item] + +Allow easier use of a slave interp for actual command execution +(especially when operating in "not local" mode). + +[item] + +Add host list (xhost-like) or "magic cookie" (xauth-like) +authentication to initial handshake. + +[list_end] + +[para] +The following are outstanding todo items. + +[list_begin itemized] +[item] + +Add an interp discovery and name->port mapping. This is likely to be +in a separate, optional nameserver. (See also the related work, +below.) + +[item] + +Fix the [emph {{id host}}] form so as not to be dependent upon +canonical hostnames. This requires fixes to Tcl to resolve hostnames! + +[list_end] + +[para] +This man page is bigger than the source file. + +[section {On Using Old Versions Of Tcl}] + +[para] +Tcl7.5 under Windows contains a bug that causes the interpreter to +hang when EOF is reached on non-blocking sockets. This can be +triggered with a command such as this: + +[para] +[example { + "comm send $other exit" +}] + +[para] +Always make sure the channel is quiescent before closing/exiting or +use at least Tcl7.6 under Windows. + +[para] +Tcl7.6 on the Mac contains several bugs. It is recommended you use +at least Tcl7.6p2. + +[para] +Tcl8.0 on UNIX contains a socket bug that can crash Tcl. It is recommended +you use Tcl8.0p1 (or Tcl7.6p2). + +[section {Related Work}] +[para] +Tcl-DP provides an RPC-based remote execution interface, but is a +compiled Tcl extension. See +[uri http://www.cs.cornell.edu/Info/Projects/zeno/Projects/Tcl-DP.html]. + +[para] +Michael Doyle <miked@eolas.com> has code that implements the Tcl-DP +RPC interface using standard Tcl sockets, much like [package comm]. + +[para] +Andreas Kupries <andreas_kupries@users.sourceforge.net> uses +[package comm] and has built a simple nameserver as part of his Pool +library. See [uri http://www.purl.org/net/akupries/soft/pool/index.htm]. + +[vset CATEGORY comm] +[include ../doctools2base/include/feedback.inc] +[manpage_end] diff --git a/tcllib/modules/comm/comm.n.html b/tcllib/modules/comm/comm.n.html new file mode 100644 index 0000000..03f8a89 --- /dev/null +++ b/tcllib/modules/comm/comm.n.html @@ -0,0 +1,1067 @@ +<html> +<head> +<!-- This file has been generated by unroff 1.0, 05/30/98 15:43:05. --> +<!-- Do not edit! --> +<!-- $Id: comm.n.html,v 1.3 2004/01/15 06:36:12 andreas_kupries Exp $ --> +<!-- %%_OSF_FREE_COPYRIGHT_%% --> +<!-- Copyright (C) 1995-1998 The Open Group. All Rights Reserved. --> +<!-- (Please see the file "comm.LICENSE" that accompanied this source) --> +<!-- unroff -fhtml -man comm.n --> +<!-- (then fix <title>) --> +<!-- # CS - begin code excerpt --> +<!-- # CE - end code excerpt --> +<title>Manual page for comm(n) version 3.7.1</title> +</head> +<body> +<h2> +comm.tcl - A remote communications facility for Tcl (7.6, 8.0, and later) +<hr></h2> +<h2>SYNOPSIS</h2> +<b>package require Comm 3</b> +<p> +<!-- define all interfaces ONCE --> +<!-- iD taken (see i2) --> +<!-- iE taken (see i6) --> +<!-- iF taken (see i6) --> +<!-- Show all interfaces --> +<b></b><i>chan</i><b> send </b>?<i>-async</i>? <i>id cmd </i>?<i>arg arg ...</i>?<tt> </tt> +<p> +<b></b><i>chan</i><b> interps</b> +<p> +<b></b><i>chan</i><b> ids</b> +<p> +<b></b><i>chan</i><b> self</b> +<p> +<b></b><i>chan</i><b> connect </b>?<i>id</i>?<tt> </tt> +<p> +<b></b><i>chan</i><b> config +<br> +</b><b></b><i>chan</i><b> config </b><i>name</i> +<br> +<b></b><i>chan</i><b> config ?</b><i>name value ...</i>?<tt> </tt> +<br> +<dl><dt><dd> +-<b>listen </b>?<i>0|1</i>?<tt> </tt> +-<b>local </b>?<i>0|1</i>?<tt> </tt> +-<b>port </b>?<i>port</i>?<tt> </tt> +</dl> +<p> +<b></b><i>chan</i><b> new </b><i>chan</i> ?<i>name value ...</i>?<tt> </tt> +<p> +<b></b><i>chan</i><b> channels</b> +<p> +<b></b><i>chan</i><b> shutdown </b><i>id</i> +<p> +<b></b><i>chan</i><b> abort</b> +<p> +<b></b><i>chan</i><b> destroy</b> +<p> +<b></b><i>chan</i><b> remoteid</b> +<p> +<b></b><i>chan</i><b> hook </b><i>event</i> ?<b>+</b>??<i>script</i>?<tt> </tt> +<p> +The package initializes <b>comm</b> as the default <i>chan</i>.<tt> </tt> +<h2>INTRODUCTION</h2> +<p> +The +<b>comm +</b>command provides an inter-interpreter remote execution facility +much like Tk's +<i>send</i>(n)<i>, +</i>except that it uses sockets rather than +the X server for the communication path.<tt> </tt> +As a result, +<b>comm +</b>works with multiple interpreters, +works on Windows and Macintosh systems, +and +provides control over the remote execution path.<tt> </tt> +<p> +These commands work just like +<b>send +</b>and +<b>winfo interps</b>: +<tt></tt><dl><dt><dd> +<b></b><b>comm</b><b> send </b>?<i>-async</i>? <i>id cmd </i>?<i>arg arg ...</i>? +<br> +<b></b><b>comm</b><b> interps</b> +<br> +</dl> +This is all that is really needed to know in order to use +<b>comm</b>.<tt> </tt> +<h2>DESCRIPTION</h2> +<p> +<b>comm +</b>names communication endpoints with an +<i>id +</i>unique to each machine.<tt> </tt> +Before sending commands, the +<i>id +</i>of another interpreter is needed.<tt> </tt> +Unlike Tk's send, +<b>comm +</b>doesn't implicitly know the +<i>id</i>'s +of all the interpreters on the system.<tt> </tt> +<dl> +<dt><b></b><b>comm</b><b> send </b>?<i>-async</i>? <i>id cmd </i>?<i>arg arg ...</i>?<tt> </tt> +<dd> +This invokes the given command in the interpreter named by +<i>id</i>.<tt> </tt> +The command waits for the result and remote errors are returned +unless the +<b>-async +</b>option is given.<tt> </tt> +<dt><b></b><b>comm</b><b> self</b> +<dd> +Returns the +<i>id +</i>for this channel.<tt> </tt> +<dt><b></b><b>comm</b><b> interps</b> +<dd> +Returns a list of all the remote +<i>id</i>'s +to which this channel is connected.<tt> </tt> +<b>comm +</b>learns a new remote +<i>id +</i>when a command is first issued it, +or when a remote +<i>id +</i>first issues a command to this comm channel.<tt> </tt> +<b></b><b>comm</b><b> ids</b> +is an alias for this method.<tt> </tt> +<dt><b></b><b>comm</b><b> connect </b>?<i>id</i>?<tt> </tt> +<dd> +Whereas +<b>comm send +</b>will automatically connect to the given +<i>id</i>, +this forces a connection to a remote +<i>id +</i>without sending a command.<tt> </tt> +After this, the remote +<i>id +</i>will appear in +<b>comm interps</b>.<tt> </tt> +</dl> +<p> +These four methods make up the basic +<b>comm +</b>interface.<tt> </tt> +<h2>EVAL SEMANTICS</h2> +<p> +The evaluation semantics of +<b>comm send +</b>are intended to match Tk's +<b>send +</b><i>exactly</i>.<tt> </tt> +This means that +<b>comm +</b>evaluates arguments on the remote side.<tt> </tt> +<p> +If you find that +<b>comm send +</b>doesn't work for a particular command, +try the same thing with Tk's send and see if the result is different.<tt> </tt> +If there is a problem, please report it.<tt> </tt> +For instance, there was had one report that this command produced an error.<tt> </tt> +Note that the equivalent +<b>send +</b>command also produces the same error.<tt> </tt> +<tt></tt><dl><dt><dd> +% <b>comm send </b><i>id</i><b> llength {a b c}</b> +<br> +<b>wrong # args: should be "llength list"</b> +<br> +% <b>send </b><i>name</i><b> llength {a b c}</b> +<br> +<b>wrong # args: should be "llength list"</b> +<br> +</dl> +<p> +The +<b>eval +</b>hook (described below) can be used to change from +<b>send</b>'s +double eval semantics to single eval semantics.<tt> </tt> +<h2>MULTIPLE CHANNELS</h2> +<p> +More than one +<b>comm +</b>channel (or +<i>listener</i>) +can be created in each Tcl interpeter.<tt> </tt> +This allows flexibility to create full and restricted channels.<tt> </tt> +For instance, +<b>hook +</b>scripts are specific to the channel they are defined against. +<dl> +<dt><b></b><b>comm</b><b> new </b><i>chan</i> ?<i>name value ...</i>?<tt> </tt> +<dd> +This creates a new channel and Tcl command with the given channel name.<tt> </tt> +This new command controls the new channel and takes all the same +arguments as +<b>comm</b>.<tt> </tt> +Any remaining arguments are passed to the +<b>config +</b>method.<tt> </tt> +<dt><b></b><b>comm</b><b> channels</b> +<dd> +This lists all the channels allocated in this Tcl interpreter.<tt> </tt> +</dl> +<p> +The default configuration parameters for a new channel are: +<tt></tt><dl><dt><dd> +<b>-port 0 -local 1 -listen 0 +</b></dl> +The default channel +<b>comm +</b>is created with: +<tt></tt><dl><dt><dd> +<b>comm new comm -port 0 -local 1 -listen 1 +</b></dl> +<h2>CHANNEL CONFIGURATION</h2> +<p> +The +<b>config +</b>method acts similar to +<b>fconfigure +</b>in that it sets or queries configuration variables associated with a channel.<tt> </tt> +<dl><dt><dd> +<b></b><b>comm</b><b> config +<br> +</b><b></b><b>comm</b><b> config </b><i>name</i> +<br> +<b></b><b>comm</b><b> config ?</b><i>name value ...</i>?<tt> </tt> +</dl> +When given no arguments, +<b>config +</b>returns a list of all variables and their value +With one argument, +<b>config +</b>returns the value of just that argument.<tt> </tt> +With an even number of arguments, the given variables are set to the +given values.<tt> </tt> +<p> +These configuration variables can be changed +(descriptions of them are elsewhere in this manual page): +<dl><dt><dd> +-<b>listen </b>?<i>0|1</i>?<tt> </tt> +-<b>local </b>?<i>0|1</i>?<tt> </tt> +-<b>port </b>?<i>port</i>?<tt> </tt> +</dl> +<p> +These configuration variables are readonly: +<dl><dt><dd> +-<b>chan</b> <i>chan</i> +-<b>serial</b> <i>n</i> +-<b>socket</b> sock<i>n</i> +</dl> +<p> +When +<b>config +</b>changes the parameters of an existing channel, +it closes and reopens the listening socket.<tt> </tt> +An automatically assigned channel +<i>id +</i>will change when this happens.<tt> </tt> +Recycling the socket is done by invoking +<b>comm abort</b>, +which causes all active sends to terminate.<tt> </tt> +<h2>ID/PORT ASSIGNMENTS</h2> +<p> +<b>comm +</b>uses a TCP port for endpoint +<i>id</i>.<tt> </tt> +The +<b>interps +</b>(or +<b>ids</b>) +method merely lists all the TCP ports to which the channel is connected.<tt> </tt> +By default, each channel's +<i>id +</i>is randomly assigned by the operating system +(but usually starts at a low value around 1024 and increases +each time a new socket is opened).<tt> </tt> +This behavior is accomplished by giving the +<b>-port +</b>config option a value of 0.<tt> </tt> +Alternately, a specific TCP port number may be provided for a given channel.<tt> </tt> +As a special case, comm contains code to allocate a +a high-numbered TCP port (>10000) by using +<b>-port {}</b>.<tt> </tt> +Note that a channel won't be created and initialized +unless the specific port can be allocated.<tt> </tt> +<p> +As a special case, if the channel is configured with +<b>-listen 0</b>, +then it will not create a listening socket and will use an id of +<i>0 +</i>for itself.<tt> </tt> +Such a channel is only good for outgoing connections +(although once a connection is established, it can carry send traffic +in both directions).<tt> </tt> +<h2>REMOTE INTERPRETERS</h2> +<p> +By default, each channel is restricted to accepting connections from the +local system. This can be overriden by using the +<b>-local 0 +</b>configuration option +For such channels, the +<i>id +</i>parameter takes the form +<b>{</b><i>id host</i><b>} +</b><b></b>.<tt> </tt> +<p> +<b>WARNING</b>: +The +<i>host +</i>must always be specified in the same form +(e.g., as either a fully qualified domain name, +plain hostname or an IP address).<tt> </tt> +<h2>CLOSING CONNECTIONS</h2> +<p> +These methods give control over closing connections: +<dl> +<dt><b></b><b>comm</b><b> shutdown </b><i>id</i> +<dd> +This closes the connection to +<i>id</i>, +aborting all outstanding commands in progress. Note that nothing +prevents the connection from being immediately reopened by another +incoming or outgoing command.<tt> </tt> +<dt><b></b><b>comm</b><b> abort</b> +<dd> +This invokes shutdown on all open connections in this comm channel.<tt> </tt> +<dt><b></b><b>comm</b><b> destroy</b> +<dd> +This aborts all connections and then destroys the this comm channel itself, +including closing the listening socket.<tt> </tt> +Special code allows the default +<b>comm +</b>channel to be closed +such that the +<b>comm +</b>command it is not destroyed.<tt> </tt> +Doing so closes the listening socket, preventing both +incoming and outgoing commands on the channel.<tt> </tt> +This sequence reinitializes the default channel: +<tt></tt></dl> +<dl><dt><dd> +<b>comm destroy; comm new comm +</b></dl> +<p> +When a remote connection is lost (because the remote exited or called +<b>shutdown</b>), +<b>comm +</b>can invoke an application callback.<tt> </tt> +This can be used to cleanup or restart an ancillary process, +for instance.<tt> </tt> +See the +<b>lost +</b>callback below.<tt> </tt> +<h2>CALLBACKS</h2> +<p> +This is a mechanism for setting hooks for particular events: +<tt></tt><dl><dt><dd> +<b></b><b>comm</b><b> hook </b><i>event</i> ?<b>+</b>??<i>script</i>? +<br> +</dl> +<p> +This uses a syntax similar to Tk's +<b>bind +</b>command.<tt> </tt> +Prefixing +<i>script +</i>with a + causes the new script to be appended.<tt> </tt> +Without this, a new +<i>script +</i>replaces any existing script.<tt> </tt> +When invoked without a script, no change is made.<tt> </tt> +In all cases, the new hook script is returned by the command.<tt> </tt> +<p> +When an +<i>event +</i>occurs, +the +<i>script +</i>associated with it is evaluated +with the listed variables in scope and available.<tt> </tt> +The return code +(<b>not +</b>the return value) of the script +is commonly used decide how to further process after the hook.<tt> </tt> +<p> +Common variables include: +<dl><dt><dd> +<dl> +<dt><b>chan</b><dd> +the name of the comm channel (and command) +<dt><b>id</b><dd> +the id of the remote in question +<dt><b>fid</b><dd> +the file id for the socket of the connection +</dl> +</dl> + + +These are the defined +<i>events</i>: +<dl> +<dt><b>connecting +</b><dd> +Variables: +<i>chan id host port +</i><br> +This hook is invoked before making a connection +to the remote named in +<i>id</i>.<tt> </tt> +An error return (via +<b>error</b>) +will abort the connection attempt with the error.<tt> </tt> +Example: +<p> +<tt></tt></dl> +<dl><dt><dd> +% comm hook connecting { +<br> + if [string match {*[02468]} $id] { +<br> + error "Can't connect to even ids" +<br> + } +<br> +} +<br> +% comm send 10000 puts ok +<br> +Connect to remote failed: Can't connect to even ids +<br> +% +<br> +</dl> + +<dl> +<dt><b>connected +</b><dd> +Variables: +<i>chan fid id host port +</i><br> +This hook is invoked immediately after making a remote connection to +<i>id</i>, +allowing arbitrary authentication over the socket +named by +<i>fid</i>.<tt> </tt> +An error return (via +<b>error</b>) +will close the connection with the error.<tt> </tt> +<i>host +</i>and +<i>port +</i>are merely extracted from the +<i>id</i>; +changing any of these will have no effect on the connection, however.<tt> </tt> +It is also possible to substitute and replace +<i>fid . +</i> + +<dt><b>incoming +</b><dd> +Variables: +<i>chan fid addr remport +</i><br> +Hook invoked when receiving an incoming connection, +allowing arbitrary authentication over socket +named by +<i>fid</i>.<tt> </tt> +An error return (via +<b>error</b>) +will close the connection with the error.<tt> </tt> +Note that the peer is named by +<i>remport</i> and <i>addr +</i>but that the remote +<i>id +</i>is still unknown. Example: +<p> +<tt></tt></dl> +<dl><dt><dd> +comm hook incoming { +<br> + if [string match 127.0.0.1 $addr] { +<br> + error "I don't talk to myself" +<br> + } +<br> +} +<br> +</dl> + +<dl> +<dt><b>eval +</b><dd> +Variables: +<i>chan id cmd buffer +</i><br> +This hook is invoked after collecting a complete script from a remote +but +<b>before +</b>evalutating it.<tt> </tt> +This allows complete control over the processing of incoming commands.<tt> </tt> +<i>cmd +</i>contains either +<b>send</b> or <b>async</b>.<tt> </tt> +<i>buffer +</i>holds the script to evaluate.<tt> </tt> +At the time the hook is called, +<b>$chan remoteid +</b>is identical in value to +<b>id. +</b><p> +By changing +<i>buffer</i>, +the hook can change the script to be evaluated.<tt> </tt> +The hook can short circuit evaluation and cause a +value to be immediately returned by using +<b>return +</b><i>result +</i>(or, from within a procedure, +<b>return -code return +</b><i>result</i>).<tt> </tt> +An error return (via +<b>error</b>) +will return an error result, as is if the script caused the error.<tt> </tt> +Any other return will evaluate the script in +<i>buffer +</i>as normal.<tt> </tt> +For compatibility with 3.2, +<b>break +</b>and +<b>return -code break +</b><i>result +</i>is supported, acting similarly to +<b>return {} +</b>and +<b>return -code return +</b><i>result</i>.<tt> </tt> +<p> +Examples: +</dl> +<dl><dt><dd> +1. augmenting a command +<tt></tt><dl><dt><dd> +% comm send [comm self] pid +<br> +5013 +<br> +% comm hook eval {puts "going to execute $buffer"} +<br> +% comm send [comm self] pid +<br> +going to execute pid +<br> +5013 +<br> +</dl> +2. short circuting a command +<tt></tt><dl><dt><dd> +% comm hook eval {puts "would have executed $buffer"; return 0} +<br> +% comm send [comm self] pid +<br> +would have executed pid +<br> +0 +<br> +</dl> +3. Replacing double eval semantics +<tt></tt><dl><dt><dd> +% comm send [comm self] llength {a b c} +<br> +wrong # args: should be "llength list" +<br> +% comm hook eval {return [uplevel #0 $buffer]} +<br> +return [uplevel #0 $buffer] +<br> +% comm send [comm self] llength {a b c} +<br> +3 +<br> +</dl> +4. Using a slave interpreter +<tt></tt><dl><dt><dd> +% interp create foo +<br> +% comm hook eval {return [foo eval $buffer]} +<br> +% comm send [comm self] set myvar 123 +<br> +123 +<br> +% set myvar +<br> +can't read "myvar": no such variable +<br> +% foo eval set myvar +<br> +123 +<br> +</dl> +5. Using a slave interpreter (double eval) +<tt></tt><dl><dt><dd> +% comm hook eval {return [eval foo eval $buffer]} +<br> +</dl> +6. Subverting the script to execute +<tt></tt><dl><dt><dd> +% comm hook eval { +<br> + switch -- $buffer { +<br> + a {return A-OK} b {return B-OK} default {error "$buffer is a no-no"} +<br> + } +<br> +} +<br> +% comm send [comm self] pid +<br> +pid is a no-no +<br> +% comm send [comm self] a +<br> +A-OK +<br> +</dl> +</dl> + +<dl> +<dt><b>reply +</b><dd> +Variables: +<i>chan id buffer ret return() +</i><br> +This hook is invoked after collecting a complete reply script from a remote +but +<b>before +</b>evalutating it.<tt> </tt> +This allows complete control over the processing of replies to sent commands.<tt> </tt> +The reply +<i>buffer +</i>is in one of the following forms +</dl> +<dl><dt><dd> +<tt></tt><dl><dt><dd> +return <i>result</i> +<br> +return -code <i>code</i> <i>result</i> +<br> +return -code <i>code</i> -errorinfo <i>info</i> -errorcode <i>ecode</i> <i>msg</i> +<br> +</dl> +For safety reasons, this is decomposed. The return result +is in +<i>ret</i>, +and the return switches are in the return array: +<tt></tt><dl><dt><dd> +<i>return(-code) +</i><i>return(-errorinfo) +</i><i>return(-errordcode) +</i></dl> +Any of these may be the empty string.<tt> </tt> +Modifying +these four variables can change the return value, whereas +modifying +<i>buffer +</i>has no effect.<tt> </tt> +</dl> + +<dl> +<dt><b>lost +</b><dd> +Variables: +<i>chan id reason +</i><br> +This hook is invoked when the connection to +<i>id +</i>is lost.<tt> </tt> +Return value (or thrown error) is ignored.<tt> </tt> +<i>reason +</i>is an explanatory string indicating why the connection was lost.<tt> </tt> +Example: +<p> +<tt></tt></dl> +<dl><dt><dd> +comm hook lost { +<br> + global myvar +<br> + if {$myvar(id) == $id} { +<br> + myfunc +<br> + return +<br> + } +<br> +} +<br> +</dl> +<h2>UNSUPPORTED</h2> +<p> +These interfaces may change or go away in subsequence releases.<tt> </tt> +<dl> +<dt><b></b><b>comm</b><b> remoteid</b> +<dd> +Returns the +<i>id +</i>of the sender of the last remote command executed on this channel.<tt> </tt> +If used by a proc being invoked remotely, it +must be called before any events are processed.<tt> </tt> +Otherwise, another command may get invoked and change the value.<tt> </tt> +<dt><b>comm_send +</b><dd> +Invoking this procedure will substitute the Tk +<b>send +</b>and +<b>winfo interps +</b>commands with these equivalents that use +<b>comm</b>.<tt> </tt> +<p> +<tt></tt></dl> +<dl><dt><dd> +proc send {args} { +<br> + eval comm send $args +<br> +} +<br> +rename winfo tk_winfo +<br> +proc winfo {cmd args} { +<br> + if ![string match in* $cmd] {return [eval [list tk_winfo $cmd] $args]} +<br> + return [comm interps] +<br> +} +<br> +</dl> +<h2>SECURITY</h2> +<p> +Something here soon.<tt> </tt> +<h2>BLOCKING SEMANTICS</h2> +<p> +There is one outstanding difference between +<b>comm +</b>and +<b>send</b>.<tt> </tt> +When blocking in a synchronous remote command, +<b>send +</b>uses an internal C hook (Tk_RestrictEvents) +to the event loop to look ahead for +send-related events and only process those without processing any other events.<tt> </tt> +In contrast, +<b>comm +</b>uses the +<b>vwait +</b>command as a semaphore to indicate the return message has arrived.<tt> </tt> +The difference is that a synchornous +<b>send +</b>will block the application and prevent all events +(including window related ones) from being processed, +while a synchronous +<b>comm +</b>will block the application but still allow +other events will still get processed.<tt> </tt> +In particular, +<b>after idle +</b>handlers will fire immediately when comm blocks.<tt> </tt> +<p> +What can be done about this?<tt> </tt> +First, note that this behavior will come from any code using +<b>vwait +</b>to block and wait for an event to occur.<tt> </tt> +At the cost of multiple channel support, +<b>comm +</b>could be changed to do blocking I/O on the socket, +givng send-like blocking semantics.<tt> </tt> +However, multiple channel support is a very useful feature of comm +that it is deemed too important to lose.<tt> </tt> +The remaining approaches involve a new loadable module written in C +(which is somewhat against the philosophy of +<b>comm</b>) +One way would be to create a modified version of the +<b>vwait +</b>command that allow the event flags passed to Tcl_DoOneEvent to be specified.<tt> </tt> +For +<b>comm</b>, +just the TCL_FILE_EVENTS would be processed.<tt> </tt> +Another way would be to implement a mechanism like Tk_RestrictEvents, but +apply it to the Tcl event loop (since +<b>comm +</b>doesn't require Tk).<tt> </tt> +One of these approaches will be available in a future +<b>comm +</b>release as an optional component.<tt> </tt> +<h2>COMPATIBILITY</h2> +<p> +<b>Comm +</b>exports itself as a package.<tt> </tt> +The package version number is in the form +<i>major</i>.<i>minor</i>, +where the major version will only change when +a non-compatible change happens to the API or protocol.<tt> </tt> +Minor bug fixes and changes will only affect the minor version.<tt> </tt> +To load +<b>comm +</b>this command is usually used: +<tt></tt><dl><dt><dd> +<b>package require Comm 3</b> +<br> +</dl> +Note that requiring no version (or a specific version) can also be done.<tt> </tt> +<p> +The revision history of +<b>comm +</b>includes these releases: + +<dl> +<dt>3.6<dd> +A bug in the looking up of the remoteid for a executed command +could be triggered when the connection was closed while several +asynchronous sends were queued to be executed.<tt> </tt> + +<dt>3.5<dd> +Internal change to how reply messages from a +<b>send +</b>are handled.<tt> </tt> +Reply messages are now decoded into the +<i>value +</i>to pass to +<b>return</b>; +a new return statement is then cons'd up to with this value.<tt> </tt> +Previously, the return code was passed in from the remote as a +command to evaluate. Since the wire protocol has not changed, +this is still the case. Instead, the reply handling code decodes the +<b>reply +</b>message.<tt> </tt> + +<dt>3.4<dd> +Added more source commentary, as well as documenting config variables +in this man page.<tt> </tt> +Fixed bug were loss of connection would give error about a variable +named +rather than the message about the lost connection.<tt> </tt> +<b>comm ids +</b>is now an alias for +<b>comm interps +</b>(previously, it an alias for +<b>comm chans</b>).<tt> </tt> +Since the method invocation change of 3.0, break and other exceptional +conditions were not being returned correctly from +<b>comm send</b>.<tt> </tt> +This has been fixed by removing the extra level of indirection into +the internal procedure +<b>commSend</b>.<tt> </tt> +Also added propogation of the +<i>errorCode +</i>variable.<tt> </tt> +This means that these commands return exactly as they would with +<b>send</b>: +</dl> +<dl><dt><dd> +<tt></tt><dl><dt><dd> +comm send <i>id</i> break +<br> +catch {comm send <i>id</i> break} +<br> +comm send <i>id</i> expr 1 / 0 +<br> +</dl> +Added a new hook for reply messages.<tt> </tt> +Reworked method invocation to avoid the use of comm:* procedures; +this also cut the invocation time down by 40%.<tt> </tt> +Documented +<b>comm config +</b>(as this manual page still listed the defunct +<b>comm init</b>!) +</dl> + +<dl> +<dt>3.3<dd> +Some minor bugs were corrected and the documentation was cleaned up.<tt> </tt> +Added some examples for hooks. The return semantics of the +<b>eval +</b>hook were changed.<tt> </tt> + +<dt>3.2<dd> +A new wire protocol, version 3, was added. This is backwards compatible +with version 2 but adds an exchange of supported protocol versions to +allow protocol negotiation in the future.<tt> </tt> +Several bugs with the hook implementation were fixed.<tt> </tt> +A new section of the man page on blocking semantics was added.<tt> </tt> + +<dt>3.1<dd> +All the documented hooks were implemented.<tt> </tt> +<b>commLostHook +</b>was removed.<tt> </tt> +A bug in +<b>comm new +</b>was fixed.<tt> </tt> + +<dt>3.0<dd> +This is a new version of +<b>comm +</b>with several major changes.<tt> </tt> +There is a new way of creating the methods available under the +<b>comm +</b>command.<tt> </tt> +The +<b>comm init +</b>method has been retired and is replaced by +<b>comm configure +</b>which allows access to many of the well-defined internal variables.<tt> </tt> +This also generalizes the options available to +<b>comm new</b>.<tt> </tt> +Finally, there is now a protocol version exchanged when a connection +is established. This will allow for future on-wire protocol changes.<tt> </tt> +Currently, the protocol version is set to 2.<tt> </tt> + +<dt>2.3<dd> +<b>comm ids +</b>was renamed to +<b>comm channels . +</b>General support for +<b>comm hook +</b>was fully implemented, but +only the +<b>lost +</b>hook exists, and it was changed to follow the general hook API.<tt> </tt> +<b>commLostHook +</b>was unsupported (replaced by +<b>comm hook lost ) +</b>and +<b>commLost +</b>was removed.<tt> </tt> + +<dt>2.2<dd> +The +<b>died +</b>hook was renamed +<b>lost</b>, +to be accessed by +<b>commLostHook +</b>and an early implementation of +<b>comm lost hook</b>.<tt> </tt> +As such, +<b>commDied +</b>is now +<b>commLost</b>.<tt> </tt> + +<dt>2.1<dd> +Unsupported method +<b>comm remoteid +</b>was added.<tt> </tt> + +<dt>2.0<dd> +<b>comm +</b>has been rewritten from scratch (but is fully compatible with Comm 1.0, +without the requirement to use obTcl).<tt> </tt> +</dl> +<h2>SEE ALSO</h2> +<i>send</i>(n) +<h2>AUTHOR</h2> +John LoVerso, John@LoVerso.Southborough.MA.US +<p> +<i>http://www.opengroup.org/~loverso/tcl-tk/#comm +</i><h2>COPYRIGHT</h2> +Copyright (C) 1995-1998 The Open Group. All Rights Reserved.<tt> </tt> +Please see the file +<i>comm.LICENSE +</i>that accompanied this source, +or +<i>http://www.opengroup.org/www/dist_client/caubweb/COPYRIGHT.free.html</i>.<tt> </tt> +<p> +This license for +<b>comm</b>, +new as of version 3.2, +allows it to be used for free, +without any licensing fee or royalty.<tt> </tt> +<h2>BUGS</h2> +<ul> +<li> +If there is a failure initializing a channel created with +<b>comm new</b>, +then the channel should be destroyed.<tt> </tt> +Currently, it is left in an inconsistent state.<tt> </tt> +<li> +There should be a way to force a channel to quiesce when changing the +configuration.<tt> </tt> +</ul> +<p> +The following items can be implemented with the existing hooks +and are listed here as a reminder to provide a sample hook in a future version.<tt> </tt> +<ul> +<li> +Allow easier use of a slave interp for actual command execution +(especially when operating in "not local" mode).<tt> </tt> +<li> +Add host list (xhost-like) or "magic cookie" (xauth-like) +authentication to initial handshake.<tt> </tt> +</ul> +<p> +The following are outstanding todo items.<tt> </tt> +<ul> +<li> +Add an interp discovery and name->port mapping.<tt> </tt> +This is likely to be in a separate, optional nameserver.<tt> </tt> +(See also the related work, below.) +<li> +Fix the +<i>{id host} +</i>form so as not to be dependent upon canonical hostnames.<tt> </tt> +This requires fixes to Tcl to resolve hostnames!<tt> </tt> +</ul> +<p> +<p> +<p> +This man page is bigger than the source file.<tt> </tt> +<h2>ON USING OLD VERSIONS OF TCL</h2> +<p> +Tcl7.5 under Windows contains a bug that causes the interpreter to +hang when EOF is reached on non-blocking sockets. This can be +triggered with a command such as this: +<tt></tt><dl><dt><dd> +<b>comm send $other exit +</b></dl> +Always make sure the channel is quiescent before closing/exiting or +use at least Tcl7.6 under Windows.<tt> </tt> +<p> +Tcl7.6 on the Mac contains several bugs. It is recommended you use +at least Tcl7.6p2.<tt> </tt> +<p> +Tcl8.0 on UNIX contains a socket bug that can crash Tcl. It is recommended +you use Tcl8.0p1 (or Tcl7.6p2).<tt> </tt> +<h2>RELATED WORK</h2> +<p> +Tcl-DP provides an RPC-based remote execution interface, but is a compiled +Tcl extension. See +<i>http://www.cs.cornell.edu/Info/Projects/zeno/Projects/Tcl-DP.html</i>.<tt> </tt> +<p> +Michael Doyle <miked@eolas.com> has code that implements the Tcl-DP RPC +interface using standard Tcl sockets, much like +<b>comm</b>.<tt> </tt> +<p> +Andreas Kupries <a.kupries@westend.com> uses +<b>comm +</b>and has built a simple nameserver as part of his Pool library.<tt> </tt> +See +<i>http://www.westend.com/~kupries/doc/pool/index.htm</i>.<tt> </tt> +<!-- eof --> +<p><hr> +Markup created by <em>unroff</em> 1.0, <tt> </tt> <tt> </tt>May 30, 1998. +</body> +</html> diff --git a/tcllib/modules/comm/comm.pcx b/tcllib/modules/comm/comm.pcx new file mode 100644 index 0000000..86a1dd0 --- /dev/null +++ b/tcllib/modules/comm/comm.pcx @@ -0,0 +1,99 @@ +# -*- tcl -*- comm.pcx +# Syntax of the commands provided by package comm. + +# For use by TclDevKit's static syntax checker. +# See http://www.activestate.com/solutions/tcl/ +# See http://aspn.activestate.com/ASPN/docs/Tcl_Dev_Kit/4.0/Checker.html#pcx_api +# for the documentation describing the format of the code contained in this file +# + +package require pcx +pcx::register comm +pcx::tcldep 4.5.7 needs tcl 8.3 + +namespace eval ::comm {} + +#pcx::message FOO {... text ...} type +#pcx::scan <VERSION> <NAME> <RULE> + +# TODO: new, word = name of comm channel object, register in scan mode, using this syntax. +# TODO: hook, word = bind script, look at tk bind syntax. + +pcx::check 4.5.7 std ::comm::comm \ + {checkSimpleArgs 1 -1 { + {checkOption { + {abort {checkSimpleArgs 0 0 {}}} + {channels {checkSimpleArgs 0 0 {}}} + {configure {checkSimpleArgs 0 -1 { + comm::checkCommSwitches + }}} + {connect {checkSimpleArgs 1 1 { + comm::checkCommId + }}} + {debug {checkSimpleArgs 1 1 { + checkBoolean + }}} + {destroy {checkSimpleArgs 0 0 {}}} + {hook {checkSimpleArgs 1 2 { + comm::checkCommHook + checkWord + }}} + {ids {checkSimpleArgs 0 0 {}}} + {interps {checkSimpleArgs 0 0 {}}} + {new {checkSimpleArgs 1 -1 { + checkWord + comm::checkCommSwitches + }}} + {remoteid {checkSimpleArgs 0 0 {}}} + {return_async {checkSimpleArgs 0 0 {}}} + {self {checkSimpleArgs 0 0 {}}} + {send {checkSimpleArgs 1 -1 { + {checkSwitches 1 { + -async + {-command {checkWord}} + } {checkSimpleArgs 2 -1 { + comm::checkCommId + checkEvalArgs + }}} + }}} + {shutdown {checkSimpleArgs 1 1 {comm::checkCommId}}} + } {}} + }} +pcx::check 4.5.7 std ::comm::comm_send \ + {checkSimpleArgs 0 0 {}} + +# Initialization via pcx::init. +# Use a ::comm::init procedure for non-standard initialization. + +proc comm::checkCommId {t i} { + return [checkListValues 1 2 { + checkWholeNum + checkWord + } $t $i] +} + +proc comm::checkCommHook {t i} { + return [checkKeyword 1 { + connecting connected incoming eval callback reply lost + } $t $i] +} + +proc comm::checkCommSwitches {t i} { + # socket, serial, encoding are read-only, hence the check that + # they are used only without an argument. + return [checkSwitches 1 { + {-chan {checkSimpleArgs 0 0 {}}} + {-encoding checkWord} + {-events {checkListValues 0 -1 {comm::checkCommHook}}} + {-interp checkWord} + {-listen checkBoolean} + {-local checkBoolean} + {-port checkWholeNum} + {-serial {checkSimpleArgs 0 0 {}}} + {-silent checkBoolean} + {-socket {checkSimpleArgs 0 0 {}}} + {-socketcmd checkWord} + } {} $t $i] +} + +pcx::complete diff --git a/tcllib/modules/comm/comm.slaveboot b/tcllib/modules/comm/comm.slaveboot new file mode 100644 index 0000000..e70bcf9 --- /dev/null +++ b/tcllib/modules/comm/comm.slaveboot @@ -0,0 +1,42 @@ +# -*- tcl -*- +# Script to boot a child running an open comm server + +set spawncode [makeFile { + catch {wm withdraw .} + ##puts [set fh [open ~/foo w]] $argv ; close $fh + + source [lindex $argv 0] ; # load 'snit' + source [lindex $argv 1].tcl ; # load 'comm' + # and wait for commands. But first send our + # own server socket to the initiator + ::comm::comm send [lindex $argv 2] [list slaveat [::comm::comm self]] + vwait forever +} spawn] + +proc slaveat {id} { + #puts "Slave @ $id" + proc slave {} [list return $id] + set ::go . +} + +#puts "self @ [::comm::comm self]" + +exec \ + [info nameofexecutable] $spawncode \ + [tcllibPath snit/snit.tcl] \ + [file rootname [info script]] \ + [::comm::comm self] & + +#puts "Waiting for spawned comm system to boot" +# Wait for the slave to initialize itself. +vwait ::go + +#puts "Running tests" +#::comm::comm debug 1 + +proc slavestop {} { + ::comm::comm send -async [slave] {{exit}} + ::comm::comm abort + removeFile spawn + return +} diff --git a/tcllib/modules/comm/comm.tcl b/tcllib/modules/comm/comm.tcl new file mode 100644 index 0000000..55eda34 --- /dev/null +++ b/tcllib/modules/comm/comm.tcl @@ -0,0 +1,1818 @@ +# comm.tcl -- +# +# socket-based 'send'ing of commands between interpreters. +# +# %%_OSF_FREE_COPYRIGHT_%% +# Copyright (C) 1995-1998 The Open Group. All Rights Reserved. +# (Please see the file "comm.LICENSE" that accompanied this source, +# or http://www.opengroup.org/www/dist_client/caubweb/COPYRIGHT.free.html) +# Copyright (c) 2003-2007 ActiveState Corporation +# +# This is the 'comm' package written by Jon Robert LoVerso, placed +# into its own namespace during integration into tcllib. +# +# Note that the actual code was changed in several places (Reordered, +# eval speedup) +# +# comm works just like Tk's send, except that it uses sockets. +# These commands work just like "send" and "winfo interps": +# +# comm send ?-async? <id> <cmd> ?<arg> ...? +# comm interps +# +# See the manual page comm.n for further details on this package. +# +# RCS: @(#) $Id: comm.tcl,v 1.34 2010/09/15 19:48:33 andreas_kupries Exp $ + +package require Tcl 8.3 +package require snit ; # comm::future objects. + +namespace eval ::comm { + namespace export comm comm_send + + variable comm + array set comm {} + + if {![info exists comm(chans)]} { + array set comm { + debug 0 chans {} localhost 127.0.0.1 + connecting,hook 1 + connected,hook 1 + incoming,hook 1 + eval,hook 1 + callback,hook 1 + reply,hook 1 + lost,hook 1 + offerVers {3 2} + acceptVers {3 2} + defVers 2 + defaultEncoding "utf-8" + defaultSilent 0 + } + set comm(lastport) [expr {[pid] % 32768 + 9999}] + # fast check for acceptable versions + foreach comm(_x) $comm(acceptVers) { + set comm($comm(_x),vers) 1 + } + catch {unset comm(_x)} + } + + # Class variables: + # lastport saves last default listening port allocated + # debug enable debug output + # chans list of allocated channels + # future,fid,$fid List of futures a specific peer is waiting for. + # + # Channel instance variables: + # comm() + # $ch,port listening port (our id) + # $ch,socket listening socket + # $ch,socketcmd command to use to create sockets. + # $ch,silent boolean to indicate whether to throw error on + # protocol negotiation failure + # $ch,local boolean to indicate if port is local + # $ch,interp interpreter to run received scripts in. + # If not empty we own it! = We destroy it + # with the channel + # $ch,events List of hoks to run in the 'interp', if defined + # $ch,serial next serial number for commands + # + # $ch,hook,$hook script for hook $hook + # + # $ch,peers,$id open connections to peers; ch,id=>fid + # $ch,fids,$fid reverse mapping for peers; ch,fid=>id + # $ch,vers,$id negotiated protocol version for id + # $ch,pending,$id list of outstanding send serial numbers for id + # + # $ch,buf,$fid buffer to collect incoming data + # $ch,result,$serial result value set here to wake up sender + # $ch,return,$serial return codes to go along with result + + if {0} { + # Propagate result, code, and errorCode. Can't just eval + # otherwise TCL_BREAK gets turned into TCL_ERROR. + global errorInfo errorCode + set code [catch [concat commSend $args] res] + return -code $code -errorinfo $errorInfo -errorcode $errorCode $res + } +} + +# ::comm::comm_send -- +# +# Convenience command. Replaces Tk 'send' and 'winfo' with +# versions using the 'comm' variants. Multiple calls are +# allowed, only the first one will have an effect. +# +# Arguments: +# None. +# +# Results: +# None. + +proc ::comm::comm_send {} { + proc send {args} { + # Use pure lists to speed this up. + uplevel 1 [linsert $args 0 ::comm::comm send] + } + rename winfo tk_winfo + proc winfo {cmd args} { + if {![string match in* $cmd]} { + # Use pure lists to speed this up ... + return [uplevel 1 [linsert $args 0 tk_winfo $cmd]] + } + return [::comm::comm interps] + } + proc ::comm::comm_send {} {} +} + +# ::comm::comm -- +# +# See documentation for public methods of "comm". +# This procedure is followed by the definition of +# the public methods themselves. +# +# Arguments: +# cmd Invoked method +# args Arguments to method. +# +# Results: +# As of the invoked method. + +proc ::comm::comm {cmd args} { + set method [info commands ::comm::comm_cmd_$cmd*] + + if {[llength $method] == 1} { + set chan ::comm::comm; # passed to methods + return [uplevel 1 [linsert $args 0 $method $chan]] + } else { + foreach c [info commands ::comm::comm_cmd_*] { + # remove ::comm::comm_cmd_ + lappend cmds [string range $c 17 end] + } + return -code error "unknown subcommand \"$cmd\":\ + must be one of [join [lsort $cmds] {, }]" + } +} + +proc ::comm::comm_cmd_connect {chan args} { + uplevel 1 [linsert $args 0 [namespace current]::commConnect $chan] +} +proc ::comm::comm_cmd_self {chan args} { + variable comm + return $comm($chan,port) +} +proc ::comm::comm_cmd_channels {chan args} { + variable comm + return $comm(chans) +} +proc ::comm::comm_cmd_configure {chan args} { + uplevel 1 [linsert $args 0 [namespace current]::commConfigure $chan 0] +} +proc ::comm::comm_cmd_ids {chan args} { + variable comm + set res $comm($chan,port) + foreach {i id} [array get comm $chan,fids,*] {lappend res $id} + return $res +} +interp alias {} ::comm::comm_cmd_interps {} ::comm::comm_cmd_ids +proc ::comm::comm_cmd_remoteid {chan args} { + variable comm + if {[info exists comm($chan,remoteid)]} { + set comm($chan,remoteid) + } else { + return -code error "No remote commands processed yet" + } +} +proc ::comm::comm_cmd_debug {chan bool} { + variable comm + return [set comm(debug) [string is true -strict $bool]] +} + +# ### ### ### ######### ######### ######### +## API: Setup async result generation for a remotely invoked command. + +# (future,fid,<fid>) -> list (future) +# (current,async) -> bool (default 0) +# (current,state) -> list (chan fid cmd ser) + +proc ::comm::comm_cmd_return_async {chan} { + variable comm + + if {![info exists comm(current,async)]} { + return -code error "No remote commands processed yet" + } + if {$comm(current,async)} { + # Return the same future which were generated by the first + # call. + return $comm(current,state) + } + + foreach {cmdchan cmdfid cmd ser} $comm(current,state) break + + # Assert that the channel performing the request and the channel + # the current command came in are identical. Panic if not. + + if {![string equal $chan $cmdchan]} { + return -code error "Internal error: Trying to activate\ + async return for a command on a different channel" + } + + # Establish the future for the command and return a handle for + # it. Remember the outstanding futures for a peer, so that we can + # cancel them if the peer is lost before the promise implicit in + # the future is redeemed. + + set future [::comm::future %AUTO% $chan $cmdfid $cmd $ser] + + lappend comm(future,fid,$cmdfid) $future + set comm(current,state) $future + + # Mark the current command as using async result return. We do + # this last to ensure that all errors in this method are reported + # through the regular channels. + + set comm(current,async) 1 + + return $future +} + +# hook -- +# +# Internal command. Implements 'comm hook'. +# +# Arguments: +# hook hook to modify +# script Script to add/remove to/from the hook +# +# Results: +# None. +# +proc ::comm::comm_cmd_hook {chan hook {script +}} { + variable comm + if {![info exists comm($hook,hook)]} { + return -code error "Unknown hook invoked" + } + if {!$comm($hook,hook)} { + return -code error "Unimplemented hook invoked" + } + if {[string equal + $script]} { + if {[catch {set comm($chan,hook,$hook)} ret]} { + return + } + return $ret + } + if {[string match +* $script]} { + append comm($chan,hook,$hook) \n [string range $script 1 end] + } else { + set comm($chan,hook,$hook) $script + } + return +} + +# abort -- +# +# Close down all peer connections. +# Implements the 'comm abort' method. +# +# Arguments: +# None. +# +# Results: +# None. + +proc ::comm::comm_cmd_abort {chan} { + variable comm + + foreach pid [array names comm $chan,peers,*] { + commLostConn $chan $comm($pid) "Connection aborted by request" + } +} + +# destroy -- +# +# Destroy the channel invoking it. +# Implements the 'comm destroy' method. +# +# Arguments: +# None. +# +# Results: +# None. +# +proc ::comm::comm_cmd_destroy {chan} { + variable comm + catch {close $comm($chan,socket)} + comm_cmd_abort $chan + if {$comm($chan,interp) != {}} { + interp delete $comm($chan,interp) + } + catch {unset comm($chan,port)} + catch {unset comm($chan,local)} + catch {unset comm($chan,silent)} + catch {unset comm($chan,interp)} + catch {unset comm($chan,events)} + catch {unset comm($chan,socket)} + catch {unset comm($chan,socketcmd)} + catch {unset comm($chan,remoteid)} + unset comm($chan,serial) + unset comm($chan,chan) + unset comm($chan,encoding) + unset comm($chan,listen) + # array unset would have been nicer, but is not available in + # 8.2/8.3 + foreach pattern {hook,* interp,* vers,*} { + foreach k [array names comm $chan,$pattern] {unset comm($k)} + } + set pos [lsearch -exact $comm(chans) $chan] + set comm(chans) [lreplace $comm(chans) $pos $pos] + if { + ![string equal ::comm::comm $chan] && + ![string equal [info proc $chan] ""] + } { + rename $chan {} + } + return +} + +# shutdown -- +# +# Close down a peer connection. +# Implements the 'comm shutdown' method. +# +# Arguments: +# id Reference to the remote interp +# +# Results: +# None. +# +proc ::comm::comm_cmd_shutdown {chan id} { + variable comm + + if {[info exists comm($chan,peers,$id)]} { + commLostConn $chan $comm($chan,peers,$id) \ + "Connection shutdown by request" + } +} + +# new -- +# +# Create a new comm channel/instance. +# Implements the 'comm new' method. +# +# Arguments: +# ch Name of the new channel +# args Configuration, in the form of -option value pairs. +# +# Results: +# None. +# +proc ::comm::comm_cmd_new {chan ch args} { + variable comm + + if {[lsearch -exact $comm(chans) $ch] >= 0} { + return -code error "Already existing channel: $ch" + } + if {([llength $args] % 2) != 0} { + return -code error "Must have an even number of config arguments" + } + # ensure that the new channel name is fully qualified + set ch ::[string trimleft $ch :] + if {[string equal ::comm::comm $ch]} { + # allow comm to be recreated after destroy + } elseif {[string equal $ch [info commands $ch]]} { + return -code error "Already existing command: $ch" + } else { + # Create the new channel with fully qualified proc name + proc $ch {cmd args} { + set method [info commands ::comm::comm_cmd_$cmd*] + + if {[llength $method] == 1} { + # this should work right even if aliased + # it is passed to methods to identify itself + set chan [namespace origin [lindex [info level 0] 0]] + return [uplevel 1 [linsert $args 0 $method $chan]] + } else { + foreach c [info commands ::comm::comm_cmd_*] { + # remove ::comm::comm_cmd_ + lappend cmds [string range $c 17 end] + } + return -code error "unknown subcommand \"$cmd\":\ + must be one of [join [lsort $cmds] {, }]" + } + } + } + lappend comm(chans) $ch + set chan $ch + set comm($chan,serial) 0 + set comm($chan,chan) $chan + set comm($chan,port) 0 + set comm($chan,listen) 0 + set comm($chan,socket) "" + set comm($chan,local) 1 + set comm($chan,silent) $comm(defaultSilent) + set comm($chan,encoding) $comm(defaultEncoding) + set comm($chan,interp) {} + set comm($chan,events) {} + set comm($chan,socketcmd) ::socket + + if {[llength $args] > 0} { + if {[catch [linsert $args 0 commConfigure $chan 1] err]} { + comm_cmd_destroy $chan + return -code error $err + } + } + return $chan +} + +# send -- +# +# Send command to a specified channel. +# Implements the 'comm send' method. +# +# Arguments: +# args see inside +# +# Results: +# varies. +# +proc ::comm::comm_cmd_send {chan args} { + variable comm + + set cmd send + + # args = ?-async | -command command? id cmd ?arg arg ...? + set i 0 + set opt [lindex $args $i] + if {[string equal -async $opt]} { + set cmd async + incr i + } elseif {[string equal -command $opt]} { + set cmd command + set callback [lindex $args [incr i]] + incr i + } + # args = id cmd ?arg arg ...? + + set id [lindex $args $i] + incr i + set args [lrange $args $i end] + + if {![info complete $args]} { + return -code error "Incomplete command" + } + if {![llength $args]} { + return -code error \ + "wrong # args: should be \"send ?-async? id arg ?arg ...?\"" + } + if {[catch {commConnect $chan $id} fid]} { + return -code error "Connect to remote failed: $fid" + } + + set ser [incr comm($chan,serial)] + # This is unneeded - wraps from 2147483647 to -2147483648 + ### if {$comm($chan,serial) == 0x7fffffff} {set comm($chan,serial) 0} + + commDebug {puts stderr "<$chan> send <[list [list $cmd $ser $args]]>"} + + # The double list assures that the command is a single list when read. + puts $fid [list [list $cmd $ser $args]] + flush $fid + + commDebug {puts stderr "<$chan> sent"} + + # wait for reply if so requested + + if {[string equal command $cmd]} { + # In this case, don't wait on the command result. Set the callback + # in the return and that will be invoked by the result. + lappend comm($chan,pending,$id) [list $ser callback] + set comm($chan,return,$ser) $callback + return $ser + } elseif {[string equal send $cmd]} { + upvar 0 comm($chan,pending,$id) pending ;# shorter variable name + + lappend pending $ser + set comm($chan,return,$ser) "" ;# we're waiting + + commDebug {puts stderr "<$chan> --<<waiting $ser>>--"} + vwait ::comm::comm($chan,result,$ser) + + # if connection was lost, pending is gone + if {[info exists pending]} { + set pos [lsearch -exact $pending $ser] + set pending [lreplace $pending $pos $pos] + } + + commDebug { + puts stderr "<$chan> result\ + <$comm($chan,return,$ser);$comm($chan,result,$ser)>" + } + + array set return $comm($chan,return,$ser) + unset comm($chan,return,$ser) + set thisres $comm($chan,result,$ser) + unset comm($chan,result,$ser) + switch -- $return(-code) { + "" - 0 {return $thisres} + 1 { + return -code $return(-code) \ + -errorinfo $return(-errorinfo) \ + -errorcode $return(-errorcode) \ + $thisres + } + default {return -code $return(-code) $thisres} + } + } +} + +############################################################################### + +# ::comm::commDebug -- +# +# Internal command. Conditionally executes debugging +# statements. Currently this are only puts commands logging the +# various interactions. These could be replaced with calls into +# the 'log' module. +# +# Arguments: +# arg Tcl script to execute. +# +# Results: +# None. + +proc ::comm::commDebug {cmd} { + variable comm + if {$comm(debug)} { + uplevel 1 $cmd + } +} + +# ::comm::commConfVars -- +# +# Internal command. Used to declare configuration options. +# +# Arguments: +# v Name of configuration option. +# t Default value. +# +# Results: +# None. + +proc ::comm::commConfVars {v t} { + variable comm + set comm($v,var) $t + set comm(vars) {} + foreach c [array names comm *,var] { + lappend comm(vars) [lindex [split $c ,] 0] + } + return +} +::comm::commConfVars port p +::comm::commConfVars local b +::comm::commConfVars listen b +::comm::commConfVars socket ro +::comm::commConfVars socketcmd socketcmd +::comm::commConfVars chan ro +::comm::commConfVars serial ro +::comm::commConfVars encoding enc +::comm::commConfVars silent b +::comm::commConfVars interp interp +::comm::commConfVars events ev + +# ::comm::commConfigure -- +# +# Internal command. Implements 'comm configure'. +# +# Arguments: +# force Boolean flag. If set the socket is reinitialized. +# args New configuration, as -option value pairs. +# +# Results: +# None. + +proc ::comm::commConfigure {chan {force 0} args} { + variable comm + + # query + if {[llength $args] == 0} { + foreach v $comm(vars) {lappend res -$v $comm($chan,$v)} + return $res + } elseif {[llength $args] == 1} { + set arg [lindex $args 0] + set var [string range $arg 1 end] + if {![string match -* $arg] || ![info exists comm($var,var)]} { + return -code error "Unknown configuration option: $arg" + } + return $comm($chan,$var) + } + + # set + set opt 0 + foreach arg $args { + incr opt + if {[info exists skip]} {unset skip; continue} + set var [string range $arg 1 end] + if {![string match -* $arg] || ![info exists comm($var,var)]} { + return -code error "Unknown configuration option: $arg" + } + set optval [lindex $args $opt] + switch $comm($var,var) { + ev { + if {![string equal $optval ""]} { + set err 0 + if {[catch { + foreach ev $optval { + if {[lsearch -exact {connecting connected incoming eval callback reply lost} $ev] < 0} { + set err 1 + break + } + } + }]} { + set err 1 + } + if {$err} { + return -code error \ + "Non-event to configuration option: -$var" + } + } + # FRINK: nocheck + set $var $optval + set skip 1 + } + interp { + if { + ![string equal $optval ""] && + ![interp exists $optval] + } { + return -code error \ + "Non-interpreter to configuration option: -$var" + } + # FRINK: nocheck + set $var $optval + set skip 1 + } + b { + # FRINK: nocheck + set $var [string is true -strict $optval] + set skip 1 + } + v { + # FRINK: nocheck + set $var $optval + set skip 1 + } + p { + if { + ![string equal $optval ""] && + ![string is integer $optval] + } { + return -code error \ + "Non-port to configuration option: -$var" + } + # FRINK: nocheck + set $var $optval + set skip 1 + } + i { + if {![string is integer $optval]} { + return -code error \ + "Non-integer to configuration option: -$var" + } + # FRINK: nocheck + set $var $optval + set skip 1 + } + enc { + # to configure encodings, we will need to extend the + # protocol to allow for handshaked encoding changes + return -code error "encoding not configurable" + if {[lsearch -exact [encoding names] $optval] == -1} { + return -code error \ + "Unknown encoding to configuration option: -$var" + } + set $var $optval + set skip 1 + } + ro { + return -code error "Readonly configuration option: -$var" + } + socketcmd { + if {$optval eq {}} { + return -code error \ + "Non-command to configuration option: -$var" + } + + set $var $optval + set skip 1 + } + } + } + if {[info exists skip]} { + return -code error "Missing value for option: $arg" + } + + foreach var {port listen local socketcmd} { + # FRINK: nocheck + if {[info exists $var] && [set $var] != $comm($chan,$var)} { + incr force + # FRINK: nocheck + set comm($chan,$var) [set $var] + } + } + + foreach var {silent interp events} { + # FRINK: nocheck + if {[info exists $var] && ([set $var] != $comm($chan,$var))} { + # FRINK: nocheck + set comm($chan,$var) [set ip [set $var]] + if {[string equal $var "interp"] && ($ip != "")} { + # Interrogate the interp about its capabilities. + # + # Like: set, array set, uplevel present ? + # Or: The above, hidden ? + # + # This is needed to decide how to execute hook scripts + # and regular scripts in this interpreter. + set comm($chan,interp,set) [Capability $ip set] + set comm($chan,interp,aset) [Capability $ip array] + set comm($chan,interp,upl) [Capability $ip uplevel] + } + } + } + + if {[info exists encoding] && + ![string equal $encoding $comm($chan,encoding)]} { + # This should not be entered yet + set comm($chan,encoding) $encoding + fconfigure $comm($chan,socket) -encoding $encoding + foreach {i sock} [array get comm $chan,peers,*] { + fconfigure $sock -encoding $encoding + } + } + + # do not re-init socket + if {!$force} {return ""} + + # User is recycling object, possibly to change from local to !local + if {[info exists comm($chan,socket)]} { + comm_cmd_abort $chan + catch {close $comm($chan,socket)} + unset comm($chan,socket) + } + + set comm($chan,socket) "" + if {!$comm($chan,listen)} { + set comm($chan,port) 0 + return "" + } + + if {[info exists port] && [string equal "" $comm($chan,port)]} { + set nport [incr comm(lastport)] + } else { + set userport 1 + set nport $comm($chan,port) + } + while {1} { + set cmd [list $comm($chan,socketcmd) -server [list ::comm::commIncoming $chan]] + if {$comm($chan,local)} { + lappend cmd -myaddr $comm(localhost) + } + lappend cmd $nport + if {![catch $cmd ret]} { + break + } + if {[info exists userport] || ![string match "*already in use" $ret]} { + # don't eradicate the class + if { + ![string equal ::comm::comm $chan] && + ![string equal [info proc $chan] ""] + } { + rename $chan {} + } + return -code error $ret + } + set nport [incr comm(lastport)] + } + set comm($chan,socket) $ret + fconfigure $ret -translation lf -encoding $comm($chan,encoding) + + # If port was 0, system allocated it for us + set comm($chan,port) [lindex [fconfigure $ret -sockname] 2] + return "" +} + +# ::comm::Capability -- +# +# Internal command. Interogate an interp for +# the commands needed to execute regular and +# hook scripts. + +proc ::comm::Capability {interp cmd} { + if {[lsearch -exact [interp hidden $interp] $cmd] >= 0} { + # The command is present, although hidden. + return hidden + } + + # The command is not a hidden command. Use info to determine if it + # is present as regular command. Note that the 'info' command + # itself might be hidden. + + if {[catch { + set has [llength [interp eval $interp [list info commands $cmd]]] + }] && [catch { + set has [llength [interp invokehidden $interp info commands $cmd]] + }]} { + # Unable to interogate the interpreter in any way. Assume that + # the command is not present. + set has 0 + } + return [expr {$has ? "ok" : "no"}] +} + +# ::comm::commConnect -- +# +# Internal command. Called to connect to a remote interp +# +# Arguments: +# id Specification of the location of the remote interp. +# A list containing either one or two elements. +# One element = port, host is localhost. +# Two elements = port and host, in this order. +# +# Results: +# fid channel handle of the socket the connection goes through. + +proc ::comm::commConnect {chan id} { + variable comm + + commDebug {puts stderr "<$chan> commConnect $id"} + + # process connecting hook now + CommRunHook $chan connecting + + if {[info exists comm($chan,peers,$id)]} { + return $comm($chan,peers,$id) + } + if {[lindex $id 0] == 0} { + return -code error "Remote comm is anonymous; cannot connect" + } + + if {[llength $id] > 1} { + set host [lindex $id 1] + } else { + set host $comm(localhost) + } + set port [lindex $id 0] + set fid [$comm($chan,socketcmd) $host $port] + + # process connected hook now + if {[catch { + CommRunHook $chan connected + } err]} { + global errorInfo + set ei $errorInfo + close $fid + error $err $ei + } + + # commit new connection + commNewConn $chan $id $fid + + # send offered protocols versions and id to identify ourselves to remote + puts $fid [list $comm(offerVers) $comm($chan,port)] + set comm($chan,vers,$id) $comm(defVers) ;# default proto vers + flush $fid + return $fid +} + +# ::comm::commIncoming -- +# +# Internal command. Called for an incoming new connection. +# Handles connection setup and initialization. +# +# Arguments: +# chan logical channel handling the connection. +# fid channel handle of the socket running the connection. +# addr ip address of the socket channel 'fid' +# remport remote port for the socket channel 'fid' +# +# Results: +# None. + +proc ::comm::commIncoming {chan fid addr remport} { + variable comm + + commDebug {puts stderr "<$chan> commIncoming $fid $addr $remport"} + + # process incoming hook now + if {[catch { + CommRunHook $chan incoming + } err]} { + global errorInfo + set ei $errorInfo + close $fid + error $err $ei + } + + # Wait for offered version, without blocking the entire system. + # Bug 3066872. For a Tcl 8.6 implementation consider use of + # coroutines to hide the CSP and properly handle everything + # event based. + + fconfigure $fid -blocking 0 + fileevent $fid readable [list ::comm::commIncomingOffered $chan $fid $addr $remport] + return +} + +proc ::comm::commIncomingOffered {chan fid addr remport} { + variable comm + + # Check if we have a complete line. + if {[gets $fid protoline] < 0} { + #commDebug {puts stderr "commIncomingOffered: no data"} + if {[eof $fid]} { + commDebug {puts stderr "commIncomingOffered: eof on fid=$fid"} + catch { + close $fid + } + } + return + } + + # Protocol version line has been received, disable event handling + # again. + fileevent $fid readable {} + fconfigure $fid -blocking 1 + + # a list of offered proto versions is the first word of first line + # remote id is the second word of first line + # rest of first line is ignored + + set offeredvers [lindex $protoline 0] + set remid [lindex $protoline 1] + + commDebug {puts stderr "<$chan> offered <$protoline>"} + + # use the first supported version in the offered list + foreach v $offeredvers { + if {[info exists comm($v,vers)]} { + set vers $v + break + } + } + if {![info exists vers]} { + close $fid + if {[info exists comm($chan,silent)] && + [string is true -strict $comm($chan,silent)]} then return + error "Unknown offered protocols \"$protoline\" from $addr/$remport" + } + + # If the remote host addr isn't our local host addr, + # then add it to the remote id. + if {[string equal [lindex [fconfigure $fid -sockname] 0] $addr]} { + set id $remid + } else { + set id [list $remid $addr] + } + + # Detect race condition of two comms connecting to each other + # simultaneously. It is OK when we are talking to ourselves. + + if {[info exists comm($chan,peers,$id)] && $id != $comm($chan,port)} { + + puts stderr "commIncoming race condition: $id" + puts stderr "peers=$comm($chan,peers,$id) port=$comm($chan,port)" + + # To avoid the race, we really want to terminate one connection. + # However, both sides are committed to using it. + # commConnect needs to be synchronous and detect the close. + # close $fid + # return $comm($chan,peers,$id) + } + + # Make a protocol response. Avoid any temptation to use {$vers > 2} + # - this forces forwards compatibility issues on protocol versions + # that haven't been invented yet. DON'T DO IT! Instead, test for + # each supported version explicitly. I.e., {$vers >2 && $vers < 5} is OK. + + switch $vers { + 3 { + # Respond with the selected version number + puts $fid [list [list vers $vers]] + flush $fid + } + } + + # commit new connection + commNewConn $chan $id $fid + set comm($chan,vers,$id) $vers +} + +# ::comm::commNewConn -- +# +# Internal command. Common new connection processing +# +# Arguments: +# id Reference to the remote interp +# fid channel handle of the socket running the connection. +# +# Results: +# None. + +proc ::comm::commNewConn {chan id fid} { + variable comm + + commDebug {puts stderr "<$chan> commNewConn $id $fid"} + + # There can be a race condition two where comms connect to each other + # simultaneously. This code favors our outgoing connection. + + if {[info exists comm($chan,peers,$id)]} { + # abort this connection, use the existing one + # close $fid + # return -code return $comm($chan,peers,$id) + } else { + set comm($chan,pending,$id) {} + set comm($chan,peers,$id) $fid + } + set comm($chan,fids,$fid) $id + fconfigure $fid -translation lf -encoding $comm($chan,encoding) -blocking 0 + fileevent $fid readable [list ::comm::commCollect $chan $fid] +} + +# ::comm::commLostConn -- +# +# Internal command. Called to tidy up a lost connection, +# including aborting ongoing sends. Each send should clean +# themselves up in pending/result. +# +# Arguments: +# fid Channel handle of the socket which got lost. +# reason Message describing the reason of the loss. +# +# Results: +# reason + +proc ::comm::commLostConn {chan fid reason} { + variable comm + + commDebug {puts stderr "<$chan> commLostConn $fid $reason"} + + catch {close $fid} + + set id $comm($chan,fids,$fid) + + # Invoke the callbacks of all commands which have such and are + # still waiting for a response from the lost peer. Use an + # appropriate error. + + foreach s $comm($chan,pending,$id) { + if {[string equal "callback" [lindex $s end]]} { + set ser [lindex $s 0] + if {[info exists comm($chan,return,$ser)]} { + set args [list -id $id \ + -serial $ser \ + -chan $chan \ + -code -1 \ + -errorcode NONE \ + -errorinfo "" \ + -result $reason \ + ] + if {[catch {uplevel \#0 $comm($chan,return,$ser) $args} err]} { + commBgerror $err + } + } + } else { + set comm($chan,return,$s) {-code error} + set comm($chan,result,$s) $reason + } + } + unset comm($chan,pending,$id) + unset comm($chan,fids,$fid) + catch {unset comm($chan,peers,$id)} ;# race condition + catch {unset comm($chan,buf,$fid)} + + # Cancel all outstanding futures for requests which were made by + # the lost peer, if there are any. This does not destroy + # them. They will stay around until the long-running operations + # they belong too kill them. + + CancelFutures $fid + + # process lost hook now + catch {CommRunHook $chan lost} + + return $reason +} + +proc ::comm::commBgerror {err} { + # SF Tcllib Patch #526499 + # (See http://sourceforge.net/tracker/?func=detail&aid=526499&group_id=12883&atid=312883 + # for initial request and comments) + # + # Error in async call. Look for [bgerror] to report it. Same + # logic as in Tcl itself. Errors thrown by bgerror itself get + # reported to stderr. + if {[catch {bgerror $err} msg]} { + puts stderr "bgerror failed to handle background error." + puts stderr " Original error: $err" + puts stderr " Error in bgerror: $msg" + flush stderr + } +} + +# CancelFutures: Mark futures associated with a comm channel as +# expired, done when the connection to the peer has been lost. The +# marked futures will not generate result anymore. They will also stay +# around until destroyed by the script they belong to. + +proc ::comm::CancelFutures {fid} { + variable comm + if {![info exists comm(future,fid,$fid)]} return + + commDebug {puts stderr "\tCanceling futures: [join $comm(future,fid,$fid) \ + "\n\t : "]"} + + foreach future $comm(future,fid,$fid) { + $future Cancel + } + + unset comm(future,fid,$fid) + return +} + +############################################################################### + +# ::comm::commCollect -- +# +# Internal command. Called from the fileevent to read from fid +# and append to the buffer. This continues until we get a whole +# command, which we then invoke. +# +# Arguments: +# chan logical channel collecting the data +# fid channel handle of the socket we collect. +# +# Results: +# None. + +proc ::comm::commCollect {chan fid} { + variable comm + upvar #0 comm($chan,buf,$fid) data + + # Tcl8 may return an error on read after a close + if {[catch {read $fid} nbuf] || [eof $fid]} { + commDebug {puts stderr "<$chan> collect/lost eof $fid = [eof $fid]"} + commDebug {puts stderr "<$chan> collect/lost nbuf = <$nbuf>"} + commDebug {puts stderr "<$chan> collect/lost [fconfigure $fid]"} + + fileevent $fid readable {} ;# be safe + commLostConn $chan $fid "target application died or connection lost" + return + } + append data $nbuf + + commDebug {puts stderr "<$chan> collect <$data>"} + + # If data contains at least one complete command, we will + # be able to take off the first element, which is a list holding + # the command. This is true even if data isn't a well-formed + # list overall, with unmatched open braces. This works because + # each command in the protocol ends with a newline, thus allowing + # lindex and lreplace to work. + # + # This isn't true with Tcl8.0, which will return an error until + # the whole buffer is a valid list. This is probably OK, although + # it could potentially cause a deadlock. + + # [AK] Actually no. This breaks down if the sender shoves so much + # data at us so fast that the receiver runs into out of memory + # before the list is fully well-formed and thus able to be + # processed. + + while {![catch { + set cmdrange [Word0 data] + # word0 is essentially the pre-8.0 'lindex <list> 0', getting + # the first word of a list, even if the remainder is not fully + # well-formed. Slight API change, we get the char indices the + # word is between, and a relative index to the remainder of + # the list. + }]} { + # Unpack the indices, then extract the word. + foreach {s e step} $cmdrange break + set cmd [string range $data $s $e] + commDebug {puts stderr "<$chan> cmd <$data>"} + if {[string equal "" $cmd]} break + if {[info complete $cmd]} { + # The word is a command, step to the remainder of the + # list, and delete the word we have processed. + incr e $step + set data [string range $data $e end] + after idle \ + [list ::comm::commExec $chan $fid $comm($chan,fids,$fid) $cmd] + } + } +} + +proc ::comm::Word0 {dv} { + upvar 1 $dv data + + # data + # + # The string we expect to be either a full well-formed list, or a + # well-formed list until the end of the first word in the list, + # with non-wellformed data following after, i.e. an incomplete + # list with a complete first word. + + if {[regexp -indices "^\\s*(\{)" $data -> bracerange]} { + # The word is brace-quoted, starting at index 'lindex + # bracerange 0'. We now have to find the closing brace, + # counting inner braces, ignoring quoted braces. We fail if + # there is no proper closing brace. + + foreach {s e} $bracerange break + incr s ; # index of the first char after the brace. + incr e ; # same. but this is our running index. + + set level 1 + set max [string length $data] + + while {$level} { + # We are looking for the first regular or backslash-quoted + # opening or closing brace in the string. If none is found + # then the word is not complete, and we abort our search. + + # Bug 2972571: To avoid the bogus detection of + # backslash-quoted braces we look for double-backslashes + # as well and skip them. Without this a string like '{puts + # \\}' will incorrectly find a \} at the end, missing the + # end of the word. + + if {![regexp -indices -start $e {((\\\\)|([{}])|(\\[{}]))} $data -> any dbs regular quoted]} { + # ^^ ^ ^ + # |\\ regular \quoted + # any + return -code error "no complete word found/1" + } + + foreach {ds de} $dbs break + foreach {qs qe} $quoted break + foreach {rs re} $regular break + + if {$ds >= 0} { + # Skip double-backslashes ... + set e $de + incr e + continue + } elseif {$qs >= 0} { + # Skip quoted braces ... + set e $qe + incr e + continue + } elseif {$rs >= 0} { + # Step one nesting level in or out. + if {[string index $data $rs] eq "\{"} { + incr level + } else { + incr level -1 + } + set e $re + incr e + #puts @$e + continue + } else { + return -code error "internal error" + } + } + + incr e -2 ; # index of character just before the brace. + return [list $s $e 2] + + } elseif {[regexp -indices {^\s*(\S+)\s} $data -> wordrange]} { + # The word is a simple literal which ends at the next + # whitespace character. Note that there has to be a whitespace + # for us to recognize a word, for while there is no whitespace + # behind it in the buffer the word itself may be incomplete. + + return [linsert $wordrange end 1] + } + + return -code error "no complete word found/2" +} + +# ::comm::commExec -- +# +# Internal command. Receives and executes a remote command, +# returning the result and/or error. Unknown protocol commands +# are silently discarded +# +# Arguments: +# chan logical channel collecting the data +# fid channel handle of the socket we collect. +# remoteid id of the other side. +# buf buffer containing the command to execute. +# +# Results: +# None. + +proc ::comm::commExec {chan fid remoteid buf} { + variable comm + + # buffer should contain: + # send # {cmd} execute cmd and send reply with serial # + # async # {cmd} execute cmd but send no reply + # reply # {cmd} execute cmd as reply to serial # + + # these variables are documented in the hook interface + set cmd [lindex $buf 0] + set ser [lindex $buf 1] + set buf [lrange $buf 2 end] + set buffer [lindex $buf 0] + + # Save remoteid for "comm remoteid". This will only be valid + # if retrieved before any additional events occur on this channel. + # N.B. we could have already lost the connection to remote, making + # this id be purely informational! + set comm($chan,remoteid) [set id $remoteid] + + # Save state for possible async result generation + AsyncPrepare $chan $fid $cmd $ser + + commDebug {puts stderr "<$chan> exec <$cmd,$ser,$buf>"} + + switch -- $cmd { + send - async - command {} + callback { + if {![info exists comm($chan,return,$ser)]} { + commDebug {puts stderr "<$chan> No one waiting for serial \"$ser\""} + return + } + + # Decompose reply command to assure it only uses "return" + # with no side effects. + + array set return {-code "" -errorinfo "" -errorcode ""} + set ret [lindex $buffer end] + set len [llength $buffer] + incr len -2 + foreach {sw val} [lrange $buffer 1 $len] { + if {![info exists return($sw)]} continue + set return($sw) $val + } + + catch {CommRunHook $chan callback} + + # this wakes up the sender + commDebug {puts stderr "<$chan> --<<wakeup $ser>>--"} + + # the return holds the callback command + # string map the optional %-subs + set args [list -id $id \ + -serial $ser \ + -chan $chan \ + -code $return(-code) \ + -errorcode $return(-errorcode) \ + -errorinfo $return(-errorinfo) \ + -result $ret \ + ] + set code [catch {uplevel \#0 $comm($chan,return,$ser) $args} err] + catch {unset comm($chan,return,$ser)} + + # remove pending serial + upvar 0 comm($chan,pending,$id) pending + if {[info exists pending]} { + set pos [lsearch -exact $pending [list $ser callback]] + if {$pos != -1} { + set pending [lreplace $pending $pos $pos] + } + } + if {$code} { + commBgerror $err + } + return + } + reply { + if {![info exists comm($chan,return,$ser)]} { + commDebug {puts stderr "<$chan> No one waiting for serial \"$ser\""} + return + } + + # Decompose reply command to assure it only uses "return" + # with no side effects. + + array set return {-code "" -errorinfo "" -errorcode ""} + set ret [lindex $buffer end] + set len [llength $buffer] + incr len -2 + foreach {sw val} [lrange $buffer 1 $len] { + if {![info exists return($sw)]} continue + set return($sw) $val + } + + catch {CommRunHook $chan reply} + + # this wakes up the sender + commDebug {puts stderr "<$chan> --<<wakeup $ser>>--"} + set comm($chan,result,$ser) $ret + set comm($chan,return,$ser) [array get return] + return + } + vers { + set ::comm::comm($chan,vers,$id) $ser + return + } + default { + commDebug {puts stderr "<$chan> unknown command; discard \"$cmd\""} + return + } + } + + # process eval hook now + set done 0 + set err 0 + if {[info exists comm($chan,hook,eval)]} { + set err [catch {CommRunHook $chan eval} ret] + commDebug {puts stderr "<$chan> eval hook res <$err,$ret>"} + switch $err { + 1 { + # error + set done 1 + } + 2 - 3 { + # return / break + set err 0 + set done 1 + } + } + } + + commDebug {puts stderr "<$chan> hook(eval) done=$done, err=$err"} + + # exec command + if {!$done} { + commDebug {puts stderr "<$chan> exec ($buffer)"} + + # Sadly, the uplevel needs to be in the catch to access the local + # variables buffer and ret. These cannot simply be global because + # commExec is reentrant (i.e., they could be linked to an allocated + # serial number). + + if {$comm($chan,interp) == {}} { + # Main interpreter + set thecmd [concat [list uplevel \#0] $buffer] + set err [catch $thecmd ret] + } else { + # Redirect execution into the configured slave + # interpreter. The exact command used depends on the + # capabilities of the interpreter. A best effort is made + # to execute the script in the global namespace. + set interp $comm($chan,interp) + + if {$comm($chan,interp,upl) == "ok"} { + set thecmd [concat [list uplevel \#0] $buffer] + set err [catch {interp eval $interp $thecmd} ret] + } elseif {$comm($chan,interp,aset) == "hidden"} { + set thecmd [linsert $buffer 0 interp invokehidden $interp uplevel \#0] + set err [catch $thecmd ret] + } else { + set thecmd [concat [list interp eval $interp] $buffer] + set err [catch $thecmd ret] + } + } + } + + # Check and handle possible async result generation. + if {[AsyncCheck]} return + + commSendReply $chan $fid $cmd $ser $err $ret + return +} + +# ::comm::commSendReply -- +# +# Internal command. Executed to construct and send the reply +# for a command. +# +# Arguments: +# fid channel handle of the socket we are replying to. +# cmd The type of request (send, command) we are replying to. +# ser Serial number of the request the reply is for. +# err result code to place into the reply. +# ret result value to place into the reply. +# +# Results: +# None. + +proc ::comm::commSendReply {chan fid cmd ser err ret} { + variable comm + + commDebug {puts stderr "<$chan> res <$err,$ret> /$cmd"} + + # The double list assures that the command is a single list when read. + if {[string equal send $cmd] || [string equal command $cmd]} { + # The catch here is just in case we lose the target. Consider: + # comm send $other comm send [comm self] exit + catch { + set return [list return -code $err] + # send error or result + if {$err == 1} { + global errorInfo errorCode + lappend return -errorinfo $errorInfo -errorcode $errorCode + } + lappend return $ret + if {[string equal send $cmd]} { + set reply reply + } else { + set reply callback + } + puts $fid [list [list $reply $ser $return]] + flush $fid + } + commDebug {puts stderr "<$chan> reply sent"} + } + + if {$err == 1} { + commBgerror $ret + } + commDebug {puts stderr "<$chan> exec complete"} + return +} + +proc ::comm::CommRunHook {chan event} { + variable comm + + # The documentation promises the hook scripts to have access to a + # number of internal variables. For a regular hook we simply + # execute it in the calling level to fulfill this. When the hook + # is redirected into an interpreter however we do a best-effort + # copying of the variable values into the interpreter. Best-effort + # because the 'set' command may not be available in the + # interpreter, not even hidden. + + if {![info exists comm($chan,hook,$event)]} return + set cmd $comm($chan,hook,$event) + set interp $comm($chan,interp) + commDebug {puts stderr "<$chan> hook($event) run <$cmd>"} + + if { + ($interp != {}) && + ([lsearch -exact $comm($chan,events) $event] >= 0) + } { + # Best-effort to copy the context into the interpreter for + # access by the hook script. + set vars { + addr buffer chan cmd fid host + id port reason remport ret var + } + + if {$comm($chan,interp,set) == "ok"} { + foreach v $vars { + upvar 1 $v V + if {![info exists V]} continue + interp eval $interp [list set $v $V] + } + } elseif {$comm($chan,interp,set) == "hidden"} { + foreach v $vars { + upvar 1 $v V + if {![info exists V]} continue + interp invokehidden $interp set $v $V + } + } + upvar 1 return AV + if {[info exists AV]} { + if {$comm($chan,interp,aset) == "ok"} { + interp eval $interp [list array set return [array get AV]] + } elseif {$comm($chan,interp,aset) == "hidden"} { + interp invokehidden $interp array set return [array get AV] + } + } + + commDebug {puts stderr "<$chan> /interp $interp"} + set code [catch {interp eval $interp $cmd} res] + } else { + commDebug {puts stderr "<$chan> /main"} + set code [catch {uplevel 1 $cmd} res] + } + + # Perform the return code propagation promised + # to the hook scripts. + switch -exact -- $code { + 0 {} + 1 { + return -errorinfo $::errorInfo -errorcode $::errorCode -code error $res + } + 3 {return} + 4 {} + default {return -code $code $res} + } + return +} + +# ### ### ### ######### ######### ######### +## Hooks to link async return and future processing into the regular +## system. + +# AsyncPrepare, AsyncCheck: Initialize state information for async +# return upon start of a remote invokation, and checking the state for +# async return. + +proc ::comm::AsyncPrepare {chan fid cmd ser} { + variable comm + set comm(current,async) 0 + set comm(current,state) [list $chan $fid $cmd $ser] + return +} + +proc ::comm::AsyncCheck {} { + # Check if the executed command notified us of an async return. If + # not we let the regular return processing handle the end of the + # script. Otherwise we stop the caller from proceeding, preventing + # a regular return. + + variable comm + if {!$comm(current,async)} {return 0} + return 1 +} + +# FutureDone: Action taken by an uncanceled future to deliver the +# generated result to the proper invoker. This also removes the future +# from the list of pending futures for the comm channel. + +proc comm::FutureDone {future chan fid cmd sid rcode rvalue} { + variable comm + commSendReply $chan $fid $cmd $sid $rcode $rvalue + + set pos [lsearch -exact $comm(future,fid,$fid) $future] + set comm(future,fid,$fid) [lreplace $comm(future,fid,$fid) $pos $pos] + return +} + +# ### ### ### ######### ######### ######### +## Hooks to save command state across nested eventloops a remotely +## invoked command may run before finally activating async result +## generation. + +# DANGER !! We have to refer to comm internals using fully-qualified +# names because the wrappers will execute in the global namespace +# after their installation. + +proc ::comm::Vwait {varname} { + variable ::comm::comm + + set hasstate [info exists comm(current,async)] + set hasremote 0 + if {$hasstate} { + set chan [lindex $comm(current,state) 0] + set async $comm(current,async) + set state $comm(current,state) + set hasremote [info exists comm($chan,remoteid)] + if {$hasremote} { + set remoteid $comm($chan,remoteid) + } + } + + set code [catch {uplevel 1 [list ::comm::VwaitOrig $varname]} res] + + if {$hasstate} { + set comm(current,async) $async + set comm(current,state) $state + } + if {$hasremote} { + set comm($chan,remoteid) $remoteid + } + + return -code $code $res +} + +proc ::comm::Update {args} { + variable ::comm::comm + + set hasstate [info exists comm(current,async)] + set hasremote 0 + if {$hasstate} { + set chan [lindex $comm(current,state) 0] + set async $comm(current,async) + set state $comm(current,state) + + set hasremote [info exists comm($chan,remoteid)] + if {$hasremote} { + set remoteid $comm($chan,remoteid) + } + } + + set code [catch {uplevel 1 [linsert $args 0 ::comm::UpdateOrig]} res] + + if {$hasstate} { + set comm(current,async) $async + set comm(current,state) $state + } + if {$hasremote} { + set comm($chan,remoteid) $remoteid + } + + return -code $code $res +} + +# Install the wrappers. + +proc ::comm::InitWrappers {} { + rename ::vwait ::comm::VwaitOrig + rename ::comm::Vwait ::vwait + + rename ::update ::comm::UpdateOrig + rename ::comm::Update ::update + + proc ::comm::InitWrappers {} {} + return +} + +# ### ### ### ######### ######### ######### +## API: Future objects. + +snit::type comm::future { + option -command -default {} + + constructor {chan fid cmd ser} { + set xfid $fid + set xcmd $cmd + set xser $ser + set xchan $chan + return + } + + destructor { + if {!$canceled} { + return -code error \ + "Illegal attempt to destroy unresolved future \"$self\"" + } + } + + method return {args} { + # Syntax: | 0 + # : -code x | 2 + # : -code x val | 3 + # : val | 4 + # Allowing multiple -code settings, last one is taken. + + set rcode 0 + set rvalue {} + + while {[lindex $args 0] == "-code"} { + set rcode [lindex $args 1] + set args [lrange $args 2 end] + } + if {[llength $args] > 1} { + return -code error "wrong\#args, expected \"?-code errcode? ?result?\"" + } + if {[llength $args] == 1} { + set rvalue [lindex $args 0] + } + + if {!$canceled} { + comm::FutureDone $self $xchan $xfid $xcmd $xser $rcode $rvalue + set canceled 1 + } + # assert: canceled == 1 + $self destroy + return + } + + variable xfid {} + variable xcmd {} + variable xser {} + variable xchan {} + variable canceled 0 + + # Internal method for use by comm channels. Marks the future as + # expired, no peer to return a result back to. + + method Cancel {} { + set canceled 1 + if {![llength $options(-command)]} {return} + uplevel #0 [linsert $options(-command) end $self] + return + } +} + +# ### ### ### ######### ######### ######### +## Setup +::comm::InitWrappers + +############################################################################### +# +# Finish creating "comm" using the default port for this interp. +# + +if {![info exists ::comm::comm(comm,port)]} { + if {[string equal macintosh $tcl_platform(platform)]} { + ::comm::comm new ::comm::comm -port 0 -local 0 -listen 1 + set ::comm::comm(localhost) \ + [lindex [fconfigure $::comm::comm(::comm::comm,socket) -sockname] 0] + ::comm::comm config -local 1 + } else { + ::comm::comm new ::comm::comm -port 0 -local 1 -listen 1 + } +} + +#eof +package provide comm 4.6.3.1 diff --git a/tcllib/modules/comm/comm.test b/tcllib/modules/comm/comm.test new file mode 100644 index 0000000..2289446 --- /dev/null +++ b/tcllib/modules/comm/comm.test @@ -0,0 +1,318 @@ +# -*- tcl -*- +# Tests for the comm module. +# +# Sourcing this file into Tcl runs the tests and generates output for errors. +# No output means no errors were found. +# +# Copyright (c) 2001 by ActiveState Tool Corp. +# All rights reserved. +# +# RCS: @(#) $Id: comm.test,v 1.14 2010/09/15 19:48:33 andreas_kupries Exp $ + +# ------------------------------------------------------------------------- + +source [file join \ + [file dirname [file dirname [file join [pwd] [info script]]]] \ + devtools testutilities.tcl] + +testsNeedTcl 8.3 ; # snit +testsNeedTcltest 1.0 + +tcltest::testConstraint hastls [expr {![catch {package require tls}]}] + +support { + # Using snit1 here, whatever the version of Tcl + use snit/snit.tcl snit +} +testing { + useLocal comm.tcl comm +} + +# ------------------------------------------------------------------------ +# First order of things is to spawn a separate tclsh into the background +# and have it execute comm too, with some general code to respond to our +# requests + +useLocalFile comm.slaveboot + +# ------------------------------------------------------------------------ + +test comm-1.0 {set remote variable} { + ::comm::comm send [slave] {set foo b} +} {b} + +test comm-1.1 {set remote variable, async} { + ::comm::comm send -async [slave] {set fox a} +} {} + +test comm-1.2 {get remote variables} { + ::comm::comm send [slave] {list $foo $fox} +} {b a} + +# ------------------------------------------------------------------------ + +set hack [interp create] + +test comm-2.0 {-interp configuration} { + ::comm::comm configure -interp $hack +} {} + +test comm-2.1 {-interp configuration} { + ::comm::comm configure -interp +} $hack + +test comm-2.2 {-interp configuration} { + res! + res+ [::comm::comm configure -interp $hack] [::comm::comm configure -interp] + res+ [::comm::comm configure -interp {}] [::comm::comm configure -interp] + res? +} [list [list {} $hack] {{} {}}] + +test comm-2.3 {-interp configuration} { + catch {::comm::comm configure -interp bad} msg + set msg +} {Non-interpreter to configuration option: -interp} + +test comm-2.4 {-interp configuration, destruction} { + res! + res+ [interp exists $hack] + res+ [info commands FOO] + comm::comm new FOO -interp $hack + FOO destroy + res+ [interp exists $hack] + res+ [info commands FOO] + res? +} {1 {{}} 0 {{}}} + +set hack [interp create] +set beta [interp create] + +test comm-2.5 {-interp configuration, destruction} { + res! + res+ [interp exists $hack] + res+ [interp exists $beta] + res+ [info commands FOO] + comm::comm new FOO -interp $hack + FOO configure -interp $beta + FOO destroy + res+ [interp exists $hack] + res+ [interp exists $beta] + res+ [info commands FOO] + res? +} {1 1 {{}} 1 0 {{}}} + +test comm-2.6 {-interp use for received scripts} { + set FOO [::comm::comm send [slave] { + set hack [interp create] + interp eval $hack {set fox 0} + comm::comm new FOO -interp $hack -listen 1 + FOO self + }] ; # {} + + comm::comm send $FOO {set fox 1} + set res [comm::comm send [slave] { + interp eval $hack {set fox} + }] ; # {} + comm::comm send [slave] {FOO destroy} + set res +} 1 + +test comm-2.7 {-interp use for received scripts} { + set FOO [::comm::comm send [slave] { + set hack [interp create] + interp eval $hack {set fox 0} + comm::comm new FOO -interp $hack -listen 1 + FOO self + }] ; # {} + + comm::comm send $FOO set fox 2 + set res [comm::comm send [slave] { + interp eval $hack {set fox} + }] ; # {} + comm::comm send [slave] {FOO destroy} + set res +} 2 + +# ------------------------------------------------------------------------ + +test comm-3.0 {-events configuration} { + ::comm::comm configure -events eval +} {} + +test comm-3.1 {-events configuration} { + ::comm::comm configure -events +} eval + +test comm-3.2 {-events configuration} { + res! + res+ [::comm::comm configure -events eval] [::comm::comm configure -events] + res+ [::comm::comm configure -events {}] [::comm::comm configure -events] + res? +} {{{} eval} {{} {}}} + +test comm-3.3 {-events configuration} { + catch {::comm::comm configure -events bad} msg + set msg +} {Non-event to configuration option: -events} + + +test comm-3.4 {-interp use for -events scripts, eval} { + set FOO [::comm::comm send [slave] { + set hack [interp create] + interp eval $hack {set fox 0 ; set wolf 0} + comm::comm new FOO -interp $hack -listen 1 -events eval + FOO hook eval {set wolf 2} + FOO self + }] ; # {} + + comm::comm send $FOO {set fox 1} + set res [comm::comm send [slave] { + interp eval $hack {set wolf} + }] ; # {} + comm::comm send [slave] {FOO destroy} + set res +} 2 + +# ------------------------------------------------------------------------ + +test comm-4.0 {async generation of result on remote side} { + ::comm::comm send [slave] { + proc future {} { + set f [comm::comm return_async] + after 3000 [list $f return "delayed $f"] + return ignored + } + } + ::comm::comm send [slave] {future} +} {delayed ::comm::future1} + +test comm-4.1 {async reception of a result via callback} { + set res {} + proc foo {args} { + array set tmp $args + unset tmp(-id) + unset tmp(-serial) + global res ; lappend res [dictsort [array get tmp]] + } + ::comm::comm send -command foo [slave] {list $foo $fox} + vwait res + rename foo {} + set res +} {{-chan ::comm::comm -code 0 -errorcode {} -errorinfo {} -result {b a}}} + +test comm-4.2 {async generation/reception of results in parallel} { + + # Setup long running operations with async result generation. + ::comm::comm send [slave] { + proc future {n x} { + set f [comm::comm return_async] + after $n [list $f return "delayed $x"] + return ignored + } + } + + # Setup async receiver callback. + proc receive {args} { + global res tick tock + array set tmp $args + unset tmp(-id) + unset tmp(-serial) + unset tmp(-chan) + unset tmp(-code) + unset tmp(-errorcode) + unset tmp(-errorinfo) + lappend res [dictsort [array get tmp]] + incr tock -1 + if {!$tock} {set tick .} + return + } + + # Execute two requests, the second of which is returns before the first. + # The main point is that the server does process it due to first doing + # an async return. + + set tick . + set tock 2 + set res {} + + ::comm::comm send -command receive [slave] {future 5000 A} + ::comm::comm send -command receive [slave] {future 2500 B} + vwait tick + rename receive {} + set res + # B returned before A, A was sent before B +} {{-result {delayed B}} {-result {delayed A}}} + + +test comm-4.3 {bug 2972571, handling of \\ by Word0} { + ::comm::comm send [slave] { + proc foo {args} { + return nothing + } + } + ::comm::comm send [slave] {foo \\} +} {nothing} + + +# ------------------------------------------------------------------------ + +test comm-5.0 {-port already in use} { + # First start a server on port 12345 + set port 12345 + catch {set shdl [socket -server foo $port]} + catch {::comm::comm new bar -port $port -listen 1 -local 0} msg + catch {close $shdl} + unset -nocomplain shdl port + set msg +} {couldn't open socket: address already in use} + +# ------------------------------------------------------------------------ + +test comm-6.0 {secured communication via tls package} hastls { + # Setup secured channel in main process. + tls::init \ + -keyfile [tcllibPath devtools/receiver.key] \ + -certfile [tcllibPath devtools/receiver.crt] \ + -cafile [tcllibPath devtools/ca.crt] \ + -ssl2 1 \ + -ssl3 1 \ + -tls1 0 \ + -require 1 + comm::comm new BAR -socketcmd tls::socket -listen 1 + + # Setup secured channel in slave process + ::comm::comm send [slave] { + package require tls + set fox dog + } + ::comm::comm send [slave] \ + [list \ + tls::init \ + -keyfile [tcllibPath devtools/transmitter.key] \ + -certfile [tcllibPath devtools/transmitter.crt] \ + -cafile [tcllibPath devtools/ca.crt] \ + -ssl2 1 \ + -ssl3 1 \ + -tls1 0 \ + -require 1] + set FOO [::comm::comm send [slave] { + comm::comm new FOO -socketcmd tls::socket -listen 1 + FOO self + }] ; # {} + + # Run command interaction over the secured channel + set res [BAR send $FOO {set fox}] + + # Cleanup, remove secured endpoints + comm::comm send [slave] {FOO destroy} + BAR destroy + + # Return result of the secured command + set res +} dog + +# ------------------------------------------------------------------------ + +slavestop +testsuiteCleanup +return diff --git a/tcllib/modules/comm/comm_wire.man b/tcllib/modules/comm/comm_wire.man new file mode 100644 index 0000000..e0419f1 --- /dev/null +++ b/tcllib/modules/comm/comm_wire.man @@ -0,0 +1,284 @@ +[manpage_begin comm_wire n 3] +[see_also comm] +[keywords comm] +[keywords communication] +[keywords ipc] +[keywords message] +[keywords {remote communication}] +[keywords {remote execution}] +[keywords rpc] +[keywords socket] +[copyright {2005 Docs. Andreas Kupries <andreas_kupries@users.sourceforge.net>}] +[moddesc {Remote communication}] +[titledesc {The comm wire protocol}] +[category {Programming tools}] +[require comm] +[description] + +[para] + +The [package comm] command provides an inter-interpreter remote +execution facility much like Tk's [cmd send(n)], except that it uses +sockets rather than the X server for the communication path. As a +result, [package comm] works with multiple interpreters, works on +Windows and Macintosh systems, and provides control over the remote +execution path. + +[para] + +This document contains a specification of the various versions of the +wire protocol used by comm internally for the communication between +its endpoints. It has no relevance to users of [package comm], only to +developers who wish to modify the package, write a compatible facility +in a different language, or some other facility based on the same +protocol. + +[comment { + An example of some other facility could be a router layer which is + able to get messages for many different endpoints and then routes + them to the correct one. Why is this interesting ? Because it + allows mesh-routing, i.e. an application fires a command to some + other endpoint without having to worry if there is direct + connection to this endpoint or not. A secure tunnel then neatly + fits into this. Its endpoints are routing agents which take + arbitrarily commands, route them through the tunnel and then + dispatch them to the correct endpoint on the other side. + + Note: A special case would be to have such a router facility built + into the core comm package, making each endpoint a router as + well. Like with the ability to listen to for non-local connection + this is something the user should be able to disable. +}] + +[comment { + Motivation for documenting the protocol + --------------------------------------- + + While the comm package allows the transport and execution of arbitrary + Tcl scripts a particular application can use the hooks to restrict the + scripts to single commands, and the legal commands to a specific set + as well. + + If this is done (*) comm becomes more of a transport layer for a + regular RPC, and the data transported over the wire is less of a Tcl + script and more of a declaration of which remote procedure is wanted, + plus arguments. + + At this point it begins to make sense to have implementations in other + scripting languages. Because then it becomes irrelevant in what + language the server is implemented. The comm protocol becomes a + portable RPC protocol, which can also be used for transport Tcl + scripts when both sides are Tcl and allowing this. + + (*) And IMHO it should be done 90% of the time, just to get proper + security. Note that just using a safe interp is not quite enough, as + it still allows arbitrary scripts. The interp has to contains aliases + for the wanted commands, and only them for us to get a large security + wall. +}] + +[section {Wire Protocol Version 3}] +[subsection {Basic Layer}] + +The basic encoding for [emph all] data is UTF-8. Because of this +binary data, including the NULL character, can be sent over the wire +as is, without the need for armoring it. + +[subsection {Basic Message Layer}] + +On top of the [sectref {Basic Layer}] we have a + +[term {message oriented}] exchange of data. + +The totality of all characters written to the channel is a Tcl list, +with each element a separate [term message], each itself a list. The +messages in the overall list are separated by EOL. Note that EOL +characters can occur within the list as well. They can be +distinguished from the message-separating EOL by the fact that the +data from the beginning up to their location is not a valid Tcl list. + +[para] + +EOL is signaled through the linefeed character, i.e [const LF], or, +hex [const 0x0a]. This is following the unix convention for +line-endings. + +[para] + +As a list each message is composed of [term words]. Their meaning +depends on when the message was sent in the overall exchange. This is +described in the upcoming sections. + +[subsection {Negotiation Messages - Initial Handshake} ih] + +The command protocol is defined like this: + +[list_begin itemized] +[item] + +The first message send by a client to a server, when opening the +connection, contains two words. The first word is a list as well, and +contains the versions of the wire protocol the client is willing to +accept, with the most preferred version first. The second word is the +TCP port the client is listening on for connections to itself. The +value [const 0] is used here to signal that the client will not listen +for connections, i.e. that it is purely for sending commands, and not +receiving them. + +[item] + +The first message sent by the server to the client, in response to the +message above contains only one word. This word is a list, containing +the string [const vers] as its first element, and the version of the +wire protocol the server has selected from the offered versions as the +second. + +[comment { + NOTE / DANGER + + The terminating EOL for this first response will be the socket + specific default EOL (Windows/Internet convention: "\r\n"). + However all future messages use Unix convention, i.e. "\n", + for their EOLs, embedded or terminating. + + Reason: The internal command commNewComm does the common + processing for new connections, doing the + + fconfigure -translation lf + + However the handshake response containing the accepted + version is sent before commNewComm is called (in + commIncoming). + + NOTE 2: + + This inconsistency has been fixed locally already, but + not been committed yet. +}] +[list_end] + +[subsection {Script/Command Messages}] + +All messages coming after the [sectref ih {initial handshake}] +consist of three words. These are an instruction, a transaction id, +and the payload. The valid instructions are shown below. The +transaction ids are used by the client to match any incoming replies +to the command messages it sent. This means that a server has to copy +the transaction id from a command message to the reply it sends for +that message. + +[list_begin definitions] + +[def [const send]] +[def [const async]] +[def [const command]] + +The payload is the Tcl script to execute on the server. It is actually +a list containing the script fragments. These fragment are + +[cmd concat]enated together by the server to form the full script to +execute on the server side. + +This emulates the Tcl "eval" semantics. + +In most cases it is best to have only one word in the list, a list +containing the exact command. + +[para] +Examples: +[para] +[example { + (a) {send 1 {{array get tcl_platform}}} + (b) {send 1 {array get tcl_platform}} + (c) {send 1 {array {get tcl_platform}}} + + are all valid representations of the same command. They are + generated via + + (a') send {array get tcl_platform} + (b') send array get tcl_platform + (c') send array {get tcl_platform} + + respectively +}] +[para] + +Note that (a), generated by (a'), is the usual form, if only single +commands are sent by the client. + +For example constructed using [cmd list], if the command contains +variable arguments. Like + +[para] +[example { + send [list array get $the_variable] +}] +[para] + +These three instructions all invoke the script on the server +side. Their difference is in the treatment of result values, and thus +determines if a reply is expected. + +[list_begin definitions] +[def [const send]] + +A reply is expected. The sender is waiting for the result. + +[def [const async]] + +No reply is expected, the sender has no interest in the result and is +not waiting for any. + +[def [const command]] + +A reply is expected, but the sender is not waiting for it. It has +arranged to get a process-internal notification when the result +arrives. + +[list_end] + +[def [const reply]] + +Like the previous three command, however the tcl script in the payload +is highly restricted. + +It has to be a syntactically valid Tcl [cmd return] command. This +contains result code, value, error code, and error info. + +[para] +Examples: +[para] +[example { + {reply 1 {return -code 0 {}}} + {reply 1 {return -code 0 {osVersion 2.4.21-99-default byteOrder littleEndian machine i686 platform unix os Linux user andreask wordSize 4}}} +}] + +[list_end] + +[comment { + Socket Miscellanea + ------------------ + + It is possible to have two sockets between a client and a + server. This happens if both sides connected to each other at + the same time. + + Current protocol versions + ------------------------- + + V2 + + V3 This is preferred version and uses UTF 8 encoding. + + This is actually the only version which will work IIU + the code right. Because the server part of comm will + send the version reply if and only if version 3 was + negotiated. + + IOW if v2 is used the client will not see a version + reply during the negotiation handshake. +}] + +[vset CATEGORY comm] +[include ../doctools2base/include/feedback.inc] +[manpage_end] diff --git a/tcllib/modules/comm/pkgIndex.tcl b/tcllib/modules/comm/pkgIndex.tcl new file mode 100644 index 0000000..b0372e1 --- /dev/null +++ b/tcllib/modules/comm/pkgIndex.tcl @@ -0,0 +1,2 @@ +if {![package vsatisfies [package provide Tcl] 8.3]} {return} +package ifneeded comm 4.6.3.1 [list source [file join $dir comm.tcl]] |