####################################################################### ## proc send_expect ## ## Copyright (c) David L. Fisher, 1999 ## ## This software is copyrighted David L. Fisher. The following ## terms apply to all files associated with the software unless ## explicitly disclaimed in individual files. ## ## The author hereby grants 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. Modifications to this ## software may be copyrighted by their authors and need not follow ## the licensing terms described here, provided that the new terms are ## clearly indicated on the first page of each file where they apply. ## ## IN NO EVENT SHALL THE AUTHOR OR DISTRIBUTORS BE LIABLE TO ANY PARTY ## FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ## ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY ## DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE ## POSSIBILITY OF SUCH DAMAGE. ## ## THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, ## INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF ## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND ## NON-INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, ## AND THE AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE ## MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. ## ## GOVERNMENT USE: If you are acquiring this software on behalf of the ## U.S. government, the Government shall have only "Restricted Rights" ## in the software and related documentation as defined in the Federal ## Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you ## are acquiring the software on behalf of the Department of Defense, ## the software shall be classified as "Commercial Computer Software" ## and the Government shall have only "Restricted Rights" as defined ## in Clause 252.227-7013 (c) (1) of DFARs. Notwithstanding the ## foregoing, the authors grant the U.S. Government and others acting ## in its behalf permission to use and distribute the software in ## accordance with the terms specified in this license. ## ####################################################################### ####################################################################### ## NAME ## send_expect ## ## DESCRIPTION ## Send a string to a remote system and look for the echo. ## ## INPUTS ## id - spawn id of the remote process ## command - command string to send (default is blank) ## flags - currently unused ## ## OUTPUTS ## On success, returns 0 ## On failure, returns 1 and saves the expect internal ## diagnostics that were generated on the failed transmission ## attempt ## ## NOTES ## ## 1: This procedure uses four different transmission modes in an ## attempt to match the echo string with the send string. Only ## after all have been exhausted will an error be returned. ## The modes are: ## 1 - send fast - send the entire string without management ## 2 - send with send_slow - send the entire string with a ## specified send_slow list ## 3 - send blocks - send small blocks of the string and ## generate regular expressions on the fly to match garbage ## characters in the echo (this is useful for noisy routers ## and those which don't allow convenient control of their ## tty settings) ## 4 - send characer loop - send one character at a time, and ## wait for the echo before sending the nexxt character. ## This mode is basically guaranteed to work as long as ## there's a valid connection, but it's the slowest mode. ## If a mode fails to match the echo, a GNU standard control-U is ## sent to kill the current command line and give the harness a ## clean slate for trying the next transmission mode ## ## 2: DIAGNOSTICS FILE - this procedure manages the creation and ## association of the diagnostics file(s) - a small file is ## created for each send attempt, and if there is a failure, the ## file is renamed and kept. If there is no error, it is deleted ## and recreated for the next attempt. This is to keep the size ## of the diagnostics file small, and to keep its content ## directly associated with a send failure. Since internals are ## always logged, there is the same performance hit as in ## creating the huge single file. The benefit of this approach is ## in debugging. ## ## 3: COMMAND TERMINATION - this procedure strips off the very last ## character in the send string (whether it's a carriage return ## or text character) and won't send it until the string has ## matched up to that point. This is becuase on some systems, ## other characters may preclude the need for a carriage return ## and execute a command upon reception of the last character - ## we don't want this unless we've matched the entire send string ## and know that the command being executed is the one we sent. ## ## 4: this procedure now keeps the diags files for any command that ## must be retransmitted after a kill character due to a mode ## timeout the counter for the diag files here is ## sendGlobals(killed) and the array element conatining the ## filenames is sendGlobals(kill$sendGlobals(killed)) ## ####################################################################### proc send_expect { id command { flags "" } } { global sendGlobals; global send_slow; global ch; global sessions; global files; global globals; ## this is more than enough time for the matching we're doing here set timeout 2; ##----------------------------------------------------------------- ## if we're called just to send a carriage return, send it and return - ## do not update the history for this send ##----------------------------------------------------------------- if { "$command" == "\r" } { if { [send_only $id "\r"] } { puts stderr "Unable to send carriage return"; return $sendGlobals(error); } else { return $sendGlobals(success); } } ##----------------------------------------------------------------- ## create the command portion of the diagnostics file name - just want ## the first word in the command string ##----------------------------------------------------------------- set cmdname [lindex $command 0]; set saveFile "send.kill.[expr $sendGlobals(killed)+1].$cmdname.diags"; ##----------------------------------------------------------------- ## initialize settings for this attempt ##----------------------------------------------------------------- set sendGlobals(command) "$command"; set sendGlobals(stringSent) ""; set sendGlobals(ssFlag) ""; set sendGlobals(thisMode) 0; incr sendGlobals(numSent); ##----------------------------------------------------------------- ## make sure we have a valid useMode element ##----------------------------------------------------------------- if { $sendGlobals(useMode) < 1 || $sendGlobals(useMode) > 4 } { puts stderr "Invalid sendGlobals(useMode) - defaulting to 1"; set sendGlobals(useMode) 1; } ##----------------------------------------------------------------- ## strip off the last character and use it as the terminator ##----------------------------------------------------------------- set sendGlobals(terminator) [string range $command end end]; set len [expr [string length $command] - 1]; set command [string range $command 0 [expr $len - 1]]; ##------------------------------------------------------------------ ## handle diagnostics file and exp_internal ## every time we open a file use the same name - this is to ## overwrite the last file since the send was successful ## if its contents are associated with an error, we rename it ## later on when we detect the error ##------------------------------------------------------------------ if { $sendGlobals(logDiags) } { set exp_internal_old [exp_internal -info]; exp_internal -f $sendGlobals(diagFile) $exp_internal_old; } #################################################################### ################ time to send the string ########################### #################################################################### ##------------------------------------------------------------------ ## MODE 1 - send the entire string without further management; ## send_slow is disabled for this mode (trying for fastest transmission) ##------------------------------------------------------------------ if { $sendGlobals(useMode) == 1 } { if { $sendGlobals(Mode1FailMax) > 0 && $sendGlobals(Mode1FailMax) > $sendGlobals(Mode1Failures) } { set sendGlobals(thisMode) 1; if { [send_only $id "$command"] } { puts stderr "Unable to send command \"$command\""; return $sendGlobals(error); } expect { -i $id -re "$command" { set sendGlobals(stringSent) "$command"; if { "$sendGlobals(terminator)" != "" } { if { [send_only $id "$sendGlobals(terminator)"] } { puts stderr "Unable to terminate command"; return $sendGlobals(error); } } ## this only works with UNIX currently ## or at least, not with the cygwin NT expect port if { $sendGlobals(logDiags) } { exp_internal $exp_internal_old; catch {exec rm $sendGlobals(diagFile)}; #catch {file delete -force $sendGlobals(diagFile)}; } return $sendGlobals(success); } timeout { puts stderr "Mode 1 failed to match entire string"; } } ##---------------------------------------------------------- ## kill the command line and clear the buffer ##---------------------------------------------------------- if { [send_only $id $sendGlobals(kill) 0] } { puts stderr "Unable to kill command"; return $sendGlobals(error); } expect -i $id -re "\[^\r\n\]*"; incr sendGlobals(kills); incr sendGlobals(Mode1Failures); puts stderr "Mode 1 failed"; } # if we haven't failed out of this mode } # if we're supposed to use this mode ##------------------------------------------------------------------ ## MODE 2 - use the send_slow variable ## NOTE: developers may want to experiment with adding some ## iterations and modifying the send_slow list on the fly ##------------------------------------------------------------------ set sendGlobals(ssFlag) "-s"; if { $sendGlobals(useMode) <= 2 } { if { $sendGlobals(Mode2FailMax) > 0 && $sendGlobals(Mode2FailMax) > $sendGlobals(Mode2Failures) } { puts stderr "Entering send mode 2"; set sendGlobals(thisMode) 2; ## manage the send_slow list - add a delay between each char set send_slow "$sendGlobals(interval) $sendGlobals(delay)"; if { [send_only $id "$command"] } { puts stderr "Unable to send command \"$command\""; return $sendGlobals(error); } expect { -i $id -re "$command" { set sendGlobals(stringSent) "$command"; if { "$sendGlobals(terminator)" != "" } { if { [send_only $id "$sendGlobals(terminator)"] } { puts stderr "Unable to terminate command"; return $sendGlobals(error); } } ## keep the diags file to debug the interface if { $sendGlobals(logDiags) } { exp_internal "$exp_internal_old"; catch {exec mv $sendGlobals(diagFile) $saveFile} #catch {file rename -force $sendGlobals(diagFile) $saveFile}; set sendGlobals(kill$sendGlobals(killed)) $saveFile; exp_internal "$exp_internal_old"; puts stderr "Command killed in mode 1; see $saveFile"; } return $sendGlobals(success); } timeout { puts stderr "Mode 2 failed to match entire string"; } } ##---------------------------------------------------------- ## kill the command line and clear the buffer ##---------------------------------------------------------- if { [send_only $id $sendGlobals(kill) 0] } { puts stderr "Unable to kill command"; return $sendGlobals(error); } expect -i $id -re "\[^\r\n\]*"; incr sendGlobals(kills); incr sendGlobals(Mode2Failures); puts stderr "Mode 2 failed"; } } ##------------------------------------------------------------------ ## MODE 3 - send in small blocks and tolerate garbage characters ## in the echo - the size of the block is hardcoded at 8 ## because of the static expect expression that follows ## and the regular expression generation ##------------------------------------------------------------------ set sendGlobals(stringSent) ""; set sendLength [string length $command]; set success 1; set timeout 1; set blocksize 8; set sendGlobals(ssFlag) "-s"; if { $sendGlobals(useMode) <= 3 } { if { $sendGlobals(Mode3FailMax) > 0 && $sendGlobals(Mode3FailMax) > $sendGlobals(Mode3Failures) } { puts stderr "Entering send mode 3"; set sendGlobals(thisMode) 3; set sendBlock ""; set sendLength [string length $command]; set success 1; ##-------------------------------------------------------------- ## loop through blocks of blocksize characters - create ## expressions to match the send block with control ## sequences anywhere in the block ##-------------------------------------------------------------- for {set sendGlobals(index) 0} \ {$sendGlobals(index) < $sendLength} \ {incr sendGlobals(index) $blocksize} { ## generate the regular expressions for the expect clause set sendBlock [string range $command $sendGlobals(index) \ [expr $sendGlobals(index) + $blocksize - 1]]; for { set k 0 } { $k < $blocksize } { incr k } { set s${k}_left [string range $sendBlock 0 $k]; set s${k}_right [string range $sendBlock [expr $k+1] end]; } ## send the block if { [send_only $id "$sendBlock"] } { puts stderr "Unable to send block \"$sendBlock\""; return $sendGlobals(error); } ##--------------------------------------------------------- ## now look for the echo of the block sent ##---------------------------------------------------------- expect { -i $id -re "$sendBlock" { } -re "${s0_left}\[^\r\n\]*${s0_right}" { } -re "${s1_left}\[^\r\n\]*${s1_right}" { } -re "${s2_left}\[^\r\n\]*${s2_right}" { } -re "${s3_left}\[^\r\n\]*${s3_right}" { } -re "${s4_left}\[^\r\n\]*${s4_right}" { } -re "${s5_left}\[^\r\n\]*${s5_right}" { } -re "${s6_left}\[^\r\n\]*${s6_right}" { } -re "${s7_left}\[^\r\n\]*${s7_right}" { } timeout { set success 0; puts stderr "Mode 3 failed to match block:"; puts stderr "$sendGlobals(stringSent)\"$sendBlock\""; break; } } append sendGlobals(stringSent) "$sendBlock"; } # block loop ##-------------------------------------------------------------- ## if this attempt was successful, send the terminating character ##-------------------------------------------------------------- if { $success } { ## only call send_only if the terminator is nonblank if { "$sendGlobals(terminator)" != "" } { if { [send_only $id "$sendGlobals(terminator)"] } { puts stderr "Unable to terminate command"; return $sendGlobals(error); } } ## here we know the command had to be killed in modes 1 & 2 if { $sendGlobals(logDiags) } { exp_internal "$exp_internal_old"; set sendGlobals(kill$sendGlobals(killed)) $saveFile; catch {exec mv $sendGlobals(diagFile) $saveFile} #catch {file rename $sendGlobals(diagFile) $saveFile}; puts stderr "Command killed in modes 1 & 2; see $saveFile"; } return $sendGlobals(success); } ##---------------------------------------------------------- ## kill the command line and clear the buffer ##---------------------------------------------------------- if { [send_only $id $sendGlobals(kill) 0] } { puts stderr "Unable to kill command"; return $sendGlobals(error); } expect -i $id -re "\[^\r\n\]*"; incr sendGlobals(kills); incr sendGlobals(Mode3Failures); puts stderr "Mode 3 failed"; } # if failmax not reached } # if use this mode ##----------------------------------------------------------------- ## MODE 4 - index through the string one character at a time ## if this mode fails, it's pretty much all over ## NOTE: there is no failmax check for this mode - if mode ## rejection is being used (in other words, enough failures causes ## the mode to be skipped) this mode cannot be skipped - it's the ## last hope of a successful transmission ## send_slow is disabled for this mode (unnecessary) ##------------------------------------------------------------------ set sendGlobals(stringSent) ""; set sendLength [string length $command]; set success 1; set timeout 1; set sendGlobals(ssFlag) ""; puts stderr "Entering send mode 4"; set sendGlobals(thisMode) 4; if { $sendGlobals(useMode) <= 4 } { for {set sendGlobals(index) 0} \ {$sendGlobals(index) < $sendLength} \ {incr sendGlobals(index)} { set sendChar [string range $command \ $sendGlobals(index) $sendGlobals(index)]; if { [send_only $id $sendChar] } { puts stderr "Unable to send character \"$sendChar\""; return $sendGlobals(error); } ##---------------------------------------------------------- ## need to add the backslash for shell-significant and ## expression-significant characters or this expression may ## match when we don't want it to ##---------------------------------------------------------- set expChar "\\$sendChar"; expect { -i $id -re "$expChar" { append sendGlobals(stringSent) "$sendChar"; continue; } timeout { ##################################################### ## FAILURE!!! ## ## this is where we call it a day - can't even ## ## verify a single character ## ##################################################### puts stderr "Mode 4 failed to match single character"; puts stderr "Unable to send \"$command\""; puts stderr "Successfully sent: \"$sendGlobals(stringSent)\""; if { [send_only $id $sendGlobals(kill) 0] } { puts stderr "Unable to kill command"; return $sendGlobals(error); } expect -i $id -re "\[^\r\n\]*"; incr sendGlobals(kills); incr sendGlobals(errors); ##--------------------------------------------------- ## use the first word in the command string as the ## command name for the diags file - then rename the ## file so its doesn't get overwritten ##--------------------------------------------------- set cmd [lindex $command 0]; if { $sendGlobals(logDiags) } { set sendGlobals($sendGlobals(errors),file) $saveFile; set sendGlobals(kill$sendGlobals(killed)) $saveFile; catch {exec mv $sendGlobals(diagFile) $saveFile} #catch {file rename -force $sendGlobals(diagFile) $saveFile} exp_internal "$exp_internal_old"; puts stderr "TRANSMISSION FAILURE:\n[getMsg]"; } puts stderr "Interface timeout on session $id"; return $sendGlobals(error); } } # expect clause } # char loop ## once we get here, send the carriage-return if { "$sendGlobals(terminator)" != "" } { if { [send_only $id "$sendGlobals(terminator)"] } { puts stderr "Unable to terminate command"; return $sendGlobals(error); } } ## here we know the command had to be killed in modes 1,2 & 3 if { $sendGlobals(logDiags) } { exp_internal "$exp_internal_old"; set sendGlobals(kill$sendGlobals(killed)) $saveFile; catch {exec mv $sendGlobals(diagFile) $saveFile} #catch {file rename -force $sendGlobals(diagFile) $saveFile} puts stderr "Command killed in modes 1, 2 & 3; see $saveFile"; } return $sendGlobals(success); } # if use this mode ## if we get here, something went wrong puts stderr "send_expect: unable to complete send"; return $sendGlobals(error); }